simplestarの技術ブログ

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

Unity:job-system-cookbookからC#JobSystemの実例を確認

ちょっと前の私の記事
simplestar-tech.hatenablog.com
こちらでメッシュの変形やCubeの移動、画像処理などのサンプルをみつけたので、今回はそのサンプルの実装を確かめていきます。

Unity 2108.2.5f1
PackageManager にて
Burst 0.2.4-preview.26
Entities 0.0.12-preview.11
を有効化して動作確認しました。

ExampleScenes は 8つありますが、その中でも画像処理を行う WebcamProcessing のコードを追ってみます。
注意点として Webカメラから画像情報を引っ張ってくる処理と、その画像情報をテクスチャに適用する処理がメインスレッドの処理の99%を占めているので
その辺のコードを外してみるとパフォーマンスのすごさが体感できると思います。(いつか、カメラ画像情報の取得のオーバーヘッド減るといいね、画像処理研究してる人は50年ほど悲しんでいる)

コードを見ていて最初に衝撃を受けたのが次の SliceWithStride
Unity.Collections.NativeSlice_1.SliceWithStride - Unity スクリプトリファレンス

        m_Data = new Color32[m_WebcamTextureSize.x * m_WebcamTextureSize.y];
        m_NativeColors = new NativeArray<Color32>(m_Data, Allocator.Persistent);

        var slice = new NativeSlice<Color32>(m_NativeColors);
        m_NativeRed = slice.SliceWithStride<byte>(0);
        m_NativeGreen = slice.SliceWithStride<byte>(1);
        m_NativeBlue = slice.SliceWithStride<byte>(2);

NativeSlice は native 故に便利関数が用意できるんですね。
要素単位のオフセット指定で byte を取り出せる配列になるとか、C++でできた高速アクセステクニックが復活してきた

毎フレーム呼ばれるのが以下の関数

    void BurstExclusiveOrProcessing(NativeSlice<byte> r, NativeSlice<byte> g, NativeSlice<byte> b, ref JobHandle handle)
    {
        var redJob = new ThresholdExclusiveOrBurstJob()
        {
            data = r,
            threshold = m_ColorThreshold.r,
            widthOverLineSkip = m_WebcamTextureSize.x / lineSkip,
            height = m_WebcamTextureSize.y,
        };

        var greenJob = new ThresholdExclusiveOrBurstJob()
        {
            data = g,
            threshold = m_ColorThreshold.g,
            widthOverLineSkip = m_WebcamTextureSize.x / lineSkip,
            height = m_WebcamTextureSize.y,
        };

        var blueJob = new ThresholdExclusiveOrBurstJob()
        {
            data = b,
            threshold = m_ColorThreshold.b,
            widthOverLineSkip = m_WebcamTextureSize.x / lineSkip,
            height = m_WebcamTextureSize.y,
        };

        var length = m_NativeRed.Length;
        var rHandle = redJob.Schedule(length, 1024);
        var gHandle = greenJob.Schedule(length, 1024, rHandle);
        handle = blueJob.Schedule(length, 1024, gHandle);
    }

ThresholdExclusiveOrBurstJob を引数を r,g, b と変えながら3つ定義し、r, g, b の順でスケジュールしています。
Job 処理は 1024 画素ごとに行うように指定している

処理を理解するうえで重要なのは次のジョブの実装

[BurstCompile]
public struct ThresholdExclusiveOrBurstJob : IJobParallelFor
{
    public NativeSlice<byte> data;
    public byte threshold;
    public int height;
    public int widthOverLineSkip;

    public void Execute(int i)
    {
        bool operateOnThisPixel = (i % height) < widthOverLineSkip;
        bool overThreshold = data[i] > threshold;
        data[i] = (byte)math.select(data[i], data[i] ^ threshold, overThreshold && operateOnThisPixel);
    }
}

math.select は条件分岐を提供する Unity の新 math ライブラリの関数
どんなものがこれから登場するかは次のスライドの 33 p で予告されていた

www.slideshare.net

math ライブラリも NativeSlice もドキュメント整備はこれからって感じで、しっかりした説明にはまだたどり着けない

論理 XOR 演算子も復習しておかないと、すぐにはコードを読んでイメージできない
^ 演算子 (C# リファレンス) | Microsoft Docs

とりあえず ECS を抜いた C# Job System + Burst の書式で書かれた非常に短いサンプルですね。
理解できました。

メッシュ操作の方が気になるので、もう少しサンプルを見ていきましょうか

MeshComplexParallel というクラスの実装をみてます。

    NativeArray<Vector3> m_Vertices;
    NativeArray<Vector3> m_Normals;

    Mesh m_Mesh;

主な登場人物はこんな感じ(人ではないけど)

頻繁な更新を行うためにメッシュを最適化するのに

        m_Mesh = gameObject.GetComponent<MeshFilter>().mesh;
        m_Mesh.MarkDynamic();

        // this persistent memory setup assumes our vertex count will not expand
        m_Vertices = new NativeArray<Vector3>(m_Mesh.vertices, Allocator.Persistent);
        m_Normals = new NativeArray<Vector3>(m_Mesh.normals, Allocator.Persistent);

こんなことしてます。
マジかよ!!超いいこと知ってしまったぜ。(ていうか、最初から知っておくべき

頂点を更新するのにパーリンノイズを利用していたり、頂点自体が Vector3 と blitable ではないので Birst Compile は諦めている様子

    struct MeshModJob : IJobParallelFor
    {
        public NativeArray<Vector3> vertices;
        public NativeArray<Vector3> normals;

        public float sinTime;
        public float cosTime;

        public float strength;

        public void Execute(int i)
        {
            var vertex = vertices[i];

            var perlin = Mathf.PerlinNoise(vertex.z, vertex.y * vertex.x);
            perlin *= strength * 2;
            var noise = normals[i] * perlin;
            var sine = normals[i] * sinTime * strength;

            vertex = vertex - sine + noise;

            vertices[i] = vertex;

            normals[i] += Vector3.one * cosTime * perlin;
        }
    }

とても参考になります。
マインクラフト風の地形生成をこんな感じで Job を使って高速化してみたいですね