サーフェイスシェーダーライティングの例
頂点シェーダーとフラグメントシェーダーのプログラミング

DX11 / OpenGL コアテッセレーションのサーフェースシェーダー

Surface Shader は DirectX 11 / OpenGL Core GPU テッセレーションを一部サポートします。具体的には以下の通りです。

  • テッセレーションは tessellate:FunctionName 修飾子により指定されます。この関数は三角形の辺とテッセレーション要素の内部を計算します。
  • テッセレーションが使用されている場合、“vertex modifier” (vertex:FunctionName) がテッセレーションの 後に、ドメインシェーダーの中のおのおの生成された頂点に対して、実行されます。通常は Displacement マッピングです。
  • サーフェイスシェーダーは任意で phong tessellation を計算し、モデルの表面を、Displacement マッピングなしに、均等にスムージングします。

現時点でのテッセレーションのサポートに関する制約

  • 三角形ドメインのみサポートしており、四角形や等値線テッセレーションはサポートしていません。
  • テッセレーションが使用されている場合、シェーダーは自動的にシェーダーモデル 5.0 ターゲットにコンパイルされ、すなわち DX11/12、OpenGL Core、PS4/XB1 でしか動作しません。

GPU テッセレーションなし、頂点モディファイアに Displacement あり

初めに、テッセレーション なし で Displacement マッピングを行うサーフェイスシェーダーを見ていきます。頂点を法線に沿って、Displacement マップから与えられた量に基づき移動します。

    Shader "Tessellation Sample" {
        Properties {
            _MainTex ("Base (RGB)", 2D) = "white" {}
            _DispTex ("Disp Texture", 2D) = "gray" {}
            _NormalMap ("Normalmap", 2D) = "bump" {}
            _Displacement ("Displacement", Range(0, 1.0)) = 0.3
            _Color ("Color", color) = (1,1,1,0)
            _SpecColor ("Spec color", color) = (0.5,0.5,0.5,0.5)
        }
        SubShader {
            Tags { "RenderType"="Opaque" }
            LOD 300
            
            CGPROGRAM
            #pragma surface surf BlinnPhong addshadow fullforwardshadows vertex:disp nolightmap
            #pragma target 4.6

            struct appdata {
                float4 vertex : POSITION;
                float4 tangent : TANGENT;
                float3 normal : NORMAL;
                float2 texcoord : TEXCOORD0;
            };

            sampler2D _DispTex;
            float _Displacement;

            void disp (inout appdata v)
            {
                float d = tex2Dlod(_DispTex, float4(v.texcoord.xy,0,0)).r * _Displacement;
                v.vertex.xyz += v.normal * d;
            }

            struct Input {
                float2 uv_MainTex;
            };

            sampler2D _MainTex;
            sampler2D _NormalMap;
            fixed4 _Color;

            void surf (Input IN, inout SurfaceOutput o) {
                half4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
                o.Albedo = c.rgb;
                o.Specular = 0.2;
                o.Gloss = 1.0;
                o.Normal = UnpackNormal(tex2D(_NormalMap, IN.uv_MainTex));
            }
            ENDCG
        }
        FallBack "Diffuse"
    }

上のシェーダーは比較的標準的です。注目すべき点は以下の通りです。

  • disp 頂点モディファイアは Displacment マップをサンプリングし、頂点を法線に沿って移動します。
  • カスタムの“頂点データ入力” 構造( appdata )を、デフォルトの appdata_full の代わりに、使用します。これはまだ必要ありませんが、テッセレーションでできるかぎり小さい構造を使用するのに効果的です。
  • 頂点データは二番目の UV 座標がないため、nolightmap ディレクティブを追加してライトマップを除外します。

次はシンプルなオブジェクトがこのシェーダーを用いてどのように映るか示します。

固定量のテッセレーション

固定量のテッセレーション、例えばメッシュ全体で同じテッセレーションレベル、を追加してみます。このアプローチは、モデルの面が画面でもおおよそ同じ大きである場合に適切です。テッセレーションレベルを、カメラからの距離に基づいて、別のスクリプトでコードから変更できます。

    Shader "Tessellation Sample" {
        Properties {
            _Tess ("Tessellation", Range(1,32)) = 4
            _MainTex ("Base (RGB)", 2D) = "white" {}
            _DispTex ("Disp Texture", 2D) = "gray" {}
            _NormalMap ("Normalmap", 2D) = "bump" {}
            _Displacement ("Displacement", Range(0, 1.0)) = 0.3
            _Color ("Color", color) = (1,1,1,0)
            _SpecColor ("Spec color", color) = (0.5,0.5,0.5,0.5)
        }
        SubShader {
            Tags { "RenderType"="Opaque" }
            LOD 300
            
            CGPROGRAM
            #pragma surface surf BlinnPhong addshadow fullforwardshadows vertex:disp tessellate:tessFixed nolightmap
            #pragma target 4.6

            struct appdata {
                float4 vertex : POSITION;
                float4 tangent : TANGENT;
                float3 normal : NORMAL;
                float2 texcoord : TEXCOORD0;
            };

            float _Tess;

            float4 tessFixed()
            {
                return _Tess;
            }

            sampler2D _DispTex;
            float _Displacement;

            void disp (inout appdata v)
            {
                float d = tex2Dlod(_DispTex, float4(v.texcoord.xy,0,0)).r * _Displacement;
                v.vertex.xyz += v.normal * d;
            }

            struct Input {
                float2 uv_MainTex;
            };

            sampler2D _MainTex;
            sampler2D _NormalMap;
            fixed4 _Color;

            void surf (Input IN, inout SurfaceOutput o) {
                half4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
                o.Albedo = c.rgb;
                o.Specular = 0.2;
                o.Gloss = 1.0;
                o.Normal = UnpackNormal(tex2D(_NormalMap, IN.uv_MainTex));
            }
            ENDCG
        }
        FallBack "Diffuse"
    }

シェーダーのテッセレーション関数、tessFixed は 4 つのテッセレーション要素をひとつの float4 の値として戻します。三角形の辺ごとに 3 つの要素、そして 1 つの要素で三角形の内部です。次に、マテリアルプロパティーに設定する定数値を戻します。

距離にもとづいたテッセレーション

カメラからの距離にもとづいてテッセレーションのレベルを変更することができます。例えば、テッセレーションが最大になる距離(例えば 10 メートル)、そしてテッセレーションが徐々に減少していくまでの距離(例えば 20 メートル)の二つの距離の値を定義するとします。

    Shader "Tessellation Sample" {
        Properties {
            _Tess ("Tessellation", Range(1,32)) = 4
            _MainTex ("Base (RGB)", 2D) = "white" {}
            _DispTex ("Disp Texture", 2D) = "gray" {}
            _NormalMap ("Normalmap", 2D) = "bump" {}
            _Displacement ("Displacement", Range(0, 1.0)) = 0.3
            _Color ("Color", color) = (1,1,1,0)
            _SpecColor ("Spec color", color) = (0.5,0.5,0.5,0.5)
        }
        SubShader {
            Tags { "RenderType"="Opaque" }
            LOD 300
            
            CGPROGRAM
            #pragma surface surf BlinnPhong addshadow fullforwardshadows vertex:disp tessellate:tessDistance nolightmap
            #pragma target 4.6
            #include "Tessellation.cginc"

            struct appdata {
                float4 vertex : POSITION;
                float4 tangent : TANGENT;
                float3 normal : NORMAL;
                float2 texcoord : TEXCOORD0;
            };

            float _Tess;

            float4 tessDistance (appdata v0, appdata v1, appdata v2) {
                float minDist = 10.0;
                float maxDist = 25.0;
                return UnityDistanceBasedTess(v0.vertex, v1.vertex, v2.vertex, minDist, maxDist, _Tess);
            }

            sampler2D _DispTex;
            float _Displacement;

            void disp (inout appdata v)
            {
                float d = tex2Dlod(_DispTex, float4(v.texcoord.xy,0,0)).r * _Displacement;
                v.vertex.xyz += v.normal * d;
            }

            struct Input {
                float2 uv_MainTex;
            };

            sampler2D _MainTex;
            sampler2D _NormalMap;
            fixed4 _Color;

            void surf (Input IN, inout SurfaceOutput o) {
                half4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
                o.Albedo = c.rgb;
                o.Specular = 0.2;
                o.Gloss = 1.0;
                o.Normal = UnpackNormal(tex2D(_NormalMap, IN.uv_MainTex));
            }
            ENDCG
        }
        FallBack "Diffuse"
    }

ここでテッセレーション関数は 3 つの引数を持ちます。テッセレーション前の、3 つの三角形の角の頂点データです。これは、頂点の位置に依存するとここで仮定したテッセレーションレベルを計算するのに必要です。内蔵のヘルパーファイル Tessellation.cginc をインクルードして UnityDistanceBasedTess 関数を呼び出し、そこで必要な処理が行われます。関数は頂点からカメラまでの距離を計算し、最終的なテッセレーション要素を算出します。

辺の長さにもとづいたテッセレーション

純粋に距離にもとづいたテッセレーションは三角形のサイズが比較的同じである場合にのみ有効です。上の画像で、小さな三角形をもつオブジェクトのテッセレーションが多すぎであり、大きな三角形をもつオブジェクトはテッセレーションが少なすぎることが判ります。

その代わりに、テッセレーションレベルは画面上の三角形の辺の長さにもとづいて計算することができます、辺が長いほど、大きなテッセレーション要素が適用されます。

    Shader "Tessellation Sample" {
        Properties {
            _EdgeLength ("Edge length", Range(2,50)) = 15
            _MainTex ("Base (RGB)", 2D) = "white" {}
            _DispTex ("Disp Texture", 2D) = "gray" {}
            _NormalMap ("Normalmap", 2D) = "bump" {}
            _Displacement ("Displacement", Range(0, 1.0)) = 0.3
            _Color ("Color", color) = (1,1,1,0)
            _SpecColor ("Spec color", color) = (0.5,0.5,0.5,0.5)
        }
        SubShader {
            Tags { "RenderType"="Opaque" }
            LOD 300
            
            CGPROGRAM
            #pragma surface surf BlinnPhong addshadow fullforwardshadows vertex:disp tessellate:tessEdge nolightmap
            #pragma target 4.6
            #include "Tessellation.cginc"

            struct appdata {
                float4 vertex : POSITION;
                float4 tangent : TANGENT;
                float3 normal : NORMAL;
                float2 texcoord : TEXCOORD0;
            };

            float _EdgeLength;

            float4 tessEdge (appdata v0, appdata v1, appdata v2)
            {
                return UnityEdgeLengthBasedTess (v0.vertex, v1.vertex, v2.vertex, _EdgeLength);
            }

            sampler2D _DispTex;
            float _Displacement;

            void disp (inout appdata v)
            {
                float d = tex2Dlod(_DispTex, float4(v.texcoord.xy,0,0)).r * _Displacement;
                v.vertex.xyz += v.normal * d;
            }

            struct Input {
                float2 uv_MainTex;
            };

            sampler2D _MainTex;
            sampler2D _NormalMap;
            fixed4 _Color;

            void surf (Input IN, inout SurfaceOutput o) {
                half4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
                o.Albedo = c.rgb;
                o.Specular = 0.2;
                o.Gloss = 1.0;
                o.Normal = UnpackNormal(tex2D(_NormalMap, IN.uv_MainTex));
            }
            ENDCG
        }
        FallBack "Diffuse"
    }

ここではまた、UnityEdgeLengthBasedTess 関数を Tessellation.cginc ファイルから呼び出すだけで必要な処理を行わせます。

パフォーマンス上の理由から、UnityEdgeLengthBasedTessCull を呼ぶことが推奨され、Patch Frustum カリングが行われます。これによりシェーダーがより高価になりますが、カメラレビュー外にあるメッシュ部分の GPU 処理を軽減します。

Phong テッセレーション

Phong Tessellation は、結果となる表面がメッシュの法線にある程度沿うように、再分割( subdivide )された面の位置を修正します。ローポリのメッシュについてスムージングするのにかなり効果的な方法です。

Unity のサーフェイスシェーダーは、tessphong:VariableName ディレクティブを使用することで、Phong テッセレーションを自動計算します。次にシェーダーでの例を示します。

    Shader "Phong Tessellation" {
        Properties {
            _EdgeLength ("Edge length", Range(2,50)) = 5
            _Phong ("Phong Strengh", Range(0,1)) = 0.5
            _MainTex ("Base (RGB)", 2D) = "white" {}
            _Color ("Color", color) = (1,1,1,0)
        }
        SubShader {
            Tags { "RenderType"="Opaque" }
            LOD 300
            
            CGPROGRAM
            #pragma surface surf Lambert vertex:dispNone tessellate:tessEdge tessphong:_Phong nolightmap
            #include "Tessellation.cginc"

            struct appdata {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float2 texcoord : TEXCOORD0;
            };

            void dispNone (inout appdata v) { }

            float _Phong;
            float _EdgeLength;

            float4 tessEdge (appdata v0, appdata v1, appdata v2)
            {
                return UnityEdgeLengthBasedTess (v0.vertex, v1.vertex, v2.vertex, _EdgeLength);
            }

            struct Input {
                float2 uv_MainTex;
            };

            fixed4 _Color;
            sampler2D _MainTex;

            void surf (Input IN, inout SurfaceOutput o) {
                half4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
                o.Albedo = c.rgb;
                o.Alpha = c.a;
            }

            ENDCG
        }
        FallBack "Diffuse"
    }

次に通常のシェーダー(上列)と Phong テッセレーションを使用したシェーダー(下列)の比較です。Displacement マッピングなしでも 表面はより丸くなることがわかります。

サーフェイスシェーダーライティングの例
頂点シェーダーとフラグメントシェーダーのプログラミング