この記事では,この画像のようにあるオブジェクト以外の画面を暗くすることのできるエフェクトの実装方法について紹介します.
あるオブジェクトだけを暗くしたり,あるオブジェクト以外を明るくしたり,その逆もできるので一風変わった強調表現として使えるかと思います.
この記事はUnity2018.4.22f1とUnity2018.4.0f1のWindowsビルドで動作検証しています.バージョンやプラットフォームによってうまく動かないことがあるかもしれません.
サンプルとしてUnityちゃん(© Unity Technologies Japan/UCL)を利用させていただきました.
実装
今回のエフェクトを実装するために,
- 明るさを保持したいオブジェクトにその旨を記述するためのシェーダ
- 実際にエフェクトを適用させるポストエフェクトシェーダ
- エフェクトをカメラに適用するためのC#スクリプト
の3つのコードを記述する必要があります.
これらの実装を順に,軽く説明していきます
明るさを保持したいオブジェクトにその旨を記述するためのシェーダ
このシェーダでは「その旨」を記述するわけですが,書き込むために「ステンシルバッファ」を利用します.
細かい話は補足として最後に載せました.
以下に一般的なUnlitシェーダ(光源の影響を受けないシェーダ)にステンシルバッファの記述したサンプルを載せますが,Stencil{...}
部分を他のシェーダに移植することで問題なく実行できるはずです.
Shader "Unlit/TextureWriteStencil"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType" = "Opaque" "Queue" = "Geometry-1"}
Pass
{
Stencil
{
Ref 1
Comp Always
Pass Replace
}
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;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
return col;
}
ENDCG
}
}
}
実際にエフェクトを適用させるポストエフェクトシェーダ
ポストエフェクトに記述するシェーダも通常のオブジェクトを塗るためのシェーダと同様の書き方をします.
詳しい話は省略しますが,画面に表示される画像が入力として得られるので,その入力にステンシルバッファの書き込みがあった場合に処理を実行するようにします.
ここでもステンシルバッファを利用して条件分岐を行います.
以下に対象のステンシル以外の色を暗くするシェーダスクリプトを載せます.
Durationの値でカメラ画像と塗りつぶす色をグラデーションさせています.
Shader "Hidden/DimEffect"
{
Properties
{
_MainTex ("MainTex", 2D) = "white" {}
_FogColor ("Fog Color", Color) = (0, 0, 0, 1)
_Duration ("Duration", Range(0, 1)) = 0
}
SubShader
{
Cull Off ZWrite Off ZTest Always
Pass
{
Stencil
{
Ref 1
Comp NotEqual
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
sampler2D _MainTex;
float4 _FogColor;
float _Duration;
float _StencilMask;
struct v2f {
half4 pos : POSITION;
half2 uv : TEXCOORD0;
};
float4 _MainTex_ST;
v2f vert(appdata_base v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
return o;
}
half4 frag(v2f i) : COLOR
{
float4 color = tex2D(_MainTex, i.uv);
float3 lerpColor = lerp(color, _FogColor, _Duration).rgb;
return float4(lerpColor, color.w);
}
ENDCG
}
}
}
エフェクトをカメラに適用するためのC#スクリプト
オブジェクトに対するシェーダは適用するマテリアルにアタッチすればよいですが,ポストプロセスシェーダを適用するためには,そのシェーダがアタッチされたマテリアルをカメラに渡してあげる作業が必要になります.
Post Processing Stack v2はその辺の処理を自動化してくれる便利なやつですが,「ステンシルバッファの値がクリアされた後の画面情報しか得られない」という重大な問題があり,これをそのまま利用することはできません.この事実を探すのにとても苦労しました...
以下のスクリプトを作成し,利用するカメラにアタッチします.
using UnityEngine;
using UnityEngine.Rendering;
[ExecuteAlways]
[RequireComponent(typeof(UnityEngine.Camera))]
public class DimEffect : MonoBehaviour
{
[SerializeField] private bool state;
[SerializeField] private Shader dimEffectShader;
[SerializeField] private Color fogColor;
[Range(0f, 1f)][SerializeField] private float duration;
private Material _material;
private static readonly int _FogColor = Shader.PropertyToID("_FogColor");
private static readonly int _Duration = Shader.PropertyToID("_Duration");
private UnityEngine.Camera _camera;
private CommandBuffer _buffer;
private int _tempTextureIdentifier;
private bool _currentState = false;
public void SetState(bool value)
{
if (_currentState == value) return;
_currentState = value;
if (value)
{
Init();
_camera.AddCommandBuffer(CameraEvent.AfterForwardAlpha, _buffer);
}
else
{
_camera.RemoveCommandBuffer(CameraEvent.AfterForwardAlpha, _buffer);
}
}
public void OnValidate()
{
SetState(state);
if (_material != null)
{
_material.SetColor(_FogColor, fogColor);
_material.SetFloat(_Duration, duration);
}
}
private void Init()
{
if (_camera == null)
{
_camera = this.GetComponent<UnityEngine.Camera>();
}
if (_material == null)
{
_material = new Material(dimEffectShader) {hideFlags = HideFlags.DontSave};
}
_material.SetColor(_FogColor, fogColor);
_material.SetFloat(_Duration, duration);
if(_tempTextureIdentifier == 0) _tempTextureIdentifier = Shader.PropertyToID("_PostEffect");
if (_buffer == null)
{
_buffer = new CommandBuffer {name = "DimEffect"};
_buffer.GetTemporaryRT(_tempTextureIdentifier, -1, -1, 0);
_buffer.Blit(BuiltinRenderTextureType.CameraTarget, _tempTextureIdentifier);
_buffer.Blit(_tempTextureIdentifier, BuiltinRenderTextureType.CameraTarget, _material);
_buffer.ReleaseTemporaryRT(_tempTextureIdentifier);
}
}
}
実行結果
このようなサンプルステージを用意しました.
カメラに上のC#スクリプトをアタッチし,図のようにパラメータを設定します.
Stateがエフェクトをかけるかどうかで,Dim Effect Shaderの部分にポストエフェクト用シェーダを貼り付けます.
手前の一つの物体にのみ先ほど作成したステンシルを書きこむシェーダーのついたマテリアルをアタッチし,エフェクトを適用すると以下のような描画が得られます.
このように,特定の物体のみ色をそのままに,残りの部分を暗くするエフェクトを実装することができました.
少し違ったオブジェクトの強調表現として使えそうです.
おわりに
今回はステンシルバッファとポストエフェクトを組み合わせることで対象の物体(以外)にエフェクトをかける方法について説明しました.
周囲の明るさを暗くする方法は今回述べたとおりですが,結構汎用性のありそうな技術なのでいろいろとできることがありそうです.(対象だけ黒く塗りつぶす,発光させる等...?)
補足
ステンシルバッファとは
ステンシルバッファは,シェーダ要素の一つです.レンダリング時に描画するかどうかを判断します.
簡単に言うとif文のようなもので,対象の物体を描画するときに,それ以前に描画された背景オブジェクトのステンシル値と比較して描画するかどうかを決定します.
Shader "Hidden/Sample"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Pass
{
Stencil
{
Ref 1
Comp Always
}
・・・
のように記述します.
SurfaceシェーダではSubShader内に,FragmentシェーダではPass内に記述します.
各シェーダの意味が分からない方は,
シェーダ内にSurfaceシェーダは #define surface
と記述されているもの,Fragmentシェーダは #define fragment
と書かれているものがそれだと思ってください.
Stencilブロック内の記述
今描画しようとしている背景の物体がStencilブロック内の条件を満たす場合に描画処理を実行します.
Stencilブロック内には以下のような要素を記述することができます.詳しい内容は公式リファレンスマニュアルを確認してください.
- Ref (Number)
(Number)に数字を記載し,以下のブロックでこの数字を参照することで比較を行います.
デフォルト値は0です.
- ReadMask (Number)
(Number)に数字を記載し,読み込みを行う場合の比較対象とします.
- WriteMask
(Number)に数字を記載し,書き込みを行う場合の比較対象とします.
- Comp (CompFunction)
(CompFunction)に条件を書くことで,その条件を基に比較を行います
詳しい内容は省きますが,条件には
Greater(Ref < Mask)
Less(Ref > Mask)
Equal(Ref == Mask)
NotEqual(Ref != Mask)
等があります.
- Pass (StencilFunction)
Compの条件に合致した場合のステンシル値の操作方法を定義します.
- Fail (StencilFunction)
Compの条件に合致しなかった場合のステンシル値の操作方法を定義します.
条件には
Keep : 書き換えを行わない
Zero : 0を代入する
Replace : Refの値を記入する
等があります.
例えば
Stencil
{
Ref 2
Comp Equal
Pass Zero
}
のように記述することで背景のブロックを参照し,2が記述されていた時に0を代入します.
ステンシルに記述するときの落とし穴
ステンシルバッファに値を書き込めばそれ以降に参照されるシェーダにその値を伝えることが可能になりますが,数点気にしなければいけない場所があります.
- レンダリング順序に気を付ける
当然ですが,ステンシルバッファに値を入れても他のシェーダに値を上書きされてしまうとその値は参照することができません.
そのために,描画順について意識する必要があります.
特に今回はポストプロセスに使用するためにレンダリングの最終段階まで残るようにしなければいけません.
そのために,SubShaderのTagプロパティとして
SubShader
{
Tags { "RenderType"="Transparent" "Queue"="Transparent+1"}
....
等記述する必要があります.
Transparentは透明な物体の描画順位なので,これに+1することで確実に値を残すことができます.
詳しい描画順位についての話はドキュメントを参照してください.