Unityのシェーダーで頂点カラーを使ったディゾルブ


はじめに

Unreal Engineのビジュアルスクリプティングで試したものを、シェーダーを理解するためにUnityでも再現してみたという話です。

2020年7月のUE4 VFX Art Dive Onlineのスクエア・エニックスの林さんによる講演『ディゾブルマテリアルで表現する立体魔法陣』で、シェーダーに興味を持ちました。まず、講演の途中で出てきた作例そのままをUEで試したもの(講演だけでは分からないところは推測で調整したもの)が以下になります。

Unityのシェーダーにも興味があったのですが、せっかくだからコードでシェーダーを書く方法も勉強したいと思ったので、UEで作ったものをUnityで再現して勉強した記録がこの記事の内容です。なので、技術の解説ではなく、今回勉強したUnityのシェーダーの書き方の覚え書きであることをご了承ください。(技術面は林さんのご講演をご覧ください。)

使用するモデル

林さんの講演通りにHoudiniでモデルを作成しました。今回の処理に必要なのは、ディゾルブに使う頂点カラーとディスプレースメントに使用するNormal Vectorです。

単純なディゾルブ

Shader "Custom/Dissolve"
{
    Properties{
        _EmissionColor("Emission Color", Color) = (0.3, 0.7, 1, 1.0)
    }

        SubShader{
            Tags { "Queue" = "Transparent" }
            LOD 200

            CGPROGRAM
            #pragma surface surf Standard vertex:vert alpha:fade
            #pragma target 3.0

            half4 _EmissionColor;

        float TimeLoop(float time)
        {
            float TimeScale = 0.5;
            float Adjust = 1.5;
            float periodic = frac(time * TimeScale) * 2 - 1; //-1から1ののこぎり
            float tmp = abs( periodic ); //0から1の三角波
            //拡大し、拡大量の半分ずらす
            return  tmp * Adjust - (Adjust - 1) * 0.5; // -Adjust/2から1+Adjust/2の三角波
        }

        struct Input {
            float4 vertColor;
            float alpha;
        };

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

        void surf(Input IN, inout SurfaceOutputStandard o) {
            float EdgeWidth = 0.05;
            o.Alpha = 1 - step( TimeLoop(_Time.y) + EdgeWidth, IN.alpha );
            o.Emission = _EmissionColor * step(TimeLoop(_Time.y), IN.alpha);
        }


    ENDCG
    }
        FallBack "Diffuse"
}

メモ

  • プロパティを使うには、SubShaderでも変数の宣言が必要。floatは高精度浮動小数点だが、halfは中程度浮動小数点、fixedは低精度固定小数点。いろんな記事を参考にしたので、これらが混じっているが使い分けがどの程度必要かは不明。
  • タグ。"Queue"は描画順を表す。"RenderType"は何も指定していないが、問題ないのだろうか?
  • #pragma surface surf Standard vertex:vert alpha:fade の行について。Standardはライティングモデルオプションを表す。何を指定するかでsurfのoutputの種類が変わる。StandardならSurfaceOutputStandard。vertex:vertとalpha:fadeはオプションパラメータ。それぞれ、vertexシェーダーを使うこととfade-transparencyを使えるようにすることを伝えている。
  • vertexシェーダーからsurfaceシェーダーにデータを渡すときの構造体は自分で定義する。今回はInput。

ワールド座標を用いたスクロール

Shader "Custom/Position"
{
        SubShader{
            Tags { "Queue" = "Transparent" }
            LOD 200

            CGPROGRAM
            #pragma surface surf Standard alpha:fade
            #pragma target 3.0


        float TimeLoop()
        {
            float TimeScale = 0.5;
            float Adjust = 1.5;
            float periodic = frac(_Time.y * TimeScale) * 2 - 1; //-1から1ののこぎり
            float tmp = abs( periodic ); //0から1の三角波
            //拡大し、拡大量の半分ずらす
            return  tmp * Adjust - (Adjust - 1) * 0.5; // -Adjust/2から1+Adjust/2の三角波
        }

        struct Input {
            float3 worldPos;
        };


        void surf(Input IN, inout SurfaceOutputStandard o) {
            float Bounds = 1; 
            float position = IN.worldPos.x;
            float SStepWidth = 0.3;
            float max = (SStepWidth + 1) * TimeLoop();
            float min = max - SStepWidth;
            o.Alpha = smoothstep(min, max, (position/Bounds + 1) * 0.5);
        }


    ENDCG
    }
        FallBack "Diffuse"
}

メモ

  • ワールド座標を使う作例。InputでworldPosと書くだけで、ワールド座標が入っている?
  • のちの処理のためにスムースステップを使っている。

ディゾルブとスクロールを合わせる

Shader "Custom/DissolvePosition"
{
    Properties{
        _EmissionColor("Emission Color", Color) = (0.3, 0.7, 1, 1.0)
    }

        SubShader{
            Tags { "Queue" = "Transparent" }
            LOD 200

            CGPROGRAM
            #pragma surface surf Standard vertex:vert alpha:fade
            #pragma target 3.0

            half4 _EmissionColor;

    float TimeLoop()
    {
        float TimeScale = 0.1;
        float Adjust = 1.5;
        float periodic = frac(_Time.y * TimeScale) * 2 - 1; //-1から1ののこぎり
        float tmp = abs(periodic); //0から1の三角波
        //拡大し、拡大量の半分ずらす
        return  tmp * Adjust - (Adjust - 1) * 0.5; // -Adjust/2から1+Adjust/2の三角波
    }

        struct Input {
            float3 worldPos;
            float4 vertColor;
            float alpha;
        };

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

        void surf(Input IN, inout SurfaceOutputStandard o) {

            float Bounds = 3;
            float position = IN.worldPos.x;
            float SStepWidth = 0.3;
            float max = (SStepWidth + 1) * TimeLoop();
            float min = max - SStepWidth;

            float Adjust = 1.5;


            float EdgeWidth = 0.05;

            float inLoop = 1 -  (Adjust * (smoothstep(min, max, (position / Bounds + 1) * 0.5 )  - (Adjust - 1) * 0.5));



            o.Alpha = 1 - step(inLoop, IN.alpha);
            o.Emission = _EmissionColor * step(inLoop - EdgeWidth, IN.alpha);
        }


    ENDCG
    }
        FallBack "Diffuse"
}

メモ

  • 講演のノードでうまくいくのかよく分からなかったので、自分なりに変更しています。

さらにバンプ関数を作って、一方向のスクロールも作ります。

Shader "Custom/DissolvePosition2"
{
    Properties{
        _BaseColor("Base Color", Color) = (0.1, 0.12, 0.15, 1.0)
        _EmissionColor("Emission Color", Color) = (0.3, 0.7, 1, 1.0)
    }

        SubShader{
            Tags { "Queue" = "Transparent" }
            LOD 200

            CGPROGRAM
            #pragma surface surf Standard vertex:vert alpha:fade
            #pragma target 3.0

            half4 _EmissionColor;
    half4 _BaseColor;

    float TimeLoop()
    {
        float TimeScale = 0.15;
        float Adjust = 1.5;
        float periodic = frac(_Time.y * TimeScale); //0から1ののこぎり
        //拡大し、拡大量の半分ずらす
        return  periodic * Adjust - (Adjust - 1) * 0.5; // -Adjust/2から1+Adjust/2の三角波
    }

        struct Input {
            float3 worldPos;
            float4 vertColor;
            float alpha;
        };

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

        void surf(Input IN, inout SurfaceOutputStandard o) {

            float Bounds = 3;
            float position = IN.worldPos.x;


            //バンプ関数をつくる
            float SStepWidth1 = 0.3;
            float SStepWidth2 = 1;
            float SStepWidth3 = 0.3;


            float tmp = (SStepWidth1 + SStepWidth2 + SStepWidth3 + 1) * TimeLoop();
            float min1 = tmp - (SStepWidth1 + SStepWidth2 + SStepWidth3);
            float max1 = tmp - (SStepWidth2 + SStepWidth3);

            float min2 = tmp;
            float max2 = tmp - SStepWidth3;

            float Adjust = 1.5;

            float bump = saturate((smoothstep(min1, max1, (position / Bounds + 1) * 0.5))) * saturate((smoothstep(min2, max2, (position / Bounds + 1) * 0.5)));
            float inLoop = 1 -  (Adjust * bump  - (Adjust - 1) * 0.5);


            float EdgeWidth = 0.05;

            o.Albedo = _BaseColor;
![disolveposition2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/313474/2bdb30d8-bd98-bef2-b3db-d3cc3942c3e6.gif)
            o.Alpha = 1 - step(inLoop, IN.alpha);
            o.Emission = _EmissionColor * step(inLoop - EdgeWidth, IN.alpha);
        }


    ENDCG
    }
    FallBack "Diffuse"
}
  • TimeLoopの関数を三角波からのこぎり波に変更しています。

ディスプレースメント

講演の作例と同様にディスプレースメントもつけました。

Shader "Custom/Displace"
{
    Properties{
        _BaseColor("Base Color", Color) = (0.1, 0.12, 0.15, 1.0)
        _EmissionColor("Emission Color", Color) = (0.3, 0.7, 1, 1.0)
    }

        SubShader{
            Tags { "Queue" = "Transparent" }
            LOD 200

            Cull Off

            CGPROGRAM
            #pragma surface surf Standard vertex:vert alpha:fade
            #pragma target 3.0

            half4 _EmissionColor;
    half4 _BaseColor;

    float TimeLoop()
    {
        float TimeScale = 0.15;
        float Adjust = 1.5;
        float periodic = frac(_Time.y * TimeScale); //0から1ののこぎり
        //拡大し、拡大量の半分ずらす
        return  periodic * Adjust - (Adjust - 1) * 0.5; // -Adjust/2から1+Adjust/2の三角波
    }

    float BumpLoop(float position)
    {
        float Bounds = 3;

        //バンプ関数をつくる
        float SStepWidth1 = 0.3;
        float SStepWidth2 = 1;
        float SStepWidth3 = 0.3;


        float tmp = (SStepWidth1 + SStepWidth2 + SStepWidth3 + 1) * TimeLoop();
        float min1 = tmp - (SStepWidth1 + SStepWidth2 + SStepWidth3);
        float max1 = tmp - (SStepWidth2 + SStepWidth3);

        float min2 = tmp;
        float max2 = tmp - SStepWidth3;

        float Adjust = 1.5;

        float bump = saturate((smoothstep(min1, max1, (position / Bounds + 1) * 0.5))) * saturate((smoothstep(min2, max2, (position / Bounds + 1) * 0.5)));
        return 1 - (Adjust * bump - (Adjust - 1) * 0.5);
    }

        struct Input {
            float3 worldPos;
            float4 vertColor;
            float alpha;
        };

        void vert(inout appdata_full v, out Input o) {
            UNITY_INITIALIZE_OUTPUT(Input, o);
            o.vertColor = v.color;
            o.alpha = v.color.x;
            v.vertex.xyz += 0.05 * v.normal * (1 - saturate(BumpLoop(v.vertex.x) ));
        }

        void surf(Input IN, inout SurfaceOutputStandard o) {

![displace.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/313474/b6224be8-514b-3286-2c9c-bbedf314b54f.gif)

            float EdgeWidth = 0.05;

            o.Albedo = _BaseColor;
            o.Alpha = 1 - step(BumpLoop(IN.worldPos.x), IN.alpha);
            o.Emission = _EmissionColor * step(BumpLoop(IN.worldPos.x) - EdgeWidth, IN.alpha);
        }


    ENDCG
    }
        FallBack "Diffuse"
}

メモ

  • 両面表示するためにCull Offを追加した。
  • vertでもバンプ関数が必要になるため、surfの外で関数を定義しなおした。

おまけ

数式の処理を書く場合、ビジュアルスクリプティングよりコードの方が圧倒的にいいと思っていました。ノードからコードに書き換える作業をすることで、けっこう問題は複雑だと感じました。
ノードだとどの値が他の計算で使われているか一目でわかります。コードの場合、再利用したいものは変数として定義する必要があります。
例えば、ノードで出力がたくさん出ていれば、それを変数で定義すればコードが簡単に書けると判断できます。しかし、何回も使われるノードはけっこうあるので、2回以上使われているからといってすべて変数にしていては大変なことになります。
数式で書く利点は、ひとまとめの式として意味を把握しやすいことなので、逆に言えば、そのようにうまく途中式や関数を書く必要があります。
ビジュアルスクリプトは、簡単な数式でも書くのが面倒ですが、値が別の場所でどのように使われているかというのが一目で分かります。

ビジュアルスクリプティングはコードベースの書き方と比べて、多くの人が思っている以上に思想の違うものだと思います。そして、別物として正しく捉えることができれば、読みやすく使いやすいプログラムが書けるようになるのだと思います。そのための新しい捉え方を考えて、まとめたいと思っています。

参考サイト

ディゾブルマテリアルで表現する立体魔法陣
【Unityシェーダ入門】透明なシェーダを作る
【Unityシェーダ入門】シェーダで旗や水面をなびかせる
Shader(HLSL), 手続き的にテクスチャ生成など行うとき使用頻度の高い関数