🌈

そろそろShaderをやるパート62 アウトラインの表現

2022/03/19に公開

そろそろShaderをやります

そろそろShaderをやります。そろそろShaderをやりたいからです。
パート100までダラダラ頑張ります。10年かかってもいいのでやります。
100記事分くらい学べば私レベルの初心者でもまあまあ理解できるかなと思っています。

という感じでやってます。

※初心者がメモレベルで記録するので
 技術記事としてはお力になれないかもしれません。

下準備

下記参考
そろそろShaderをやるパート1 Unite 2017の動画を見る(基礎知識~フラグメントシェーダーで色を変える)

デモ

アウトラインを描画するShaderのデモです。

Shaderサンプル

Shader "Custom/OutLine"
{
    Properties
    {
        _Color("MainColor",Color) = (0,0,0,0)
        _OutlineWidth ("Outline width", Range (0.005, 0.03)) = 0.01
        [HDR]_OutlineColor ("Outline Color", Color) = (0,0,0,1)
        [Toggle(USE_VERTEX_EXPANSION)] _UseVertexExpansion("Use vertex for Outline", int) = 0
    }

    SubShader
    {
        //普通の塗りつぶし
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            struct v2f
            {
                float4 pos : SV_POSITION;
            };

            half4 _Color;

            v2f vert(appdata_full v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                return o;
            }

            half4 frag(v2f i) : COLOR
            {
                return _Color;
            }
            ENDCG
        }

        //アウトラインを描画
        Pass
        {
            Cull Front

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma shader_feature USE_VERTEX_EXPANSION
            #include "UnityCG.cginc"

            float _OutlineWidth;
            float4 _OutlineColor;

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

            struct v2f
            {
                float4 pos:SV_POSITION;
            };

            //頂点シェーダー
            v2f vert(appdata v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                float3 n = 0;

                #ifdef USE_VERTEX_EXPANSION //モデルの頂点方向に拡大するパターン

                //モデルの原点からみた各頂点の位置ベクトルを計算
                float3 dir = normalize(v.vertex.xyz);
                //UNITY_MATRIX_IT_MVはモデルビュー行列の逆行列の転置行列
                //各頂点の位置ベクトルをモデル座標系からビュー座標系に変換し正規化
                n = normalize(mul((float3x3)UNITY_MATRIX_IT_MV, dir));
                
                #else //モデルの法線方向に拡大するパターン

                //法線をモデル座標系からビュー座標系に変換し正規化
                n = normalize(mul((float3x3)UNITY_MATRIX_IT_MV, v.normal));

                #endif

                //ビュー座標系に変換した法線を投影座標系に変換 
                //アウトラインとして描画予定であるピクセルのXY方向のオフセット
                float2 offset = TransformViewToProjection(n.xy);
                o.pos.xy += offset * _OutlineWidth;
                return o;
            }

            //フラグメントシェーダー
            fixed4 frag(v2f i) : SV_Target
            {
                return _OutlineColor;
            }
            ENDCG
        }
    }
}

Shader内の座標系については下記リンクで解説しています。
【参考リンク】:そろそろShaderをやるパート61 ビューイングパイプラインについて整理する

アウトラインの描画ロジック

アウトラインの描画ロジックは単純です。

アウトライン以外の箇所の描画処理を1つ目のパスで書きます。
次に裏面のみ描画される少し大きめの領域を2つ目のパスで描画します。

これにより、はみ出た部分がアウトラインとして描画されます。

"少し大きめの領域"の実装手法についてですが、以下の2パターンを切り替えることができるようにしています。

・法線方向への拡大
・モデル座標系を原点とした場合の各頂点の座標(ローカル座標)方向への拡大

以下がその箇所です。

#ifdef USE_VERTEX_EXPANSION //モデルの頂点方向に拡大するパターン

//モデルの原点からみた各頂点の位置ベクトルを計算
float3 dir = normalize(v.vertex.xyz);
//UNITY_MATRIX_IT_MVはモデルビュー行列の逆行列の転置行列
//各頂点の位置ベクトルをモデル座標系からビュー座標系に変換し正規化
n = normalize(mul((float3x3)UNITY_MATRIX_IT_MV, dir));

#else //モデルの法線方向に拡大するパターン

//法線をモデル座標系からビュー座標系に変換し正規化
n = normalize(mul((float3x3)UNITY_MATRIX_IT_MV, v.normal));

#endif

法線方向への拡大だと、Cubeのような形状の場合にアウトラインが破綻します。

各頂点のローカル座標方向への拡大だと、きれいにアウトラインを描画することができます。

この流れだと一見、各頂点のローカル座標方向への拡大のみで良いような気もしますが、
法線が多いモデルや原点がモデルの中心でない場合など、条件が変われば法線方向への拡大の方が見栄えが良いです。

トゥーン調の影の表現との組み合わせ

トゥーン調の影の表現と組み合わせたデモです。
一番右のShaderが該当します。

Shaderサンプル

まずはトゥーン調の影の表現です。
解説は過去記事でまとめています。
【参考リンク】:そろそろShaderをやるパート60 トゥーン調の影の表現

Shader "Custom/ToonLit"
{
    Properties
    {
        _MainTexture ("Main Texture", 2D) = "white" {}
        _ShadowTexture ("Shadow Texture", 2D) = "white" {}
        _ShadowStrength("Shadow Strength",Range(0,1)) = 0.5
    }

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

            sampler2D _MainTexture;
            sampler2D _ShadowTexture;
            float _ShadowStrength;

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

            struct v2f
            {
                float4 pos:SV_POSITION;
                float3 worldNormal:TEXCOORD0;
                float2 uv : TEXCOORD1;
            };

            //頂点シェーダー
            v2f vert(appdata v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                //法線方向のベクトル
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                o.uv = v.uv;
                return o;
            }

            //フラグメントシェーダー
            fixed4 frag(v2f i) : SV_Target
            {
                //1つ目のライトのベクトルを正規化
                float3 l = normalize(_WorldSpaceLightPos0.xyz);
                //ワールド座標系の法線を正規化
                float3 n = normalize(i.worldNormal);
                //内積でLerpの補間値を計算 0以下の場合のみ補間値を利用する
                float interpolation = step(dot(n, l),0);
                //絶対値で正数にすることで影の領域を塗分ける
                float2 absD = abs(dot(n, l));
                //影の領域のテクスチャをサンプリング
                float3 shadowColor = tex2D(_ShadowTexture, absD).rgb;
                //メインのテクスチャをサンプリング
                float3 mainColor = tex2D(_MainTexture, i.uv).rgb;
                //補間値を用いて色を塗分け 影の強さ(影テクスチャーの濃さ)もここで調整
                float3 finalColor = lerp(mainColor, shadowColor * (1 - _ShadowStrength) * mainColor,interpolation);
                return float4(finalColor,1);
            }
            ENDCG
        }
    }
}

このShaderのポイントはName "TOON"と記した箇所です。
Name ○○とすることで他のShaderでパスを利用することができるようになります。

実際に利用している側のShaderが以下です。

Shader "Custom/ToonOutLine"
{
    Properties
    {
        _MainTexture ("Main Texture", 2D) = "white" {}
        _ShadowTexture ("Shadow Texture", 2D) = "white" {}
        _ShadowStrength("Shadow Strength",Range(0,1)) = 0.5
        _OutlineWidth ("Outline width", Range (0.005, 0.03)) = 0.01
        [HDR]_OutlineColor ("Outline Color", Color) = (0,0,0,1)
        [Toggle(USE_VERTEX_EXPANSION)] _UseVertexExpansion("Use vertex for Outline", int) = 0
    }

    SubShader
    {
        //他のShaderのパスを利用
        UsePass "Custom/ToonLit/TOON"
        Pass
        {
            Cull Front

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma shader_feature USE_VERTEX_EXPANSION
            #include "UnityCG.cginc"

            float _OutlineWidth;
            float4 _OutlineColor;

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

            struct v2f
            {
                float4 pos:SV_POSITION;
            };

            //頂点シェーダー
            v2f vert(appdata v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                float3 n = 0;

                #ifdef USE_VERTEX_EXPANSION //モデルの頂点方向に拡大するパターン
                
                //モデルの原点からみた各頂点の位置ベクトルを計算
                float3 dir = normalize(v.vertex.xyz);
                //UNITY_MATRIX_IT_MVはモデルビュー行列の逆行列の転置行列
                //各頂点の位置ベクトルをモデル座標系からビュー座標系に変換し正規化
                n = normalize(mul((float3x3)UNITY_MATRIX_IT_MV, dir));
                
                #else //モデルの法線方向に拡大するパターン
                
                //法線をモデル座標系からビュー座標系に変換し正規化
                n = normalize(mul((float3x3)UNITY_MATRIX_IT_MV, v.normal));
                
                #endif

                //ビュー座標系に変換した法線を投影座標系に変換 
                //アウトラインとして描画予定であるピクセルのXY方向のオフセット
                float2 offset = TransformViewToProjection(n.xy);
                o.pos.xy += offset * _OutlineWidth;
                return o;
            }

            //フラグメントシェーダー
            fixed4 frag(v2f i) : SV_Target
            {
                return _OutlineColor;
            }
            ENDCG
        }
    }
}

"UsePass Shaderの名前/Nameで定義した名前"とすることで、他のShader内でもパスを使用することができます。

注意点としては、Propertiesの値は利用される側で再定義する必要があることです。

参考リンク

Unityでシェーダー描いてみたい
[Unity] シェーダで使える定義済値

Discussion