simplestarの技術ブログ

目的を書いて、思想と試行、結果と考察、そして具体的な手段を記録します。

Unity:ECSとJobSystemsでFractal Brownian Motion(fBm)ノイズを生成

記事制作は途中ですが、近いうちに完成させます。

■前提知識

ECS は 2018年8月に自分が書いた記事を読み直して、再度頭に叩き込みます。(うむ、全然わからないな、自分の記事なのに)
Unity:CEDEC2018のECSの発表から学ぶ - simplestarの技術ブログ

2018年9月に詳しい解説記事が出来てました!(こっちの方が分かりやすそう、全部読みます!)
qiita.com

ComponentSystem は必ず world に属するって?知らなかったぞ

MonoBehaviour からの情報の引き渡しの書式を教わりましたよ。
ComponentSystem のコンストラクタで readonly のメンバに格納して利用するみたいです。

登場人物(クラスや構造体)
・マネージャー(MonoBehavior から world を作成して、ComponentSystem たちを管理します)
それが world に System を渡すときに CreateManager というものを行うようなのです。(ようなのですって…)
つまり、ComponentSystem ごとにマネージャーがいる、、素直に world に System を作成するというメンタルモデルを持つことにしました。

・ComponentSystem
例のインジェクションは?と思ったけど、OnCreateManager で ComponentGroup を作成しているみたい

g = GetComponentGroup(ComponentType.ReadOnly<Count>())

・ComponentData
上のコードでいうところの Count がそう、とにかくこの構造体がないと Entity の型を定義できない

最初に知っておきたかったメンタルモデル
ComponentSystem は Manager によって、自身の ComponentGroup を持つ Entity が world に存在しているときに反応する
反応の仕方は今のところ OnUpdate でフレームに一度だけ呼ばれる、Entity の数は関係なく、一つでも ComponentGroup があれば System が反応する

…ふふふ、全然理解できない。
ということで、先にはじめての ECS の記事を書きます。

Job Systems は
Unity:job-system-cookbookからC#JobSystemの実例を確認 - simplestarの技術ブログ

パーリンノイズの作り方
nn-hokuson.hatenablog.com

■構想

■実践

まずは基礎関数だけで作った疑似ランダムノイズ
f:id:simplestar_tech:20180925084903j:plain

作成コードはこちら

public class RandomInstanticate : MonoBehaviour
{
    [SerializeField]
    private float scale = 1;

    [SerializeField]
    private GameObject cubePrefab;

    const int SIDE = 20;


    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.A))
        {
            for (int x = 0; x < SIDE; x++)
            {
                for (int z = 0; z < SIDE; z++)
                {
                    float rand = Random(new Vector2(x / (float)SIDE, z / (float)SIDE));
                    Instantiate(cubePrefab, new Vector3(x, rand, z), Quaternion.identity, transform);
                }
            }
            
        }
    }

    static private float Random(Vector2 p)
    {
        float f = Mathf.Sin(Vector2.Dot(p, new Vector2(12.9898f, 78.233f)) * 43758.5453f);
        return f - Mathf.Floor(f);
    }
}

ブロックノイズはこんな感じ
f:id:simplestar_tech:20180925085701j:plain

実装変更箇所は次の行

float rand = Random(new Vector2(Mathf.Floor(4 * x / (float)SIDE), Mathf.Floor(4 * z / (float)SIDE)));

バリューノイズはこの通り
f:id:simplestar_tech:20180926093037j:plain

編集箇所はこちら

            for (int x = 0; x < SIDE; x++)
            {
                for (int z = 0; z < SIDE; z++)
                {
                    float vx = x / (float)SIDE * 8;
                    float vz = z / (float)SIDE * 8;

                    float fl_x = Mathf.Floor(vx);
                    float fl_z = Mathf.Floor(vz);
                    float fr_x = vx - fl_x;
                    float fr_z = vz - fl_z;

                    float v00 = Random(new Vector2(fl_x + 0, fl_z + 0));
                    float v10 = Random(new Vector2(fl_x + 1, fl_z + 0));
                    float v01 = Random(new Vector2(fl_x + 0, fl_z + 1));
                    float v11 = Random(new Vector2(fl_x + 1, fl_z + 1));

                    float ux = fr_x * fr_x * (3.0f - 2.0f * fr_x);
                    float uz = fr_z * fr_z * (3.0f - 2.0f * fr_z);

                    float v0010 = (1.0f - ux) * v00 + ux * v10;
                    float v0111 = (1.0f - ux) * v01 + ux * v11;

                    float rand = (1.0f - uz) * v0010 + uz * v0111;

                    Instantiate(cubePrefab, new Vector3(x, rand, z), Quaternion.identity, transform);
                }
            }

パーリンノイズの作成

f:id:simplestar_tech:20180926225000j:plain

編集箇所はこちら
乱数生成が 2次元になり、補間処理にも内積計算を利用しています

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.A))
        {
            for (int x = 0; x < SIDE; x++)
            {
                for (int z = 0; z < SIDE; z++)
                {
                    float vx = x / (float)SIDE * 4;
                    float vz = z / (float)SIDE * 4;

                    float fl_x = Mathf.Floor(vx);
                    float fl_z = Mathf.Floor(vz);

                    float fr_x = vx - fl_x;
                    float fr_z = vz - fl_z;

                    float ux = fr_x * fr_x * (3.0f - 2.0f * fr_x);
                    float uz = fr_z * fr_z * (3.0f - 2.0f * fr_z);

                    Vector2 v00 = Random2(new Vector2(fl_x + 0, fl_z + 0));
                    Vector2 v10 = Random2(new Vector2(fl_x + 1, fl_z + 0));
                    Vector2 v01 = Random2(new Vector2(fl_x + 0, fl_z + 1));
                    Vector2 v11 = Random2(new Vector2(fl_x + 1, fl_z + 1));

                    float v0010 = (1.0f - ux) * Vector2.Dot(v00, new Vector2(fr_x - 0, fr_z - 0)) + ux * Vector2.Dot(v10, new Vector2(fr_x - 1, fr_z - 0));
                    float v0111 = (1.0f - ux) * Vector2.Dot(v01, new Vector2(fr_x - 0, fr_z - 1)) + ux * Vector2.Dot(v11, new Vector2(fr_x - 1, fr_z - 1));

                    float rand = (1.0f - uz) * v0010 + uz * v0111;

                    Instantiate(cubePrefab, new Vector3(x, rand * 4, z), Quaternion.identity, transform);
                }
            }
            
        }
    }
    static private Vector2 Random2(Vector2 p)
    {
        p = new Vector2(Vector2.Dot(p, new Vector2(127.1f, 311.7f)), Vector2.Dot(p, new Vector2(269.5f, 183.3f)));
        Vector2 f = new Vector2(Mathf.Sin(p.x) * 43758.5453123f, Mathf.Sin(p.y) * 43758.5453123f);
        return new Vector2(-1.0f + 2.0f * (f.x - Mathf.Floor(f.x)), -1.0f + 2.0f * (f.y - Mathf.Floor(f.y)));
    }

Fractal Brownian Motion(fBm)ノイズ

f:id:simplestar_tech:20180927072218j:plain

実装はこんな感じに修正しました。

    private float parlin(int x, int z)
    {
        float vx = x / (float)SIDE * 4;
        float vz = z / (float)SIDE * 4;

        float fl_x = Mathf.Floor(vx);
        float fl_z = Mathf.Floor(vz);

        float fr_x = vx - fl_x;
        float fr_z = vz - fl_z;

        float ux = fr_x * fr_x * (3.0f - 2.0f * fr_x);
        float uz = fr_z * fr_z * (3.0f - 2.0f * fr_z);

        Vector2 v00 = Random2(new Vector2(fl_x + 0, fl_z + 0));
        Vector2 v10 = Random2(new Vector2(fl_x + 1, fl_z + 0));
        Vector2 v01 = Random2(new Vector2(fl_x + 0, fl_z + 1));
        Vector2 v11 = Random2(new Vector2(fl_x + 1, fl_z + 1));

        float v0010 = (1.0f - ux) * Vector2.Dot(v00, new Vector2(fr_x - 0, fr_z - 0)) + ux * Vector2.Dot(v10, new Vector2(fr_x - 1, fr_z - 0));
        float v0111 = (1.0f - ux) * Vector2.Dot(v01, new Vector2(fr_x - 0, fr_z - 1)) + ux * Vector2.Dot(v11, new Vector2(fr_x - 1, fr_z - 1));

        float rand = (1.0f - uz) * v0010 + uz * v0111;
        return rand;
    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.A))
        {
            for (int x = 0; x < SIDE; x++)
            {
                for (int z = 0; z < SIDE; z++)
                {
                    float rand = 0.5f * parlin(x, z);
                    rand += 0.25f * parlin(x << 1, z << 1);
                    rand += 0.125f * parlin(x << 2, z << 2);
                    rand += 0.0625f * parlin(x << 3, z << 3);
                    Instantiate(cubePrefab, new Vector3(x, rand * 8, z), Quaternion.identity, transform);
                }
            }
            
        }
    }
    static private Vector2 Random2(Vector2 p)
    {
        p = new Vector2(Vector2.Dot(p, new Vector2(127.1f, 311.7f)), Vector2.Dot(p, new Vector2(269.5f, 183.3f)));
        Vector2 f = new Vector2(Mathf.Sin(p.x), Mathf.Sin(p.y)) * 43758.5453123f;
        return 2.0f * new Vector2(f.x - Mathf.Floor(f.x), f.y - Mathf.Floor(f.y)) - Vector2.one;
    }

■考察
さて、ここから ECSとJobSystems による高速化について考えていきます。
そもそも ECS はどうやって計算をキックするんでしたっけ?

■改善

■まとめ

■具体的なコード

■気になったこと1
Unity の surf シェーダのライティングの種類をざっと確認したい
説明はこちらにあり
>ライティングモデルには、ディフューズライティングには Lambert、スペキュラーライティングには BlinnPhong という 2 つのビルトインモデルがあります。
サーフェスシェーダーでのカスタムライティングモデル - Unity マニュアル
…もっとあるんじゃない?

はい、あります。
UnityPBSLighting.cginc というファイルの中に

inline half4 LightingStandard (・・・) {・・・}
inline half4 LightingStandard_Deferred (・・・) {・・・}

と用意されています。
詳説してくれている記事を見つけました。感謝です!
tsumikiseisaku.com
この方も独自に調査して発見していますね。
なんで公式ドキュメントは説明を怠ったのでしょうか?

■気になったこと2
Unity ShaderLab の基礎関数、特に frac が何を意味するのか知りたい
ということで、次の記事を参考にしました。
Unity ShaderLab ノート
frac frac(x) 小数値の小数部分を返す
なるほど、もっと複雑な処理を想像していましたが、安心しました。