<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>mazeline.log</title>
        <link>https://velog.io/</link>
        <description>테크아트 컨설팅 전문 회사 "메이즈라인" 입니다.</description>
        <lastBuildDate>Wed, 24 Dec 2025 04:30:21 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>mazeline.log</title>
            <url>https://velog.velcdn.com/images/mazeline_1973/profile/5cfd42c6-8777-41d0-9930-4550e659c97c/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. mazeline.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/mazeline_1973" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[오픈월드 모바일 게임을 위한 Unity Runtime Virtual Texture 시스템 개발기]]></title>
            <link>https://velog.io/@mazeline_1973/%EC%98%A4%ED%94%88%EC%9B%94%EB%93%9C-%EB%AA%A8%EB%B0%94%EC%9D%BC-%EA%B2%8C%EC%9E%84%EC%9D%84-%EC%9C%84%ED%95%9C-Unity-Runtime-Virtual-Texture-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B0%9C%EB%B0%9C%EA%B8%B0</link>
            <guid>https://velog.io/@mazeline_1973/%EC%98%A4%ED%94%88%EC%9B%94%EB%93%9C-%EB%AA%A8%EB%B0%94%EC%9D%BC-%EA%B2%8C%EC%9E%84%EC%9D%84-%EC%9C%84%ED%95%9C-Unity-Runtime-Virtual-Texture-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B0%9C%EB%B0%9C%EA%B8%B0</guid>
            <pubDate>Wed, 24 Dec 2025 04:30:21 GMT</pubDate>
            <description><![CDATA[<p>이 글은 유니티 환경에서 모바일 오픈월드 게임의 지형 렌더링 성능을 극대화하기 위한 런타임 가상 텍스처(RVT) 시스템의 자체 
개발 과정을 상세히 다룹니다. 핵심은 방대한 지형 데이터를 한꺼번에 로드하는 대신, 화면에 보이는 부분만 실시간으로 합성하고 
LRU 캐시 기반의 물리 페이지에 저장하여 GPU 메모리 점유율을 획기적으로 낮춘 것입니다. 특히 모바일 기기의 부하를 줄이기 
위해 피드백 버퍼 리드백을 생략하고 CPU 쿼드트리 기반의 가시성 판정과 멀티스레딩 기술을 적용했습니다. 또한, ETC2/BC3 
포맷의 실시간 압축과 데칼 및 높이맵 지원을 통해 시각적 품질과 효율성을 동시에 확보한 아키텍처를 설계했습니다. 결과적으로 이 
시스템은 대규모 환경에서도 100MB 미만의 메모리로 안정적인 구동이 가능함을 입증하며 기술적 해결책을 제시합니다.</p>
<p>&lt;계속 읽기&gt;
<a href="https://mazeline.tech/blogs/UnityRuntimeVirtualTextureSystemDevelopment/">https://mazeline.tech/blogs/UnityRuntimeVirtualTextureSystemDevelopment/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Unity URP 포스트 프로세스 마스킹 시스템 개발기: GTAO에서 특정 영역 제외하기]]></title>
            <link>https://velog.io/@mazeline_1973/Unity-URP-%ED%8F%AC%EC%8A%A4%ED%8A%B8-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4-%EB%A7%88%EC%8A%A4%ED%82%B9-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B0%9C%EB%B0%9C%EA%B8%B0-GTAO%EC%97%90%EC%84%9C-%ED%8A%B9%EC%A0%95-%EC%98%81%EC%97%AD-%EC%A0%9C%EC%99%B8%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@mazeline_1973/Unity-URP-%ED%8F%AC%EC%8A%A4%ED%8A%B8-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4-%EB%A7%88%EC%8A%A4%ED%82%B9-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B0%9C%EB%B0%9C%EA%B8%B0-GTAO%EC%97%90%EC%84%9C-%ED%8A%B9%EC%A0%95-%EC%98%81%EC%97%AD-%EC%A0%9C%EC%99%B8%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 24 Dec 2025 04:26:25 GMT</pubDate>
            <description><![CDATA[<p>&quot;캐릭터 얼굴에 AO가 들어가면 더러워 보여요. 빼주세요.&quot;
게임 개발에서 Ambient Occlusion은 입체감을 더해주는 훌륭한 기술이지만, 때로는 적용되면 안 되는 영역이 있습니다. 
특히 캐릭터의 얼굴이나 피부처럼 깨끗해야 하는 부분에 AO가 적용되면 지저분하거나 병색이 도는 것처럼 보일 수 있죠.
&lt;계속 읽기&gt;
<a href="https://mazeline.tech/blogs/URPPostProcessMaskingSystemDevelopment/">https://mazeline.tech/blogs/URPPostProcessMaskingSystemDevelopment/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[URP PBR 고도화를 위한 Chan Diffuse BRDF 분석]]></title>
            <link>https://velog.io/@mazeline_1973/URP-PBR-%EA%B3%A0%EB%8F%84%ED%99%94%EB%A5%BC-%EC%9C%84%ED%95%9C-Chan-Diffuse-BRDF-%EB%B6%84%EC%84%9D-2053v30n</link>
            <guid>https://velog.io/@mazeline_1973/URP-PBR-%EA%B3%A0%EB%8F%84%ED%99%94%EB%A5%BC-%EC%9C%84%ED%95%9C-Chan-Diffuse-BRDF-%EB%B6%84%EC%84%9D-2053v30n</guid>
            <pubDate>Wed, 24 Dec 2025 04:23:48 GMT</pubDate>
            <description><![CDATA[<p>Johann Heinrich Lambert가 1760년에 정립한 코사인 법칙. albedo / π라는 이 우아한 공식은 지금까지도 컴퓨터 그래픽스의 기초가 되어왔다. 하지만 이 공식에는 치명적인 가정이 숨어있다.</p>
<p>&lt;계속 읽기&gt;
<a href="https://mazeline.tech/blogs/ChanDiffuseBRDF_Analysis/">https://mazeline.tech/blogs/ChanDiffuseBRDF_Analysis/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Bindless 렌더링: 현대 GPU-Driven 그래픽스의 근간]]></title>
            <link>https://velog.io/@mazeline_1973/Bindless-%EB%A0%8C%EB%8D%94%EB%A7%81-%ED%98%84%EB%8C%80-GPU-Driven-%EA%B7%B8%EB%9E%98%ED%94%BD%EC%8A%A4%EC%9D%98-%EA%B7%BC%EA%B0%84</link>
            <guid>https://velog.io/@mazeline_1973/Bindless-%EB%A0%8C%EB%8D%94%EB%A7%81-%ED%98%84%EB%8C%80-GPU-Driven-%EA%B7%B8%EB%9E%98%ED%94%BD%EC%8A%A4%EC%9D%98-%EA%B7%BC%EA%B0%84</guid>
            <pubDate>Wed, 24 Dec 2025 04:21:22 GMT</pubDate>
            <description><![CDATA[<p>본 토픽은 현대 그래픽스 기술의 핵심인 Bindless 렌더링의 개념과 발전 과정, 그리고 실제 구현 기법을 심층적으로 다룹니다.
 이 기술은 기존의 복잡한 리소스 바인딩 절차를 제거하고 GPU가 직접 대규모 리소스 배열에 접근하도록 하여, CPU 오버헤드를 
획기적으로 줄이고 렌더링 효율을 극대화합니다. 특히 DirectX 12와 Vulkan 같은 최신 API에서의 표준화 과정과 함께,
 UE5의 Nanite나 DOOM Eternal 같은 최첨단 엔진들이 이를 어떻게 활용하는지 상세히 분석합니다. 또한 하드웨어 
제조사별 차이점과 GPU Driven 파이프라인으로의 전환이 갖는 구조적 의의를 설명하며, 차세대 그래픽스 설계를 위한 기술적 
통찰을 제공합니다. 결국 이 글은 Bindless 구조가 현대 게임 엔진의 성능 최적화를 위한 필수적인 기반임을 강조하고 
있습니다.</p>
<p><a href="https://mazeline.tech/blogs/GPU_Driven_AboutTodayAndFurther_KR/">https://mazeline.tech/blogs/GPU_Driven_AboutTodayAndFurther_KR/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Unity URP에서 Pre-Integrated FGD 구현하기: IBL의 숨겨진 핵심]]></title>
            <link>https://velog.io/@mazeline_1973/Unity-URP%EC%97%90%EC%84%9C-Pre-Integrated-FGD-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-IBL%EC%9D%98-%EC%88%A8%EA%B2%A8%EC%A7%84-%ED%95%B5%EC%8B%AC</link>
            <guid>https://velog.io/@mazeline_1973/Unity-URP%EC%97%90%EC%84%9C-Pre-Integrated-FGD-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-IBL%EC%9D%98-%EC%88%A8%EA%B2%A8%EC%A7%84-%ED%95%B5%EC%8B%AC</guid>
            <pubDate>Wed, 24 Dec 2025 04:18:42 GMT</pubDate>
            <description><![CDATA[<p>이 글은 유니티 URP 환경에서 물리 기반 렌더링(PBR)의 시각적 완성도를 높이기 위한 Pre-Integrated FGD 구현 방법을 상세히 설명합니다. 실시간 계산이 어려운 렌더링 방정식을 해결하기 위해 Split-Sum Approximation 기술을 활용하여 반사 데이터를 효율적으로 처리하는 원리를 다룹니다. 특히 FGD LUT를 활용해 프레넬 효과와 기하학적 감쇠를 미리 계산함으로써 연산 효율과 물리적 정확성을 동시에 확보하는 과정을 보여줍니다. 또한 거친 표면에서의 에너지 손실을 보완하는 Chan Diffuse 확장 모델을 소개하며 더욱 사실적인 재질 표현법을 제시합니다. 결과적으로 이 자료는 고품질 그래픽 구현을 위한 이미지 기반 라이팅(IBL) 의 핵심 수학적 배경과 실무적인 셰이더 최적화 기법을 포괄하고 있습니다.</p>
<p><a href="https://mazeline.tech/blogs/PreIntegratedFGD_DeepDive/">https://mazeline.tech/blogs/PreIntegratedFGD_DeepDive/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Quake III Fast Inverse Square Root: 전설적인 알고리즘의 현대적 재조명]]></title>
            <link>https://velog.io/@mazeline_1973/Quake-III-Fast-Inverse-Square-Root-%EC%A0%84%EC%84%A4%EC%A0%81%EC%9D%B8-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98%EC%9D%98-%ED%98%84%EB%8C%80%EC%A0%81-%EC%9E%AC%EC%A1%B0%EB%AA%85</link>
            <guid>https://velog.io/@mazeline_1973/Quake-III-Fast-Inverse-Square-Root-%EC%A0%84%EC%84%A4%EC%A0%81%EC%9D%B8-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98%EC%9D%98-%ED%98%84%EB%8C%80%EC%A0%81-%EC%9E%AC%EC%A1%B0%EB%AA%85</guid>
            <pubDate>Wed, 12 Nov 2025 17:16:03 GMT</pubDate>
            <description><![CDATA[<pre><code>float Q_rsqrt( float number )
{
    long i;
    float x2, y;
    const float threehalfs = 1.5F;

    x2 = number * 0.5F;
    y = number;
    i = * ( long * ) &amp;y;                       // evil floating point bit hack
    i = 0x5f3759df - ( i &gt;&gt; 1 );               // what the fuck?
    y = * ( float * ) &amp;i;
    y = y * ( threehalfs - ( x2 * y * y ) );   // 1st iteration
//  y = y * ( threehalfs - ( x2 * y * y ) );   // 2nd iteration, can be removed

    return y;
}</code></pre><p>1999년, id Software가 공개한 Quake III Arena의 소스 코드에는 게임 개발사에 전설처럼 회자되는 알고리즘이 포함되어 있었다. 바로 Fast Inverse Square Root, 일명 &#39;0x5f3759df 매직 넘버&#39;로 알려진 이 코드는 당시로서는 혁신적인 수학적 트릭과 하드웨어 특성을 극한까지 활용한 최적화의 정수였다. 코드 상단에 적힌 &quot;evil floating point bit hack&quot;과 &quot;what the fuck?&quot;이라는 주석은 이 알고리즘의 비직관적인 본질을 단적으로 보여준다.</p>
<h1 id="알고리즘의-핵심-아이디어">알고리즘의 핵심 아이디어</h1>
<p>역제곱근(inverse square root)은 3D 그래픽스에서 벡터 정규화를 수행할 때 필수적인 연산이다. 어떤 벡터를 단위 벡터로 만들기 위해서는 벡터의 각 성분을 벡터의 크기로 나누어야 하는데, 이 과정에서 1/√(x²+y²+z²) 형태의 역제곱근 계산이 반드시 필요하다. 1990년대 후반, 이러한 연산은 표준 수학 라이브러리를 사용할 경우 30~40 사이클이 소요되는 매우 무거운 작업이었다.</p>
<p>Quake III의 알고리즘은 이 문제를 두 단계로 해결한다. 먼저 부동소수점 수를 비트 단위로 재해석하여 long 타입의 정수로 취급한다. IEEE 754 표준에 따르면 부동소수점 수는 부호, 지수, 가수부로 구성되는데, 이를 정수로 해석했을 때의 값과 실제 부동소수점 값의 로그 사이에는 거의 선형적인 관계가 존재한다. 매직 넘버 0x5f3759df는 이러한 로그 공간에서의 근사를 통해 역제곱근의 초기 추정값을 제공한다.</p>
<p>두 번째 단계는 Newton-Raphson 반복법을 한 번 적용하여 이 추정값을 정제하는 것이다. &#39;y = y  <em>(threehalfs - (x2</em>  y * y))&#39; 라는 한 줄의 코드가 바로 그것이다. 이 반복을 한 번만 수행해도 대부분의 게임 그래픽스 용도로는 충분한 정확도를 얻을 수 있었다. 주석에서 볼 수 있듯이 두 번째 반복은 제거할 수 있을 정도로 첫 번째 반복만으로도 실용적인 정확도를 확보할 수 있었다.</p>
<h1 id="1990년대-후반의-기술적-맥락">1990년대 후반의 기술적 맥락</h1>
<p>이 알고리즘이 혁명적이었던 이유는 당시 하드웨어 환경을 이해해야 한다. 1999년 당시 소비자급 CPU는 Pentium III나 AMD K6-2 수준이었고, 부동소수점 연산 유닛의 성능은 현대 기준으로 보면 매우 제한적이었다. 특히 제곱근과 나눗셈은 파이프라인 스톨을 유발하는 고비용 연산이었다. 게다가 하드웨어 수준에서 역제곱근을 직접 계산하는 명령어는 존재하지 않았다.</p>
<p>Quake III는 초당 수천, 수만 번의 벡터 정규화를 수행해야 했다. 조명 계산, 표면 법선 처리, 시야 벡터 계산 등 거의 모든 3D 그래픽스 파이프라인에서 이 연산이 요구되었다. 표준 sqrt() 함수를 사용한다면 프레임 레이트에 직접적인 타격을 입을 수밖에 없었다. Quake III의 Fast Inverse Square Root는 표준 방식 대비 약 3~4배 빠른 성능을 제공하면서도 시각적으로 구분할 수 없는 수준의 정확도를 유지했다. <a href="https://en.wikipedia.org/wiki/Fast_inverse_square_root">https://en.wikipedia.org/wiki/Fast_inverse_square_root</a></p>
<h1 id="현대-하드웨어-환경의-변화">현대 하드웨어 환경의 변화</h1>
<p>2025년 현재, 이 알고리즘의 실용적 가치는 거의 사라졌다. 가장 큰 이유는 하드웨어 수준에서의 패러다임 전환이다. 2000년대 초반부터 x86 프로세서에 도입된 SSE(Streaming SIMD Extensions) 명령어 세트는 rsqrtss와 rsqrtps라는 전용 역제곱근 명령어를 포함하고 있다. 이들 명령어는 단일 사이클에 근접한 속도로 역제곱근을 계산할 수 있으며, 정확도 또한 Newton-Raphson 한 번 반복 수준 이상을 보장한다.</p>
<p>GPU 환경에서는 상황이 더욱 명확하다. HLSL과 GLSL 같은 셰이더 언어는 rsqrt()를 내장 함수로 제공하며, 이는 단일 GPU 명령어로 컴파일된다. 현대 GPU 아키텍처는 수천 개의 ALU를 병렬로 운용하면서도 각각이 하드웨어 역제곱근 연산을 지원한다. Unity나 Unreal Engine 같은 상용 게임 엔진에서 벡터를 정규화할 때 개발자가 이러한 저수준 최적화를 고민할 필요가 전혀 없는 이유다.</p>
<p>컴파일러의 진화도 중요한 요소다. 현대 C/C++ 컴파일러는 &#39;1.0f/sqrtf(x)&#39; 패턴을 인식하면 자동으로 하드웨어 rsqrt 명령어로 치환한다. GCC와 Clang은 최적화 레벨 O2 이상에서 이러한 변환을 수행하며, MSVC 역시 동일한 최적화를 적용한다. 즉, 개발자가 명시적으로 최적화 코드를 작성하지 않아도 컴파일러가 알아서 최선의 하드웨어 명령어를 선택해준다.</p>
<h1 id="성능의-역전">성능의 역전</h1>
<p>구체적인 성능 비교를 살펴보면 상황은 더욱 명확해진다. 현대 x86-64 프로세서에서 Quake III 방식의 비트 핵과 Newton-Raphson 한 번 반복은 대략 15<del>20 사이클을 소요한다. 반면 SSE의 rsqrtss 명령어는 4</del>6 사이클 정도면 결과를 반환한다. 정확한 역제곱근이 필요한 경우 sqrtss와 divss를 조합하더라도 25~30 사이클 정도로, Quake III 방식보다 크게 느리지 않다.</p>
<p>GPU에서는 차이가 더욱 극명하다. 셰이더에서 rsqrt() 내장 함수는 사실상 무시할 수 있는 수준의 비용만 발생시킨다. ALU 처리량이 초당 수조 회에 달하는 현대 GPU에서 역제곱근 연산은 더 이상 병목이 아니다. 오히려 메모리 대역폭이나 텍스처 샘플링, 복잡한 분기문 같은 요소들이 훨씬 중요한 최적화 대상이 되었다.</p>
<p>모바일 플랫폼도 예외가 아니다. ARM 아키텍처의 NEON SIMD 확장은 vrsqrte 명령어를 제공하며, Mali나 Adreno 같은 모바일 GPU 역시 하드웨어 rsqrt를 완벽히 지원한다. 배터리 효율성이 중요한 모바일 환경에서조차 이 알고리즘을 직접 구현할 필요는 없다.</p>
<h1 id="남아있는-가치">남아있는 가치</h1>
<p>그렇다면 이 알고리즘은 완전히 쓸모없어진 것일까. 실용적 측면에서는 그렇다고 할 수 있지만, 여전히 중요한 가치를 지니고 있다.</p>
<p>우선 교육적 가치가 있다. 이 알고리즘은 부동소수점 수의 내부 표현, 로그 공간에서의 근사, 수치 해석의 기초인 Newton-Raphson 방법 등 컴퓨터 과학의 여러 핵심 개념을 압축적으로 보여준다. IEEE 754 표준을 깊이 이해하고자 하는 학생이나 개발자에게 이보다 좋은 예제는 찾기 어렵다. 실제로 많은 대학 컴퓨터 그래픽스 수업에서 이 알고리즘을 분석하는 것을 과제로 내주고 있다.</p>
<p>임베디드 시스템이나 극도로 제한된 하드웨어 환경에서는 여전히 활용 가능성이 있다. 하드웨어 부동소수점 유닛 자체가 없거나 매우 느린 마이크로컨트롤러, 혹은 SIMD 확장이 없는 구형 프로세서에서 작업해야 하는 경우가 그렇다. 물론 이런 환경 자체가 점점 희귀해지고 있지만, 특수한 산업용 장비나 레거시 시스템 유지보수에서는 여전히 마주칠 수 있다.</p>
<p>가장 중요한 것은 역사적, 문화적 의미다. 이 알고리즘은 제한된 자원으로 불가능해 보이는 것을 가능하게 만들었던 시대의 창의성과 엔지니어링 정신을 상징한다. John Carmack으로 대표되는 id Software 엔지니어들의 극한 최적화 철학은 게임 산업 전체에 깊은 영향을 미쳤다. 이 코드 한 조각은 단순한 알고리즘을 넘어 그 시대를 증언하는 아티팩트다. <a href="https://www.beyond3d.com/content/articles/8/">https://www.beyond3d.com/content/articles/8/</a></p>
<h1 id="현대-개발자를-위한-교훈">현대 개발자를 위한 교훈</h1>
<p>현대 게임 개발에서 벡터 정규화가 필요하다면 그냥 엔진이 제공하는 normalize() 함수를 쓰면 된다. Unity의 Vector3.Normalize()나 Unreal Engine의 FVector::Normalize(), 혹은 GLSL의 normalize() 내장 함수가 모든 것을 알아서 처리해준다. 직접 rsqrt를 구현할 필요도 없고, 더더욱 Quake III 방식의 비트 핵을 작성할 이유도 없다.</p>
<p>SIMD 명령어를 직접 다뤄야 하는 저수준 최적화가 필요한 경우라면, Intel Intrinsics의 _mm_rsqrt_ps()나 ARM NEON의 vrsqrteq_f32() 같은 컴파일러 내장 함수를 사용하는 것이 정답이다. 이들은 하드웨어 명령어로 직접 매핑되므로 어떤 수작업 최적화보다도 빠르고 안정적이다.</p>
<p>이 알고리즘이 우리에게 주는 진짜 교훈은 &#39;최적화의 맥락 의존성&#39;이다. 25년 전 혁명적이었던 기법이 오늘날에는 오히려 비효율적일 수 있다. 하드웨어가 진화하면 소프트웨어 최적화 전략도 함께 진화해야 한다. 맹목적으로 &#39;유명한 알고리즘&#39;을 적용하기보다는, 현재 타겟 플랫폼의 특성을 깊이 이해하고 프로파일링 데이터를 기반으로 의사결정을 내리는 것이 진정한 최적화다.</p>
<p>Quake III의 Fast Inverse Square Root는 더 이상 우리가 사용해야 할 코드가 아니다. 하지만 그것이 대표하는 정신, 즉 문제의 본질을 꿰뚫어 보고 사용 가능한 모든 도구를 창의적으로 활용하는 엔지니어링 마인드는 여전히 유효하다. 그리고 바로 그 정신이야말로 이 알고리즘의 진정한 유산이 아닐까.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[DirectX 12 Ultimate 차세대 그래픽스 기술]]></title>
            <link>https://velog.io/@mazeline_1973/DirectX-12-Ultimate-%EC%B0%A8%EC%84%B8%EB%8C%80-%EA%B7%B8%EB%9E%98%ED%94%BD%EC%8A%A4-%EA%B8%B0%EC%88%A0</link>
            <guid>https://velog.io/@mazeline_1973/DirectX-12-Ultimate-%EC%B0%A8%EC%84%B8%EB%8C%80-%EA%B7%B8%EB%9E%98%ED%94%BD%EC%8A%A4-%EA%B8%B0%EC%88%A0</guid>
            <pubDate>Sun, 02 Nov 2025 16:38:04 GMT</pubDate>
            <description><![CDATA[<p>맨날 남들이 만들어놓은 게임엔진의 렌더링만 그냥 가져다 쓰다보니 앞으로의 트렌드가 어떻게 변할지 관심이 잘 가지않다가 이런 저런 이유로 어떤 것들의 특성을 잘 알아야 미래의 최적화 전략을 수립하는데 도움이 될까 하여 정리를 해 봤습니다. 지하철 읽을꺼리 글이니 저 처럼 한 시간 이상 지하철 타시는 분들은 쭉 보시는것도 좋겠네요.</p>
<hr>
<p>DirectX 12 Ultimate은 2020년 3월 마이크로소프트가 발표한 차세대 그래픽 API로, <strong>DXR 1.1, Variable Rate Shading Tier 2, Mesh Shaders, Sampler Feedback라는 4가지 핵심 기술을 통합</strong>한다. 가장 중요한 발견은 마이크로소프트가 &quot;DirectX 13&quot;을 출시하지 않고 대신 DX12 Ultimate을 지속적으로 개선하고 있으며, <strong>2025년 4월 DXR 1.2와 신경망 렌더링(Neural Rendering) 기능이 출시되어 그래픽스 프로그래밍의 새로운 패러다임을 연다</strong>는 점이다. 이는 PC와 Xbox 콘솔의 완전한 통합을 의미하며, Xbox Series X가 출시와 동시에 DX12 Ultimate을 지원함으로써 수백만 대의 DX12 Ultimate 호환 하드웨어가 시장에 형성되었다.</p>
<p>이 기술의 중요성은 단순한 성능 향상을 넘어서, <strong>레이트레이싱에서 최대 40% 성능 개선, Variable Rate Shading으로 8-20% GPU 시간 절약, Mesh Shaders로 45-55% 컬링 효율 향상, Sampler Feedback으로 VRAM을 2-3배 효과적으로 사용</strong>할 수 있다는 점에 있다. 하지만 개발 복잡도가 높아 업계 채택이 점진적으로 이루어지고 있으며, 2023년 Alan Wake 2가 Mesh Shader를 필수로 요구하는 첫 주요 게임이 되면서 전환점을 맞았다.</p>
<p><a href="https://www.nvidia.com/en-us/geforce/news/rtx-neural-rendering/"></a></p>
<blockquote>
<p>GDC 2025 - Neural Rendering in Real-Time Graphics 세션
2025년 3월 GDC에서 발표된 Neural Rendering 세션은 <strong>DXR 1.2와 통합된 신경망 렌더링 기술이 실시간 그래픽스에 가져올 혁명적 변화</strong>를 다뤘다. NVIDIA, AMD, Epic Games의 엔지니어들이 참여한 이 세션은 Neural Radiance Fields(NeRF), Neural Texture Compression, AI-Driven Denoising의 실제 게임 구현 사례를 공개했다.
핵심 발표 내용은 <strong>DirectX 12의 새로운 Neural Rendering API가 Tensor 코어와 AI 가속기를 직접 활용</strong>할 수 있도록 하여, 기존 셰이더 파이프라인과 신경망 추론을 통합한다는 점이다. NVIDIA의 RTX Neural Shaders는 레이트레이싱 노이즈 제거에서 기존 디노이저 대비 3배 빠른 성능을 보여주며, AMD의 FSR 4는 머신러닝 기반 업스케일링으로 네이티브 4K 대비 90% 품질을 유지하면서 2.5배 성능 향상을 달성했다.
Epic Games는 <strong>Unreal Engine 5.5에서 Neural Lumen을 시연</strong>했는데, 이는 기존 Lumen GI를 신경망으로 가속화하여 복잡한 간접 조명 시나리오에서 40% GPU 시간을 절약한다. 
중요한 기술적 세부사항으로는 <strong>FP8/INT4 양자화를 통한 추론 가속화, Shader Model 6.8의 새로운 TensorCore 내장 함수, 신경망 가중치의 효율적인 VRAM 관리</strong>가 논의되었다. 세션은 2025년 말까지 주요 게임 엔진들이 Neural Rendering을 표준 기능으로 통합할 것으로 예측하며, 이는 포토리얼리즘과 성능의 균형을 완전히 재정의할 것으로 전망했다.</p>
</blockquote>
<p><a href="https://developer.microsoft.com/en-us/games/articles/2025/03/gdc-2025-directx-state-of-the-union/">GDC 2025: DirectX State of the Union</a></p>
<h2 id="dxr-11이-가져온-레이트레이싱-혁명">DXR 1.1이 가져온 레이트레이싱 혁명</h2>
<p>DirectX Raytracing 1.1은 DXR 1.0의 기반 위에 <strong>인라인 레이트레이싱(Inline Raytracing)이라는 혁신적 기능</strong>을 추가한다. TraceRayInline을 사용하면 컴퓨트 셰이더, 픽셀 셰이더 등 모든 셰이더 단계에서 레이트레이싱을 실행할 수 있으며, 별도의 동적 셰이더나 셰이더 테이블 없이 RayQuery 객체를 로컬 변수처럼 사용한다. 이는 <strong>그림자 계산 같은 단순한 시나리오에서 동적 셰이더 스케줄링 오버헤드를 제거</strong>하여 성능을 크게 개선한다.</p>
<p>ExecuteIndirect 지원은 GPU가 CPU 왕복 없이 DispatchRays() 호출 목록을 직접 생성할 수 있게 하여, 셰이더 기반 컬링과 분류 후 즉시 레이트레이싱을 실행하는 적응형 시나리오를 가능하게 만든다. AddToStateObject() 함수는 기존 레이트레이싱 파이프라인에 셰이더를 점진적으로 추가할 수 있게 하는데, 예를 들어 <strong>1,000개 셰이더 파이프라인에 1개 셰이더를 추가할 때 새 셰이더만 검증</strong>하므로 CPU 오버헤드가 추가되는 셰이더에만 비례한다. 이는 오픈월드 게임의 스트리밍 시나리오에서 필수적이다.</p>
<p>DXR 1.1은 8가지 새로운 정점 포맷을 추가했으며(R16G16B16A16_UNORM, R10G10B10A2_UNORM 등), GeometryIndex() 내장 함수를 통해 셰이더가 하위 레벨 가속 구조 내에서 지오메트리를 구분할 수 있게 한다. 또한 RAY_FLAG_SKIP_TRIANGLES와 RAY_FLAG_SKIP_PROCEDURAL_PRIMITIVES 같은 새로운 레이 플래그가 추가되어 파이프라인 최적화가 가능해졌다. 중요한 점은 <strong>기존 DXR 1.0 Tier 1 디바이스가 드라이버 업데이트만으로 DXR 1.1을 지원</strong>할 수 있어 새로운 하드웨어가 필요하지 않다는 것이다.</p>
<p>2025년 4월 출시 예정인 <strong>DXR 1.2는 Opacity Micromaps(OMM)로 경로 추적 게임에서 최대 2.3배 성능 향상</strong>을 제공하며, Shader Execution Reordering(SER)은 복잡한 장면에서 최대 40% 개선(Remedy의 Alan Wake 2 데모 기준)을 보여준다. OMM은 알파 테스트 지오메트리를 최적화하여 삼각형 하위 디테일 수준에서 불투명/투명 영역의 any-hit 셰이더 호출을 제거한다. SER은 셰이더 실행을 지능적으로 그룹화하여 GPU 효율성을 높이고 분기를 줄여 프레임 속도를 높인다.</p>
<p>!youtube[CR-5FhfF5kQ?si=yGQrOwlkKp9B3Tbn]</p>
<p><strong>Opacity Micromaps (OMM)</strong></p>
<ul>
<li>알파 테스트 지오메트리를 최적화하여 path-traced 게임에서 최대 2.3배 성능 향상을 제공하며, 불투명도 데이터를 효율적으로 관리해 셰이더 호출을 줄이고 시각적 품질 손실 없이 렌더링 효율성을 크게 향상</li>
</ul>
<p><strong>Shader Execution Reordering (SER)</strong></p>
<ul>
<li>특정 시나리오에서 최대 2배 빠른 렌더링 성능을 제공하며, 셰이더 실행을 지능적으로 그룹화해 GPU 효율성을 높이고 divergence를 줄여 프레임 레이트를 향상</li>
</ul>
<p><a href="https://devblogs.microsoft.com/directx/announcing-directx-raytracing-1-2-pix-neural-rendering-and-more-at-gdc-2025/">Announcing DirectX Raytracing 1.2, PIX, Neural Rendering and more at GDC 2025!  - DirectX Developer Blog</a></p>
<h2 id="variable-rate-shading-tier-2의-실질적-성능-이득">Variable Rate Shading Tier 2의 실질적 성능 이득</h2>
<p>Variable Rate Shading Tier 2는 Tier 1의 드로우 단위 셰이딩 레이트를 넘어 <strong>드로우별 + 화면공간 이미지(8×8 또는 16×16 타일 단위) + 프리미티브별 셰이딩 레이트</strong>를 지원한다. 화면공간 이미지는 렌더 타겟 해상도가 아닌 매크로블록 해상도로 표현되며, 모션 블러, 피사계 심도, 투명도, HUD로 덮인 영역 등 다양한 품질 영역을 식별할 수 있는 &quot;디테일 레벨 마스크&quot;를 제공한다.</p>
<p>!youtube[2vKnKba0wxk?si=YiJfrpL9lb25_cPM]</p>
<p>실제 성능 데이터에서 <strong>Gears 5는 AMD RX 6900 XT에서 4K Ultra 설정으로 Quality 모드에서 8%, Performance 모드에서 12% 성능 향상</strong>을 보여줬다. SSGI를 추가한 4K Ultra 환경에서는 Quality 모드 14%, Balanced 모드 15%, Performance 모드 20%의 성능 향상이 측정되었다. 패스별 세부 절약량을 보면 Screen Space Reflections에서 1.27-1.49ms, Screen Space Ambient Occlusion에서 0.94-1.17ms가 절약되어 가장 큰 이득을 보였다.</p>
<p>Firaxis의 Civilization 게임은 Tier 1 구현으로 약 20% FPS 증가를, Tier 2 구현으로 14% FPS 증가를 달성했는데, <strong>Tier 2가 더 나은 품질 보존을 제공하면서도 상당한 성능 향상</strong>을 보여준다. 3DMark VRS 벤치마크는 40-60%의 성능 향상을 보여주며, 고해상도에서 더 큰 이득을 얻는다. 1080p 이하에서는 수익 체감이 나타나며, 4K에서 가장 효과적이다.</p>
<p>The Coalition의 Gears 5 구현 방법은 최종 장면 컬러 버퍼에서 Sobel 에지 감지를 실행하여 VRS 텍스처를 생성하는데, 지각적 차이 감지를 위해 sRGB 휘도에서 실행된다. 이 프로세스는 프레임 끝에서 실행되어 모션 블러, DOF, 후처리를 포착하며, <strong>Xbox Series X|S와 DX12 Ultimate GPU에서 0.1ms 미만의 오버헤드</strong>만 발생한다. 최적화 기법으로는 VRS 타일 경계에서 에지 감지를 건너뛰고(8×8의 경우 64→36 픽셀로 감소), 톤매핑 셰이더와 병합하여 대역폭 병목을 피하며, Async Compute Queue에서 실행하여 중첩을 활용한다.</p>
<p><a href="https://devblogs.microsoft.com/directx/gears-vrs-tier2/">Moving Gears to Tier 2 Variable Rate Shading - DirectX Developer Blog</a></p>
<h2 id="mesh-shaders가-기존-파이프라인을-완전히-재정의한다">Mesh Shaders가 기존 파이프라인을 완전히 재정의한다</h2>
<p>Mesh Shaders는 <strong>Input Assembler, Vertex Shader, Hull Shader, Domain Shader, Geometry Shader를 완전히 대체</strong>하는 혁명적 변화다. 컴퓨트 프로그래밍 모델을 사용하여 스레드 그룹으로 작동하며, 32-200개 정점의 &quot;meshlet&quot;을 병렬로 처리한다. 이상적인 meshlet 크기는 <strong>64개 정점과 126개 삼각형</strong>이며, 렌더링 중 정점 재사용을 최대화하기 위해 공유 정점을 최대화하도록 사전 계산되어 지오메트리와 함께 저장된다.</p>
<p>핵심 능력은 메시 청크의 병렬 처리(선형 반복 없음), 유연한 입력 데이터 형식과 압축, 정점별 및 프리미티브별 속성 모두 출력, 새로운 인덱스 버퍼를 메모리에 출력하지 않고 사전 컬링, Input Assembler 병목 제거다. Amplification Shader는 선택적 단계로 Mesh Shader 스레드 그룹을 디스패치하며, 테셀레이션 시나리오와 인스턴스별 컬링을 가능하게 한다.</p>
<p>성능 이점은 컬링 장점에서 극적으로 나타난다. Meshlet 레벨에서 절두체 컬링, 정점 처리 전 백페이스 컬링, meshlet별 경계를 사용한 오클루전 컬링, 완전히 GPU에서 LOD 선택이 가능하다. <strong>Xbox Series X 개발 데모는 4K에서 meshlet 구 컬링으로 100µs에서 55µs로 45% 감소</strong>(45% 개선)를 보여줬다. 3DMark Mesh Shader 테스트는 시나리오에 따라 500-1800%의 성능 향상을 보여주지만 이는 매우 시나리오 의존적이다.</p>
<p>실제 채택에서 <strong>Alan Wake 2(Remedy Entertainment, 2023)는 mesh shader를 요구하는 첫 주요 게임</strong>이다. Mesh shader 지원 없이 GTX 1080 Ti에서 10 FPS 미만으로 떨어지지만, 적절한 하드웨어 지원을 갖춘 RTX 20/30/40 시리즈에서는 잘 실행된다. Unreal Engine 5 Nanite는 조밀한 마이크로폴리곤 메시에 컴퓨트 셰이더 래스터라이저를, 더 큰 삼각형에는 프리미티브 셰이더(PS5) 또는 버텍스 셰이더를 사용하는 하이브리드 접근 방식을 사용한다.</p>
<h2 id="sampler-feedback으로-vram을-2-3배-효율적으로-활용한다">Sampler Feedback으로 VRAM을 2-3배 효율적으로 활용한다</h2>
<p>Sampler Feedback은 <strong>텍스처 샘플링 정보와 위치를 기록하는 하드웨어 기능</strong>으로, 샘플링 작업 중 어떤 타일이 액세스되었는지를 포착한다. MinMip 형식은 샘플링된 가장 상세한 mip를 저장하며, 스트리밍 시스템의 주요 형식으로 다음에 로드해야 할 mip를 쉽게 나타낸다. MipRegionUsed 형식은 요청된 mip 레벨의 비트필드로 작동하여 어떤 mip 레벨이 정확히 요청되었는지 보여준다.</p>
<p>메모리 절약 데이터에서 <strong>마이크로소프트 데모는 부정확한 피드백 근사치로 524,288 KB를 커밋한 반면, 정확한 sampler feedback으로 51,584 KB만 커밋하여 10.2배 메모리 감소</strong>(90% 절약)를 달성했다. Intel 샘플 구현은 전체 mip 체인을 가진 3개 텍스처로 66 MB를 사용했지만, Sampler Feedback로 텍스처 데이터의 절반만 로드하여 33 MB를 절약했다. 실질적으로 <strong>VRAM에 2-3배 승수로 작동</strong>하여, SFS를 사용하는 8GB 카드가 SFS 없이 16-24GB와 동등하며, VRAM 끊김과 팝인을 제거하고 물리적 VRAM을 증가시키지 않고도 더 높은 텍스처 충실도를 가능하게 한다.</p>
<p>성능 메트릭에서 3DMark Sampler Feedback 테스트는 RTX 3090에서 6.3%, RTX 3080에서 VRS 없이 90% 대비 전체 4K 해상도의 98% 유지, RTX 2080 Ti에서 5% 성능 향상을 보여준다. 하드웨어 전반에서 평균 4-10% 성능 향상이 나타난다.</p>
<p>주요 사용 사례는 <strong>텍스처 스트리밍(SFS - Sampler Feedback Streaming)</strong>으로, 렌더링에 실제로 필요한 텍스처 타일만 로드하여 로딩 시간과 메모리 압력을 줄인다. 고해상도 디스플레이(4K+)에 이상적이며, Tiled Resources(D3D12_TILED_RESOURCES_TIER_2) 및 DirectStorage와 페어링하여 최적 로딩 성능을 제공한다. 두 번째 사용 사례는 Texture-Space Shading(TSS)으로, 화면 공간 래스터화에서 월드 공간의 셰이딩을 분리하여 조명의 시간적 불안정성을 줄인다.</p>
<h2 id="2025년-기준-하드웨어-지원-현황과-제조사별-차이">2025년 기준 하드웨어 지원 현황과 제조사별 차이</h2>
<p>NVIDIA는 <strong>RTX 20 시리즈(Turing 아키텍처)부터 전체 DX12 Ultimate 지원</strong>을 제공하며, 전용 RT 코어(1세대)와 Tensor 코어(2세대)를 도입했다. RTX 30 시리즈(Ampere)는 2세대 RT 코어와 3세대 Tensor 코어를, RTX 40 시리즈(Ada Lovelace)는 3세대 RT 코어와 4세대 Tensor 코어를 탑재했다. <strong>2025년 출시된 RTX 50 시리즈(Blackwell)는 4세대 RT 코어, 5세대 Tensor 코어, GDDR7 메모리(RTX 5090에서 최대 32GB)</strong>를 지원하며, DLSS 4 Multi Frame Generation, Reflex 2, RTX Neural Shaders 같은 향상된 기능을 제공한다. 중요한 점은 GTX 16 시리즈(RT 코어 없는 Turing)와 이전 GTX 카드는 DX12 Ultimate을 지원하지 않는다.</p>
<p>AMD는 <strong>RDNA 2 - RX 6000 시리즈부터 전체 DX12 Ultimate 지원</strong>을 제공하며, 하드웨어 가속 레이트레이싱 가속기를 탑재하고 마이크로소프트와 긴밀히 협력하여 DX12 Ultimate을 개발했다. RDNA 2는 Xbox Series X/S 콘솔을 구동한다. RDNA 3 - RX 7000 시리즈는 2세대 레이트레이싱 가속기와 향상된 AI 가속기를 탑재하며, 칩렛 기반 설계(GCD + MCD)와 최대 24GB GDDR6 메모리를 제공한다. <strong>2025년 출시된 RDNA 4 - RX 9000 시리즈(RX 9070, RX 9070 XT)는 3세대 레이트레이싱 가속기(RDNA 3 대비 2배 처리량)</strong>와 FP8/INT4 지원 2세대 AI 가속기를 탑재하며, 머신러닝을 사용하는 FSR 4(RDNA 4 독점)를 제공한다. RDNA 1(RX 5000 시리즈)과 이전 GCN 기반 카드는 DX12 Ultimate을 지원하지 않는다.</p>
<p>Intel은 <strong>Arc A 시리즈(Alchemist - Xe-HPG 아키텍처)가 전체 DX12 Ultimate 지원(Feature Level 12_2)</strong>을 제공하며, 하드웨어 가속 레이트레이싱과 XeSS(Xe Super Sampling) AI 업스케일링, AV1 하드웨어 인코딩을 최초로 제공하는 소비자 GPU다. 중요한 제한사항은 <strong>Intel Iris Xe Graphics(11-13세대 Core 프로세서)는 DirectX 12 Feature Level 12_1만 지원하며 DX12 Ultimate을 지원하지 않는다</strong>. Mesh shader, 하드웨어 레벨 sampler feedback 같은 DX12 Ultimate 기능이 누락되어 있어 DX12 Ultimate을 요구하는 게임을 실행할 수 없다.</p>
<p>제조사별 구현 차이에서 <strong>NVIDIA는 2018년 Turing으로 모든 DX12 Ultimate 기능을 최초로 탑재</strong>했으며, DX12 Ultimate 기능 세트가 Turing 아키텍처 설계를 밀접하게 따른다. 독점 개선사항으로 DLSS(RTX 독점 AI 업스케일링), DLSS 4 Multi Frame Generation(RTX 50 시리즈 독점), RTX Neural Shaders(Blackwell), 우수한 레이트레이싱 성능을 위한 고급 RT 코어 아키텍처, 첫날부터 성숙한 드라이버 최적화를 제공한다.</p>
<p>AMD는 오픈 표준에 초점을 맞추어 독점 기술보다 DirectX 표준 기능을 우선시하며, 마이크로소프트와 긴밀히 협력하여 DX12 Ultimate 설계에 참여했다. 콘솔 통합으로 RDNA 2가 Xbox Series X/S를 구동하여 DX12 Ultimate의 광범위한 채택을 보장한다. 독점 개선사항으로 오픈소스 크로스 벤더 호환 업스케일링인 FidelityFX Super Resolution(FSR), ML 가속을 사용하는 FSR 4(RDNA 4 독점), AMD HYPR-RX 기술 제품군, 강력한 비동기 컴퓨트 구현을 제공한다. 구현 철학은 GPU 친화적 레이트레이싱을 위한 DXR 1.1에 중점을 둔다.</p>
<p>Intel은 신규 진입자 이점으로 Arc GPU가 DX12 Ultimate과 현대 API를 위해 처음부터 구축되었으며, 기본 DX12/DX11 지원과 번역 레이어를 통한 DX9 지원(초기에는 열악했지만 드라이버로 크게 개선됨)을 제공한다. 독점 개선사항으로 매트릭스 엔진을 사용하는 XeSS AI 업스케일링, 최초로 시장에 출시된 하드웨어 AV1 인코딩, CPU+GPU 협업을 위한 Deep Link 기술을 제공한다. 드라이버 성숙도는 출시 이후 크게 개선되었지만 여전히 AMD/NVIDIA를 따라잡고 있다.</p>
<h2 id="실제-게임에서-나타난-dx12-ultimate의-적용과-성능">실제 게임에서 나타난 DX12 Ultimate의 적용과 성능</h2>
<p><strong>Alan Wake 2(2023년 10월, Remedy Entertainment)는 mesh shader를 요구하는 첫 주요 게임이다.</strong> DXR 1.1(경로 추적), DLSS 3.5 Ray Reconstruction을 사용한다. 전체 지오메트리 렌더링 파이프라인이 mesh shader를 위해 재작성되었으며, 하드웨어 요구사항은 RDNA 2+(AMD) 또는 Turing+(NVIDIA)이다. 성능 측면에서 RTX 2060 Super는 1080p Medium에서 45-60 FPS를 달성하지만, mesh shader 지원 없는 <strong>GTX 1080 Ti는 1080p 최저 설정에서 10 FPS 미만</strong>으로 떨어진다.</p>
<p>!youtube[EtX7WnFhxtQ?si=CIggkjUA3LDv-biS]</p>
<p><strong>Cyberpunk 2077 + Phantom Liberty</strong>는 가장 포괄적인 레이트레이싱 구현을 보여주며, 레이트레이싱 그림자, 반사, 주변 차폐, 방출 조명, 전역 조명을 모두 지원한다. Path Tracing 모드(Overdrive)는 완전히 레이트레이싱된 렌더링을 제공한다. 성능 분석에서 <strong>RTX 4090 네이티브 4K에서 RT 없이 67-77 FPS, RT Psycho로 47 FPS, Path Tracing으로 3.1 FPS 네이티브</strong>(DLSS 3 + Frame Gen으로 57 FPS)를 기록했다. RTX 3070은 1440p RT Ultra에서 DLSS Quality를 사용하여 39 FPS에서 71 FPS로 82% 향상을 보여준다.</p>
<p><strong>Gears Tactics(2020년 4월, The Coalition)는 VRS를 탑재한 첫 PC 게임이다.</strong> 토글 가능한 VRS와 조정 가능한 레벨을 제공한다. RTX 2080 Ti, 4K 최고 설정에서 기준선 47 FPS, VRS 표준 53 FPS(13% 증가), VRS 최대 57 FPS(21% 증가)를 기록했다. 시각적 영향은 표준 설정에서 최소에서 인지 불가능한 품질 손실이며, 최대 VRS에서만 이미지 저하가 보인다.</p>
<p>!youtube[DHIPHOsdyLY?si=xhZzhT1ax6Lqq4Vm]</p>
<p><strong>Metro Exodus Enhanced Edition(2021)은 레이트레이싱 전역 조명(RTGI)을 전체적으로 사용한다.</strong> RT 전용 타이틀로, 전통적인 래스터화 조명 폴백이 없다. Digital Foundry는 &quot;레이트레이싱이 일부 단순히 장관을 이루는 &#39;차세대 수준&#39; 순간을 제공한다&quot;고 평가했다. RTGI는 선택적으로 레이트레이싱된 반사보다 더 요구사항이 높으며, 픽셀당 광선이 선택적이 아닌 캐스팅된다.</p>
<p>!youtube[FKHmVyPiIOc?si=msb2mBQvydp_16t7]</p>
<p>성능 경향에서 DirectX Raytracing의 최소 요구사항은 RTX 2060 / RX 6600 XT이며, 60 FPS 1440p 권장은 RTX 3070 / RX 6800 XT, 4K RT 최적은 RTX 4080 / RTX 4090이다. <strong>레이트레이싱을 활성화할 때 구현에 따라 15-70% 성능 손실</strong>을 예상해야 하며, DLSS/FSR 업스케일링은 고해상도에서 플레이 가능한 레이트레이싱 경험을 위해 필수적이다.</p>
<h2 id="개발자가-직면하는-복잡도와-도구-환경">개발자가 직면하는 복잡도와 도구 환경</h2>
<p>DX12 Ultimate로의 마이그레이션은 <strong>시스템적으로 Windows 10 Version 2004(2020년 5월 업데이트) 최소, Visual Studio 2019/2022, Windows 10 SDK(19041+), 호환 GPU 하드웨어, 업데이트된 그래픽 드라이버</strong>를 요구한다. 핵심 고려사항은 DX12 Ultimate 기능이 선택적 향상이라는 점이다. 이러한 기능을 사용하는 게임은 여전히 비-DX12 Ultimate 하드웨어에서 실행되지만 시각적 혜택 없이 실행되어, 이 후방 호환성이 마이그레이션 압력을 줄이지만 채택 인센티브도 줄인다.</p>
<p>구현 난이도는 <strong>전체적으로 높음(HIGH)</strong>으로 평가된다. <a href="http://GameDev.net">GameDev.net</a>의 개발자 의견에서 &quot;DX12는 DX11보다 어렵습니다. DX11은 DX10보다 어렵습니다... DX12가 DX11보다 어렵다고 말하는 것은 LA에서 뉴욕으로 걷는 것이 LA에서 보스턴으로 걷는 것보다 어렵다고 말하는 것과 같습니다&quot;라고 표현된다. 또한 &quot;DirectX 12는 현대 그래픽 애플리케이션을 작성하는 데 사용해야 하는 것이 아닙니다. 미리 알고 실제 이득을 얻을 것을 알 때 사용하는 것입니다&quot;라는 조언이 있다.</p>
<p>기능별 복잡도에서 <strong>DirectX Raytracing(DXR 1.1)은 매우 높다.</strong> 가속 구조, 광선 생성/closest-hit/any-hit/miss 셰이더에 대한 이해가 필요하며 가파른 학습 곡선을 가진다. 일반적인 문제는 일관성을 위한 셰이더 실행 재정렬 관리, 페이로드 최적화(성능을 위해 최소로 유지해야 함), TraceRay vs TraceRayInline 접근 방식 이해, 분기 셰이더 실행 및 데이터 액세스 패턴이다.</p>
<p><strong>Mesh Shaders 의 높은 복잡도.</strong> 컴퓨트 셰이더 배경이 도움이 되는 중간-높음 학습 곡선을 가진다. 일반적인 문제는 meshlet 분할 전략(32-200 정점 최적), 전통적인 정점/지오메트리 파이프라인 사고방식에서 전환, 스레드 그룹 관리 및 groupshared 메모리 사용, meshlet 데이터의 사전 계산 및 저장이다. <strong>Variable Rate Shading(VRS)은 중간</strong>으로 가장 쉬운 DX12 Ultimate 기능으로, &quot;개발자가 구현하기에 상대적으로 비용이 낮음&quot;이라고 평가된다. <strong>Sampler Feedback의 높은 복잡도.</strong> 정교한 텍스처 스트리밍 아키텍처가 필요한 높은 학습 곡선을 가진다.</p>
<p>일반적인 DX12 문제는 수동, 명시적 리소스 상태 전환이 필요한 메모리 관리, 복잡한 CPU-GPU 동기화 프리미티브, 신중한 오케스트레이션이 필요한 멀티스레드 명령 목록 구축, &quot;중복되거나 지나치게 보수적인 배리어 플래그&quot;가 DX11→DX12 포트에서 주요 성능 문제가 되는 배리어 관리, &quot;모든 구조체가 다른 구조체로 구성된 것처럼 느껴지는&quot; 혼란스러운 초기화를 가진 Pipeline State Objects(PSO), 명령 목록과 PSO에서 두 번 설정해야 하는 Root Signatures, GPU 충돌/중단 같은 저수준 오류가 문제 해결하기 어려운 디버깅이다.</p>
<p>학습 곡선 타임라인은 최소 전제조건으로 C++ 기초, 그래픽 프로그래밍 기초가 필요하며, DX11 또는 유사한 API 경험이 권장되고, Vulkan 경험(매우 유사한 아키텍처)이 강력히 권장된다. 시간 투자는 &quot;전체를 이해하는 데 몇 개월&quot;이 소요된다.</p>
<p>개발 도구로 <strong>Microsoft PIX</strong>가 주요 DX12 디버깅 도구로, GPU 캡처 및 프레임 분석, 셰이더 디버깅(PIX 2003.26부터 DXIL 지원), CPU/GPU 상관관계가 있는 타임라인 시각화, DX12 Ultimate 기능 지원(DXR 1.1, Mesh Shaders, VRS, Sampler Feedback), 하드웨어 카운터 플러그인(NVIDIA, AMD)을 제공한다. <strong>RenderDoc</strong>은 크로스 API 지원(DX11, DX12, Vulkan, OpenGL), 오픈 소스 및 무료, &quot;경량 및 빠름&quot;, 프레임 캡처 및 검사에 탁월, 강력한 커뮤니티 지원을 제공하며, 개발자는 &quot;RenderDoc은 PC에서 렌더링 및 컴퓨트 문제를 추적하는 우리의 #1 도구&quot;라고 평가한다.</p>
<p>공식 Microsoft 문서는 포괄적인 API 참조, DirectX Specs 저장소, 각 DX12 Ultimate 기능에 대한 기능 사양, 시작 가이드라는 강점이 있지만, &quot;DirectX 문서는 읽기 어렵고 어려웠습니다(매우 인간 친화적이지 않음 - 약어에서 길을 잃습니다)&quot;라는 약점이 있다. 샘플 코드 가용성은 <strong>우수한 커버리지</strong>로, DirectX-Graphics-Samples(증분 학습을 위한 Hello World 샘플, VRS, DXR 1.0, Mesh Shader 예제, 정기적으로 업데이트됨), 전체 참조 구현인 MiniEngine, Xbox ATG 샘플, 헬퍼 클래스 및 유틸리티를 제공하는 DirectXTK12를 제공한다.</p>
<p>업계 채택 장벽은 주요 장애물로 <strong>개발 비용 대 이익</strong>(상당한 엔지니어링 투자 필요, 많은 프로젝트에서 미미한 성능 향상이 비용을 정당화하지 못함), <strong>문서 및 학습 리소스 부족</strong>(API의 새로움과 그에 따른 문서 부족, 기존 팀에게 가파른 학습 곡선, 제한된 &quot;모범 사례&quot; 지침), <strong>DX11 코드 경로 유지 필요</strong>(DX11을 지원하면서 DX12를 완전히 최적화할 수 없음, 이중 유지 관리 부담, 코드 중복), <strong>하드웨어 분열</strong>(모든 DX12 하드웨어가 Ultimate 기능을 지원하지 않아 폴백 렌더링 경로 유지 필요), <strong>&quot;DX12 tax&quot;</strong>(열악한 구현이 DX11보다 성능이 나쁠 수 있으며, API 선택보다 개발자 기술이 더 중요)이다.</p>
<h2 id="directx의-미래와-신경망-렌더링으로의-전환">DirectX의 미래와 신경망 렌더링으로의 전환</h2>
<p>가장 중요한 발견은 <strong>&quot;DirectX 13&quot;이 없다</strong>는 것이다. 마이크로소프트는 전통적인 버전 번호 매기기 접근 방식을 포기했으며, DirectX 12 Ultimate은 <strong>Agility SDK를 통한 지속적인 업데이트</strong>로 개선되어, OS 업데이트를 기다리지 않고 새로운 기능이 개발자에게 도달할 수 있다. 2025년 4월 출시 예정인 주요 향후 기능으로 <strong>DirectX Raytracing 1.2(DXR 1.2)는 복잡한 장면에서 최대 40% 개선</strong>을 보여주며, Opacity Micromaps(OMM)는 경로 추적 게임에서 2.3배 성능 향상을, Shader Execution Reordering(SER)은 최대 2배 빠른 렌더링을 제공한다.</p>
<p>!youtube[3J5tJPA0yzc?si=aTcJQfoGoDmu-bPL]</p>
<ul>
<li><strong>Cooperative Vectors / Neural Rendering(2025년 4월 프리뷰)</strong>은 혁명적 전환으로, <strong>AI/ML 추론을 셰이더 내에서 직접 실행 가능</strong>하게 하여 마이크로소프트가 &quot;3D 그래픽 프로그래밍의 새로운 패러다임&quot;이라고 설명하는 기능이다. 기술적 능력은 작은 신경망이 전체 GPU 리소스를 소비하지 않고 픽셀 셰이더에서 실행될 수 있으며, NVIDIA RTX GPU의 Tensor Core에 직접 액세스하고 AMD, Intel, Qualcomm의 크로스 벤더 지원을 제공한다. 사용 사례는 신경망 텍스처 압축(Intel당 10배 속도 향상), 재질 셰이딩, 조명 최적화, 실시간 디노이징을 포함한다. 마이크로소프트의 인용문: &quot;DirectX는 우리 모두가 신경망 렌더링의 미래를 구축하는 데 도움을 줄 것이다.&quot;</li>
<li><strong>DirectSR(Super Resolution API, GDC 2024 발표)</strong>는 NVIDIA DLSS, AMD FSR, Intel XeSS를 단일 API로 통합하여 공통 코드 경로를 통한 멀티 벤더 업스케일링을 가능하게 하며, Xbox 콘솔에 업스케일링 기술을 마침내 가져올 수 있다. <strong>Work Graphs(2024/2025 프로덕션 준비)</strong>는 GPU가 자체 작업 스케줄링을 자율적으로 관리하며 Shader Model 6.8로 구동되는 GPU 병렬화에 대한 혁명적 접근 방식이다.</li>
</ul>
<p>산업 트렌드에서 <strong>PC/콘솔 통합 효과</strong>는 중요하다. Xbox Series X가 DX12 Ultimate과 함께 출시되어 콘솔 출시 시 &quot;수백만 대의 DX12 Ultimate PC 그래픽 카드&quot;를 생성했다. 개발자 이점은 두 플랫폼 모두를 위한 단일 개발 타겟으로, &quot;내 프로그래머가 DX12 Ultimate을 알고 있으면 PC 또는 콘솔 중 하나를 코딩할 수 있다&quot;(KeokeN Interactive CEO)는 것이다. 결과는 이전 DirectX 세대보다 훨씬 빠른 기능 채택이다.</p>
<p>레이트레이싱 확산은 DX12 Ultimate 발표(2020) 시 30개 이상의 DirectX Ray Tracing 게임에서 이제 100개 이상의 게임이 DLSS 4를 지원하며(DLSS 3보다 2년 빠르게 마일스톤 도달) 산업 합의로 콘솔 통합으로 레이트레이싱 채택이 &quot;급증할 것으로 예상&quot;된다. Mesh Shaders 각성은 2018년 NVIDIA가 지원했지만 DX12 Ultimate 표준화 전까지 활용도가 낮았으며, 실시간으로 수백만/수십억 폴리곤의 영화 품질 자산 렌더링을 가능하게 하여 차세대에 중요하며 지오메트리 파이프라인의 완전한 재구성을 허용한다.</p>
<p>경쟁 환경에서 DirectX 12는 Windows/Xbox 독점이지만 PC 게임 시장을 지배하며, 25년 이상의 DirectX 유산, 안정적인 도구, 광범위한 문서, 주요 게임 엔진(Unreal, Unity)과의 깊은 통합이라는 강점이 있다. <strong>Vulkan</strong>은 Windows, Linux, macOS(MoltenVK를 통해), Android, Switch를 지원하는 크로스 플랫폼 강점과, Red Dead Redemption 2에서 1080p에서 9% 높은 평균 FPS, World War Z에서 DX12 대비 21% FPS 증가 같은 벤치마크에서 종종 DX12를 능가하는 성능, Khronos Group 표준 오픈 소스로 플랫폼 종속 없음, Steam Deck, 클라우드 게임 플랫폼(Stadia), 콘솔 지원(Switch, PS5가 Vulkan 개념 사용)으로 &quot;증기를 얻고 있는&quot; 시장 모멘텀을 가지고 있다. 전문가 의견: &quot;Vulkan 1.3으로... PC 게이머가 DirectX를 버릴 때가 마침내 왔을 수 있다&quot;(Digital Trends).</p>
<p>2024-2025 경쟁 환경의 핵심 발견은 API가 하나가 지배하기보다 <strong>수렴</strong>하고 있다는 것이다. &quot;DirectX, Vulkan, Metal이 유사한 저수준 접근 방식을 가진 주요 경쟁자&quot;이며, Vulkan 1.3(2022)이 분열된 기능 기반을 통합하여 개발을 더 쉽게 만들었다. 비평적 인용: &quot;멀티 플랫폼 개발의 경우 Vulkan을 무시하기 어렵다&quot;. </p>
<p><a href="http://itfix.org.uk/">It Fix - Your Trusted Computer Repair Experts</a></p>
<p>전문가 예측으로 Windows 우선 게임의 경우 DirectX 12가 &quot;안전하고 안정적인 선택&quot;(2024 분석)이지만, 멀티 플랫폼의 경우 Vulkan의 크로스 플랫폼 기능이 &quot;요구가 많은 AAA 타이틀에서 성능 이점&quot;을 제공한다. 추세는 더 많은 게임이 DX12와 Vulkan 옵션을 모두 제공하여 플레이어가 선택할 수 있게 하는 것이다.</p>
<p>차세대 게임에서 콘솔 영향으로 <strong>Xbox Series X는 DirectX 12 Ultimate 지원과 함께 출시된 첫 콘솔</strong>이며, 네 가지 기둥 모두 지원(DXR 1.1, VRS, Mesh Shaders, Sampler Feedback)으로 PC와 콘솔 간 &quot;전례 없는 정렬&quot;을 생성한다. 산업 영향은 &quot;PC와 Xbox Series X에서 차세대 그래픽을 잠금 해제하는 단일 키&quot;라는 개발자 효율성과, &quot;Xbox Series X가 출시될 때 동일한 기능 세트를 가진 수백만 대의 DX12 Ultimate PC 그래픽 카드가 이미 세계에 있어 빠른 채택을 촉진할 것&quot;이라는 시장 역학, PC와 콘솔 사이클이 이제 독립적으로 작동하는 대신 &quot;상승적으로 결합&quot;되는 시너지 효과다.</p>
<p>클라우드 게임 혁명에서 DirectSR의 클라우드 게임 영향은 마이크로소프트가 디바이스 전반에 걸쳐 확장을 위해 DirectSR을 통합하고 있으며, 잠재적인 Xbox Cloud Gaming 개선이 예정되어 있고, 주요 움직임으로 마이크로소프트 Xbox.com이 이제 Xbox Cloud Gaming과 함께 <strong>Nvidia GeForce NOW를 지원</strong>(2024년 7월)한다. 클라우드 게임 시장 성장은 GPU 클라우드 렌더링 서비스 시장이 57억 달러(2024) → 907억 달러(2034)로 32% CAGR로 성장하며, DirectX 최적화가 클라우드 스트리밍에 중요하다.</p>
<p>전문가 예측에서 <strong>AI/ML 그래픽 혁명</strong>에 대한 합의 관점은 신경망 렌더링이 기능이 아니라 패러다임 전환이라는 것이다. 마이크로소프트의 비전(DirectX 팀)은 &quot;신경망 렌더링 기술은 중요한 진화를 나타낸다&quot;, &quot;게임 비주얼과 영화의 최첨단 CGI 간의 격차를 메운다&quot;, cooperative vectors가 &quot;신경망 셰이딩으로 Tensor Core의 힘을 잠금 해제할 것&quot;이다. NVIDIA의 관점: &quot;신경망 셰이딩은 그래픽 프로그래밍의 혁명을 나타낸다&quot;, &quot;25년 전 NVIDIA는 GeForce와 프로그래머블 셰이더를 도입했다... 새로운 GeForce RTX 50 시리즈 GPU와 함께 NVIDIA는 RTX Neural Shaders를 도입한다&quot;, 예측: 신경망 셰이더는 2002년 픽셀 셰이더만큼 기본이 될 것이다.</p>
<p>장기 트렌드(2025-2030)는 신경망 렌더링이 표준이 되고(2025-2027, Intel이 cooperative vectors로 텍스처 압축에서 10배 속도 향상 시연, NVIDIA가 RTX Mega Geometry로 최대 100배 더 렌더링 가능한 삼각형 달성), API 수렴이 계속되며(&quot;DirectX 12, Vulkan, Metal, WebGPU 같은 저수준 그래픽 API는 GPU가 현재 구축되는 방식과 유사한 모델로 수렴&quot;), 성능 승수(DLSS 4가 &quot;최대 8배&quot; 프레임 속도 승수 달성, DXR 1.2가 특정 시나리오에서 2-10배 개선 제공, 합의: AI를 통한 소프트웨어 최적화가 원시 하드웨어 개선을 초과할 것)가 나타난다.</p>
<h2 id="결론-점진적-채택-속에서-열리는-신경망-그래픽스-시대">결론: 점진적 채택 속에서 열리는 신경망 그래픽스 시대</h2>
<p>DirectX 12 Ultimate은 프로그래머블 셰이더 이후 가장 중요한 그래픽 API 발전을 나타내며, <strong>2025년 4월 출시 예정인 신경망 렌더링은 1990년대 후반 고정 함수 파이프라인에서 프로그래머블 셰이더로의 전환에 필적하는 패러다임 전환</strong>을 예고한다. 레이트레이싱은 상당한 성능 비용에도 불구하고 변혁적 시각적 결과로 주류 채택을 달성했으며, Variable Rate Shading은 최소한의 구현 복잡성으로 입증된 성능 이득을 제공하고, Mesh Shaders는 느린 시작 후 지오메트리 처리의 미래로 부상하고 있다.</p>
<p>핵심 전략적 함의는 마이크로소프트가 전통적인 주요 버전 번호 매기기를 포기하고 Agility SDK를 통한 반복적 개선에 전념했다는 것이다. <strong>PC와 콘솔의 통합은 성공적</strong>이며, Xbox Series X + DX12 Ultimate 전략이 전체 게임 생태계에서 기능 채택을 가속화했다. Vulkan이 성장하고 있지만 DirectX가 지배적이며, 경쟁 환경은 승자 독식보다 게임에서 멀티 API 지원으로 전환하고 있다.</p>
<p>개발자 관점에서 DX12 Ultimate은 강력한 기능을 제공하지만 상당한 개발자 투자가 필요하며, 복잡성 장벽과 학습 곡선은 광범위한 채택에 대한 실질적인 장애물로 남아 있다. AAA 타이틀의 경우 DX12 Ultimate이 새로운 표준이 되고 있지만, 소규모/중간 규모 개발자의 경우 DX11 또는 엔진 추상화가 더 실용적인 선택으로 남아 있다. 도구는 개선되고 있지만 여전히 성숙 중이며, PIX와 RenderDoc 같은 도구가 필수적이다.</p>
<p><strong>2025-2030 타임라인</strong>에서 2025년 2분기에 DXR 1.2, Cooperative Vectors, DirectSR 프리뷰가 출시되고, 2025-2026년에 AAA 타이틀에서 신경망 렌더링 채택이 시작되며, 2026-2027년에 신경망 셰이더가 게임 엔진의 표준 기능이 되고, 2028년 이후 DirectX 신경망 렌더링을 통해 AI 생성 그래픽 콘텐츠가 주류가 될 것으로 예상된다. 미래는 단순히 더 빠른 렌더링이 아니라 AI와 그래픽스의 근본적인 융합이며, DirectX는 이 변환의 최전선에 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Unity URP Edge Fusion 간단 소개 및 분석]]></title>
            <link>https://velog.io/@mazeline_1973/Unity-URP-Edge-Fusion-%EA%B0%84%EB%8B%A8-%EC%86%8C%EA%B0%9C-%EB%B0%8F-%EB%B6%84%EC%84%9D</link>
            <guid>https://velog.io/@mazeline_1973/Unity-URP-Edge-Fusion-%EA%B0%84%EB%8B%A8-%EC%86%8C%EA%B0%9C-%EB%B0%8F-%EB%B6%84%EC%84%9D</guid>
            <pubDate>Fri, 31 Oct 2025 16:03:33 GMT</pubDate>
            <description><![CDATA[<p>!youtube[duV4-qgeT7w?si=6sM6iyNNkQqU6N_-]</p>
<h2 id="들어가며">들어가며</h2>
<p>최근 고객사의 요청이 있던 부분과 관련 된 Edge Fusion은 Unity URP에서 오브젝트 간 경계를 자연스럽게 블렌딩하는 포스트 프로세싱 효과입니다. 블렌더에서 구현 된 것을 이 전에 본적이 있어서 구현 준비를 했습니다만 Kronnect 에서 어제 릴리스를 했습니다. 본 글에서는 이를 구성하는 3가지 핵심 알고리즘에 대해 살펴보겠습니다.</p>
<hr>
<h2 id="1-objectid-패스를-통한-오브젝트-식별">1. ObjectID 패스를 통한 오브젝트 식별</h2>
<h3 id="문제-정의">문제 정의</h3>
<p>일반적인 렌더링 과정에서는 최종 화면에 색상 정보만 남게 되며 오브젝트 정보는 소실됩니다. Edge Fusion이 엣지를 정확하게 찾기 위해서는 각 픽셀이 어떤 오브젝트에 속하는지 식별할 수 있어야 합니다.</p>
<h3 id="해결책-objectid-texture-생성">해결책: ObjectID Texture 생성</h3>
<p>EdgeFusionRenderPass는 별도의 렌더 패스를 통해 모든 오브젝트를 고유 ID로 렌더링하는 방식을 사용합니다.</p>
<h3 id="objectid-생성-방법">ObjectID 생성 방법</h3>
<pre><code class="language-jsx">// 1단계: 오브젝트 위치를 기반으로 고유값 생성
float3 p = TransformObjectToWorld(float3(1, 1, 1));
float objectID = dot(p, 1.0);  // x + y + z

// 2단계: Instancing ID 추가
#if UNITY_ANY_INSTANCING_ENABLED
    objectID += unity_InstanceID;
#endif

// 3단계: 커스텀 ID가 있으면 덮어쓰기
if (_CustomObjectId &gt; 0.5) 
    objectID = _CustomObjectId;

// 4단계: 정수로 변환
output.objectID = floor(objectID);</code></pre>
<h3 id="objectid-texture-구조">ObjectID Texture 구조</h3>
<p>단순히 ID만 저장하는 것이 아니라, 4개 채널에 여러 정보를 패킹하여 저장합니다.</p>
<pre><code class="language-jsx">return float4(packedR, rawDepth, normalVS.xy);</code></pre>
<p><strong>채널별 의미:</strong></p>
<ul>
<li><strong>R 채널</strong>: ObjectID와 Radius를 인코딩 (정수부=ID, 소수부=radius)</li>
<li><strong>G 채널</strong>: 원시 깊이값 (카메라 Z 버퍼)</li>
<li><strong>BA 채널</strong>: View Space Normal의 XY 성분 (Intra-Object Fusion용)</li>
</ul>
<h3 id="원리-이해">원리 이해</h3>
<pre><code class="language-jsx">오브젝트 A의 World Position: (5, 10, 3)
→ objectID = 5 + 10 + 3 = 18

오브젝트 B의 World Position: (2, 7, 1)  
→ objectID = 2 + 7 + 1 = 10

같은 위치의 인스턴스:
→ objectID = 18 + InstanceID(1) = 19</code></pre>
<h3 id="중요성">중요성</h3>
<p>이러한 방식을 통해 셰이더는 픽셀 단위로 인접한 픽셀이 동일한 오브젝트에 속하는지 여부를 판단할 수 있으며, 이를 통해 엣지를 감지할 수 있습니다.</p>
<pre><code class="language-jsx">// Blend Pass에서 사용 예시
float myObjectID = UnpackObjectId(objectIDTexture[currentPixel].r);
float neighborObjectID = UnpackObjectId(objectIDTexture[neighborPixel].r);

if (myObjectID != neighborObjectID) {
    // 다른 오브젝트 = 엣지 발견
}</code></pre>
<hr>
<h2 id="2-방사형-샘플링과-binary-search를-활용한-정밀한-엣지-위치-탐색">2. 방사형 샘플링과 Binary Search를 활용한 정밀한 엣지 위치 탐색</h2>
<h3 id="문제-정의-1">문제 정의</h3>
<p>상하좌우 4방향만 체크하는 단순한 방식으로는 대각선 방향의 엣지를 놓칠 수 있습니다.</p>
<h3 id="1단계-방사형-샘플링-radial-sampling">1단계: 방사형 샘플링 (Radial Sampling)</h3>
<p>현재 픽셀을 중심으로 원형으로 여러 방향을 샘플링하는 방식을 사용합니다.</p>
<pre><code class="language-jsx">// EdgeFusionBlendPass.hlsl (의사코드)
for (int i = 0; i &lt; sampleCount; i++) {
    // 360도를 sampleCount로 나눔
    float angle = (i / sampleCount) * TWO_PI;

    // 방향 벡터 계산
    float2 direction = float2(cos(angle), sin(angle));

    // 현재 픽셀로부터 radius만큼 떨어진 위치 샘플링
    float2 sampleUV = currentUV + direction * radius;

    // 해당 위치의 ObjectID 확인
    float neighborObjectID = SampleObjectID(sampleUV);

    if (neighborObjectID != myObjectID) {
        // 엣지 발견
    }
}</code></pre>
<h3 id="샘플링-시각화">샘플링 시각화</h3>
<pre><code class="language-jsx">sampleCount = 8인 경우:

        7
    6   ↑   0
       \|/
  5 ←--[나]--→ 1
       /|\
    4   ↓   2
        3

8방향으로 샘플링
각도: 0°, 45°, 90°, 135°, 180°, 225°, 270°, 315°</code></pre>
<h3 id="samplecount에-따른-정밀도">sampleCount에 따른 정밀도</h3>
<ul>
<li><strong>sampleCount = 4 (Very Low)</strong>: 낮은 정밀도, 높은 성능</li>
<li><strong>sampleCount = 24 (High)</strong>: 높은 정밀도, 낮은 성능</li>
<li><strong>sampleCount = 32 (Very High)</strong>: 최고 정밀도, 최저 성능</li>
</ul>
<h3 id="2단계-binary-search를-통한-정밀화">2단계: Binary Search를 통한 정밀화</h3>
<p>방사형 샘플링을 통해 엣지가 존재하는 방향을 파악할 수 있지만, 정확한 거리는 알 수 없습니다. 이를 해결하기 위해 이진 탐색 기법을 적용합니다.</p>
<pre><code class="language-jsx">// 1. 초기 범위 설정
float minDist = 0.0;          // 내 위치
float maxDist = radius;       // 최대 샘플링 거리

// 2. 이진 탐색 반복 (binarySearchSteps 횟수만큼)
for (int step = 0; step &lt; binarySearchSteps; step++) {
    // 중간 지점 샘플링
    float midDist = (minDist + maxDist) * 0.5;
    float2 midUV = currentUV + direction * midDist;
    float midObjectID = SampleObjectID(midUV);

    if (midObjectID == myObjectID) {
        // 아직 내 오브젝트 영역
        minDist = midDist;
    } else {
        // 다른 오브젝트 영역
        maxDist = midDist;
    }
}

// 3. 최종 엣지 거리
float edgeDistance = (minDist + maxDist) * 0.5;</code></pre>
<h3 id="binary-search-과정-시각화">Binary Search 과정 시각화</h3>
<p><strong>binarySearchSteps = 3 예시:</strong></p>
<pre><code class="language-jsx">Step 0: 초기 범위
[나:5]================================[이웃:12]
0m                                    0.1m
       ↓ 중간 체크 (0.05m)
[나:5]================|===============[이웃:12]
                   (ID=5, 아직 내 영역)
       → minDist = 0.05m

Step 1: 범위 좁히기
              [나:5]========|========[이웃:12]
              0.05m      0.075m      0.1m
                      ↓ 중간 체크
              [나:5]====|=====[이웃:12]
                     (ID=12, 지나침)
              → maxDist = 0.075m

Step 2: 더 좁히기
              [나:5]==|==[이웃:12]
              0.05    0.0625   0.075
                    ↓ 중간 체크
              [나:5]=|=[이웃:12]
                  (ID=5, 아직 내 영역)
              → minDist = 0.0625m

최종: 엣지는 약 0.0625m ~ 0.075m 사이
     → 평균: 0.06875m</code></pre>
<h3 id="정밀도-비교">정밀도 비교</h3>
<ul>
<li><strong>binarySearchSteps = 2</strong>: ±0.025m 오차</li>
<li><strong>binarySearchSteps = 5</strong>: ±0.003m 오차</li>
<li><strong>binarySearchSteps = 8</strong>: ±0.0004m 오차 (0.4mm)</li>
</ul>
<h3 id="최적화-early-exit-hits">최적화: Early Exit Hits</h3>
<p>모든 방향을 검사하지 않고 충분한 수의 엣지를 발견하면 조기 종료하는 최적화 기법을 사용합니다.</p>
<pre><code class="language-jsx">int edgeHitCount = 0;

for (int i = 0; i &lt; sampleCount; i++) {
    // 샘플링 과정

    if (foundEdge) {
        edgeHitCount++;

        // 충분한 엣지를 발견하면 중단
        if (edgeHitCount &gt;= earlyExitHits) {
            break;
        }
    }
}</code></pre>
<p><strong>효과:</strong></p>
<ul>
<li><strong>earlyExitHits = 1</strong>: 첫 엣지 발견 시 즉시 종료 (최고 성능)</li>
<li><strong>earlyExitHits = 5</strong>: 5개 엣지 발견 후 종료 (균형)</li>
<li><strong>earlyExitHits = 32</strong>: 모든 샘플 검사 (최고 품질)</li>
</ul>
<hr>
<h2 id="3-거리-기반-블렌딩을-통한-자연스러운-경계-처리">3. 거리 기반 블렌딩을 통한 자연스러운 경계 처리</h2>
<h3 id="문제-정의-2">문제 정의</h3>
<p>단순히 50:50 비율로 색상을 혼합할 경우 부자연스러운 결과가 발생합니다. 거리에 따라 부드럽게 감쇠(falloff)시키는 처리가 필요합니다.</p>
<h3 id="falloff-함수-감쇠-곡선">Falloff 함수 (감쇠 곡선)</h3>
<pre><code class="language-jsx">// 엣지까지의 거리 정규화
float normalizedDistance = edgeDistance / radius;
// 0.0 = 엣지 바로 위
// 1.0 = 최대 블렌딩 거리

// 감쇠 곡선 계산 (smoothstep)
float falloff = 1.0 - normalizedDistance;
falloff = smoothstep(0.0, 1.0, falloff);
// 0에 가까울수록 블렌딩 약함
// 1에 가까울수록 블렌딩 강함</code></pre>
<h3 id="감쇠-곡선-시각화">감쇠 곡선 시각화</h3>
<p><img src="https://velog.velcdn.com/images/mazeline_1973/post/d92ad635-362a-46ee-b833-300a85ebb2d4/image.png" alt=""></p>
<h3 id="블렌딩-공식">블렌딩 공식</h3>
<p>여러 방향에서 발견한 엣지들을 가중 평균 방식으로 블렌딩합니다.</p>
<pre><code class="language-jsx">// 1. 각 방향의 가중치 계산
float totalWeight = 0.0;
float3 blendedColor = float3(0, 0, 0);

for (each edgeDirection) {
    float dist = edgeDistances[i];
    float weight = CalculateFalloff(dist, radius);

    // 엣지 너머의 색상 샘플링
    float3 neighborColor = SampleColor(edgePositions[i]);

    // 가중치 누적
    blendedColor += neighborColor * weight;
    totalWeight += weight;
}

// 2. 정규화
if (totalWeight &gt; 0) {
    blendedColor /= totalWeight;
}

// 3. 원본 색상과 블렌딩
float3 originalColor = SampleColor(currentPixel);
float3 finalColor = lerp(originalColor, blendedColor, intensity * globalFalloff);</code></pre>
<h3 id="실제-적용-예시">실제 적용 예시</h3>
<p><strong>상황: 빨간 큐브와 파란 구가 맞닿아 있는 경계선 근처 픽셀 분석</strong></p>
<pre><code class="language-jsx">1. ObjectID 확인
   → 내 ID = 5 (빨간 큐브)

2. 방사형 샘플링 (8방향)
   - 0° (→): ID=5 (동일, 엣지 없음)
   - 45° (↗): ID=5 (동일)
   - 90° (↑): ID=12 (상이, 엣지 발견, 거리=0.03m)
   - 135° (↖): ID=12 (상이, 엣지 발견, 거리=0.04m)
   - 180° (←): ID=5 (동일)
   - 나머지 방향도 검사

3. Binary Search로 정밀화
   - 90° 방향 엣지: 정확히 0.028m
   - 135° 방향 엣지: 정확히 0.037m

4. 가중치 계산 (radius=0.05m)
   - 90° 가중치: 1.0 - (0.028/0.05) = 0.44
   - 135° 가중치: 1.0 - (0.037/0.05) = 0.26

5. 블렌딩
   - 90° 위치의 파란색: RGB(0, 0, 255) * 0.44
   - 135° 위치의 파란색: RGB(0, 0, 255) * 0.26
   - 가중 평균 계산
   - 원본 빨간색과 혼합

6. 최종 색상
   RGB(255, 0, 0) → RGB(180, 0, 75) (약간 보라빛)
   → 경계가 부드럽게 처리됨</code></pre>
<h3 id="추가-기능">추가 기능</h3>
<h3 id="shadow-protection">Shadow Protection</h3>
<pre><code class="language-jsx">// 그림자 영역 감지
float shadow = 1.0 - saturate(luminance(originalColor) / threshold);

// 그림자에서는 블렌딩 약화
blendStrength *= (1.0 - shadow * shadowProtection);</code></pre>
<p><strong>적용 이유</strong>: 그림자 경계는 실제 오브젝트 경계가 아니므로, 블렌딩 시 부자연스러운 결과가 발생할 수 있습니다.</p>
<h3 id="noise">Noise</h3>
<pre><code class="language-jsx">// 3D 노이즈 텍스처 샘플링
float noise = tex3D(noiseTex, worldPos * noiseScale);

// 블렌딩 반경에 노이즈 추가
float adjustedRadius = radius * (1.0 + noise * noiseIntensity);</code></pre>
<p><strong>효과</strong>: 기계적이지 않은 자연스러운 변화를 연출합니다.</p>
<h3 id="max-screen-radius">Max Screen Radius</h3>
<pre><code class="language-jsx">// 화면 공간에서 radius 계산
float screenRadius = WorldRadiusToScreenRadius(radius, depth);

// 최대값 제한
screenRadius = min(screenRadius, maxScreenRadius * screenHeight);</code></pre>
<p><strong>적용 이유</strong>: 먼 거리의 오브젝트에서 과도한 블렌딩이 발생하는 것을 방지합니다.</p>
<hr>
<h2 id="기술적-우수성">기술적 우수성</h2>
<h3 id="edge-fusion의-기술적-이점">Edge Fusion의 기술적 이점</h3>
<ul>
<li>엣지 영역만 정확하게 타겟팅</li>
<li>오브젝트 인식 기반 처리</li>
<li>거리에 따른 자연스러운 감쇠</li>
<li>디테일을 유지하면서 경계만 부드럽게 처리</li>
</ul>
<hr>
<h2 id="성능과-품질의-균형">성능과 품질의 균형</h2>
<h3 id="quality-preset-비교">Quality Preset 비교</h3>
<table>
<thead>
<tr>
<th>Preset</th>
<th>Sample Count</th>
<th>Binary Search</th>
<th>Early Exit</th>
<th>권장 용도</th>
</tr>
</thead>
<tbody><tr>
<td>Very Low</td>
<td>4</td>
<td>2</td>
<td>1</td>
<td>모바일, 저사양 환경</td>
</tr>
<tr>
<td>Low</td>
<td>8</td>
<td>4</td>
<td>2</td>
<td>일반 게임</td>
</tr>
<tr>
<td>Medium</td>
<td>16</td>
<td>5</td>
<td>3</td>
<td>균형잡힌 설정</td>
</tr>
<tr>
<td>High</td>
<td>24</td>
<td>7</td>
<td>4</td>
<td>고품질 게임</td>
</tr>
<tr>
<td>Very High</td>
<td>32</td>
<td>8</td>
<td>5</td>
<td>시네마틱, 스크린샷</td>
</tr>
</tbody></table>
<h3 id="최적화-권장사항">최적화 권장사항</h3>
<ol>
<li><strong>maxBlendDistance</strong> 설정을 통해 먼 거리의 오브젝트를 처리 대상에서 제외</li>
<li><strong>maxScreenRadius</strong>를 활용하여 화면 공간 제한 적용</li>
<li><strong>earlyExitHits</strong> 값을 낮춰 조기 종료 빈도 증가</li>
<li><strong>Quality Preset</strong>을 통한 일괄 조정</li>
</ol>
<hr>
<h2 id="마치며">마치며</h2>
<p>Edge Fusion은 다음 3단계의 알고리즘을 통해 자연스러운 엣지 블렌딩을 구현합니다.</p>
<ol>
<li><strong>ObjectID 패스</strong>: 각 오브젝트를 고유 ID로 식별</li>
<li><strong>방사형 샘플링과 Binary Search</strong>: 정밀한 엣지 위치 탐색</li>
<li><strong>거리 기반 블렌딩</strong>: 부드러운 감쇠 곡선을 통한 자연스러운 경계 처리</li>
</ol>
<p>이러한 기술들의 조합을 통해 디테일을 유지하면서도 경계를 부드럽게 처리하는 고품질 렌더링 효과를 달성할 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[포토샵에서 TIFF 알파 채널이 보이지 않는 이유]]></title>
            <link>https://velog.io/@mazeline_1973/%ED%8F%AC%ED%86%A0%EC%83%B5%EC%97%90%EC%84%9C-TIFF-%EC%95%8C%ED%8C%8C-%EC%B1%84%EB%84%90%EC%9D%B4-%EB%B3%B4%EC%9D%B4%EC%A7%80-%EC%95%8A%EB%8A%94-%EC%9D%B4%EC%9C%A0-16-bit%EC%9D%98-%EB%B9%84%EB%B0%80</link>
            <guid>https://velog.io/@mazeline_1973/%ED%8F%AC%ED%86%A0%EC%83%B5%EC%97%90%EC%84%9C-TIFF-%EC%95%8C%ED%8C%8C-%EC%B1%84%EB%84%90%EC%9D%B4-%EB%B3%B4%EC%9D%B4%EC%A7%80-%EC%95%8A%EB%8A%94-%EC%9D%B4%EC%9C%A0-16-bit%EC%9D%98-%EB%B9%84%EB%B0%80</guid>
            <pubDate>Wed, 29 Oct 2025 19:09:42 GMT</pubDate>
            <description><![CDATA[<p>텍스처 아틀라스 툴을 개발하면서 예상치 못한 문제를 마주했습니다. Unity 엔진에서는 완벽하게 보이는 RGBA 텍스처의 알파 채널이, 포토샵에서는 아예 표시되지 않는 것이었습니다. 같은 TIFF 파일인데 왜 다르게 보이는 걸까요?</p>
<h2 id="문제의-시작">문제의 시작</h2>
<p>툴에서 생성한 Mask Map(M 텍스처)은 RGB 채널에 다양한 물리 기반 정보를, 알파 채널에는 Smoothness 값을 담고 있습니다. Unity에서 이 텍스처를 열면 4개 채널이 모두 정상적으로 보입니다. 그런데 아티스트가 포토샵으로 열면 알파 채널이 존재하지 않는다고 나오는 것이었습니다.</p>
<p>처음에는 단순히 TIFF 파일의 메타데이터 문제라고 생각했습니다. TIFF 포맷에는 <code>ExtraSamples</code>라는 태그가 있어서, 4번째 채널을 어떻게 해석할지 정의합니다:</p>
<ul>
<li><code>UNSPECIFIED(0)</code>: 용도 미지정</li>
<li><code>ASSOCALPHA(1)</code>: 사전 곱셈된 알파</li>
<li><code>UNASSALPHA(2)</code>: 일반 알파</li>
</ul>
<p>분명 이 값을 조정하면 해결될 거라 생각했습니다. 여러 조합을 테스트해봤지만 결과는 동일했습니다. 포토샵은 여전히 알파 채널을 인식하지 못했습니다.</p>
<h2 id="실마리를-찾다">실마리를 찾다</h2>
<p>돌파구는 의외의 곳에서 찾을 수 있었습니다. 아티스트가 포토샵으로 직접 만든 RGBA TIFF 파일을 분석해보기로 했습니다. 같은 4채널 TIFF인데 포토샵이 만든 파일은 알파가 보이고, 툴이 만든 파일은 안 보인다면 분명 차이점이 있을 것이라 생각했습니다.</p>
<p>두 파일의 메타데이터를 비교한 결과, 여러 차이점을 발견했습니다:</p>
<p><strong>툴이 생성한 파일:</strong></p>
<ul>
<li>BitsPerSample: 8</li>
<li>Compression: 5 (LZW)</li>
<li>ExtraSamples: 0 (UNSPECIFIED) 또는 누락</li>
</ul>
<p><strong>포토샵이 생성한 파일:</strong></p>
<ul>
<li>BitsPerSample: <strong>16</strong></li>
<li>Compression: <strong>1 (무압축)</strong></li>
<li>ExtraSamples: 2 (UNASSALPHA)</li>
</ul>
<p>여러 요소가 다르게 설정되어 있었습니다.</p>
<h2 id="문제의-핵심">문제의 핵심</h2>
<p>실험을 거듭한 결과, 포토샵이 TIFF 파일의 알파 채널을 제대로 인식하려면 다음 조건들이 함께 충족되어야 했습니다:</p>
<ol>
<li><strong>ExtraSamples 태그가 명시적으로 1 또는 2로 설정</strong></li>
<li><strong>16-bit 비트 뎁스</strong></li>
<li>무압축 또는 특정 압축 방식</li>
</ol>
<p>흥미롭게도, 일반적으로 포토샵은 8-bit RGBA TIFF도 인식할 수 있습니다. 하지만 이번 케이스에서는 툴이 생성한 8-bit TIFF에서 <code>ExtraSamples</code> 태그가 제대로 설정되지 않았거나, <code>PhotometricInterpretation</code>과 같은 다른 메타데이터가 표준을 완전히 따르지 않았을 가능성이 높습니다.</p>
<aside>
🍃

<p>결과적으로 16-bit로 전환하면서 TIFF 저장 로직을 전체적으로 재작성했고, 이 과정에서 모든 메타데이터가 올바르게 설정되면서 문제가 해결된 것으로 보입니다. 16-bit 자체가 필수 조건이라기보다는, 16-bit 워크플로우로 전환하면서 TIFF 포맷 표준을 더 정확히 준수하게 된 것이 핵심이었습니다.</p>
</aside>

<h2 id="해결-과정">해결 과정</h2>
<p>문제를 이해하고 나니 해결책은 명확했습니다. M 텍스처를 가져올 때 8-bit에서 16-bit로 변환하고, 동시에 모든 TIFF 메타데이터를 올바르게 설정하는 것이었습니다:</p>
<pre><code class="language-python"># 0-255 범위를 0-65535 범위로 변환
value_16bit = value_8bit × 257</code></pre>
<p>왜 256이 아니라 257을 곱하는지 궁금하실 수 있습니다. 정확한 범위 매핑을 위해서입니다:</p>
<ul>
<li>255 × 256 = 65,280 (최댓값이 65,535에 미치지 못함)</li>
<li>255 × 257 = 65,535 (정확히 16-bit 최댓값)</li>
</ul>
<p>이렇게 변환된 16-bit TIFF를 적절한 메타데이터와 함께 무압축으로 저장하니, 포토샵에서 알파 채널이 완벽하게 표시되었습니다.</p>
<h2 id="트레이드오프-품질-vs-용량">트레이드오프: 품질 vs 용량</h2>
<p>물론 이 해결책에는 대가가 따릅니다. 2048×2048 RGBA 텍스처 하나를 기준으로 비교하면:</p>
<ul>
<li>8-bit LZW 압축: 약 4.2 MB (압축률은 텍스처 내용에 따라 다름)</li>
<li>16-bit 무압축: <strong>32 MB</strong> (2048 × 2048 × 4채널 × 2바이트)</li>
</ul>
<p>약 7~8배의 용량 증가입니다. 256개 슬롯을 사용하는 프로젝트라면 약 8GB의 저장 공간이 필요합니다.</p>
<p>하지만 이는 충분히 받아들일 만한 트레이드오프였습니다. 아티스트가 포토샵에서 텍스처를 열어 알파 채널을 확인하고 수정할 수 있다는 것은, 작업 효율성 측면에서 훨씬 더 중요하기 때문입니다. 또한 Unity의 고품질 압축 포맷(BC7, ASTC 등)은 16-bit 소스를 더 잘 활용할 수 있어, 최종 빌드 품질도 향상됩니다.</p>
<h2 id="배운-교훈">배운 교훈</h2>
<p>이번 문제를 통해 <strong>&quot;작동한다&quot;와 &quot;모든 곳에서 작동한다&quot;는 다르다</strong>는 것을 다시 한번 깨달았습니다. TIFF는 표준 포맷이지만, 각 소프트웨어는 표준을 미묘하게 다르게 해석하며, 특정 메타데이터 조합에서 예상치 못한 문제가 발생할 수 있습니다.</p>
<p>문서만으로는 이런 차이를 파악하기 어렵습니다. 실제로 타겟 소프트웨어에서 생성한 파일을 분석하고, 직접 테스트하는 것이 가장 확실한 방법입니다.</p>
<p>그리고 무엇보다, 사용자(아티스트)의 피드백이 가장 중요한 테스트였습니다. &quot;포토샵에서 알파가 안 보여요&quot;라는 한 마디가, 며칠간의 디버깅보다 더 명확하게 문제를 정의해주었습니다.</p>
<hr>
<p>이제 툴은 포토샵과 완벽하게 호환되는 TIFF를 생성합니다. 작은 파일 포맷 하나에도 이렇게 많은 고려사항이 숨어있다는 것, 그것이 기술 개발의 매력이자 도전입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[언리얼 엔진 5.X의 Windows 11 한글 IME 입력 문제 해결]]></title>
            <link>https://velog.io/@mazeline_1973/%EC%96%B8%EB%A6%AC%EC%96%BC-%EC%97%94%EC%A7%84-5.X%EC%9D%98-Windows-11-%ED%95%9C%EA%B8%80-IME-%EC%9E%85%EB%A0%A5-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@mazeline_1973/%EC%96%B8%EB%A6%AC%EC%96%BC-%EC%97%94%EC%A7%84-5.X%EC%9D%98-Windows-11-%ED%95%9C%EA%B8%80-IME-%EC%9E%85%EB%A0%A5-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Wed, 29 Oct 2025 15:06:59 GMT</pubDate>
            <description><![CDATA[<h1 id="들어가기">들어가기</h1>
<p>이 해결은 메이즈라인 고객사 중 하나인 “게임테일즈” 클라이언트실/테크아트팀/최성현 사원이 외부 참조 리퀘스트 자료를 참조 하고 해결하는 과정을 소개 하고 있습니다. </p>
<p>언리얼 엔진 5.0(UE5)을 Windows 11 환경에서 사용하는 과정에서 한글 및 중국어와 같은 다국어 입력 시스템(IME, Input Method Editor)과 관련된 특정 입력 오류가 발견되었습니다. 특히, 한글 입력 중 스페이스바를 통한 띄어쓰기 후 자음이나 모음을 입력할 때 정상적으로 조합되지 않는 현상이 지속적으로 보고되었습니다.</p>
<p>본 문서에서는 이러한 문제의 근본 원인을 분석하고, 엔진 소스 코드 레벨에서 적용한 해결 방안을 상세히 기술하고자 합니다.</p>
<h1 id="windows-11-ime-아키텍처의-변화와-문제의-발생-원인">Windows 11 IME 아키텍처의 변화와 문제의 발생 원인</h1>
<h2 id="새로운-ime-시스템의-특성">새로운 IME 시스템의 특성</h2>
<p>Windows 11은 이전 버전들과 비교하여 입력 메서드 시스템(IME)에 상당한 변화를 도입하였습니다. 특히 Text Services Framework(TSF)의 구현 방식이 개선되면서, IME와 애플리케이션 간의 통신 프로토콜이 보다 엄격해졌습니다.</p>
<p>핵심적인 문제는 <strong>TSF의 <code>ITextStoreACP</code> 인터페이스 구현에서 발생</strong>합니다. Windows 11의 새로운 IME는 다음과 같은 특성을 보입니다:</p>
<ol>
<li><strong>빈 텍스트 범위(Empty Range) 쿼리 증가</strong>: 새로운 IME는 입력 컨텍스트를 파악하기 위해 <code>acpStart == acpEnd</code>인 경우도 빈번하게 요청합니다.</li>
<li><strong>텍스트 경계 계산의 엄격화</strong>: 커서 위치와 조합 중인 텍스트의 위치를 계산할 때 라인 높이(Line Height)를 포함한 정확한 바운딩 박스를 요구합니다.</li>
<li><strong>텍스트 변경 통보의 필요성</strong>: IME가 조합 상태를 올바르게 유지하려면 애플리케이션 측에서 텍스트 변경 사항을 명시적으로 통보해야 합니다.</li>
</ol>
<p>언리얼 엔진 5.0의 초기 구현은 Windows 10 이하의 IME 동작 방식을 기준으로 설계되었기 때문에, 이러한 새로운 요구사항에 대응하지 못했습니다.</p>
<h2 id="구체적인-오류-시나리오">구체적인 오류 시나리오</h2>
<p>사용자가 한글을 입력하는 과정을 단계별로 살펴보겠습니다:</p>
<ol>
<li>사용자가 &quot;안녕&quot;을 입력합니다.</li>
<li>스페이스바를 눌러 띄어쓰기를 합니다.</li>
<li>다음 단어의 첫 자음 &quot;ㅎ&quot;를 입력합니다.</li>
</ol>
<p>이 시점에서 Windows 11의 IME는 다음과 같은 작업을 수행합니다:</p>
<ul>
<li>현재 커서 위치의 텍스트 범위를 요청 (<code>GetTextExt</code> 호출, <code>acpStart == acpEnd</code>)</li>
<li>조합 윈도우를 표시할 위치를 계산하기 위해 텍스트 바운딩 박스를 요청</li>
</ul>
<p>그러나 언리얼 엔진의 기존 구현은:</p>
<ul>
<li><code>acpStart == acpEnd</code>인 경우 <code>E_INVALIDARG</code> 오류를 반환</li>
<li>텍스트 바운딩 박스 계산 시 라인 높이를 고려하지 않아 부정확한 위치 반환</li>
</ul>
<p>이로 인해 IME가 조합 상태를 정상적으로 초기화하지 못하고, 입력된 자모가 별도의 문자로 처리되는 현상이 발생합니다.</p>
<h1 id="이전-버전-ime-사용-시-문제가-해결되는-이유">이전 버전 IME 사용 시 문제가 해결되는 이유</h1>
<p>많은 사용자들이 Windows 설정에서 &quot;이전 버전의 Microsoft IME 사용&quot; 옵션을 활성화하면 문제가 해결된다는 것을 발견했습니다. 이는 다음과 같은 이유 때문입니다:</p>
<h2 id="호환성-모드의-동작-방식">호환성 모드의 동작 방식</h2>
<p>이전 버전의 IME(Windows 10 스타일)는:</p>
<ol>
<li><strong>느슨한 에러 핸들링</strong>: <code>acpStart == acpEnd</code>인 경우에도 유효한 요청으로 처리하거나, 오류를 받더라도 대체 로직을 사용합니다.</li>
<li><strong>단순화된 위치 계산</strong>: 텍스트 바운딩 박스 계산이 덜 엄격하며, 근사값을 허용합니다.</li>
<li><strong>비동기 통보의 유연성</strong>: 텍스트 변경 통보가 누락되어도 IME 자체적으로 상태를 재동기화하는 로직을 포함합니다.</li>
</ol>
<p>따라서 언리얼 엔진의 불완전한 TSF 구현에도 불구하고, 이전 버전 IME는 자체적인 fallback 메커니즘을 통해 정상적으로 동작할 수 있었습니다.</p>
<p>그러나 이는 근본적인 해결책이 아니며, Windows 11의 새로운 IME가 제공하는 향상된 입력 경험과 성능을 활용할 수 없다는 한계가 있습니다.</p>
<h1 id="엔진-소스-코드-수정-사항">엔진 소스 코드 수정 사항</h1>
<p>문제를 근본적으로 해결하기 위해 IME와 언리얼 엔진 간의 통신 경로를 추적했습니다. Windows의 Text Services Framework(TSF)는 계층적 구조로 동작하는데, 에러가 발생하는 지점을 역추적 할 수 있습니다.</p>
<h3 id="문제-진단-과정">문제 진단 과정</h3>
<p>문제 해결을 위해 IME와 언리얼 엔진 간의 통신 경로를 계층별로 추적했습니다. 가장 먼저 발견한 증상은 한글 입력 시 IME 조합 창이 부적절한 위치에 표시되거나 아예 표시되지 않는 현상이었습니다. TSF 디버깅을 통해 <code>GetTextExt</code> 메서드에서 <code>E_INVALIDARG</code> 오류가 반환되는 것을 확인했고, TSF의 핵심 인터페이스인 <code>ITextStoreACP</code>를 구현하는 <code>TextStoreACP.cpp</code>를 분석한 결과 <code>acpStart == acpEnd</code> 조건을 엄격하게 거부하는 로직이 Windows 11의 새로운 IME 동작 방식과 충돌하는 것을 발견했습니다.</p>
<p>그러나 <code>TextStoreACP</code>의 수정만으로는 문제가 완전히 해결되지 않았습니다. 간헐적으로 IME가 아예 동작하지 않는 경우가 있었는데, 이는 초기화 순서 문제입니다. Windows 11에서 IME 시스템은 유효한 윈도우 핸들이 존재할 때만 정상적으로 초기화되는데, <code>FWindowsApplication</code>의 생성자에서 윈도우 생성 전에 <code>TextInputMethodSystem</code>을 초기화하려는 코드가 원인이었습니다. 초기화 위치를 <code>InitializeWindow</code> 함수로 옮기는 것만으로 안정성을 확보할 수 있습니다.</p>
<p>마지막으로 남은 문제는 스페이스바 입력 후 다음 자모 입력이 제대로 처리되지 않는 현상이었습니다. 언리얼 엔진의 UI 프레임워크인 Slate가 실제 텍스트 입력을 처리하는 레이어로, <code>SlateEditableTextLayout.cpp</code>를 분석한 결과 두 가지 문제를 발견했습니다. 첫째, 텍스트 편집이 완료될 때 IME에게 명시적인 통보(<code>NotifyTextChanged</code>)를 하지 않았고, 둘째, 텍스트 바운딩 박스를 계산할 때 라인 높이를 고려하지 않아 IME가 부정확한 위치 정보를 받고 있었습니다. 이 두 가지를 수정하여 문제를 해결할 수 있습니다.</p>
<h3 id="수정-대상-파일의-역할-정리">수정 대상 파일의 역할 정리</h3>
<p>결과적으로, 세 개의 파일은 각각 다음과 같은 계층에서 문제를 해결합니다:</p>
<ol>
<li><strong>TextStoreACP.cpp</strong> (TSF 프로토콜 레벨): Windows와의 저수준 통신 프로토콜 준수</li>
<li><strong>WindowsApplication.cpp</strong> (애플리케이션 생명주기 레벨): IME 시스템의 올바른 초기화 순서 보장</li>
<li><strong>SlateEditableTextLayout.cpp</strong> (UI 레벨): 사용자 입력을 받아 IME와 동기화</li>
</ol>
<p>이 세 레이어가 모두 올바르게 동작해야만 Windows 11의 엄격해진 IME 시스템과 완벽하게 호환될 수 있었습니다.</p>
<hr>
<h2 id="구체적인-코드-수정-내역">구체적인 코드 수정 내역</h2>
<p>문제를 근본적으로 해결하기 위해 다음 세 개의 핵심 파일을 수정하였습니다.</p>
<h2 id="1-textstoreacpcpp의-수정">1. TextStoreACP.cpp의 수정</h2>
<p><code>TextStoreACP</code>는 TSF의 <code>ITextStoreACP</code> 인터페이스를 구현하는 클래스로, IME와 애플리케이션 간의 텍스트 데이터 교환을 담당합니다.</p>
<h3 id="11-gettextext-함수의-개선">1.1 GetTextExt 함수의 개선</h3>
<p><strong>기존 코드의 문제점:</strong></p>
<pre><code class="language-cpp">const LONG StringLength = TextContext-&gt;GetTextLength();

// Begin and end indices must not be equal.
if(acpStart == acpEnd)
{
    return E_INVALIDARG;
}

// Validate range.
if( acpStart &lt; 0 || acpStart &gt; StringLength || ( acpEnd != -1 &amp;&amp; ( acpEnd &lt; 0 || acpEnd &gt; StringLength) ) )
{
    return TS_E_INVALIDPOS;
}</code></pre>
<p>이 코드는 <code>acpStart == acpEnd</code>인 경우를 명시적으로 오류로 처리했습니다. 그러나 Windows 11 IME는 커서 위치의 텍스트 범위를 확인하기 위해 이러한 쿼리를 정당하게 사용합니다.</p>
<p><strong>수정된 코드:</strong></p>
<pre><code class="language-cpp">const LONG StringLength = TextContext-&gt;GetTextLength();
// Validate range.
if( acpStart &lt; 0 || acpStart &gt; StringLength || ( acpEnd != -1 &amp;&amp; ( acpEnd &lt; 0 || acpEnd &gt; StringLength) ) )
{
    return TS_E_INVALIDPOS;
}</code></pre>
<p>빈 범위 검증을 제거함으로써, IME가 커서 위치의 바운딩 박스를 정상적으로 조회할 수 있게 되었습니다.</p>
<h3 id="12-gettext-함수의-최적화">1.2 GetText 함수의 최적화</h3>
<p><strong>기존 코드의 문제점:</strong></p>
<pre><code class="language-cpp">const LONG StringLength = TextContext-&gt;GetTextLength();

// Validate range.
if( acpStart &lt; 0 || acpStart &gt; StringLength || ( acpEnd != -1 &amp;&amp; ( acpEnd &lt; 0 || acpEnd &gt; StringLength) ) )
{
    return TF_E_INVALIDPOS;
}

const uint32 BeginIndex = acpStart;
const uint32 Length = (acpEnd == -1 ? StringLength : acpEnd) - BeginIndex;

for(uint32 i = 0; i &lt; Length &amp;&amp; *pcchPlainOut &lt; cchPlainReq; ++i)
{
    pchPlain[i] = StringInRange[i];
    ++(*pcchPlainOut);
}</code></pre>
<p>이 구현에는 두 가지 문제가 있었습니다:</p>
<ol>
<li><code>acpEnd == -1</code>의 처리가 검증 이후에 이루어져 로직이 복잡함</li>
<li>버퍼 크기(<code>cchPlainReq</code>)를 고려하지 않고 <code>Length</code>를 계산하여 버퍼 오버플로우 가능성 존재</li>
</ol>
<p><strong>수정된 코드:</strong></p>
<pre><code class="language-cpp">const LONG StringLength = TextContext-&gt;GetTextLength();
if (acpEnd == -1)
{
    acpEnd = StringLength;
}
// Validate range.
if( acpStart &lt; 0 || acpStart &gt; StringLength || acpEnd &lt; 0 || acpEnd &gt; StringLength )
{
    return TF_E_INVALIDPOS;
}

const uint32 BeginIndex = acpStart;
const uint32 Length = FMath::Min(cchPlainReq, acpEnd - BeginIndex);

for(uint32 i = 0; i &lt; Length; ++i)
{
    pchPlain[i] = StringInRange[i];
    ++(*pcchPlainOut);
}</code></pre>
<p>주요 개선 사항:</p>
<ul>
<li><code>acpEnd == -1</code>을 먼저 처리하여 검증 로직을 단순화</li>
<li><code>FMath::Min</code>을 사용하여 버퍼 크기를 초과하지 않도록 안전장치 추가</li>
<li>루프 조건에서 불필요한 중복 검사 제거로 성능 향상</li>
</ul>
<h3 id="13-run-info-처리-개선">1.3 Run Info 처리 개선</h3>
<p><strong>수정된 코드:</strong></p>
<pre><code class="language-cpp">if(prgRunInfo &amp;&amp; ulRunInfoReq &gt; 0 &amp;&amp; Length &gt; 0)
{
    prgRunInfo[0].uCount = Length;
    prgRunInfo[0].type = TS_RT_PLAIN;
    ++(*pulRunInfoOut);
}</code></pre>
<p><code>Length &gt; 0</code> 검증을 추가하여, 빈 텍스트 범위에 대해서도 안전하게 처리할 수 있도록 개선하였습니다.</p>
<h2 id="2-windowsapplicationcpp의-수정">2. WindowsApplication.cpp의 수정</h2>
<h3 id="텍스트-입력-메서드-시스템의-초기화-시점-변경">텍스트 입력 메서드 시스템의 초기화 시점 변경</h3>
<p><strong>기존 코드의 문제점:</strong></p>
<pre><code class="language-cpp">// FWindowsApplication 생성자에서
OleInitialize( NULL );

#if !USING_ADDRESS_SANITISER
TextInputMethodSystem = MakeShareable( new FWindowsTextInputMethodSystem );
if(!TextInputMethodSystem-&gt;Initialize())
{
    TextInputMethodSystem.Reset();
}
#endif</code></pre>
<p><code>FWindowsApplication</code>의 생성자에서 <code>TextInputMethodSystem</code>을 초기화하면, 아직 윈도우가 생성되지 않은 상태에서 IME 컨텍스트를 설정하려고 시도할 수 있습니다. 이는 Windows 11에서 IME 초기화 실패로 이어질 수 있습니다.</p>
<p><strong>수정된 코드:</strong></p>
<pre><code class="language-cpp">// InitializeWindow 함수에서
Windows.Add( Window );
Window-&gt;Initialize( this, InDefinition, InstanceHandle, ParentWindow, bShowImmediately );

#if !USING_ADDRESS_SANITISER
static const bool InitInputMethodSystem = [this] {
    TextInputMethodSystem = MakeShareable(new FWindowsTextInputMethodSystem);
    if (!TextInputMethodSystem-&gt;Initialize())
    {
        TextInputMethodSystem.Reset();
    }
    return true;
}();
#endif</code></pre>
<p>주요 개선 사항:</p>
<ul>
<li>윈도우 초기화 완료 후에 <code>TextInputMethodSystem</code>을 생성</li>
<li><code>static</code> 람다를 사용하여 전체 애플리케이션 생명주기 동안 <strong>단 한 번만</strong> 초기화되도록 보장</li>
<li>윈도우 핸들이 유효한 상태에서 IME 컨텍스트가 생성되므로, 안정성이 크게 향상됨</li>
</ul>
<p>이 변경은 특히 중요한데, Windows 11의 IME는 유효한 윈도우 핸들과의 연결을 필수적으로 요구하기 때문입니다.</p>
<h2 id="3-slateeditabletextlayoutcpp의-수정">3. SlateEditableTextLayout.cpp의 수정</h2>
<h3 id="31-포커스-손실-시-조합-취소-처리">3.1 포커스 손실 시 조합 취소 처리</h3>
<p><strong>수정된 코드:</strong></p>
<pre><code class="language-cpp">bool FSlateEditableTextLayout::HandleFocusLost(const FFocusEvent&amp; InFocusEvent)
{
    // ... 기존 코드 ...

    ITextInputMethodSystem* const TextInputMethodSystem = FSlateApplication::Get().GetTextInputMethodSystem();
    if (TextInputMethodSystem &amp;&amp; bHasRegisteredTextInputMethodContext)
    {
        if (TextInputMethodContext-&gt;IsComposing() &amp;&amp; TextInputMethodChangeNotifier.IsValid())
        {
            TextInputMethodChangeNotifier-&gt;CancelComposition();
        }
        TextInputMethodSystem-&gt;DeactivateContext(TextInputMethodContext.ToSharedRef());
    }
}</code></pre>
<p>에디터블 텍스트 위젯이 포커스를 잃을 때, 진행 중인 조합(Composition)이 있다면 명시적으로 취소합니다. 이는 다른 위젯으로 포커스가 이동할 때 IME 상태가 올바르게 정리되도록 보장합니다.</p>
<h3 id="32-커서-하이라이트-로직-수정">3.2 커서 하이라이트 로직 수정</h3>
<p><strong>기존 코드:</strong></p>
<pre><code class="language-cpp">if (/* 조합 중 */) {
    // 조합 중인 텍스트 하이라이트
}
else if (bHasSelection) {
    // 선택 영역 하이라이트
}</code></pre>
<p><strong>수정된 코드:</strong></p>
<pre><code class="language-cpp">if (/* 조합 중 */) {
    // 조합 중인 텍스트 하이라이트
}

if (bHasSelection) {
    // 선택 영역 하이라이트
}</code></pre>
<p><code>else if</code>를 <code>if</code>로 변경하여, 조합 중인 텍스트와 선택 영역이 동시에 표시될 수 있도록 수정했습니다. 이는 일부 IME가 조합 중에도 텍스트 선택을 유지하는 경우를 올바르게 처리합니다.</p>
<h3 id="33-텍스트-변경-통보-추가">3.3 텍스트 변경 통보 추가</h3>
<p><strong>수정된 코드:</strong></p>
<pre><code class="language-cpp">void FSlateEditableTextLayout::EndEditTransaction()
{
    // ... 기존 코드 ...

    SaveText(EditedText);

    if (TextInputMethodChangeNotifier.IsValid() &amp;&amp; TextInputMethodContext.IsValid())
    {
        uint32 OldLen = StateBeforeChangingText.GetValue().Text.ToString().Len();
        uint32 NewLen = EditedText.ToString().Len();
        TextInputMethodChangeNotifier-&gt;NotifyTextChanged(0, OldLen, NewLen);
    }

    PushUndoState(StateBeforeChangingText.GetValue());
}</code></pre>
<p>텍스트 편집이 완료될 때마다 IME에게 변경 사항을 명시적으로 통보합니다. 이는 IME가 내부 상태를 올바르게 동기화할 수 있도록 하며, 특히 스페이스바 입력 후 다음 문자를 입력할 때 발생하던 문제를 해결하는 핵심 수정입니다.</p>
<h3 id="34-텍스트-바운딩-박스-계산-개선">3.4 텍스트 바운딩 박스 계산 개선</h3>
<p><strong>기존 코드:</strong></p>
<pre><code class="language-cpp">const FVector2D BeginPosition = OwnerLayout-&gt;TextLayout-&gt;GetLocationAt(BeginLocation, false);
const FVector2D EndPosition = OwnerLayout-&gt;TextLayout-&gt;GetLocationAt(EndLocation, false);

if (BeginPosition.Y == EndPosition.Y)
{
    Position = BeginPosition;
    Size = EndPosition - BeginPosition;
}
else
{
    Position = FVector2D(0.0f, BeginPosition.Y);
    Size = FVector2D(OwnerLayout-&gt;TextLayout-&gt;GetDrawSize().X, EndPosition.Y - BeginPosition.Y);
}</code></pre>
<p><strong>수정된 코드:</strong></p>
<pre><code class="language-cpp">const FVector2D BeginPosition = OwnerLayout-&gt;TextLayout-&gt;GetLocationAt(BeginLocation, false);
const FVector2D EndPosition = OwnerLayout-&gt;TextLayout-&gt;GetLocationAt(EndLocation, false);

const TArray&lt; FTextLayout::FLineView &gt;&amp; LineViews = OwnerLayout-&gt;TextLayout-&gt;GetLineViews();
int32 LineViewIndex = OwnerLayout-&gt;TextLayout-&gt;GetLineViewIndexForTextLocation(LineViews, BeginLocation, false);
double BeginLineHeight{};
if (LineViewIndex != INDEX_NONE)
{
    BeginLineHeight = LineViews[LineViewIndex].Size.Y;
}

if (BeginPosition.Y == EndPosition.Y)
{
    Position = FVector2D(BeginPosition.X, BeginPosition.Y - BeginLineHeight);
    Size = EndPosition - Position;
}
else
{
    Position = FVector2D(0.0f, BeginPosition.Y - BeginLineHeight);
    Size = FVector2D(OwnerLayout-&gt;TextLayout-&gt;GetDrawSize().X, EndPosition.Y - Position.Y);
}</code></pre>
<p>주요 개선 사항:</p>
<ul>
<li>해당 라인의 높이(<code>BeginLineHeight</code>)를 계산에 포함</li>
<li>Y 좌표를 라인 상단부터 시작하도록 조정 (<code>BeginPosition.Y - BeginLineHeight</code>)</li>
<li>이를 통해 IME 조합 윈도우가 텍스트의 정확한 위치에 표시됨</li>
</ul>
<p>Windows 11 IME는 조합 윈도우의 위치를 결정하기 위해 매우 정확한 바운딩 박스 정보를 필요로 합니다. 기존 구현은 베이스라인 위치만 제공했지만, 수정된 코드는 텍스트의 전체 높이를 포함한 정확한 영역을 제공합니다.</p>
<h1 id="결론">결론</h1>
<p>본 수정 사항들은 언리얼 엔진 5.0이 Windows 11의 새로운 IME 아키텍처와 완전히 호환되도록 만듭니다. 정리 해 보면 다음과 같습니다:</p>
<ol>
<li><strong>TSF 프로토콜의 완전한 준수</strong>: <code>ITextStoreACP</code> 인터페이스를 Windows 11 IME의 요구사항에 맞게 올바르게 구현</li>
<li><strong>안정성 향상</strong>: 초기화 순서 개선으로 IME 시스템의 안정성 확보</li>
<li><strong>사용자 경험 개선</strong>: 한글, 중국어 등 복잡한 입력 시스템에서도 자연스러운 입력 경험 제공</li>
<li><strong>성능 최적화</strong>: 불필요한 검증 제거 및 효율적인 버퍼 관리</li>
</ol>
<p>이러한 수정을 통해 사용자들은 더 이상 &quot;이전 버전의 IME 사용&quot; 옵션에 의존할 필요 없이, Windows 11의 최신 입력 기능을 완전히 활용할 수 있게 되었습니다.</p>
<p>게임 엔진과 운영체제의 입력 시스템 통합은 사용자 경험의 근간을 이루는 중요한 요소입니다. 본 사례는 플랫폼 업데이트에 따른 호환성 이슈를 체계적으로 분석하고 해결하는 과정을 보여주며, 향후 유사한 문제에 대한 참고 자료로 활용될 수 있을 것입니다.</p>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[Unreal Engine의 숨겨진 최적화: Roughness 1.0이 만드는 마법]]></title>
            <link>https://velog.io/@mazeline_1973/Unreal-Engine%EC%9D%98-%EC%88%A8%EA%B2%A8%EC%A7%84-%EC%B5%9C%EC%A0%81%ED%99%94-Roughness-1.0%EC%9D%B4-%EB%A7%8C%EB%93%9C%EB%8A%94-%EB%A7%88%EB%B2%95</link>
            <guid>https://velog.io/@mazeline_1973/Unreal-Engine%EC%9D%98-%EC%88%A8%EA%B2%A8%EC%A7%84-%EC%B5%9C%EC%A0%81%ED%99%94-Roughness-1.0%EC%9D%B4-%EB%A7%8C%EB%93%9C%EB%8A%94-%EB%A7%88%EB%B2%95</guid>
            <pubDate>Mon, 27 Oct 2025 16:01:37 GMT</pubDate>
            <description><![CDATA[<h2 id="바퀴를-다시-만들-뻔한-이야기">바퀴를 다시 만들 뻔한 이야기</h2>
<p>오늘은 제가 언리얼 엔진 소스를 파헤치다가 발견한 흥미로운 최적화 기능과, 그것을 몰라서 같은 기능을 직접 구현하려 했던 뻘짓(?)에 대한 이야기를 들려드리려 합니다.</p>
<p>사실 며칠 전까지만 해도 저는 모바일 플랫폼에서 TwoSided Foliage 셰이더의 성능을 개선하기 위해 고민하고 있었습니다. &quot;모바일에서는 어차피 반사가 별로 안 보이니까, 강제로 Fully Rough 처리를 하면 어떨까?&quot;라는 생각으로 직접 코드를 수정하려고 MaterialHLSLEmitter.cpp를 열었죠.</p>
<p>제가 원했던 것은 머티리얼 인스펙터의 Fully Rough 토글을 사용하지 않고, PC와 모바일 플랫폼 간의 자동 처리를 구현하는 것이었습니다.</p>
<p>그런데...</p>
<pre><code class="language-cpp">bool bIsFullyRough,  // 317번 줄에서 만난 운명적인 파라미터</code></pre>
<p>이 한 줄이 제 눈에 들어왔습니다. &quot;어? 이게 뭐지?&quot;
사실은 동료 TA 인 이성학 사원이 예전에 말 했던 기억이 얼핏 있었습니다. 뭐 망각의 동물 아니겠습니까? ㅎㅎ</p>
<h2 id="발견의-순간">발견의 순간</h2>
<p>호기심에 코드를 더 파보니, 언리얼 엔진이 이미 제가 하려던 일을 자동으로 처리하고 있었습니다:</p>
<pre><code class="language-cpp">// MaterialHLSLEmitter.cpp (915-931번 줄)
bIsFullyRough = bIsFullyRough || EmitContext.Material-&gt;IsFullyRough();
if (!bIsFullyRough)
{
    // Roughness 필드를 찾아서...
    const FStructField* RoughnessField = 
        CachedTree-&gt;GetMaterialAttributesType()-&gt;FindFieldByName(
            *FMaterialAttributeDefinitionMap::GetAttributeName(MP_Roughness));

    // 상수값인지 확인하고...
    if (Evaluation == EExpressionEvaluation::Constant)
    {
        // 값이 1.0이면 Fully Rough로 처리!
        bIsFullyRough = ConstantValue.Component[RoughnessField-&gt;ComponentIndex].Float == 1.f;
    }
}</code></pre>
<p>네, 맞습니다. <strong>머티리얼 에디터에서 Roughness 포트에 상수 1.0을 연결하면, 엔진이 자동으로 이를 감지해서 최적화를 수행</strong>하고 있었던 것입니다!</p>
<h2 id="roughness-10이-뭐가-특별한가요">Roughness 1.0이 뭐가 특별한가요?</h2>
<p>PBR(Physically Based Rendering)에서 Roughness는 표면의 거칠기를 나타냅니다:</p>
<ul>
<li><strong>0.0</strong>: 완벽한 거울 (perfect mirror)</li>
<li><strong>1.0</strong>: 완전히 거친 표면 (fully rough)</li>
</ul>
<p>Roughness가 1.0이라는 것은 표면이 너무 거칠어서 빛이 모든 방향으로 균등하게 산란된다는 의미입니다. 즉, <strong>반사 계산이 필요 없다</strong>는 뜻이죠!</p>
<h2 id="엔진이-해주는-최적화">엔진이 해주는 최적화</h2>
<p>이 최적화가 활성화되면:</p>
<h3 id="1-셰이더-컴파일-시점에-매크로-정의">1. 셰이더 컴파일 시점에 매크로 정의</h3>
<pre><code class="language-cpp">// MaterialHLSLEmitter.cpp (486번 줄)
OutEnvironment.SetDefine(TEXT(&quot;MATERIAL_FULLY_ROUGH&quot;), bIsFullyRough);</code></pre>
<h3 id="2-픽셀-셰이더에서-불필요한-계산-스킵">2. 픽셀 셰이더에서 불필요한 계산 스킵</h3>
<pre><code>// BasePassPixelShader.usf
#define FORCE_FULLY_ROUGH (MATERIAL_FULLY_ROUGH)

#if !FORCE_FULLY_ROUGH
    // 반사 캡처 관련 복잡한 계산들...
    // 이 부분이 통째로 컴파일에서 제외됩니다!
#endif</code></pre><h3 id="3-모바일에서는-더-많은-최적화">3. 모바일에서는 더 많은 최적화</h3>
<pre><code>// MobileBasePassPixelShader.usf
#define FULLY_ROUGH (MATERIAL_FULLY_ROUGH || MOBILE_QL_FORCE_FULLY_ROUGH)

// SSR도 자동으로 비활성화
#if MOBILE_SSR_ENABLED &amp;&amp; !(MOBILE_QL_FORCE_FULLY_ROUGH || MATERIAL_FULLY_ROUGH)
    // Screen Space Reflection 계산
#endif</code></pre><h2 id="잠깐-specular-0과-roughness-10은-뭐가-다른가요">잠깐, Specular 0과 Roughness 1.0은 뭐가 다른가요?</h2>
<p>테스트하면서 흥미로운 의문이 생겼습니다. &quot;Specular 포트에 0을 넣어도 반사가 없어서 Fully Rough처럼 보이는데, 똑같은 최적화가 되는 걸까?&quot;</p>
<p>답은 <strong>&quot;아니오&quot;</strong>입니다!</p>
<h3 id="specular-0의-진실">Specular 0의 진실</h3>
<p>코드를 분석해보니 흥미로운 사실을 발견했습니다:</p>
<pre><code class="language-cpp">// MaterialHLSLEmitter.cpp에서는 오직 Roughness만 체크합니다
if (Evaluation == EExpressionEvaluation::Constant)
{
    bIsFullyRough = ConstantValue.Float == 1.f;  // Roughness가 1.0인지만 확인
}

// Specular에 대한 유사한 최적화는 없습니다!</code></pre>
<h3 id="렌더링-시점의-차이">렌더링 시점의 차이</h3>
<p>더 중요한 차이는 셰이더에서 나타납니다:</p>
<pre><code>// FORCE_FULLY_ROUGH가 활성화되면 (Roughness = 1.0)
#if !FORCE_FULLY_ROUGH
    // 이 전체 블록이 컴파일에서 제외됩니다!
    // 반사 관련 복잡한 계산들...
#endif

// 하지만 Specular = 0일 때는
float3 SpecularColor = lerp(0.08f * [Specular.xxx](http://Specular.xxx), BaseColor, Metallic);
// SpecularColor가 0이 되지만, 여전히 모든 BRDF 계산을 수행합니다!</code></pre><h3 id="envbrdfapproxfullyrough의-비밀">EnvBRDFApproxFullyRough의 비밀</h3>
<p>Roughness가 1.0일 때만 특별한 함수가 호출됩니다:</p>
<pre><code>// BRDF.ush
void EnvBRDFApproxFullyRough(inout half3 DiffuseColor, inout half3 SpecularColor)
{
    // Factors derived from EnvBRDFApprox( SpecularColor, 1, 1 ) == SpecularColor * 0.4524 - 0.0024
    DiffuseColor += SpecularColor * 0.45;  // 에너지 보존을 위해 Diffuse에 추가
    SpecularColor = 0;                      // Specular 제거
}</code></pre><p>이 함수는:</p>
<ol>
<li><strong>에너지 보존</strong>: 반사되지 않은 빛을 Diffuse로 전환</li>
<li><strong>물리적 정확성</strong>: Roughness 1.0에서의 실제 BRDF 근사값 사용</li>
</ol>
<h3 id="성능-차이-요약">성능 차이 요약</h3>
<table>
<thead>
<tr>
<th>설정</th>
<th>컴파일 타임 최적화</th>
<th>런타임 계산</th>
<th>에너지 보존</th>
</tr>
</thead>
<tbody><tr>
<td>Roughness = 1.0</td>
<td>BRDF 코드 제거</td>
<td>계산 스킵</td>
<td>자동 처리</td>
</tr>
<tr>
<td>Specular = 0</td>
<td>없음</td>
<td>모든 계산 수행</td>
<td>수동 필요</td>
</tr>
</tbody></table>
<h2 id="결론-platform-switch가-답이었다">결론: Platform Switch가 답이었다!</h2>
<p>그래서 제가 결국 깨달은 것은, <strong>코드를 수정할 필요가 전혀 없었다</strong>는 것입니다!</p>
<h3 id="머티리얼-에디터에서-platform-switch-활용하기">머티리얼 에디터에서 Platform Switch 활용하기</h3>
<p>![Platform Switch 노드 예시]</p>
<pre><code>[Platform Switch]
├─ Default → Texture Sample (기존 Roughness 텍스처)
├─ Mobile → Constant 1.0
└─ Output → Roughness</code></pre><p>이렇게 간단하게 설정하면:</p>
<ul>
<li><strong>PC</strong>: 원래 의도한 Roughness 텍스처 사용</li>
<li><strong>모바일</strong>: 자동으로 1.0 상수값 → Fully Rough 최적화 발동!</li>
</ul>
<p>특정 셰이더(예: TwoSided Foliage)에만 이런 설정을 명시적으로 해주면, 플랫폼별로 자동으로 최적화가 적용됩니다.</p>
<h2 id="실제로-얼마나-효과가-있나요">실제로 얼마나 효과가 있나요?</h2>
<p>제가 테스트해본 결과:</p>
<ul>
<li><strong>PC</strong>: 원본 퀄리티 유지</li>
<li><strong>모바일</strong>:<ul>
<li>픽셀당 약 10-15개의 ALU 명령어 감소</li>
<li>SSR 비활성화로 추가 성능 향상</li>
<li>특히 foliage처럼 오버드로우가 심한 경우 눈에 띄는 개선</li>
</ul>
</li>
</ul>
<h2 id="실무-활용-팁">실무 활용 팁</h2>
<h3 id="1-platform-switch-활용-예시">1. Platform Switch 활용 예시</h3>
<pre><code>// Foliage 머티리얼
[Platform Switch: Roughness]
├─ Default → RoughnessTexture.Sample
├─ Mobile → 1.0
└─ Output → Roughness

// 지면 머티리얼
[Platform Switch: Roughness]  
├─ Default → Lerp(0.3, 0.8, DirtMask)
├─ Mobile → 0.95  // 거의 Rough하지만 완전히는 아닌
└─ Output → Roughness</code></pre><h3 id="2-다른-최적화-조합">2. 다른 최적화 조합</h3>
<pre><code>// 모바일 최적화 콤보
[Platform Switch: Metallic] → Mobile: 0.0
[Platform Switch: Specular] → Mobile: 0.0  
[Platform Switch: Roughness] → Mobile: 1.0</code></pre><h3 id="3-quality-switch와의-조합">3. Quality Switch와의 조합</h3>
<pre><code>[Quality Switch]
├─ Low → 1.0 (모든 플랫폼에서 최적화)
├─ Medium → Platform Switch (플랫폼별 분기)
├─ High → Original Texture
└─ Output → Roughness</code></pre><h3 id="4-실무-팁">4. 실무 팁</h3>
<pre><code>// 최적화를 원한다면
[Platform Switch: Roughness] → Mobile: 1.0  (추천)

// 이것만으로는 부족합니다
[Platform Switch: Specular] → Mobile: 0.0   (비추천)

// 최상의 조합 (필요시)
[Platform Switch: Roughness] → Mobile: 1.0
[Platform Switch: Metallic] → Mobile: 0.0
// Specular는 건드리지 마세요!</code></pre><h2 id="엔진을-믿고-엔진을-알자">엔진을 믿고, 엔진을 알자</h2>
<p>이번 경험을 통해 얻은 교훈:</p>
<ol>
<li><strong>엔진이 제공하는 도구를 먼저 찾아보자</strong>: Platform Switch 같은 기본 도구로도 충분했습니다</li>
<li><strong>소스 코드를 읽으면 원리를 이해할 수 있다</strong>: 왜 1.0이 특별한지, 왜 Specular 0이 다른지 알게 되었습니다</li>
<li><strong>복잡한 해결책보다 단순한 해결책을</strong>: 코드 수정보다 노드 하나가 더 우아했습니다</li>
<li><strong>비슷해 보여도 다를 수 있다</strong>: Specular 0과 Roughness 1.0의 차이처럼</li>
</ol>
<p>저처럼 &quot;모바일에서 TwoSided Foliage를 강제로 Fully Rough 처리하는&quot; 코드를 짜려다가, Platform Switch 노드 하나로 해결할 수 있다는 걸 뒤늦게 깨달은 분들이 있으실 겁니다.</p>
<p>언리얼 엔진은 우리가 생각하는 것보다 훨씬 똑똑하고, 이미 우리가 필요한 도구들을 제공하고 있습니다. 때로는 코드를 수정하기 전에, 에디터에서 제공하는 기능들을 다시 한 번 살펴보는 것도 좋은 것 같습니다.</p>
<p>다음에는 또 어떤 숨겨진 최적화를 발견하게 될까요?</p>
<hr>
<p><em>P.S. 혹시 Platform Switch로 해결할 수 있는 걸 코드로 해결하려 했던 경험이 있으신 분들은 댓글로 공유해주세요! 함께 바퀴를 재발명하지 않는 개발자가 됩시다.</em></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[SSS URP RenderGraph 전환 연구(planned)]]></title>
            <link>https://velog.io/@mazeline_1973/SSS-URP-RenderGraph-%EC%A0%84%ED%99%98-%EC%97%B0%EA%B5%AC</link>
            <guid>https://velog.io/@mazeline_1973/SSS-URP-RenderGraph-%EC%A0%84%ED%99%98-%EC%97%B0%EA%B5%AC</guid>
            <pubDate>Mon, 27 Oct 2025 14:40:33 GMT</pubDate>
            <description><![CDATA[<h2 id="프로젝트-배경">프로젝트 배경</h2>
<p>본 연구는 고객사 NTRANCE로부터 의뢰받은 SSS URP(Subsurface Scattering for Universal Render Pipeline) 시스템의 기술적 현대화 작업으로, 현재 레거시 ScriptableRenderPass API 기반으로 구현된 스킨 렌더링 파이프라인을 Unity 6.2+의 RenderGraph 아키텍처로 마이그레이션하는 것을 목적으로 합니다.</p>
<p>Unity Technologies는 Unity 6.0부터 차세대 렌더링 아키텍처인 RenderGraph API를 공식 도입하였으며, 이는 기존 CommandBuffer 기반의 즉시 실행(immediate-mode) 방식에서 선언적(declarative) 렌더링 모델로의 패러다임 전환을 의미합니다. 이러한 변화에 따라 레거시 커스텀 렌더 패스들은 향후 deprecation 대상으로 분류되었으며, 특히 수동 리소스 관리 방식(<code>GetTemporaryRT</code>/<code>ReleaseTemporaryRT</code>)은 다음과 같은 기술적 한계를 내포하고 있습니다.</p>
<p>첫째, 개발자가 텍스처 라이프타임을 명시적으로 관리해야 하므로 메모리 누수(memory leak)의 위험이 상존하며, 둘째, 렌더 패스 간 의존성을 수동으로 추적해야 하는 구조적 복잡도가 존재합니다. 셋째, 런타임에 동적으로 메모리를 할당/해제하는 방식으로 인해 메모리 단편화(fragmentation) 및 할당 오버헤드가 발생할 수 있습니다.</p>
<p>반면 RenderGraph는 컴파일 타임에 전체 렌더링 플로우를 분석하여 자동으로 리소스 aliasing, 의존성 해결, 불필요한 패스 제거 등의 최적화를 수행하는 그래프 기반 스케줄링 시스템입니다. 따라서 본 프로젝트를 통해 SSS URP를 RenderGraph로 전환함으로써, 최신 렌더링 파이프라인과의 기술적 호환성을 확보하고 시스템 전반의 효율성을 향상시키고자 합니다.</p>
<h2 id="프로젝트-개요">프로젝트 개요</h2>
<p>본 프로젝트는 실시간 스킨 렌더링을 위한 Subsurface Scattering 시스템을 Unity 6.2 이상의 RenderGraph API 체계로 완전히 재설계하는 것을 핵심 목표로 합니다. 현재 시스템은 RenderGraph를 지원하지 않는 레거시 CommandBuffer 및 Blit 기반 구현으로 되어 있어, 리소스의 명시적 관리가 필수적이며 RenderGraph가 제공하는 자동화된 최적화 메커니즘을 활용할 수 없는 구조적 제약이 존재합니다.</p>
<p>본 전환 작업을 통해 기대할 수 있는 기술적 이점은 다음과 같이 정리됩니다.</p>
<p>첫째, <strong>메모리 효율성 개선</strong>입니다. RenderGraph의 자동 텍스처 aliasing 메커니즘을 통해 동일한 해상도와 포맷을 가진 임시 텍스처들이 메모리 공간을 공유할 수 있게 되어, GPU 메모리 사용량을 유의미하게 절감할 수 있습니다.</p>
<p>둘째, <strong>의존성 관리 자동화</strong>입니다. 렌더 패스 간의 리소스 의존성을 RenderGraph가 자동으로 분석 및 관리하게 되어, 개발자가 수동으로 실행 순서를 제어하던 복잡도가 제거되며 유지보수성이 향상됩니다.</p>
<p>셋째, <strong>자동 패스 컬링</strong>입니다. RenderGraph는 정적 분석을 통해 최종 출력에 기여하지 않는 렌더 패스를 런타임에 자동으로 제거하여, 불필요한 연산 오버헤드를 감소시킵니다.</p>
<p>넷째, <strong>플랫폼 호환성 확보</strong>입니다. Unity 6.2 이상의 최신 렌더링 파이프라인과 완벽하게 호환되어 향후 엔진 업데이트에 따른 기술적 부채(technical debt)를 사전에 해소할 수 있습니다.</p>
<hr>
<h2 id="현재-시스템-분석">현재 시스템 분석</h2>
<h3 id="렌더링-파이프라인-구조">렌더링 파이프라인 구조</h3>
<p><img src="https://velog.velcdn.com/images/mazeline_1973/post/b1646bc3-d8b5-487c-a149-54cd954105de/image.png" alt=""></p>
<h3 id="pass-1-light-pass">Pass 1: Light Pass</h3>
<p><strong>역할:</strong> SSS 오브젝트의 디퓨즈 라이팅만 별도로 렌더링</p>
<p><strong>구현:</strong></p>
<ul>
<li><code>RenderWithShader.cs</code> 사용</li>
<li><code>LightPass.shader</code>로 씬 재렌더링</li>
<li>Layer Mask 필터링 (예: Skin 레이어)</li>
</ul>
<p><strong>출력:</strong> <code>_SSS_LightPass</code> 텍스처</p>
<p><strong>레거시 코드:</strong></p>
<pre><code>// Configure()
cmd.GetTemporaryRT(targetId, width, height, 24, FilterMode.Bilinear, RT_Format);
ConfigureTarget(RT);

// Execute()
drawingSettings.overrideShader = PassShader;
context.DrawRenderers(cullResults, ref drawingSettings, ref filterSettings);
cmd.SetGlobalTexture(targetName, RT);

// FrameCleanup()
cmd.ReleaseTemporaryRT(targetId);</code></pre><h3 id="pass-2-profile-pass">Pass 2: Profile Pass</h3>
<p><strong>역할:</strong> SSS 프로파일 정보 렌더링</p>
<p><strong>출력 데이터:</strong></p>
<ul>
<li><strong>RGB:</strong> Profile Color (서브서피스 스캐터링 색상)</li>
<li><strong>Alpha:</strong> Blur Radius (픽셀별 가변 블러 반경)</li>
</ul>
<p><strong>ProfilePass.shader 핵심:</strong></p>
<pre><code class="language-jsx">// Fragment Output
Color = (profileTexture * ProfileColor).rgb;
Alpha = (profileTexture.a * _Blur * ProfileColor.a);</code></pre>
<p><strong>출력:</strong> <code>_SSS_ProfilePass</code> 텍스처</p>
<h3 id="pass-3-sss-blur-pass">Pass 3: SSS Blur Pass</h3>
<p><strong>역할:</strong> Edge-aware 스크린 스페이스 블러</p>
<p><strong>핵심 기능:</strong></p>
<h3 id="1-ping-pong-블러">1. Ping-Pong 블러</h3>
<pre><code>// 2개의 임시 텍스처 생성
cmd.GetTemporaryRT(tmpId1, width/downsample, height/downsample, 0, ...);
cmd.GetTemporaryRT(tmpId2, width/downsample, height/downsample, 0, ...);

// CopyLight: _SSS_LightPass → tmpRT1
cmd.Blit(null, tmpRT1, CopyLight);

// Ping-pong iteration
for (int i = 0; i &lt; blurPasses; i++)
{
    cmd.Blit(tmpRT1, tmpRT2, blurMaterial);
    // swap
    var tmp = tmpRT1;
    tmpRT1 = tmpRT2;
    tmpRT2 = tmp;
}

// 최종 결과
cmd.SetGlobalTexture(&quot;_SSS_Blur&quot;, tmpRT2);</code></pre><h3 id="2-depth-aware-blur">2. Depth-Aware Blur</h3>
<pre><code class="language-jsx">float DepthTest(float d0, float d1)
{
    float diff = abs(d0 - d1);
    return diff &lt; _DepthTest ? 1.0 : 0.0;
}</code></pre>
<h3 id="3-profile-aware-blur-조건부">3. Profile-Aware Blur (조건부)</h3>
<pre><code class="language-jsx">#ifdef PROFILE_TEST
float ProfileTest(float4 center, float4 sample)
{
    float colorDiff = distance(center.rgb, sample.rgb);
    float radiusDiff = abs(center.a - sample.a);
    return (colorDiff &lt; ProfileColorTest &amp;&amp; radiusDiff &lt; ProfileRadiusTest);
}
#endif</code></pre>
<h3 id="4-normal-aware-blur-조건부">4. Normal-Aware Blur (조건부)</h3>
<pre><code class="language-jsx">#ifdef NORMAL_TEST
float NormalTest(float4 n0, float4 n1)
{
    float dotResult = dot(</code></pre>
<h3 id="5-dithered-sampling">5. Dithered Sampling</h3>
<pre><code class="language-jsx">// 블루 노이즈 기반 샘플링 랜덤화 (밴딩 감소)
float2 random = tex2D(_NoiseTexture, uv * DitherScale + _Time.xx * 200).xy;
float2x2 RotationMatrix = float2x2(random.x, random.y, -random.y, random.x);
offset = mul(offset, RotationMatrix);</code></pre>
<h3 id="6-pixel-leak-fix">6. Pixel Leak Fix</h3>
<pre><code class="language-jsx">// Edge에서 오프셋 감소
float2 Offset1 = offset * EdgeTest1 * _FixPixelLeak;</code></pre>
<h3 id="7-가우시안-가중치">7. 가우시안 가중치</h3>
<pre><code class="language-jsx">// 프로파일 기반 가중치
float3 weight = exp(-Pow2(step / profile.rgb));
weightSum += weight;
col.rgb += sample.rgb * weight;

// 정규화
col.rgb = col.rgb / weightSum;</code></pre>
<hr>
<h2 id="레거시-vs-rendergraph-비교">레거시 vs RenderGraph 비교</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>레거시 방식</th>
<th>RenderGraph 방식</th>
</tr>
</thead>
<tbody><tr>
<td><strong>텍스처 생성</strong></td>
<td><code>cmd.GetTemporaryRT()</code></td>
<td><code>builder.CreateTransientTexture()</code></td>
</tr>
<tr>
<td><strong>텍스처 해제</strong></td>
<td><code>cmd.ReleaseTemporaryRT()</code> (수동)</td>
<td>자동 관리</td>
</tr>
<tr>
<td><strong>의존성 관리</strong></td>
<td>수동 (패스 순서)</td>
<td><code>builder.UseTexture()</code> 자동</td>
</tr>
<tr>
<td><strong>메모리 최적화</strong></td>
<td>없음</td>
<td>자동 Aliasing</td>
</tr>
<tr>
<td><strong>Culling</strong></td>
<td>없음</td>
<td>불필요한 패스 자동 스킵</td>
</tr>
<tr>
<td><strong>리소스 라이프타임</strong></td>
<td>수동 추적</td>
<td>RenderGraph 자동 관리</td>
</tr>
<tr>
<td><strong>디버깅</strong></td>
<td>CommandBuffer 추적</td>
<td>RenderGraph Viewer 활용</td>
</tr>
</tbody></table>
<hr>
<h2 id="rendergraph-전환-설계">RenderGraph 전환 설계</h2>
<h3 id="프로토타입-디렉토리-구조">프로토타입 디렉토리 구조</h3>
<pre><code>Assets/SSS_URP_RenderGraph/
│
├── Core/
│   ├── SSSRenderGraphFeature.cs          // 통합 Renderer Feature
│   ├── SSSLightPassRG.cs                  // Light Pass (RenderGraph)
│   ├── SSSProfilePassRG.cs                // Profile Pass (RenderGraph)
│   ├── SSSBlurPassRG.cs                   // Blur Pass (RenderGraph)
│   └── SSSPassData.cs                     // 공통 PassData 구조체
│
├── Shaders/
│   ├── LightPass.shader                   // 기존 셰이더 재사용
│   ├── ProfilePass.shader                 // 기존 셰이더 재사용
│   ├── SSS_Blur.shader                    // 기존 셰이더 재사용
│   └── SSS_Common.hlsl                    // 공통 함수
│
├── Volume/
│   └── SSSVolumeComponent.cs              // Volume override
│
├── Utilities/
│   ├── RenderGraphTexturePool.cs          // TextureHandle 헬퍼
│   └── SSSDebugger.cs                     // 디버깅 도구
│
└── Documentation/
    ├── MIGRATION_[GUIDE.md](http://GUIDE.md)</code></pre><h3 id="핵심-클래스-설계-의사-코드">핵심 클래스 설계 (의사 코드)</h3>
<h3 id="sssrendergraphfeaturecs">SSSRenderGraphFeature.cs</h3>
<pre><code>public class SSSRenderGraphFeature : ScriptableRendererFeature
{
    public SSSSettings settings;

    public override void RecordRenderGraph(RenderGraph renderGraph, ...)
    {
        // Pass 1: Light Pass
        TextureHandle lightPassOutput = 
            SSSLightPassRG.Record(renderGraph, settings);

        // Pass 2: Profile Pass
        TextureHandle profilePassOutput = 
            SSSProfilePassRG.Record(renderGraph, settings);

        // Pass 3: Blur Pass (여러 iteration)
        TextureHandle blurredOutput = 
            SSSBlurPassRG.Record(renderGraph, lightPassOutput, 
                                  profilePassOutput, settings);

        // 최종 결과
        renderGraph.SetGlobalTexture(&quot;_SSS_Blur&quot;, blurredOutput);
    }
}</code></pre><h3 id="ssspassdatacs">SSSPassData.cs</h3>
<pre><code>class LightPassData
{
    public RendererListHandle rendererList;
    public TextureHandle outputTexture;
    public Material overrideShader;
    public FilteringSettings filterSettings;
}

class BlurPassData
{
    public TextureHandle inputTexture;
    public TextureHandle outputTexture;
    public TextureHandle depthTexture;
    public TextureHandle normalTexture;
    public TextureHandle profileTexture;
    public Material blurMaterial;
    public Vector4 blurParams;
}</code></pre><h3 id="sssblurpassrgcs---ping-pong-블러">SSSBlurPassRG.cs - Ping-pong 블러</h3>
<pre><code>public static TextureHandle Record(
    RenderGraph renderGraph,
    TextureHandle input,
    TextureHandle profile,
    SSSSettings settings)
{
    TextureHandle current = input;
    TextureHandle temp;

    // Ping-pong iteration
    for (int i = 0; i &lt; settings.blurPasses; i++)
    {
        // Transient texture 생성 (자동 메모리 aliasing)
        temp = renderGraph.CreateTransientTexture(desc);

        using (var builder = renderGraph.AddRasterRenderPass&lt;BlurPassData&gt;(...))
        {
            builder.UseTexture(current);
            builder.UseTexture(profile);
            builder.UseTexture(depthTexture);
            builder.SetRenderAttachment(temp, 0);

            builder.SetRenderFunc((BlurPassData data, RasterGraphContext ctx) =&gt;
            {
                ctx.cmd.Blit(data.inputTexture, data.outputTexture, data.blurMaterial);
            });
        }

        current = temp; // Swap
    }

    return current;
}</code></pre><hr>
<h2 id="마일스톤-로드맵">마일스톤 로드맵</h2>
<h3 id="phase-1-기반-연구-및-분석-1-2주---완료">Phase 1: 기반 연구 및 분석 (1-2주) - 완료</h3>
<p><strong>연구 과제:</strong></p>
<ul>
<li>Unity 6.2 RenderGraph API 학습</li>
<li>현재 시스템 심층 분석</li>
<li>렌더링 플로우 매핑</li>
<li>리소스 의존성 분석</li>
</ul>
<p><strong>산출물:</strong></p>
<ul>
<li>RenderGraph API 핵심 개념 정리</li>
<li>현재 렌더링 플로우 다이어그램</li>
<li>리소스 의존성 맵</li>
<li>프로토타입 디렉토리 구조</li>
</ul>
<hr>
<h3 id="phase-2-프로토타입-구현-2-3주">Phase 2: 프로토타입 구현 (2-3주)</h3>
<p><strong>목표:</strong> 핵심 렌더 패스의 RenderGraph 버전 구현</p>
<p><strong>구현 과제:</strong></p>
<ol>
<li><strong>RenderWithShader RenderGraph 전환</strong><ul>
<li><code>RenderWithShaderRG.cs</code> 신규 클래스</li>
<li><code>RecordRenderGraph</code> 메서드 구현</li>
<li><code>TextureHandle</code> 리소스 관리</li>
<li><code>RenderGraphBuilder</code> 리소스 선언</li>
</ul>
</li>
<li><strong>SSS_Blur RenderGraph 전환</strong><ul>
<li><code>SSS_BlurRG.cs</code> 신규 클래스</li>
<li>Ping-pong 블러를 RenderGraph 패스 체인으로 재구성</li>
<li>Transient texture 최적화</li>
<li>Volume override 시스템 유지</li>
</ul>
</li>
<li><strong>리소스 디스크립터 시스템</strong><ul>
<li><code>RenderTextureDescriptor</code> → <code>TextureDesc</code> 전환</li>
<li>다운샘플링 로직 재구현</li>
</ul>
</li>
</ol>
<p><strong>산출물:</strong></p>
<ul>
<li><code>RenderWithShaderRG.cs</code></li>
<li><code>SSS_BlurRG.cs</code></li>
<li>기본 동작 프로토타입</li>
</ul>
<hr>
<h3 id="phase-3-통합-및-호환성-2-3주">Phase 3: 통합 및 호환성 (2-3주)</h3>
<p><strong>목표:</strong> 전체 시스템 통합 및 기존 기능 유지</p>
<p><strong>통합 과제:</strong></p>
<ol>
<li><strong>셰이더 호환성 검증</strong><ul>
<li>Global texture 바인딩 확인</li>
<li>Include 파일 검증</li>
</ul>
</li>
<li><strong>Volume 시스템 통합</strong><ul>
<li><code>SSS_Volume.cs</code>와 RenderGraph 연동</li>
<li>런타임 파라미터 오버라이드</li>
</ul>
</li>
<li><strong>레이어 마스킹 및 필터링</strong><ul>
<li><code>FilteringSettings</code> 전환</li>
<li>Rendering Layer Mask 지원</li>
</ul>
</li>
<li><strong>Cascade 가중치 시스템</strong><ul>
<li>Translucency cascade weight 유지</li>
</ul>
</li>
</ol>
<p><strong>산출물:</strong></p>
<ul>
<li>완전 통합된 RenderGraph 버전</li>
<li>레거시 기능 100% 호환성</li>
</ul>
<hr>
<h3 id="phase-4-최적화-및-검증-1-2주">Phase 4: 최적화 및 검증 (1-2주)</h3>
<p><strong>목표:</strong> 성능 최적화 및 품질 검증</p>
<p><strong>최적화 과제:</strong></p>
<ol>
<li><strong>RenderGraph 자동 최적화 활용</strong><ul>
<li>Culling 효과 측정</li>
<li>Memory aliasing 검증</li>
<li>Transient resource 재사용 확인</li>
</ul>
</li>
<li><strong>성능 프로파일링</strong><ul>
<li>Frame Debugger 패스 순서 확인</li>
<li>RenderGraph Viewer 리소스 라이프타임 분석</li>
<li>GPU 메모리 사용량 비교</li>
</ul>
</li>
<li><strong>품질 검증</strong><ul>
<li>모든 데모 씬 테스트</li>
<li>Visual diff 체크</li>
<li>Edge case 테스트</li>
</ul>
</li>
</ol>
<p><strong>산출물:</strong></p>
<ul>
<li>성능 비교 리포트</li>
<li>최적화된 최종 버전</li>
</ul>
<hr>
<h3 id="phase-5-문서화-및-마이그레이션-가이드-1주">Phase 5: 문서화 및 마이그레이션 가이드 (1주)</h3>
<p><strong>목표:</strong> 개발자 문서 작성</p>
<p><strong>문서화 과제:</strong></p>
<ol>
<li><strong>API 변경사항 문서</strong><ul>
<li>레거시 vs RenderGraph API 매핑표</li>
<li>Breaking changes 정리</li>
</ul>
</li>
<li><strong>마이그레이션 가이드</strong><ul>
<li>기존 설정 이전 방법</li>
<li>트러블슈팅 가이드</li>
</ul>
</li>
<li><strong>코드 주석 및 예제</strong></li>
</ol>
<p><strong>산출물:</strong></p>
<ul>
<li><code>MIGRATION_[GUIDE.md](http://GUIDE.md)</code></li>
<li>주석이 잘 달린 최종 코드</li>
</ul>
<hr>
<h2 id="전체-타임라인">전체 타임라인</h2>
<pre><code>Week 1-2  : Phase 1 (연구) - 완료
Week 3-5  : Phase 2 (프로토타입)
Week 6-8  : Phase 3 (통합)
Week 9-10 : Phase 4 (최적화)
Week 11   : Phase 5 (문서화)</code></pre><p><strong>총 소요 기간:</strong> 약 11주 (2.5개월)</p>
<hr>
<h2 id="주요-리스크-및-대응-방안">주요 리스크 및 대응 방안</h2>
<table>
<thead>
<tr>
<th>리스크</th>
<th>영향도</th>
<th>대응 방안</th>
</tr>
</thead>
<tbody><tr>
<td>RenderGraph API 변경</td>
<td>높음</td>
<td>최신 문서 지속 모니터링</td>
</tr>
<tr>
<td>Ping-pong 블러 복잡도</td>
<td>중간</td>
<td>단계별 디버깅, Frame Debugger 활용</td>
</tr>
<tr>
<td>레거시 셰이더 호환성</td>
<td>중간</td>
<td>점진적 통합, A/B 테스트</td>
</tr>
<tr>
<td>성능 회귀</td>
<td>낮음</td>
<td>RenderGraph 자동 최적화로 개선 예상</td>
</tr>
</tbody></table>
<hr>
<h2 id="핵심-인사이트">핵심 인사이트</h2>
<h3 id="1-ping-pong-블러가-가장-복잡한-전환-포인트">1. Ping-pong 블러가 가장 복잡한 전환 포인트</h3>
<ul>
<li>여러 iteration의 TextureHandle 관리 필요</li>
<li>Transient texture 전략 중요</li>
</ul>
<h3 id="2-셰이더는-100-재사용-가능">2. 셰이더는 100% 재사용 가능</h3>
<ul>
<li>RenderGraph는 C# 코드만 변경</li>
<li>기존 HLSL 코드 완전 호환</li>
</ul>
<h3 id="3-volume-시스템은-독립적">3. Volume 시스템은 독립적</h3>
<ul>
<li>RenderGraph와 독립적으로 동작</li>
<li>기존 로직 그대로 유지 가능</li>
</ul>
<h3 id="4-주요-변경사항">4. 주요 변경사항</h3>
<pre><code>// Before (Legacy)
cmd.GetTemporaryRT(targetId, ...);
cmd.Blit(source, target, material);
cmd.ReleaseTemporaryRT(targetId);

// After (RenderGraph)
TextureHandle target = builder.CreateTransientTexture(desc);
builder.UseTexture(sourceTexture);
builder.SetRenderAttachment(target, 0);
// 자동 해제 - 코드 불필요</code></pre><hr>
<h2 id="참고-자료">참고 자료</h2>
<h3 id="rendergraph-api">RenderGraph API</h3>
<ul>
<li><code>RecordRenderGraph()</code>: 렌더 패스 기록</li>
<li><code>AddRasterRenderPass&lt;T&gt;()</code>: 래스터 패스 추가</li>
<li><code>CreateTransientTexture()</code>: 임시 텍스처 생성</li>
<li><code>UseTexture()</code>: 텍스처 리소스 의존성 선언</li>
<li><code>SetRenderAttachment()</code>: 렌더 타겟 설정</li>
<li><code>SetRenderFunc()</code>: 실제 렌더링 함수 정의</li>
</ul>
<h3 id="rendergraph-장점">RenderGraph 장점</h3>
<ul>
<li>패스 간 의존성 자동 해결</li>
<li>메모리 자동 최적화 (aliasing, culling)</li>
<li>불필요한 패스 자동 스킵</li>
<li>리소스 라이프타임 자동 관리</li>
</ul>
<hr>
<h2 id="다음-아이템">다음 아이템</h2>
<h3 id="즉시-시작-가능한-작업">즉시 시작 가능한 작업:</h3>
<ol>
<li>[완료] RenderGraph 레퍼런스 수집 완료</li>
<li>[완료] 현재 렌더링 플로우 분석 완료</li>
<li>[완료] 프로토타입 디렉토리 구조 설계 완료</li>
<li>[대기] <code>SSSRenderGraphFeature.cs</code> 프로토타입 작성 (Phase 2)</li>
<li>[대기] Light Pass RenderGraph 구현 (Phase 2)</li>
</ol>
<hr>
<p><strong>작성일:</strong> 2025-10-27</p>
<p><strong>버전:</strong> 1.0</p>
<p><strong>상태:</strong> Phase 1 완료, Phase 2 대기 중</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[SGE Atlas Builder ver 1.0.1 build5]]></title>
            <link>https://velog.io/@mazeline_1973/SGE-Atlas-Builder-ver-1.0.1-build5</link>
            <guid>https://velog.io/@mazeline_1973/SGE-Atlas-Builder-ver-1.0.1-build5</guid>
            <pubDate>Sat, 25 Oct 2025 15:17:29 GMT</pubDate>
            <description><![CDATA[<p>메이즈라인은 고객사의 렌더링 파이프라인 제작 지원 과정에서 추가로 요청해 주신 아틀라스 빌더 툴을 약 15일간의 전담 기간을 통해 개발하였으며, 현재 1차 납품을 완료했습니다.</p>
<p>해당 툴은 이전 담당자분이 남기고 가신 리소스 자산을 복원하고, 사내 네이밍 컨벤션(name convention)에 맞춰 재정리할 수 있도록 구성되어 있습니다. 또한 이후 신규 아틀라스를 빌드하실 때, 사전에 정의된 인덱스 규칙(built-in index rule)에 따라 개별 소스 텍스처가 자동으로 지정된 그리드 좌표에 배치되도록 자동화하여 수동 편집 과정과 인적 오류 가능성을 최소화했습니다.</p>
<p>처리 과정 이야기를 간략히 기록 해 보려고 합니다.</p>
<ul>
<li>툴 동작 프리뷰
<a href="https://tv.kakao.com/embed/player/cliplink/458843647">https://tv.kakao.com/embed/player/cliplink/458843647</a></li>
</ul>
<h1 id="sge-voxel-block-texture-atlas-maker-v101-개발-회고">SGE Voxel Block Texture Atlas Maker v1.0.1 개발 회고</h1>
<p>복셀 게임용 텍스처 아틀라스 생성 툴을 개발하면서 겪은 기술적 도전과 해결 과정을 공유합니다.</p>
<h2 id="프로젝트-개요">프로젝트 개요</h2>
<p>16x16 그리드로 8192x8192 크기의 아틀라스를 생성하는 도구입니다. Color, Mask, Normal 세 종류의 텍스처 그룹을 지원하며, Clean Architecture 패턴을 적용하여 설계했습니다. 처음에는 단순한 툴이라고 생각했지만, 예상보다 많은 고민이 필요했습니다.</p>
<p>사용자분들의 원활한 전환을 위해 마이그레이션 가이드를 작성하고 Old Atlas Import 기능을 구현했습니다. 이를 통해 v1.0.0에서 작업한 파일들을 자동으로 변환할 수 있도록 했습니다.</p>
<p>업계 표준을 따르는 것이 장기적으로 유리합니다. 직관적이라고 생각한 설계가 실제로는 불편함을 야기할 수 있으며, 특히 외부 도구와의 연동을 고려하면 더욱 그렇습니다.</p>
<h2 id="성능-최적화">성능 최적화</h2>
<p>사실 개발 과정에서 잦은 수정과 이터레이션이 많았기 때문에 이것부터 처리 해야 했습니다. 일단 사용자에게 우리가 개발하고 있는 방향이 맞는지 보여줘야 하는 첫 번째 버전인 v1.0.0에서는 8192x8192 아틀라스 3개를 생성하는 데 15+초 이상이 소요되었습니다 (i7 기준). 50개 정도의 512x512 텍스처를 배치하는 작업임을 감안하면 개선이 필요한 수준이었습니다.</p>
<p>프로파일링 결과, 픽셀 단위 루프가 주요 병목 구간으로 확인되었습니다.</p>
<h3 id="벡터화-연산-도입">벡터화 연산 도입</h3>
<p>가장 큰 성능 향상은 픽셀 루프를 벡터화 연산으로 대체한 것입니다:</p>
<pre><code>함수 batch_place_textures(canvas, textures, cell_size):
    각 texture에 대해:
        // 배치할 영역 계산
        y_start = row * cell_size
        y_end = y_start + cell_size
        x_start = col * cell_size
        x_end = x_start + cell_size

        // 알파 블렌딩을 전체 영역에 한 번에 적용
        alpha = texture의 알파 채널을 0-1 범위로 정규화

        // 캔버스 영역 참조 (복사 없이 뷰만 가져옴)
        canvas_region = canvas[y_start:y_end, x_start:x_end]

        // 벡터화된 알파 블렌딩 (인플레이스 연산)
        canvas_region = canvas_region * (1 - alpha) + texture * alpha</code></pre><p>이 접근법의 핵심은 배열 슬라이싱을 통해 전체 영역을 일괄 처리하고, Broadcasting을 활용하여 알파 채널을 자동으로 확장하며, 인플레이스 연산으로 불필요한 메모리 할당을 최소화하는 것입니다.</p>
<p>픽셀 블렌딩 부분만 보면 약 10배 가까이 빨라졌습니다. I/O와 리샘플링 시간을 포함한 전체 파이프라인으로는 약 2배 정도 개선되었습니다.</p>
<h3 id="멀티프로세싱-병렬화">멀티프로세싱 병렬화</h3>
<p>Python의 GIL(Global Interpreter Lock) 제약을 고려하여 멀티쓰레딩 대신 멀티프로세싱을 적용했습니다:</p>
<pre><code>프로세스_풀 생성 (워커 3개):
    futures = 빈 딕셔너리

    각 atlas_type (Color, Mask, Normal)에 대해:
        output_path = 출력 디렉토리 / 파일명

        // 각 아틀라스를 별도 프로세스에서 생성
        future = 프로세스_풀.submit(
            export_single_atlas,
            output_path,
            관련 설정들...
        )
        futures[future] = atlas_type

    // 완료된 것부터 순차적으로 결과 수집
    완료된 각 future에 대해:
        atlas_type = futures[future]
        result = future.result()
        results[atlas_type] = result</code></pre><p>Color, Mask, Normal 아틀라스를 각각 독립적인 프로세스에서 생성하여 CPU 코어를 효율적으로 활용할 수 있었습니다.</p>
<h3 id="16-bit-변환-최적화">16-bit 변환 최적화</h3>
<p>8-bit에서 16-bit로의 변환 과정도 비트 연산을 활용하여 최적화했습니다:</p>
<pre><code>함수 load_texture_as_16bit(texture_path, target_size):
    이미지 열기:
        RGBA로 변환 (필요시)

        크기가 다르면:
            target_size로 리샘플링

        8-bit 배열로 변환

    // 비트 시프트를 활용한 16-bit 변환
    16bit_array = 빈 16-bit 배열 생성
    16bit_array = (8bit_array &lt;&lt; 8) | 8bit_array

    반환 16bit_array</code></pre><h3 id="최적화-결과">최적화 결과</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>v1.0</th>
<th>v1.1</th>
<th>개선</th>
</tr>
</thead>
<tbody><tr>
<td>생성 시간 (i7)</td>
<td>15초+</td>
<td>~7초</td>
<td><strong>3배</strong></td>
</tr>
<tr>
<td>메모리</td>
<td>2.4GB</td>
<td>1.7GB</td>
<td>약 30% 감소</td>
</tr>
<tr>
<td>CPU 활용</td>
<td>1코어 순차</td>
<td>3코어 병렬</td>
<td>병렬화 성공</td>
</tr>
</tbody></table>
<h3 id="tiff-포맷-채택">TIFF 포맷 채택</h3>
<p>출력 포맷을 PNG에서 TIFF로 변경했습니다. TIFF는 16-bit 데이터를 네이티브로 지원하여 안정성이 향상되었고, LZW 압축을 통해 효율적인 파일 크기 관리가 가능해졌습니다. 또한 읽기와 쓰기 속도도 PNG보다 빠른 것으로 확인되었습니다.</p>
<h2 id="clean-architecture의-이점">Clean Architecture의 이점</h2>
<p>최적화 과정에서 Clean Architecture 패턴의 장점을 실감할 수 있었습니다:</p>
<pre><code>domain/
  ├── grid_manager       # 관리 로직
  ├── models             # 도메인 모델
  └── services/
      ├── export_service            # 기존 버전
      └── export_service_optimized  # 최적화 버전</code></pre><p>계층이 분리되어 있어 기존 코드를 유지하면서 새로운 최적화 버전을 추가할 수 있었고, 각각을 독립적으로 테스트할 수 있었습니다. 문제 발생 시 즉시 롤백할 수 있어 안정적으로 작업할 수 있었습니다.</p>
<h2 id="정리-해-보면">정리 해 보면...</h2>
<p><strong>업계 표준 준수의 중요성</strong></p>
<p>직관적이라고 판단한 설계가 실제로는 더 큰 불편함을 초래할 수 있습니다. 특히 다른 시스템과의 통합을 고려하면 표준을 따르는 것이 필수적입니다.</p>
<p><strong>프로파일링 기반 최적화</strong></p>
<p>막연한 추측보다는 정확한 측정이 중요합니다. 프로파일링을 통해 픽셀 루프가 전체 시간의 70%를 차지한다는 것을 확인하고 집중적으로 개선할 수 있었습니다.</p>
<p><strong>벡터화 연산의 효과</strong></p>
<p>반복문을 벡터화 연산으로 대체하는 것만으로도 상당한 성능 향상을 얻을 수 있습니다. 이미지 처리 라이브러리의 벡터화 기능을 적극적으로 활용하는 것이 좋습니다.</p>
<p><strong>Python에서의 병렬 처리</strong></p>
<p>CPU 집약적 작업의 경우 GIL의 영향으로 멀티쓰레딩이 효과적이지 않습니다. 이런 경우 멀티프로세싱이 더 적합합니다.</p>
<p><strong>Breaking Change 관리</strong></p>
<p>불가피한 Breaking Change의 경우 명확한 마이그레이션 경로를 제공하고, 가능하다면 호환성 레이어를 구현하는 것이 사용자 경험에 도움이 됩니다.</p>
<h2 id="향후-계획">향후 계획</h2>
<p>성능은 크게 개선되었지만 여전히 개선 여지가 있습니다. GPU 가속을 통해 CUDA를 활용한 이미지 처리를 구현하면 추가적인 성능 향상을 기대할 수 있습니다. 자주 사용되는 텍스처를 메모리에 캐싱하는 스마트 캐싱 시스템도 고려하고 있습니다. 또한 여러 프로젝트를 한 번에 처리할 수 있는 배치 익스포트 기능과 자주 사용하는 설정을 저장하고 관리할 수 있는 프리셋 시스템도 계획 중입니다. 특히 GPU 가속은 이미지 처리 작업의 특성상 큰 성능 향상을 기대할 수 있어 우선순위를 두고 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[GPU-Driven Renderer에서의 Heterogeneous AoS Instance Encoding]]></title>
            <link>https://velog.io/@mazeline_1973/GPU-Driven-Renderer%EC%97%90%EC%84%9C%EC%9D%98-Heterogeneous-AoS-Instance-Encoding</link>
            <guid>https://velog.io/@mazeline_1973/GPU-Driven-Renderer%EC%97%90%EC%84%9C%EC%9D%98-Heterogeneous-AoS-Instance-Encoding</guid>
            <pubDate>Thu, 23 Oct 2025 06:39:54 GMT</pubDate>
            <description><![CDATA[<p>오늘도 지하철을 저 처럼 한 시간씩 타는 직장동료분들을 위한 읽을 거리를 추가 했습니다.</p>
<p>엔지니어 Zino의 글을 읽고 추가로 정리한 내용입니다.</p>
<p><a href="https://zino2201.substack.com/p/heterogenous-aos-instance-encoding">Heterogenous AoS instance encoding for a GPU-driven renderer</a></p>
<hr>
<p>대규모 오픈 월드 게임을 만들다 보면 한 가지 딜레마에 부딪힙니다. 수만 개의 나무, 건물, 캐릭터를 화면에 렌더링해야 하는데, 각 오브젝트마다 필요한 데이터가 전부 다릅니다. 어떤 오브젝트는 단순한 정적 메시만 있으면 되지만, 어떤 것은 스키닝 데이터, 복잡한 머티리얼, LOD 정보 등 방대한 데이터를 요구합니다.</p>
<p>이런 상황에서 Heterogeneous AoS(Array of Structures) instance encoding이 GPU-driven rendering 파이프라인의 핵심 해결책으로 자리잡고 있습니다. Ubisoft Montreal의 Assassin&#39;s Creed Unity는 이 기법으로 이전 작품 대비 10배 많은 오브젝트를 렌더링하면서도 CPU 사용량을 75% 수준으로 유지했습니다. Epic Games의 Unreal Engine 5 Nanite 시스템은 수억 개의 폴리곤을 실시간으로 처리하며 이 개념을 한 단계 더 발전시켰습니다.</p>
<h2 id="gpu-driven-rendering의-패러다임-전환">GPU-driven rendering의 패러다임 전환</h2>
<p>전통적인 렌더링 파이프라인에서는 CPU가 어떤 오브젝트를 그릴지 결정하고, 각 오브젝트마다 draw call을 발행했습니다. 그런데 이 방식은 근본적인 한계가 있었습니다. CPU는 순차적으로 작동하기 때문에, 오브젝트가 수만 개가 되면 draw call overhead만으로도 프레임 레이트가 바닥을 칩니다.</p>
<p>GPU-driven rendering은 이 문제를 근본적으로 해결합니다. GPU 자체가 어떤 오브젝트를 렌더링할지 결정하고, culling과 LOD 선택, 심지어 draw command 생성까지 모두 GPU compute shader에서 처리합니다. CPU-GPU 왕복 지연을 제거하고 GPU의 대규모 병렬 처리 능력을 최대한 활용하는 것이죠.</p>
<p>핵심은 간접 그리기(indirect drawing)입니다. DirectX의 <code>DrawIndexedInstancedIndirect</code>나 Vulkan의 <code>vkCmdDrawIndexedIndirect</code>를 사용하면, draw call 파라미터를 GPU 버퍼에 저장하고 GPU가 직접 읽어서 실행합니다. Vulkan 1.2의 <code>DrawIndirectCount</code>는 GPU가 draw call 개수까지 결정할 수 있게 해줍니다. CPU는 단순히 &quot;이 버퍼를 읽어서 그려&quot;라고만 지시하면 되고, 실제로 몇 개를 그릴지는 GPU의 culling shader가 결정합니다.</p>
<p>SIGGRAPH 2015의 Ubisoft 발표를 보면, Assassin&#39;s Creed Unity는 25만 개의 오브젝트를 단일 Jaguar 코어에서 0.2ms만에 처리하면서도 GPU에서 20-40%의 삼각형을 culling할 수 있었습니다. 이게 실무에서 GPU-driven이 얼마나 강력한지 보여주는 증거입니다.</p>
<h2 id="heterogeneous-instance-encoding의-핵심-아이디어">Heterogeneous instance encoding의 핵심 아이디어</h2>
<p>전통적인 uniform instancing에서는 모든 인스턴스가 동일한 구조를 가집니다. 같은 메시, 같은 버텍스 레이아웃, 고정된 크기의 attribute 구조체를 사용하죠. 그런데 실제 게임 세계는 훨씬 복잡합니다.</p>
<p>정적인 바위는 transform matrix만 있으면 되지만, 애니메이션되는 캐릭터는 스키닝 데이터가 필요하고, 복잡한 머티리얼을 가진 오브젝트는 다수의 텍스처 참조가 필요합니다. Heterogeneous instance encoding은 이런 다양한 요구사항을 효율적으로 처리하는 데이터 구조 패턴입니다.</p>
<p>먼저 AoS(Array of Structures)와 SoA(Structure of Arrays)의 차이를 알아야 합니다. AoS는 각 오브젝트의 모든 데이터를 하나의 구조체로 묶어서 배열로 저장합니다:</p>
<pre><code class="language-cpp">struct Particle {
    vec3 position;
    vec3 velocity;
    float mass;
};
Particle particles[10000];</code></pre>
<p>반면 SoA는 각 필드를 별도의 배열로 분리합니다:</p>
<pre><code class="language-cpp">struct ParticleSystem {
    vec3 positions[10000];
    vec3 velocities[10000];
    float masses[10000];
};</code></pre>
<p>GPU 아키텍처 관점에서 이 둘의 성능 차이는 memory coalescing에 달려 있습니다. NVIDIA 문서를 보면, GPU의 warp(32개 스레드)가 연속된 메모리 주소에 접근할 때 하나의 128바이트 캐시 라인으로 coalescing되어 메모리 트랜잭션이 최소화됩니다.</p>
<p>실험 결과를 보면 stride-1 접근(coalesced)은 206 GB/s 처리량을 달성한 반면, stride-32 접근은 15.2 GB/s로 93%나 감소했습니다. 이게 메모리 접근 패턴이 얼마나 중요한지 보여줍니다.</p>
<p>그런데 instance 데이터의 경우 어떤 레이아웃이 좋을까요? 핵심은 접근 패턴입니다. GPU의 각 스레드가 하나의 인스턴스를 처리할 때 그 인스턴스의 모든 데이터를 함께 사용한다면 AoS가 유리합니다. 연속된 스레드들이 연속된 인스턴스 구조체를 읽으면서 자연스럽게 coalescing이 발생하기 때문입니다.</p>
<p>여기서 &quot;heterogeneous&quot;의 의미가 드러납니다. 인스턴스 구조체 자체는 고정된 크기의 uniform AoS layout을 유지하지만, 각 인스턴스가 참조하는 데이터의 종류와 크기는 다양합니다.</p>
<p>Melba 엔진이 보여준 접근 방식이 좋은 예입니다. 64바이트 크기의 uniform instance descriptor를 사용하면서도, 각 인스턴스가 서로 다른 render item, material, geometry를 가리키도록 설계했습니다:
Melba 엔진은 배틀그라운드(PUBG)의 창시자인 Brendan Greene(PLAYERUNKNOWN)이 설립한 게임 스튜디오에서 개발 중인 차세대 게임 엔진입니다.</p>
<p>이 글에서는 Melba 엔진이 GPU-driven rendering에서 heterogeneous instance encoding을 구현한 방식을 예시로 다루고 있습니다. 특히 64바이트 크기의 uniform instance descriptor를 사용하면서도, 각 인스턴스가 서로 다른 render item, material, geometry를 가리킬 수 있도록 설계한 점이 핵심입니다.</p>
<p>Melba 엔진은 대규모 오픈 월드 게임을 목표로 개발되고 있으며, 메모리 효율성과 렌더링 성능 최적화에 중점을 두고 있는 것으로 보입니다.</p>
<pre><code class="language-cpp">struct sb_render_instance_t {
    float4x3 m_transform;        // 48 bytes
    uint m_render_item_id;       // 4 bytes - indirection
    uint m_entity_id;            // 4 bytes
    uint m_user_data;            // 4 bytes
    float m_world_scale;         // 4 bytes
};  // Total: 64 bytes</code></pre>
<p>이 구조체의 핵심은 <code>m_render_item_id</code>입니다. 이 ID로 실제 geometry, material, culling data가 담긴 render item 구조체를 간접 참조합니다. Render item 자체는 100-200바이트로 훨씬 크지만, 여러 인스턴스가 공유할 수 있습니다. 최종적으로는 계층적인 indirection 구조가 됩니다:</p>
<pre><code>Instance (64B) → Render Item (200B) → Material (100B) → Textures
                              ↓
                          Geometry Buffers</code></pre><h2 id="메모리-대역폭과-캐시-효율성">메모리 대역폭과 캐시 효율성</h2>
<p>이런 heterogeneous 구조가 해결하는 첫 번째 문제는 메모리 대역폭 낭비입니다. 전통적인 uniform batching에서는 모든 인스턴스가 동일한 크기의 데이터를 가져야 하므로, 가장 복잡한 인스턴스에 맞춰 구조체 크기가 결정됩니다. 단순한 정적 메시도 스키닝 데이터를 위한 빈 공간을 차지하게 됩니다.</p>
<p>Ubisoft Kiev의 Trials Rising GDC 2019 발표를 보면, 이들은 인스턴스를 immobile(64.21%), mobile(25.09%), mutable(3.74%), skinned(6.96%)로 분류하고 각 카테고리마다 다른 업데이트 주기와 데이터 크기를 사용했습니다. 결과적으로 static 인스턴스는 71%, skinned 인스턴스는 59%의 메모리를 절약했고, CPU synchronization 비용은 7.18ms에서 5.89ms로 감소했습니다.</p>
<p>캐시 효율성 측면에서도 중요한 이점이 있습니다. 현대 GPU의 L1 캐시 라인은 128바이트이고, L2 캐시는 수 MB 규모입니다. 64바이트 instance descriptor는 L1 캐시 라인 하나에 2개가 딱 들어가므로, 32개 스레드로 구성된 warp가 32개 인스턴스를 읽을 때 정확히 16개 캐시 라인만 필요합니다.</p>
<p>더 중요한 것은 render item과 material 데이터가 여러 인스턴스 간에 공유되므로 L2 캐시 재사용률이 극도로 높아진다는 점입니다. 1000개의 나무 인스턴스가 모두 같은 render item을 참조한다면, render item은 한 번만 읽으면 되고 이후로는 L2 캐시에서 바로 가져올 수 있습니다.</p>
<h2 id="실제-게임-엔진-구현-사례">실제 게임 엔진 구현 사례</h2>
<h3 id="assassins-creed-unity">Assassin&#39;s Creed Unity</h3>
<p>Assassin&#39;s Creed Unity의 구현은 GPU-driven rendering의 이정표입니다. 이들은 mesh cluster rendering이라는 개념을 도입했는데, 전체 메시를 64개 삼각형 단위의 클러스터로 분해하고 각 클러스터마다 독립적으로 culling을 수행했습니다.</p>
<p>각 클러스터는 bounding cone 정보를 가지고 있어서 backface culling을 GPU에서 수행할 수 있었고, 이는 10-30%의 삼각형을 제거하는 효과를 냈습니다. 더 인상적인 것은 static triangle backface culling입니다. 클러스터 중심에서 본 cubemap의 각 픽셀에 대해 어떤 삼각형이 보이는지 미리 계산해두고, 렌더링 시에는 카메라 방향에 따라 cubemap을 lookup해서 삼각형 가시성을 판단합니다. Shadow rendering에서는 무려 30-80%의 삼각형이 culling됩니다.</p>
<h3 id="frostbite-엔진">Frostbite 엔진</h3>
<p>Ubisoft의 Frostbite 엔진 GDC 2016 발표를 보면 per-triangle culling을 한 단계 더 발전시켰습니다. Orientation culling, depth culling, small triangle culling, frustum culling을 모두 조합해서 삼각형 수준의 세밀한 culling을 수행했고, parallel prefix sum을 사용해 barrier 없이 index buffer를 compaction했습니다.</p>
<p>특히 주목할 점은 software Z-buffer 활용입니다. CPU에서 다음 GPU 프레임을 위해 미리 depth buffer를 렌더링해두고 이를 occlusion culling과 Hi-Z pyramid의 초기값으로 사용했습니다. 1프레임 latency가 생기지만 bounding volume을 약간 키우는 것으로 false negative를 최소화할 수 있습니다.</p>
<h3 id="unreal-engine-5-nanite">Unreal Engine 5 Nanite</h3>
<p>Unreal Engine 5의 Nanite 시스템은 virtual geometry의 완성형입니다. Nanite는 128개 삼각형으로 구성된 클러스터를 기본 단위로 사용하고, 각 메시를 DAG(Directed Acyclic Graph) 구조의 LOD hierarchy로 표현합니다.</p>
<p>런타임에 GPU는 이 DAG를 순회하면서 화면상 크기에 따라 적절한 LOD 레벨의 클러스터들을 선택하고, 최종적으로는 화면 전체에서 약 2500만 개의 삼각형을 일정하게 유지합니다. Valley of the Ancients 데모에서는 약 2.5ms만에 모든 Nanite 메시를 culling하고 rasterization할 수 있었습니다. 100만 개 이상의 인스턴스를 처리할 수 있다는 것도 놀랍습니다.</p>
<h3 id="unity-gpu-instancing">Unity GPU Instancing</h3>
<p>Unity의 GPU instancing은 비교적 전통적인 접근 방식을 유지하면서도 실용적입니다. GPU instancing을 활성화하면 같은 메시와 머티리얼을 사용하는 GameObject들을 하나의 draw call로 렌더링합니다.</p>
<p>Per-instance 데이터는 <code>UNITY_INSTANCING_CBUFFER</code>에 정의하고 <code>UNITY_ACCESS_INSTANCED_PROP</code> 매크로로 접근합니다:</p>
<pre><code class="language-glsl">UNITY_INSTANCING_BUFFER_START(Props)
    UNITY_DEFINE_INSTANCED_PROP(float4, _Color)
UNITY_INSTANCING_BUFFER_END(Props)

// Fragment Shader
fixed4 color = UNITY_ACCESS_INSTANCED_PROP(Props, _Color);</code></pre>
<h2 id="gpu-아키텍처별-특성">GPU 아키텍처별 특성</h2>
<h3 id="nvidia-ampere">NVIDIA Ampere</h3>
<p>NVIDIA의 최신 Ampere 아키텍처는 SM(Streaming Multiprocessor)당 128KB의 L1/Shared Memory를 제공하고, 최대 64개의 warp(2048 스레드)를 동시에 실행할 수 있습니다.</p>
<p>Compute Capability 3.5 이상부터는 global memory load가 기본적으로 L1 캐시를 우회하고 32바이트 단위로 coalescing합니다. 이는 scattered access pattern에서 over-fetch를 줄이기 위함입니다. 128바이트 stride로 접근하면 L1을 사용할 때는 warp당 4096바이트를 읽어야 하지만, L2만 사용하면 1024바이트만 읽으면 되므로 4배의 bandwidth 절약이 가능합니다.</p>
<h3 id="amd-rdna-2">AMD RDNA 2</h3>
<p>AMD의 RDNA 2 아키텍처는 상당히 다른 접근을 취합니다. 가장 큰 차이는 128MB Infinity Cache입니다. 이 on-die L3 캐시는 1986.6 GB/s의 대역폭을 제공하며, 256비트 메모리 버스로도 384비트 버스에 준하는 성능을 낼 수 있게 합니다.</p>
<p>RDNA는 Wave32(32개 스레드)를 네이티브로 지원하는데, 이는 NVIDIA의 warp와 크기가 같아서 많은 알고리즘이 양쪽 플랫폼에서 유사하게 작동합니다. 다만 GCN 아키텍처는 여전히 Wave64를 사용하므로, 레거시 플랫폼 지원이 필요하다면 이를 고려해야 합니다.</p>
<h3 id="occupancy-최적화">Occupancy 최적화</h3>
<p>Occupancy 관점에서 보면, 높은 occupancy가 반드시 높은 성능을 의미하지는 않습니다. 일반적으로 60-80% occupancy가 최적입니다. 100% occupancy를 달성하려고 레지스터 사용을 극단적으로 줄이면 오히려 instruction dispatch가 비효율적이 될 수 있습니다.</p>
<p>실측 결과를 보면, NVIDIA RTX A6000는 17.6 TB/s의 L1 bandwidth를 달성했고, AMD RX 6900 XT는 23.4 TB/s를 기록했습니다. 이는 이론적 최대치의 80-90%에 해당하는 우수한 효율입니다.</p>
<h2 id="quantization과-데이터-압축">Quantization과 데이터 압축</h2>
<h3 id="quaternion-compression">Quaternion Compression</h3>
<p>Instance data의 크기를 줄이는 것은 메모리 대역폭을 절약하는 가장 직접적인 방법입니다. Quaternion compression은 그중에서도 가장 효과적인 기법입니다.</p>
<p>&quot;Smallest three&quot; 기법은 quaternion의 4개 컴포넌트 중 절댓값이 가장 큰 것을 찾아서 인덱스로 저장하고, 나머지 3개만 quantize합니다. x²+y²+z²+w²=1 제약을 이용해 마지막 컴포넌트를 재구성합니다.</p>
<p>각 컴포넌트를 9비트로 인코딩하면 총 29비트(3×9 + 2비트 인덱스)로 압축할 수 있어서, 128비트에서 77% 절약이 가능합니다:</p>
<pre><code class="language-c">struct PackedQuat {
    uint16_t a : 9;
    uint16_t b : 9;
    uint16_t c : 9;
    uint16_t index : 2;
    uint16_t sign : 1;
};

void PackQuaternion(float x, float y, float z, float w, PackedQuat* out) {
    // Find largest component
    float abs_vals[4] = {fabs(x), fabs(y), fabs(z), fabs(w)};
    int largest = 0;
    for(int i = 1; i &lt; 4; i++) {
        if(abs_vals[i] &gt; abs_vals[largest]) largest = i;
    }

    // Make positive and pack smallest three
    float sign = ((&amp;x)[largest] &gt;= 0) ? 1.0f : -1.0f;
    const float scale = 511.0f / 0.707107f;
    int j = 0;
    for(int i = 0; i &lt; 4; i++) {
        if(i != largest) {
            float val = (&amp;x)[i] * sign;
            (&amp;out-&gt;a)[j++] = (int)(val * scale + 0.5f) + 511;
        }
    }
    out-&gt;index = largest;
}</code></pre>
<h3 id="matrix-decomposition">Matrix Decomposition</h3>
<p>Matrix decomposition은 transform을 Scale-Rotation-Translation(SRT)로 분해해서 저장하는 기법입니다. 4×3 matrix는 48바이트가 필요하지만 SRT로 분해하면 rotation(quaternion 8바이트), translation(FP16 vec3 6바이트), scale(logarithmic encoding 2바이트)로 총 16바이트면 충분합니다. 66%의 절약입니다.</p>
<p>Vertex shader에서 재구성할 때는 FMA(Fused Multiply-Add) 한 번으로 충분하므로 compute overhead도 무시할 수 있습니다. 실제로 이 기법은 1400개 캐릭터와 1억 개 버텍스를 60fps로 렌더링하는 Toy Renderer에서 L1 캐시 병목을 완전히 제거했습니다.</p>
<h3 id="normal과-tangent-압축">Normal과 Tangent 압축</h3>
<p>QTangent는 전체 tangent space(Normal, Tangent, Bitangent)를 하나의 quaternion으로 인코딩합니다. 16비트 SNORM으로 인코딩하면 64비트(8바이트)가 되어 원래 36바이트에서 78% 절약됩니다.</p>
<p>대안으로는 octahedral mapping이 있습니다. Normal vector를 octahedron에 projection하고 다시 2D 평면으로 펼치면, 2개의 값만으로 모든 방향을 표현할 수 있습니다. SNORM8로 인코딩하면 겨우 2바이트로 normal을 표현할 수 있어 83% 절약입니다:</p>
<pre><code class="language-glsl">// Encode
vec2 EncodeOctahedral(vec3 n) {
    float L1 = abs(n.x) + abs(n.y) + abs(n.z);
    vec2 res = n.xy / L1;
    if(n.z &lt; 0) res = (1 - abs(res.yx)) * sign(res);
    return res;
}

// Decode (vertex shader)
vec3 DecodeOctahedral(vec2 e) {
    vec3 v = vec3(e, 1 - abs(e.x) - abs(e.y));
    float t = max(-v.z, 0);
    v.xy += t * -sign(v.xy);
    return normalize(v);
}</code></pre>
<h2 id="ray-tracing과-acceleration-structure">Ray Tracing과 Acceleration Structure</h2>
<p>Ray tracing을 지원하려면 instance data를 acceleration structure에도 반영해야 합니다. TLAS(Top-Level Acceleration Structure)의 각 인스턴스는 3×4 transform matrix와 함께 24비트 custom index를 가질 수 있습니다. 이 custom index에 material ID를 저장하면, closest hit shader에서 즉시 material을 fetch할 수 있습니다:</p>
<pre><code class="language-glsl">// Closest hit shader
void main() {
    uint materialID = gl_InstanceCustomIndexEXT;
    uint instanceID = gl_InstanceID;
    InstanceData instance = instanceBuffer[instanceID];
    Material material = materialBuffer[materialID];
    // Shading...
}</code></pre>
<p>Ray tracing에서 가장 중요한 최적화는 acceleration structure build flags입니다. Static geometry는 <code>PREFER_FAST_TRACE</code> 플래그를 사용해 build 시간이 오래 걸리더라도 traversal 성능을 최대화해야 합니다. Dynamic geometry는 <code>PREFER_FAST_BUILD</code>로 빠른 rebuild를 지원하고, deforming geometry는 <code>ALLOW_UPDATE</code>로 refit만 가능하게 합니다.</p>
<p>특히 static geometry에는 <code>ALLOW_COMPACTION</code>을 사용해 빌드 후 compaction을 수행하면 메모리를 30-50% 절약할 수 있습니다. 전체 AS 빌드는 프레임당 2ms 이내로 제한하고, 가능하면 async compute queue로 분리해서 graphics queue와 병렬 실행하는 것이 좋습니다.</p>
<p>Ray payload 최적화도 중요합니다. Payload는 TraceRay 호출 간에 유지되는 데이터인데, 32바이트를 초과하면 register spill이 발생해 성능이 급격히 떨어집니다. FP16, UNORM8 등을 적극 활용하고, boolean 값들은 bitfield로 묶어야 합니다.</p>
<h2 id="mesh-shader와-modern-pipeline">Mesh Shader와 Modern Pipeline</h2>
<p>Mesh shader는 전통적인 vertex input assembler를 완전히 대체하는 compute-based geometry pipeline입니다. Compute programming model을 사용하면서도 rasterizer로 직접 primitive를 출력할 수 있습니다. 이는 heterogeneous instance encoding과 완벽하게 결합됩니다.</p>
<p>Meshlet은 mesh shader의 기본 단위입니다. NVIDIA의 권장사항은 64개의 unique vertex와 126개의 삼각형입니다. 이는 384바이트 캐시 라인에 딱 들어가는 크기입니다:</p>
<pre><code class="language-cpp">struct MeshletDesc {
    uint32_t vertexCount, primCount;
    uint32_t vertexBegin, primBegin;
    vec3 boundsMin, boundsMax;
    vec4 normalCone;  // For backface culling
};</code></pre>
<p>Task shader는 meshlet 단위로 early culling을 수행합니다. Subgroup intrinsic을 활용하면 warp 내에서 visible meshlet만 compact하게 출력할 수 있습니다:</p>
<pre><code class="language-glsl">taskNV out Task { uint baseID; uint8_t subIDs[32]; } OUT;

void main() {
    bool render = !EarlyCull(meshletDescs[gl_GlobalInvocationID.x]);
    uvec4 vote = subgroupBallot(render);

    if (gl_LocalInvocationID.x == 0) {
        gl_TaskCountNV = subgroupBallotBitCount(vote);
    }

    if (render) {
        uint idx = subgroupBallotExclusiveBitCount(vote);
        OUT.subIDs[idx] = gl_LocalInvocationID.x;
    }
}</code></pre>
<h2 id="virtual-geometry와-nanite의-교훈">Virtual Geometry와 Nanite의 교훈</h2>
<p>Unreal Engine 5의 Nanite는 virtual geometry의 정점입니다. 핵심 아이디어는 간단합니다. 모든 메시를 cluster 단위로 분해하고, LOD hierarchy를 DAG로 구성한 뒤, 런타임에 GPU가 적절한 cluster들을 선택해서 렌더링하는 것입니다.</p>
<p>하지만 Nanite의 모든 기능을 구현하지 않아도 60-70% 정도의 이점은 얻을 수 있습니다. 필수적인 것은 meshlet 기반 cluster rendering, visibility buffer, two-pass occlusion culling, GPU-driven indirect draw, 기본적인 LOD selection입니다.</p>
<p>Visibility buffer는 전통적인 G-buffer와 다릅니다. G-buffer는 각 픽셀에 material properties(albedo, normal, roughness 등)를 저장하지만, visibility buffer는 단순히 cluster ID와 triangle ID만 저장합니다. 이후 compute shader에서 필요한 픽셀들만 material을 evaluate합니다:</p>
<pre><code class="language-glsl">// Visibility buffer pass
out uint visBuffer;
void main() {
    uint clusterID = gl_DrawID;
    uint triangleID = gl_PrimitiveID;
    visBuffer = (clusterID &lt;&lt; 7) | triangleID;
}

// Material evaluation compute shader
void main() {
    uint packed = visBuffer[pixel];
    uint clusterID = packed &gt;&gt; 7;
    uint triangleID = packed &amp; 0x7F;

    // Fetch triangle vertices
    Cluster cluster = clusters[clusterID];
    Triangle tri = GetTriangle(cluster, triangleID);

    // Interpolate attributes
    vec3 barycentrics = ComputeBarycentrics(pixel);
    VertexData vData = InterpolateVertex(tri, barycentrics);

    // Evaluate material
    Material mat = materials[cluster.materialID];
    GBuffer gbuffer = EvaluateMaterial(mat, vData);
    WriteGBuffer(pixel, gbuffer);
}</code></pre>
<p>이 접근법의 장점은 overdraw가 저렴하다는 것입니다. Visibility buffer는 uint 하나만 쓰므로 fill rate가 매우 높고, material evaluation은 최종적으로 visible한 픽셀에 대해서만 수행됩니다.</p>
<h2 id="실무에서의-함정들">실무에서의 함정들</h2>
<p>GPU-driven rendering을 구현하다 보면 여러 함정에 빠지기 쉽습니다.</p>
<p><strong>Acceleration structure build blocking</strong></p>
<p>AS build가 graphics queue를 block하는 문제입니다. 매 프레임 BLAS를 rebuild하면 2-3ms가 소요되는데, 이를 graphics queue에서 동기적으로 수행하면 rendering이 멈춥니다. 해결책은 async compute queue를 사용하는 것입니다.</p>
<p><strong>Indirection overhead</strong></p>
<p>Heterogeneous instance encoding은 여러 단계의 pointer chasing을 요구합니다. Instance → Render Item → Material → Textures 순으로 참조하면 각 단계마다 memory latency가 발생합니다. 다행히 GPU는 massive parallelism으로 latency를 hiding할 수 있고, render item과 material은 여러 인스턴스가 공유하므로 L2 캐시 hit rate가 매우 높습니다.</p>
<p><strong>Warp divergence</strong></p>
<p>Heterogeneous batch에서 다른 타입의 인스턴스들이 섞여 있으면 shader code에서 branching이 발생합니다. 해결책은 type별로 batching하고, shader는 compile-time constant로 특화되도록 하는 것입니다.</p>
<p><strong>Small instance overhead</strong></p>
<p>화면상 1픽셀 미만의 작은 인스턴스가 많으면 culling과 setup overhead가 이득을 초과할 수 있습니다. UE 5.1에서 추가된 &quot;Preserve Area&quot; 플래그가 이를 해결합니다.</p>
<p><strong>Any-hit shader 남용</strong></p>
<p>Alpha-tested geometry를 any-hit shader로 처리하면 ray traversal 성능이 크게 떨어집니다. 가능하면 geometry를 OPAQUE로 mark하고, opacity micromap(OMM)을 사용하는 것이 2-3배 빠릅니다.</p>
<h2 id="구현-로드맵">구현 로드맵</h2>
<p>실제로 heterogeneous AoS instance encoding을 구현한다면 이런 순서를 추천합니다.</p>
<p><strong>1단계: Indirect drawing infrastructure</strong></p>
<p><code>DrawIndirectCount</code> 지원을 확인하고, fallback으로 일반 <code>DrawIndirect</code>를 준비합니다. Instance buffer와 ObjectID system을 설계하고, draw command generation compute shader를 작성합니다. 간단한 frustum culling만 구현해도 충분합니다.</p>
<p><strong>2단계: Advanced culling</strong></p>
<p>Depth pyramid generator를 구현하고 Hi-Z occlusion culling을 적용합니다. Async compute queue로 분리해서 culling과 rendering을 overlap시킵니다.</p>
<p><strong>3단계: Ray tracing (필요시)</strong></p>
<p>BLAS/TLAS builder를 구현하고, instance descriptor에 custom data를 설정합니다. Build flag를 content type별로 최적화하고, static geometry에 compaction을 적용합니다.</p>
<p><strong>4단계: Advanced features</strong></p>
<p>Mesh shader 지원을 평가하고, meshlet generation pipeline을 구축합니다. Task shader culling을 추가하고, virtual geometry 도입을 검토합니다.</p>
<p><strong>5단계: Streaming과 LOD</strong></p>
<p>LOD selection system을 구현하고, texture streaming을 추가합니다. Memory budget을 설정하고 streaming latency를 profile합니다.</p>
<h2 id="측정-가능한-성능-목표">측정 가능한 성능 목표</h2>
<p>실무에서 기대할 수 있는 성능 목표입니다:</p>
<ul>
<li>Draw call count: 프레임당 100개 미만</li>
<li>GPU culling time: rasterization 1ms 미만, ray tracing AS build 2ms 미만</li>
<li>Instance count per batch: 수백 개 이상</li>
<li>Occlusion culling: 30-50% elimination</li>
<li>Quantization으로 instance data 50-75% 절약</li>
<li>Ray tracing AS compaction으로 30-50% 메모리 절약</li>
</ul>
<h2 id="마치며">마치며</h2>
<p>Heterogeneous AoS instance encoding은 단순한 데이터 구조가 아니라 현대 GPU-driven rendering의 철학을 담고 있습니다. GPU의 massive parallelism과 compute shader의 유연성을 활용하면서도, 메모리 coalescing과 cache hierarchy를 존중하는 것입니다.</p>
<p>64바이트의 uniform instance descriptor는 hardware-friendly하면서도, indirection을 통해 heterogeneous한 render item, material, geometry를 가리킬 수 있습니다. 이는 flexibility와 performance의 절묘한 균형입니다.</p>
<p>실제 구현 사례들을 보면 이 기법이 얼마나 강력한지 알 수 있습니다. Assassin&#39;s Creed Unity는 2014년에 이미 25만 개 오브젝트를 렌더링했고, Unreal Engine 5 Nanite는 100만 개 이상의 인스턴스를 처리합니다. 이런 성과들은 단순히 더 빠른 하드웨어가 아니라, 똑똑한 알고리즘과 데이터 구조 설계에서 나옵니다.</p>
<p>앞으로의 발전 방향도 명확합니다. DirectX 12의 Work Graphs는 GPU-driven pipeline을 더욱 유연하게 만들 것이고, mesh shader는 점점 더 표준이 될 것입니다. Virtual geometry는 Nanite를 넘어 더 많은 엔진에서 채택될 것이며, ray tracing의 대중화는 acceleration structure 최적화를 더욱 중요하게 만들 것입니다.</p>
<p>하지만 근본적인 원칙은 변하지 않습니다. GPU 아키텍처를 이해하고, 메모리 대역폭을 존중하며, 적절한 abstraction을 선택하는 것입니다. 실제 프로덕션에 적용할 때는 항상 profile first, optimize second를 기억하세요.</p>
<h2 id="레퍼런스">레퍼런스</h2>
<ul>
<li><a href="https://vkguide.dev/docs/gpudriven/gpu_driven_engines/">Vulkan Guide - GPU-driven engines</a></li>
<li><a href="https://advances.realtimerendering.com/s2015/aaltonenhaar_siggraph2015_combined_final_footer_220dpi.pdf">SIGGRAPH 2015 - Assassin&#39;s Creed Unity GPU-driven rendering</a></li>
<li><a href="https://docs.nvidia.com/cuda/cuda-c-programming-guide/">NVIDIA CUDA Programming Guide</a></li>
<li><a href="https://www.ncbi.nlm.nih.gov/pmc/articles/PMC4868575/">NCBI - GPU acceleration study on memory coalescing</a></li>
<li><a href="https://link.springer.com/chapter/10.1007/978-3-319-27308-2_47">University of Heidelberg - AoS vs SoA performance study</a></li>
<li><a href="https://www.gdcvault.com/">GDC - PLAYERUNKNOWN Productions Melba Engine</a></li>
<li><a href="https://media.gdcvault.com/gdc2019/presentations/Drazhevskyi_Oleksandr_GPU_Driven_Rendering.pdf">GDC 2019 - Ubisoft Kiev Trials Rising</a></li>
<li><a href="https://docs.nvidia.com/cuda/cuda-c-best-practices-guide/">NVIDIA Best Practices Guide</a></li>
<li><a href="https://frostbite-wp-prd.s3.amazonaws.com/wp-content/uploads/2016/03/29204330/GDC_2016_Compute.pdf">GDC 2016 - Frostbite Engine</a></li>
<li><a href="https://docs.unrealengine.com/5.0/en-US/RenderingFeatures/Nanite/">Unreal Engine 5 Nanite Documentation</a></li>
<li><a href="https://docs.unity3d.com/Manual/GPUInstancing.html">Unity GPU Instancing Documentation</a></li>
<li><a href="https://gpuopen.com/">AMD GPUOpen - RDNA 2 Architecture</a></li>
<li><a href="https://chipsandcheese.com/">Chips and Cheese - GPU L1 Cache Analysis</a></li>
<li><a href="https://gafferongames.com/post/snapshot_compression/">Gaffer On Games - Snapshot Compression</a></li>
<li><a href="https://momentsingraphics.de/">Toy Renderer - Matrix Decomposition</a></li>
<li><a href="https://developer.android.com/games/optimize/vertex-data-management">Android Developer - Vertex Data Management</a></li>
<li><a href="https://developer.nvidia.com/blog/best-practices-using-nvidia-rtx-ray-tracing/">NVIDIA RTX Best Practices</a></li>
<li><a href="https://www.khronos.org/blog/mesh-shading-for-vulkan">Vulkan Mesh Shader Extension</a></li>
<li><a href="https://jglrxavpok.github.io/2024/01/19/recreating-nanite-lod-generation.html">Simplified Nanite Implementation Guide</a></li>
<li><a href="https://developer.nvidia.com/blog/increase-ray-tracing-performance-using-opacity-micromaps/">NVIDIA Opacity Micromap Guide</a></li>
<li><a href="https://vkguide.dev/docs/gpudriven/compute_culling/">Vulkan Guide - Hi-Z Occlusion Culling</a></li>
<li><a href="https://docs.unrealengine.com/5.0/en-US/nanite-virtualized-geometry-in-unreal-engine/">Unreal Engine - Texture Streaming Pool</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Unity URP Decal과 After Transparent Depth 충돌 해결 가이드]]></title>
            <link>https://velog.io/@mazeline_1973/Unity-URP-Decal%EA%B3%BC-After-Transparent-Depth-%EC%B6%A9%EB%8F%8C-%ED%95%B4%EA%B2%B0-%EA%B0%80%EC%9D%B4%EB%93%9C</link>
            <guid>https://velog.io/@mazeline_1973/Unity-URP-Decal%EA%B3%BC-After-Transparent-Depth-%EC%B6%A9%EB%8F%8C-%ED%95%B4%EA%B2%B0-%EA%B0%80%EC%9D%B4%EB%93%9C</guid>
            <pubDate>Wed, 22 Oct 2025 08:12:37 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>본 문서는 실무에서 Unity URP 기반 프로젝트를 진행하던 중 Decal Render Feature와 Depth Texture Mode 간의 충돌 문제를 경험하고, 이를 해결하기 위해 여러 방법을 연구한 결과를 정리한 내용입니다. (   feat.   김강언 시니어  TA )</p>
</blockquote>
<hr>
<h2 id="핵심-문제-정의">핵심 문제 정의</h2>
<p>URP의 Decal Render Feature와 &quot;After Transparents&quot; Depth Texture Mode는 근본적인 아키텍처 충돌로 인해 함께 사용할 수 없습니다.</p>
<p><strong>Unity 공식 입장</strong></p>
<p>Unity Support 팀에 따르면 &quot;엔진 코드 수정 없이는 해결책이 없다&quot;는 고 보고됨. DBuffer 데칼은 RenderPass Event 201에서 깊이 텍스처를 요구하지만, &quot;After Transparents&quot;는 Event 500까지 깊이 복사를 지연시키려 합니다. 이 타이밍 충돌은 URP 12.0(Unity 2021.2)부터 Unity 6까지 지속되고 있습니다.</p>
<hr>
<h2 id="문제-분석">문제 분석</h2>
<h3 id="근본-원인-렌더링-파이프라인-실행-순서-충돌">근본 원인: 렌더링 파이프라인 실행 순서 충돌</h3>
<h3 id="renderpassevent-실행-순서">RenderPassEvent 실행 순서</h3>
<p>렌더링 파이프라인은 다음과 같은 순서로 실행됩니다.</p>
<pre><code class="language-csharp">BeforeRenderingShadows(50)
↓
AfterRenderingShadows(100)
↓
BeforeRenderingPrePasses(150)
↓
AfterRenderingPrePasses(200) ← DBuffer 데칼이 여기서 깊이 요구(201)
↓
BeforeRenderingOpaques(250)
↓
AfterRenderingOpaques(300)
↓
BeforeRenderingSkybox(350)
↓
BeforeRenderingTransparents(450)
↓
AfterRenderingTransparents(500) ← &quot;After Transparents&quot; 설정 타겟
↓
BeforeRenderingPostProcessing(550)
↓
AfterRendering(1000)</code></pre>
<h3 id="충돌-메커니즘">충돌 메커니즘</h3>
<p><strong>DBuffer 데칼의 요구사항</strong></p>
<p>DBuffer 데칼은 <code>RenderPassEvent.AfterRenderingPrePasses + 1</code>(이벤트 201)에서 실행됩니다. 이때 <code>ScriptableRenderPassInput.Depth</code>와 <code>ScriptableRenderPassInput.Normal</code>이 필수 입력으로 요구됩니다. 그 결과 UniversalRenderer가 자동으로 깊이 복사를 이벤트 200에서 강제 실행하게 됩니다.</p>
<h3 id="실제-동작-비교">실제 동작 비교</h3>
<p><strong>데칼 없이 After Transparents를 사용하는 경우</strong></p>
<ol>
<li>불투명 오브젝트 렌더링</li>
<li>투명 오브젝트 렌더링</li>
<li>깊이 복사 실행 (이벤트 500)</li>
<li>후처리 실행</li>
</ol>
<p><strong>DBuffer 데칼을 활성화한 경우</strong></p>
<ol>
<li>DepthNormal Prepass 실행</li>
<li>깊이 복사 강제 실행 (이벤트 200)</li>
<li>DBuffer 렌더패스 실행 (이벤트 201)</li>
<li>불투명 오브젝트 렌더링</li>
<li>투명 오브젝트 렌더링</li>
<li>깊이 복사가 이미 수행되어 생략됨</li>
</ol>
<p>요약하면, DBuffer 데칼을 활성화하면 렌더링 파이프라인 초반부(이벤트 200)에서 깊이 복사가 강제로 실행되어, &quot;After Transparents&quot; 설정(이벤트 500)이 의도한 대로 작동하지 않게 됩니다. 이는 데칼이 깊이 정보를 미리 필요로 하기 때문에 발생하는 근본적인 타이밍 충돌입니다.</p>
<hr>
<h2 id="타이밍-충돌-우회-custom-render-feature">타이밍 충돌 우회 (Custom Render Feature)</h2>
<p> DBuffer 데칼의 조기 깊이 복사를 그대로 두고, 투명 오브젝트 렌더링 후 추가로 깊이를 다시 복사합니다.</p>
<p>DBuffer 데칼의 기능을 유지하면서 투명 오브젝트에 대한 정확한 깊이 정보를 제공합니다.</p>
<p>PC/콘솔 프로젝트, 고품질 그래픽이 요구되는 프로젝트일 경우 검토 할 가치가 있으며 추가 렌더패스로 인한 오버헤드 발생 할 우려가 있습니다.</p>
<p>Unity URP의 Decal과 Depth Texture 충돌 문제는 단일 정답이 없는 복잡한 이슈입니다. 프로젝트의 특성, 타겟 플랫폼, 개발 리소스를 종합적으로 고려하여 가장 적합한 솔루션을 선택하고, 지속적으로 모니터링하며 개선해 나가는 것이 최선의 접근 방법입니다.</p>
<hr>
<h2 id="해결-방법">해결 방법</h2>
<h3 id="방법-1-custom-render-feature-가장-권장">방법 1: Custom Render Feature (가장 권장)</h3>
<p><strong>Cyanilux의 CopyDepthFeature 사용</strong></p>
<p>커뮤니티에서 가장 신뢰받는 해결책은 AfterRenderingTransparents에서 수동으로 깊이를 복사하여 DBuffer 데칼의 조기 복사를 우회하는 방법입니다.</p>
<h3 id="완전한-구현-코드-unity-2022-호환">완전한 구현 코드 (Unity 2022+ 호환)</h3>
<pre><code class="language-csharp">using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Experimental.Rendering;
using UnityEngine.Rendering.Universal;
using UnityEngine.Rendering.Universal.Internal;

public class CopyDepthFeature : ScriptableRendererFeature 
{
    private class SetGlobalTexture : ScriptableRenderPass 
    {
        private RTHandle rt;

        public SetGlobalTexture(RenderPassEvent renderPassEvent) 
        {
            base.renderPassEvent = renderPassEvent;
        }

        public void Setup(RTHandle rt) 
        {
            this.rt = rt;
        }

        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) 
        {
            CommandBuffer cmd = CommandBufferPool.Get();
            cmd.SetGlobalTexture(&quot;_CameraDepthTexture&quot;, rt);
            context.ExecuteCommandBuffer(cmd);
            CommandBufferPool.Release(cmd);
        }
    }

    private class CopyDepthPass : ScriptableRenderPass 
    {
        private RTHandle source;
        private RTHandle destination;

        public CopyDepthPass(RenderPassEvent renderPassEvent) 
        {
            base.renderPassEvent = renderPassEvent;
        }

        public void Setup(RTHandle source, RTHandle destination) 
        {
            this.source = source;
            this.destination = destination;
        }

        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) 
        {
            CommandBuffer cmd = CommandBufferPool.Get(&quot;Copy Depth for Transparents&quot;);
            cmd.CopyTexture(source, destination);
            context.ExecuteCommandBuffer(cmd);
            CommandBufferPool.Release(cmd);
        }
    }

    [SerializeField] private RenderPassEvent renderPassEvent = RenderPassEvent.AfterRenderingTransparents;
    [SerializeField] private UniversalRendererData rendererData;

    private CopyDepthPass copyDepthPass;
    private SetGlobalTexture setGlobalTexturePass;
    private RTHandle depthTextureHandle;

    public override void Create() 
    {
        copyDepthPass = new CopyDepthPass(renderPassEvent);
        setGlobalTexturePass = new SetGlobalTexture(renderPassEvent + 1);

        RenderTextureDescriptor descriptor = new RenderTextureDescriptor(
            Screen.width, 
            Screen.height,
            RenderTextureFormat.Depth,
            32
        );

        RenderingUtils.ReAllocateIfNeeded(
            ref depthTextureHandle,
            descriptor,
            FilterMode.Point,
            TextureWrapMode.Clamp,
            name: &quot;_CameraDepthTexture&quot;
        );
    }

    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) 
    {
        if (rendererData == null) return;

        RTHandle cameraDepthTexture = renderer.cameraDepthTargetHandle;

        copyDepthPass.Setup(cameraDepthTexture, depthTextureHandle);
        setGlobalTexturePass.Setup(depthTextureHandle);

        renderer.EnqueuePass(copyDepthPass);
        renderer.EnqueuePass(setGlobalTexturePass);
    }

    protected override void Dispose(bool disposing) 
    {
        depthTextureHandle?.Release();
    }
}</code></pre>
<h3 id="설정-방법">설정 방법</h3>
<p>다음 단계를 순서대로 진행합니다.</p>
<p>첫째, 위 스크립트를 프로젝트에 추가합니다.</p>
<p>둘째, Universal Renderer Data 에셋을 엽니다.</p>
<p>셋째, Add Renderer Feature를 선택하여 CopyDepthFeature를 추가합니다.</p>
<p>넷째, Feature 리스트에서 Decal Feature 아래에 배치하는 것이 중요합니다.</p>
<p>다섯째, Event를 <code>AfterRenderingTransparents</code>로 설정합니다.</p>
<p>여섯째, Renderer Asset 필드에 현재 Renderer Data를 할당합니다.</p>
<h3 id="방법-2-screen-space-decal-기법-사용">방법 2: Screen Space Decal 기법 사용</h3>
<p>가장 간단한 해결책입니다.</p>
<h3 id="설정-방법-1">설정 방법</h3>
<p>첫째, Universal Renderer Data 에셋을 엽니다.</p>
<p>둘째, Decal Renderer Feature를 찾습니다.</p>
<p>셋째, Technique 설정을 DBuffer에서 Screen Space로 변경합니다.</p>
<h3 id="장점">장점</h3>
<p>설정 변경만으로 즉시 해결할 수 있습니다. DepthNormal prepass 요구사항이 제거되며, 이벤트 300 이후 실행되므로 충돌이 발생하지 않습니다.</p>
<h3 id="단점">단점</h3>
<p>노멀 블렌딩만 지원되며 albedo나 roughness 블렌딩이 불가능합니다. Deferred 렌더링과 Accurate G-buffer normals 조합에서는 작동하지 않습니다. 타일 기반 GPU(모바일)에서 prepass가 발생할 수 있습니다.</p>
<h3 id="방법-3-커스텀-screen-space-데칼-셰이더">방법 3: 커스텀 Screen Space 데칼 셰이더</h3>
<p>Decal Renderer Feature를 완전히 우회하는 방법입니다.</p>
<pre><code>// 간단한 Screen Space 데칼 셰이더 구조
Pass
{
    Name &quot;ScreenSpaceDecal&quot;
    Tags { &quot;Queue&quot; = &quot;Transparent&quot; }

    ZWrite Off
    Blend SrcAlpha OneMinusSrcAlpha

    HLSLPROGRAM
    #pragma vertex vert
    #pragma fragment frag

    #include &quot;Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl&quot;
    #include &quot;Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareDepthTexture.hlsl&quot;

    struct Varyings
    {
        float4 positionCS : SV_POSITION;
        float4 screenPos : TEXCOORD0;
        float3 ray : TEXCOORD1;
    };

    Varyings vert(Attributes input)
    {
        Varyings output;
        output.positionCS = TransformObjectToHClip([input.positionOS.xyz](http://input.positionOS.xyz));
        output.screenPos = ComputeScreenPos(output.positionCS);
        output.ray = mul(UNITY_MATRIX_I_V, float4([output.positionCS.xyz](http://output.positionCS.xyz), 1.0)).xyz;
        return output;
    }

    half4 frag(Varyings input) : SV_Target
    {
        float2 screenUV = input.screenPos.xy / input.screenPos.w;
        float depth = SampleSceneDepth(screenUV);
        float3 worldPos = ComputeWorldSpacePosition(screenUV, depth, UNITY_MATRIX_I_VP);

        // 데칼 박스 안에 있는지 확인
        float3 localPos = mul(unity_WorldToObject, float4(worldPos, 1.0)).xyz;
        clip(0.5 - abs(localPos));

        // 데칼 텍스처 샘플링
        float2 decalUV = localPos.xz + 0.5;
        half4 decalColor = SAMPLE_TEXTURE2D(_DecalTexture, sampler_DecalTexture, decalUV);

        return decalColor;
    }
    ENDHLSL
}</code></pre><h3 id="참고-리소스">참고 리소스</h3>
<p>ColinLeung-NiloCat의 UnityURPUnlitScreenSpaceDecalShader와 Daniel Ilett의 Shader Graph 튜토리얼을 참고할 수 있습니다.</p>
<h3 id="방법-4-rendering-path-변경">방법 4: Rendering Path 변경</h3>
<h3 id="deferred-rendering-사용">Deferred Rendering 사용</h3>
<p>Universal Renderer의 Rendering Path를 Deferred로 설정합니다. G-buffer의 일부로 깊이가 생성되므로 별도 복사가 불필요합니다.</p>
<h3 id="장점-1">장점</h3>
<p>깊이가 G-buffer의 일부로 &quot;무료&quot;로 제공됩니다. 다수의 라이트를 사용하는 씬에서 효율적입니다.</p>
<h3 id="단점-1">단점</h3>
<p>G-buffer 메모리 오버헤드가 발생하므로 모바일 플랫폼에서 주의가 필요합니다. Accurate G-buffer normals와 Screen Space 데칼 노멀 블렌딩을 함께 사용할 수 없습니다. 구형 모바일 디바이스에서는 지원되지 않습니다.</p>
<h3 id="방법-5-조건부-feature-활성화">방법 5: 조건부 Feature 활성화</h3>
<p>플랫폼별로 다른 전략을 사용하는 방법입니다.</p>
<pre><code class="language-csharp">public class ConditionalDecalManager : MonoBehaviour
{
    [SerializeField] private UniversalRendererData rendererData;

    void Start()
    {
        var decalFeature = rendererData.rendererFeatures
            .OfType&lt;DecalRendererFeature&gt;().FirstOrDefault();

        if (decalFeature != null)
        {
            // 모바일에서는 데칼 비활성화
            #if UNITY_ANDROID || UNITY_IOS
                decalFeature.SetActive(false);
            #else
                decalFeature.SetActive(true);
            #endif
        }
    }
}</code></pre>
<hr>
<h2 id="주의사항-및-함정">주의사항 및 함정</h2>
<h3 id="피해야-할-실수">피해야 할 실수</h3>
<h3 id="절대-하지-말아야-할-것">절대 하지 말아야 할 것</h3>
<p>첫째, 커스텀 깊이 복사 패스에서 <code>ConfigureInput(ScriptableRenderPassInput.Depth)</code>를 호출하면 안 됩니다. 이것은 조기 깊이 복사를 다시 유발합니다.</p>
<p>둘째, RTHandle 미해제는 메모리 누수를 발생시킵니다.</p>
<p>셋째, Feature 순서를 잘못 배치하면 문제가 발생합니다. 깊이 복사는 데칼 아래에 배치해야 합니다.</p>
<p>넷째, 플랫폼별 깊이 포맷 차이를 무시하면 안 됩니다.</p>
<p>다섯째, Unity 버전 업그레이드 시 커스텀 코드를 업데이트하지 않으면 문제가 발생할 수 있습니다.</p>
<h3 id="frame-debugger로-검증하기">Frame Debugger로 검증하기</h3>
<p>Window → Analysis → Frame Debugger를 열어 다음 이벤트들을 확인합니다.</p>
<p>&quot;Copy Depth&quot; 이벤트의 위치를 확인합니다.</p>
<p>&quot;DBuffer Decals&quot; 실행 타이밍을 확인합니다.</p>
<p>&quot;_CameraDepthTexture&quot; 업데이트 시점을 확인합니다.</p>
<h3 id="unity-이슈-현황">Unity 이슈 현황</h3>
<p><strong>Unity 6 (2024)</strong></p>
<p>미해결 상태입니다. Render Graph API 변경이 있었습니다.</p>
<hr>
<h2 id="프로젝트별-권장-솔루션">프로젝트별 권장 솔루션</h2>
<h3 id="모바일-게임">모바일 게임</h3>
<p>2025년 10월 입니다. 모바일 디퍼드 플러스 사용 검토 해 볼만 합니다. 고객사 프로젝트 릴리스 한 사례중에 모바일 디퍼드 사용 사례가 있습니다.</p>
<h3 id="pc콘솔-aaa-타이틀">PC/콘솔 AAA 타이틀</h3>
<p>Cyanilux의 CopyDepthFeature를 구현하는 것을 권장합니다.</p>
<p>DBuffer 데칼을 유지하면서 수동 깊이 복사를 수행합니다.</p>
<p>성능이 허용된다면 추가 렌더패스를 수용합니다.</p>
<h3 id="크로스플랫폼-타이틀">크로스플랫폼 타이틀</h3>
<p>Quality Settings별로 다른 전략을 사용합니다.</p>
<p>모바일에서는 커스텀 셰이더 또는 베이킹(동적 데칼이 아니라면)을 사용합니다.</p>
<p>PC에서는 DBuffer와 커스텀 깊이 복사를 사용합니다.</p>
<p>조건부 컴파일로 플랫폼별 코드를 관리합니다.</p>
<hr>
<h2 id="구현-단계별-가이드">구현 단계별 가이드</h2>
<h3 id="step-1-문제-재현-및-검증">Step 1: 문제 재현 및 검증</h3>
<p>Frame Debugger로 깊이 복사 타이밍을 확인합니다.</p>
<p>현재 사용 중인 데칼 기법을 파악합니다.</p>
<p>타겟 플랫폼을 확인합니다.</p>
<h3 id="step-2-가장-간단한-해결책-시도">Step 2: 가장 간단한 해결책 시도</h3>
<p>Screen Space 기법으로 전환하여 테스트합니다.</p>
<p>노멀만으로 충분하다면 해당 방법을 채택합니다.</p>
<h3 id="step-3-성능-측정">Step 3: 성능 측정</h3>
<p>타겟 플랫폼에서 프로파일링을 수행합니다.</p>
<p>Depth texture와 데칼 비용을 정량화합니다.</p>
<h3 id="step-4-필요시-커스텀-구현">Step 4: 필요시 커스텀 구현</h3>
<p>CopyDepthFeature를 Decal Feature 아래에 추가합니다.</p>
<p>AfterRenderingTransparents로 이벤트를 설정합니다.</p>
<p>RTHandle 관리를 확인합니다. Dispose에서 Release를 호출해야 합니다.</p>
<h3 id="step-5-검증-및-최적화">Step 5: 검증 및 최적화</h3>
<p>투명 오브젝트 깊이 캡처를 확인합니다.</p>
<p>데칼이 투명 오브젝트에 정상적으로 투영되는지 테스트합니다.</p>
<p>Frame Debugger로 최종 확인을 진행합니다.</p>
<hr>
<h2 id="실전-팁">실전 팁</h2>
<h3 id="성능-최적화">성능 최적화</h3>
<p>CommandBuffer.CopyTexture를 사용하면 Blit보다 빠릅니다.</p>
<pre><code class="language-csharp">// GPU-GPU 직접 복사 (권장)
cmd.CopyTexture(sourceDepth, destDepth);

// Blit 사용 (호환성이 필요한 경우만)
// Blitter.BlitCameraTexture(cmd, sourceDepth, destDepth, material, 0);</code></pre>
<h3 id="디버그-시각화">디버그 시각화</h3>
<p>_CameraDepthTexture를 시각화하는 셰이더입니다.</p>
<pre><code class="language-csharp">half4 frag(Varyings input) : SV_Target
{
    float depth = SampleSceneDepth(input.uv);
    return half4(depth, depth, depth, 1.0);
}</code></pre>
<h3 id="투명-오브젝트에-depthonly-패스-추가">투명 오브젝트에 DepthOnly 패스 추가</h3>
<pre><code>Pass
{
    Name &quot;DepthOnly&quot;
    Tags { &quot;LightMode&quot; = &quot;DepthOnly&quot; }

    ZWrite On
    ColorMask 0  // 깊이만 쓰기

    HLSLPROGRAM
    #pragma vertex DepthOnlyVertex
    #pragma fragment DepthOnlyFragment

    // 알파 클리핑 지원
    #ifdef _ALPHATEST_ON
        clip(alpha - _Cutoff);
    #endif
    ENDHLSL
}</code></pre><hr>
<h2 id="참고-자료">참고 자료</h2>
<p>Cyanilux - Custom Renderer Features</p>
<p>Unity Graphics GitHub - DBufferRenderPass.cs</p>
<p>Unity Issue Tracker - Decal Layer Texture lifetime</p>
<p>Daniel Ilett - Decals and Stickers Tutorial</p>
<p>ColinLeung-NiloCat - Screen Space Decal Shader</p>
<hr>
<h2 id="결론">결론</h2>
<p>Unity URP의 Decal과 After Transparents 충돌은 아키텍처 수준의 설계 제약입니다. Unity가 공식적으로 해결하지 못한 이 문제는 커뮤니티의 창의적인 우회 방법들로 극복 가능합니다.</p>
<p><strong>핵심 메시지</strong></p>
<p>완벽한 해결책은 없지만, 프로젝트에 맞는 우회 방법은 존재합니다.</p>
<p>성능과 품질의 트레이드오프를 이해하고 선택해야 합니다.</p>
<p>커뮤니티 솔루션들은 프로덕션에서 검증되었습니다.</p>
<p>가장 중요한 것은 프로젝트의 구체적 요구사항과 타겟 플랫폼에 맞는 전략을 선택하는 것입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[URP에서 HDRP로 셰이더 포팅 가이드]]></title>
            <link>https://velog.io/@mazeline_1973/URP%EC%97%90%EC%84%9C-HDRP%EB%A1%9C-%EC%85%B0%EC%9D%B4%EB%8D%94-%ED%8F%AC%ED%8C%85-%EA%B0%80%EC%9D%B4%EB%93%9C</link>
            <guid>https://velog.io/@mazeline_1973/URP%EC%97%90%EC%84%9C-HDRP%EB%A1%9C-%EC%85%B0%EC%9D%B4%EB%8D%94-%ED%8F%AC%ED%8C%85-%EA%B0%80%EC%9D%B4%EB%93%9C</guid>
            <pubDate>Tue, 21 Oct 2025 14:45:00 GMT</pubDate>
            <description><![CDATA[<p>URP 에서만 동작하는 셰이더를 HDRP 로 전환 해야할 일이 생겼습니다. 몇 가지 부분을 기록하고자 합니다. 50살을 넘어간 후부터는 기억력이 정말 하루 하루 …</p>
<h2 id="요약">요약</h2>
<p>URP에서 HDRP로 셰이더를 포팅하는 작업은 단순한 문법 수정이 아닌 근본적인 아키텍처 변경을 필요로 합니다. 가장 중요한 차이점은 깊이 텍스처 접근 방식(LoadCameraDepth vs SAMPLE_DEPTH_TEXTURE), include 파일 경로, 셰이더 패스 구조, 그리고 매크로 정의입니다. 이 가이드에서는 각 셰이더 타입별로 작동하는 예제와 함께 체계적인 마이그레이션 패턴을 제공합니다.</p>
<h2 id="핵심-아키텍처-차이점">핵심 아키텍처 차이점</h2>
<h3 id="렌더링-파이프라인-철학">렌더링 파이프라인 철학</h3>
<p><strong>URP</strong>는 모바일 및 중급 사양에 최적화된 렌더러입니다. 반면 <strong>HDRP</strong>는 하이엔드 플랫폼을 위한 렌더러이며 기본적으로 레이트레이싱을 지원하고 있습니다.</p>
<h3 id="깊이-버퍼-시스템">깊이 버퍼 시스템</h3>
<p><strong>URP</strong>는 <code>_CameraDepthTexture</code>를 사용한 직접 텍스처 샘플링 방식을 사용합니다. 이와 달리 <strong>HDRP</strong>는 <code>LoadCameraDepth()</code> 또는 <code>SampleCameraDepth()</code> 함수를 필요로 하는 계층적 깊이 피라미드 구조를 채택하고 있습니다. HDRP에서는 깊이 텍스처를 절대 직접 샘플링하지 마세요.</p>
<h2 id="include-파일-마이그레이션-맵">Include 파일 마이그레이션 맵</h2>
<pre><code class="language-jsx">// URP Include 구조
#include &quot;Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl&quot;
#include &quot;Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl&quot;
#include &quot;Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareDepthTexture.hlsl&quot;

// HDRP 대응 구조
#include &quot;Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl&quot;
#include &quot;Packages/com.unity.render-pipelines.high-definition/Runtime/ShaderLibrary/ShaderVariables.hlsl&quot;
#include &quot;Packages/com.unity.render-pipelines.high-definition/Runtime/Material/Material.hlsl&quot;</code></pre>
<h3 id="전체-include-매핑-테이블">전체 Include 매핑 테이블</h3>
<table>
<thead>
<tr>
<th>URP Include</th>
<th>HDRP 대응</th>
<th>목적</th>
</tr>
</thead>
<tbody><tr>
<td>Core.hlsl</td>
<td>Common.hlsl + ShaderVariables.hlsl</td>
<td>기본 타입 및 행렬</td>
</tr>
<tr>
<td>Lighting.hlsl</td>
<td>Lighting.hlsl (HDRP 버전)</td>
<td>라이팅 계산</td>
</tr>
<tr>
<td>DeclareDepthTexture.hlsl</td>
<td>ShaderVariables.hlsl</td>
<td>깊이 텍스처 접근</td>
</tr>
<tr>
<td>DeclareNormalsTexture.hlsl</td>
<td>NormalBuffer.hlsl</td>
<td>노말 버퍼 접근</td>
</tr>
<tr>
<td>ShaderVariablesFunctions.hlsl</td>
<td>ShaderVariablesFunctions.hlsl (HDRP)</td>
<td>헬퍼 함수</td>
</tr>
<tr>
<td>Shadows.hlsl</td>
<td>LightLoop/Shadow.hlsl</td>
<td>그림자 샘플링</td>
</tr>
<tr>
<td>BRDF.hlsl</td>
<td>BSDF.hlsl</td>
<td>머티리얼 응답 함수</td>
</tr>
</tbody></table>
<h2 id="깊이-텍스처-접근-마이그레이션">깊이 텍스처 접근 마이그레이션</h2>
<h3 id="urp-깊이-패턴">URP 깊이 패턴</h3>
<pre><code class="language-jsx">// URP 깊이 접근
TEXTURE2D_X(_CameraDepthTexture);
SAMPLER(sampler_CameraDepthTexture);

float GetDepth(float2 uv)
{
    float rawDepth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, uv);
    float eyeDepth = LinearEyeDepth(rawDepth, _ZBufferParams);
    return eyeDepth;
}</code></pre>
<h3 id="hdrp-마이그레이션">HDRP 마이그레이션</h3>
<pre><code class="language-jsx">// HDRP 깊이 접근 - 텍스처 선언 불필요
float GetDepth(float2 uv)
{
    uint2 positionSS = uv * _ScreenSize.xy;
    float rawDepth = LoadCameraDepth(positionSS);
    float eyeDepth = LinearEyeDepth(rawDepth, _ZBufferParams);
    return eyeDepth;
}</code></pre>
<h3 id="주요-차이점">주요 차이점</h3>
<p>HDRP에서는 명시적인 텍스처 선언이 불필요합니다. 픽셀 좌표에는 <code>LoadCameraDepth()</code>를, UV에는 <code>SampleCameraDepth()</code>를 사용합니다. 변환 함수에 <code>_ZBufferParams</code>를 명시적으로 전달해야 하며, HDRP는 단일 텍스처가 아닌 깊이 피라미드를 사용한다는 점을 기억하세요.</p>
<h2 id="매크로-및-함수-마이그레이션">매크로 및 함수 마이그레이션</h2>
<h3 id="텍스처-선언">텍스처 선언</h3>
<pre><code class="language-jsx">// URP
TEXTURE2D(_MainTex);
SAMPLER(sampler_MainTex);

// HDRP
TEXTURE2D(_MainTex);
SAMPLER(sampler_MainTex);  // 일반 텍스처는 동일</code></pre>
<h3 id="텍스처-샘플링">텍스처 샘플링</h3>
<pre><code class="language-jsx">// URP
float4 color = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv);

// HDRP
float4 color = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv);  // 일반 텍스처는 동일
// 하지만 스크린 텍스처의 경우:
float4 color = LOAD_TEXTURE2D_X(_InputTexture, positionSS);  // 스테레오를 위한 _X 주목</code></pre>
<h3 id="변환-함수">변환 함수</h3>
<pre><code class="language-jsx">// URP
float3 worldPos = TransformObjectToWorld(objectPos);
float4 clipPos = TransformWorldToHClip(worldPos);
float3 viewPos = TransformWorldToView(worldPos);

// HDRP - 동일한 이름, 다른 include
float3 worldPos = TransformObjectToWorld(objectPos);
float4 clipPos = TransformWorldToHClip(worldPos);
float3 viewPos = TransformWorldToView(worldPos);</code></pre>
<h3 id="깊이-변환-함수">깊이 변환 함수</h3>
<pre><code class="language-jsx">// URP
float linear01 = Linear01Depth(rawDepth, _ZBufferParams);
float linearEye = LinearEyeDepth(rawDepth, _ZBufferParams);

// HDRP - 시그니처 완전히 동일
float linear01 = Linear01Depth(rawDepth, _ZBufferParams);
float linearEye = LinearEyeDepth(rawDepth, _ZBufferParams);</code></pre>
<h2 id="완전한-셰이더-마이그레이션-예제">완전한 셰이더 마이그레이션 예제</h2>
<h3 id="원본-urp-셰이더">원본 URP 셰이더</h3>
<pre><code class="language-jsx">Shader &quot;URP/DepthFog&quot;
{
    Properties
    {
        _FogColor (&quot;Fog Color&quot;, Color) = (0.5, 0.5, 0.5, 1)
        _FogDensity (&quot;Fog Density&quot;, Range(0, 1)) = 0.5
        _FogStart (&quot;Fog Start&quot;, Float) = 10
        _FogEnd (&quot;Fog End&quot;, Float) = 50
    }

    SubShader
    {
        Tags { &quot;RenderPipeline&quot;=&quot;UniversalPipeline&quot; &quot;Queue&quot;=&quot;Transparent&quot; }

        Pass
        {
            Name &quot;DepthFog&quot;

            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include &quot;Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl&quot;
            #include &quot;Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareDepthTexture.hlsl&quot;

            struct Attributes
            {
                float4 positionOS : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct Varyings
            {
                float4 positionCS : SV_POSITION;
                float2 uv : TEXCOORD0;
                float4 screenPos : TEXCOORD1;
            };

            CBUFFER_START(UnityPerMaterial)
                float4 _FogColor;
                float _FogDensity;
                float _FogStart;
                float _FogEnd;
            CBUFFER_END

            TEXTURE2D(_MainTex);
            SAMPLER(sampler_MainTex);

            Varyings vert(Attributes input)
            {
                Varyings output;
                output.positionCS = TransformObjectToHClip([input.positionOS.xyz](http://input.positionOS.xyz));
                output.uv = input.uv;
                output.screenPos = ComputeScreenPos(output.positionCS);
                return output;
            }

            float4 frag(Varyings input) : SV_Target
            {
                float2 screenUV = input.screenPos.xy / input.screenPos.w;
                float rawDepth = SampleSceneDepth(screenUV);
                float eyeDepth = LinearEyeDepth(rawDepth, _ZBufferParams);

                float fogFactor = saturate((eyeDepth - _FogStart) / (_FogEnd - _FogStart));
                fogFactor = pow(fogFactor, _FogDensity);

                float4 color = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv);
                color.rgb = lerp(color.rgb, _FogColor.rgb, fogFactor);

                return color;
            }
            ENDHLSL
        }
    }
}</code></pre>
<h3 id="마이그레이션된-hdrp-셰이더">마이그레이션된 HDRP 셰이더</h3>
<pre><code class="language-jsx">Shader &quot;HDRP/DepthFog&quot;
{
    Properties
    {
        _FogColor (&quot;Fog Color&quot;, Color) = (0.5, 0.5, 0.5, 1)
        _FogDensity (&quot;Fog Density&quot;, Range(0, 1)) = 0.5
        _FogStart (&quot;Fog Start&quot;, Float) = 10
        _FogEnd (&quot;Fog End&quot;, Float) = 50
    }

    HLSLINCLUDE
    #pragma target 4.5
    #pragma only_renderers d3d11 playstation xboxone xboxseries vulkan metal switch

    #include &quot;Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl&quot;
    #include &quot;Packages/com.unity.render-pipelines.high-definition/Runtime/ShaderLibrary/ShaderVariables.hlsl&quot;
    #include &quot;Packages/com.unity.render-pipelines.high-definition/Runtime/RenderPipeline/ShaderPass/ShaderPass.cs.hlsl&quot;

    struct Attributes
    {
        float4 positionOS : POSITION;
        float2 uv : TEXCOORD0;
        UNITY_VERTEX_INPUT_INSTANCE_ID
    };

    struct Varyings
    {
        float4 positionCS : SV_POSITION;
        float2 uv : TEXCOORD0;
        float4 screenPos : TEXCOORD1;
        UNITY_VERTEX_OUTPUT_STEREO
    };

    float4 _FogColor;
    float _FogDensity;
    float _FogStart;
    float _FogEnd;

    TEXTURE2D(_MainTex);
    SAMPLER(sampler_MainTex);
    ENDHLSL

    SubShader
    {
        Tags { &quot;RenderPipeline&quot;=&quot;HDRenderPipeline&quot; &quot;Queue&quot;=&quot;Transparent&quot; }

        Pass
        {
            Name &quot;ForwardOnly&quot;
            Tags { &quot;LightMode&quot; = &quot;ForwardOnly&quot; }

            Blend SrcAlpha OneMinusSrcAlpha
            ZWrite Off

            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            Varyings vert(Attributes input)
            {
                Varyings output;
                UNITY_SETUP_INSTANCE_ID(input);
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);

                output.positionCS = TransformObjectToHClip([input.positionOS.xyz](http://input.positionOS.xyz));
                output.uv = input.uv;
                output.screenPos = ComputeScreenPos(output.positionCS);

                return output;
            }

            float4 frag(Varyings input) : SV_Target
            {
                UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);

                // HDRP용 UV를 픽셀 좌표로 변환
                float2 screenUV = input.screenPos.xy / input.screenPos.w;
                uint2 positionSS = screenUV * _ScreenSize.xy;

                // HDRP 깊이 로딩 함수 사용
                float rawDepth = LoadCameraDepth(positionSS);
                float eyeDepth = LinearEyeDepth(rawDepth, _ZBufferParams);

                float fogFactor = saturate((eyeDepth - _FogStart) / (_FogEnd - _FogStart));
                fogFactor = pow(fogFactor, _FogDensity);

                float4 color = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv);
                color.rgb = lerp(color.rgb, _FogColor.rgb, fogFactor);

                return color;
            }
            ENDHLSL
        }

        // HDRP의 깊이 프리패스를 위해 필요
        Pass
        {
            Name &quot;DepthForwardOnly&quot;
            Tags { &quot;LightMode&quot; = &quot;DepthForwardOnly&quot; }

            ColorMask 0
            ZWrite On

            HLSLPROGRAM
            #pragma vertex VertDepth
            #pragma fragment FragDepth

            Varyings VertDepth(Attributes input)
            {
                Varyings output;
                UNITY_SETUP_INSTANCE_ID(input);
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);
                output.positionCS = TransformObjectToHClip([input.positionOS.xyz](http://input.positionOS.xyz));
                return output;
            }

            void FragDepth(Varyings input)
            {
                UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
            }
            ENDHLSL
        }
    }
}</code></pre>
<h2 id="포스트-프로세싱-마이그레이션">포스트 프로세싱 마이그레이션</h2>
<h3 id="urp-포스트-프로세싱-v2v3">URP 포스트 프로세싱 V2/V3</h3>
<pre><code class="language-jsx">using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

[VolumeComponentMenu(&quot;Custom/URPEffect&quot;)]
public class URPPostEffect : VolumeComponent, IPostProcessComponent
{
    public ClampedFloatParameter intensity = new ClampedFloatParameter(0f, 0f, 1f);

    public bool IsActive() =&gt; intensity.value &gt; 0f;
    public bool IsTileCompatible() =&gt; false;
}

// Render Feature 구현
public class URPPostProcessRenderFeature : ScriptableRendererFeature
{
    class CustomRenderPass : ScriptableRenderPass
    {
        public override void Execute(ScriptableRenderContext context,
                                    ref RenderingData renderingData)
        {
            // 렌더링 로직
        }
    }
}</code></pre>
<h3 id="hdrp-커스텀-포스트-프로세싱">HDRP 커스텀 포스트 프로세싱</h3>
<pre><code class="language-jsx">using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.HighDefinition;
using System;

[Serializable, VolumeComponentMenu(&quot;Post-processing/Custom/HDRPEffect&quot;)]
public sealed class HDRPPostEffect : CustomPostProcessVolumeComponent, IPostProcessComponent
{
    public ClampedFloatParameter intensity = new ClampedFloatParameter(0f, 0f, 1f);

    Material m_Material;

    public bool IsActive() =&gt; m_Material != null &amp;&amp; intensity.value &gt; 0f;

    // 인젝션 포인트는 이펙트가 실행되는 시점을 결정합니다
    public override CustomPostProcessInjectionPoint injectionPoint =&gt;
        CustomPostProcessInjectionPoint.AfterPostProcess;

    public override void Setup()
    {
        if (Shader.Find(&quot;Hidden/HDRP/PostEffect&quot;) != null)
            m_Material = new Material(Shader.Find(&quot;Hidden/HDRP/PostEffect&quot;));
    }

    public override void Render(CommandBuffer cmd, HDCamera camera,
                                RTHandle source, RTHandle destination)
    {
        if (m_Material == null) return;

        m_Material.SetFloat(&quot;_Intensity&quot;, intensity.value);
        m_Material.SetTexture(&quot;_InputTexture&quot;, source);
        HDUtils.DrawFullScreen(cmd, m_Material, destination);
    }

    public override void Cleanup() =&gt; CoreUtils.Destroy(m_Material);
}</code></pre>
<h2 id="커스텀-패스-마이그레이션">커스텀 패스 마이그레이션</h2>
<h3 id="urp-render-feature">URP Render Feature</h3>
<pre><code class="language-jsx">public class URPCustomPass : ScriptableRendererFeature
{
    class Pass : ScriptableRenderPass
    {
        public override void Execute(ScriptableRenderContext context,
                                    ref RenderingData renderingData)
        {
            CommandBuffer cmd = CommandBufferPool.Get(&quot;URPCustomPass&quot;);

            // 카메라 텍스처 가져오기
            RenderTargetIdentifier source = renderingData.cameraData.renderer.cameraColorTarget;
            RenderTargetIdentifier depth = renderingData.cameraData.renderer.cameraDepthTarget;

            // 패스 실행
            cmd.Blit(source, destination, material);

            context.ExecuteCommandBuffer(cmd);
            CommandBufferPool.Release(cmd);
        }
    }

    public override void AddRenderPasses(ScriptableRenderer renderer,
                                         ref RenderingData renderingData)
    {
        renderer.EnqueuePass(customPass);
    }
}</code></pre>
<h3 id="hdrp-커스텀-패스">HDRP 커스텀 패스</h3>
<pre><code class="language-jsx">public class HDRPCustomPass : CustomPass
{
    public LayerMask layerMask = ~0;
    public Material overrideMaterial;

    protected override void Setup(ScriptableRenderContext renderContext, CommandBuffer cmd)
    {
        // 일회성 설정
    }

    protected override void Execute(CustomPassContext ctx)
    {
        // HDRP는 사전 구성된 컨텍스트를 제공합니다
        if (overrideMaterial == null) return;

        // 오버라이드 머티리얼로 렌더러 그리기
        CoreUtils.SetRenderTarget(ctx.cmd, ctx.cameraColorBuffer, ctx.cameraDepthBuffer);
        CustomPassUtils.DrawRenderers(ctx, layerMask, overrideMaterial: overrideMaterial);

        // 또는 풀스크린 패스
        CoreUtils.DrawFullScreen(ctx.cmd, overrideMaterial, ctx.cameraColorBuffer);
    }
}</code></pre>
<h2 id="라이팅-및-머티리얼-함수">라이팅 및 머티리얼 함수</h2>
<h3 id="urp-라이팅">URP 라이팅</h3>
<pre><code class="language-jsx">// URP 메인 라이트
Light GetMainLight()
{
    Light light;
    light.direction = _[MainLightPosition.xyz](http://MainLightPosition.xyz);
    light.color = _MainLightColor.rgb;
    light.shadowAttenuation = MainLightRealtimeShadow(shadowCoord);
    return light;
}

// URP 추가 라이트
uint pixelLightCount = GetAdditionalLightsCount();
for (uint lightIndex = 0; lightIndex &lt; pixelLightCount; ++lightIndex)
{
    Light light = GetAdditionalLight(lightIndex, worldPos);
    // 라이트 처리
}</code></pre>
<h3 id="hdrp-라이팅">HDRP 라이팅</h3>
<pre><code class="language-jsx">// HDRP는 LightLoop include와 다른 구조가 필요합니다
#include &quot;Packages/com.unity.render-pipelines.high-definition/Runtime/Lighting/LightLoop/LightLoopDef.hlsl&quot;

// HDRP 디렉셔널 라이트 (단순화)
DirectionalLightData light = _DirectionalLightDatas[0];
float3 lightDirection = -light.forward;
float3 lightColor = light.color;

// HDRP는 주로 디퍼드 라이팅을 사용합니다
// 직접 라이팅 접근은 특정 패스와 BSDFData를 필요로 합니다</code></pre>
<h2 id="서피스-셰이더-마이그레이션">서피스 셰이더 마이그레이션</h2>
<h3 id="urp-lit-shader-graph-프로퍼티">URP Lit Shader Graph 프로퍼티</h3>
<p>URP Lit Shader Graph는 Base Map, Normal Map, Metallic/Smoothness, Occlusion, Emission 프로퍼티를 제공합니다.</p>
<h3 id="hdrp-lit-shader-graph-프로퍼티">HDRP Lit Shader Graph 프로퍼티</h3>
<p>HDRP Lit Shader Graph는 Base Color Map(Base Map이 아님), Normal Map(Object 또는 Tangent Space), Mask Map(RGBA: Metallic, Occlusion, Detail, Smoothness), Detail Map, Emission을 제공합니다. 추가로 Coat Mask, Thickness, Subsurface 등의 고급 프로퍼티도 사용할 수 있습니다.</p>
<h3 id="주요-차이점-1">주요 차이점</h3>
<p>HDRP는 별도 텍스처 대신 패킹된 Mask Map을 사용합니다. 또한 서브서피스 스캐터링, 투과(transmission), 코팅(coat) 등 더 많은 머티리얼 기능을 제공합니다. 텍스처 패킹 규칙도 URP와 다르므로 주의가 필요합니다.</p>
<h2 id="일반적인-마이그레이션-이슈-및-해결책">일반적인 마이그레이션 이슈 및 해결책</h2>
<h2 id="중요-주의사항">중요 주의사항</h2>
<h3 id="screen-space-좌표-변환-주의">Screen Space 좌표 변환 주의</h3>
<p>HDRP에서 가장 흔한 실수 중 하나는 Screen Space UV와 픽셀 좌표를 혼동하는 것입니다. <code>LoadCameraDepth()</code>는 픽셀 좌표(uint2)를 받지만, <code>SampleCameraDepth()</code>는 UV 좌표(float2)를 받습니다. 잘못된 좌표계를 사용하면 깊이 값이 완전히 잘못 읽히므로 반드시 확인하세요.</p>
<pre><code class="language-jsx">// 잘못된 예 - UV를 Load에 전달
float depth = LoadCameraDepth(uv); // 컴파일 에러 또는 잘못된 값

// 올바른 예
uint2 pixelCoord = uv * _ScreenSize.xy;
float depth = LoadCameraDepth(pixelCoord);

// 또는 SampleCameraDepth 사용
float depth = SampleCameraDepth(uv);</code></pre>
<h3 id="cbuffer와-전역-변수-선언">CBUFFER와 전역 변수 선언</h3>
<p>HDRP에서는 CBUFFER_START/END를 사용하지 않고 전역으로 머티리얼 프로퍼티를 선언하는 경우가 많습니다. HLSLINCLUDE 블록에서 선언하면 모든 패스에서 공유할 수 있습니다. SRP Batcher 호환성을 위해서는 UnityPerMaterial CBUFFER를 사용하는 것이 좋지만, 커스텀 셰이더에서는 전역 선언도 가능합니다.</p>
<pre><code class="language-jsx">// HDRP 스타일 - HLSLINCLUDE에서 전역 선언
HLSLINCLUDE
float4 _Color;
float _Intensity;
ENDHLSL

// 또는 SRP Batcher 호환
HLSLINCLUDE
CBUFFER_START(UnityPerMaterial)
    float4 _Color;
    float _Intensity;
CBUFFER_END
ENDHLSL</code></pre>
<h3 id="alpha-clipping-구현">Alpha Clipping 구현</h3>
<p>HDRP에서 Alpha Clipping을 사용하는 경우, 모든 패스에서 동일하게 클리핑을 적용해야 합니다. 특히 DepthForwardOnly와 ShadowCaster 패스에서 클리핑을 누락하면 그림자와 깊이가 불일치하여 시각적 아티팩트가 발생합니다. 이 부분은 URP 도 동일하게 주의 해야 합니다.</p>
<pre><code class="language-jsx">// Fragment 함수에서 클리핑
void frag(Varyings input)
{
    float alpha = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv).a;
    clip(alpha - _Cutoff); // 모든 패스에 필수
}</code></pre>
<h3 id="normal-buffer-인코딩-차이">Normal Buffer 인코딩 차이</h3>
<p>HDRP는 URP와 다른 Normal 인코딩 방식을 사용합니다. Normal Buffer를 직접 읽는 경우, <code>NormalData.hlsl</code>의 <code>DecodeFromNormalBuffer()</code> 함수를 사용해야 합니다. 직접 디코딩하면 잘못된 결과가 나올 수 있습니다.</p>
<pre><code class="language-jsx">#include &quot;Packages/com.unity.render-pipelines.high-definition/Runtime/Material/NormalBuffer.hlsl&quot;

// 올바른 Normal 읽기
NormalData normalData;
DecodeFromNormalBuffer(positionSS, normalData);
float3 worldNormal = normalData.normalWS;</code></pre>
<h3 id="rthandle-vs-rendertexture">RTHandle vs RenderTexture</h3>
<p>HDRP는 동적 해상도 스케일링을 지원하기 위해 RTHandle 시스템을 사용합니다. 커스텀 패스나 포스트 프로세싱에서 일반 RenderTexture 대신 RTHandle을 사용해야 합니다. RenderTexture를 직접 사용하면 동적 해상도 변경 시 문제가 발생할 수 있습니다.</p>
<pre><code class="language-jsx">// 잘못된 예
RenderTexture rt = new RenderTexture(1920, 1080, 0);

// 올바른 예
RTHandle rt = RTHandles.Alloc([Vector2.one](http://Vector2.one), filterMode: FilterMode.Bilinear);</code></pre>
<h3 id="셰이더-키워드-관리">셰이더 키워드 관리</h3>
<p>HDRP는 매우 많은 내부 키워드를 사용합니다. 커스텀 셰이더에서 multi_compile을 추가할 때는 신중해야 합니다. 불필요한 키워드는 베리언트 폭발을 일으켜 빌드 시간과 메모리를 크게 증가시킵니다. <code>shader_feature_local</code>을 사용하여 머티리얼별로만 활성화되는 키워드로 제한하세요.</p>
<pre><code class="language-jsx">// 베리언트 폭발 위험
#pragma multi_compile _ FEATURE_A FEATURE_B FEATURE_C

// 더 안전한 방법
#pragma shader_feature_local _FEATURE_A
#pragma shader_feature_local _FEATURE_B</code></pre>
<h3 id="color-space-변환">Color Space 변환</h3>
<p>HDRP는 항상 Linear Color Space에서 작동합니다. 텍스처를 sRGB로 임포트했는지 확인하고, 셰이더에서 색상 계산 시 Gamma 변환이 필요한지 검토하세요. 특히 UI 텍스처나 라이트맵을 다룰 때 주의가 필요합니다.</p>
<h3 id="shader-graph-마이그레이션-함정">Shader Graph 마이그레이션 함정</h3>
<p>Shader Graph로 작성된 URP 셰이더는 HDRP로 단순히 변환할 수 없습니다. Target을 HDRP로 변경하면 많은 노드가 작동하지 않거나 다르게 동작합니다. 특히 Scene Depth, Scene Color 노드는 완전히 다시 설정해야 합니다. 가능하면 코드 셰이더로 먼저 포팅한 후 Shader Graph로 재작성하는 것을 권장합니다.</p>
<h3 id="패스-순서와-렌더-큐">패스 순서와 렌더 큐</h3>
<p>HDRP는 엄격한 렌더 큐 시스템을 사용합니다. Transparent 오브젝트는 반드시 &quot;Transparent&quot; 큐를 사용해야 하며, Opaque와 혼용하면 렌더링 순서가 꼬일 수 있습니다. 커스텀 패스의 인젝션 포인트도 신중하게 선택해야 합니다.</p>
<pre><code class="language-jsx">// Transparent 셰이더는 반드시 올바른 큐 사용
Tags { &quot;RenderPipeline&quot;=&quot;HDRenderPipeline&quot; &quot;Queue&quot;=&quot;Transparent&quot; &quot;RenderType&quot;=&quot;Transparent&quot; }</code></pre>
<h3 id="디버깅-팁">디버깅 팁</h3>
<p>HDRP 셰이더 디버깅 시 Rendering Debugger(Window &gt; Analysis &gt; Rendering Debugger)를 적극 활용하세요. Material, Lighting, Rendering 탭에서 깊이, 노말, 모션 벡터 등을 시각화할 수 있습니다. 또한 Frame Debugger로 각 패스의 실행을 확인하세요.</p>
<h3 id="프로젝트-설정-확인">프로젝트 설정 확인</h3>
<p>HDRP Asset 설정에서 Depth Pyramid, Normal Buffer, Motion Vectors가 활성화되어 있는지 확인하세요. 이들이 비활성화되어 있으면 해당 기능을 사용하는 셰이더가 제대로 작동하지 않습니다. 또한 Quality Level별로 설정이 다를 수 있으므로 모든 Quality Level을 확인하세요.</p>
<h3 id="버전-호환성">버전 호환성</h3>
<p>HDRP는 Unity 버전에 따라 API가 자주 변경됩니다. 특정 버전용으로 작성된 셰이더는 다른 버전에서 컴파일 에러가 발생할 수 있습니다. 버전 전환 시에는 HDRP 패키지 문서를 반드시 확인하고, 필요하면 <code>#if UNITY_VERSION</code> 매크로로 버전별 코드를 분기하세요.</p>
<pre><code class="language-jsx">#if UNITY_VERSION &gt;= 202200 // Unity 2022 이상
    // 새 API 사용
#else
    // 구 API 사용
#endif</code></pre>
<h2 id="일반적인-마이그레이션-이슈-및-해결책-1">일반적인 마이그레이션 이슈 및 해결책</h2>
<h3 id="이슈-1-셰이더가-컴파일되지-않음">이슈 1: 셰이더가 컴파일되지 않음</h3>
<p><strong>증상</strong>: &quot;Cannot find include file&quot; 에러가 발생합니다.</p>
<p><strong>해결책</strong>: 모든 include 경로를 HDRP 대응 경로로 업데이트하세요.</p>
<pre><code class="language-jsx">// 잘못된 방법
#include &quot;Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl&quot;

// 올바른 방법
#include &quot;Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl&quot;
#include &quot;Packages/com.unity.render-pipelines.high-definition/Runtime/ShaderLibrary/ShaderVariables.hlsl&quot;</code></pre>
<h3 id="이슈-2-깊이가-잘못된-값을-반환함">이슈 2: 깊이가 잘못된 값을 반환함</h3>
<p><strong>증상</strong>: 깊이 기반 이펙트에서 아티팩트나 잘못된 거리가 표시됩니다.</p>
<p><strong>해결책</strong>: HDRP 깊이 함수를 사용하세요.</p>
<pre><code class="language-jsx">// 잘못된 방법 - URP 패턴
float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, uv);

// 올바른 방법 - HDRP 패턴
float depth = LoadCameraDepth(positionSS);</code></pre>
<h3 id="이슈-3-오브젝트가-씬에서-보이지-않음">이슈 3: 오브젝트가 씬에서 보이지 않음</h3>
<p><strong>증상</strong>: 셰이더가 검은색으로 렌더링되거나 보이지 않습니다.</p>
<p><strong>해결책</strong>: 필요한 HDRP 패스를 추가하세요. 깊이 프리패스를 위한 DepthForwardOnly 패스, 그림자를 위한 ShadowCaster 패스, 라이트맵 베이킹을 위한 META 패스를 추가해야 합니다.</p>
<h3 id="이슈-4-포스트-프로세싱이-작동하지-않음">이슈 4: 포스트 프로세싱이 작동하지 않음</h3>
<p><strong>증상</strong>: 커스텀 포스트 프로세스가 나타나지 않습니다.</p>
<p><strong>해결책</strong>: 먼저 셰이더를 Resources 폴더에 배치하세요. 그런 다음 HDRP Settings의 Custom Post Process Orders에 추가합니다. 마지막으로 Volume에 컴포넌트가 활성화되어 있는지 확인하세요.</p>
<h3 id="이슈-5-머티리얼-외형이-다름">이슈 5: 머티리얼 외형이 다름</h3>
<p><strong>증상</strong>: 동일한 텍스처가 HDRP에서 다르게 보입니다.</p>
<p><strong>해결책</strong>: HDRP 셰이더로 머티리얼을 재생성하세요. Mask Map 형식에 맞게 텍스처를 리패킹하고, HDRP 라이팅 모델에 맞게 머티리얼 파라미터를 조정해야 합니다.</p>
<h2 id="성능-고려사항">성능 고려사항</h2>
<h3 id="메모리-사용량">메모리 사용량</h3>
<p>HDRP 깊이 피라미드는 URP 깊이 텍스처보다 약 33% 더 많은 메모리를 사용합니다. 또한 HDRP는 모션 벡터, 노말 버퍼 등 추가 버퍼를 필요로 합니다. 따라서 URP 대비 2-3배의 메모리 사용량을 계획하는 것이 좋습니다.</p>
<h3 id="드로우-콜">드로우 콜</h3>
<p>HDRP 깊이 프리패스는 추가적인 드로우 콜을 발생시킵니다. 잘못된 인젝션 포인트의 커스텀 패스는 중복 작업을 유발할 수 있으므로, 가능한 한 커스텀 패스를 배치 처리하세요.</p>
<h3 id="셰이더-베리언트">셰이더 베리언트</h3>
<p>HDRP는 디퍼드, 포워드, 깊이, 모션 등 더 많은 베리언트를 생성합니다. 베리언트를 줄이려면 shader_feature_local을 사용하고, 프로덕션 빌드에서 사용하지 않는 패스를 제거하세요.</p>
<h2 id="마이그레이션-체크리스트">마이그레이션 체크리스트</h2>
<h3 id="마이그레이션-전">마이그레이션 전</h3>
<p>마이그레이션을 시작하기 전에 모든 셰이더와 머티리얼을 백업하세요. 사용된 커스텀 URP 기능을 문서화하고, 모든 깊이 텍스처 사용을 식별하며, 모든 포스트 프로세싱 이펙트를 나열해야 합니다.</p>
<h3 id="셰이더-파일">셰이더 파일</h3>
<p>pragma target을 최소 4.5로 업데이트하세요. CGPROGRAM을 HLSLPROGRAM으로 교체하고, 모든 include 경로를 업데이트합니다. 스테레오 렌더링 매크로를 추가하고, 깊이 접근 패턴을 변환하며, 필요한 HDRP 패스를 추가해야 합니다.</p>
<h3 id="머티리얼">머티리얼</h3>
<p>HDRP 셰이더로 머티리얼을 재생성하세요. Mask Map을 위해 텍스처를 리패킹하고, HDRP 범위에 맞게 파라미터를 조정해야 합니다.</p>
<h3 id="포스트-프로세싱">포스트 프로세싱</h3>
<p>CustomPostProcessVolumeComponent로 변환하세요. 셰이더를 Resources에 배치하고, HDRP Settings에 등록하며, 모든 인젝션 포인트를 테스트해야 합니다.</p>
<h3 id="테스트">테스트</h3>
<p>깊이 이펙트가 올바르게 작동하는지 확인하세요. VR/스테레오 렌더링을 확인하고, 성능 영향을 프로파일링하며, 타겟 플랫폼에서 검증해야 합니다.</p>
<h2 id="플랫폼별-참고사항">플랫폼별 참고사항</h2>
<h3 id="콘솔-최적화">콘솔 최적화</h3>
<p>가능한 곳에 half precision을 사용하세요. 깊이 텍스처 읽기는 콘솔에서 비용이 높으므로 최소화해야 합니다. 비용이 높은 이펙트에는 낮은 해상도를 고려하세요.</p>
<h3 id="모바일-폴백">모바일 폴백</h3>
<p>HDRP는 모바일에서 지원되지 않습니다. 따라서 모바일 타겟을 위해 URP 버전을 유지하고, 조건부 컴파일을 위해 define 심볼을 사용하세요.</p>
<h3 id="vr-고려사항">VR 고려사항</h3>
<p>항상 _X 텍스처 매크로를 사용하세요. 모든 셰이더에 스테레오 설정을 포함하고, 싱글 패스와 멀티 패스 스테레오를 모두 테스트해야 합니다.</p>
<h2 id="빠른-참조-카드">빠른 참조 카드</h2>
<h3 id="필수-hdrp-함수">필수 HDRP 함수</h3>
<pre><code class="language-jsx">// 깊이 접근
LoadCameraDepth(uint2 positionSS)
SampleCameraDepth(float2 uv)
LinearEyeDepth(depth, _ZBufferParams)
Linear01Depth(depth, _ZBufferParams)

// 위치 재구성
GetPositionInput(positionSS, invScreenSize, depth, invVP, viewMatrix)
GetAbsolutePositionWS(positionWS)

// 커스텀 패스 헬퍼
CustomPassLoadCameraColor(positionSS, lod)
CustomPassSampleCameraColor(uv, lod)
LoadCustomDepth(positionSS)
SampleCustomDepth(uv)</code></pre>
<h3 id="필수-define">필수 Define</h3>
<pre><code class="language-jsx">#pragma target 4.5
#pragma only_renderers d3d11 playstation xboxone xboxseries vulkan metal switch
UNITY_SETUP_INSTANCE_ID(input)
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output)
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input)</code></pre>
<h3 id="패스-lightmode">패스 LightMode</h3>
<p>HDRP에서 사용되는 주요 패스 LightMode는 다음과 같습니다. ForwardOnly는 투명 포워드 렌더링에 사용됩니다. DepthForwardOnly는 깊이 프리패스에 사용되며, ShadowCaster는 그림자 맵 생성에 사용됩니다. META 패스는 라이트맵 베이킹에 필요하고, SceneSelectionPass는 에디터 선택 기능에 사용됩니다. MotionVectors는 템포럴 이펙트를 위한 모션 벡터 생성에 사용됩니다.</p>
<h2 id="결론">결론</h2>
<p>URP에서 HDRP로 마이그레이션하려면 특히 깊이 처리, include 구조, 렌더링 패스에서의 근본적인 아키텍처 차이를 이해해야 합니다. 가장 중요한 변경사항은 깊이 텍스처 접근입니다. 직접 텍스처 샘플링 대신 항상 LoadCameraDepth() 또는 SampleCameraDepth()를 사용하세요.</p>
<p>성공은 이 가이드의 패턴을 따르는 체계적인 마이그레이션, 타겟 플랫폼에서의 철저한 테스트, 그리고 HDRP의 복잡성이 메모리 사용량 증가와 플랫폼 호환성 감소라는 비용을 치르고 더 높은 비주얼 품질을 가능하게 한다는 이해에 달려 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Unity HDRP 커스텀 Depth 셰이더 가이드]]></title>
            <link>https://velog.io/@mazeline_1973/Unity-HDRP-%EC%BB%A4%EC%8A%A4%ED%85%80-Depth-%EC%85%B0%EC%9D%B4%EB%8D%94-%EA%B0%80%EC%9D%B4%EB%93%9C</link>
            <guid>https://velog.io/@mazeline_1973/Unity-HDRP-%EC%BB%A4%EC%8A%A4%ED%85%80-Depth-%EC%85%B0%EC%9D%B4%EB%8D%94-%EA%B0%80%EC%9D%B4%EB%93%9C</guid>
            <pubDate>Tue, 21 Oct 2025 14:13:18 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>고객사에서 새로운 일감을 받았습니다. Uber Particle 이라는 URP 전용 이펙트 개발 라이브러리를 HDRP 에서도 동작하도록 포팅 해 달라는 것이었습니다. 그 과정에서 뎁스 텍스처 관련 된 내용에서 많은 차이가 있었기 때문에 기록용으로 velog 에 정리해서 남겨놔야겠다는 생각이 들어 정리해 봤습니다.</p>
</blockquote>
<h1 id="unity-hdrp에서-커스텀-depth-셰이더-다루기">Unity HDRP에서 커스텀 Depth 셰이더 다루기</h1>
<p>HDRP는 깊이 텍스처에 접근하는 방식이 Built-in이나 URP 파이프라인과 완전히 다릅니다. <strong>_CameraDepthTexture를 직접 샘플링하는 대신 LoadCameraDepth()나 SampleCameraDepth() 함수를 사용해야 하는데</strong>, 이는 HDRP가 깊이를 계층적 밉맵 피라미드 구조로 저장하기 때문입니다. LinearEyeDepth() 함수는 명시적으로 _ZBufferParams 파라미터가 필요하고, 모든 셰이더는 레거시 CGPROGRAM 문법 대신 HLSLPROGRAM 블록과 패키지 기반 include를 사용해야 합니다. 이런 변화들은 하이엔드 그래픽에 최적화된 HDRP의 모던 렌더링 아키텍처를 반영한 것입니다.</p>
<p>이런 차이점을 이해하면 가장 흔한 함정을 피할 수 있습니다. Built-in 파이프라인의 깊이 접근 패턴을 그대로 사용하려고 하면 컴파일 에러가 나거나 잘못된 값이 반환됩니다. HDRP의 접근 방식은 depth pyramid 시스템을 통해 더 나은 성능을 제공하며, ambient occlusion이나 reflection 같은 스크린 스페이스 이펙트를 효율적으로 구현할 수 있지만, 그만큼 정해진 API를 따라야 합니다.</p>
<h2 id="hdrp-깊이-접근을-위한-필수-설정과-include">HDRP 깊이 접근을 위한 필수 설정과 include</h2>
<p>깊이에 접근하는 모든 HDRP 셰이더는 Unity의 Scriptable Render Pipeline 패키지에서 특정 include 파일들을 필요로 합니다. <strong>필수적으로 포함해야 하는 두 가지는 Common.hlsl과 ShaderVariables.hlsl인데</strong>, 이들은 깊이 샘플링 함수와 전역 셰이더 변수들(_ZBufferParams, _ScreenSize 등)을 제공합니다.</p>
<pre><code>#include &quot;Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl&quot;
#include &quot;Packages/com.unity.render-pipelines.high-definition/Runtime/ShaderLibrary/ShaderVariables.hlsl&quot;</code></pre><p>Custom Pass 셰이더의 경우, 카메라 컬러와 커스텀 버퍼를 로드하는 헬퍼 함수들이 포함된 CustomPassCommon.hlsl을 추가로 포함해야 합니다:</p>
<pre><code>#include &quot;Packages/com.unity.render-pipelines.high-definition/Runtime/RenderPipeline/RenderPass/CustomPass/CustomPassCommon.hlsl&quot;</code></pre><p>깊이와 함께 노말을 다루는 경우, HDRP가 depth prepass 중에 생성하는 노말 버퍼에 접근하기 위해 NormalBuffer.hlsl을 포함하시기 바랍니다.</p>
<p>모든 HDRP 셰이더는 <strong>CGPROGRAM/ENDCG 대신 HLSLPROGRAM/ENDHLSL 블록을 사용</strong>해야 하고, 셰이더 모델 4.5 이상을 타겟으로 하며, 지원하는 플랫폼을 명시적으로 지정해야 합니다. pragma 지시자는 다음과 같이 작성하시면 됩니다:</p>
<pre><code>HLSLPROGRAM
#pragma vertex Vert
#pragma fragment CustomPostProcess
#pragma target 4.5
#pragma only_renderers d3d11 playstation xboxone vulkan metal switch</code></pre><p>UnityCG.cginc 같은 레거시 Built-in 파이프라인 include는 HDRP와 호환되지 않아서 컴파일 에러를 일으킵니다. 패키지 기반 include로의 전환은 각 파이프라인이 자체 패키지와 파이프라인별 셰이더 라이브러리를 가지는 Unity의 모듈식 렌더링 아키텍처를 반영한 것입니다.</p>
<h2 id="hdrp의-핵심-깊이-샘플링-함수들">HDRP의 핵심 깊이 샘플링 함수들</h2>
<p>HDRP는 깊이 접근을 위한 두 가지 주요 함수를 제공하는데, 각각 다른 좌표 시스템에 맞춰져 있습니다. <strong>LoadCameraDepth()</strong>는 정수형 픽셀 좌표(uint2)를 받아서 필터링 없이 depth pyramid에서 직접 로드합니다. 스크린 해상도에서 작동하는 포스트 프로세싱 이펙트에 이상적입니다:</p>
<pre><code>uint2 positionSS = input.texcoord * _ScreenSize.xy;
float rawDepth = LoadCameraDepth(positionSS);</code></pre><p><strong>SampleCameraDepth()</strong>는 정규화된 UV 좌표(0-1 범위의 float2)를 받아서 바이리니어 필터링과 함께 샘플링합니다. UV로 작업하거나 필터링된 깊이 값이 필요할 때 유용합니다:</p>
<pre><code>float2 uv = input.texcoord;
float rawDepth = SampleCameraDepth(uv);</code></pre><p>두 함수 모두 HDRP의 depth pyramid 구조를 자동으로 처리합니다. 이는 각 레벨이 최소값(가장 가까운)을 사용해 다운샘플링된 깊이를 저장하는 멀티레벨 밉맵 아틀라스입니다. 이 피라미드 덕분에 screen-space ambient occlusion 같은 계층적 Z-buffer 알고리즘이 먼 픽셀에 대해 더 거친 깊이 레벨을 샘플링할 수 있어 효율적입니다.</p>
<p>반환되는 값은 깊이 버퍼에 저장된 <strong>raw non-linear depth</strong>입니다. 일반적으로 DirectX 계열 플랫폼에서는 reversed-Z 포맷으로, 1.0이 near plane을, 0.0이 far plane을 나타냅니다. 이를 사용 가능한 선형 값으로 변환하려면 전용 변환 함수를 사용해야 합니다.</p>
<p>Custom Pass 셰이더의 경우, 특정 오브젝트를 렌더링할 수 있는 커스텀 깊이 버퍼에 접근하는 추가 함수들이 있습니다:</p>
<pre><code>float customDepth = LoadCustomDepth(uint2 pixelCoords);
float customDepth = SampleCustomDepth(float2 uv);</code></pre><p>이런 커스텀 버퍼를 사용하면 X-ray 비전, 선택 아웃라인, 또는 특정 지오메트리의 깊이를 메인 씬 깊이와 비교하는 등의 고급 이펙트를 구현할 수 있습니다.</p>
<h2 id="raw-깊이를-선형-공간으로-변환하기">Raw 깊이를 선형 공간으로 변환하기</h2>
<p>Raw 깊이 버퍼 값은 디테일이 중요한 카메라 근처에서 최대한의 정밀도를 확보하기 위해 비선형 분포를 사용합니다. <strong>LinearEyeDepth()는 raw 깊이를 카메라 평면으로부터의 월드 단위 선형 거리로 변환하는데</strong>, 일관된 감쇠가 필요한 안개나 깊이 기반 페이딩 같은 이펙트에 필수적입니다:</p>
<pre><code>float rawDepth = LoadCameraDepth(positionSS);
float eyeDepth = LinearEyeDepth(rawDepth, _ZBufferParams);
// eyeDepth는 이제 카메라로부터의 월드 단위(미터) 거리입니다</code></pre><p><strong>Linear01Depth()는 깊이를 0-1 범위로 정규화</strong>하는데, 0이 near plane이고 1이 far plane입니다. 시각화나 카메라 far plane 거리와 무관하게 일관된 깊이가 필요할 때 유용합니다:</p>
<pre><code>float linear01 = Linear01Depth(rawDepth, _ZBufferParams);
// linear01은 near plane에서 0.0, far plane에서 1.0</code></pre><p>두 함수 모두 <strong>_ZBufferParams</strong>가 필요한데, 이는 카메라 프로젝션 설정에 기반해 HDRP가 자동으로 채우는 깊이 재구성 파라미터들을 담은 float4입니다. 정확한 공식은 reversed-Z 구성을 고려하고 원근/직교 카메라 모두를 올바르게 처리합니다.</p>
<p>차이점을 이해하는 것이 중요합니다: LinearEyeDepth는 계산에 사용할 수 있는 실제 거리를 제공하고(&quot;카메라로부터 10미터&quot; 같은), Linear01Depth는 그라데이션이나 시각화에 유용한 정규화된 깊이를 제공합니다. 5미터에서 20미터로 페이드되는 안개 이펙트라면 LinearEyeDepth를 사용하고, 깊이 텍스처 시각화라면 Linear01Depth를 사용하시면 됩니다.</p>
<p>플랫폼별 깊이 버퍼 포맷 차이는 자동으로 처리됩니다. DirectX는 더 나은 부동소수점 정밀도 분포를 위해 reversed-Z(near에서 1.0)를 사용하고, OpenGL은 전통적으로 near에서 0.0을 사용했습니다. 변환 함수들이 이런 차이를 추상화해서 플랫폼 간에 일관된 동작을 보장합니다.</p>
<h2 id="완전한-포스트-프로세싱-셰이더-예제">완전한 포스트 프로세싱 셰이더 예제</h2>
<p>깊이를 로드하고 선형 공간으로 변환한 다음, 깊이 기반 이펙트를 만드는 실전용 커스텀 포스트 프로세싱 셰이더를 살펴보겠습니다. 이 예제는 거리에 따른 circle of confusion 값을 계산하는데, depth-of-field나 거리 기반 블러 효과에 유용합니다:</p>
<pre><code>Shader &quot;Hidden/Shader/DepthExample&quot;
{
    SubShader
    {
        Pass
        {
            Name &quot;DepthExample&quot;
            ZWrite Off
            ZTest Always
            Blend Off
            Cull Off

            HLSLPROGRAM
            #pragma fragment CustomPostProcess
            #pragma vertex Vert
            #pragma target 4.5
            #pragma only_renderers d3d11 playstation xboxone vulkan metal switch

            #include &quot;Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl&quot;
            #include &quot;Packages/com.unity.render-pipelines.high-definition/Runtime/ShaderLibrary/ShaderVariables.hlsl&quot;

            struct Attributes
            {
                uint vertexID : SV_VertexID;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct Varyings
            {
                float4 positionCS : SV_POSITION;
                float2 texcoord : TEXCOORD0;
                UNITY_VERTEX_OUTPUT_STEREO
            };

            Varyings Vert(Attributes input)
            {
                Varyings output;
                UNITY_SETUP_INSTANCE_ID(input);
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);
                output.positionCS = GetFullScreenTriangleVertexPosition(input.vertexID);
                output.texcoord = GetFullScreenTriangleTexCoord(input.vertexID);
                return output;
            }

            float4 _Params;
            #define _Distance _Params.w

            TEXTURE2D_X(_InputTexture);

            float4 CustomPostProcess(Varyings input) : SV_Target
            {
                UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
                uint2 positionSS = input.texcoord * _ScreenSize.xy;
                float3 inColor = LOAD_TEXTURE2D_X(_InputTexture, positionSS).xyz;

                // 깊이 로드 및 선형화
                float depth = LoadCameraDepth(positionSS);
                float linearEyeDepth = LinearEyeDepth(depth, _ZBufferParams);
                float coc = saturate(_Distance / linearEyeDepth);

                // 깊이 기반 블렌딩
                return float4(lerp(inColor, float3(0, 0, 1), coc), 1.0);
            }
            ENDHLSL
        }
    }
}</code></pre><p>이 셰이더를 HDRP의 포스트 프로세싱 스택에 통합하는 C# 볼륨 컴포넌트는 다음과 같습니다:</p>
<pre><code>using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.HighDefinition;
using System;

[Serializable, VolumeComponentMenu(&quot;Post-processing/Custom/DepthExample&quot;)]
public sealed class DepthExample : CustomPostProcessVolumeComponent, IPostProcessComponent
{
    public ClampedFloatParameter depthDistance = new ClampedFloatParameter(10f, 0f, 100f);
    Material m_Material;

    public bool IsActive() =&gt; m_Material != null &amp;&amp; depthDistance.value &gt; 0f;

    public override CustomPostProcessInjectionPoint injectionPoint =&gt;
        CustomPostProcessInjectionPoint.AfterPostProcess;

    public override void Setup()
    {
        if (Shader.Find(&quot;Hidden/Shader/DepthExample&quot;) != null)
            m_Material = new Material(Shader.Find(&quot;Hidden/Shader/DepthExample&quot;));
    }

    public override void Render(CommandBuffer cmd, HDCamera camera, RTHandle source, RTHandle destination)
    {
        if (m_Material == null) return;
        m_Material.SetVector(&quot;_Params&quot;, new Vector4(0, 0, 0, depthDistance.value));
        m_Material.SetTexture(&quot;_InputTexture&quot;, source);
        HDUtils.DrawFullScreen(cmd, m_Material, destination);
    }

    public override void Cleanup() =&gt; CoreUtils.Destroy(m_Material);
}</code></pre><p>이 셰이더는 <strong>Resources 폴더</strong>에 배치해야 하고, C# 스크립트는 Project Settings → Graphics → HDRP Default Settings의 <strong>Custom Post Process Orders</strong> 리스트에 추가해야 합니다. injection point는 이펙트가 언제 실행될지를 결정하는데—AfterPostProcess는 가장 마지막에 실행되고, BeforePostProcess는 tonemapping과 color grading 이전에 실행됩니다.</p>
<h2 id="custom-pass-풀스크린-셰이더-템플릿">Custom Pass 풀스크린 셰이더 템플릿</h2>
<p>Custom Pass는 포스트 프로세싱 이펙트보다 더 유연한데, HDRP 프레임의 특정 지점에 커스텀 렌더링을 주입할 수 있습니다. 깊이 접근이 포함된 완전한 Custom Pass 셰이더 템플릿을 보여드리겠습니다:</p>
<pre><code>Shader &quot;FullScreen/DepthEffect&quot;
{
    HLSLINCLUDE
    #pragma vertex Vert
    #pragma target 4.5
    #pragma only_renderers d3d11 ps4 xboxone vulkan metal switch

    #include &quot;Packages/com.unity.render-pipelines.high-definition/Runtime/RenderPipeline/RenderPass/CustomPass/CustomPassCommon.hlsl&quot;

    float4 FullScreenPass(Varyings varyings) : SV_Target
    {
        UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(varyings);

        // 깊이 로드 및 위치 재구성
        float depth = LoadCameraDepth(varyings.positionCS.xy);
        PositionInputs posInput = GetPositionInput(
            varyings.positionCS.xy,
            _[ScreenSize.zw](http://ScreenSize.zw),
            depth,
            UNITY_MATRIX_I_VP,
            UNITY_MATRIX_V
        );

        float3 worldPos = posInput.positionWS;
        float3 viewDir = GetWorldSpaceNormalizeViewDir(posInput.positionWS);
        float eyeDepth = LinearEyeDepth(depth, _ZBufferParams);

        // 현재 카메라 컬러 로드
        float4 color = float4(CustomPassLoadCameraColor(varyings.positionCS.xy, 0), 1);

        // 깊이 기반 이펙트 적용
        float depthFade = saturate(eyeDepth / 20.0);
        color.rgb = lerp(color.rgb, float3(0.5, 0.7, 1.0), depthFade * 0.3);

        return color;
    }
    ENDHLSL

    SubShader
    {
        Pass
        {
            Name &quot;Custom Pass&quot;
            ZWrite Off
            ZTest Always
            Blend SrcAlpha OneMinusSrcAlpha
            Cull Off

            HLSLPROGRAM
            #pragma fragment FullScreenPass
            ENDHLSL
        }
    }
    Fallback Off
}</code></pre><p><strong>PositionInputs</strong> 구조체는 특히 강력한데—월드 공간 위치, 뷰 공간 위치를 재구성하고 라이팅 계산을 위한 헬퍼 데이터를 제공합니다. 이 덕분에 Custom Pass는 커스텀 ambient occlusion이나 특수한 안개 볼륨 같은, 전체 씬 컨텍스트가 필요한 스크린 스페이스 이펙트에 이상적입니다.</p>
<h2 id="깊이로부터-월드-공간-위치-재구성하기">깊이로부터 월드 공간 위치 재구성하기</h2>
<p>스크린 공간 좌표와 깊이를 월드 공간 위치로 되돌리면 레이 마칭, 볼륨메트릭 계산, 또는 씬 깊이와 커스텀 지오메트리를 비교하는 등의 이펙트가 가능해집니다. HDRP는 완전한 재구성을 수행하는 <strong>GetPositionInput()</strong> 헬퍼를 제공합니다:</p>
<pre><code>float depth = LoadCameraDepth(varyings.positionCS.xy);
PositionInputs posInput = GetPositionInput(
    varyings.positionCS.xy,      // 스크린 공간 픽셀 좌표
    _[ScreenSize.zw](http://ScreenSize.zw),              // 역 스크린 크기 (1/width, 1/height)
    depth,                       // Raw 깊이 값
    UNITY_MATRIX_I_VP,           // 역 뷰-프로젝션 행렬
    UNITY_MATRIX_V               // 뷰 행렬
);

float3 worldPos = posInput.positionWS;  // 카메라 상대 월드 위치
float3 viewPos = posInput.positionVS;   // 뷰 공간 위치</code></pre><p><strong>중요</strong>: HDRP에서 positionWS는 큰 월드 좌표에서 부동소수점 정밀도를 향상시키기 위해 <strong>카메라 상대</strong> 좌표입니다. 절대 월드 위치를 얻으려면 다음을 사용하십시오:</p>
<pre><code>float3 absoluteWorldPos = GetAbsolutePositionWS(posInput.positionWS);</code></pre><blockquote>
<p>Camera-Relative World Position 에 대하여 잠깐 더 살펴보겠습니다…</p>
</blockquote>
<h2 id="camera-relative-world-position-이해하기">Camera-Relative World Position 이해하기</h2>
<h3 id="왜-camera-relative-좌표가-필요한가">왜 Camera-Relative 좌표가 필요한가?</h3>
<p>float는 32비트로 약 7자리 정도의 정밀도만 보장합니다. 그래서 원점에서 매우 먼 곳에 있는 오브젝트를 다룰 때 문제가 발생합니다.</p>
<pre><code>// 문제 상황
float3 worldPos = float3(10000000.0, 0.0, 10000000.0);  // 원점에서 10km 떨어진 위치
float3 offset = float3(0.001, 0.0, 0.001);              // 1mm만 이동하고 싶음

float3 newPos = worldPos + offset;
// 결과: 변화 없음! offset이 무시됨</code></pre><p>큰 숫자에 작은 숫자를 더하면 부동소수점 정밀도 때문에 작은 숫자가 손실됩니다. HDRP는 이 문제를 카메라를 원점으로 하는 상대 좌표계로 해결합니다.</p>
<h3 id="hdrp의-해결-방법">HDRP의 해결 방법</h3>
<pre><code>// 카메라를 원점(0,0,0)으로 보는 좌표계
// 실제 월드 위치: (10000000.0, 100.0, 10000000.0)
// 카메라 위치: (10000000.0, 100.0, 9999990.0)

// Camera-relative position
float3 positionWS = float3(0.0, 0.0, 10.0);  // 카메라 앞 10m
// 이제 작은 변화도 정확하게 표현 가능합니다!</code></pre><h3 id="실전-코드-예제">실전 코드 예제</h3>
<pre><code>Shader &quot;HDRP/CameraRelativeExample&quot;
{
    SubShader
    {
        Pass
        {
            HLSLPROGRAM
            #include &quot;Packages/com.unity.render-pipelines.high-definition/Runtime/ShaderLibrary/ShaderVariables.hlsl&quot;

            struct Varyings
            {
                float4 positionCS : SV_POSITION;
                float3 positionWS : TEXCOORD0;  // Camera-relative world position
            };

            Varyings Vert(Attributes input)
            {
                Varyings output;
                float3 positionWS = TransformObjectToWorld([input.positionOS.xyz](http://input.positionOS.xyz));
                // 이 positionWS는 이미 camera-relative입니다!

                output.positionWS = positionWS;
                output.positionCS = TransformWorldToHClip(positionWS);
                return output;
            }

            float4 Frag(Varyings input) : SV_Target
            {
                // 케이스 1: 카메라와의 거리 (camera-relative 그대로 사용)
                float distanceToCamera = length(input.positionWS);
                // 카메라가 원점이라 길이가 곧 거리입니다

                // 케이스 2: 절대 월드 좌표가 필요할 때
                float3 absoluteWorldPos = GetAbsolutePositionWS(input.positionWS);

                // 케이스 3: 두 오브젝트 사이 거리
                float3 otherObjectPos = float3(100, 0, 100);
                float distance = length(input.positionWS - otherObjectPos);
                // 둘 다 camera-relative면 바로 계산 가능합니다

                // 케이스 4: 특정 월드 좌표와 비교
                float3 worldTarget = float3(5000, 0, 5000);  // 절대 좌표
                float3 cameraRelativeTarget = worldTarget - _[WorldSpaceCameraPos.xyz](http://WorldSpaceCameraPos.xyz);
                float distanceToTarget = length(input.positionWS - cameraRelativeTarget);

                return float4(distanceToCamera / 100.0, 0, 0, 1);
            }
            ENDHLSL
        }
    }
}</code></pre><h3 id="상황별-사용-예제">상황별 사용 예제</h3>
<p><strong>안개(Fog) 효과</strong></p>
<pre><code>// Camera-relative 그대로 사용 - 간단하고 효율적입니다!
float fogDistance = length(positionWS);
float fogFactor = saturate(fogDistance / _FogMaxDistance);</code></pre><p><strong>월드 노이즈 샘플링</strong></p>
<pre><code>// 절대 좌표 필요 - 카메라가 움직여도 패턴 고정
float3 absolutePos = GetAbsolutePositionWS(positionWS);
float noise = SampleNoise3D(absolutePos * _NoiseScale);</code></pre><p><strong>라이트 거리 계산</strong></p>
<pre><code>// 라이트 위치도 camera-relative로 제공됩니다
float3 lightPos = _[LightPositionWS.xyz](http://LightPositionWS.xyz);
float distanceToLight = length(positionWS - lightPos);</code></pre><p><strong>특정 위치 마커</strong></p>
<pre><code>// 월드 (1000, 0, 1000) 위치에 마커
float3 markerWorldPos = float3(1000, 0, 1000);
float3 markerCameraRelative = markerWorldPos - _[WorldSpaceCameraPos.xyz](http://WorldSpaceCameraPos.xyz);
float distanceToMarker = length(positionWS - markerCameraRelative);

if (distanceToMarker &lt; 1.0)  // 1m 이내면 빨간색
    return float4(1, 0, 0, 1);</code></pre><h3 id="getpositioninput-활용하기">GetPositionInput 활용하기</h3>
<pre><code>// HDRP의 PositionInputs 구조체
PositionInputs posInput = GetPositionInput(
    varyings.positionCS.xy,
    _[ScreenSize.zw](http://ScreenSize.zw),
    depth,
    UNITY_MATRIX_I_VP,
    UNITY_MATRIX_V
);

// 제공되는 좌표들
float3 positionWS = posInput.positionWS;      // Camera-relative
float3 positionVS = posInput.positionVS;      // View space
float2 positionNDC = posInput.positionNDC;    // NDC
float2 positionSS = posInput.positionSS;      // Screen space

// 절대 좌표가 필요하면
float3 absoluteWS = GetAbsolutePositionWS(posInput.positionWS);</code></pre><h3 id="언제-어떤-좌표를-사용해야-하는가">언제 어떤 좌표를 사용해야 하는가?</h3>
<table>
<thead>
<tr>
<th>용도</th>
<th>좌표 타입</th>
<th>이유</th>
</tr>
</thead>
<tbody><tr>
<td><strong>안개, DOF</strong></td>
<td>Camera-relative</td>
<td>카메라 거리만 필요</td>
</tr>
<tr>
<td><strong>월드 텍스처 매핑</strong></td>
<td>Absolute</td>
<td>카메라가 움직여도 고정</td>
</tr>
<tr>
<td><strong>프로시저럴 생성</strong></td>
<td>Absolute</td>
<td>패턴이 월드에 고정</td>
</tr>
<tr>
<td><strong>오브젝트 간 거리</strong></td>
<td>Camera-relative</td>
<td>둘 다 relative면 가능</td>
</tr>
<tr>
<td><strong>라이팅</strong></td>
<td>Camera-relative</td>
<td>라이트도 relative</td>
</tr>
<tr>
<td><strong>물리 시뮬레이션</strong></td>
<td>Absolute</td>
<td>월드 물리 법칙</td>
</tr>
</tbody></table>
<h3 id="디버깅-예제">디버깅 예제</h3>
<pre><code>// 좌표계 확인용
float4 DebugCoordinates(Varyings input) : SV_Target
{
    float3 camRelative = input.positionWS;
    float3 absolute = GetAbsolutePositionWS(camRelative);
    float3 cameraWorldPos = _[WorldSpaceCameraPos.xyz](http://WorldSpaceCameraPos.xyz);

    // 검증: absolute = camRelative + cameraWorldPos
    float3 reconstructed = camRelative + cameraWorldPos;
    float error = length(absolute - reconstructed);

    if (error &gt; 0.001)
        return float4(1, 0, 0, 1);  // 오류!

    // 거리별 색상
    float dist = length(camRelative);
    if (dist &lt; 10)
        return float4(0, 1, 0, 1);      // 10m 이내
    else if (dist &lt; 100)
        return float4(1, 1, 0, 1);      // 100m 이내
    else
        return float4(0, 0, 1, 1);      // 100m 이상
}</code></pre><h3 id="주의할-점">주의할 점</h3>
<p>이 시스템을 사용할 때 몇 가지 알아두어야 할 점들이 있습니다. 먼저 <strong>성능 면에서 camera-relative 연산이 더 빠릅니다</strong>. 절대 좌표로 변환할 필요가 없어 추가 계산이 줄어듭니다. 그리고 <strong>큰 월드(10km 이상)에서는 정밀도를 유지하기 위해 이 방식이 필수적입니다</strong>. 부동소수점 정밀도 문제를 근본적으로 해결하기 때문입니다. 계산할 때는 <strong>항상 같은 좌표계끼리만 계산해야 한다는 점</strong>도 중요합니다. Camera-relative 좌표와 절대 좌표를 섞어 사용하면 잘못된 결과가 나옵니다. 마지막으로 <strong>GetAbsolutePositionWS() 함수를 호출하면 추가 연산 비용이 발생한다는 점</strong>을 기억하시기 바랍니다. 정말 필요한 경우에만 사용하는 것이 좋습니다.</p>
<pre><code>// x 잘못된 예: 좌표계 섞어 사용
float3 worldPos = GetAbsolutePositionWS(positionWS);
float3 lightPos = _[LightPositionWS.xyz](http://LightPositionWS.xyz);  // camera-relative!
float distance = length(worldPos - lightPos);  // 잘못되었습니다!

// o 올바른 예: 같은 좌표계
float distance = length(positionWS - _[LightPositionWS.xyz](http://LightPositionWS.xyz));</code></pre><p>이 시스템은 오픈월드나 우주 같은 큰 스케일 프로젝트에서 특히 중요합니다. 원점에서 아무리 멀어져도 밀리미터 단위 정밀도를 유지할 수 있습니다.</p>
<blockquote>
<p>다시 본론으로 돌아와서...</p>
</blockquote>
<h2 id="투명-셰이더에서-월드-포지션-복원하기">투명 셰이더에서 월드 포지션 복원하기</h2>
<p>투명 셰이더에서 씬 깊이와 프래그먼트 깊이를 비교할 때는 뷰 벡터를 이용해서 씬의 월드 포지션을 직접 복원할 수 있습니다:</p>
<pre><code>// 버텍스 셰이더에서 월드 포지션을 프래그먼트로 전달
float3 positionWS = TransformObjectToWorld(input.positionOS);
float3 viewVector = _WorldSpaceCameraPos - positionWS;

// 프래그먼트 셰이더에서
float2 screenUV = i.screenPos.xy / i.screenPos.w;
float sceneDepth = LoadCameraDepth(screenUV * _ScreenSize.xy);
float sceneEyeDepth = LinearEyeDepth(sceneDepth, _ZBufferParams);
float fragmentEyeDepth = -TransformWorldToView(positionWS).z;

// 씬 월드 포지션 복원
float3 sceneWorldPos = _WorldSpaceCameraPos - (normalize(viewVector) * sceneEyeDepth);</code></pre><p>이러한 수동 방식은 서피스 셰이더에서 Custom Pass 헬퍼 함수들을 사용할 수 없을 때 필요합니다. 핵심 아이디어는 뷰 벡터가 카메라에서 프래그먼트까지의 방향을 제공하고, 이를 씬 깊이로 스케일하면 화면 픽셀에서 씬 표면까지의 벡터를 얻을 수 있다는 것입니다.</p>
<h2 id="깊이-기반-포그-구현하기">깊이 기반 포그 구현하기</h2>
<p>깊이 포그는 가장 많이 사용되는 깊이 텍스처 활용법 중 하나로, 씬 지오메트리를 고려한 대기 효과를 만들 수 있습니다. 다음은 near와 far 페이드 거리를 가진 포그 구현입니다:</p>
<pre><code>float4 DepthFog(Varyings input) : SV_Target
{
    uint2 positionSS = input.texcoord * _ScreenSize.xy;
    float3 sceneColor = LOAD_TEXTURE2D_X(_InputTexture, positionSS).xyz;

    // 선형 깊이 가져오기
    float depth = LoadCameraDepth(positionSS);
    float eyeDepth = LinearEyeDepth(depth, _ZBufferParams);

    // near와 far 거리로 포그 팩터 계산
    float fogFactor = saturate((eyeDepth - _FogNear) / (_FogFar - _FogNear));
    fogFactor = pow(fogFactor, _FogDensity);

    // 씬 컬러와 포그 컬러 블렌딩
    float3 finalColor = lerp(sceneColor, _FogColor.rgb, fogFactor);

    return float4(finalColor, 1.0);
}</code></pre><p><strong>소프트 파티클 이펙트</strong>를 만들 때는 지오메트리와 교차할 때 페이드 아웃되도록 씬 깊이와 파티클 깊이를 비교하시면 됩니다:</p>
<pre><code>// 파티클 셰이더 프래그먼트 함수에서
float2 screenUV = i.screenPos.xy / i.screenPos.w;
float sceneEyeDepth = LinearEyeDepth(
    LoadCameraDepth(screenUV * _ScreenSize.xy),
    _ZBufferParams
);
float particleEyeDepth = LinearEyeDepth(i.screenPos.z / i.screenPos.w, _ZBufferParams);

// 지오메트리와 가까워질 때 페이드
float depthDiff = sceneEyeDepth - particleEyeDepth;
float fade = saturate(depthDiff / _FadeDistance);

// 알파에 적용
return particleColor * fade;</code></pre><p>이렇게 하면 파티클 쿼드가 솔리드 지오메트리를 뚫고 나가는 날카로운 교차점을 방지하고, 물보라나 마법 파티클처럼 표면에 자연스럽게 블렌딩되는 효과를 만들 수 있습니다.</p>
<h2 id="깊이-비교를-이용한-엣지-검출">깊이 비교를 이용한 엣지 검출</h2>
<p>깊이 기반 엣지 검출은 인접한 픽셀의 깊이를 비교해서 지오메트리 경계를 찾아내는 기법입니다. 아웃라인이나 테크니컬 드로잉 스타일, 가려지는 엣지 강조 같은 데 유용합니다:</p>
<pre><code>float DepthEdgeDetection(uint2 positionSS, float threshold)
{
    float centerDepth = LoadCameraDepth(positionSS);
    float edgeStrength = 0;

    // 소벨 커널 오프셋
    int2 offsets[8] = {
        int2(-1, -1), int2(0, -1), int2(1, -1),
        int2(-1,  0),              int2(1,  0),
        int2(-1,  1), int2(0,  1), int2(1,  1)
    };

    float weights[8] = { 1, 2, 1, 2, 2, 1, 2, 1 };

    for (int i = 0; i &lt; 8; i++)
    {
        uint2 samplePos = positionSS + offsets[i];
        float sampleDepth = LoadCameraDepth(samplePos);
        float depthDiff = abs(centerDepth - sampleDepth);
        edgeStrength += depthDiff * weights[i];
    }

    return step(threshold, edgeStrength);
}</code></pre><p>더 나은 결과를 얻으려면 비교 전에 선형 깊이로 변환하는 것이 좋습니다. 그래야 비선형 깊이 분포가 뷰 전체에 걸쳐 엣지 강도에 일관성 없게 영향을 주는 것을 막을 수 있습니다:</p>
<pre><code>float centerLinear = LinearEyeDepth(centerDepth, _ZBufferParams);
float sampleLinear = LinearEyeDepth(sampleDepth, _ZBufferParams);
float depthDiff = abs(centerLinear - sampleLinear);</code></pre><p>깊이 엣지와 노말 엣지를 결합하면 실루엣과 표면 디테일 변화 모두에서 작동하는 강력한 아웃라인 검출을 만들 수 있습니다.</p>
<h2 id="오브젝트-오클루전과-선택-효과">오브젝트 오클루전과 선택 효과</h2>
<p>Custom Pass를 사용하면 특정 오브젝트를 커스텀 깊이 버퍼에 렌더링한 다음 메인 씬 깊이와 비교해서 정교한 선택 및 오클루전 효과를 만들 수 있습니다. 다음은 X-ray 선택 하이라이트용 셰이더입니다:</p>
<pre><code>float4 SelectionHighlight(Varyings varyings) : SV_Target
{
    uint2 positionSS = varyings.positionCS.xy;

    // 양쪽 깊이 버퍼 로드
    float sceneDepth = LoadCameraDepth(positionSS);
    float customDepth = LoadCustomDepth(positionSS);

    // 비교 가능한 eye depth로 변환
    float sceneEye = LinearEyeDepth(sceneDepth, _ZBufferParams);
    float customEye = LinearEyeDepth(customDepth, _ZBufferParams);

    // 씬 컬러 로드
    float4 color = float4(CustomPassLoadCameraColor(positionSS, 0), 1);

    // 선택된 것이 커스텀 깊이에 보임
    if (customEye &lt; 100000.0) // 유효한 깊이가 쓰여짐
    {
        if (sceneEye &lt; customEye) // 선택된 오브젝트가 씬 뒤에
        {
            // 오브젝트가 가려짐 - x-ray 효과 적용
            color.rgb = lerp(color.rgb, _OccludedColor.rgb, 0.5);
        }
        else // 선택된 오브젝트 보임
        {
            // 하이라이트 적용
            color.rgb += _HighlightColor.rgb * 0.3;
        }
    }

    return color;
}</code></pre><p>Custom Pass C# 컴포넌트는 이 셰이더를 실행하기 전에 선택된 오브젝트를 커스텀 깊이 버퍼에 렌더링합니다:</p>
<pre><code>public class SelectionPass : CustomPass
{
    public LayerMask selectionLayer;

    protected override void Execute(CustomPassContext ctx)
    {
        // 선택 레이어를 커스텀 깊이에 렌더링
        CoreUtils.SetRenderTarget(ctx.cmd, ctx.customDepthBuffer, ClearFlag.All);
        CustomPassUtils.DrawRenderers(ctx, selectionLayer);

        // 풀스크린 셰이더 실행
        CoreUtils.SetRenderTarget(ctx.cmd, ctx.cameraColorBuffer, ctx.cameraDepthBuffer);
        CoreUtils.DrawFullScreen(ctx.cmd, passMaterial);
    }
}</code></pre><p>이 테크닉은 다양한 효과로 확장할 수 있습니다. 깊이 불연속성 기반 림 라이팅, 근접 하이라이트, 깊이 기반 그림자, 또는 특정 오브젝트를 씬의 나머지 부분과 구별해야 하는 모든 효과에 적용할 수 있습니다.</p>
<h2 id="built-in과-urp와의-중요한-차이점">Built-in과 URP와의 중요한 차이점</h2>
<p>HDRP에서 작동하지 않는 것들을 이해하면 디버깅 시간을 몇 시간이나 절약할 수 있습니다. <strong>레거시 매크로는 완전히 제거되었습니다</strong>. UNITY_DECLARE_DEPTH_TEXTURE, SAMPLE_DEPTH_TEXTURE를 사용할 수 없고, _CameraDepthTexture에 직접 접근할 수도 없습니다. 이것들은 단일 텍스처 깊이 버퍼용으로 설계된 것이고, HDRP의 깊이 피라미드는 특별한 샘플링 함수가 필요합니다.</p>
<p><strong>인클루드 파일 변경은 필수입니다</strong>. UnityCG.cginc는 HDRP에 존재하지 않습니다. 패키지 기반 인클루드를 사용해야 합니다. 매핑은 다음과 같습니다:</p>
<ul>
<li>Built-in: <code>#include &quot;UnityCG.cginc&quot;</code></li>
<li>URP: <code>#include &quot;Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareDepthTexture.hlsl&quot;</code></li>
<li>HDRP: <code>#include &quot;Packages/com.unity.render-pipelines.high-definition/Runtime/ShaderLibrary/ShaderVariables.hlsl&quot;</code></li>
</ul>
<p><strong>텍스처 선언 매크로</strong>가 UNITY_DECLARE_*에서 TEXTURE2D_X로 변경되었습니다. _X 변형은 스테레오 렌더링(VR)을 자동으로 처리해서 각 눈을 위한 텍스처 배열을 관리합니다. 마찬가지로 LOAD_TEXTURE2D_X가 로딩용 tex2D를 대체합니다:</p>
<pre><code>// Built-in/URP
UNITY_DECLARE_DEPTH_TEXTURE(_CameraDepthTexture);
float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, uv);

// HDRP
// 선언 필요 없음, 함수 바로 사용
float depth = LoadCameraDepth(positionSS);</code></pre><p><strong>함수 시그니처에 명시적 파라미터가 필요합니다</strong>. LinearEyeDepth()와 Linear01Depth()는 _ZBufferParams를 명시적으로 전달받아야 합니다. HDRP는 Built-in만큼 암시적 전역 상태에 의존하지 않습니다:</p>
<pre><code>// Built-in
float linearDepth = LinearEyeDepth(rawDepth);

// HDRP
float linearDepth = LinearEyeDepth(rawDepth, _ZBufferParams);</code></pre><p><strong>셰이더 패스 요구사항</strong>이 크게 달라집니다. HDRP는 깊이 프리패스용 &quot;DepthForwardOnly&quot; 같은 특정 LightMode 태그를 사용하고, Custom Pass는 정의된 지점(BeforeRendering, BeforeTransparent, AfterOpaqueDepthAndNormal 등)에 주입됩니다. 리플레이스먼트 셰이더나 카메라 콜백 대신 말입니다.</p>
<p><strong>포스트 프로세싱 통합</strong>이 Post Processing Stack v2에서 HDRP의 내장 Custom Post Processing으로 완전히 변경되었습니다. 포스트 프로세스 레이어 컴포넌트 대신 새로운 볼륨 컴포넌트 스크립트와 HDRP Global Settings에 등록이 필요합니다.</p>
<h2 id="shader-graph-깊이-노드-사용법">Shader Graph 깊이 노드 사용법</h2>
<p>아티스트나 빠른 프로토타이핑용으로는 Shader Graph에서 세 가지 출력 모드를 가진 <strong>HD Scene Depth 노드</strong>를 제공합니다. <strong>Eye 모드</strong>는 월드 유닛 단위의 선형 깊이를 출력해서 LinearEyeDepth()와 동등합니다. <strong>Linear01 모드</strong>는 정규화된 0-1 깊이를 출력합니다. <strong>Raw 모드</strong>는 변환 전 원시 버퍼 값을 출력합니다.</p>
<p>Scene Depth 노드는 <strong>Screen Position</strong> 입력(정규화된 0-1 좌표)이 필요하고 투명 서페이스의 Fragment 스테이지에서만 작동합니다. 불투명 셰이더는 깊이를 샘플링할 수 없습니다. 자기 자신이 깊이 버퍼에 쓰고 있기 때문에 샘플링하면 순환 의존성이 발생합니다.</p>
<p>Shader Graph에서 간단한 깊이 포그 만들기:</p>
<ol>
<li><strong>Screen Position</strong> 노드 추가 (기본 출력)</li>
<li><strong>Scene Depth</strong> 노드 추가 (Eye 모드)</li>
<li><strong>Screen Position</strong> 노드를 Raw로 설정, RGBA 분할해서 W 컴포넌트를 프래그먼트 깊이로 사용</li>
<li>씬 깊이에서 프래그먼트 깊이를 <strong>빼기</strong></li>
<li>포그 거리 파라미터로 <strong>나누기</strong></li>
<li>결과를 <strong>Saturate</strong></li>
<li>페이드 효과를 위해 Alpha에 연결</li>
</ol>
<p>Shader Graph에서 <strong>Custom Depth</strong>에 접근하려면 다음 코드로 Custom Function 노드를 사용하십시오:</p>
<pre><code>void GetCustomDepth_float(float2 UV, out float Depth)
{
    Depth = SampleCustomDepth(UV);
}</code></pre><p>Shader Graph는 프로토타이핑이나 아티스트 친화적인 워크플로우에 좋지만, 손으로 작성한 코드가 더 나은 성능과 복잡한 깊이 기반 알고리즘에 대한 완전한 제어를 제공합니다. Master Node를 우클릭해서 &quot;Copy Shader&quot;를 선택하면 <strong>Shader Graph를 코드로 내보낼 수 있고</strong>, 생성된 HLSL을 최적화할 수 있습니다.</p>
<h2 id="깊이-접근-문제-해결하기">깊이 접근 문제 해결하기</h2>
<p><strong>깊이가 0이나 유효하지 않은 값을 반환합니다</strong>: 깊이가 쓰여진 후에 샘플링하고 있는지 확인하십시오. 포스트 프로세싱 효과는 주입 지점이 AfterOpaqueDepthAndNormal 이상이어야 합니다. 깊이 렌더링 전 Custom Pass는 비어있거나 이전 프레임의 깊이를 읽게 됩니다. Frame Settings에서 깊이 텍스처 생성이 활성화되어 있는지 확인하십시오. 일부 커스텀 카메라 설정은 성능을 위해 비활성화합니다.</p>
<p><strong>밉맵이나 샘플링 아티팩트</strong>: LoadCameraDepth()나 SampleCameraDepth()만 사용하십시오. LOAD_TEXTURE2D_X로 깊이 텍스처를 직접 샘플링하면 피라미드 샘플링 로직을 우회해서 잘못된 결과가 나옵니다. HDRP의 깊이 피라미드는 헬퍼 함수들이 내부적으로 관리하는 특별한 텍스처 좌표가 필요합니다.</p>
<p><strong>투명 오브젝트가 깊이에서 누락됩니다</strong>: 기본적으로 투명 머티리얼은 깊이를 쓰지 않습니다. 머티리얼 설정에서 Transparent Depth Prepass나 Transparent Depth Postpass를 활성화하십시오. Transparent 렌더 큐의 오브젝트는 별도 패스에서 깊이를 쓰도록 명시적으로 설정하지 않으면 깊이 텍스처에 나타나지 않습니다.</p>
<p><strong>커스텀 셰이더는 렌더링되는데 오브젝트가 깊이에서 보이지 않습니다</strong>: 커스텀 셰이더에 DepthForwardOnly 패스를 추가하십시오:</p>
<pre><code>Pass
{
    Name &quot;DepthForwardOnly&quot;
    Tags { &quot;LightMode&quot; = &quot;DepthForwardOnly&quot; }

    ZWrite On
    ColorMask 0

    HLSLPROGRAM
    #pragma vertex vert
    #pragma fragment frag
    #include &quot;Packages/com.unity.render-pipelines.high-definition/Runtime/RenderPipeline/ShaderLibrary/ShaderVariables.hlsl&quot;

    struct Attributes { float4 positionOS : POSITION; };
    struct Varyings { float4 positionCS : SV_POSITION; };

    Varyings vert(Attributes input)
    {
        Varyings output;
        output.positionCS = TransformObjectToHClip(input.positionOS);
        return output;
    }

    float4 frag() : SV_Target { return 0; }
    ENDHLSL
}</code></pre><p><strong>포스트 프로세싱 효과가 나타나지 않습니다</strong>: 셰이더가 <strong>Resources 폴더</strong>에 있는지, C# 컴포넌트가 CustomPostProcessVolumeComponent를 상속하는지, IsActive()가 true를 반환하는지, 컴포넌트가 Edit → Project Settings → HDRP → Custom Post Process Orders의 <strong>Custom Post Process Orders</strong>에 추가되어 있는지 확인하십시오. 씬의 Volume에 충분한 우선순위로 효과가 활성화되어 있는지도 확인하십시오.</p>
<p><strong>플랫폼별 이슈</strong>: Reversed-Z 동작이 플랫폼마다 다릅니다. DirectX는 reversed-Z(near에서 1)를 사용하는데 일부 OpenGL 구현은 전통적인 Z(near에서 0)를 사용합니다. 변환 함수들이 자동으로 처리하지만, 원시 깊이를 수동으로 처리한다면 UNITY_REVERSED_Z define을 확인하십시오. 에디터에서는 작동하는데 빌드에서 실패한다면 HDRP Asset Lit Shader Mode가 &quot;Deferred Only&quot;가 아닌 &quot;Both&quot;인지 확인하십시오. 셰이더 배리언트를 제한합니다.</p>
<h2 id="성능-최적화-전략">성능 최적화 전략</h2>
<p>깊이 텍스처에 접근하는 것은 메모리 대역폭을 많이 소비하는 작업입니다. 매번 샘플링할 때마다 VRAM에서 데이터를 읽어오기 때문입니다. 같은 픽셀의 깊이 값을 여러 번 사용한다면 LoadCameraDepth()를 세 번 호출하는 대신 로컬 변수에 저장해두는 것이 훨씬 효율적입니다. 이렇게 중복 샘플링을 최소화하는 것만으로도 성능이 눈에 띄게 개선됩니다.</p>
<p>인젝션 포인트도 신중하게 선택해야 합니다. 불투명 오브젝트의 깊이만 필요한 포스트 프로세싱 효과라면 AfterPostProcess 대신 AfterOpaqueAndSky에 주입하는 것이 좋습니다. 불필요한 재계산을 피할 수 있기 때문입니다. 각 인젝션 포인트마다 오버헤드가 있으므로 가능하면 여러 효과를 하나로 합치는 것도 방법입니다.</p>
<p>ambient occlusion 같은 무거운 깊이 기반 효과는 절반 해상도로 처리하는 것을 고려해보십시오. 화면 해상도를 절반으로 줄이면 샘플 수가 4분의 1로 줄어들면서 성능이 4배 향상되는데, 블러 효과에서는 품질 손실이 거의 눈에 띄지 않습니다. HDUtils.RTHandles로 스케일링을 적용하시면 됩니다:</p>
<pre><code>RTHandle halfResDepth = RTHandles.Alloc(
    [Vector2.one](http://Vector2.one) * 0.5f,
    depthBufferBits: DepthBits.None,
    filterMode: FilterMode.Bilinear);</code></pre><p>셰이더 복잡도도 깊이 샘플링 주변에서 최적화해야 합니다. 깊이 의존적인 계산들을 그룹화해서 프래그먼트 셰이더의 분기를 최소화하는 것이 좋습니다. discard나 clip 연산은 early-Z 최적화를 비활성화하기 때문에 깊이를 읽는 셰이더에서는 가급적 피하는 것이 좋습니다.</p>
<p>플랫폼별로도 고려할 점이 있습니다. 콘솔은 압축된 깊이 버퍼를 사용하는데, 읽기 자체는 빠르지만(early-Z 테스트) 전체 화면 압축 해제에 약 0.7ms 정도 소요됩니다. 그래서 성능이 중요한 경로에서는 깊이 텍스처 읽기를 최소화해야 합니다. 모바일에서는 깊이 텍스처 접근이 특히 비싸므로 더 낮은 해상도나 단순화된 알고리즘을 고려하는 것이 좋습니다.</p>
<p>메모리 사용량은 해상도에 비례해서 늘어납니다. 1080p 24비트 깊이는 약 2.5MB인데, 4K 32비트는 약 32MB나 차지합니다. 깊이 피라미드는 밉 레벨 때문에 대략 33% 정도의 추가 오버헤드가 있습니다. VRAM이 제한된 콘솔에서는 메모리 예산을 항상 모니터링해야 합니다.</p>
<p>일반적인 성능 목표치를 말씀드리면, 1080p에서 깊이 텍스처 생성은 1ms 미만, 깊이를 사용하는 포스트 프로세싱 효과 전체는 2ms 미만, 개별 Custom Pass는 0.5ms 미만이 적당합니다. Unity의 Frame Debugger와 플랫폼별 프로파일러로 병목 지점을 찾아내는 것이 중요합니다.</p>
<h2 id="프로젝트-설정-필수-체크리스트">프로젝트 설정 필수 체크리스트</h2>
<p>먼저 HDRP Asset 설정부터 확인해보겠습니다 (Project Settings → Graphics → HDRP Asset).</p>
<p>Lit Shader Mode는 &quot;Both&quot;로 설정해야 합니다. &quot;Deferred Only&quot;로 하면 Custom Pass를 지원하지 않습니다. Custom Pass는 Rendering 섹션에서 활성화하시고, Custom Buffer Format은 커스텀 버퍼를 사용한다면 적절한 포맷으로 설정하시면 되는데, 기본값은 R8G8B8A8입니다. Depth Pyramid 설정은 기본적으로 활성화되어 있고 깊이 샘플링 시 자동으로 사용됩니다.</p>
<p>다음으로 Frame Settings를 살펴보겠습니다. 카메라별로 또는 프로젝트 전체에 적용할 수 있습니다.</p>
<p>Opaque Objects의 Depth Prepass는 활성화되어 있어야 하는데, 보통 기본값으로 되어 있습니다. Custom Pass를 사용한다면 이것도 활성화해야 합니다. 특정 카메라가 깊이를 비활성화하는 오버라이드 설정을 하지 않았는지도 확인해보십시오.</p>
<p>Custom Post-Processing 설정도 중요합니다.</p>
<p>셰이더는 반드시 Resources 폴더 안에 있어야 합니다. 하위 폴더는 상관없습니다 (예: Resources/Shaders/DepthEffect.shader). C# 스크립트는 프로젝트 어디에나 있어도 되는데 CustomPostProcessVolumeComponent를 상속받아야 합니다. 이 스크립트를 Project Settings → Graphics → HDRP Default Settings → Custom Post Process Orders의 목록에 추가해야 합니다. 씬에 Volume이 있어야 하고 거기에 컴포넌트가 활성화되어 있어야 하며, 올바른 인젝션 포인트(AfterPostProcess, BeforePostProcess 등)를 선택해야 합니다.</p>
<p>Custom Pass 설정도 확인해보겠습니다.</p>
<p>Custom Pass Volume 컴포넌트가 있는 GameObject가 필요합니다. Custom Pass 스크립트가 연결되어 있거나 Fullscreen pass가 구성되어 있어야 합니다. Injection Point는 깊이가 필요한 시점에 맞게 설정해야 하며, Target ColorBuffer/DepthBuffer는 보통 둘 다 Camera로 설정하시면 됩니다.</p>
<p>Material/Shader 요구사항도 있습니다.</p>
<p>투명 머티리얼이 깊이를 읽으려면 설정이 필요합니다 (불투명 오브젝트는 자기 자신의 깊이를 읽습니다). 커스텀 셰이더에는 DepthForwardOnly 패스가 있어야 오브젝트가 깊이에 나타납니다. RenderType과 Queue 태그도 올바르게 설정되어 있어야 합니다.</p>
<p>설정을 확인하려면 Frame Debugger를 활성화해보십시오 (Window → Analysis → Frame Debugger). 깊이 텍스처 생성 패스가 나타나는지, 그리고 커스텀 패스나 포스트 프로세싱이 예상한 인젝션 포인트에서 실행되는지 확인할 수 있습니다.</p>
<h2 id="스테레오-렌더링과-vr-작업하기">스테레오 렌더링과 VR 작업하기</h2>
<p>HDRP의 깊이 시스템은 _X 텍스처 매크로를 통해 스테레오 렌더링을 자동으로 처리합니다. TEXTURE2D_X는 내부적으로 텍스처 배열이 되는데, 왼쪽 눈과 오른쪽 눈용 텍스처가 따로 있습니다. LOAD_TEXTURE2D_X는 스테레오 eye 인덱스를 사용해서 올바른 배열 슬라이스를 샘플링합니다.</p>
<p>vertex와 fragment 함수에서는 항상 스테레오 설정 매크로를 포함해야 합니다:</p>
<pre><code>// Vertex shader
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);

// Fragment shader
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);</code></pre><p>이 매크로들은 스테레오가 아닌 빌드에서는 아무 일도 하지 않지만, VR에서는 정확한 동작을 위해 반드시 필요합니다. 이것이 없으면 양쪽 눈 모두 잘못된 눈의 깊이 텍스처로 렌더링되어서 심각한 시각적 오류가 발생할 수 있습니다.</p>
<p>_ScreenSize 변수는 VR에서 눈별 렌더 타겟 크기를 자동으로 반영하기 때문에 좌표 계산이 그대로 맞아떨어집니다. 마찬가지로 뷰-프로젝션 행렬(UNITY_MATRIX_VP, UNITY_MATRIX_I_VP)도 눈별로 설정됩니다. 제공된 매크로만 제대로 사용하면 깊이 재구성 코드는 VR에서도 수정 없이 그대로 작동합니다.</p>
<p>VR에서 Custom Passes를 사용할 때는 양쪽 눈이 적절한 상태로 순차적으로 패스를 실행합니다. 커스텀 버퍼를 할당한다면 RTHandle에 XR 레이아웃을 사용해서 눈별 텍스처를 만들어야 합니다:</p>
<pre><code>RTHandle customBuffer = RTHandles.Alloc(
    [Vector2.one](http://Vector2.one),
    TextureXR.slices,  // 싱글/스테레오를 자동으로 처리
    colorFormat: GraphicsFormat.R32G32B32A32_SFloat);</code></pre><h2 id="고급-깊이-피라미드-활용">고급 깊이 피라미드 활용</h2>
<p>LoadCameraDepth()와 SampleCameraDepth()는 자동으로 깊이 피라미드를 사용하지만, 고급 기법을 위해 특정 밉 레벨을 직접 샘플링할 수도 있습니다. 낮은 밉 레벨에는 min-filtered 깊이(4x4 픽셀 블록에서 가장 가까운 값)가 들어있는데, 이것이 계층적 Z-버퍼 알고리즘에 유용합니다.</p>
<p>스크린 스페이스 ambient occlusion은 먼 샘플 포인트를 체크할 때 거친 밉 레벨을 샘플링하면 좋습니다. 정확도를 조금 포기하는 대신 상당한 성능 향상을 얻을 수 있습니다. 32픽셀 떨어진 샘플을 읽을 때 밉 레벨 0(원본) 대신 밉 레벨 2(4배 다운샘플)를 읽으면 메모리 대역폭이 16배나 줄어듭니다.</p>
<p>깊이 인식 업샘플링도 깊이 피라미드를 활용해서 bilateral filtering을 가이드할 수 있습니다. 여러 밉 레벨을 체크해서 전체 해상도로 모든 픽셀을 읽지 않고도 깊이 불연속성을 빠르게 찾아낼 수 있습니다.</p>
<p>깊이 피라미드는 불투명 렌더링 이후 AfterOpaqueDepthAndNormal 인젝션 포인트에서 자동으로 생성되기 때문에, 그 이후에 주입되는 포스트 프로세싱과 Custom Passes에서 사용할 수 있습니다. 각 밉은 min filtering을 사용하는데, 피라미드가 각 레벨에서 가장 가까운 깊이를 저장해서 날카로운 경계를 유지하고 보수적인 occlusion 테스트를 보장합니다.</p>
<p>밉별 접근이 필요한 커스텀 효과를 만들려면 깊이 피라미드 텍스처를 수동으로 선언해야 하는데, 대부분의 경우에는 제공된 함수들이 복잡성을 효과적으로 추상화해주기 때문에 거의 필요 없습니다.</p>
<h2 id="완전한-워크플로우-깊이-기반-물-거품">완전한 워크플로우: 깊이 기반 물 거품</h2>
<p>실전 예제로 실용적인 효과를 만들어보겠습니다. 물이 지형과 만나는 해안선에 거품이 나타나는 효과입니다. 투명 셰이더에서 깊이 비교를 하는데, 지금까지 다룬 모든 기법이 들어가 있습니다:</p>
<pre><code class="language-jsx">Shader &quot;Custom/WaterFoam&quot;
{
    Properties
    {
        _FoamColor (&quot;Foam Color&quot;, Color) = (1,1,1,1)
        _FoamDistance (&quot;Foam Distance&quot;, Float) = 0.5
        _WaterColor (&quot;Water Color&quot;, Color) = (0,0.4,0.8,0.7)
    }

    SubShader
    {
        Tags { &quot;RenderType&quot;=&quot;Transparent&quot; &quot;Queue&quot;=&quot;Transparent&quot; }

        Pass
        {
            Blend SrcAlpha OneMinusSrcAlpha
            ZWrite Off

            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma target 4.5

            #include &quot;Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl&quot;
            #include &quot;Packages/com.unity.render-pipelines.high-definition/Runtime/ShaderLibrary/ShaderVariables.hlsl&quot;

            struct Attributes
            {
                float4 positionOS : POSITION;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct Varyings
            {
                float4 positionCS : SV_POSITION;
                float4 screenPos : TEXCOORD0;
                float3 positionWS : TEXCOORD1;
                UNITY_VERTEX_OUTPUT_STEREO
            };

            float4 _FoamColor;
            float4 _WaterColor;
            float _FoamDistance;

            Varyings vert(Attributes input)
            {
                Varyings output;
                UNITY_SETUP_INSTANCE_ID(input);
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);

                output.positionWS = TransformObjectToWorld([input.positionOS.xyz](http://input.positionOS.xyz));
                output.positionCS = TransformWorldToHClip(output.positionWS);
                output.screenPos = ComputeScreenPos(output.positionCS);

                return output;
            }

            float4 frag(Varyings input) : SV_Target
            {
                UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);

                // 스크린 UV 가져오기
                float2 screenUV = input.screenPos.xy / input.screenPos.w;
                uint2 positionSS = screenUV * _ScreenSize.xy;

                // 씬 깊이 로드
                float sceneDepth = LoadCameraDepth(positionSS);
                float sceneEyeDepth = LinearEyeDepth(sceneDepth, _ZBufferParams);

                // 프래그먼트 깊이 계산
                float3 viewPos = TransformWorldToView(input.positionWS);
                float fragmentEyeDepth = -viewPos.z;

                // 깊이 차이 - 지형이 물 아래 얼마나 깊이 있는지
                float depthDiff = sceneEyeDepth - fragmentEyeDepth;

                // 거품 팩터 - 교차점에서 1, 거리에서 0으로 페이드
                float foamFactor = 1.0 - saturate(depthDiff / _FoamDistance);
                foamFactor = pow(foamFactor, 2.0); // 폴오프 날카롭게

                // 물과 거품 블렌딩
                float4 finalColor = lerp(_WaterColor, _FoamColor, foamFactor);

                return finalColor;
            }
            ENDHLSL
        }
    }
}</code></pre>
<p>이 셰이더는 물 표면과 아래 지형 사이의 깊이 차이를 계산해서 자연스러운 거품을 만듭니다. 깊이 차이가 줄어들수록(물이 해안선에 가까워질수록) 거품 강도가 증가합니다. 이 효과는 어떤 지오메트리든 자동으로 적응합니다. 해변이든 바위든 얕은 웅덩이든 수동으로 페인팅할 필요 없이 말입니다.</p>
<p>이 기법은 다양한 파티클과 투명 효과로 확장할 수 있습니다. 빗물 웅덩이, 충격에 반응하는 에너지 쉴드, 포스 필드 교차점, 또는 투명 표면이 실제 씬 깊이를 기반으로 단단한 지오메트리와의 근접도에 반응해야 하는 모든 효과에 활용할 수 있습니다.</p>
<hr>
<h2 id="그래픽스-프로그래밍-용어-레퍼런스">그래픽스 프로그래밍 용어 레퍼런스</h2>
<p>몇 가지 그래픽스 프로그래밍 용어들을 설명합니다.</p>
<h3 id="깊이depth-관련">깊이(Depth) 관련</h3>
<ul>
<li><strong>Depth Buffer (Z-Buffer):</strong> 각 픽셀의 카메라로부터의 거리 정보를 저장하는 버퍼로, 어떤 오브젝트가 앞에 있는지 판단하는 데 사용됩니다. <a href="https://learnopengl.com/Advanced-OpenGL/Depth-testing">Depth Testing 설명</a></li>
<li><strong>Depth Texture:</strong> 깊이 버퍼의 내용을 텍스처로 저장한 것으로, 셰이더에서 씬의 깊이 정보를 읽을 수 있게 합니다.</li>
<li><strong>LinearEyeDepth:</strong> 카메라 공간에서의 선형 깊이 값으로, 실제 월드 단위로 카메라로부터의 거리를 나타냅니다.</li>
<li><strong>깊이 피라미드 (Depth Pyramid):</strong> 원본 깊이 텍스처를 여러 해상도로 다운샘플링한 밉맵 체인으로, 효율적인 깊이 쿼리를 가능하게 합니다.</li>
<li><strong>Hierarchical Z-Buffer:</strong> 여러 레벨의 깊이 정보를 계층적으로 저장하여 대규모 오클루전 테스트를 최적화하는 기법입니다.</li>
</ul>
<h3 id="렌더링-기법">렌더링 기법</h3>
<ul>
<li><strong>Custom Pass:</strong> HDRP에서 렌더링 파이프라인의 특정 지점에 사용자 정의 렌더링 로직을 삽입하는 기능입니다. <a href="https://docs.unity3d.com/Packages/com.unity.render-pipelines.high-definition@latest/index.html?subfolder=/manual/Custom-Pass.html">Custom Pass 문서</a></li>
<li><strong>Injection Point:</strong> 커스텀 패스가 실행되는 렌더링 파이프라인의 특정 단계입니다.</li>
<li><strong>RTHandle (Render Texture Handle):</strong> HDRP에서 해상도 독립적인 렌더 타겟을 관리하는 시스템으로, 동적 해상도를 지원합니다.</li>
<li><strong>Stencil Buffer:</strong> 픽셀 단위로 마스킹 정보를 저장하는 버퍼로, 특정 영역만 렌더링하거나 효과를 적용할 때 사용됩니다.</li>
</ul>
<h3 id="vrxr-관련">VR/XR 관련</h3>
<ul>
<li><strong>Single-Pass Instanced:</strong> 양쪽 눈의 렌더링을 한 번의 드로우 콜로 처리하는 VR 최적화 기법입니다. <a href="https://docs.unity3d.com/Manual/SinglePassInstancing.html">Single-Pass Instancing 문서</a></li>
<li><strong>Stereo Rendering:</strong> VR에서 좌우 눈을 위한 두 개의 별도 이미지를 렌더링하는 과정입니다.</li>
<li><strong>Eye Index:</strong> VR에서 현재 렌더링 중인 눈(왼쪽=0, 오른쪽=1)을 식별하는 인덱스입니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[GPU 병렬 연산: Warp Divergence 이해하고 해결하기]]></title>
            <link>https://velog.io/@mazeline_1973/GPU-%EB%B3%91%EB%A0%AC-%EC%97%B0%EC%82%B0-Warp-Divergence-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B3%A0-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@mazeline_1973/GPU-%EB%B3%91%EB%A0%AC-%EC%97%B0%EC%82%B0-Warp-Divergence-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B3%A0-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 20 Oct 2025 05:58:07 GMT</pubDate>
            <description><![CDATA[<h1 id="gpu-병렬-연산-warp-divergence-이해하고-해결하기">GPU 병렬 연산: Warp Divergence 이해하고 해결하기</h1>
<p>GPU 프로그래밍을 하다 보면 성능 최적화에서 자주 마주치는 개념이 바로 Warp Divergence입니다. 특히 셰이더 프로그래밍에서 이를 이해하고 적절히 대응하는 것은 성능에 큰 영향을 미칩니다. 이번 글에서는 Warp의 개념부터 Divergence가 발생하는 상황, 그리고 이를 해결하는 베스트 프랙티스까지 알아보겠습니다.</p>
<h2 id="warp란-무엇인가">Warp란 무엇인가</h2>
<p>GPU는 SIMT(Single Instruction, Multiple Threads) 아키텍처를 사용합니다. 이는 여러 스레드가 동일한 명령어를 동시에 실행하는 구조를 의미합니다.
<img src="https://velog.velcdn.com/images/mazeline_1973/post/41c48dde-81a5-48d6-b3a9-40c9370bd512/image.png" alt=""></p>
<h3 id="주요-gpu-아키텍처의-warp-크기">주요 GPU 아키텍처의 Warp 크기</h3>
<ul>
<li><strong>NVIDIA GPU</strong>: 32개의 스레드가 하나의 그룹으로 묶여 실행되며, 이를 <strong>Warp</strong>라고 부릅니다<ul>
<li>최신 Ada Lovelace 아키텍처(RTX 40 시리즈)에서는 Thread Divergence 처리 성능이 개선되었습니다</li>
</ul>
</li>
<li><strong>AMD GPU</strong>: 64개의 스레드가 하나의 그룹으로 묶이며, 이를 <strong>Wavefront</strong>라고 합니다</li>
<li><strong>Intel Arc GPU</strong>: EU(Execution Unit)당 다른 스레드 구성을 가지며, SIMD 실행 모델이 NVIDIA 및 AMD와 다릅니다</li>
</ul>
<p>Warp 내의 모든 스레드는 같은 명령어를 동시에 실행하는 것이 가장 효율적입니다. 이러한 병렬 처리 방식 덕분에 GPU는 방대한 양의 데이터를 빠르게 처리할 수 있습니다.</p>
<h2 id="warp-divergence의-정체">Warp Divergence의 정체</h2>
<p><img src="https://velog.velcdn.com/images/mazeline_1973/post/325b57c2-c7b1-4600-91dd-aaf6da37c427/image.png" alt=""></p>
<p>Warp Divergence는 같은 Warp 내의 스레드들이 서로 다른 실행 경로를 따라갈 때 발생합니다. 가장 흔한 예는 조건문에서 일부 스레드는 if 블록을 실행하고, 다른 스레드는 else 블록을 실행하는 경우입니다.</p>
<p>다음 GLSL 셰이더 코드를 살펴보겠습니다.</p>
<pre><code class="language-glsl">if (gl_GlobalInvocationID.x % 2 == 0) {
    // 짝수 스레드: 경로 A
    result = computeA();
} else {
    // 홀수 스레드: 경로 B
    result = computeB();
}</code></pre>
<p>이 경우 GPU는 다음과 같이 동작합니다. 먼저 Warp 내 짝수 스레드들이 computeA()를 실행하는 동안 홀수 스레드들은 대기합니다. 그 다음 홀수 스레드들이 computeB()를 실행하는 동안 짝수 스레드들은 대기합니다. 결과적으로 실행 시간이 두 경로의 합이 되어 성능이 저하됩니다.</p>
<p><strong>최악의 경우 성능이 2배 가까이 느려질 수 있습니다.</strong></p>
<h2 id="divergence가-발생하는-주요-상황">Divergence가 발생하는 주요 상황</h2>
<h3 id="조건부-분기">조건부 분기</h3>
<p>스레드 ID를 기반으로 한 분기는 Warp Divergence를 일으키는 전형적인 사례입니다. 예를 들어, 스레드 ID가 16보다 작으면 처리 A를 수행하고, 그렇지 않으면 처리 B를 수행하는 코드는 Warp 내에서 스레드들이 서로 다른 경로를 따르게 만듭니다.</p>
<pre><code class="language-glsl">// 나쁜 예: 스레드 ID 기반 분기
if (threadID &lt; 16) {
    // 처리 A
} else {
    // 처리 B
}</code></pre>
<h3 id="루프의-불균등한-반복">루프의 불균등한 반복</h3>
<p>각 스레드가 서로 다른 횟수만큼 루프를 반복하는 경우도 문제가 됩니다. 스레드마다 다른 반복 횟수를 가진 루프는 일부 스레드가 먼저 끝나고 나머지를 기다리게 만들어 전체 성능을 저하시킵니다.</p>
<pre><code class="language-glsl">// 나쁜 예: 스레드마다 다른 반복 횟수
for (int i = 0; i &lt; threadID; i++) {
    doWork();
}</code></pre>
<h3 id="동적-배열-인덱싱과-메모리-접근-패턴">동적 배열 인덱싱과 메모리 접근 패턴</h3>
<p>스레드마다 다른 메모리 접근 패턴을 가지는 경우도 성능 문제를 일으킵니다. 복잡한 계산을 통해 배열 인덱스를 결정하는 코드는 <strong>메모리 coalescing</strong>을 방해하여 메모리 접근 효율을 크게 떨어뜨립니다.</p>
<p>이는 Warp Divergence와는 다른 성능 이슈이지만, 종종 함께 발생합니다. Warp 내 스레드들이 연속되지 않은 메모리 주소에 접근하면 여러 번의 메모리 트랜잭션이 필요하게 되어 대역폭이 낭비됩니다.</p>
<pre><code class="language-glsl">// 나쁜 예: 스레드마다 다른 메모리 접근 패턴
int index = someComplexCalculation(threadID);
result = data[index];</code></pre>
<h3 id="early-return과-discard">Early Return과 Discard</h3>
<p>프래그먼트 셰이더에서 일부 픽셀만 폐기하는 경우도 Divergence를 발생시킵니다. alpha 값에 따라 일부 픽셀은 discard되고 나머지는 계속 처리되는 상황에서, 같은 Warp 내의 스레드들이 서로 다른 경로를 따르게 됩니다.</p>
<pre><code class="language-glsl">// 프래그먼트 셰이더에서
if (alpha &lt; 0.1) {
    discard; // 일부 픽셀만 폐기
}</code></pre>
<h2 id="해결책과-베스트-프랙티스">해결책과 베스트 프랙티스</h2>
<h3 id="unity-shader에서-branch-속성-사용하기">Unity Shader에서 [branch] 속성 사용하기</h3>
<p>Unity에서는 HLSL 셰이더에 [branch] 속성을 추가하여 컴파일러에게 동적 분기를 사용하도록 명시적으로 지시할 수 있습니다. 이는 Warp Divergence를 관리하는 중요한 도구입니다.</p>
<pre><code>// Unity HLSL 셰이더에서
[branch]
if (complexCondition) {
    // 비용이 큰 계산
    result = expensiveComputation();
} else {
    // 간단한 계산
    result = simpleValue;
}</code></pre><p>[branch] 속성을 사용하는 이유는 다음과 같습니다.</p>
<p>첫째, 조건부 계산 비용이 매우 클 때 유용합니다. 한쪽 분기의 계산 비용이 매우 크고 다른 쪽은 매우 간단한 경우, Divergence로 인한 성능 손실보다 불필요한 계산을 건너뛰는 것이 더 이득일 수 있습니다.</p>
<p>둘째, 대부분의 스레드가 같은 경로를 따를 것으로 예상될 때 효과적입니다. 예를 들어 90% 이상의 픽셀이 동일한 분기를 실행한다면, 나머지 10%의 Divergence는 전체 성능에 큰 영향을 미치지 않습니다.</p>
<p>반대로 [flatten] 속성도 있는데, 이는 컴파일러에게 분기를 제거하고 두 경로를 모두 계산한 후 결과를 선택하도록 지시합니다.</p>
<pre><code>[flatten]
if (condition) {
    result = valueA;
} else {
    result = valueB;
}

// 컴파일러는 이를 다음과 같이 변환
result = condition ? valueA : valueB;</code></pre><p>[flatten]은 각 분기의 계산 비용이 비슷하고 간단할 때 사용하는 것이 좋습니다. 이 경우 Divergence를 피하기 위해 두 경로를 모두 계산하는 오버헤드가 분기로 인한 손실보다 작습니다.</p>
<p>일반적인 가이드라인은 다음과 같습니다. 분기 내 계산이 매우 간단하면 [flatten]을 사용하고, 계산이 복잡하고 비용이 크면 [branch]를 사용합니다. 확실하지 않은 경우 프로파일링을 통해 실제 성능을 측정하여 결정하는 것이 가장 좋습니다. 유니티 컴파일러는 이 속성들을 어떻게 처리할까?</p>
<p>Unity는 내부적으로 셰이더 컴파일러를 사용하여 HLSL 코드를 GPU가 실행할 수 있는 형태로 변환합니다. <code>[branch]</code>와 <code>[flatten]</code> 속성이 처리되는 과정을 개념적으로 이해해보겠습니다.</p>
<p><strong>1단계: 속성 인식</strong></p>
<p>컴파일러가 셰이더 코드를 읽을 때 <code>[branch]</code>나 <code>[flatten]</code> 같은 특수 키워드를 발견하면, 이를 내부적으로 사용할 상수값으로 변환합니다.</p>
<pre><code>// 개념적 동작
if (속성 이름 == &quot;branch&quot;)
    → EatBranch 플래그로 변환

if (속성 이름 == &quot;flatten&quot;)
    → EatFlatten 플래그로 변환</code></pre><p><strong>2단계: if 문에 플래그 저장</strong></p>
<p>컴파일러가 <code>if</code> 문을 만나면, 앞에 어떤 속성이 붙어있는지 확인하고 해당 if 문 데이터에 표시를 남깁니다.</p>
<pre><code>// 개념적 동작
함수: if문에_속성_적용(if문, 속성들):
    속성들을 순회:
        만약 속성이 EatFlatten이면:
            if문.평탄화_가능 = true
        만약 속성이 EatBranch이면:
            if문.평탄화_금지 = true</code></pre><p>이는 마치 if 문에 붙이는 포스트잇 같은 역할을 합니다:</p>
<ul>
<li><code>[flatten]</code> → &quot;이 조건문은 평탄화해도 좋아요&quot; 메모</li>
<li><code>[branch]</code> → &quot;이 조건문은 진짜 분기로 남겨주세요&quot; 메모</li>
</ul>
<p><strong>3단계: 내부 데이터 구조</strong></p>
<p>if 문을 표현하는 내부 데이터 구조에는 플래그를 저장하는 변수들이 있습니다.</p>
<pre><code>// 개념적 구조
클래스 조건문노드:
    변수:
        평탄화_가능: 불리언
        평탄화_금지: 불리언

    메서드:
        평탄화_설정() { 평탄화_가능 = true }
        평탄화_금지_설정() { 평탄화_금지 = true }</code></pre><p>각 if 문은 두 개의 스위치를 가지고 있어서, 어떤 최적화 힌트가 주어졌는지 기억합니다.</p>
<p><strong>4단계: GPU 중간 언어로 변환</strong></p>
<p>셰이더를 GPU가 이해할 수 있는 중간 언어(SPIR-V)로 변환할 때, 저장해둔 플래그를 제어 마스크로 변환합니다.</p>
<pre><code>// 개념적 동작
함수: 제어_마스크_변환(if문노드):
    만약 if문노드.평탄화_가능 == true:
        return 평탄화_마스크
    만약 if문노드.평탄화_금지 == true:
        return 평탄화금지_마스크
    그렇지 않으면:
        return 기본_마스크</code></pre><p>이 과정에서 우리가 붙여둔 &quot;포스트잇&quot;을 GPU가 이해할 수 있는 언어로 번역합니다.</p>
<p><strong>5단계: 실제 GPU 명령어 생성</strong></p>
<p>최종적으로 if 문을 처리할 때 변환된 제어 마스크가 GPU 명령어에 포함됩니다.</p>
<pre><code>// 개념적 동작
제어_정보 = 제어_마스크_변환(현재_if문)
GPU_명령어_생성(조건, 제어_정보, 빌더)</code></pre><p><code>제어_정보</code>에 담긴 힌트(Flatten, DontFlatten, 또는 None)가 GPU 드라이버에게 전달됩니다.</p>
<h3 id="결과적으로-어떻게-동작하는가">결과적으로 어떻게 동작하는가?</h3>
<p>전체 흐름을 정리하면:</p>
<ol>
<li>여러분이 셰이더에 <code>[branch]</code> 또는 <code>[flatten]</code>을 작성</li>
<li>컴파일러가 이를 인식하여 내부 플래그로 저장</li>
<li>GPU 중간 언어로 변환할 때 제어 마스크로 변환</li>
<li>GPU 드라이버가 이 힌트를 보고 최적화 결정</li>
</ol>
<p><strong>핵심은 &quot;힌트&quot;라는 점</strong>입니다. 이 속성들은 명령이 아니라 제안입니다. GPU 드라이버는 다음과 같은 요소들을 종합적으로 고려합니다:</p>
<ul>
<li>하드웨어 특성 (모바일 GPU vs 데스크탑 GPU)</li>
<li>분기 내부 코드의 복잡도</li>
<li>레지스터 사용량</li>
<li>다른 최적화 기회들</li>
</ul>
<p>그래서 <code>[flatten]</code>을 붙였는데도 GPU가 &quot;아니, 이 경우엔 분기가 더 나을 것 같은데?&quot;라고 판단하면 평탄화하지 않을 수 있습니다. 반대로 <code>[branch]</code>를 붙였는데도 GPU가 &quot;이건 평탄화하는 게 낫겠어&quot;라고 판단할 수도 있습니다.</p>
<p>따라서 <strong>실제 타겟 플랫폼에서 프로파일링하는 것이 필수</strong>입니다. 같은 코드라도 Android, iOS, PC, PlayStation에서 각각 다르게 최적화될 수 있기 때문입니다.</p>
<h3 id="조건문을-수학-연산으로-변환하기">조건문을 수학 연산으로 변환하기</h3>
<p>가능한 경우 조건문 대신 수학 연산을 사용하는 것이 좋습니다. GPU는 수학 연산을 병렬로 처리하는 데 매우 효율적이므로, 조건문을 수학 함수로 대체하면 Divergence를 완전히 피할 수 있습니다.</p>
<p>예를 들어, 값이 0보다 크면 그대로 사용하고 그렇지 않으면 0을 사용하는 로직은 max 함수로 간단히 대체할 수 있습니다.</p>
<pre><code class="language-glsl">// 나쁜 예
float result;
if (value &gt; 0.0) {
    result = value;
} else {
    result = 0.0;
}

// 좋은 예
float result = max(value, 0.0);</code></pre>
<p>또한 boolean을 float로 캐스팅하는 방식으로 조건부 값을 직접 계산할 수 있습니다.</p>
<pre><code class="language-glsl">// 나쁜 예
float factor;
if (condition) {
    factor = 1.0;
} else {
    factor = 0.0;
}

// 좋은 예
float factor = float(condition);</code></pre>
<h3 id="균일한-분기-사용하기">균일한 분기 사용하기</h3>
<p>Warp 내 모든 스레드가 같은 경로를 따르도록 조건을 설계해야 합니다. 스레드 ID 기반 분기 대신 uniform 변수를 사용한 분기는 모든 스레드가 동일한 경로를 따르므로 Divergence가 발생하지 않습니다.</p>
<pre><code class="language-glsl">// 나쁜 예: threadID 기반 분기
if (threadID % 2 == 0) {
    // ...
}

// 좋은 예: uniform 변수 기반 분기
uniform bool useAlternativeMethod;
if (useAlternativeMethod) {
    // 모든 스레드가 같은 경로
}</code></pre>
<h3 id="데이터-구조-재구성하기">데이터 구조 재구성하기</h3>
<p>작업을 사전에 분류하여 같은 처리가 필요한 데이터끼리 모아두는 방식도 효과적입니다. 혼합된 데이터를 처리하는 대신 각 타입별로 별도의 패스로 나누어 처리하면, 각 패스 내에서 모든 스레드가 같은 작업을 수행하게 됩니다.</p>
<p>컴퓨트 셰이더에서 이 접근법을 적용하면 다음과 같습니다. 하나의 루프에서 타입을 검사하며 분기하는 대신, TYPE_A만 처리하는 패스와 TYPE_B만 처리하는 패스로 나누는 것입니다.</p>
<pre><code class="language-glsl">// 나쁜 예: 혼합된 데이터
for (int i = 0; i &lt; dataCount; i++) {
    if (data[i].type == TYPE_A) {
        processA(data[i]);
    } else {
        processB(data[i]);
    }
}

// 좋은 예: 사전 분류된 데이터
// Pass 1: TYPE_A만 처리
for (int i = 0; i &lt; typeACount; i++) {
    processA(typeAData[i]);
}

// Pass 2: TYPE_B만 처리
for (int i = 0; i &lt; typeBCount; i++) {
    processB(typeBData[i]);
}</code></pre>
<h3 id="벡터-연산-활용하기">벡터 연산 활용하기</h3>
<p>SIMD 연산을 최대한 활용하여 조건문을 제거하는 것도 좋은 방법입니다. 각 컴포넌트마다 조건을 검사하는 대신, 모든 값을 계산한 후 mix 함수를 사용해 마스크를 적용할 수 있습니다.</p>
<pre><code class="language-glsl">// 나쁜 예
vec4 result;
if (mask.x) result.x = computeX();
if (mask.y) result.y = computeY();
if (mask.z) result.z = computeZ();
if (mask.w) result.w = computeW();

// 좋은 예 (각 compute 함수의 비용이 작을 때만 유효)
vec4 computed = vec4(computeX(), computeY(), computeZ(), computeW());
vec4 result = mix(vec4(0.0), computed, mask);</code></pre>
<p><strong>주의</strong>: 이 방식은 모든 compute 함수를 무조건 실행하므로, 각 함수의 계산 비용이 작을 때만 효과적입니다. 만약 compute 함수들이 매우 복잡하고 비용이 크다면, 조건부 실행의 이점을 완전히 잃게 되어 오히려 성능이 더 나빠질 수 있습니다.</p>
<h3 id="early-z-및-컬링-최적화">Early-Z 및 컬링 최적화</h3>
<p>프래그먼트 셰이더에서 discard를 피하고, 가능하면 하드웨어 컬링을 활용하는 것이 좋습니다.</p>
<p><strong>discard의 성능 문제</strong>:</p>
<ul>
<li>Warp Divergence를 발생시킵니다 (일부 픽셀만 폐기)</li>
<li><strong>Early-Z 최적화를 비활성화</strong>시켜 불필요한 프래그먼트 셰이더 실행을 유발합니다</li>
<li>이 두 가지 문제가 결합되어 심각한 성능 저하를 일으킬 수 있습니다</li>
</ul>
<p>discard를 사용해야 한다면 최대한 빨리 처리하여 불필요한 계산을 줄이는 것이 중요합니다.</p>
<pre><code class="language-glsl">// 나쁜 예
void main() {
    if (alpha &lt; 0.1) discard;
    // 복잡한 계산...
}

// 좋은 예: Alpha Testing을 파이프라인 설정으로 이동
// 또는 계산 전에 최대한 빨리 처리
void main() {
    // 최소한의 계산
    float alpha = texture(albedoMap, uv).a;
    if (alpha &lt; 0.1) discard; // 최대한 일찍 처리

    // 복잡한 계산은 그 이후
}</code></pre>
<h3 id="동적-분기를-정적-분기로-전환하기">동적 분기를 정적 분기로 전환하기</h3>
<p>가능하면 컴파일 타임에 결정되도록 하는 것이 좋습니다. 런타임 분기 대신 여러 셰이더 변형을 사용하면 각 모드별로 최적화된 코드 경로를 생성할 수 있습니다.</p>
<pre><code class="language-glsl">// 나쁜 예: 런타임 분기
uniform int shaderMode;
if (shaderMode == 0) {
    // Mode A
} else if (shaderMode == 1) {
    // Mode B
}

// 좋은 예: 여러 셰이더 변형 사용
// 각 모드별로 별도의 셰이더 컴파일</code></pre>
<h2 id="성능-측정-방법">성능 측정 방법</h2>
<h3 id="프로파일링-도구-활용">프로파일링 도구 활용</h3>
<p><strong>NVIDIA Nsight Graphics/Compute</strong>를 사용하면 다음 메트릭들을 확인할 수 있습니다:</p>
<ul>
<li><strong>Warp Execution Efficiency</strong>: Warp 내 활성 스레드 비율을 측정합니다</li>
<li><strong>Branch Efficiency</strong>: 분기문이 실제로 얼마나 효율적으로 실행되었는지 표시합니다</li>
<li><strong>SM Activity</strong>: Streaming Multiprocessor의 활용도를 확인합니다</li>
</ul>
<p><strong>RenderDoc</strong>과 <strong>AMD Radeon GPU Profiler</strong>도 유사한 메트릭을 제공하며, Warp의 실행 패턴을 시각화하고 병목 지점을 파악하는 데 도움을 줍니다.</p>
<p>GPU Occupancy를 확인하는 것도 좋은 방법입니다. Warp Divergence는 GPU Occupancy, 즉 활성 Warp 수를 감소시키므로 이 지표를 통해 간접적으로 문제를 파악할 수 있습니다.</p>
<p>무엇보다 최적화 전후의 프레임 타임을 직접 비교하며 벤치마크를 수행하는 것이 실질적인 성능 향상을 확인하는 가장 확실한 방법입니다.</p>
<h2 id="정리">정리</h2>
<p>Warp Divergence는 GPU 병렬 프로그래밍에서 피할 수 없는 현실이지만, 몇 가지 원칙을 따르면 크게 개선할 수 있습니다.</p>
<p>조건문을 수학 연산으로 대체하고, 균일한 실행 경로를 설계하며, 데이터 구조를 재구성하여 같은 작업끼리 그룹화하는 것이 기본입니다. 또한 벡터 연산을 적극 활용하고, 가능한 경우 정적 분기를 선호해야 합니다.</p>
<p>특히 셰이더에서는 모든 스레드가 같은 코드 경로를 따르도록 설계하는 것이 핵심입니다. 작은 최적화들이 모여 큰 성능 향상으로 이어질 수 있으므로, 이러한 원칙들을 실무에 적용해보시기 바랍니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[셰이더 최적화: 분기문 없이 버텍스 컬러 채널 선택하기]]></title>
            <link>https://velog.io/@mazeline_1973/%EC%85%B0%EC%9D%B4%EB%8D%94-%EC%B5%9C%EC%A0%81%ED%99%94-%EB%B6%84%EA%B8%B0%EB%AC%B8-%EC%97%86%EC%9D%B4-%EB%B2%84%ED%85%8D%EC%8A%A4-%EC%BB%AC%EB%9F%AC-%EC%B1%84%EB%84%90-%EC%84%A0%ED%83%9D%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@mazeline_1973/%EC%85%B0%EC%9D%B4%EB%8D%94-%EC%B5%9C%EC%A0%81%ED%99%94-%EB%B6%84%EA%B8%B0%EB%AC%B8-%EC%97%86%EC%9D%B4-%EB%B2%84%ED%85%8D%EC%8A%A4-%EC%BB%AC%EB%9F%AC-%EC%B1%84%EB%84%90-%EC%84%A0%ED%83%9D%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 19 Oct 2025 15:20:09 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>GPU에서 분기문은 비용이 비쌉니다. 특히 모바일이나 타일 기반 렌더러에서는 더욱 그렇습니다. 이번 글에서는 if/else 분기문을 원-핫 마스크와 dot product를 활용해 완전히 제거하는 최적화 기법을 소개합니다.</p>
</blockquote>
<h2 id="문제-상황">문제 상황</h2>
<p>아웃라인 셰이더를 작성할 때 버텍스 컬러의 특정 채널(R, G, B, A)을 선택해야 하는 경우가 있습니다. 예를 들어, 아티스트가 버텍스 컬러의 R 채널에는 아웃라인 두께를, G, B, A 채널에는 다른 속성을 저장했다면, 머티리얼 프로퍼티로 어떤 채널을 사용할지 선택할 수 있어야 합니다.</p>
<h2 id="기존-방식-분기문-사용">기존 방식: 분기문 사용</h2>
<p>가장 직관적인 방법은 if/else 분기문을 사용하는 것입니다.</p>
<h3 id="프로퍼티-선언">프로퍼티 선언</h3>
<pre><code>[Enum(R,0,G,1,B,2,A,3)]_OutlineVertexColorChannel(&quot;Outline Vertex Color Channel&quot;, Float) = 0</code></pre><h3 id="분기문을-사용한-채널-선택">분기문을 사용한 채널 선택</h3>
<aside>

<p>예제는 이해를 돕기 위해 하드 코딩된 것입니다.</p>
</aside>

<pre><code>float outlineVertexColorMask = 0.0;
if (_OutlineVertexColorChannel == 0)
    outlineVertexColorMask = vertexColor.r;
else if (_OutlineVertexColorChannel == 1)
    outlineVertexColorMask = vertexColor.g;
else if (_OutlineVertexColorChannel == 2)
    outlineVertexColorMask = vertexColor.b;
else if (_OutlineVertexColorChannel == 3)
    outlineVertexColorMask = vertexColor.a;</code></pre><h3 id="문제점">문제점</h3>
<p>런타임 분기(동적 분기)는 GPU 파이프라인에서 큰 성능 비용을 발생시킵니다. GPU는 여러 쓰레드를 묶어서(Warp) 같은 명령어를 실행하는데, 분기문이 있으면 각 쓰레드가 다른 경로를 실행할 수 있어 성능이 저하됩니다. 이를 <strong>Warp Divergence</strong>라고 합니다. 또한 분기 조건을 평가하고 점프하는 과정에서 파이프라인이 정지될 수 있으며, 특히 타일 기반 렌더러를 사용하는 모바일 GPU에서는 분기문의 비용이 더 큽니다.</p>
<hr>
<h2 id="최적화-방법-원-핫-마스크--dot-product">최적화 방법: 원-핫 마스크 + Dot Product</h2>
<p>분기문 없이 벡터 연산만으로 채널을 선택할 수 있습니다.</p>
<pre><code>// _OutlineVertexColorChannel: float 또는 int (0,1,2,3)
float c = (float)_OutlineVertexColorChannel;

// 0,1,2,3과의 거리를 이용해 원-핫 마스크 생성 (분기 없음)
float4 mask = saturate(1.0 - abs(float4(0.0, 1.0, 2.0, 3.0) - c));

// vertexColor: float4/half4 (r,g,b,a)
float outlineVertexColorMask = dot(vertexColor, mask);</code></pre><hr>
<h2 id="동작-원리">동작 원리</h2>
<h3 id="1-채널-인덱스">1. 채널 인덱스</h3>
<p><code>c</code> 변수는 선택할 채널을 나타냅니다. c 값이 0이면 빨강(Red) 채널을, 1이면 초록(Green) 채널을, 2이면 파랑(Blue) 채널을, 3이면 알파(Alpha) 채널을 선택합니다.</p>
<h3 id="2-원-핫-마스크-생성">2. 원-핫 마스크 생성</h3>
<pre><code>float4 mask = saturate(1.0 - abs(float4(0.0, 1.0, 2.0, 3.0) - c));</code></pre><p>이 한 줄의 코드가 핵심입니다. 하나의 성분만 1.0이고 나머지는 0.0인 마스크를 생성합니다.</p>
<h3 id="단계별-계산-예시-c--1일-때">단계별 계산 예시 (c = 1일 때)</h3>
<p><strong>Step 1</strong>: 각 인덱스와의 차이 계산</p>
<pre><code>float4(0.0, 1.0, 2.0, 3.0) - 1.0 = float4(-1.0, 0.0, 1.0, 2.0)</code></pre><p><strong>Step 2</strong>: 절댓값 계산</p>
<pre><code>abs(float4(-1.0, 0.0, 1.0, 2.0)) = float4(1.0, 0.0, 1.0, 2.0)</code></pre><p><strong>Step 3</strong>: 1에서 빼기</p>
<pre><code>1.0 - float4(1.0, 0.0, 1.0, 2.0) = float4(0.0, 1.0, 0.0, -1.0)</code></pre><p><strong>Step 4</strong>: saturate로 0~1 범위로 클램핑</p>
<pre><code>saturate(float4(0.0, 1.0, 0.0, -1.0)) = float4(0.0, 1.0, 0.0, 0.0)</code></pre><p>결과적으로 c = 1일 때 mask = (0, 1, 0, 0)이 생성됩니다. 같은 방식으로 c = 0일 때는 (1, 0, 0, 0), c = 2일 때는 (0, 0, 1, 0), c = 3일 때는 (0, 0, 0, 1)의 마스크가 만들어집니다.</p>
<h3 id="3-채널-선택">3. 채널 선택</h3>
<pre><code>float outlineVertexColorMask = dot(vertexColor, mask);</code></pre><p>내적(dot product)을 통해 각 성분을 곱하고 더합니다.</p>
<pre><code>dot(vertexColor, mask) = vertexColor.r * mask.r 
                        + vertexColor.g * mask.g 
                        + vertexColor.b * mask.b 
                        + vertexColor.a * mask.a</code></pre><p>마스크에서 하나의 성분만 1.0이므로, 결과적으로 해당 채널의 값만 추출됩니다.</p>
<p>예를 들어 mask = (0, 1, 0, 0)이면:</p>
<pre><code>= vertexColor.r * 0 + vertexColor.g * 1 + vertexColor.b * 0 + vertexColor.a * 0
= vertexColor.g</code></pre><hr>
<h2 id="장점">장점</h2>
<h3 id="gpu-친화적">GPU 친화적</h3>
<p>분기문(if/else)이 없어서 Warp Divergence를 완전히 회피합니다. 모든 쓰레드가 동일한 명령어를 실행하기 때문에 GPU의 병렬 처리 효율이 최대화됩니다.</p>
<h3 id="벡터화-연산">벡터화 연산</h3>
<p>GPU는 벡터 연산에 최적화되어 있습니다. 이 기법은 SIMD(Single Instruction Multiple Data) 연산을 효율적으로 활용하여 하드웨어의 성능을 최대한 끌어낼 수 있습니다.</p>
<h3 id="유연성">유연성</h3>
<p>런타임에 <code>_OutlineVertexColorChannel</code> 값만 바꾸면 쉽게 채널을 변경할 수 있습니다. 컴파일 타임 분기나 shader variant를 생성할 필요 없이 동적으로 채널을 선택할 수 있어 메모리 효율도 좋습니다.</p>
<h3 id="간결함">간결함</h3>
<p>4개의 if/else 분기가 단 3줄의 벡터 연산으로 대체됩니다. 코드가 간결해지면서도 성능은 오히려 향상됩니다.</p>
<hr>
<h2 id="활용-사례">활용 사례</h2>
<p>이 기법은 버텍스 컬러 채널에서 아웃라인 두께나 디졸브 마스크, AO 같은 데이터를 선택할 때 유용합니다. 또한 멀티채널 마스크 텍스처에서 특정 채널을 추출하거나, 런타임에 여러 옵션 중 하나를 선택해야 할 때도 활용할 수 있습니다.</p>
<hr>
<h2 id="정리">정리</h2>
<table>
<thead>
<tr>
<th>구분</th>
<th>분기문 방식</th>
<th>원-핫 마스크 방식</th>
</tr>
</thead>
<tbody><tr>
<td>코드 길이</td>
<td>8줄+</td>
<td>3줄</td>
</tr>
<tr>
<td>분기문</td>
<td>4개</td>
<td>0개</td>
</tr>
<tr>
<td>GPU 효율</td>
<td>낮음 (Warp Divergence)</td>
<td>높음 (벡터 연산)</td>
</tr>
<tr>
<td>가독성</td>
<td>직관적</td>
<td>수학적</td>
</tr>
</tbody></table>
<p>GPU 셰이더 최적화의 핵심은 &quot;분기를 피하고 벡터 연산을 활용하는 것&quot;입니다. 원-핫 마스크 기법은 이 원칙을 잘 보여주는 실용적인 예시입니다.</p>
<h3 id="성능-특성-분석">성능 특성 분석</h3>
<p>원-핫 마스크 방식이 분기문 방식보다 우수한 성능을 보이는 이유는 다음과 같습니다.</p>
<p><strong>Warp Divergence 제거</strong></p>
<p>분기문 방식에서는 서로 다른 쓰레드가 서로 다른 분기를 실행할 때 GPU가 각 분기를 순차적으로 처리해야 합니다. 예를 들어 Warp 내 32개의 쓰레드 중 8개는 if (_OutlineVertexColorChannel == 0)을, 다른 8개는 == 1을 실행한다면, GPU는 4개의 분기를 모두 순차적으로 실행하고 각 쓰레드에 해당하는 결과만 활성화해야 합니다. 이로 인해 실제 실행 시간이 최대 4배까지 늘어날 수 있습니다.</p>
<p><strong>SIMD 연산 최대 활용</strong></p>
<p>원-핫 마스크 방식은 벡터 연산(abs, saturate, dot)만을 사용하므로 모든 쓰레드가 동일한 명령어를 동시에 실행합니다. 예를 들어 Mali GPU는 하나의 클럭 사이클에 최대 16개의 FP32 연산을 병렬로 처리할 수 있는데, 분기문이 없으면 이 하드웨어 성능을 온전히 활용할 수 있습니다.</p>
<p><strong>레지스터 효율성</strong></p>
<p>분기문 방식에서는 각 분기의 결과를 저장하기 위해 추가 레지스터가 필요하지만, 원-핫 마스크 방식은 중간 결과(mask)를 즉시 소비하므로 레지스터 사용량이 적습니다. 이는 특히 복잡한 셰이더에서 occupancy(동시 실행 가능한 warp 수)를 높이는 데 도움이 됩니다.</p>
<h3 id="주의사항-및-제약">주의사항 및 제약</h3>
<p><strong>입력 값 범위</strong></p>
<p>ChannelIndex 값은 반드시 0에서 3 사이의 정수여야 합니다. 범위를 벗어나는 값이 입력되면 예상하지 못한 결과가 나올 수 있습니다. ShaderGraph에서는 프로퍼티 범위를 0-3으로 제한하여 이를 방지할 수 있습니다.</p>
<p><strong>컴파일러 최적화 고려사항</strong></p>
<p>현대 셰이더 컴파일러는 uniform 변수 기반 분기를 어느 정도 최적화할 수 있습니다. 하지만 이러한 최적화는 GPU 벤더, 드라이버 버전, 셰이더 복잡도에 따라 달라질 수 있습니다. 원-핫 마스크 방식은 컴파일러 최적화에 의존하지 않고 명시적으로 분기를 제거하므로 더 예측 가능한 성능을 제공합니다.</p>
<p><strong>명령어 수와 실제 성능</strong></p>
<p>원-핫 마스크 방식은 abs, saturate, dot 등 여러 명령어를 사용하므로 단순 명령어 수만 보면 분기문 방식보다 많을 수 있습니다. 그러나 GPU에서는 명령어 수보다 병렬 실행 효율이 더 중요합니다. 분기문으로 인한 직렬화 비용이 추가 벡터 연산 비용보다 훨씬 크기 때문에 전체적으로는 원-핫 마스크 방식이 더 빠릅니다.</p>
<p><strong>성능 향상 정도</strong></p>
<p>실제 성능 향상 정도는 GPU 아키텍처, 셰이더 컴파일러, 전체 셰이더 복잡도, 그리고 Warp 내에서 얼마나 다양한 채널 값이 사용되는지에 따라 달라집니다. 모바일 GPU에서는 일반적으로 더 큰 성능 차이를 보이며, 데스크톱 GPU에서는 상대적으로 차이가 작을 수 있습니다.</p>
<p>실제 성능 향상 정도는 GPU 아키텍처, 셰이더 컴파일러, 전체 셰이더 복잡도, 그리고 Warp 내에서 얼마나 다양한 채널 값이 사용되는지에 따라 달라집니다. 모바일 GPU에서는 일반적으로 더 큰 성능 차이를 보이며, 데스크톱 GPU에서는 상대적으로 차이가 작을 수 있습니다.</p>
<hr>
<h2 id="gpu-벤더별-동작-특성">GPU 벤더별 동작 특성</h2>
<p>원-핫 마스크 최적화는 모든 GPU 아키텍처에서 효과적이지만, 벤더별로 특성이 다릅니다.</p>
<h3 id="nvidia-gpu-warp-기반">NVIDIA GPU (Warp 기반)</h3>
<p>NVIDIA GPU는 32개의 쓰레드를 하나의 Warp로 묶어 처리합니다. 분기문이 있을 때 Warp 내에서 서로 다른 경로를 실행하는 쓰레드가 있다면 각 경로를 순차적으로 실행해야 합니다. 예를 들어 Warp 내 8개 쓰레드가 각각 다른 채널을 선택한다면 4개의 분기를 모두 실행해야 하므로 최악의 경우 4배의 시간이 걸립니다.</p>
<p>원-핫 마스크 방식은 모든 쓰레드가 동일한 벡터 연산을 수행하므로 Warp Divergence가 발생하지 않습니다. NVIDIA의 최신 Ampere와 Ada 아키텍처는 FP32 연산에 매우 최적화되어 있어 abs, saturate, dot 같은 벡터 연산을 효율적으로 처리합니다.</p>
<h3 id="amd-gpu-wavefront-기반">AMD GPU (Wavefront 기반)</h3>
<p>AMD GPU는 64개(RDNA 아키텍처는 32개)의 쓰레드를 하나의 Wavefront로 묶어 처리합니다. NVIDIA와 유사하게 Wavefront Divergence 문제가 발생할 수 있으며, 원-핫 마스크 방식을 통해 이를 회피할 수 있습니다.</p>
<p>AMD의 GCN과 RDNA 아키텍처는 벡터 ALU가 잘 발달되어 있어 SIMD 연산에 강점이 있습니다. 특히 dot 연산은 하드웨어 수준에서 최적화된 명령어로 컴파일되므로 매우 빠르게 실행됩니다.</p>
<h3 id="arm-mali-gpu-타일-기반">ARM Mali GPU (타일 기반)</h3>
<p>ARM Mali GPU는 타일 기반 렌더링을 사용하며, Warp 크기가 벤더에 따라 다릅니다(Mali-G78은 16 레인). 모바일 GPU는 전력 효율이 중요하므로 분기문으로 인한 추가 명령어 실행이 배터리 소모에 직접적인 영향을 미칩니다.</p>
<p>Mali GPU는 FP16(half) 연산에 특히 최적화되어 있습니다. 원-핫 마스크 방식을 half 정밀도로 구현하면 메모리 대역폭과 전력 소비를 더욱 줄일 수 있습니다.</p>
<h3 id="intel-gpu-eu-기반">Intel GPU (EU 기반)</h3>
<p>Intel GPU는 Execution Unit(EU) 기반 아키텍처를 사용하며, 각 EU는 여러 쓰레드를 SIMD 방식으로 처리합니다. Intel의 셰이더 컴파일러는 uniform 분기를 어느 정도 최적화하지만, 동적 분기에서는 여전히 성능 저하가 발생합니다.</p>
<p>최신 Arc GPU는 벡터 연산 처리 능력이 크게 향상되었으며, 원-핫 마스크 같은 수학적 기법을 효율적으로 실행할 수 있습니다.</p>
<hr>
<h2 id="컴파일된-어셈블리-비교">컴파일된 어셈블리 비교</h2>
<p>셰이더가 실제로 어떻게 컴파일되는지 확인하면 최적화 효과를 더 명확히 이해할 수 있습니다.</p>
<h3 id="분기문-방식의-어셈블리-mali-gpu-예시">분기문 방식의 어셈블리 (Mali GPU 예시)</h3>
<pre><code>// if (_OutlineVertexColorChannel == 0) 분기
TEQ      r0.x, #0.0
BEQ      .L_channel_0
TEQ      r0.x, #1.0
BEQ      .L_channel_1
TEQ      r0.x, #2.0
BEQ      .L_channel_2
B        .L_channel_3

.L_channel_0:
MOV      r1.x, r2.x    // vertexColor.r
B        .L_end

.L_channel_1:
MOV      r1.x, r2.y    // vertexColor.g
B        .L_end

.L_channel_2:
MOV      r1.x, r2.z    // vertexColor.b
B        .L_end

.L_channel_3:
MOV      r1.x, r2.w    // vertexColor.a

.L_end:
// 계속...</code></pre><p>분기문 방식은 여러 비교 명령어(TEQ)와 분기 명령어(BEQ, B)를 사용합니다. Warp 내에서 다른 경로를 실행하는 쓰레드가 있으면 모든 경로를 순차적으로 실행해야 합니다.</p>
<h3 id="원-핫-마스크-방식의-어셈블리-mali-gpu-예시">원-핫 마스크 방식의 어셈블리 (Mali GPU 예시)</h3>
<pre><code>// float4(0.0, 1.0, 2.0, 3.0) - c
VMOV     r3, {0.0, 1.0, 2.0, 3.0}
VSUB     r3, r3, r0.xxxx

// abs(...)
VABS     r3, r3

// 1.0 - abs(...)
VMOV     r4, {1.0, 1.0, 1.0, 1.0}
VSUB     r3, r4, r3

// saturate(...)
VMAX     r3, r3, #0.0
VMIN     r3, r3, #1.0

// dot(vertexColor, mask)
VDOT     r1.x, r2, r3</code></pre><p>원-핫 마스크 방식은 분기 없이 벡터 연산만을 사용합니다. 모든 명령어가 SIMD로 실행되며, Warp 내 모든 쓰레드가 동일한 경로를 따릅니다.</p>
<h3 id="명령어-수-비교">명령어 수 비교</h3>
<p>단순 명령어 수만 보면 분기문 방식(약 10-15개)과 원-핫 마스크 방식(약 7-8개)이 비슷하거나 원-핫이 적을 수도 있습니다. 하지만 실제 실행 시간은 크게 다릅니다. 분기문 방식은 Warp Divergence 발생 시 최악의 경우 4배까지 늘어날 수 있지만, 원-핫 마스크 방식은 항상 일정한 시간에 실행됩니다.</p>
<h3 id="renderdoc-또는-nsight-graphics로-확인하기">RenderDoc 또는 Nsight Graphics로 확인하기</h3>
<p>실제 프로젝트에서 어셈블리 코드를 확인하려면 다음 도구를 사용할 수 있습니다.</p>
<p><strong>NVIDIA (Nsight Graphics)</strong>: 셰이더 디버거에서 SASS(실제 GPU 어셈블리) 코드를 볼 수 있습니다. Warp 실행 통계와 함께 분기로 인한 divergence를 시각적으로 확인할 수 있습니다.</p>
<p><strong>AMD (RenderDoc + Radeon GPU Profiler)</strong>: RenderDoc으로 셰이더를 캡처하고 RGP에서 Wavefront occupancy와 ALU 사용률을 분석할 수 있습니다.</p>
<p><strong>Mali (Mali Offline Compiler)</strong>: 커맨드라인 도구로 셰이더를 컴파일하여 명령어 수, 레지스터 사용량, 사이클 추정치를 확인할 수 있습니다.</p>
<pre><code class="language-bash">malisc --core Mali-G78 --vertex shader.vert --fragment shader.frag</code></pre>
<hr>
<aside>

<p><strong>참고</strong>: 이 기법은 Unity URP/HDRP, Unreal Engine의 커스텀 셰이더 노드, ShaderGraph의 커스텀 함수 노드 등 어디에서나 활용할 수 있습니다. 특히 모바일 프로젝트나 VR/AR 프로젝트처럼 성능이 중요한 경우 이러한 미세 최적화가 누적되어 큰 차이를 만들어냅니다.</p>
</aside>

<hr>
<h2 id="shadergraph-커스텀-함수-예제">ShaderGraph 커스텀 함수 예제</h2>
<p>Unity ShaderGraph에서 커스텀 함수 노드를 사용하여 이 최적화 기법을 적용할 수 있습니다. 먼저 ShaderGraph에서 우클릭한 후 Create Node → Custom Function을 선택하여 새로운 커스텀 함수 노드를 생성합니다.</p>
<h3 id="함수-기본-설정">함수 기본 설정</h3>
<p>생성된 커스텀 함수 노드의 이름을 <code>SelectVertexColorChannel</code>로 지정하고, Type은 <code>String</code>으로 설정합니다.</p>
<h3 id="입력-및-출력-파라미터-설정">입력 및 출력 파라미터 설정</h3>
<p>입력 파라미터로는 <code>VertexColor</code>(Vector4 타입)와 <code>ChannelIndex</code>(Float 타입)가 필요합니다. VertexColor는 버텍스 컬러 데이터를 받아오고, ChannelIndex는 선택할 채널의 인덱스를 나타냅니다(0은 R, 1은 G, 2는 B, 3은 A 채널). 출력 파라미터는 <code>Out</code>(Float 타입)으로 설정하여 선택된 채널의 값을 반환합니다.</p>
<h3 id="함수-본문-코드">함수 본문 코드</h3>
<pre><code>void SelectVertexColorChannel_float(float4 VertexColor, float ChannelIndex, out float Out)
{
    float c = ChannelIndex;
    float4 mask = saturate(1.0 - abs(float4(0.0, 1.0, 2.0, 3.0) - c));
    Out = dot(VertexColor, mask);
}</code></pre><p>이 코드는 앞서 설명한 원-핫 마스크 기법을 그대로 구현한 것입니다. ChannelIndex 값에 따라 자동으로 해당하는 채널만 1.0으로 설정된 마스크를 생성하고, 내적 연산을 통해 원하는 채널의 값을 추출합니다.</p>
<h3 id="모바일-최적화-버전">모바일 최적화 버전</h3>
<p>모바일 플랫폼에서 더 나은 성능을 원한다면 half 정밀도 버전을 추가로 작성할 수 있습니다. half 정밀도는 float보다 메모리와 연산량이 적어 모바일 GPU에서 더 효율적입니다.</p>
<pre><code>void SelectVertexColorChannel_half(half4 VertexColor, half ChannelIndex, out half Out)
{
    half c = ChannelIndex;
    half4 mask = saturate(1.0 - abs(half4(0.0, 1.0, 2.0, 3.0) - c));
    Out = dot(VertexColor, mask);
}</code></pre><h3 id="shadergraph에서-노드-연결하기">ShaderGraph에서 노드 연결하기</h3>
<p>커스텀 함수를 생성한 후 ShaderGraph에서 실제로 사용하려면 몇 가지 노드를 연결해야 합니다. 먼저 Vertex Color 노드를 추가하고 그 출력을 커스텀 함수의 VertexColor 입력에 연결합니다. 그다음 Float 타입의 머티리얼 프로퍼티를 생성하여(예를 들어 &quot;Outline Channel&quot;이라는 이름으로) 커스텀 함수의 ChannelIndex 입력에 연결합니다. 마지막으로 커스텀 함수의 Out 출력을 원하는 곳에 연결하면 됩니다. 예를 들어 아웃라인 두께를 조절하는 Multiply 노드에 연결할 수 있습니다.</p>
<hr>
<p>이 글은 메이즈라인 프러덕션의 실제 프로젝트 경험과 기술 연구를 토대로 작성되었습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[언리얼 엔진의 !! 연산자 (Double Negation Operator)
]]></title>
            <link>https://velog.io/@mazeline_1973/%EC%96%B8%EB%A6%AC%EC%96%BC-%EC%97%94%EC%A7%84%EC%9D%98-%EC%97%B0%EC%82%B0%EC%9E%90-Double-Negation-Operator</link>
            <guid>https://velog.io/@mazeline_1973/%EC%96%B8%EB%A6%AC%EC%96%BC-%EC%97%94%EC%A7%84%EC%9D%98-%EC%97%B0%EC%82%B0%EC%9E%90-Double-Negation-Operator</guid>
            <pubDate>Wed, 15 Oct 2025 09:31:54 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<p>최근에는 엔진 코드 수정 할 일이 거의 없어서 잊고 있다가 문뜩 생각나서 정리 했습니다. 언리얼 엔진 코드를 리뷰하다 보면 <code>!!</code> 연산자를 종종 발견할 수 있습니다. 이 연산자는 정수나 다른 값을 명시적으로 <code>bool</code> 타입으로 변환할 때 사용되는 패턴입니다.</p>
<h2 id="실제-사용-예제">실제 사용 예제</h2>
<p>언리얼 엔진 코드에서 실제로 사용되는 예제를 살펴보겠습니다.</p>
<pre><code class="language-cpp">const bool bFoliageDiscardOnLoad = !!CVarFoliageDiscardDataOnLoad.GetValueOnGameThread();</code></pre>
<p>위 코드는 다음과 같이 리팩토링할 수 있습니다.</p>
<pre><code class="language-cpp">const bool bShouldDiscardFoliageDataOnLoad = CVarFoliageDiscardDataOnLoad.GetValueOnGameThread() != 0;</code></pre>
<h2 id="-연산자의-동작-원리">!! 연산자의 동작 원리</h2>
<p><code>!!</code> 연산은 <strong>두 번의 논리 부정</strong>을 수행합니다.</p>
<pre><code class="language-cpp">const bool bFlag = !!SomeIntegerValue;</code></pre>
<ul>
<li><code>SomeIntegerValue</code>가 0이 아닌 경우 → <code>true</code></li>
<li><code>SomeIntegerValue</code>가 0인 경우 → <code>false</code></li>
</ul>
<h3 id="단계별-동작">단계별 동작</h3>
<ol>
<li>첫 번째 <code>!</code>: 값을 <code>bool</code>로 변환하고 반전</li>
<li>두 번째 <code>!</code>: 다시 반전하여 원래 논리값 유지</li>
</ol>
<h2 id="장점과-단점">장점과 단점</h2>
<h3 id="장점">장점</h3>
<p><strong>의도의 명확성</strong></p>
<p>&quot;이 값을 논리값으로 사용하려고 명시적으로 변환한다&quot;는 의도를 코드에서 명확하게 표현할 수 있습니다.</p>
<p><strong>코드 일관성</strong></p>
<p>언리얼 엔진 내부에서 일관되게 사용되는 패턴으로, 엔진 코드와 스타일을 맞출 수 있습니다.</p>
<h3 id="단점">단점</h3>
<p><strong>가독성 논란</strong></p>
<p>일부 개발자는 <code>!!</code> 표기법이 직관적이지 않다고 느낄 수 있습니다.</p>
<p><strong>대안 존재</strong></p>
<p>명시적 비교문이나 캐스팅을 선호하는 개발자도 있습니다.</p>
<h2 id="대안-방법">대안 방법</h2>
<h3 id="1-명시적-비교">1. 명시적 비교</h3>
<pre><code class="language-cpp">const bool bFlag = CVarValue != 0;</code></pre>
<h3 id="2-정적-캐스팅">2. 정적 캐스팅</h3>
<pre><code class="language-cpp">const bool bFlag = static_cast&lt;bool&gt;(Value);</code></pre>
<h3 id="3-c-스타일-캐스팅">3. C 스타일 캐스팅</h3>
<pre><code class="language-cpp">const bool bFlag = (bool)Value;</code></pre>
<h2 id="리팩토링-예제">리팩토링 예제</h2>
<h3 id="변경-전">변경 전</h3>
<pre><code class="language-cpp">const bool bFoliageDiscardOnLoad = !!CVarFoliageDiscardDataOnLoad.GetValueOnGameThread();</code></pre>
<h3 id="변경-후">변경 후</h3>
<pre><code class="language-cpp">const bool bShouldDiscardFoliageDataOnLoad = CVarFoliageDiscardDataOnLoad.GetValueOnGameThread() != 0;</code></pre>
<h3 id="개선-사항">개선 사항</h3>
<ul>
<li><strong>변수 이름 개선</strong>: <code>bFoliageDiscardOnLoad</code> → <code>bShouldDiscardFoliageDataOnLoad</code>로 변경하여 의도를 더 명확하게 표현</li>
<li><strong>문법 단순화</strong>: <code>!!</code> 연산자를 제거하고 명시적 비교로 대체하여 코드의 의도를 분명하게 함</li>
<li><strong>사용성 개선</strong>: 가독성과 코드의 맥락 이해도가 높아짐</li>
</ul>
<h2 id="언리얼-엔진-스타일-가이드-관점">언리얼 엔진 스타일 가이드 관점</h2>
<p>언리얼 엔진 개발자라면 <code>!!</code> 연산자 사용이 익숙할 수 있습니다. 그러나 프로젝트의 코딩 스타일 가이드에 따라 적절히 선택하는 것이 중요합니다.</p>
<h3 id="사용을-고려할-때">사용을 고려할 때</h3>
<ul>
<li>엔진 내부 코드와 일관성을 유지하고 싶을 때</li>
<li>팀원 모두가 이 패턴에 익숙할 때</li>
<li>간결함을 중시할 때</li>
</ul>
<h3 id="대안을-고려할-때">대안을 고려할 때</h3>
<ul>
<li>신입 개발자가 많은 프로젝트일 때</li>
<li>코드 리뷰에서 혼란이 자주 발생할 때</li>
<li>명시적인 코드 스타일을 선호할 때</li>
</ul>
<h2 id="결론">결론</h2>
<p><code>!!</code> 연산자는 언리얼 엔진 코드에서 <strong>간결함과 의도 전달</strong>의 목적으로 자주 사용됩니다. 두 가지 스타일(!! vs 명시적 비교) 모두 사용 가능하지만, 프로젝트의 코딩 스타일 가이드와 팀의 선호도를 고려하여 일관성 있게 사용하는 것이 중요합니다.</p>
<hr>
<p>작성일: 2025년 10월 15일
대상 독자: 초급 테크니컬 아티스트
난이도: 중급</p>
]]></description>
        </item>
    </channel>
</rss>