1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Hitmanの"インスティンクト"風ポストエフェクト

Posted at

バージョンについて

今回の記事では
Unity2021.3.25f1 , URP12 , を使用しています。
本当はURP14に対応させたかったのですが......色々行き詰って今回はこうなりました m( _ _ )m
詳細は後述(有識者求む)
もし解決したら、そっちはそっちで記事にしようかと思います。

今回のレポジトリ
https://github.com/kamahir0/InstinctEffect

序論

Hitmanというゲームシリーズがあります。

簡単に言うと...
作りこまれた箱庭型のステージ内を歩き回るターゲットを、周囲のNPCにバレないよう暗殺するステルスゲーです。こ~れ大好きなんですよね私。

そんなHitmanですが、"インスティンクト"という便利機能が近年は標準搭載となっています。

スクリーンショット 2024-05-07 230635.png


──さあ キミの死角はどこかな


スクリーンショット 2024-05-07 230647.png


スケスケだぜ!!


と、こんな風に壁の向こう側にいる人間もシルエットで見えるようになるという。
NPCらの様子をうかがって隠密行動するゲームですから、とっても便利ですよね。

このインスティンクトのグラフィック表現に注目し、(簡単にですが)Unityで再現してみよう!というのが今回の内容です。

方法は色々と考えられますが、今回は敢えてポストエフェクトでやってみようと思います。

ポストエフェクトでやる理由は、今さらビルトインレンダーでもできるような実装をフツーにやっても面白くないと考えたためです。
あと最近URPを覚えたんで、揮ってみたいんですよね──この”力”を

作戦

上述の通りポストエフェクトで行く訳ですが、具体的にどうやるのか。
流れとしては、

  1. 人オブジェクトのシェーダーでステンシルに書き込む
  2. ScriptableRendererFeature/Passでポストエフェクトをかける

で、2番目のポストエフェクトでステンシルテストを行うワケです。なおRenderPassEventはAfterRenderingTransparentsBeforeRenderingPostProcessing に設定します。

ポストエフェクトの実装

URP12のScriptableRenderPassにおいて、ポストエフェクトはCommandBuffer.Blitメソッドでかけられます。

InstinctPass.cs
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のリポジトリをご覧ください。)

となると、次はコピー時に使われるシェーダーの中身ですね。

InstinctEffect.shader
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でこういう書き方はあまり見ないかもしれませんね。
これは、まず最初のHLSLINCLUDEENDHLSL 間で、変数やフラグメントシェーダの定義、.hlslファイルのインクルード等を記述しています。

このシェーダーは見ての通りマルチパスですが、全てのパスにおいて共通の要素が多いためこのような書き方をしました。
おかげで各パス1つあたりの記述はかなりスッキリしています。

InstinctEffect.shader
Pass
{
    Stencil
    {
        Ref 0
        Comp Equal
    }

    HLSLPROGRAM
    #pragma vertex Vert
    #pragma fragment Frag0
    ENDHLSL
}

このように。

3つのパスはそれぞれ、ステンシル時のRef値が0, 1, 2、またフラグメントシェーダはFrag0, Frag1, Frag2となっています。
これらのフラグメントシェーダは見ての通りほとんど同じ処理で、要約すれば 「_Strengthの値に応じた濃さで色を乗算する」 というものです。違うのは乗算する色だけ。

つまり、全体としてやりたいこととしては 「ステンシルの値に応じた色を各ピクセルに乗算させたい」「乗算する色の濃さはパラメータで可変にしたい」 といった感じです。

ステンシルに書き込む側の実装

前段でステンシルテストを行っていることを示しました。
ということは、当然ステンシルに書き込む側の存在が必要となります。ぶっちゃけここまで来たらもうウィニングランですね。

PeopleStencil.shader
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
            }
        }
    }
}
Hitman/BossStencil.shader
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には複数のマテリアルがセットできるので)

完成

Instinct_forQiita.gif
今回はかなり大ざっぱな再現なのでこんな感じです。
我こそは暗殺ステルスゲーを作らんという方がおられれば、これをベースに色々手を加えてみるのもいいんじゃないかと。準備は一任します。

URP14への対応

序論で述べた通り、今回は最新のUnity2022に搭載されているURP14ではなく、それより古いURP12で実装しています。

12→14間ではRenderTargetIdentifierRTHandleまわりの仕様が大きく変わっており、RenderTargetIdentifierでレンダーテクスチャーを色々操作する12以前のやり方はいずれ消えるとのこと。

こりゃあURP14に対応するしかないでしょって思うんですが、私がさんざん試したところ....ついぞポストエフェクト時のシェーダーでステンシルを正しく読み取ることができませんでした。
書き込みはちゃんとできてるっぽいんですけどね。

原因分かる方がいらっしゃったら是非教えてほしいです。
今回は以上です。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?