Unity、URP、RenderFeature를 사용한 멀티 렌더링【 실전편 】
-
2025년 11월 27일
안녕하세요.
Cocone Engineering(이하 CE)에서 그래픽 엔지니어 겸 테크니컬 아티스트 TechLead로 데이터 파이프라인 및 렌더 파이프라인 개발, 에셋 제작 환경 구축 등을 담당하고 있는 타카노 마사야라고 합니다.
지난 글에서는 Unity URP의 RenderFeature를 이용한 멀티패스 렌더링의 기초에 대해 설명드렸습니다.
・Unity、URP、RenderFeature를 사용한 멀티 렌더링【구현편】
・Unity、URP、RenderFeature를 사용한 멀티 렌더링【해설편】
이번 실전편에서는 멀티패스 렌더링 기술을 활용하여 ‘반투명 오브젝트가 겹쳐졌을 때 반투명 맨 앞면만 그리는 시스템’을 구현하는 방법을 설명하곘습니다. 실제 게임 개발에서 부딪히는 문제를 해결할 수 있는 보다 실용적인 내용으로 구성되어 있습니다.
1. 기능 개요
전면 반투명 그리기 기능은 반투명이 겹치는 부분에 대해 전면 반투명만 그리는 기능입니다. 캐릭터 전체를 반투명하게 만들었을때 내부 구조가 보이는 문제를 개선합니다.
반투명이 중첩된 부분 그리기

반투명의 맨 앞면만 그리기

Unity 6000.2.12f1, URP 17.2.0, Forward Rendering Path에서 동작을 확인하였으나, Unity 6000 계열(Unity 6)이라면 문제없이 동작할 것입니다.
2. 구현 절차
① ForegroundTransparentFeature.cs、ForegroundTransparentPass.cs、ForegroundTransparent.shader의 3개의 파일을 Unity 프로젝트 내에 생성하고, 내용을 복사 및 붙여넣기하여 저장합니다.
using System;
using UnityEngine;
using UnityEngine.Rendering.Universal;
namespace TechBlog.Rendering
{
/// <summary>
/// ForegroundTransparentFeature
///
/// 반투명 오브젝트를 맨앞면에 그리는 ScriptableRendererFeature。
/// 심도 프리패스를 쓰는 것으로, 반투명 오브젝트끼리의 심도를 정확하게 제어합니다.
///
/// 구성:
/// ① _depthPass : DepthOnly 패스로 심도 프리패스
/// ② _colorPass : 컬러 드로잉 패스(SRPDefaultUnlit / UniversalForward)
///
/// 전부 RenderPassEvent 의 순서로 실행됩니다.
/// </summary>
public class ForegroundTransparentFeature : ScriptableRendererFeature
{
[Serializable]
public class Settings
{
[Tooltip("그리기 대상의 레이어 마스크")]
public LayerMask layerMask = 0;
[HideInInspector]
public string[] depthTagNames = { "DepthOnly" };
[HideInInspector]
public string[] colorTagNames = { "UniversalForward", "SRPDefaultUnlit" };
[HideInInspector]
public string passTagPrefix = "ForegroundTransparent";
}
public Settings settings = new();
// 각 패스
private ForegroundTransparentPass _depthPass;
private ForegroundTransparentPass _colorPass;
public override void Create()
{
// ① DepthOnly 프리패스
_depthPass = new ForegroundTransparentPass(
$"{settings.passTagPrefix}_Depth",
settings.depthTagNames,
RenderPassEvent.BeforeRenderingTransparents,
settings.layerMask,
ForegroundTransparentPass.PassMode.DepthOnly
);
// ② 컬러 드로잉 패스(Unlit + Lit모두 지원)
_colorPass = new ForegroundTransparentPass(
$"{settings.passTagPrefix}_Color",
settings.colorTagNames,
RenderPassEvent.BeforeRenderingTransparents + 1,
settings.layerMask,
ForegroundTransparentPass.PassMode.Color
);
}
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{
// Preview카메라에서는 스킵
if (renderingData.cameraData.cameraType == CameraType.Preview)
{
return;
}
// RenderPassEvent의 오름차순으로 자동 실행됩니다.
renderer.EnqueuePass(_depthPass);
renderer.EnqueuePass(_colorPass);
}
}
}using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.RendererUtils;
using UnityEngine.Rendering.RenderGraphModule;
using UnityEngine.Rendering.Universal;
namespace TechBlog.Rendering
{
/// <summary>
/// 가장 앞쪽 반투명 오브젝트용 범용 렌더 패스
/// 모드에 따라 심도만 쓰기, 또는 컬러 + 심도 쓰기로 전환 가능
/// </summary>
public class ForegroundTransparentPass : ScriptableRenderPass
{
/// <summary>
/// 패스 모드
/// </summary>
public enum PassMode
{
DepthOnly, // 심도만 쓰기
Color, // 컬러 + 심도 쓰기
}
private readonly string _profilerTag;
private readonly ProfilingSampler _profilingSampler;
private readonly LayerMask _layerMask;
private readonly RenderStateBlock _renderStateBlock;
private readonly List<ShaderTagId> _shaderTagIds = new();
private readonly PassMode _mode;
public ForegroundTransparentPass(
string tag,
string[] drawTagNames,
RenderPassEvent passEvent,
LayerMask layerMask,
PassMode mode
)
{
_profilerTag = tag;
_profilingSampler = new ProfilingSampler(tag);
renderPassEvent = passEvent;
_layerMask = layerMask;
_mode = mode;
// 렌더 상태의 사용자 정의가 필요한 경우 여기서 설정
_renderStateBlock = new RenderStateBlock(RenderStateMask.Nothing);
if (drawTagNames != null)
{
foreach (var name in drawTagNames)
{
if (!string.IsNullOrEmpty(name))
{
_shaderTagIds.Add(new ShaderTagId(name));
}
}
}
}
public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
var cameraData = frameData.Get<UniversalCameraData>();
var renderingData = frameData.Get<UniversalRenderingData>();
var resourceData = frameData.Get<UniversalResourceData>();
// 반투명 오브젝트의 표준적인 순서
var sorting = SortingCriteria.CommonTransparent;
// Lit 쉐더에서 필요한, PerObject 정보를 유효화
// (Unlit 의 경우는 불필요한 정보는 무시됩니다)
var perObject = PerObjectData.Lightmaps
| PerObjectData.LightProbe
| PerObjectData.LightProbeProxyVolume
| PerObjectData.ReflectionProbes
| PerObjectData.ShadowMask
| PerObjectData.OcclusionProbe
| PerObjectData.OcclusionProbeProxyVolume
| PerObjectData.LightData
| PerObjectData.ReflectionProbeData;
foreach (var shaderTag in _shaderTagIds)
{
using var builder = renderGraph.AddRasterRenderPass<PassData>(
$"{_profilerTag}_{shaderTag.name}", out var passData, _profilingSampler);
// 모드에 따라 어태치먼트를 설정
if (_mode == PassMode.Color)
{
// 컬러 + 심도 쓰기
passData.Color = resourceData.activeColorTexture;
builder.SetRenderAttachment(passData.Color, 0);
builder.SetRenderAttachmentDepth(resourceData.activeDepthTexture);
}
else // DepthOnly
{
// 컬러 어태치먼트를 읽기 전용으로 어태치
// (RenderGraph의 유효성 검사 요건을 충족하기 위해)
builder.SetRenderAttachment(resourceData.activeColorTexture, 0, AccessFlags.Read);
// 심도 어태치먼트에는 쓰기
builder.SetRenderAttachmentDepth(resourceData.activeDepthTexture, AccessFlags.Write);
}
var rendererListDesc = new RendererListDesc(shaderTag, renderingData.cullResults, cameraData.camera)
{
sortingCriteria = sorting,
renderQueueRange = RenderQueueRange.all,
layerMask = _layerMask,
stateBlock = _renderStateBlock,
rendererConfiguration = perObject,
};
passData.RendererList = renderGraph.CreateRendererList(rendererListDesc);
builder.UseRendererList(passData.RendererList);
// 프로파일상에 가시화하기 위해, 컬링을 무효화
builder.AllowPassCulling(false);
builder.SetRenderFunc((PassData data, RasterGraphContext context) =>
{
context.cmd.DrawRendererList(data.RendererList);
});
}
}
private class PassData
{
public TextureHandle Color;
public RendererListHandle RendererList;
}
}
}Shader "TechBlog/ForegroundTransparent"
{
Properties
{
_BaseColor("Base Color", Color) = (1, 1, 1, 1)
}
SubShader
{
Tags
{
"RenderType" = "Transparent"
"Queue" = "Transparent"
}
Pass
{
Name "Unlit"
Tags { "LightMode" = "SRPDefaultUnlit" }
Blend SrcAlpha One
ZWrite Off
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;
};
CBUFFER_START(UnityPerMaterial)
half4 _BaseColor;
CBUFFER_END
Varyings vert(Attributes input)
{
Varyings output = (Varyings)0;
output.positionCS = TransformObjectToHClip(input.positionOS.xyz);
return output;
}
half4 frag(Varyings input) : SV_Target
{
return _BaseColor;
}
ENDHLSL
}
Pass
{
Name "DepthPrepass"
Tags { "LightMode" = "DepthOnly" }
ZWrite On
ColorMask 0
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 = (Varyings)0;
output.positionCS = TransformObjectToHClip(input.positionOS.xyz);
return output;
}
half4 frag(Varyings input) : SV_Target
{
return half4(0, 0, 0, 0);
}
ENDHLSL
}
}
FallBack "Hidden/Universal Render Pipeline/FallbackError"
}
② 그리기 모델의 머티리얼에 ForegroundTransparent.shader 이더를 할당합니다. 또한, 이 처리를 적용할 전용 레이어(예: ForegroundTransparent)를 Unity의 Project Settings > Tags and Layers에서 추가하여 모델에 할당합니다.

③ 사용 중인 UniversalRenderData 하단의 Add Render Feature 버튼을 눌러 ForegroundTransparentFeature를 추가합니다. (모바일 기본 설정은 Assets/URP-Balanced-Renderer 사용) Layer Mask에 추가한 ForegroundTransparentLayer만 설정합니다.

④ 마지막으로 사용 중인 UniversalRenderData의 Transparent Layer Mask에서 ForegroundTransparentLayer의 체크를 해제합니다. 이는 표준 투명 그리기 패스에서 이중으로 그려지는 것을 방지하기 위한 것으로, ForegroundTransparentFeature에서 전용으로 처리하기 때문에 표준 패스에서 제외해야 합니다.

이상으로 구현이 완료되었습니다. 정상적으로 처리되었다면 반투명 부분의 맨 앞면만 그려져 있을 것이다.
3. 구현 내용 설명
이 기능은 심도 프리패스(Depth Prepass)와 컬러 드로잉 패스의 두 개의 패스 렌더링으로 구현됩니다. 아래에서 각 파일의 역할과 구현에 대해 자세히 설명하겠습니다.
3.1 ForegroundTransparentFeature.cs – 렌더링 파이프라인 통합
ForegroundTransparentFeature는 URP의ScriptableRendererFeature를 상속받은 클래스로, 렌더링 파이프라인에 독자적인 드로잉 처리를 추가합니다.
주요 역할:
・두 개의 렌더 패스(심도 프리패스와 컬러 드로잉 패스)를 생성하고 관리
・레이어 마스크에 의한 그리기 대상 제어
・패스의 실행 타이밍을RenderPassEvent.BeforeRenderingTransparents로 설정
// 심도 프리패스(먼저 심도 정보만 쓰기)
_depthPass = new ForegroundTransparentPass(
$"{settings.passTagPrefix}_Depth",
settings.depthTagNames,
RenderPassEvent.BeforeRenderingTransparents,
settings.layerMask,
ForegroundTransparentPass.PassMode.DepthOnly
);
// 컬러 드로잉 패스컬러(심도 테스트를 사용하여 맨 앞면만 그리기)
_colorPass = new ForegroundTransparentPass(
$"{settings.passTagPrefix}_Color",
settings.colorTagNames,
RenderPassEvent.BeforeRenderingTransparents + 1,
settings.layerMask,
ForegroundTransparentPass.PassMode.Color
);심도 패스가 먼저 실행되고, 그 후에 컬러 패스가 실행됨으로써 반투명 오브젝트의 맨 앞면만 그려지는 구조입니다.
3.2 ForegroundTransparentPass.cs – 드로잉 로직 구현
ForegroundTransparentPass는 ScriptableRenderPass를 상속받아 실제 드로잉 처리를 담당합니다.
Unity 6のRenderGraphについて: Unity 6에서는 RenderGraph 기반 구현이 권장되며, 이 구현 역시 RecordRenderGraph 메서드를 사용합니다. RenderGraph는 렌더링 경로의 리소스 종속성을 자동으로 관리하고 최적화하는 시스템입니다.
주요 특징:
1.패스 모드 전환
・DepthOnly: 심도 버퍼에만 쓰기(ColorMask 0 상당)
・Color: 컬러와 심도 양쪽에 쓰기
2.리소스 관리 최적화
・RenderGraph으로 인한 자동 리소스 종속정 관리
・텍스쳐 어태치먼트의 명시적 정의
3.Lit/Unlit 쉐이더 모두 지원
・UniversalForward태그(Lit쉐이더)와SRPDefaultUnlit태그(Unlit쉐이더)양쪽 다 지원
・PerObjectData를 적절하게 설정하여, 라이팅 정보를 제공
// 모드에 따라 어태치먼트를 설정if (_mode == PassMode.Color)
{
// 컬러 + 심도 쓰기
passData.Color = resourceData.activeColorTexture;
builder.SetRenderAttachment(passData.Color, 0);
builder.SetRenderAttachmentDepth(resourceData.activeDepthTexture);
}
else// DepthOnly
{
// 심도에만 쓰기(컬러를 읽기 전용)
builder.SetRenderAttachment(resourceData.activeColorTexture, 0, AccessFlags.Read);
builder.SetRenderAttachmentDepth(resourceData.activeDepthTexture, AccessFlags.Write);
}3.3 ForegroundTransparent.shader – 쉐이더 구현
이 셰이더는 두 개의 패스를 가진 멀티패스 쉐이더입니다.
Pass 1: Unlit 패스 (컬러 드로잉)
・LightMode: SRPDefaultUnlit
・ZWrite: Off (심도 쓰기 없음)
・ZTest: LEqual (기존 심도와 비교)
・Blend: SrcAlpha One (가산적 반투명)
Pass
{
Name "Unlit"
Tags { "LightMode" = "SRPDefaultUnlit" }
Blend SrcAlpha One
ZWrite Off // 심도는 쓰지 않는다
ZTest LEqual // 심도 프리패스에서 쓴 심도와 비교
Cull Back
}블렌드 모드에 대해:
이 쉐이더는 Blend SrcAlpha One 을 사용합니다. 이는 Additive Blending (가산 블렌딩)이라고 불리는 방식으로, 일반적인 반투명 드로잉과는 다른 특성이 있습니다.
・가산 블렌딩 (Blend SrcAlpha One):
◦ 소스 컬러의 알파값으로 곱한 색상을 배경색에 더합니다.
◦ 발광 효과나 글로우와 같이 밝은 표현에 적합합니다.
◦ 겹칠수록 밝아집니다.
・일반 반투명 (Blend SrcAlpha OneMinusSrcAlpha):
◦ 소스 컬러와 배경색을 알파값으로 선형 보간 합니다.
◦ 일반적인 투명 유리나 물과 같은 표현에 사용합니다.
◦ 배경색과 혼합됩니다.
이 샘플에서는 가산 블렌드를 사용했지만, 용도에 따라 블렌드 모드를 변경할 수 있습니다. 예를 들어 일반적인 반투명 표현이 필요한 경우 쉐이더의 Blend SrcAlpha One 을 Blend SrcAlpha OneMinusSrcAlpha 로 변경하면 됩니다.
Pass 2: DepthPrepass 패스(심도 전용)
・LightMode: DepthOnly
・ZWrite: On (심도 쓰기 있음)
・ColorMask: 0 (컬러 버퍼에 쓰지 않음)
Pass
{
Name "DepthPrepass"
Tags { "LightMode" = "DepthOnly" }
ZWrite On // 심도에만 쓰기
ColorMask 0 // 컬러에는 쓰지 않음
ZTest LEqual
Cull Back
}3.4 렌더링 흐름 전체 그림
실제 렌더링은 다음과 같은 순서로 실행됩니다:
1.깊이 프리패스(ForegroundTransparent_Depth)
◦ DepthOnly패스에서 반투명 오브젝트의 심도 정보만 뎁스 버퍼에 쓰기
◦ 이 시점에서 컬러 버퍼에는 아무것도 그려지지 않음
2.컬러 드로잉 패스(ForegroundTransparent_Color)
◦ SRPDefaultUnlit패스에서 반투명 오브젝트를 드로잉
◦ ZTest LEqual로 인해、심도 프리패스에 쓴 맨 앞면의 픽셀만 드로잉
◦ 안쪽에 있는 픽셀은 심도 테스트에서 걸러집니다.
3.결과
◦ 반투명 오브젝트가 겹쳐져있는 부분에서는, 맨 앞면의 오브젝트만 드로잉됩니다.
◦ 내부 구조에 비쳐 보이는 문제가 해결됩니다.
이 구조를 통해 일반적인 반투명 렌더링에서 피할 수 없는 ‘반투명이 겹치면 내부가 보임’ 문제를 추가적인 깊이 정보를 활용하여 해결하고 있습니다.
마지막으로
지금까지 멀티패스 렌더링을 이용한 구체적인 구현 예제를 소개해드렸습니다. 이 글이 여러분에게 도움이 되었기를 바랍니다.