Shader / URP
Intermediate
Unity 2021.3+ / Unity 6 (URP)
URP 스텐실 버퍼(Stencil Buffer) 3D 포탈 셰이더
특정 포탈 게이트 평면을 통해서만 내부 3D 공간과 오브젝트가 보이도록 만드는 URP 스텐실 버퍼 마스킹 셰이더 기법과 렌더 큐 설정.
#Shader #URP #Stencil #HLSL #Rendering
핵심 개념스텐실 버퍼는 각 픽셀마다 8비트 정수(0~255) 값을 기록할 수 있는 버퍼입니다. 포탈 문(Mask)이 먼저 스텐실 버퍼에 특정 숫자(예: 1)를 기록하고, 포탈 안쪽 공간(Content)은 스텐실 버퍼가 1인 픽셀에만 자신을 그리도록 제한하면 별도의 보조 카메라나 렌더 텍스처 없이 완벽한 3D 포탈을 구현할 수 있습니다.
1. 동작 원리 및 렌더 큐 순서
이 기법의 성패는 렌더 큐(Render Queue) 순서에 달려 있습니다. 렌더 큐 순서가 맞지 않으면 마스크가 먼저 버퍼를 채우지 못해 공간이 가려지거나 뚫려 보이지 않습니다.
- 1단계 (마스크 오브젝트): 포탈 입구 평면입니다. 화면에 실제 색상을 칠하지 않지만(
ColorMask 0), 스텐실 버퍼에 참조값(Ref 1)을 씁니다. 렌더 큐는Geometry-1(1999)로 설정해 일반 물체보다 먼저 그려지게 합니다. - 2단계 (포탈 내부 오브젝트): 문 뒤에 숨겨진 방이나 오브젝트입니다. 스텐실 버퍼가 1인 곳에만 그려지도록 검사(
Comp Equal)합니다. 렌더 큐는 기본Geometry(2000)를 사용합니다. - 3단계 (일반 월드 오브젝트): 스텐실 검사를 하지 않는 통상적인 물체들입니다.
2. 포탈 마스크 셰이더 (Portal Mask)
색상을 출력하지 않고 깊이 버퍼와 스텐실 버퍼만 조작하는 URP 언릿 셰이더입니다.
Shader "Custom/URP/PortalMask"
{
Properties
{
[IntRange] _StencilRef("Stencil ID", Range(0, 255)) = 1
}
SubShader
{
Tags
{
"RenderType" = "Opaque"
"RenderPipeline" = "UniversalPipeline"
"Queue" = "Geometry-1"
}
Pass
{
Name "MaskPass"
ZWrite On
ColorMask 0 // 픽셀 색상 버퍼에는 아무것도 기록하지 않음
Stencil
{
Ref [_StencilRef]
Comp Always
Pass Replace
}
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
}
}
}
3. 포탈 내부 오브젝트 셰이더 (Portal Content)
스텐실 버퍼가 마스크의 ID와 일치할 때만 픽셀을 렌더링하는 셰이더입니다.
Shader "Custom/URP/PortalContent"
{
Properties
{
_BaseMap("Texture", 2D) = "white" {}
_BaseColor("Color", Color) = (1, 1, 1, 1)
[IntRange] _StencilRef("Stencil ID", Range(0, 255)) = 1
}
SubShader
{
Tags
{
"RenderType" = "Opaque"
"RenderPipeline" = "UniversalPipeline"
"Queue" = "Geometry"
}
Pass
{
Name "ForwardLit"
Tags { "LightMode" = "UniversalForward" }
Stencil
{
Ref [_StencilRef]
Comp Equal // 스텐실 버퍼 값이 _StencilRef와 같을 때만 통과
Pass Keep
}
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
struct Attributes
{
float4 positionOS : POSITION;
float2 uv : TEXCOORD0;
};
struct Varyings
{
float4 positionCS : SV_POSITION;
float2 uv : TEXCOORD0;
};
TEXTURE2D(_BaseMap);
SAMPLER(sampler_BaseMap);
CBUFFER_START(UnityPerMaterial)
float4 _BaseMap_ST;
half4 _BaseColor;
CBUFFER_END
Varyings vert(Attributes input)
{
Varyings output;
output.positionCS = TransformObjectToHClip(input.positionOS.xyz);
output.uv = TRANSFORM_TEX(input.uv, _BaseMap);
return output;
}
half4 frag(Varyings input) : SV_Target
{
half4 col = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv) * _BaseColor;
return col;
}
ENDHLSL
}
}
}
4. 파라미터 및 인스펙터 설정
| 프로퍼티 | 권장값 | 설명 |
|---|---|---|
| _StencilRef (Mask) | 1 | 포탈 문 평면에 부여할 스텐실 ID (0~255 사이 정수) |
| _StencilRef (Content) | 1 | 내부 오브젝트가 검사할 스텐실 ID (Mask와 동일해야 보임) |
| Queue (Mask) | Geometry-1 (1999) | 일반 불투명 오브젝트보다 먼저 그려져 스텐실 버퍼를 선점해야 함 |
| Queue (Content) | Geometry (2000) | 마스크가 기록된 직후 그려지도록 설정 |
| ZWrite | On | 포탈 앞을 다른 물체가 가로막을 때 올바르게 가려지도록 깊이 기록 유지 |
5. 실무 주의점 및 트러블슈팅
- Depth Prepass 문제: URP Forward+ 또는 Depth Priming이 켜져 있는 파이프라인 에셋의 경우 깊이 버퍼가 먼저 기록되어 렌더 큐가 의도와 다르게 동작할 수 있습니다. 마스크 패스가 Depth Prepass에 포함되지 않도록
DepthNormals/DepthOnly패스를 생략하거나 큐 오프셋을 주의 깊게 지정해야 합니다. - 다중 포탈 구현 시 ID 분리: 포탈 A는 Stencil Ref 1, 포탈 B는 Stencil Ref 2로 지정하여 방마다 서로 다른 ID를 배정하면 하나의 씬에서 여러 포탈이 상호 간섭하지 않습니다.
- 후면 컬링(Cull Back): 포탈 입구 뒤쪽에서 앞으로 바라볼 때는 마스크가 렌더링되지 않도록 평면(Quad)의 노멀 방향을 진입 방향으로 맞춰야 합니다.