저자: Irelans
Unity URP 외곽선 — 원신 ‘대장’의 밤혼 효과(에너지 왜곡 아웃라인) 구현기
이 글은 기존 공유글을 바탕으로 URP Render Feature로 간단히 재현한 “청춘판” 구현 기록입니다. 구현 코드는 일부만 발췌된 부분이 있어도 의도된 예시이니, 잘린 코드로 오해하지 마세요.
- 원문 참고:
仿O神邪♂道描边扰动制作分享_哔哩哔哩_bilibili
未经作者授权,禁止转载
www.bilibili.com
동일 계열 참고: 원신 나타 ‘밤혼’의 외곽선 효과와 원리는 유사하며, 하나는 컬러 아웃라인, 하나는 2색 아웃라인 차이입니다.
배경
빅 타이틀 컷신인 「赤炎凛霜之争」에서 ‘대장’ 캐릭터의 2색 외곽선 연출이 인상적입니다. Bilibili 공유를 참고해 URP Render Feature로 화면공간 외곽선에 왜곡을 얹는 방식을 재현했습니다.
예시효과:

대장의 흑백 2색 외곽선

아이디어 요약
- 외곽선을 적용할 “대상 물체” 렌더링: RenderLayerMask로 대상을 구분합니다. DC 부담을 줄이기 위해 depth only 패스로 마스크용 알파(실루엣)만 뽑습니다.
- 화면공간 외곽선 합성: 1단계 RT를 입력으로 왜곡 + 에지 검출을 수행해 최종 외곽선을 만듭니다.
결과 미리보기

단색 외곽선

2색 외곽선
컷신의 실제 대장은 서로 다른 왜곡을 가진 2층 레이어가 겹쳐 더 풍부한 결과가 납니다. 여기서는 경량화를 위해 단층 외곽선의 안쪽/바깥쪽 구간을 보간해 2색을 구현했습니다.
구현 단계
1) Render Feature 설정
RenderLayerMask의 두 번째 비트를 외곽선 대상 레이어로 사용했습니다. 필요 시 Render Feature 코드에서 바꿔도 됩니다.

Mesh Renderer에서 RenderLayerMask 선택

커스텀 Render Layer 생성 예시
ScriptableRendererFeature 코드
using UnityEngine;
using UnityEngine.Rendering.Universal;
using System.Collections.Generic;
using UnityEditor.Rendering;
using UnityEngine.Experimental.Rendering;
public class SS_Outline_Distorted : ScriptableRendererFeature
{
OutlineDistortPass outlineDistortPass;
public Material OutlineDistortMat;
public GraphicsFormat gfxFormat;
public RenderPassEvent Event = RenderPassEvent.BeforeRenderingPostProcessing;
public class OutlineDistortPass : ScriptableRenderPass
{
SS_OutlineVolume outline_volume;
public SS_Outline_Distorted feature;
private RTHandle cameraColor;
private readonly RenderTargetHandle OutlineSourceRT = RenderTargetHandle.CameraTarget;
private List<ShaderTagId> shaderTagIdList = new List<ShaderTagId>
{
new ShaderTagId("UniversalForward"),
};
public Material OutlineDistortMat;
public OutlineDistortPass(SS_Outline_Distorted feature)
{
this.feature = feature;
this.OutlineDistortMat = feature.OutlineDistortMat; // 외곽선 합성용 머티리얼
OutlineSourceRT.Init("OutlineSourceRT");
}
public void SetTarget(RTHandle cameraColor)
{
this.cameraColor = cameraColor;
}
public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
{
var descriptor = renderingData.cameraData.cameraTargetDescriptor;
descriptor.colorFormat = RenderTextureFormat.ARGB32;
descriptor.depthBufferBits = 0;
// 외곽선 소스 RT 준비
var full = renderingData.cameraData.cameraTargetDescriptor;
full.width = Mathf.RoundToInt(full.width);
full.height = Mathf.RoundToInt(full.height);
full.graphicsFormat = feature.gfxFormat; // 지원 포맷 선택
cmd.GetTemporaryRT(OutlineSourceRT.id, full, FilterMode.Bilinear);
ConfigureTarget(OutlineSourceRT.Identifier());
}
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
var stac = VolumeManager.instance.stack;
if (stac.GetComponent<SS_OutlineVolume>() != null)
outline_volume = stac.GetComponent<SS_OutlineVolume>();
float Outlinewidth; Color Outlinecolor;
if (outline_volume != null && outline_volume.IsActive())
{
Outlinewidth = outline_volume.OutlineWidth.value;
Outlinecolor = outline_volume.OutlineColor.value;
}
else
{
Debug.LogWarning("outline_volume is null");
return;
}
Shader.SetGlobalFloat("_OutlineWidth", Outlinewidth);
Shader.SetGlobalVector("_OutlineColor", Outlinecolor);
var cmd = CommandBufferPool.Get("DistortOutline");
cmd.ClearRenderTarget(true, true, Color.clear, 0);
// 대상 레이어 그리기: depthonly 기반 마스크 생성
uint OutlineLayer = (uint)1 << 2; // 두 번째 비트 예시
var filteringSettings = new FilteringSettings(RenderQueueRange.all, -1, OutlineLayer);
var drawingSettings = CreateDrawingSettings(shaderTagIdList, ref renderingData, renderingData.cameraData.defaultOpaqueSortFlags);
drawingSettings.overrideShader = Shader.Find("Irelans/SS_Outline_Source");
var rendererListParams = new RendererListParams(renderingData.cullResults, drawingSettings, filteringSettings);
var rendererList = context.CreateRendererList(ref rendererListParams);
cmd.DrawRendererList(rendererList);
context.ExecuteCommandBuffer(cmd);
cmd.Clear();
// 화면공간 외곽선 합성 + 왜곡
cmd.Blit(OutlineSourceRT.Identifier(), cameraColor, OutlineDistortMat);
context.ExecuteCommandBuffer(cmd);
cmd.Clear();
CommandBufferPool.Release(cmd);
}
public override void OnCameraCleanup(CommandBuffer cmd)
{
cmd.ReleaseTemporaryRT(OutlineSourceRT.id);
}
}
public override void Create()
{
outlineDistortPass = new OutlineDistortPass(this);
outlineDistortPass.renderPassEvent = Event;
const FormatUsage usage = FormatUsage.Linear | FormatUsage.Render;
gfxFormat = SystemInfo.IsFormatSupported(GraphicsFormat.R8G8B8A8_SRGB, usage)
? GraphicsFormat.R8G8B8A8_SRGB
: GraphicsFormat.R16G16B16A16_SFloat; // HDR fallback
}
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{
renderer.EnqueuePass(outlineDistortPass);
}
public override void SetupRenderPasses(ScriptableRenderer renderer, in RenderingData renderingData)
{
outlineDistortPass.SetTarget(renderer.cameraColorTargetHandle);
}
}
2) Shader 구성
외곽선 대상 오브젝트를 그려 마스크를 만드는 셰이더와, 화면공간에서 왜곡과 에지 샘플링으로 외곽선을 합성하는 셰이더 두 종류가 필요합니다.
(A) 외곽선 대상 렌더용 셰이더
Lit의 DepthOnly 패스를 그대로 활용해 실루엣 마스크를 뽑습니다.

간단화 목적의 DepthOnly 패스 복제 예시

(B) 화면공간 왜곡 외곽선 셰이더
Shader "Irelans/SS_Outline_Distort"
{
Properties
{
_MainTex ("RT", 2D) = "white" {}
_DistortTex ("Distort Texture", 2D) = "white" {}
_Distort ("Distort", float) = 0
_DistortSpeed ("Distort Speed", float) = 0
}
SubShader
{
Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline" }
ZWrite Off
Cull Off
Blend One OneMinusSrcAlpha
Pass
{
Name "SS_Outline_Distort"
HLSLPROGRAM
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#pragma vertex vert
#pragma fragment frag
#define SAMPLE_COUNT 4
uniform float _OutlineWidth;
uniform float4 _OutlineColor;
sampler2D _MainTex; float4 _MainTex_TexelSize;
TEXTURE2D(_DistortTex); SAMPLER(sampler_DistortTex);
half4 _DistortTex_ST;
half _Distort; half _DistortSpeed;
struct appdata { float4 positionOS : POSITION; float2 uv : TEXCOORD0; };
struct Varyings { float2 uv : TEXCOORD0; float4 positionCS : SV_POSITION; };
Varyings vert(appdata v)
{
Varyings o;
VertexPositionInputs pos = GetVertexPositionInputs(v.positionOS.xyz);
o.positionCS = pos.positionCS;
o.uv = v.uv;
return o;
}
half4 frag(Varyings input) : SV_Target
{
float2 uv_D = input.uv * _DistortTex_ST.xy + _DistortTex_ST.zw;
uv_D += float2(0, _DistortSpeed) * _Time.y;
half distortR = SAMPLE_TEXTURE2D(_DistortTex, sampler_DistortTex, uv_D).r;
half2 uvDistorted = input.uv + float2(0, distortR) * _Distort * 0.1;
float4 maskC = tex2D(_MainTex, input.uv);
if (maskC.x > 0.001f) { clip(-1); } // 내부는 투명 처리
int insideCount = 0;
float2 texel = _MainTex_TexelSize.xy;
[unroll]
for (int i = 0; i < SAMPLE_COUNT; i++)
{
float s, c; sincos(radians(360.0f / (float)SAMPLE_COUNT * (float)i), s, c);
float2 uv = uvDistorted + float2(s, c) * texel * _OutlineWidth;
float4 sc = tex2D(_MainTex, uv);
if (sc.x > 0.001f) insideCount++;
}
if ((insideCount <= SAMPLE_COUNT) && (insideCount >= 1))
{
half insideMask = 1 - step(insideCount, 3);
return float4(_OutlineColor.rgb * insideMask, 1);
}
// 바깥은 0
return 0;
}
ENDHLSL
}
}
}
3) Volume 제어
프로젝트에서 흔히 쓰는 방식대로 Volume 컴포넌트를 Render Feature의 온오프 및 파라미터 인터페이스로 사용합니다.

핵심 포인트 정리
- 외곽선용 RT를 만든 뒤, UV 왜곡을 준 상태로 주변 샘플링을 수행해 에지를 판정합니다.
- 단층 외곽선에서도 내부/외부 구간을 나눠 2색 보간이 가능합니다.
- 실제 연출처럼 다층 레이어를 겹치면 더 풍부한 떨림과 에너지감을 얻습니다.
샘플 프로젝트
GitHub - IrelansTA/Irelans_DistortedOutline: DistortedOutline DemoSample
DistortedOutline DemoSample. Contribute to IrelansTA/Irelans_DistortedOutline development by creating an account on GitHub.
github.com
원문
https://zhuanlan.zhihu.com/p/21137066883?share_code=43fzsBPl7RSU&utm_psn=1972273947170050856
'TECH.ART.FLOW.IO' 카테고리의 다른 글
| [발표 번역] Finding Harmony in Anime Style and Physically Based Rendering (0) | 2025.11.19 |
|---|---|
| [번역]언리얼 엔진 5 카툰 렌더링.앞머리 그림자 구현 방법 (포스트 프로세싱 방식 제외) (0) | 2025.11.16 |
| [번역] MatCap 박막 간섭 비눗방울 렌더링 (0) | 2025.11.11 |
| [번역] UE5 Add Custom MeshDrawPass (0) | 2025.11.03 |
| [번역] UE5 시스템 솔루션: 고품질 식생 시스템 (0) | 2025.10.29 |