simplestarの技術ブログ

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

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

Unity:頂点生成三角柱のインスタンスの統合による高速化

前回 15 x 15 x 15 の計3375個の三角柱を生成したとき、その時のインスタンス作成処理に 0.3秒ほど要していました。
ここから先、画面を埋め尽くすほどのブロックの数を表示しようというのに、もう既に諦めなければならないほど低速です。
この処理を高速化するため、インスタンスの数を減らして、メッシュを統合する方法をとってみます。

これも昔行った手法です。
simplestar-tech.hatenablog.com

メッシュ作成の結果、見た目は同じでも
f:id:simplestar_tech:20180922175632j:plain

インスタンス生成時の負荷は 11.84 ms (頂点バッファ確保と計算が主な仕事)
f:id:simplestar_tech:20180922175703j:plain

メッシュ作成時の負荷は 4.72 ms (一気に 5000 頂点分の設定のため多少時間を要する)
f:id:simplestar_tech:20180922175918j:plain

でした。
もともとの三角柱ごとにインスタンスを作る方法は、インスタンス生成時に 267.92 ms 要していました。
f:id:simplestar_tech:20180922180132j:plain

もともとの三角柱ごとにインスタンスを作る方法は、メッシュ作成時に 58.73 ms 要していました。
f:id:simplestar_tech:20180922180330j:plain

もともとの三角柱ごとにインスタンスを作る方法は、このチャンク(16 x 16 x 16 の三角柱の塊り)一つ作る場合でもフレームレートが維持できませんでしたが
メッシュを結合してインスタンスを統合することで、メッシュ作成処理の負荷が抑えられ、フレームレートをなんとか維持できる処理に変わりました。

以降の記事では、この 16 x 16 x 16 のチャンクをしばらく利用します。

具体的なコードは以下、よろしければ参考にしてください。(主に自分用)

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

[RequireComponent(typeof(MeshRenderer))]
[RequireComponent(typeof(MeshFilter))]
public class Prism : MonoBehaviour
{
    [SerializeField]
    private Material _mat;

    const float root3 = 1.732051f;

    private Mesh _mesh;
    private MeshFilter _filter;

    const int SIDE = 16;
    private Vector3[] _vertices;
    private Vector3[] _normals;
    private Vector2[] _uvs;
    private int[] _triangles;

    // Use this for initialization
    void Start()
    {
        _vertices = new Vector3[SIDE * SIDE * (SIDE / 2) * vertIndices.Length];
        _normals = new Vector3[_vertices.Length];
        _uvs = new Vector2[_vertices.Length];
        _triangles = new int[_vertices.Length];
        for (int x = 0; x < SIDE; x++)
        {
            for (int y = 0; y < SIDE; y++)
            {
                for (int z = 0; z < SIDE; z += 2)
                {
                    for (int i = 0; i < vertIndices.Length; i++)
                    {
                        int vertIndex = (x * SIDE * (SIDE / 2) + y * (SIDE / 2) + z / 2) * vertIndices.Length + i;
                        ref Vector3 vert = ref _vertices[vertIndex];
                        ref Vector3 normal = ref _normals[vertIndex];
                        ref Vector2 uv = ref _uvs[vertIndex];
                        vert = positions[vertIndices[i]];
                        vert.x += x * 2;
                        vert.y += y;
                        vert.z += z * root3;

                        normal = normals[i];
                        uv = uvs[i];
                        _triangles[vertIndex] = vertIndex;
                    }
                }
            }
        }
        _mesh = new Mesh();
        
        _filter = GetComponent<MeshFilter>();

        var renderer = GetComponent<MeshRenderer>();
        renderer.material = _mat;
    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            CreateMesh();
        }
    }
    private void CreateMesh()
    {
        _mesh.vertices = _vertices;
        _mesh.triangles = _triangles;
        _mesh.uv = _uvs;
        _mesh.normals = _normals;
        _filter.sharedMesh = _mesh;
    }

    static Vector3[] positions = new Vector3[] {
        // top
        new Vector3 (0f, 1f, 0f),
        new Vector3 (1f, 1f, -root3),
        new Vector3 (-1f, 1f, -root3),
        // bottom
        new Vector3 (0f, 0f, 0f),
        new Vector3 (-1f, 0f, -root3),
        new Vector3 (1f, 0f, -root3),
    };
    static int[] vertIndices = new int[]
    {
        0, 1, 2,
        3, 4, 5,
        0, 2, 4,
        4, 3, 0,
        2, 1, 5,
        5, 4, 2,
        1, 0, 3,
        3, 5, 1
    };
    static Vector3[] vertices = new Vector3[vertIndices.Length];
    static Vector2[] uvs = new Vector2[]
    {
        // top
        new Vector2 (.25f, 0.5f),
        new Vector2 (0.5f, 0.0f),
        new Vector2 (0.0f, 0.0f),

        // bottom
        new Vector2 (.75f, 0.0f),
        new Vector2 (0.5f, 0.5f),
        new Vector2 (1.0f, 0.5f),

        // side R
        new Vector2 (0.0f, 1.0f),
        new Vector2 (.33f, 1.0f),
        new Vector2 (.33f, 0.5f),

        new Vector2 (.33f, 0.5f),
        new Vector2 (0.0f, 0.5f),
        new Vector2 (0.0f, 1.0f),

        // side G
        new Vector2 (.33f, 1.0f),
        new Vector2 (.66f, 1.0f),
        new Vector2 (.66f, 0.5f),

        new Vector2 (.66f, 0.5f),
        new Vector2 (.33f, 0.5f),
        new Vector2 (.33f, 1.0f),
        
        // side B
        new Vector2 (.66f, 1.0f),
        new Vector2 (1.0f, 1.0f),
        new Vector2 (1.0f, 0.5f),

        new Vector2 (1.0f, 0.5f),
        new Vector2 (.66f, 0.5f),
        new Vector2 (.66f, 1.0f)
    };
    static Vector3[] normals = new Vector3[]
    {
        // top
        new Vector3 (0f, 1f, 0f),
        new Vector3 (0f, 1f, 0f),
        new Vector3 (0f, 1f, 0f),

        // bottom
        new Vector3 (0f,-1f, 0f),
        new Vector3 (0f,-1f, 0f),
        new Vector3 (0f,-1f, 0f),

        // side R
        new Vector3 (-0.8660254f, 0f, 0.5f),
        new Vector3 (-0.8660254f, 0f, 0.5f),
        new Vector3 (-0.8660254f, 0f, 0.5f),

        new Vector3 (-0.8660254f, 0f, 0.5f),
        new Vector3 (-0.8660254f, 0f, 0.5f),
        new Vector3 (-0.8660254f, 0f, 0.5f),

        // side G
        new Vector3 (0f, 0f,-1f),
        new Vector3 (0f, 0f,-1f),
        new Vector3 (0f, 0f,-1f),

        new Vector3 (0f, 0f,-1f),
        new Vector3 (0f, 0f,-1f),
        new Vector3 (0f, 0f,-1f),
        
        // side B
        new Vector3 (0.8660254f, 0f, 0.5f),
        new Vector3 (0.8660254f, 0f, 0.5f),
        new Vector3 (0.8660254f, 0f, 0.5f),

        new Vector3 (0.8660254f, 0f, 0.5f),
        new Vector3 (0.8660254f, 0f, 0.5f),
        new Vector3 (0.8660254f, 0f, 0.5f),
    };
}

Unity:頂点法線の計算と事前設定の速度の違い

2週間前に、Unityのパフォーマンス測定の差先端について調べた記事を書きました。
simplestar-tech.hatenablog.com

ここで調べた手法で、頂点生成系のプログラムのいくつかの負荷試験を行ってみます。
タイトルの通り、頂点法線の計算をランタイムで行うのと、事前に設定するのとでどれくらいパフォーマンスが変わるのか見ていきたいとおもいます。

負荷が計測できる程度に前回の頂点生成で作る三角柱を並べます。(15 x 15 x 15 個、計3375)
f:id:simplestar_tech:20180922081047j:plain

処理はインスタンス生成とメッシュ作成に分けましたが、それぞれで高負荷が発生しました。
まずはインスタンス生成時のタイムラインを確認してみました。
f:id:simplestar_tech:20180922081503j:plain
CreateInstance という Unity 側に予約されている部分が大部分を占め、続いて mesh オブジェクト作成と GCAlloc にも犯罪的な処理時間を要していました。

メッシュ生成のタイムラインを確認すると次の通り
f:id:simplestar_tech:20180922081616j:plain
mesh に頂点を設定、インデックスを設定、UVを設定、メッシュを割り当てといったことに、ちりも積もれば山となるという感じで、短い計算が重なって全体的な処理時間が大きくなるという結果でした。
頂点法線の計算はほとんど時間を要していないことがわかります。

こういうことがわかるのは本当、プロファイラの良いところだと思います。

タイトルの通り、RecalculateNormals を使うか、そのまま設定するかでどのように変化するかを示して終わりたいと思います。

まずは RecalculateNormals を使う場合 PlayerLoop は total 72.97 ms 要していました。
ここで頂点法線を事前に決めて与えるように修正しました。

set normals に 9ms ほど要していました。PlayerLoop は total 72.60 ms 要しています。
ほとんど変化していないですね。

次の記事で、インスタンス化する数を減らす工夫をしてみます。

Unity:頂点生成による三角柱

頂点カラーシェーダーのテスト用にメッシュオブジェクトを用意します。

ちょうど1年前に六角柱を作りましたが、やはり最も頂点数の少ない三角柱から作ろうと思います。
simplestar-tech.hatenablog.com

もう既に書式は習っているので、知っていることを利用してコードに起こしてみます。

カタカタ…

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

[RequireComponent(typeof(MeshRenderer))]
[RequireComponent(typeof(MeshFilter))]
public class Prism : MonoBehaviour
{

    [SerializeField]
    private Material _mat;

    const float root3 = 1.732051f;

    // Use this for initialization
    void Start()
    {
        var mesh = new Mesh();

        Vector3[] positions = new Vector3[] {
            // top
            new Vector3 (0f, 1f, 0f),
            new Vector3 (1f, 1f, -root3),
            new Vector3 (-1f, 1f, -root3),
            // bottom
            new Vector3 (0f, 0f, 0f),
            new Vector3 (-1f, 0f, -root3),
            new Vector3 (1f, 0f, -root3),
        };
        int[] vertIndices = new int[]
        {
            0, 1, 2,
            3, 4, 5,
            0, 2, 4,
            4, 3, 0,
            2, 1, 5,
            5, 4, 2,
            1, 0, 3,
            3, 5, 1
        };
        Vector3[] vertices = new Vector3[vertIndices.Length];
        for (int i = 0; i < vertIndices.Length; i++)
        {
            vertices[i] = positions[vertIndices[i]];
        }
        mesh.vertices = vertices;

        int[] triangles = new int[mesh.vertices.Length];
        for (int i = 0; i < mesh.vertices.Length; i++)
        {
            triangles[i] = i;
        }
        mesh.triangles = triangles;

        Vector2[] uvSources = new Vector2[]
        {
            // 天
            new Vector2 (.25f, 0.5f),
            new Vector2 (0.5f, 0.0f),
            new Vector2 (0.0f, 0.0f),

            // 底
            new Vector2 (.75f, 0.0f),
            new Vector2 (0.5f, 0.5f),
            new Vector2 (1.0f, 0.5f),

            // side R
            new Vector2 (0.0f, 1.0f),
            new Vector2 (.33f, 1.0f),
            new Vector2 (.33f, 0.5f),

            new Vector2 (.33f, 0.5f),
            new Vector2 (0.0f, 0.5f),
            new Vector2 (0.0f, 1.0f),

            // side G
            new Vector2 (.33f, 1.0f),
            new Vector2 (.66f, 1.0f),
            new Vector2 (.66f, 0.5f),

            new Vector2 (.66f, 0.5f),
            new Vector2 (.33f, 0.5f),
            new Vector2 (.33f, 1.0f),
            
            // side B
            new Vector2 (.66f, 1.0f),
            new Vector2 (1.0f, 1.0f),
            new Vector2 (1.0f, 0.5f),

            new Vector2 (1.0f, 0.5f),
            new Vector2 (.66f, 0.5f),
            new Vector2 (.66f, 1.0f)
        };
        Vector2[] uvs = new Vector2[uvSources.Length];
        for (int i = 0; i < uvSources.Length; i++)
        {
            uvs[i] = uvSources[i];
        }
        mesh.uv = uvs;

        mesh.RecalculateNormals();

        var filter = GetComponent<MeshFilter>();
        filter.sharedMesh = mesh;

        var renderer = GetComponent<MeshRenderer>();
        renderer.material = _mat;

    }

    // Update is called once per frame
    void Update()
    {

    }
}

ジャン!
f:id:simplestar_tech:20180920090443j:plain

思いついていることがいくつかあるので、次の記事で試していきます。