バージョンについて
今回の記事では
Unity2021.3.25f1
, URP12
, を使用しています。
本当はURP14に対応させたかったのですが......色々行き詰って今回はこうなりました m( _ _ )m
詳細は後述(有識者求む)
もし解決したら、そっちはそっちで記事にしようかと思います。
今回のレポジトリ
https://github.com/kamahir0/InstinctEffect
序論
Hitmanというゲームシリーズがあります。
簡単に言うと...
作りこまれた箱庭型のステージ内を歩き回るターゲットを、周囲のNPCにバレないよう暗殺するステルスゲーです。こ~れ大好きなんですよね私。
そんなHitmanですが、"インスティンクト"という便利機能が近年は標準搭載となっています。
──さあ キミの死角はどこかな
スケスケだぜ!!
と、こんな風に壁の向こう側にいる人間もシルエットで見えるようになるという。
NPCらの様子をうかがって隠密行動するゲームですから、とっても便利ですよね。
このインスティンクトのグラフィック表現に注目し、(簡単にですが)Unityで再現してみよう!というのが今回の内容です。
方法は色々と考えられますが、今回は敢えてポストエフェクトでやってみようと思います。
ポストエフェクトでやる理由は、今さらビルトインレンダーでもできるような実装をフツーにやっても面白くないと考えたためです。
あと最近URPを覚えたんで、揮ってみたいんですよね──この”力”を。
作戦
上述の通りポストエフェクトで行く訳ですが、具体的にどうやるのか。
流れとしては、
- 人オブジェクトのシェーダーでステンシルに書き込む
- ScriptableRendererFeature/Passでポストエフェクトをかける
で、2番目のポストエフェクトでステンシルテストを行うワケです。なおRenderPassEventはAfterRenderingTransparents
か BeforeRenderingPostProcessing
に設定します。
ポストエフェクトの実装
URP12のScriptableRenderPassにおいて、ポストエフェクトはCommandBuffer.Blit
メソッドでかけられます。
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
//~~~~~~~~~~~~~~~~~~~~~~~~
//一時レンダーテクスチャーを作成
var commandBuffer = CommandBufferPool.Get();
var desc = new RenderTextureDescriptor(cameraData.camera.scaledPixelWidth, cameraData.camera.scaledPixelHeight);
commandBuffer.GetTemporaryRT(tempID, desc);
//~~~~~~~~~~~~~~~~~~~~~~~~
//一時RTコピーしてからレンダーターゲットへ逆コピーすると共に、マテリアル適用でエフェクトをかける
commandBuffer.Blit(currentTargetID, tempID);
commandBuffer.Blit(tempID, currentTargetID, material);
//処理実行
context.ExecuteCommandBuffer(commandBuffer);
}
Blitメソッドは、あるレンダーテクスチャーから別のレンダーテクスチャーへとテクスチャのコピーを行うメソッドです。しかし第3引数にマテリアルを渡すオーバーロードが存在し、これを利用することによってコピー時にマテリアルが適用され、ポストエフェクトがかかります。
予めScriptableRendererFeatureにてSelializeFieldでシェーダーをセットしておき、そこからC#でマテリアルを作成してScriptableRenderPassにて上のコードの様に使います。(Feature/Passのコード全文はGitHubのリポジトリをご覧ください。)
となると、次はコピー時に使われるシェーダーの中身ですね。
Shader "Hidden/InstinctEffect"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
HLSLINCLUDE
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/Shaders/PostProcessing/Common.hlsl"
sampler2D _MainTex;
float4 _FilterColor;
float4 _PeopleColor;
float4 _BossColor;
float _Strength;
half4 Frag0(Varyings input) : COLOR
{
half4 col = tex2D(_MainTex, input.uv);
half4 effected = lerp(col, _FilterColor * col, _Strength);
return effected;
}
half4 Frag1(Varyings input) : COLOR
{
half4 col = tex2D(_MainTex, input.uv);
half4 effected = lerp(col, _PeopleColor * col, _Strength);
return effected;
}
half4 Frag2(Varyings input) : COLOR
{
half4 col = tex2D(_MainTex, input.uv);
half4 effected = lerp(col, _BossColor * col, _Strength);
return effected;
}
ENDHLSL
SubShader
{
Tags { "RenderType" = "Qpaque" "RenderPipeline" = "UniversalPipeline" }
LOD 100
Cull Off
ZWrite Off
ZTest Always
//何もいない
Pass
{
Stencil
{
Ref 0
Comp Equal
}
HLSLPROGRAM
#pragma vertex Vert
#pragma fragment Frag0
ENDHLSL
}
//People
Pass
{
Stencil
{
Ref 1
Comp Equal
}
HLSLPROGRAM
#pragma vertex Vert
#pragma fragment Frag1
ENDHLSL
}
//Boss
Pass
{
Stencil
{
Ref 2
Comp Equal
}
HLSLPROGRAM
#pragma vertex Vert
#pragma fragment Frag2
ENDHLSL
}
}
}
すっげぇキモいコードだな!
Shaderlabでこういう書き方はあまり見ないかもしれませんね。
これは、まず最初のHLSLINCLUDE
~ENDHLSL
間で、変数やフラグメントシェーダの定義、.hlslファイルのインクルード等を記述しています。
このシェーダーは見ての通りマルチパスですが、全てのパスにおいて共通の要素が多いためこのような書き方をしました。
おかげで各パス1つあたりの記述はかなりスッキリしています。
Pass
{
Stencil
{
Ref 0
Comp Equal
}
HLSLPROGRAM
#pragma vertex Vert
#pragma fragment Frag0
ENDHLSL
}
このように。
3つのパスはそれぞれ、ステンシル時のRef
値が0, 1, 2、またフラグメントシェーダはFrag0
, Frag1
, Frag2
となっています。
これらのフラグメントシェーダは見ての通りほとんど同じ処理で、要約すれば 「_Strengthの値に応じた濃さで色を乗算する」 というものです。違うのは乗算する色だけ。
つまり、全体としてやりたいこととしては 「ステンシルの値に応じた色を各ピクセルに乗算させたい」「乗算する色の濃さはパラメータで可変にしたい」 といった感じです。
ステンシルに書き込む側の実装
前段でステンシルテストを行っていることを示しました。
ということは、当然ステンシルに書き込む側の存在が必要となります。ぶっちゃけここまで来たらもうウィニングランですね。
Shader "Custom/Hitman/PeopleStencil"
{
SubShader
{
Tags {"RenderPipeline"="UniversalPipeline"}
Pass
{
Name "InstinctStencil_L1"
Tags {"Queue"="Geometory"}
ZTest Always
ZWrite Off
ColorMask 0
Stencil
{
Ref 1
Comp Greater
Pass Replace
}
}
}
}
Shader "Custom/Hitman/BossStencil"
{
SubShader
{
Tags {"RenderPipeline"="UniversalPipeline"}
Pass
{
Name "InstinctStencil_L2"
Tags {"Queue"="Geometory"}
ZTest Always
ZWrite Off
ColorMask 0
Stencil
{
Ref 2
Comp Always
Pass Replace
}
}
}
}
People → NPC
Boss → ターゲット
を、それぞれ表しています。
一応 ColorMask
にだけ触れますと....
これが0になっていると、頂点・フラグメントシェーダを定義せずとも勝手にステンシルや深度値を書き込むだけの(=描画は行わない)シェーダーになります。
一般NPC扱いさせたいオブジェクトにはPeopleのマテリアルを、
ターゲット扱いさせたいオブジェクトにはBossのマテリアルを、追加してください。
(皆さん知っての通り、MeshRendererには複数のマテリアルがセットできるので)
完成
今回はかなり大ざっぱな再現なのでこんな感じです。
我こそは暗殺ステルスゲーを作らんという方がおられれば、これをベースに色々手を加えてみるのもいいんじゃないかと。準備は一任します。
URP14への対応
序論で述べた通り、今回は最新のUnity2022に搭載されているURP14ではなく、それより古いURP12で実装しています。
12→14間ではRenderTargetIdentifier
、RTHandle
まわりの仕様が大きく変わっており、RenderTargetIdentifier
でレンダーテクスチャーを色々操作する12以前のやり方はいずれ消えるとのこと。
こりゃあURP14に対応するしかないでしょって思うんですが、私がさんざん試したところ....ついぞポストエフェクト時のシェーダーでステンシルを正しく読み取ることができませんでした。
書き込みはちゃんとできてるっぽいんですけどね。
原因分かる方がいらっしゃったら是非教えてほしいです。
今回は以上です。