【Unity 6】URP RenderGraphで障害物に隠れたキャラクターのシルエット&アウトラインを描画する
3Dアクションゲームなどでは、「キャラクターが壁などの障害物の裏に隠れたときにシルエットや輪郭線を表示する」という表現をよく見かけます。
本記事では、この表現を URPのRenderGraph で実装する手順について解説します。
完成イメージ
開発環境 ・ 使用素材
開発環境
- エディターバージョン : Unity 6.3 LTS (6000.3.10f1)
- テンプレート : Universal 3D
- レンダーパイプライン : Universal Render Pipeline 17.3.0
使用素材
- SDユニティちゃん 3Dモデルデータ ver:3.0
実装の全体像
今回の実装は以下の3つのパスで構成されています。
1. 障害物深度バッファーの生成 (パス1)
障害物として判定したいレイヤーに属するオブジェクト(壁や地形など)に対して、色は書き込まず、深度だけを専用の深度バッファーを生成して書き込みます。
2. シルエットマスクテクスチャの生成 (パス2)
シルエット表示したいレイヤーに属するオブジェクト(キャラクターなど)に対して、パス1で作成した深度バッファーとZテストを行い、「どこが隠れていて、どこが見えているか」の情報をマスクテクスチャを生成して書き込みます。具体的には、
- 障害物の奥にある部分 = マスクテクスチャの R チャンネル
- 障害物の手前にある部分 = マスクテクスチャの G チャンネル
に 1 を書き込みます。
3. シルエット&アウトラインの合成 (パス3)
パス2で得られたマスクテクスチャを元に、画面全体へのBlit(フルスクリーン合成)を行います。同時に周囲のピクセルをサンプリングしてアウトラインを生成します。
実装の詳細
RendererFeature と RenderPass (C#側) の実装
RenderGraphにパスを登録するための C# プログラムです。
SilhouetteRendererFeature.cs
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.RenderGraphModule;
using UnityEngine.Rendering.Universal;
namespace CustomRenderGraph
{
// 指定した障害物レイヤーの奥にキャラクターが隠れた際に、シルエット・アウトラインを描画するRendererFeature
public class SilhouetteRendererFeature : ScriptableRendererFeature
{
[System.Serializable]
public class SilhouetteSettings
{
[Header("Shaders")]
[Tooltip("マスク用シェーダー(指定しない場合は Hidden/RenderGraph/Silhouette が使われます)")]
public Shader maskShader;
[Tooltip("合成・輪郭線用シェーダー(指定しない場合は Hidden/RenderGraph/SilhouetteBlit が使われます)")]
public Shader blitShader;
[Header("Layer Masks")]
[Tooltip("障害物として判定するレイヤー(これの後ろに行くとシルエット化されます)")]
public LayerMask obstacleLayerMask = 1; // Default
[Tooltip("シルエット表示したいキャラクターのレイヤー")]
public LayerMask characterLayerMask = 0; // Nothing
[Header("Colors")]
public Color silhouetteColor = new Color(0, 0, 0, 0.5f);
[ColorUsage(true, true)] // HDR対応
public Color outlineColor = new Color(1, 0, 0, 1);
[Header("Size")]
[Range(0f, 10f)]
[Tooltip("輪郭線の太さ(ピクセル数)")]
public float outlineWidth = 2.0f;
// 不透明オブジェクト描画直後にパスを挿入する
public RenderPassEvent renderPassEvent = RenderPassEvent.AfterRenderingOpaques;
}
public SilhouetteSettings settings = new SilhouetteSettings();
private SilhouettePass m_Pass;
private Material m_MaskMaterial;
private Material m_BlitMaterial;
public override void Create()
{
if (settings.maskShader == null) settings.maskShader = Shader.Find("Hidden/RenderGraph/Silhouette");
if (settings.blitShader == null) settings.blitShader = Shader.Find("Hidden/RenderGraph/SilhouetteBlit");
if (settings.maskShader != null && settings.blitShader != null)
{
// ランタイム時生成したマテリアルはDispose関数内で破棄する
m_MaskMaterial = CoreUtils.CreateEngineMaterial(settings.maskShader);
m_BlitMaterial = CoreUtils.CreateEngineMaterial (settings.blitShader);
if (m_Pass == null) m_Pass = new SilhouettePass();
m_Pass.Setup(m_MaskMaterial, m_BlitMaterial, settings);
}
}
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{
// マテリアルが指定されていない場合はスキップ
if(m_MaskMaterial == null || m_BlitMaterial == null) return;
// レイヤーが指定されていない場合はスキップ
if(settings.obstacleLayerMask == 0 || settings.characterLayerMask == 0) return;
// Unityエディタ内のゲームビュー(プレイヤーが見る画面)とシーンビューを描画するカメラを対象とする
if(renderingData.cameraData.cameraType == CameraType.Game || renderingData.cameraData.cameraType == CameraType.SceneView)
{
renderer.EnqueuePass(m_Pass);
}
}
protected override void Dispose(bool disposing)
{
// diposing
// true : プログラムのコードから明示的に Dispose が呼ばれた場合
// false : GC から Dispose が呼ばれた場合
if(disposing)
{
// ランタイム時生成したマテリアルを破棄してメモリリーク防止
CoreUtils.Destroy(m_MaskMaterial);
CoreUtils.Destroy(m_BlitMaterial);
}
}
}
public class SilhouettePass : ScriptableRenderPass
{
private Material m_MaskMaterial;
private Material m_BlitMaterial;
private SilhouetteRendererFeature.SilhouetteSettings m_Settings;
// マテリアルに設定されたシェーダーのLightModeタグに
// UniversalForward, UniversalForwardOnly, SRPDefaultUnlit
// のいずれかを持つオブジェクトを対象とする
private static readonly ShaderTagId[] s_ShaderTagIds = new ShaderTagId[]
{
new ShaderTagId("UniversalForward"),
new ShaderTagId("UniversalForwardOnly"),
new ShaderTagId("SRPDefaultUnlit"),
};
// ハッシュ計算を行うのは起動時に1回だけ(パフォーマンス, 安全性, 可読性向上)
private static readonly int s_SilhouetteColorId = Shader.PropertyToID("_SilhouetteColor");
private static readonly int s_OutlineColorId = Shader.PropertyToID("_OutlineColor");
private static readonly int s_OutlineWidthId = Shader.PropertyToID("_OutlineWidth");
private static readonly int s_SilhouetteMaskId = Shader.PropertyToID("_SilhouetteMask");
// RenderGraph 用のデータコンテナ群
// RendererListHandle で描画対象, 描画ルールをまとめる
private class ObstaclePassData
{
public RendererListHandle rendererListHandle;
}
private class MaskPassData
{
public RendererListHandle hiddenMaskRendererListHandle;
public RendererListHandle visibleMaskRendererListHandle;
}
private class BlitPassData
{
public TextureHandle maskTexture;
public Material blitMaterial;
}
public void Setup(Material maskMat, Material blitMat, SilhouetteRendererFeature.SilhouetteSettings settings)
{
m_MaskMaterial = maskMat;
m_BlitMaterial = blitMat;
m_Settings = settings;
this.renderPassEvent = settings.renderPassEvent;
}
public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
if (m_MaskMaterial == null || m_BlitMaterial == null) return;
// 主要なテクスチャリソースを保持するデータ
UniversalResourceData resourceData = frameData.Get<UniversalResourceData>();
// レンダリング関連の情報や状態を保持するデータ
UniversalRenderingData renderingData = frameData.Get<UniversalRenderingData>();
// カメラ固有の状態や設定を保持するデータ
UniversalCameraData cameraData = frameData.Get<UniversalCameraData>();
// activeColorTexture は現時点でのレンダリング結果を書き込んでいるテクスチャ
// 今回の場合は不透明オブジェクト描画後の状態
TextureHandle activeColorTexture = resourceData.activeColorTexture;
if (!activeColorTexture.IsValid()) return; // 有効かチェック
// 障害物専用デプスバッファーの作成
TextureDesc depthDesc = renderGraph.GetTextureDesc(activeColorTexture);
depthDesc.name = "ObstacleDepthTexture";
depthDesc.clearBuffer = true;
depthDesc.msaaSamples = MSAASamples.None;
depthDesc.depthBufferBits = DepthBits.Depth32;
TextureHandle obstacleDepthTexture = renderGraph.CreateTexture(depthDesc);
// キャラクターシルエット用マスクテクスチャの作成
TextureDesc maskDesc = renderGraph.GetTextureDesc(activeColorTexture);
maskDesc.name = "SilhouetteMaskTexture";
maskDesc.clearBuffer = true;
maskDesc.msaaSamples = MSAASamples.None;
maskDesc.depthBufferBits = DepthBits.None;
// R: 障害物の奥(隠蔽), G: 障害物の手前(可視)
maskDesc.colorFormat = UnityEngine.Experimental.Rendering.GraphicsFormat.R8G8_UNorm;
TextureHandle maskTexture = renderGraph.CreateTexture(maskDesc);
// **解説1**
//【パス1】障害物深度バッファーの生成
using (var builder = renderGraph.AddRasterRenderPass<ObstaclePassData>("Generate Obstacle Depth", out var passData))
{
// 深度バッファーの指定(深度値書き込み対象)
builder.SetRenderAttachmentDepth(obstacleDepthTexture, AccessFlags.Write);
// RenderGraphによるパスの自動最適化(カリング)を防止
builder.AllowPassCulling(false);
// 障害物レイヤーのみを描画対象にする
var filtering = new FilteringSettings(RenderQueueRange.all, m_Settings.obstacleLayerMask);
// ソートのルールと基準になるカメラを設定
var sorting = new SortingSettings(cameraData.camera)
{
criteria = SortingCriteria.CommonOpaque // 手前から奥
};
// シェーダーのLightModeタグでの絞り込みとマテリアルのオーバーライド
var drawing = new DrawingSettings(s_ShaderTagIds[0], sorting)
{
overrideMaterial = m_MaskMaterial,
overrideMaterialPassIndex = 0, // ObstacleDepth パス
};
for(int i = 1; i < s_ShaderTagIds.Length; i++) drawing.SetShaderPassName(i, s_ShaderTagIds[i]);
// RendererListHandle で描画対象, 描画ルールをまとめる
passData.rendererListHandle = renderGraph.CreateRendererList(new RendererListParams(renderingData.cullResults, drawing, filtering));
builder.UseRendererList(passData.rendererListHandle);
builder.SetRenderFunc(static (ObstaclePassData data, RasterGraphContext context) =>
{
// ClearRenderTarget
// 引数1 : 深度情報をクリアするか 引数2 : 色情報をクリアするか
// 引数3 : 色情報をクリアする際の塗りつぶし色
// Color.clear : RGBA全て0の黒・透明
context.cmd.ClearRenderTarget(true, false, Color.clear); // 深度情報のみクリア
context.cmd.DrawRendererList(data.rendererListHandle);
});
}
// **解説2**
//【パス2】キャラクターシルエットマスクの生成
using (var builder = renderGraph.AddRasterRenderPass<MaskPassData>("Generate Silhouette Mask", out var passData))
{
// パス1で作った障害物深度バッファーを読み込み、マスクテクスチャへ書き込む
// SetRenderAttachment の第2引数には
//「マルチレンダーターゲットの何番目に割り当てるか」を指定する
// 今回は maskTexture にしか出力しないため 0 を指定(0始まり)
builder.SetRenderAttachment(maskTexture, 0, AccessFlags.Write);
builder.SetRenderAttachmentDepth(obstacleDepthTexture, AccessFlags.Read);
// RenderGraphによるパスの自動最適化(カリング)を防止
builder.AllowPassCulling(false);
// シルエット表示したいレイヤーのみを描画対象にする
var filtering = new FilteringSettings(RenderQueueRange.all, m_Settings.characterLayerMask);
// ソートのルールと基準になるカメラを設定
var sorting = new SortingSettings(cameraData.camera)
{
criteria = SortingCriteria.CommonOpaque // 手前から奥
};
// [Red] 障害物より奥にある部分を描画
// シェーダーのLightModeタグでの絞り込みとマテリアルのオーバーライド
var hiddenDrawing = new DrawingSettings(s_ShaderTagIds[0], sorting)
{
overrideMaterial = m_MaskMaterial,
overrideMaterialPassIndex = 1 // SilhouetteMask パス
};
for (int i = 1; i < s_ShaderTagIds.Length; i++) hiddenDrawing.SetShaderPassName(i, s_ShaderTagIds[i]);
// RendererListHandle で描画対象, 描画ルールをまとめる
passData.hiddenMaskRendererListHandle = renderGraph.CreateRendererList(new RendererListParams(renderingData.cullResults, hiddenDrawing, filtering));
builder.UseRendererList(passData.hiddenMaskRendererListHandle);
// [Green] 障害物より手前にある部分を描画
// シェーダーのLightModeタグでの絞り込みとマテリアルのオーバーライド
var visibleDrawing = new DrawingSettings(s_ShaderTagIds[0], sorting)
{
overrideMaterial = m_MaskMaterial,
overrideMaterialPassIndex = 2 // VisibleMask パス
};
for (int i = 1; i < s_ShaderTagIds.Length; i++) visibleDrawing.SetShaderPassName(i, s_ShaderTagIds[i]);
passData.visibleMaskRendererListHandle = renderGraph.CreateRendererList(new RendererListParams(renderingData.cullResults, visibleDrawing, filtering));
builder.UseRendererList(passData.visibleMaskRendererListHandle);
// パス3でマスクテクスチャを参照できるようにする
// グローバルなシェーダー変数を変更することを許可する
builder.AllowGlobalStateModification(true);
// パス2終了時に生成したマスクテクスチャをシェーダー変数(_SilhouetteMask)に自動で割り当てる
builder.SetGlobalTextureAfterPass(maskTexture, s_SilhouetteMaskId);
builder.SetRenderFunc(static (MaskPassData data, RasterGraphContext context) =>
{
context.cmd.ClearRenderTarget(false, true, Color.clear); // 色情報のみクリア
context.cmd.DrawRendererList(data.hiddenMaskRendererListHandle);
context.cmd.DrawRendererList(data.visibleMaskRendererListHandle);
});
}
// **解説3**
//【パス3】フルスクリーン合成(シルエット・アウトライン表示)
// RendererFeature側で設定した値をマテリアルに設定
m_BlitMaterial.SetColor(s_SilhouetteColorId, m_Settings.silhouetteColor);
m_BlitMaterial.SetColor(s_OutlineColorId, m_Settings.outlineColor);
m_BlitMaterial.SetFloat(s_OutlineWidthId, m_Settings.outlineWidth);
using (var builder = renderGraph.AddRasterRenderPass<BlitPassData>("Apply Silhouette Composite", out var passData))
{
passData.blitMaterial = m_BlitMaterial;
passData.maskTexture = maskTexture;
// メインのカラーバッファーに対して書き込み、マスクは読み込み
builder.UseTexture(maskTexture, AccessFlags.Read);
builder.SetRenderAttachment(activeColorTexture, 0, AccessFlags.Write);
builder.SetRenderFunc(static (BlitPassData data, RasterGraphContext context) =>
{
// Blitterクラスを用いたフルスクリーンレンダリング
// 引数1 : コマンドバッファ 引数2 : 読み込み元テクスチャ
// 引数3 : UVスケールとオフセット 引数4 : 合成を行うためのマテリアル
// 引数5 : マテリアルに設定されたシェーダーの何番目のパスを使用するかのインデックス(0始まり)
Blitter.BlitTexture(context.cmd, data.maskTexture, new Vector4(1, 1, 0, 0), data.blitMaterial, 0);
});
}
}
}
}
解説1
パス1では、障害物レイヤー(obstacleLayerMask)に属するオブジェクトだけを描画対象として、専用のデプスバッファー(深度テクスチャ)を作成します。
具体的には、描画時に深度バッファー・マスク生成用シェーダーの ObstacleDepth パス を使用して、色は一切書き込まず(ColorMask 0)、深度(カメラからの距離)情報のみを更新(ZWrite On, ZTest LEqual)しています。
生成した障害物の深度情報は、次のパス2で「キャラクターが障害物の奥にいるのか、手前にいるのか」を判定するための基準として使用されます。
解説2
パス2では、隠れている部分と見えている部分のマスクを塗り分けるため、パス1で作成した深度テクスチャを読み込み専用(AccessFlags.Read)として指定し、シルエット表示したいレイヤー(characterLayerMask)に属するオブジェクトだけを描画対象として、マスクテクスチャを生成します。
シルエット表示したいオブジェクトを深度バッファー・マスク生成用シェーダー(Silhouette.shader)の SilhouetteMask パス, VisibleMask パス で2回描画することで、マスクを塗り分けています。
-
隠蔽部(Red):
SilhouetteMask パスで、障害物より奥 (ZTest Greater) にある部分をRチャンネルに描画。 -
可視部(Green) :
VisibleMask パスで、障害物より手前 (ZTest LEqual) にある部分をGチャンネルに描画。
さらに、SetGlobalTextureAfterPass を使用することで、生成したマスクテクスチャをグローバルなシェーダー変数 (_SilhouetteMask) として登録し、後続の合成パスで参照できるようにしています。
解説3
パス3では、パス2で作成したマスクテクスチャを読み込み、現在カメラが描画中のメインカラーバッファー (activeColorTexture) に直接結果を書き込みます。
インスペクターで設定されたシルエット色やアウトラインの太さを合成用マテリアルに渡し、Blitter.BlitTexture を用いてフルスクリーン描画(画面全体に1枚のポリゴンを描画する処理)します。
合成用シェーダー(SilhouetteBlit.shader)では、渡されたマスクのRチャンネル(隠れている部分)とGチャンネル(見えている部分)の情報を読み取り、「Rの領域にはシルエット色を塗り、境界部分にはアウトラインを描画する」という処理を行って最終的な絵を完成させます。
深度バッファー ・ マスク生成用シェーダー (Shader側) の実装
C# 側のパス1, 2で使用する、深度バッファー ・ マスク生成用シェーダーです。
Silhouette.shader
Shader "Hidden/RenderGraph/Silhouette"
{
Properties {}
SubShader
{
Tags
{
"RenderType" = "Opaque"
"RenderPipeline" = "UniversalPipeline"
}
LOD 100
// **解説1**
// Pass 0: Obstacle Depth
// 障害物を描き出し、キャラクターとの前後関係を測るための深度バッファを作成する
Pass
{
Name "ObstacleDepth"
ColorMask 0 // 色は一切書き込まない(深度のみが目的)
ZWrite On
ZTest LEqual
Cull Back
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
struct Attributes { float4 positionOS : POSITION; };
struct Varyings { float4 positionCS : SV_POSITION; };
Varyings vert(Attributes input)
{
Varyings output;
output.positionCS = TransformObjectToHClip(input.positionOS.xyz);
return output;
}
half4 frag(Varyings input) : SV_Target { return half4(0, 0, 0, 0); }
ENDHLSL
}
// **解説2**
// Pass 1: Silhouette Mask
// (障害物深度) < (キャラクター深度) の場合、Redに1を書き込む
Pass
{
Name "SilhouetteMask"
ColorMask R // Rのみ書き込む
ZWrite Off
ZTest Greater // (障害物深度) < (キャラクター深度) の場合だけ通る
Cull Back
Blend One Zero // 既に書かれた値に上書き
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
struct Attributes { float4 positionOS : POSITION; };
struct Varyings { float4 positionCS : SV_POSITION; };
Varyings vert(Attributes input)
{
Varyings output;
output.positionCS = TransformObjectToHClip(input.positionOS.xyz);
return output;
}
half4 frag(Varyings input) : SV_Target { return half4(1, 0, 0, 0); }
ENDHLSL
}
// **解説3**
// Pass 2: Visible Mask
// (障害物深度) >= (キャラクター深度) の場合、Greenに1を書き込む
Pass
{
Name "VisibleMask"
ColorMask G // Gのみ書き込む
ZWrite Off
ZTest LEqual // (障害物深度) >= (キャラクター深度) の場合だけ通る
Cull Back
Blend One Zero // 既に書かれた値に上書き
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
struct Attributes { float4 positionOS : POSITION; };
struct Varyings { float4 positionCS : SV_POSITION; };
Varyings vert(Attributes input)
{
Varyings output;
output.positionCS = TransformObjectToHClip(input.positionOS.xyz);
return output;
}
half4 frag(Varyings input) : SV_Target { return half4(0, 1, 0, 0); }
ENDHLSL
}
}
}
解説1
障害物レイヤーに属するオブジェクトのみを対象に、その深度を専用の深度バッファーに記録するパスです。
ColorMask 0 を指定しているので、色は一切書き込まれません。
解説2
Pass 0 で記録した障害物の深度とシルエット表示したいオブジェクトの深度を比較し、
(障害物の深度) < (シルエット表示したいオブジェクトの深度)、すなわちシルエット表示したいオブジェクトの方が奥にある場合にマスクテクスチャの R 成分に 1 を書き込むパスです。
ColorMask R を指定しているので、R 成分以外は書き込まれません。
解説3
Pass 0 で記録した障害物の深度とシルエット表示したいオブジェクトの深度を比較し、
(障害物の深度) >= (シルエット表示したいオブジェクトの深度)、すなわちシルエット表示したいオブジェクトの方が手前にある場合にマスクテクスチャの G 成分に 1 を書き込むパスです。
ColorMask G を指定しているので、G 成分以外は書き込まれません。
合成&アウトライン用シェーダー (Shader側) の実装
C# 側のパス3で使用する、画面全体へ適用するフルスクリーンシェーダーです。
SilhouetteBlit.shader
Shader "Hidden/RenderGraph/SilhouetteBlit"
{
Properties {}
SubShader
{
Tags
{
"RenderType" = "Opaque"
"RenderPipeline" = "UniversalPipeline"
}
LOD 100
Pass
{
Name "ApplySilhouette"
// **解説1**
ZTest Always // ZTest は行わない
ZWrite Off
Cull Off
Blend SrcAlpha OneMinusSrcAlpha // アルファ値に基づいて元の画面と合成
HLSLPROGRAM
#pragma vertex Vert // Blit.hlsl の頂点シェーダーを利用
#pragma fragment Frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.core/Runtime/Utilities/Blit.hlsl"
// パス2でグローバル宣言したマスクテクスチャを使用
// TEXTURE2D_X
// テクスチャの型をXR環境か非XR環境かで自動的に切り替えて定義するマクロ
TEXTURE2D_X(_SilhouetteMask);
SAMPLER(sampler_SilhouetteMask);
CBUFFER_START(UnityPerMaterial)
half4 _SilhouetteColor;
half4 _OutlineColor;
float _OutlineWidth;
float4 _SilhouetteMask_TexelSize;
CBUFFER_END
half4 Frag(Varyings input) : SV_Target
{
// UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX
// 「今から描画するピクセルが左目用(0)か右目用(1)か」という識別番号
// (ステレオアイインデックス)を内部のグローバル変数にセットするマクロ
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
float2 uv = input.texcoord;
// SAMPLE_TEXTURE2D_X
// TEXTURE2D_X で宣言したテクスチャからピクセルの色を読み込むためのマクロ
float4 center = SAMPLE_TEXTURE2D_X(_SilhouetteMask, sampler_SilhouetteMask, uv);
// **解説2**
// R = 奥に描画されるシルエット部分, G = 手前に描画される可視部分
// 純粋に障害物に隠れている部分だけを抽出
float mask = (center.r > 0.0 && center.g == 0.0) ? 1.0 : 0.0;
// **解説3**
// 輪郭線の太さ分、上下左右にシフトしてサンプリング
float2 offset = _SilhouetteMask_TexelSize.xy * _OutlineWidth;
float4 s1 = SAMPLE_TEXTURE2D_X(_SilhouetteMask, sampler_SilhouetteMask, uv + float2(offset.x, 0));
float4 s2 = SAMPLE_TEXTURE2D_X(_SilhouetteMask, sampler_SilhouetteMask, uv + float2(-offset.x, 0));
float4 s3 = SAMPLE_TEXTURE2D_X(_SilhouetteMask, sampler_SilhouetteMask, uv + float2(0, offset.y));
float4 s4 = SAMPLE_TEXTURE2D_X(_SilhouetteMask, sampler_SilhouetteMask, uv + float2(0, -offset.y));
float m1 = (s1.r > 0.0 && s1.g == 0.0) ? 1.0 : 0.0;
float m2 = (s2.r > 0.0 && s2.g == 0.0) ? 1.0 : 0.0;
float m3 = (s3.r > 0.0 && s3.g == 0.0) ? 1.0 : 0.0;
float m4 = (s4.r > 0.0 && s4.g == 0.0) ? 1.0 : 0.0;
// いずれかのピクセルがマスク範囲内であれば1.0
float maxMask = max(max(mask, m1), max(max(m2, m3), m4));
if(mask > 0.0)
{
// シルエット本体
return _SilhouetteColor;
}
else if(maxMask > 0.0)
{
// 輪郭線部分(障害物と境界を接する部分にも線を描画する)
return _OutlineColor;
}
// マスク範囲外は描画しない(元の色のまま)
return half4(0, 0, 0, 0);
/*
// if文除去版1
// mask = 1.0 ならば maxMask = 1.0 であることに注目
half4 finalColor = lerp(_OutlineColor, _SilhouetteColor, mask);
finalColor = lerp(half4(0, 0, 0, 0), finalColor, maxMask);
return finalColor;
// if文除去版2
return mask * _SilhouetteColor + (1.0 - mask) * maxMask * _OutlineColor;
*/
}
ENDHLSL
}
}
}
解説1
ZTest Always と ZWrite Off を指定し、画面の最前面に描画されるようにします。
また、Blend SrcAlpha OneMinusSrcAlpha を設定することで、シェーダーが half4(0, 0, 0, 0)(透明色) を返したピクセルは元の画面の色がそのまま残り、アルファ値を持つ色(シルエット色など) を返した場合は、元の画面と合成されるようになります。
解説2
C# 側のパス2で作成したマスクの判定処理です。
パス2では、隠れている部分を Rチャンネル , 手前にある部分を Gチャンネル に描画しました。
一見「Rが0より大きい(R = 1)場所 = シルエット」で良さそうに思えますが、実はキャラクター自身のパーツが障害物の手前と奥で前後に重なっている部分がシルエットとして判定されてしまいます。(center.g == 0.0 の条件を外してみてください。)
そこで、 center.r > 0.0 という条件に加えて center.g == 0.0 という条件を設けることで、「奥にはパーツが描画されているけれど、手前にはパーツが描画されていない(= 完全に障害物に隠れている)」 ピクセルだけを厳密にシルエットの対象として抽出しています。
解説3
シルエットの輪郭線(アウトライン)を描画するための処理です。
Unityが提供している _SilhouetteMask_TexelSize(xy には 1/画面解像度 が入っています) を使い、現在のピクセルから上下左右に少しずらした位置の色を取得しています。
もし「現在のピクセルは隠れていない(mask == 0)けれど、周りのどこかが隠れている(maxMask > 0)」状態であれば、そのピクセルはキャラクターと障害物の「境界(エッジ)」だと判定できるので、アウトライン色を適用します。
おわりに
本記事では、URP の RenderGraph で障害物に隠れたキャラクターのシルエット&アウトラインを描画する実装の流れについて解説しました。
本記事が RenderGraph 学習の一助となれば幸いです。
最後までご覧いただきありがとうございました!
