simplestarの技術ブログ

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

Android:storage/emulated/0/のファイルをPCで取得する

仲間の結婚式の余興ムービーの素材用に、音声を録音アプリで録音してきたのだけど
データ保存フォルダがPCで閲覧できない場所になってて、ちょっと調べちゃった。(30分くらい)

具体的には
storage/emulated/0/VoiceRecorder
に録音したよってアプリの設定に書いてあり、そのフォルダは PC から閲覧できないというもの

解決方法:開発者モードにして adb pull コマンドで PC に持ってくる。
開発者じゃない一般ユーザーが、こんな方法しかないと知ったら怒るんじゃないかな?

で具体的に行ったコマンドを記録しておきます。

adb pull /storage/emulated/0/VoiceRecorder C:/Users/simpl/Pictures/voice

実行すると voice フォルダの中に VoiceRecorder フォルダが作られ、録音したデータが入っていました。

録音には EZ STUDIO 製の「録音機」という広告がいっぱい表示される無料アプリを使いました。

コマンド情報ソース
stackoverflow.com

Javascriptのコード整理でESLint

一番参考になった記事
Visual Studio CodeでESLintを使う – 山本隆の開発日誌

ESLint にかけずに git に提出したら死罪だおらーみたいなプロジェクトに配属
Visual Studio Code と npm コマンドが機能する状態で ESLint について無知な私
手順を教えてください→死罪だおらー

30分後に使えないと死ぬ!
みたいな状況で、上記記事では必要な手順が示されており、これを実行することで Visual Studio 上でコードが整理されることを確認しました。
ありがとうございます。

追って ESLint の設定やらルールやらを記憶していこうと思います。

Unity の記事は忘れてません、ECS 勉強中です。(Javascript は関係ないですが)

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

■実行結果
f:id:simplestar_tech:20181021202606g:plain

10000ブロックの高さの計算にバックグラウンド処理で 2.0 ms, メインスレッドは Job をキックするだけなので 0.05 ms でした。
なお ECS による高速化前は10000 ブロックの高さの更新に 68 ms を要していました。
およそ 1360 倍の高速化に成功しました。

■前提知識

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 の記事を書きます。

約一か月勉強して記事を書きました!
qiita.com

これをベースにノイズ生成のロジックを書いていきましょうか。

Job Systems でのメッシュデータの生成のための予備知識のためにこちらを一読…しましたが
Unity:job-system-cookbookからC#JobSystemの実例を確認 - simplestarの技術ブログ
ちょっとこの記事では、メッシュ生成まで考えるとコンテンツを詰め込み過ぎになってしまうのでやめます。

パーリンノイズの作り方はこちらを参照
nn-hokuson.hatenablog.com

■構想
最初はパーリンノイズ作成を簡単に理解できるように参考資料に沿って、最適化を考えずに実装していきます。
そのあとに ECS を導入して記事は完成です。

■実践

まずは基礎関数だけで作った疑似ランダムノイズ
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 はどうやって計算をキックするんでしたっけ?

約一か月 ECS について勉強していたことになりますが、答えは「 ECS が用意している World に Component System を作ると
その Component System が対象とする Entity がシーン内に存在していると OnUpdate が毎フレーム呼ばれる」が正解でした。

詳しくはこちら
qiita.com

ECS + Job + Burst で Fractal Brownian Motion ノイズ地形生成を実装します。

■改善

実装しました。

プロジェクトの設定:
File メニュー > Build Settings > Player Settings の Scripting Define Symbols に
UNITY_DISABLE_AUTOMATIC_SYSTEM_BOOTSTRAP
を追記して(複数設定する時のデリミタは ; でした) Enter を押し、ビルド完了を待ち

ポインタを C# で扱うことになるので、事前に File メニュー > Build Settings > Player Settings を開いて Allow 'unsafe' Code にチェックを入れます。

下記スクリプトをカメラオブジェクトにアタッチしてシーンを再生します。
HelloECS.cs

using Unity.Burst;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Rendering;
using Unity.Transforms;
using UnityEngine;

[RequireComponent(typeof(Camera))]
public class HelloECS : MonoBehaviour
{
    public Material cubeMaterial;

    void Start()
    {
        InitializeWorld();

        CreateCubeForECS();
    }

    void OnDisable()
    {
        ScriptBehaviourUpdateOrder.UpdatePlayerLoop(null);
        _world?.Dispose();
    }

    void InitializeWorld()
    {
        _world = World.Active = new World("MyWorld");
        _world.CreateManager(typeof(EntityManager));
        _world.CreateManager(typeof(EndFrameTransformSystem));
        _world.CreateManager(typeof(EndFrameBarrier));
        _world.CreateManager<MeshInstanceRendererSystem>().ActiveCamera = GetComponent<Camera>();
        _world.CreateManager(typeof(RenderingSystemBootstrap));
        
        _world.CreateManager(typeof(MyCubeSystem));
        ScriptBehaviourUpdateOrder.UpdatePlayerLoop(_world);
    }

    void CreateCubeForECS()
    {
        var cube = GameObject.CreatePrimitive(PrimitiveType.Cube);
        cube.transform.position = Vector3.zero;
        cube.transform.rotation = Quaternion.identity;
        cube.transform.localScale = Vector3.one;

        var manager = _world?.GetExistingManager<EntityManager>();
        if (null != manager)
        {
            var archetype = manager.CreateArchetype(ComponentType.Create<Prefab>(), ComponentType.Create<Position>(),
            ComponentType.Create<MeshInstanceRenderer>());

            var prefabEntity = manager.CreateEntity(archetype);

            manager.SetComponentData(prefabEntity, new Position() { Value = float3.zero });

            manager.SetSharedComponentData(prefabEntity, new MeshInstanceRenderer()
            {
                mesh = cube.GetComponent<MeshFilter>().sharedMesh,
                material = cubeMaterial,
                subMesh = 0,
                castShadows = UnityEngine.Rendering.ShadowCastingMode.Off,
                receiveShadows = false
            });

            const int SIDE = 100;
            NativeArray<Entity> entities = new NativeArray<Entity>(SIDE * SIDE, Allocator.Temp, NativeArrayOptions.UninitializedMemory);
            try
            {
                manager.Instantiate(prefabEntity, entities);
                float time = Time.realtimeSinceStartup;
                for (int x = 0; x < SIDE; x++)
                {
                    for (int z = 0; z < SIDE; z++)
                    {
                        int index = x + z * SIDE;
                        float rand = 0.5f * Perlin(x, z);
                        rand += 0.25f * Perlin(x << 1, z << 1);
                        rand += 0.125f * Perlin(x << 2, z << 2);
                        rand += 0.0625f * Perlin(x << 3, z << 3);
                        manager.SetComponentData(entities[index], new Position
                        {
                            Value = new float3(x, rand * 8, z)
                        });
                    }
                }
                float span = Time.realtimeSinceStartup - time;
                Debug.Log("span = " + span.ToString("0.00000"));
            }
            finally { entities.Dispose(); }
        }

        Destroy(cube);
    }

    private float Perlin(int x, int z)
    {
        float vx = x / (float)100 * 1;
        float vz = z / (float)100 * 1;

        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;
    }

    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;
    }

    private World _world;
}

public sealed class MyCubeSystem : JobComponentSystem
{
    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        var myCubeJob = new MyCubeJob
        {
            time = Time.realtimeSinceStartup * 3
        };
        return myCubeJob.Schedule(this, inputDeps);
    }

    [BurstCompile(Accuracy.Med, Support.Relaxed)]
    struct MyCubeJob : IJobProcessComponentData<Position>
    {
        public float time;

        public unsafe void Execute(ref Position data)
        {
            float3 pos = data.Value;
            float x = data.Value.x + time;
            float z = data.Value.z + time;
            float rand = 0.5f * Perlin(x, z);
            rand += 0.25f * Perlin(x * 2, z * 2);
            rand += 0.125f * Perlin(x * 4, z * 4);
            rand += 0.0625f * Perlin(x * 8, z * 8);
            pos.y = rand * 150;
            data.Value = pos;
        }
    }

    static private float Perlin(float x, float z)
    {
        float vx = x / 100 * 1;
        float vz = z / 100 * 1;

        float fl_x = math.floor(vx);
        float fl_z = math.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);

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

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

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

    static private float2 Random2(float2 p)
    {
        p = new float2(math.dot(p, new float2(127.1f, 311.7f)), math.dot(p, new float2(269.5f, 183.3f)));
        float2 f = new float2(Mathf.Sin(p.x), Mathf.Sin(p.y)) * 43758.5453123f;
        return 2.0f * new float2(f.x - Mathf.Floor(f.x), f.y - Mathf.Floor(f.y)) - new float2(1, 1);
    }
}

実行結果:
f:id:simplestar_tech:20181021202606g:plain

10000ブロックの高さの計算にバックグラウンド処理で 2.0 ms, メインスレッドは Job をキックするだけなので 0.05 ms でした。
なお ECS による高速化前は10000 ブロックの高さの更新に 68 ms を要していました。
およそ 1360 倍の高速化に成功しました。

■まとめ

ノイズ作成コードの資料を参考に C# で五つのノイズを生成して動作確認しました。
Unity Entity Component System について勉強し、技術者向けの入門資料をまとめました。
ECS を使った fBm ノイズ生成を確認、およそ 1360 倍の高速化することを確認しました。

■気になったこと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) 小数値の小数部分を返す
なるほど、もっと複雑な処理を想像していましたが、安心しました。

Unity2017~2018の追加機能の習熟作業を振り返る

前回の目標を達成しました。

以下が前回の目標に掲げたこと

Unity に頂点カラーのマテリアルは用意されていません。
そこで、シェーダを多少はいじれるようにしておきたいと考えました。
そのほかUnityのシェーダーを編集できる UI の確認
タイムラインもそろそろ触れるようになっておきたいし
Cinemachine ってのも勉強したいです。

頂点カラーマテリアルの作り方はこちらで紹介
simplestar-tech.hatenablog.com

シェーダーを多少はいじれるようになりました。
Unityのシェーダーを編集できる UI とは Shader Graph のことで、この Graph Shader のコードも出力できますし、編集できます。
お勉強は大成功ですね!

simplestar-tech.hatenablog.com

タイムラインもその辺の Animation Clip 触っただけの素人なんか小指で吹き飛ばせるほど、スクリプトでカスタマイズできる実力も付きました。
simplestar-tech.hatenablog.com

Cinemachine も謎のままでしたが、やっと正体がつかめた気がします。
simplestar-tech.hatenablog.com

次はこれらの知識をベースに、しばらく止めていたAIに身体性を与えるためのマイクロワールドの構築を進めていきます。

なんてったって Unity がマルチスレッド処理 + コンパイラレベルでの高速化を導入してきたんですよ!

その辺の技術も興味深く勉強してきました

ECS
simplestar-tech.hatenablog.com

Job Systems
simplestar-tech.hatenablog.com

Burst Compiler
simplestar-tech.hatenablog.com

さらに Scriptable Render Pipeline についても理解を深めました。
simplestar-tech.hatenablog.com

マテリアルの切り替え回数を減らせているかを確認するデバッグ術もマスターしています。
simplestar-tech.hatenablog.com

Unity の今後の Prefab とアセットバンドル管理の仕組みも予習し、リリースに向けての土台作りも意識できるようになっています。
simplestar-tech.hatenablog.com

新しい Network 機能を Google と連携して Unity が用意しているってニュースが流れていますし、今は個人ゲーム開発のわくわくが止まらないですね。
さぁて、1年前に構想した心を持つAIの作り方
simplestar-tech.hatenablog.com

こちらを目指して実装していきましょう!

Unity:Timelineでスクリプトの関数を呼ぶ具体的な手順

昨今のゲームはアイドルが歌って踊るシーンを眺めたりするものが多く、そういったシーンを作るときにタイムラインを制作することになります。

各社が内製ツールで頑張っていたため、Unity 2017 で公式に導入されました。
docs.unity3d.com

私たちが知りたいのはタイムラインの表面的な編集方法ではなく、システムの根幹の設計思想なので、その部分を重点的にとらえてきたいと思います。

色々書いたのですが、最初にこのビデオを通しで見ることが重要なことがわかりました。(51分の動画です、全部見ました。)
www.youtube.com

動画から得られた知見:
スクリプト拡張には 3通り
1.Animation Track にて、疑似変数を MonoBehaviour に作成して、LateUpdate などで目的のスクリプトを実行(パラメータをアニメーションで完全に操作する場合に有効)
2.Control Track にて、ITimelineControl インタフェースを実装する MonoBehaviour を作成して、SetTime 関数内で目的のスクリプトを実行(シークバーの時間が double 型で取れるが、クリップ全体の長さが取得できないので注意)
実装イメージはこちらの記事がつかみやすい。
tsubakit1.hateblo.jp
3.カスタム Playable Track にて、PlayableBehabiour を実装し、ProcessFrame 関数内で目的のスクリプトを実行する(ただし Track と Clip それぞれで Behabior.ProcessFrame を利用するが、Clip の方処理落ちで呼ばれないことがあるので、使わないでください)
カスタム Playable Track でシークバーの時間を取る手段はありませんので PlayableDirector をシーンから見つけ出し time を参照してください。

■ここから導入
タイムラインを扱う時、最初に覚えなければならないのは Playable ~という用語の意味です。

まずは基本中の基本ですが、ゲーム実行中に条件がそろったタイミングでタイムラインが再生され、終了し、再びゲームに戻ってくる
このときに役割をこなすオブジェクトたちを見てみましょう。

Timeline はスクリプトから作成するときは

TimelineAsset timeline = ScriptableObject.CreateInstance<TimelineAsset>();

と書きます。ほか、プロジェクトビューのコンテキストメニューの Create > Timeline でもアセットファイルとして作成可能です。
Timeline を利用する時、必ずシーン内に作成するのが Playable Director です。

シーンに配置する場合は、Playable Director コンポーネントを追加するだけです。
f:id:simplestar_tech:20180923091629j:plain

Playable Director は再生する Timeline を一つだけ設定することができます。
ツリー構造で親から順に Timeline に登場する概念を並べると次のおとり
Playable Director(どこにでもいる) ≒ Timeline(Playable Director に配置) >Tracks (オブジェクト単位で平行世界軸方向に分散して配置) > Clips (イベント単位で時間軸方向に分散して配置)

指定したオブジェクトに何らかのイベントを働きかけるのは Clip ですが、それがいつ起きるのかは Clip の時間軸方向の配置場所によって決定され
どのオブジェクトに働きかけるかは、Track 単位で決まり、Track を無数に管理しているのが Timeline といったところです。

2週間ほど前に、Timeline と Cinemachine を統合したシーンを初めて触りました。その時の記事
simplestar-tech.hatenablog.com

Timeline の編集とは、オブジェクトに紐づくトラックを並列に追加し、そのうちの一つのトラックの中にクリップを順番に配置することだった

初見でもだいたい同じメンタルモデルを構築しているのがわかります。

さてゲーム中にこの Playable Director を任意の条件で再生するという方法はチュートリアルの動画に示されています。(8分ほどの動画です、最後まで見ました。)
www.youtube.com

動画から得られる知見:
Playable Director を実際にアニメーションするオブジェクトや、イベントを発行するオブジェクトなどに割り当てて
ディフォルトでチェックが入っている Play On Awake のチェックを外しておく

次のように、条件に合った時に Playable Director を Play してタイムラインを再生する

    [SerializeField]
    private PlayableDirector playableDirector;

    // Update is called once per frame
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.A))
        {
            playableDirector.Play();
        }
    }

基本中の基本(ゲーム実行中に条件がそろったタイミングでタイムラインが再生され、終了し、再びゲームに戻ってくる)
の具体的な手順がイメージできるようになりました。

動画に登場した Playable Asset について理解度を上げていきます。
参考にした記事はこちら

www.shibuya24.info

記事から得られた知見:
・Timeline にはいくつかのタイプの Track を追加することになりますが、Control Track は Prefab を Instantiate する Track でした。
公式ドキュメントには説明がない)
・特定のタイミングにおけるオブジェクトの操作、挙動を処理させるクラスを作る場合は PlayableBehaviour を継承して作る
・PlayableBehaviour が参照するオブジェクトを渡すのがTrackに相当する TrackAsset または PlayableAsset クラス

ここまでの Timeline の習熟作業でわかったこと
・Timeline に Track を追加する作業がある
・Track にゲームオブジェクトを割り当てる(バインディングする)作業がある
・Track に Clip を追加する作業がある

■本題の作業にフォーカス

スクリプトで Track と Clip を定義する仕組みが用意されています。
Track に相当するのが TrackAsset、Track, Clip 両方に相当するのが Playable Asset、Clip 内でのスクリプト処理を担当するのが PlayableBehaviour、複数の Clip を Mix するのが PlayableBehaviour です(混乱させてすみませんが、そういうことになっている)。

まずは空の Playable Asset を実装して Timeline に Track として追加してみましょう。手順は Create > Playables > Playable Asset C# Script を選択してクラスコードを生成します。

f:id:simplestar_tech:20180923110550j:plain

作られたコードは次の通り

using UnityEngine;
using UnityEngine.Playables;

[System.Serializable]
public class NewPlayableAsset : PlayableAsset
{
    // Factory method that generates a playable based on this asset
    public override Playable CreatePlayable(PlayableGraph graph, GameObject go)
    {
        return Playable.Create(graph);
    }
}

実際、Timeline に追加できます。(Drag & Drop または Add メニューから選択可能)
f:id:simplestar_tech:20180923111359j:plain

混乱を招いていますが、Playable Asset は Track であり Clip でもあります。
要は Clip にはどうしても Track が必要なので、勝手に Playable Track が作られるというメンタルモデルを持つと納得できると思います。
最初に紹介した 50分の動画の発表者も、これの存在を忘れていて困っていましたね。

その Clip 内でスクリプト処理を担当する Playable Behavior を作ります。
手順は Create > Playables > Playable Behavior C# Script を選択します。

生成されるコードは次の通り

using UnityEngine.Playables;

// A behaviour that is attached to a playable
public class NewPlayableBehaviour : PlayableBehaviour
{
    // Called when the owning graph starts playing
    public override void OnGraphStart(Playable playable)
    {
        
    }

    // Called when the owning graph stops playing
    public override void OnGraphStop(Playable playable)
    {
        
    }

    // Called when the state of the playable is set to Play
    public override void OnBehaviourPlay(Playable playable, FrameData info)
    {
        
    }

    // Called when the state of the playable is set to Paused
    public override void OnBehaviourPause(Playable playable, FrameData info)
    {
        
    }

    // Called each frame while the state is set to Play
    public override void PrepareFrame(Playable playable, FrameData info)
    {
        
    }
}

このスクリプトは Timeline に Drop して使うものではありません。
先ほど作った Playable Asset のクラスにて new してインスタンスを渡して使います。
コードは以下の通り

using UnityEngine;
using UnityEngine.Playables;

[System.Serializable]
public class NewPlayableAsset : PlayableAsset
{
    // original code from
    // http://www.shibuya24.info/entry/timeline_basis
    public override Playable CreatePlayable(PlayableGraph graph, GameObject go)
    {
        return ScriptPlayable<NewPlayableBehaviour>.Create(graph, new NewPlayableBehaviour());
    }
}

ゲームオブジェクトを参照したい場合は以下の通り
そろそろUnity2017のTimelineの基礎を押さえておこう - 渋谷ほととぎす通信 の記事を参考にしています。

[System.Serializable]
public class NewPlayableAsset : PlayableAsset
{
    public ExposedReference<GameObject> sceneObj;
    public GameObject projectObj;

    // original code from
    // http://www.shibuya24.info/entry/timeline_basis
    public override Playable CreatePlayable(PlayableGraph graph, GameObject go)
    {
        var behaviour = new NewPlayableBehaviour();
        behaviour.sceneObj = sceneObj.Resolve(graph.GetResolver());
        behaviour.projectObj = projectObj;
        return ScriptPlayable<NewPlayableBehaviour>.Create(graph, behaviour);
    }
}

PlayableBehaviour

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Playables;

// A behaviour that is attached to a playable
public class NewPlayableBehaviour : PlayableBehaviour
{
    public GameObject sceneObj;
    public GameObject projectObj;

    // Called when the owning graph starts playing
    public override void OnGraphStart(Playable playable)
    {
    }

    // Called when the owning graph stops playing
    public override void OnGraphStop(Playable playable)
    {
    }

    // Called when the state of the playable is set to Play
    public override void OnBehaviourPlay(Playable playable, FrameData info)
    {
        Debug.Log("Seek Bar Enter the Clip");
    }

    // Called when the state of the playable is set to Paused
    public override void OnBehaviourPause(Playable playable, FrameData info)
    {
        Debug.Log("Seek Bar Leave the Clip");
    }

    // Called each frame while the state is set to Play
    public override void PrepareFrame(Playable playable, FrameData info)
    {
        if (null != sceneObj.GetComponent<PlayableDirector>())
        {
            Debug.Log("PrepareFrame playableDirector.time = " + sceneObj.GetComponent<PlayableDirector>().time.ToString("0.000"));
        }        
    }
}

これで正しく動作することを確認できました。
動画にあったような、フレームが飛ぶ現象が考えられるというものがありましたが、それ恐怖ですね。
1フレームだけ設定した Clip が呼ばれずにすべてが破綻するようなシーンは作らないように、運用していかなければなりませんね(げんなり)

■まとめ
Timelineでスクリプトの関数を呼ぶ具体的な手順を示しました。
実際に動作確認も済ませました。
50分の動画では TrackAsset と Mixer の活用を推奨していましたが、ちょっと今回の記事に盛り込むにはボリュームありすぎでやってません。
PlayableBehaviour の OnBehaviourPlay は必ず呼ばれて欲しいなぁと思いますが、呼ばれないケースというものを体験するシーンを作るのも面倒くさくてやっていません。
おそらくタイムラインを活用する人たちは、タイミングバグに苦しむことだと思います。
案外 Activation Clip で Start 関数にスクリプトを仕込むだけの方が楽な手法なのかもと思いました。

最後に一言、Timeline のスクリプトカスタマイズは数十分でマスターできるもんじゃなかった…

Unity:ShaderGraph to ShaderCode

There is a way to convert shader graph to shader code.

f:id:simplestar_tech:20180923002633j:plain

You can open the graph, right click the master node and select "Copy shader".

Paste the shader code to your shader file.

Example: Vertex Color Shader

Shader "Unlit Master"
{
    Properties
    {

    }
    SubShader
    {
        Tags{ "RenderPipeline" = "LightweightPipeline"}
        Tags
        {
            "RenderPipeline"="HDRenderPipeline"
            "RenderType"="Opaque"
            "Queue"="Geometry"
        }
        Pass
        {
            Name "StandardUnlit"
            Tags{"LightMode" = "LightweightForward"}

            // Material options generated by graph

            Blend One Zero

            Cull Back

            ZTest LEqual

            ZWrite On

            HLSLPROGRAM
            // Required to compile gles 2.0 with standard srp library
            #pragma prefer_hlslcc gles
            #pragma exclude_renderers d3d11_9x
            #pragma target 2.0

            // -------------------------------------
            // Lightweight Pipeline keywords
            #pragma shader_feature _SAMPLE_GI

            // -------------------------------------
            // Unity defined keywords
            #pragma multi_compile_fog

            //--------------------------------------
            // GPU Instancing
            #pragma multi_compile_instancing
            
            #pragma vertex vert
            #pragma fragment frag

            // Defines generated by graph

            // Lighting include is needed because of GI
            #include "LWRP/ShaderLibrary/Core.hlsl"
            #include "LWRP/ShaderLibrary/Lighting.hlsl"
            #include "CoreRP/ShaderLibrary/Color.hlsl"
            #include "LWRP/ShaderLibrary/InputSurfaceUnlit.hlsl"
            #include "ShaderGraphLibrary/Functions.hlsl"


            struct VertexDescriptionInputs
            {
                float3 ObjectSpacePosition;
            };

            struct SurfaceDescriptionInputs
            {
                float4 VertexColor;
            };


            struct VertexDescription
            {
                float3 Position;
            };

            VertexDescription PopulateVertexData(VertexDescriptionInputs IN)
            {
                VertexDescription description = (VertexDescription)0;
                description.Position = IN.ObjectSpacePosition;
                return description;
            }

            struct SurfaceDescription
            {
                float3 Color;
                float Alpha;
                float AlphaClipThreshold;
            };

            SurfaceDescription PopulateSurfaceData(SurfaceDescriptionInputs IN)
            {
                SurfaceDescription surface = (SurfaceDescription)0;
                surface.Color = (IN.VertexColor.xyz);
                surface.Alpha = 1;
                surface.AlphaClipThreshold = 0;
                return surface;
            }

            struct GraphVertexInput
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float4 tangent : TANGENT;
                float4 color : COLOR;
                float4 texcoord1 : TEXCOORD1;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };


            struct GraphVertexOutput
            {
                float4 position : POSITION;

                // Interpolators defined by graph
                float3 WorldSpacePosition : TEXCOORD3;
                float3 WorldSpaceNormal : TEXCOORD4;
                float3 WorldSpaceTangent : TEXCOORD5;
                float3 WorldSpaceBiTangent : TEXCOORD6;
                float3 WorldSpaceViewDirection : TEXCOORD7;
                float4 VertexColor : COLOR;
                half4 uv1 : TEXCOORD8;

                UNITY_VERTEX_INPUT_INSTANCE_ID
                UNITY_VERTEX_OUTPUT_STEREO
            };

            GraphVertexOutput vert (GraphVertexInput v)
            {
                GraphVertexOutput o = (GraphVertexOutput)0;
                UNITY_SETUP_INSTANCE_ID(v);
                UNITY_TRANSFER_INSTANCE_ID(v, o);
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);

                // Vertex transformations performed by graph
                float3 WorldSpacePosition = mul(UNITY_MATRIX_M,v.vertex).xyz;
                float3 WorldSpaceNormal = normalize(mul(v.normal,(float3x3)UNITY_MATRIX_I_M));
                float3 WorldSpaceTangent = normalize(mul((float3x3)UNITY_MATRIX_M,v.tangent.xyz));
                float3 WorldSpaceBiTangent = cross(WorldSpaceNormal, WorldSpaceTangent.xyz) * v.tangent.w;
                float3 WorldSpaceViewDirection = _WorldSpaceCameraPos.xyz - mul(GetObjectToWorldMatrix(), float4(v.vertex.xyz, 1.0)).xyz;
                float4 VertexColor = v.color;
                float4 uv1 = v.texcoord1;
                float3 ObjectSpacePosition = mul(UNITY_MATRIX_I_M,float4(WorldSpacePosition,1.0)).xyz;

                VertexDescriptionInputs vdi = (VertexDescriptionInputs)0;

                // Vertex description inputs defined by graph
                vdi.ObjectSpacePosition = ObjectSpacePosition;

                VertexDescription vd = PopulateVertexData(vdi);
                v.vertex.xyz = vd.Position;

                o.position = TransformObjectToHClip(v.vertex.xyz);
                // Vertex shader outputs defined by graph
                o.WorldSpacePosition = WorldSpacePosition;
                o.WorldSpaceNormal = WorldSpaceNormal;
                o.WorldSpaceTangent = WorldSpaceTangent;
                o.WorldSpaceBiTangent = WorldSpaceBiTangent;
                o.WorldSpaceViewDirection = WorldSpaceViewDirection;
                o.VertexColor = VertexColor;
                o.uv1 = uv1;

                return o;
            }

            half4 frag (GraphVertexOutput IN ) : SV_Target
            {
                UNITY_SETUP_INSTANCE_ID(IN);

                // Pixel transformations performed by graph
                float3 WorldSpacePosition = IN.WorldSpacePosition;
                float3 WorldSpaceNormal = IN.WorldSpaceNormal;
                float3 WorldSpaceTangent = IN.WorldSpaceTangent;
                float3 WorldSpaceBiTangent = IN.WorldSpaceBiTangent;
                float3 WorldSpaceViewDirection = IN.WorldSpaceViewDirection;
                float4 VertexColor = IN.VertexColor;
                float4 uv1 = IN.uv1;

                
                SurfaceDescriptionInputs surfaceInput = (SurfaceDescriptionInputs)0;
                // Surface description inputs defined by graph
                surfaceInput.VertexColor = VertexColor;


                SurfaceDescription surf = PopulateSurfaceData(surfaceInput);
                float3 Color = float3(0.5, 0.5, 0.5);
                float Alpha = 1;
                float AlphaClipThreshold = 0;
                // Surface description remap performed by graph
                Color = surf.Color;
                Alpha = surf.Alpha;
                AlphaClipThreshold = surf.AlphaClipThreshold;

                
         #if _AlphaClip
                clip(Alpha - AlphaClipThreshold);
        #endif
                return half4(Color, Alpha);
            }
            ENDHLSL
        }
        Pass
        {
            Name "ShadowCaster"
            Tags{"LightMode" = "ShadowCaster"}

            ZWrite On ZTest LEqual

            // Material options generated by graph
            Cull Back

            HLSLPROGRAM
            // Required to compile gles 2.0 with standard srp library
            #pragma prefer_hlslcc gles
            #pragma exclude_renderers d3d11_9x
            #pragma target 2.0

            //--------------------------------------
            // GPU Instancing
            #pragma multi_compile_instancing

            #pragma vertex ShadowPassVertex
            #pragma fragment ShadowPassFragment

            // Defines generated by graph

            #include "LWRP/ShaderLibrary/Core.hlsl"
            #include "LWRP/ShaderLibrary/Lighting.hlsl"
            #include "ShaderGraphLibrary/Functions.hlsl"
            #include "CoreRP/ShaderLibrary/Color.hlsl"


            struct VertexDescriptionInputs
            {
                float3 ObjectSpacePosition;
            };

            struct SurfaceDescriptionInputs
            {
            };


            struct VertexDescription
            {
                float3 Position;
            };

            VertexDescription PopulateVertexData(VertexDescriptionInputs IN)
            {
                VertexDescription description = (VertexDescription)0;
                description.Position = IN.ObjectSpacePosition;
                return description;
            }

            struct SurfaceDescription
            {
                float Alpha;
                float AlphaClipThreshold;
            };

            SurfaceDescription PopulateSurfaceData(SurfaceDescriptionInputs IN)
            {
                SurfaceDescription surface = (SurfaceDescription)0;
                surface.Alpha = 1;
                surface.AlphaClipThreshold = 0;
                return surface;
            }

            struct GraphVertexInput
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float4 tangent : TANGENT;
                float4 texcoord1 : TEXCOORD1;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };


            struct VertexOutput
            {
                float2 uv           : TEXCOORD0;
                float4 clipPos      : SV_POSITION;
                // Interpolators defined by graph
                float3 WorldSpacePosition : TEXCOORD3;
                float3 WorldSpaceNormal : TEXCOORD4;
                float3 WorldSpaceTangent : TEXCOORD5;
                float3 WorldSpaceBiTangent : TEXCOORD6;
                float3 WorldSpaceViewDirection : TEXCOORD7;
                half4 uv1 : TEXCOORD8;

                UNITY_VERTEX_INPUT_INSTANCE_ID
                UNITY_VERTEX_OUTPUT_STEREO
            };

            // x: global clip space bias, y: normal world space bias
            float4 _ShadowBias;
            float3 _LightDirection;

            VertexOutput ShadowPassVertex(GraphVertexInput v)
            {
                VertexOutput o;
                UNITY_SETUP_INSTANCE_ID(v);
                UNITY_TRANSFER_INSTANCE_ID(v, o);
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);

                // Vertex transformations performed by graph
                float3 WorldSpacePosition = mul(UNITY_MATRIX_M,v.vertex).xyz;
                float3 WorldSpaceNormal = normalize(mul(v.normal,(float3x3)UNITY_MATRIX_I_M));
                float3 WorldSpaceTangent = normalize(mul((float3x3)UNITY_MATRIX_M,v.tangent.xyz));
                float3 WorldSpaceBiTangent = cross(WorldSpaceNormal, WorldSpaceTangent.xyz) * v.tangent.w;
                float3 WorldSpaceViewDirection = _WorldSpaceCameraPos.xyz - mul(GetObjectToWorldMatrix(), float4(v.vertex.xyz, 1.0)).xyz;
                float4 uv1 = v.texcoord1;
                float3 ObjectSpacePosition = mul(UNITY_MATRIX_I_M,float4(WorldSpacePosition,1.0)).xyz;

                VertexDescriptionInputs vdi = (VertexDescriptionInputs)0;

                // Vertex description inputs defined by graph
                vdi.ObjectSpacePosition = ObjectSpacePosition;

                VertexDescription vd = PopulateVertexData(vdi);
                v.vertex.xyz = vd.Position;

                // Vertex shader outputs defined by graph
                o.WorldSpacePosition = WorldSpacePosition;
                o.WorldSpaceNormal = WorldSpaceNormal;
                o.WorldSpaceTangent = WorldSpaceTangent;
                o.WorldSpaceBiTangent = WorldSpaceBiTangent;
                o.WorldSpaceViewDirection = WorldSpaceViewDirection;
                o.uv1 = uv1;

                
                float3 positionWS = TransformObjectToWorld(v.vertex.xyz);
                float3 normalWS = TransformObjectToWorldDir(v.normal);

                float invNdotL = 1.0 - saturate(dot(_LightDirection, normalWS));
                float scale = invNdotL * _ShadowBias.y;

                // normal bias is negative since we want to apply an inset normal offset
                positionWS = normalWS * scale.xxx + positionWS;
                float4 clipPos = TransformWorldToHClip(positionWS);

                // _ShadowBias.x sign depens on if platform has reversed z buffer
                clipPos.z += _ShadowBias.x;

            #if UNITY_REVERSED_Z
                clipPos.z = min(clipPos.z, clipPos.w * UNITY_NEAR_CLIP_VALUE);
            #else
                clipPos.z = max(clipPos.z, clipPos.w * UNITY_NEAR_CLIP_VALUE);
            #endif
                o.clipPos = clipPos;

                return o;
            }

            half4 ShadowPassFragment(VertexOutput IN) : SV_TARGET
            {
                UNITY_SETUP_INSTANCE_ID(IN);

                // Pixel transformations performed by graph
                float3 WorldSpacePosition = IN.WorldSpacePosition;
                float3 WorldSpaceNormal = IN.WorldSpaceNormal;
                float3 WorldSpaceTangent = IN.WorldSpaceTangent;
                float3 WorldSpaceBiTangent = IN.WorldSpaceBiTangent;
                float3 WorldSpaceViewDirection = IN.WorldSpaceViewDirection;
                float4 uv1 = IN.uv1;

                SurfaceDescriptionInputs surfaceInput = (SurfaceDescriptionInputs)0;

        		// Surface description inputs defined by graph

                SurfaceDescription surf = PopulateSurfaceData(surfaceInput);

        		float Alpha = 1;
        		float AlphaClipThreshold = 0;

        		// Surface description remap performed by graph
                Alpha = surf.Alpha;
                AlphaClipThreshold = surf.AlphaClipThreshold;

         #if _AlphaClip
        		clip(Alpha - AlphaClipThreshold);
        #endif
                return 0;
            }

            ENDHLSL
        }

        Pass
        {
            Name "DepthOnly"
            Tags{"LightMode" = "DepthOnly"}

            ZWrite On
            ColorMask 0

            // Material options generated by graph
            Cull Back

            HLSLPROGRAM
            // Required to compile gles 2.0 with standard srp library
            #pragma prefer_hlslcc gles
            #pragma exclude_renderers d3d11_9x
            #pragma target 2.0

            //--------------------------------------
            // GPU Instancing
            #pragma multi_compile_instancing

            #pragma vertex vert
            #pragma fragment frag

            // Defines generated by graph

            #include "LWRP/ShaderLibrary/Core.hlsl"
            #include "LWRP/ShaderLibrary/Lighting.hlsl"
            #include "ShaderGraphLibrary/Functions.hlsl"
            #include "CoreRP/ShaderLibrary/Color.hlsl"


            struct VertexDescriptionInputs
            {
                float3 ObjectSpacePosition;
            };

            struct SurfaceDescriptionInputs
            {
            };


            struct VertexDescription
            {
                float3 Position;
            };

            VertexDescription PopulateVertexData(VertexDescriptionInputs IN)
            {
                VertexDescription description = (VertexDescription)0;
                description.Position = IN.ObjectSpacePosition;
                return description;
            }

            struct SurfaceDescription
            {
                float Alpha;
                float AlphaClipThreshold;
            };

            SurfaceDescription PopulateSurfaceData(SurfaceDescriptionInputs IN)
            {
                SurfaceDescription surface = (SurfaceDescription)0;
                surface.Alpha = 1;
                surface.AlphaClipThreshold = 0;
                return surface;
            }

            struct GraphVertexInput
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float4 tangent : TANGENT;
                float4 texcoord1 : TEXCOORD1;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };


            struct VertexOutput
            {
                float2 uv           : TEXCOORD0;
                float4 clipPos      : SV_POSITION;
                // Interpolators defined by graph
                float3 WorldSpacePosition : TEXCOORD3;
                float3 WorldSpaceNormal : TEXCOORD4;
                float3 WorldSpaceTangent : TEXCOORD5;
                float3 WorldSpaceBiTangent : TEXCOORD6;
                float3 WorldSpaceViewDirection : TEXCOORD7;
                half4 uv1 : TEXCOORD8;

                UNITY_VERTEX_INPUT_INSTANCE_ID
                UNITY_VERTEX_OUTPUT_STEREO
            };

            VertexOutput vert(GraphVertexInput v)
            {
                VertexOutput o = (VertexOutput)0;
                UNITY_SETUP_INSTANCE_ID(v);
                UNITY_TRANSFER_INSTANCE_ID(v, o);
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);

                // Vertex transformations performed by graph
                float3 WorldSpacePosition = mul(UNITY_MATRIX_M,v.vertex).xyz;
                float3 WorldSpaceNormal = normalize(mul(v.normal,(float3x3)UNITY_MATRIX_I_M));
                float3 WorldSpaceTangent = normalize(mul((float3x3)UNITY_MATRIX_M,v.tangent.xyz));
                float3 WorldSpaceBiTangent = cross(WorldSpaceNormal, WorldSpaceTangent.xyz) * v.tangent.w;
                float3 WorldSpaceViewDirection = _WorldSpaceCameraPos.xyz - mul(GetObjectToWorldMatrix(), float4(v.vertex.xyz, 1.0)).xyz;
                float4 uv1 = v.texcoord1;
                float3 ObjectSpacePosition = mul(UNITY_MATRIX_I_M,float4(WorldSpacePosition,1.0)).xyz;

                VertexDescriptionInputs vdi = (VertexDescriptionInputs)0;

                // Vertex description inputs defined by graph
                vdi.ObjectSpacePosition = ObjectSpacePosition;

                VertexDescription vd = PopulateVertexData(vdi);
                v.vertex.xyz = vd.Position;

                // Vertex shader outputs defined by graph
                o.WorldSpacePosition = WorldSpacePosition;
                o.WorldSpaceNormal = WorldSpaceNormal;
                o.WorldSpaceTangent = WorldSpaceTangent;
                o.WorldSpaceBiTangent = WorldSpaceBiTangent;
                o.WorldSpaceViewDirection = WorldSpaceViewDirection;
                o.uv1 = uv1;

                o.clipPos = TransformObjectToHClip(v.vertex.xyz);
                return o;
            }

            half4 frag(VertexOutput IN) : SV_TARGET
            {
                UNITY_SETUP_INSTANCE_ID(IN);

                // Pixel transformations performed by graph
                float3 WorldSpacePosition = IN.WorldSpacePosition;
                float3 WorldSpaceNormal = IN.WorldSpaceNormal;
                float3 WorldSpaceTangent = IN.WorldSpaceTangent;
                float3 WorldSpaceBiTangent = IN.WorldSpaceBiTangent;
                float3 WorldSpaceViewDirection = IN.WorldSpaceViewDirection;
                float4 uv1 = IN.uv1;

                SurfaceDescriptionInputs surfaceInput = (SurfaceDescriptionInputs)0;

        		// Surface description inputs defined by graph

                SurfaceDescription surf = PopulateSurfaceData(surfaceInput);

        		float Alpha = 1;
        		float AlphaClipThreshold = 0;

        		// Surface description remap performed by graph
                Alpha = surf.Alpha;
                AlphaClipThreshold = surf.AlphaClipThreshold;

         #if _AlphaClip
        		clip(Alpha - AlphaClipThreshold);
        #endif
                return 0;
            }
            ENDHLSL
        }
    }
    FallBack "Hidden/InternalErrorShader"
}

Information source:
gamedev.stackexchange.com

Shader Graph で作成した Shader のコードを確認する方法:
Shader Graph で作ったコードを確認して無駄な計算を削減して高速化したいなぁって思うじゃないですか!
ちゃんとシェーダーコードを取得する手段がありましたよ。

これで勝てる!

Unity:頂点カラーマテリアルの作り方

先週の予告通り、Unity で頂点カラーマテリアルを作る方法を習得しましたので、私のメンタルモデルを記録・共有します。

f:id:simplestar_tech:20180922204437j:plain
Unity のマテリアルは元となるシェーダーのインスタンスと捉えることができ、特殊なマテリアルが欲しい場合はシェーダーをカスタマイズすることになります。

作り方は現在2通り提供されており
1.直感的な方法で Shader Graph を使ってシェーダーを作成する方法
2.シェーダーコードの書式を理解して書き換える方法
があります。

順番に具体的な手順を追ってみましょう。

■Shader Graph

Shader Graph の導入と使い方について、こちらの記事を参考に実践してみてください。
nn-hokuson.hatenablog.com

そのあとは覚えた手順に従って
Create > Shader > Unlit Graph
を選択し、作成された Shader ファイルから
Shader Graph のエディタを開いて、ノード作成より
Input > Geometry > Vertex Color
を選び out を Unlit Master ノードの color に接続します。
Save Asset して、その Shader から Create > Material を行えば頂点カラーマテリアルが完成します。

f:id:simplestar_tech:20180922205139j:plain

ちょ… Shader Graph やばくない?
何がやばいって、頂点カラーシェーダー作るのが、簡単すぎ
直感的に何でもできそうですよ、ぜひ皆さんも触ってみてください。

■シェーダーコードの書き換え

Unity のシェーダーコードは Surface 形式とピクセルシェーダー形式の二通りの記述形式があります。
Surface 形式は Unity 独自のピクセル値のライティングお任せ書式です。
ピクセルシェーダー形式は最後のピクセル値の決定まで操作できる書式です。

頂点カラーは頂点情報のみに頼るので Surface 形式を使うのが楽に書けます。
ということで Surface 形式でテンプレートを作り、そこに手を入れていきます。

Craete > Shader > Standard Surface Shader
を選択して Shader ファイルを作成します。
これを開くと Visual Studioテキストエディターになります。
その時の何もしていないテンプレコードは次の通り

Shader "Custom/VertexColor"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _Glossiness ("Smoothness", Range(0,1)) = 0.5
        _Metallic ("Metallic", Range(0,1)) = 0.0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200

        CGPROGRAM
        // Physically based Standard lighting model, and enable shadows on all light types
        #pragma surface surf Standard fullforwardshadows

        // Use shader model 3.0 target, to get nicer looking lighting
        #pragma target 3.0

        sampler2D _MainTex;

        struct Input
        {
            float2 uv_MainTex;
        };

        half _Glossiness;
        half _Metallic;
        fixed4 _Color;

        // Add instancing support for this shader. You need to check 'Enable Instancing' on materials that use the shader.
        // See https://docs.unity3d.com/Manual/GPUInstancing.html for more information about instancing.
        // #pragma instancing_options assumeuniformscaling
        UNITY_INSTANCING_BUFFER_START(Props)
            // put more per-instance properties here
        UNITY_INSTANCING_BUFFER_END(Props)

        void surf (Input IN, inout SurfaceOutputStandard o)
        {
            // Albedo comes from a texture tinted by color
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
            o.Albedo = c.rgb;
            // Metallic and smoothness come from slider variables
            o.Metallic = _Metallic;
            o.Smoothness = _Glossiness;
            o.Alpha = c.a;
        }
        ENDCG
    }
    FallBack "Diffuse"
}

ここから頂点カラーシェーダに書き換えますが、参考にする記事はこちら
nn-hokuson.hatenablog.com

上記のシェーダーコードを頂点カラー用に書き換えた結果がこちら

Shader "Custom/VertexColor"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _Glossiness ("Smoothness", Range(0,1)) = 0.5
        _Metallic ("Metallic", Range(0,1)) = 0.0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200

        CGPROGRAM
        // Physically based Standard lighting model, and enable shadows on all light types
        #pragma surface surf Standard fullforwardshadows vertex:vert

        // Use shader model 3.0 target, to get nicer looking lighting
        #pragma target 3.0

        struct Input
        {
            float4 vertColor;
        };

        half _Glossiness;
        half _Metallic;
        fixed4 _Color;

        // Add instancing support for this shader. You need to check 'Enable Instancing' on materials that use the shader.
        // See https://docs.unity3d.com/Manual/GPUInstancing.html for more information about instancing.
        // #pragma instancing_options assumeuniformscaling
        UNITY_INSTANCING_BUFFER_START(Props)
            // put more per-instance properties here
        UNITY_INSTANCING_BUFFER_END(Props)

        void vert(inout appdata_full v, out Input o) {
            UNITY_INITIALIZE_OUTPUT(Input, o);
            o.vertColor = v.color;
        }

        void surf (Input IN, inout SurfaceOutputStandard o)
        {
            o.Albedo = IN.vertColor.rgb;
            o.Alpha = IN.vertColor.a;
        }
        ENDCG
    }
    FallBack "Diffuse"
}

注目するのは Input の構造体に color を入れたのと、 vert 関数を作り vert に vert を利用すると pragma で宣言しているところ
この時 surf 内で頂点カラーを出力するように書き換えています。
これで頂点カラーを利用するようになるはずです。

結果がこちら
f:id:simplestar_tech:20180922212309j:plain

Shader Graph で作った Unlit と異なり、Surface によりライティングが施されていることが確認できます。

■おまけ Scriptable Render Pipeline 対応

ライティングが Standard だと Light Weight Scriptable Render Pipeline で正常に描画されません。
ライティングを SRP 対応に書き換えるとうまくいきます。

ここを

        #pragma surface surf Standard fullforwardshadows vertex:vert

        #pragma surface surf Lambert vertex:vert

こうすることで LWRP で正常に描画されるようになりました。

■参考

ちょっと頂点カラーより難しいですが、皆さんは次の水シェーダーのコードを読めますでしょうか?
こういう動きのあるシェーダーも書けるようになりたいですね。
baba-s.hatenablog.com