TECHARTNOMAD | TECHARTFLOWIO.COM

TECH.ART.FLOW.IO

[번역] Unity URP 아웃라인 원신 중대장의 야혼 아웃라인 효과 모방(에너지 교란 아웃라인)

jplee 2025. 11. 14. 13:58

저자: Irelans

Unity URP 외곽선 — 원신 ‘대장’의 밤혼 효과(에너지 왜곡 아웃라인) 구현기

이 글은 기존 공유글을 바탕으로 URP Render Feature로 간단히 재현한 “청춘판” 구현 기록입니다. 구현 코드는 일부만 발췌된 부분이 있어도 의도된 예시이니, 잘린 코드로 오해하지 마세요.

  • 원문 참고:

仿O神邪♂道描边扰动制作分享_哔哩哔哩_bilibili

 

仿O神邪♂道描边扰动制作分享_哔哩哔哩_bilibili

未经作者授权,禁止转载

www.bilibili.com

동일 계열 참고: 원신 나타 ‘밤혼’의 외곽선 효과와 원리는 유사하며, 하나는 컬러 아웃라인, 하나는 2색 아웃라인 차이입니다.

배경

빅 타이틀 컷신인 「赤炎凛霜之争」에서 ‘대장’ 캐릭터의 2색 외곽선 연출이 인상적입니다. Bilibili 공유를 참고해 URP Render Feature로 화면공간 외곽선에 왜곡을 얹는 방식을 재현했습니다.

예시효과:

대장의 흑백 2색 외곽선

아이디어 요약

  1. 외곽선을 적용할 “대상 물체” 렌더링: RenderLayerMask로 대상을 구분합니다. DC 부담을 줄이기 위해 depth only 패스로 마스크용 알파(실루엣)만 뽑습니다.
  2. 화면공간 외곽선 합성: 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