【Unity】【シェーダ】オブジェクトを描画した後に別のシェーダを使って追加描画する

Unityのシェーダでオブジェクトを描画した後に追加描画する方法です。

Unity2019.3.0

やりたいこと

いま、以下のようにオブジェクトが一つレンダリングされているとします。

f:id:halya_11:20200224212852p:plain:w450
オブジェクトを描画

ここで、このオブジェクトのリム部分に色を付けることを考えます。

f:id:halya_11:20200224213138p:plain:w450
リムの色を追加

ただリムに色を付けるだけなら元のシェーダを少し書き足せばいいだけですが、
本記事では一時的な演出用に使うことを想定して、元のシェーダはいじらずに追加描画でこれを実現します。

シェーダ

まず追加描画に使うシェーダを書きます。

Shader "Example"
{
    SubShader
    {
        Tags { "RenderType"="Opaque" }

        Pass
        {
            Blend SrcAlpha OneMinusSrcAlpha
            ColorMask RGB

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

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

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float3 viewDir : TEXCOORD1;
                float3 normal : TEXCOORD3;
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
                o.viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
                o.normal = UnityObjectToWorldNormal(v.normal);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                i.viewDir = normalize(i.viewDir);
                i.normal = normalize(i.normal);
                half vdotn = pow(1.0 - max(0, dot(i.normal, i.viewDir)), 3);
                return fixed4(1, 0, 0, vdotn);
            }
            ENDCG
        }
    }
}

単純にリムを赤くするだけのシェーダです。
ブレンドBlend SrcAlpha OneMinusSrcAlphaとしてアルファブレンド設定にしていることに注意してください。

スクリプト

さて次にオブジェクトを描画した後にこのシェーダを使って追加で描画を行うためのスクリプトを書きます。

方針としては、レンダラにマテリアルを追加することも考えられますが、
メッシュがサブメッシュに分割されている場合にうまくいかないので今回は以下のようにGraphics.DrawMeshNow()を使います。

using UnityEngine;

public class Example : MonoBehaviour
{
    [SerializeField]
    private Material _material;

    private MeshFilter _meshFilter;
    
    private void Start()
    {
        _meshFilter = GetComponent<MeshFilter>();
    }

    // OnRenderObjectで描画
    private void OnRenderObject()
    {
        if (_material == null) {
            return;
        }
        
        // セットパスしてからDrawMeshNowすればこのタイミングで描画できる
        _material.SetPass(0);
        Graphics.DrawMeshNow(_meshFilter.sharedMesh, transform.position, transform.rotation);
    }
}

オブジェクト描画後のタイミングはOnRenderObject()でフックできるので、ここで描画を行っています。

docs.unity3d.com

このイベントでGraphics.DrawMeshNow()を使って追加描画処理を行っています。
Graphics.DrawMeshNow()の前にMaterial.SetPass()で使いたいマテリアルとパスのインデックスを指定することを忘れないようにしてください。

SkinnedMeshRendererにも対応する

次にSkinnedMeshRendererにも対応してみます。
SkinnedMeshRendererに対応するにはスクリプトを以下のように書き換えます。

using UnityEngine;

public class Example : MonoBehaviour
{
    [SerializeField]
    private Material _material;

    private MeshFilter _meshFilter;
    private SkinnedMeshRenderer _skinnedMeshRenderer;
    private Mesh _mesh;
    
    private void Start()
    {
        _mesh = new Mesh();
        _meshFilter = GetComponent<MeshFilter>();
        _skinnedMeshRenderer = GetComponent<SkinnedMeshRenderer>();
    }

    private void OnRenderObject()
    {
        if (_material == null) {
            return;
        }
        if (_meshFilter != null) {
            _mesh = _meshFilter.sharedMesh;
        }
        else {
            _skinnedMeshRenderer.BakeMesh(_mesh);
        }
        
        _material.SetPass(0);
        Graphics.DrawMeshNow(_mesh, transform.position, transform.rotation);
    }
}

SkinnedMeshRendererはアニメーションするため、描画前にSkinnedMeshRenderer.BakeMesh()で今の頂点情報をベイクする必要があります。

レンダリング結果

このスクリプトを適当なGameObjectにアタッチし、そこにこのシェーダをアサインしたマテリアルをアサインします。
すると以下のような結果が得られます。

f:id:halya_11:20200224213138p:plain:w450
レンダリング結果

正常に追加描画されていることが確認できました。

参考

docs.unity3d.com