夜風のMixedReality

xRと出会って変わった人生と出会った技術を書き残すためのGeekなHoloRangerの居場所

UnityでShaderを勉強する その⑰ ジオメトリシェーダー

前回からMRTKのHandTriangleShaderの中身を読み解いていました。

頂点シェーダーを読み解いて、次へ進む前にジオメトリシェーダーに関して勉強していなかったため、先にジオメトリシェーダーを学んでいきます。

〇ジオメトリシェーダーとは?

頂点シェーダーはオブジェクトの頂点を、フラグメントシェーダーはピクセルを扱いました。

ジオメトリシェーダーは頂点シェーダーのフラグメントシェーダーの間で処理されるもので、頂点シェーダーからいくつかの頂点を受け取り、ポリゴン単位で処理を行えます。

〇ジオメトリシェーダーの例

Shader "Custom/Geometry/FlatShading"
{
    Properties
    {
        _Color("Color", Color) = (1,1,1,1)
        _MainTex("Albedo", 2D) = "white" {}
    }
    
    SubShader
    {
 
        Tags{ "Queue"="Geometry" "RenderType"= "Opaque" "LightMode" = "ForwardBase" }
 
        Pass
        {
            CGPROGRAM
 
            #include "UnityCG.cginc"
            #pragma vertex vert
            #pragma geometry geom
            #pragma fragment frag
 
            float4 _Color;
            sampler2D _MainTex;
 
            struct v2g
            {
                float4 pos : SV_POSITION;
                float2 uv : TEXCOORD0;
                float3 vertex : TEXCOORD1;
            };
 
            struct g2f
            {
                float4 pos : SV_POSITION;
                float2 uv : TEXCOORD0;
                float light : TEXCOORD1;
            };
 
            v2g vert(appdata_full v)
            {
                v2g o;
                o.vertex = v.vertex;
                o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
                o.uv = v.texcoord;
                return o;
            }
 
            [maxvertexcount(3)]
            void geom(triangle v2g IN[3], inout TriangleStream<g2f> triStream)
            {
                g2f o;
 
                // Compute the normal
                float3 vecA = IN[1].vertex - IN[0].vertex;
                float3 vecB = IN[2].vertex - IN[0].vertex;
                float3 normal = cross(vecA, vecB);
                normal = normalize(mul(normal, (float3x3) unity_WorldToObject));
 
                // Compute diffuse light
                float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
                o.light = max(0., dot(normal, lightDir));
 
                // Compute barycentric uv
                o.uv = (IN[0].uv + IN[1].uv + IN[2].uv) / 3;
 
                for(int i = 0; i < 3; i++)
                {
                    o.pos = IN[i].pos;
                    triStream.Append(o);
                }
            }
 
            half4 frag(g2f i) : COLOR
            {
                float4 col = tex2D(_MainTex, i.uv);
                col.rgb *= i.light * _Color;
                return col;
            }
 
            ENDCG
        }
    }
    Fallback "Diffuse"
}

〇shaderLabシンタックスを読み解く

Shader "Custom/Geometry/FlatShading"
{
    Properties
    {
        _Color("Color", Color) = (1,1,1,1)
        _MainTex("Albedo", 2D) = "white" {}
    }
    
    SubShader
    {
 
        Tags{ "Queue"="Geometry" "RenderType"= "Opaque" "LightMode" = "ForwardBase" }
 
        Pass
        {
            CGPROGRAM
 
            ENDCG
        }
    }
    Fallback "Diffuse"
}
●Properties
    Properties
    {
        _Color("Color", Color) = (1,1,1,1)
        _MainTex("Albedo", 2D) = "white" {}
    }

ここではマテリアル側で使用するプロパティが記述されます。

●SubShader
    SubShader
    {
 
        Tags{ "Queue"="Geometry" "RenderType"= "Opaque" "LightMode" = "ForwardBase" }
 
        Pass
        {
            CGPROGRAM
 
            ENDCG
        }
    }
    Fallback "Diffuse"
}

Tagsでレンダリングキューをジオメトリ(=2000)、レンダリングタイプをOpaque(=不透明)、レンダリングをForwardBaseで行うという意味です。

ここでForwardBaseをもっと掘り下げると、オブジェクトを1つのピクセルライティングと、頂点ライティング、SHライティングによってレンダリングする。というレンダリングでこれは、頂点のみでライティングを行うためレンダリングが光速になる処理のようです。

docs.unity3d.com

esprog.hatenablog.com

 では次はジオメトリシェーダーの処理を見ていきます。

   #include "UnityCG.cginc"
            #pragma vertex vert
            #pragma geometry geom
            #pragma fragment frag

            float4 _Color;
            sampler2D _MainTex;

            struct v2g
            {
                float4 pos : SV_POSITION;
                float2 uv : TEXCOORD0;
                float3 vertex : TEXCOORD1;
            };

            struct g2f
            {
                float4 pos : SV_POSITION;
                float2 uv : TEXCOORD0;
                float light : TEXCOORD1;
            };

            v2g vert(appdata_full v)
            {
                v2g o;
                o.vertex = v.vertex;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = v.texcoord;
                return o;
            }

            [maxvertexcount(3)]
            void geom(triangle v2g IN[3], inout TriangleStream<g2f> triStream)
            {
                g2f o;

                // Compute the normal
                float3 vecA = IN[1].vertex - IN[0].vertex;
                float3 vecB = IN[2].vertex - IN[0].vertex;
                float3 normal = cross(vecA, vecB);
                normal = normalize(mul(normal, (float3x3) unity_WorldToObject));

                // Compute diffuse light
                float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
                o.light = max(0., dot(normal, lightDir));

                // Compute barycentric uv
                o.uv = (IN[0].uv + IN[1].uv + IN[2].uv) / 3;

                for (int i = 0; i < 3; i++)
                {
                    o.pos = IN[i].pos;
                    triStream.Append(o);
                }
            }

            half4 frag(g2f i) : COLOR
            {
                float4 col = tex2D(_MainTex, i.uv);
                col.rgb *= i.light * _Color;
                return col;
            }
            #include "UnityCG.cginc"
            #pragma vertex vert
            #pragma geometry geom
            #pragma fragment frag

頂点シェーダーやフラグメントシェーダー同様

 #pragma geometry geom

で geomという関数名でジオメトリシェーダーを使用することを宣言しています。

●v2g構造体
           struct v2g
            {
                float4 pos : SV_POSITION;
                float2 uv : TEXCOORD0;
                float3 vertex : TEXCOORD1;
            };

これはジオメトリシェーダーの引数として使用される構造体です。

頂点シェーダーなどで見たものと同様でそれぞれの値がどのようにして使用されるかの目的が記述されています。

v2gはVertex to geometryという意味でしょうか

●g2f構造体
   struct g2f
            {
                float4 pos : SV_POSITION;
                float2 uv : TEXCOORD0;
                float light : TEXCOORD1;
            };

これはフラグメントシェーダーの引数として使用される構造体です。

v2g構造体同様それぞれの値がどのようにして使用されるかの目的が記述されています。

〇 頂点シェーダー

 v2g vert(appdata_full v)
            {
                v2g o;
                o.vertex = v.vertex;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = v.texcoord;
                return o;
            }
  v2g vert(appdata_full v)
            {
                v2g o;

                return o;
            }

はappdata_fullをvとして持ってきて処理をしてv2gに値を返すよ!という意味になります。

                o.vertex = v.vertex;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = v.texcoord;

v2gのvertex(=TEXCOORD1=二つ目のUV座標)にappdata_fullのvertex(=頂点の位置)を代入しています。

このappdata_fullはUnityCG_cgincにて以下のようになっています。

struct appdata_full {
    float4 vertex : POSITION;
    float4 tangent : TANGENT;
    float3 normal : NORMAL;
    float4 texcoord : TEXCOORD0;
    float4 texcoord1 : TEXCOORD1;
    float4 texcoord2 : TEXCOORD2;
    float4 texcoord3 : TEXCOORD3;
#if defined(SHADER_API_XBOX360)
    half4 texcoord4 : TEXCOORD4;
    half4 texcoord5 : TEXCOORD5;
#endif
    fixed4 color : COLOR;
};

次にv2gのpos(=システム上の座標)にカメラから見た座標に変換した頂点の位置を代入しています。

v2gのuv(=一つ目のUV座標)にappdata_fullで宣言されている一つ目のUV座標を代入しています。

v2gはこれらの値を受け取ります。

以上が頂点シェーダーです。

〇ジオメトリシェーダー

            [maxvertexcount(3)]
            void geom(triangle v2g IN[3], inout TriangleStream<g2f> triStream)
            {
                g2f o;

                // Compute the normal
                float3 vecA = IN[1].vertex - IN[0].vertex;
                float3 vecB = IN[2].vertex - IN[0].vertex;
                float3 normal = cross(vecA, vecB);
                normal = normalize(mul(normal, (float3x3) unity_WorldToObject));

                // Compute diffuse light
                float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
                o.light = max(0., dot(normal, lightDir));

                // Compute barycentric uv
                o.uv = (IN[0].uv + IN[1].uv + IN[2].uv) / 3;

                for (int i = 0; i < 3; i++)
                {
                    o.pos = IN[i].pos;
                    triStream.Append(o);
                }
            }
            [maxvertexcount(3)]
            void geom(triangle v2g IN[3], inout TriangleStream<g2f> triStream){}

ジオメトリシェーダーの関数自体にアトリビュート(属性)のようなものが付けられています。

冒頭でも記述しましたが、ジオメトリシェーダーはポリゴン単位での処理を行います。この[maxcertexcount()]は頂点シェーダーからいくつの頂点を受け取るかを指定します。

 ここでは [maxvertexcount(3)]のため頂点シェーダーから最大3つの頂点を受け取り三角ポリゴンとしてoutputするよ!という意味になります。

 同様にtriangle v2g IN[3]が三角形でv2gから3つの頂点をINしますという意味になります。

 もう少し掘り下げると

 [maxvertexcount(3)]
            void ShaderName(PrimitiveType DataType Name [ NumElements ], inout StreamOutputObject){}

がジオメトリシェーダーの定型の書き方で今回の場合v2gから文字列3のTrianglesリスト作ってStreamOutputObjectに渡してねという意味になるようです。

このStreamOutputObjectというものは出力数るオブジェクトのタイプになります。3つほど定義がありポリゴンとして出力するのか、ラインとして出力するのか、点として出力するのかを指定します。

今回の場合g2fにポリゴンとして出力することを意味します。

●法線の計算

頂点シェーダーでも法線をできますが、頂点シェーダーはあくまで頂点単体の処理なのでポリゴンの法線を求めることができません。

ジオメトリシェーダーではポリゴン単位で扱うことができるためポリゴン自体の法線を求めることができます。

   // Compute the normal
                float3 vecA = IN[1].vertex - IN[0].vertex;
                float3 vecB = IN[2].vertex - IN[0].vertex;
                float3 normal = cross(vecA, vecB);
                normal = normalize(mul(normal, (float3x3) unity_WorldToObject));

vecA,Bはそれぞれ2,3番目の頂点から最初の頂点のベクトルを引いています。

これを外積したものをワールド座標に変換し正規化したものがベクトルの法線になります。

●拡散光の計算

   // Compute diffuse light
                float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
                o.light = max(0., dot(normal, lightDir));

ここでは拡散光(光の反射)の計算を行っています。

  lightDirはライトの方向を意味します。ここではワールド座標のライト(DirectionalLight)の位置を正規化しています。

 g2fのLight(=2番目のUV座標)に法線をlightDir乗した値が代入されます。 もしもこの値がない場合0が代入されます。

●重心UVの計算
  // Compute barycentric uv
 o.uv = (IN[0].uv + IN[1].uv + IN[2].uv) / 3;

                for (int i = 0; i < 3; i++)
                {
                    o.pos = IN[i].pos;
                    triStream.Append(o);
                }

g2fのuv(=最初のUV座標)にポリゴンの3頂点の座標を3で割った数が代入されます。

 これはそのポリゴンの重心を意味します。

mathwords.net

                for (int i = 0; i < 3; i++)
                {
                    o.pos = IN[i].pos;
                    triStream.Append(o);
                }

for構文は処理をループさせるものです。

ここでは頂点の数=3回実行されます。

g2fのpos(=システム上の座標)にそれぞれの頂点のベクトルが代入されます。

triStream.Append(o)

でそれぞれの処理をメッシュとして生成しています。

以上がジオメトリシェーダーでやっている処理です。

ジオメトリシェーダーで行っている処理をまとめると

頂点のそれぞれのベクトルからポリゴンの法線を計算 拡散光を計算 ポリゴンの重心を求めg2fにそれぞれの頂点ベクトルを代入しメッシュとして渡す

といった処理が行われています。

〇フラグメントシェーダー

           half4 frag(g2f i) : COLOR
            {
                float4 col = tex2D(_MainTex, i.uv);
                col.rgb *= i.light * _Color;
                return col;
            }

最後にフラグメントシェーダーで処理を行っています。

セマンティクスでCOLORが指定されているためこのフラグメントシェーダーはCOLORを書き出します。

halfはFloat、Fixed同様浮動小数点を指します。なぜ異なる浮動小数点があるのか?どうやって使い分けるのかですが、これは扱う数値の精度が関係してきます。

docs.unity3d.com

今回はざっくりとみていくので深堀はしません。

              float4 col = tex2D(_MainTex, i.uv);
                col.rgb *= i.light * _Color;

colはMainTexを指します。g2fのlight(=ジオメトリシェーダーで計算した拡散光)とColorをかけたものがカラーとして返されます。

以上がフラグメントシェーダーでした。

〇このShaderの処理をざっくりとまとめる。

①頂点シェーダーでオブジェクトの頂点をカメラのクリップ座標に変換する

②ジオメトリシェーダーで3つの頂点からポリゴンを作成し、ポリゴン自体の法線を計算

③ジオメトリシェーダーでポリゴン単位でuv座標を扱うことを処理

④ジオメトリシェーダーで処理された通りポリゴン単位のuvでにテクスチャを貼り付け

⑤出力

といった処理がこのShaderで行われています。