【Unity】Geometry ShaderでGrass Shader(草)を作ったので紹介する

どーも、ぐるたか@guru_takaです。

前から興味があったGrass Shader(草シェーダー)を作ってみたので、ソースと簡単な解説を紹介します!わりとリアルな元気モリモリの雑草が生えました笑

Geometry Shaderの技術を活用しています。そのため、初見の方は当ブログの下記事を一読すると理解しやすいです。こちらもぜひ参考にしてみて下さい!
【Unity】Geometry Shader(ジオメトリシェーダー)の超入門サンプル【初心者向け】

ソース

ソースはこちらです!

Shader "Grass"
{
    Properties
    {
        //頂点の色
        _TopColor("Top Color", Color) = (1.0, 1.0, 1.0, 1.0)
        //根本の色
        _BottomColor("Bottom Color", Color) = (0.0, 0.0, 0.0, 1.0)
        //VFX用の高さのテクスチャ
        _HeightMap ("HeightMap", 2D) = "white" {}
        //VFX用の幅のテクスチャ
        _WidthMap ("WidthMap", 2D) = "white" {}
        //VFX用の風の向きのテクスチャ
        _WindDistortionMap("Wind Distortion Map", 2D) = "white" {}
        //高さの基準値
        _Height("Height", Range(0., 20)) = 10
        //幅の基準値
        _Width("Width", Range(0., 5)) = 1
        //高さの比率@bottom, middle, high
        _HeightRate ("HeightRate", Vector) = (0.3,0.4,0.5,0)
        //幅の比率@bottom, middle, high
        _WidthRate ("WidthRate", Vector) = (0.5,0.4,0.25,0)
        //風の揺れ率@bottom, middle, high
        _WindPowerRate ("WidthPowerRate", Vector) = (0.3, 1.0, 2.0, 0)
        //風の強さ
        _WindPower("WindPower", Range(0., 10.0)) = 2.0
        //風が吹く周期
        _WindFrequency("WindFrequency'", Range(0., 0.1)) = 0.05
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        Cull Off

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma geometry geom

            #include "UnityCG.cginc"

            struct v2g
            {
                float4 pos : SV_POSITION;
                float3 normal : NORMAL;
                float2 uv : TEXCOORD0;
                float2 height : TEXCOORD1;
                float2 width : TEXCOORD2;
            };

            struct g2f
            {
                float4 pos : SV_POSITION;
                fixed4 col : COLOR;
            };

            // https://forum.unity.com/threads/am-i-over-complicating-this-random-function.454887/#post-2949326
            float rand(float3 co)
            {
                return frac(sin(dot(co.xyz, float3(12.9898, 78.233, 53.539))) * 43758.5453);
            }

            sampler2D _HeightMap, _WidthMap,_WindDistortionMap;
            float4 _WindDistortionMap_ST;
            float4 _TopColor, _BottomColor, _HeightRate, _WidthRate, _WindPowerRate;
            float _Height, _Width,_WindPower,_WindFrequency;

            v2g vert (appdata_base v)
            {
                v2g o;
                float4 uv = float4(v.texcoord.xy, 0.0f, 0.0f);
                o.pos = v.vertex;
                o.uv = v.texcoord.xy;
                o.normal = v.normal;

                //VFX
                o.height = tex2Dlod(_HeightMap,uv);
                o.width = tex2Dlod(_WidthMap,uv);
                return o;
            }


            [maxvertexcount(7)]
            void geom(triangle v2g IN[3], inout TriangleStream<g2f> tristream)
            {

                float4 pos0 = IN[0].pos;
                float4 pos1 = IN[1].pos;
                float4 pos2 = IN[2].pos;


                float3 nor0 = IN[0].normal;
                float3 nor1 = IN[1].normal;
                float3 nor2 = IN[2].normal;

                //入力された三角メッシュの頂点座標の平均値
                float4 centerPos = (pos0 + pos1 + pos2 ) / 3;
                //法線ベクトルの平均値
                float4 centerNor = float4( (nor0 + nor1 + nor2).xyz / 3, 1.0f);

                // VFX用の高さ、幅の調整
                float height = (IN[0].height.r + IN[1].height.r + IN[2].height.r) / 3.0f;
                float width = (IN[0].width.r + IN[1].width.r + IN[2].width.r) / 3.0f;

                //草の向き
                float4 dir = float4(normalize(pos2 * rand(pos2)- pos0 * rand(pos1)).xyz * width, 1.0f);

                //風向きマッピング用のテクスチャ
                //tilitn, offsetを加味したuv座標+uvスクロール
                //xz平面がuv座標に対応
                float2 uv = pos0.xz * _WindDistortionMap_ST.xy  + _WindDistortionMap_ST.zw  + _WindFrequency * _Time.y;
                //風向きはRG情報@パーリンノイズ
		        float2 windDir_xy = (tex2Dlod(_WindDistortionMap, float4(uv, 0, 0)).xy * 2 - 1) * _WindPower;
                float4 wind = float4(windDir_xy, 0,0);

                g2f o[7];

                //bottom
                o[0].pos = centerPos - dir * _Width * _WidthRate.x;
                o[0].col = _BottomColor;

                o[1].pos = centerPos + dir * _Width * _WidthRate.x;
                o[1].col = _BottomColor;

                //bottom2middle
                o[2].pos = centerPos - dir * _Width * _WidthRate.y + centerNor * height * _Height * _HeightRate.x;
                o[2].col = lerp(_BottomColor, _TopColor, 0.33333f);

                o[3].pos = centerPos + dir * _Width * _WidthRate.y + centerNor * height * _Height * _HeightRate.x;
                o[3].col = lerp(_BottomColor, _TopColor, 0.33333f);

                //middley2high
                o[4].pos = o[3].pos - dir * _Width * _WidthRate.z + centerNor * height * _Height * _HeightRate.y;
                o[4].col = lerp(_BottomColor, _TopColor, 0.6666f);

                o[5].pos = o[3].pos + dir * _Width * _WidthRate.z + centerNor * height * _Height * _HeightRate.y;
                o[5].col = lerp(_BottomColor, _TopColor, 0.6666f);

                //top
                o[6].pos = o[5].pos + centerNor * height * _Height * _HeightRate.z;
                o[6].col = _TopColor;

                // wind
                o[2].pos += wind * _WindPowerRate.x;
                o[3].pos += wind * _WindPowerRate.x;
                o[4].pos += wind * _WindPowerRate.y;
                o[5].pos += wind * _WindPowerRate.y;
                o[6].pos += wind * _WindPowerRate.z;

                [unroll]
                for (int i = 0; i < 7; i++) {
                    o[i].pos = UnityObjectToClipPos(o[i].pos);
                    tristream.Append(o[i]);
                }
         //unrollの解説は以下リンクをみると参考になると思います。
         //https://wlog.flatlib.jp/item/1012
         //http://blog.livedoor.jp/akinow/archives/52404331.html

            }

            fixed4 frag (g2f i) : SV_Target
            {
                fixed4 col = i.col;
                return col;
            }
            ENDCG
        }
    }
}

以下、2つの記事を参考にさせて頂きました。
参考 Unity Graphics Programming vol.1Indie Visual Lab 参考 Grass Shaderroystan.net

使用したテクスチャ(ノイズ系)はこちらです。

HeightMap

WidthMap

WindDistortionMap

ザックリした解説

簡単にShaderの中身を解説します。

Geometry Shaderの中身

草の形状は以下のような7つの頂点で構成されています。「Unity Graphics Programming vol.1」を参考に少しイジりました。

頂点4,5における中点のx座標は、頂点3のx座標に該当します。また、頂点6は頂点5のx座標から傾く方向にずらしているので、多少曲がってます。

            //middley2high
                o[4].pos = o[3].pos - dir * _Width * _WidthRate.z + centerNor * height * _Height * _HeightRate.y;
                o[4].col = lerp(_BottomColor, _TopColor, 0.6666f);

                o[5].pos = o[3].pos + dir * _Width * _WidthRate.z + centerNor * height * _Height * _HeightRate.y;
                o[5].col = lerp(_BottomColor, _TopColor, 0.6666f);


                //top
                o[6].pos = o[5].pos + centerNor * height * _Height * _HeightRate.z;
                o[6].col = _TopColor;

草の高低差の仕組み

頂点テクスチャフェッチ(VTF)で、草の高低差や幅の違いを表現しています。詳細はこちらの記事をご覧ください。説明も引用させて頂きました!

頂点テクスチャフェッチとは、簡潔に言うなら[ 頂点シェーダ内でテクスチャを参照すること ]を指します。

参考 頂点テクスチャフェッチ(VTF)wgld.org

テクスチャの色情報を渡すことで、頂点シェーダー(ジオメトリシェーダーも!)で色々できます。便利!!!

//VFX
o.height = tex2Dlod(_HeightMap,uv);
o.width = tex2Dlod(_WidthMap,uv);

Qiitaに、テクスチャ情報から頂点を凸凹する超簡単なサンプルを過去に書いたので、ご興味ある方はこちらも是非チェックしてみてください!
参考 【Unity】シェーダーで面を凸凹(デコボコ)する方法【Tessellation・Displacement】Qiita

MEMO
使用したテクスチャはパーリンノイズとfBmノイズになります。

風の仕組み

風向きはVFXを活用。パーリンノイズのテクスチャのRG情報を風向きとして扱っています。

//風向きはRG情報@パーリンノイズ
float2 windDir_xy = (tex2Dlod(_WindDistortionMap, float4(uv, 0, 0)).xy * 2 - 1) * _WindPower;
float4 wind = float4(windDir_xy, 0,0);

続いて、以下のソースについて、簡単に解説します。

float2 uv = pos0.xz * _WindDistortionMap_ST.xy  + _WindDistortionMap_ST.zw  + _WindFrequency * _Time.y;

生成されたメッシュ(草)のuv座標は0~1です。つまり、何もせずにVFXしてしまうと、同じ風向きになってしまいます


(まるで軍隊のような動き)

これでは困ります。草全面に対し、まんべんなくVFXしたい!

そのため、平面のxz座標(ここでは、pos0.xzを使用)と、風向き用テクスチャのTiling_WindDistortionMap_ST.xyを使って、uv座標を変更しました。

ちなみに、_WindDistortionMap_STは、Textureのtilingやoffset情報を取得するために使います。

テクスチャの名前 + _STをつけたfloat4のuniformを定義することでそのTextureのtilingやoffset情報を取ってくることができます
引用:Unity でShaderの勉強 その1

このTilingを小数点以下まで有効な値にすると、あらゆる草が風でなびくようになります。

MEMO
最初に掲載したGIFではTilingが0.01になってます。

この数値は1.1でもOKですが、1.0だとダメです。

あくまで仮説ですが、pos0.xyが整数値のため、整数値分だけズレると、uv座標内では同じものを取得することになってしまい、草全てが同じ動きをしてしまうからかもしれません。

※2019/6.25追記
上記のような複雑なことをしなくても、uvの平均値を用いれば動きました!

こちらの方がシンプルですし直感的にわかりやすいので、良いです笑

//uv座標
                
float2 uv0 = IN[0].uv;
float2 uv1 = IN[1].uv;
float2 uv2 = IN[2].uv;
float2 centerUv = (uv2 + uv1 + uv0) / 3.0f;
float2 uv = centerUv  + _WindDistortionMap_ST.zw  + _WindFrequency * _Time.y;

最後に

以上です。最後にplaneオブジェクトにアタッチすれば草がはえます。大量のplaneを並べれば、大草原!!!

ぜひ風になびく草をご堪能してみてください!

コメントを残す