【Unity】【シェーダ】ステンシルバッファでアウトラインを描画する

ステンシルバッファを使ってアウトラインを描画する方法を紹介します。

考え方

次のような考え方で実装します。

  1. モデルよりも一回り大きい範囲のステンシルバッファに書き込む
  2. モデルを通常通り描画し、ステンシルバッファに上記の1とは違う値を書き込む
  3. 上記の1で書き込んだ値のフラグメントのみ、アウトラインの色で描画する

f:id:halya_11:20180410194623p:plain

以前書いた下記の方法と似ていますが、最後にアウトラインを書き込むため、下の記事の課題2のような問題が起きません。

light11.hatenadiary.com

実装: 1. ステンシルバッファに書き込む

まずモデルより一回り大きい範囲のステンシルバッファに値を書き込みます。
今回は1を書き込んでいます。

Shader "StencilOutline" {
    Properties
    {
        _OutlineWidth ("Outline Width", float) = 0.1
    }
    SubShader
    {
        Tags { "RenderType"="Transparent" "Queue"="Transparent" }

        Pass
        {
            Stencil{
                Ref 1
                Comp always
                Pass replace
            }

            Cull Front
            ZWrite Off

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

            struct appdata
            {
                half4 vertex : POSITION;
                half3 normal : NORMAL;
            };

            struct v2f
            {
                half4 pos : SV_POSITION;
            };

            half _OutlineWidth;
            
            v2f vert (appdata v)
            {
                v2f o = (v2f)0;

                o.pos = UnityObjectToClipPos(v.vertex + v.normal * _OutlineWidth);

                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                return 0;
            }
            ENDCG
        }
    }
}

描画色は最終的に上書きされるので何でもいいです。

今回は描画順をZ値できっちり制御したいため、QueueをTransparentにしています。
(不透明パスだときっちり制御できません)

light11.hatenadiary.com

ここまでの描画結果は次のようになります。

f:id:halya_11:20180507005534p:plain

実装: 2. モデルを描画する

モデルを描画しつつステンシルバッファに2を書き込みます。
ライティングはシンプルなDiffuseのみです。

Shader "Diffuse" {
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
    }
    SubShader
    {
        Tags { "RenderType"="Transparent" "Queue"="Transparent+1" }

        ZWrite Off

        Pass
        {
            Stencil{
                Ref 2
                Comp always
                Pass replace
            }

            Tags { "LightMode"="ForwardBase" }

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

            struct appdata
            {
                half4 vertex : POSITION;
                half3 normal : NORMAL;
            };

            struct v2f
            {
                half4 pos : SV_POSITION;
                half3 normal: TEXCOORD1;
            };

            half4 _Color;
            half4 _LightColor0;
            
            v2f vert (appdata v)
            {
                v2f o = (v2f)0;

                o.pos = UnityObjectToClipPos(v.vertex);
                o.normal = UnityObjectToWorldNormal(v.normal);

                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                half3 diff = max(0, dot(i.normal, _WorldSpaceLightPos0.xyz)) * _LightColor0;

                fixed4 col;
                col.rgb = _Color * diff;
                return col;
            }
            ENDCG
        }
    }
}

ここまでの描画結果は次のようになります。

f:id:halya_11:20180507005640p:plain

実装: 3. アウトラインを描画する

最後にステンシルバッファが1のピクセルのみアウトライン色で描画します。

今回はポストエフェクトとして描画します。
ただしポストエフェクトからステンシルバッファを使うにはちょっと条件があるので下記の記事のとおりCommandBufferで実装します。

light11.hatenadiary.com

CommandBuffer登録用スクリプトの実装はこのようにします。
上の記事に色の変数を追加してマテリアルに登録する処理を書いただけです。

using UnityEngine;
using UnityEngine.Rendering;

[RequireComponent(typeof(Camera))]
public class StencilOutline : MonoBehaviour
{

    [SerializeField]
    private Shader _shader;
    [SerializeField]
    private Color _outlineColor;

    private void Awake()
    {
        Initialize();
    }

    private void Initialize()
    {
        var camera = GetComponent<Camera>();
        if (camera.allowMSAA || camera.allowHDR)
        {
            return;
        }
        var material = new Material(_shader);

        // アウトラインの色を適用
        material.SetColor("_OutlineColor", _outlineColor);

        // CommandBufferを登録
        var commandBuffer = new CommandBuffer();
        int tempTextureIdentifier = Shader.PropertyToID("_WorldPostEffectTempTexture");
        commandBuffer.GetTemporaryRT(tempTextureIdentifier, -1, -1);
        commandBuffer.Blit(BuiltinRenderTextureType.CurrentActive, tempTextureIdentifier);
        commandBuffer.Blit(tempTextureIdentifier, BuiltinRenderTextureType.CurrentActive, material);
        commandBuffer.ReleaseTemporaryRT(tempTextureIdentifier);
        camera.AddCommandBuffer(CameraEvent.BeforeImageEffects, commandBuffer);
    }
}

ポストエフェクトのシェーダは次のようにします。

Shader "StencilOutlinePostEffect"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _OutlineColor ("Color", Color) = (1, 1, 1, 1)
    }
    SubShader
    {
        Cull Off ZWrite Off ZTest Always
        
        Stencil{
            Ref 1
            Comp Equal
        }

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

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

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            half4 _OutlineColor;
            
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                return _OutlineColor;
            }
            ENDCG
        }
    }
}

結果

レンダリング結果は次のようになります。

f:id:halya_11:20180507011128p:plain

モデル同士が重なっても問題は起こりません。

f:id:halya_11:20180507011250p:plain

関連

light11.hatenadiary.com

light11.hatenadiary.com

light11.hatenadiary.com