ぷるぷるの雑記

低レイヤーがんばるぞいなブログ. 記事のご利用は自己責任で.

Unityでディスプレイスメントシェーダーを実装する

タイトルの通りUnityでディスプレイスメントシェーダーを実装しました.

シェーダースクリプト

Shader "Custom/Displace"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _DisplaceMap ("DisplaceMap", 2D) = "white" {}
        _OffsetStrength("OffsetStrength", Range(0,10)) = 1
        _NormalMap("NormalMap", 2D) = "white" {}
        _Spec("SpacularStrength", Range(0,10)) = 1
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

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

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
                float3 viewDir: TEXCOORD1;
                float3 lightDir: TEXCOORD2;
            };

            sampler2D _MainTex;
            sampler2D _DisplaceMap;
            sampler2D _NormalMap;
            float _OffsetStrength;
            float _Spec;

        

            v2f vert (appdata v)
            {
                v2f o;
                // VS内で使えるテクスチャサンプリング関数
                float4 _offset = tex2Dlod(_DisplaceMap, float4(v.uv, 0, 0));

                // rgbaをサンプリングしたのちrチャネルを抽出
                // 画像が4チャネルでもグレースケールならr=g=b
                float offset = _offset.r;

                // モデル空間での法線に沿って頂点を変形させる
                v.vertex += float4(
                    v.normal * offset * _OffsetStrength,
                    0
                );

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

                // ライトとビューから頂点へのベクトルをオブジェクト空間にしたのち
                // タンジェントスペースに変換する
                TANGENT_SPACE_ROTATION;
                o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex));
                o.viewDir = mul(rotation, ObjSpaceViewDir(v.vertex));

                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                // 正規化
                i.lightDir = normalize(i.lightDir);
                i.viewDir = normalize(i.viewDir);
                // sample Normal in Tangent Space
                float3 normal = UnpackNormal(
                    tex2D(_NormalMap, i.uv)
                );
                normal = normalize(normal);

                float3 halfDir = normalize(i.lightDir + i.viewDir);
                float4 diff = saturate(
                    dot(normal, i.lightDir)
                );

                float3 spec = pow(
                    max(0, dot(normal, halfDir)),
                    _Spec * 128.0
                );

                
                // sample Albedo
                float4 tex = tex2D(_MainTex, i.uv);
                 
                // add diffuse and specular effects
                fixed4 col;
                col.rgb = tex.rgb * diff + spec*tex.rgb;
                return col;
            }
            ENDCG
        }
    }
}

なお、拡散反射と鏡面反射の部分は下記のサイトを参考にしました.

blog.applibot.co.jp

頂点テクスチャフェッチ(VTF)とは

通常テクスチャのサンプリングはフラグメントシェーダー内で行いますが、バーテックスシェーダー内でサンプリングを行うことを頂点テクスチャフェッチ(Vertex Texture Fetch)というらしいです. 特別な手続きは特に必要なく、VTF用の関数を用いるだけです.

// VS内で使えるテクスチャサンプリング関数
float4 _offset = tex2Dlod(_DisplaceMap, float4(v.uv, 0, 0));

tex2D() ではなく tex2Dlod() を利用することだけ覚えておきましょう.

ここで注意なのがFS内で使用するtex2D()とは異なり、 uv座標をfloat4で指定します .

docs.microsoft.com

docs.microsoft.com

Standardシェーダーとの比較

上記の自作シェーダーとUnityにデフォルトで備わっているStandardシェーダーをオブジェクトに適用してその見栄えを比較してみましょう.

テクスチャにはambientCGのTiles 107を利用しました.

ambientcg.com

公平な比較をするために、自作シェーダーで対応しているAlbedo、Normal Map、Height Mapの3テクスチャを適用した場合の比較になります. またStandardシェーダーのテクスチャ以外の値はデフォルトです.

自作シェーダー

Standardシェーダー

思いのほか自作シェーダーもよい感じですね. しかし、自作シェーダーとなるといずれ光の表現などに限界を感じることでしょう.

Standard Assetsのシェーダーを使おう

さて、これまでディスプレイスメントシェーダーを自作してきたわけですが、大きな問題が2つあります.

  1. ディスプレイスメントしたことによる法線の再計算をしていない
  2. ポリゴンの頂点数が少ないときにうまくディスプレイスメント出来ない

1に関してはNormal Mapを使ったりすればよいですし、再計算を実装することもそこまで難しくはありません. 2に関してもUnityにはテッセレーションに関する便利な機能があります. しかしながら、これらの機能を盛り込んだディスプレイスメントシェーダーがStandard Assets内に存在するので、特に理由がない限りはこれを使うべきでしょう.

Standard Assetsのディスプレイスメントシェーダー

assetstore.unity.com

参考

docs.unity3d.com

tsumikiseisaku.com

www.wwwmaplesyrup-cs6.work