凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

Unity で特定のモデルのみ Image Effect が効かないようにする

はじめに

キャラクタの顔だけ SSAO が効かないようにしたいがどうしたら良いか、という相談を受けたので、いくつか方法を考えてみました。

  • 顔のモデル描画時にステンシルを設定しておき、Image Effect のタイミングで特定の値がセットされている場所だけ無視する
  • CommandBuffer を使い、CameraEvent.AfterImageEffects のタイミングで顔のモデルを描画

前者だと SSAO の Image Effect が更新されるたびにメンテが必要になってしまうと思い、まずは後者の方法を相談時にやってみたのですが、影の描画の後になってしまうため影が反映されない点など幾つか問題点が合ったため諦めました。そこで、うまく行った前者を紹介したいと思います(やってみて分かりましたがメンテもそんなに大変じゃないと思います)。

サンプル

デモ

こんな感じになります。

f:id:hecomi:20161223231155p:plain

一番右のモデルだけ SSAO が効かないようになっています。

特定のモデルだけ効かない Image Effect を作る

テラシュールブログさんでも解説されているように、ステンシルを使うと面白いマスク表現が色々とできます。

tsubakit1.hateblo.jp

まずは具体的に、特定の領域だけ無視する Image Effect をどうやって作るのか見ていきましょう。

ステンシルを書き込むシェーダ

Stencil ブロックをサーフェスシェーダなら SubShader 内に、頂点・フラグメントシェーダなら Pass ブロックの中に書き込みます。

Stencil
{
    Ref [_StencilMask]
    Comp Always
    Pass Replace
}

具体的に全体のコードを見ていきましょう。ここでは Standard Surface Shader を元に作成してみます。Project ウィンドウから「Create > Shader > Standard Surface Shader」で作成したシェーダに Stencil ブロックを追加します。

Shader "Custom/Skin" 
{

Properties 
{
    _Color ("Color", Color) = (1,1,1,1)
    _MainTex ("Albedo (RGB)", 2D) = "white" {}
    _Glossiness ("Smoothness", Range(0,1)) = 0.5
    _Metallic ("Metallic", Range(0,1)) = 0.0
    _StencilMask ("Stencil Mask", int) = 1
}

SubShader 
{
    Tags { "RenderType"="Opaque" }
    LOD 200

    Stencil
    {
        Ref [_StencilMask]
        Comp Always
        Pass Replace
    }
    
    CGPROGRAM

    #pragma surface surf Standard fullforwardshadows
    #pragma target 3.0

    sampler2D _MainTex;

    struct Input 
    {
        float2 uv_MainTex;
    };

    half _Glossiness;
    half _Metallic;
    fixed4 _Color;

    void surf(Input IN, inout SurfaceOutputStandard o) 
    {
        fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
        o.Albedo = c.rgb;
        o.Metallic = _Metallic;
        o.Smoothness = _Glossiness;
        o.Alpha = c.a;
    }

    ENDCG
}

FallBack "Diffuse"

}

プロパティにしてマスク値を変更できるようにしています。

特定のマスク値を無視する Image Effect

次に特定のマスク値を無視する Image Effect を書いてみます。まずはシェーダから見ていきましょう。Project ウィンドウから「Create > Shader > Image Effect Shader」で作成したシェーダに Stencil のブロックを追加しています。シェーダは色を反転するものになっています。

Shader "Hidden/ImageEffectExceptForSkins"
{

Properties
{
    _MainTex ("Texture", 2D) = "white" {}
    _StencilMask ("Stencil Mask", int) = 1
}

SubShader
{

Cull Off ZWrite Off ZTest Always

Pass
{
    Stencil
    {
        Ref [_StencilMask]
        Comp NotEqual
    }

    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;
    };

    v2f vert (appdata v)
    {
        v2f o;
        o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
        o.uv = v.uv;
        return o;
    }
    
    sampler2D _MainTex;

    fixed4 frag (v2f i) : SV_Target
    {
        fixed4 col = tex2D(_MainTex, i.uv);
        col = 1 - col;
        return col;
    }

    ENDCG
}

}

}

Comp NotEqualRef で指定したステンシルマスク値が書かれていない場所だけ実行されるようにしています(逆に Equal にすると、ここにだけ適用されるようになります)。次にこれを利用するスクリプトを書いてカメラにアタッチします。

using UnityEngine;

[RequireComponent(typeof(Camera))]
[ExecuteInEditMode]
public class ImageEffectExceptForSkins : MonoBehaviour 
{
    [SerializeField] int stencilMask = 1;
    Material material_;

    const string shaderName = "Hidden/ImageEffectExceptForSkins";

    [ImageEffectOpaque]
    void OnRenderImage(RenderTexture src, RenderTexture dst)
    {
        if (material_ == null) {
            var shader = Shader.Find(shaderName);
            material_ = new Material(shader);
        }
        material_.SetInt("_StencilMask", stencilMask);
        Graphics.Blit(src, dst, material_, 0);
    }
}

実行結果

実行してみると以下のようになります。

f:id:hecomi:20161223231913p:plain

一番右のモデルだけマスク値を書き込むシェーダを利用したマテリアルで描画しています。他の場所は色が反転されています。

SSAO の Image Effect に適用してみる

基本は、以上見てきた通りです。対象のオブジェクトの描画時にステンシルマスク値を書き込むようにしておき、Image Effect でそのマスク値が書き込まれている場所を無視しています。

では SSAO にこれを適用してみましょう。Post-Processing Stack では Forward レンダリングで SSAO が未だ使えないため、Cinematic Image Effect に含まれる SSAO を使うことにします。

ステンシルマスク値を設定できるように、いくつかスクリプトを改造します。

Settings.cs

stencilMask 変数を追加します。

[SerializeField]
[Tooltip("Stencil mask value, where the effect is not applied.")]
public int stencilMask;

AmbientOcclusion.cs

追加した stencilMask 変数をシェーダに受け渡すようにします。

int stencilMask
{
    get { return settings.stencilMask; }
}

void UpdateMaterialProperties()
{
    var m = aoMaterial;
    m.SetFloat("_Intensity", intensity);
    m.SetFloat("_Radius", radius);
    m.SetFloat("_TargetScale", downsampling ? 0.5f : 1);
    m.SetInt("_SampleCount", sampleCountValue);
    m.SetInt("_StencilMask", stencilMask); // <-- 追加
}

AmbientOcclusionEditor.cs

インスペクタから stencilMask を設定できるようにします。

using UnityEngine;
using UnityEditor;

namespace UnityStandardAssets.CinematicEffects
{
    [CanEditMultipleObjects]
    [CustomEditor(typeof(AmbientOcclusion))]
    public class AmbientOcclusionEditor : Editor
    {
        ...
        SerializedProperty _stencilMask;
        ...

        void OnEnable()
        {
            ...
            _stencilMask = serializedObject.FindProperty("settings.stencilMask");
        }

        public override void OnInspectorGUI()
        {
            ...
            EditorGUILayout.PropertyField(_stencilMask);
            ...
        }
    }
}

AmbientOcclusion.shader

最後にシェーダで設定されたステンシルマスク値の領域を無視するようにします。

Shader "Hidden/Image Effects/Cinematic/AmbientOcclusion"
{
    Properties
    {
        ...
        _StencilMask ("Stencil Mask", int) = 1
    }
    SubShader
    {
        Pass
        {
            ZTest Always Cull Off ZWrite Off
            Stencil
            {
                Ref [_StencilMask]
                Comp NotEqual
            }
            CGPROGRAM
            #define SOURCE_DEPTH 1
            #include "AmbientOcclusion.cginc"
            #pragma vertex vert_img
            #pragma fragment frag_ao
            #pragma target 3.0
            ENDCG
        }
        // ... 以下同じように Stencil ブロックを全ての Pass に追加
    }
}

これで以下のようになります。

f:id:hecomi:20161223231155p:plain

おわりに

Non-PBR な表現のゲームではこういったこだわりが重要になってくると思いますので、是非必要になった際は参考にしてくださいませ。