はじめに
キャラクタの顔だけ SSAO が効かないようにしたいがどうしたら良いか、という相談を受けたので、いくつか方法を考えてみました。
- 顔のモデル描画時にステンシルを設定しておき、Image Effect のタイミングで特定の値がセットされている場所だけ無視する
CommandBuffer
を使い、CameraEvent.AfterImageEffects
のタイミングで顔のモデルを描画
前者だと SSAO の Image Effect が更新されるたびにメンテが必要になってしまうと思い、まずは後者の方法を相談時にやってみたのですが、影の描画の後になってしまうため影が反映されない点など幾つか問題点が合ったため諦めました。そこで、うまく行った前者を紹介したいと思います(やってみて分かりましたがメンテもそんなに大変じゃないと思います)。
サンプル
デモ
こんな感じになります。
一番右のモデルだけ SSAO が効かないようになっています。
特定のモデルだけ効かない Image Effect を作る
テラシュールブログさんでも解説されているように、ステンシルを使うと面白いマスク表現が色々とできます。
まずは具体的に、特定の領域だけ無視する 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 NotEqual
で Ref
で指定したステンシルマスク値が書かれていない場所だけ実行されるようにしています(逆に 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); } }
実行結果
実行してみると以下のようになります。
一番右のモデルだけマスク値を書き込むシェーダを利用したマテリアルで描画しています。他の場所は色が反転されています。
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 に追加 } }
これで以下のようになります。
おわりに
Non-PBR な表現のゲームではこういったこだわりが重要になってくると思いますので、是非必要になった際は参考にしてくださいませ。