GPU Resident Drawer를 켰는데 콘솔에 이런 에러가 떴다면, 이 포스팅을 위해 여기 온 겁니다.
A BatchDrawCommand is using the pass [PassName] from the shader [ShaderName]
which does not define a DOTS_INSTANCING_ON variant.
“뭔가 잘못됐는데 뭐가 잘못됐는지 모르겠다”는 상황. 커스텀 셰이더를 사용하는 프로젝트에서 GPU Resident Drawer를 도입하려면 반드시 넘어야 할 관문이 있습니다. DOTS 인스턴싱 지원입니다.
이름에 DOTS가 붙어 있어서 Entities 패키지를 써야 한다고 오해하기 쉬운데, 전혀 그렇지 않습니다. DOTS 인스턴싱은 셰이더 레벨의 인스턴스 데이터 전달 방식이고, 일반 GameObject 기반 프로젝트에서도 그대로 적용됩니다.
GPU Resident Drawer 완전 분석 시리즈
1편 — GPU Resident Drawer 동작 원리 완전 분석
2편 — 커스텀 셰이더와 DOTS 인스턴싱 완전 정복 (현재 글)
목차
- DOTS 인스턴싱이란 무엇인가
- 커스텀 셰이더에 DOTS 인스턴싱 추가하기
- 1단계: 필수 pragma 선언
- 2단계: 프로퍼티 블록 선언
- 3단계: SRP Batcher 호환 유지
- 4단계: Instance ID 전파
- Project Settings 확인 항목
- 호환성 점검 방법
- 실무에서 자주 빠지는 함정들
DOTS 인스턴싱이란 무엇인가
둘을 혼동하기 쉬운데, 데이터가 어디에 있느냐가 본질적인 차이입니다.
기존 GPU 인스턴싱(UNITY_INSTANCING_BUFFER_START 방식)은 CPU가 매 프레임마다 인스턴스 데이터(트랜스폼, 머티리얼 프로퍼티 등)를 Constant Buffer에 패킹해서 GPU에 전달합니다. 배치당 최대 1,023개(Unity 내부 한도, 64KB cbuffer 범위 내에서 인스턴스당 프로퍼티 수에 따라 더 줄어들 수 있음)라는 제약이 있고, 데이터 전달 자체가 CPU 부하입니다.
DOTS 인스턴싱은 다릅니다. 인스턴스 데이터를 GPU 메모리의 GraphicsBuffer(ByteAddressBuffer)에 상주(Resident) 시켜 놓고, 셰이더에는 그 버퍼에서 어디를 읽을지 알려주는 32비트 메타데이터 값 하나만 넘깁니다. CPU는 데이터가 바뀔 때만 업로드하면 되고, 프레임마다 반복할 필요가 없습니다.
| 항목 | 기존 GPU 인스턴싱 | DOTS 인스턴싱 |
|---|---|---|
| 인스턴스 데이터 위치 | CPU → Constant Buffer (매 프레임) | GPU 메모리 상주 GraphicsBuffer |
| 셰이더로 전달하는 것 | 프로퍼티 배열 전체 | 32비트 메타데이터 값 1개 |
| 배치 크기 한도 | ~500개 (64KB 제한) | 수천 개 이상, 제한 없음 |
| SRP Batcher 호환 | 불가 | 가능 (동시 지원) |
| 선언 매크로 | UNITY_INSTANCING_BUFFER_START |
UNITY_DOTS_INSTANCING_START |
GPU Resident Drawer는 내부적으로 BatchRendererGroup(BRG) API를 사용하는데, BRG는 DOTS 인스턴싱만 지원합니다. 기존 GPU 인스턴싱 방식의 셰이더는 BRG에서 작동하지 않습니다.
그리고 여기서 한 가지를 더 기억해두어야 합니다. DOTS 인스턴싱 셰이더는 SRP Batcher와 동시에 호환되어야 합니다. SRP Batcher 호환 조건(UnityPerMaterial cbuffer 선언)도 함께 충족해야 정상 작동합니다. 이 두 조건을 동시에 맞추는 패턴이 이 글의 핵심입니다.
커스텀 셰이더에 DOTS 인스턴싱 추가하기
1단계: 필수 pragma 선언
모든 패스(Pass)에 다음 세 줄이 있어야 합니다.
#pragma exclude_renderers gles gles3 glcore // SM 4.5 미지원 플랫폼 제외
#pragma target 4.5 // DOTS 인스턴싱 최소 요구 셰이더 모델
#pragma multi_compile _ DOTS_INSTANCING_ON // DOTS 인스턴싱 변형 생성
#pragma multi_compile_instancing도 함께 선언해 두면 기존 GPU 인스턴싱 변형도 함께 생성됩니다. 두 경로를 모두 지원하는 셰이더를 만드는 게 일반적입니다.
#pragma multi_compile_instancing
#pragma instancing_options renderinglayer
#pragma multi_compile _ DOTS_INSTANCING_ON
주의: 이 pragma는 해당 셰이더의 모든 Pass에 추가해야 합니다. Forward 패스에만 추가하고 ShadowCaster, DepthOnly 패스에 빠뜨리면 그 패스에서 에러가 납니다. 에러 메시지에 패스 이름이 명시되니, 어느 패스가 문제인지는 금방 알 수 있습니다.
2단계: DOTS 인스턴싱 프로퍼티 블록 선언
#ifdef UNITY_DOTS_INSTANCING_ENABLED 가드 안에 프로퍼티 블록을 선언합니다.
#ifdef UNITY_DOTS_INSTANCING_ENABLED
UNITY_DOTS_INSTANCING_START(UserPropertyMetadata)
UNITY_DOTS_INSTANCED_PROP(float4, _BaseColor)
UNITY_DOTS_INSTANCED_PROP(float, _Smoothness)
UNITY_DOTS_INSTANCING_END(UserPropertyMetadata)
#endif
블록 이름으로 사용할 수 있는 값은 세 가지입니다:
– BuiltinPropertyMetadata — Unity 내부 전용
– MaterialPropertyMetadata — URP/HDRP 빌트인 셰이더가 사용
– UserPropertyMetadata — 커스텀 셰이더에서 사용할 이름. 이걸 쓰면 됩니다.
UserPropertyMetadata를 사용하면 Unity 내부 셰이더 코드와 이름 충돌 없이 안전하게 프로퍼티를 선언할 수 있습니다.
접근 매크로는 세 가지 변형이 있는데, 실전에서는 WITH_DEFAULT를 기본으로 씁니다. BRG 내부적으로 버퍼의 첫 64바이트를 0으로 예약해 두어서, 프로퍼티에 값이 설정되지 않은 경우 0이 반환됩니다. _BaseColor에서 0이 나오면 검정이 뜨는데, 이 쪽이 의도치 않은 쓰레기값보다 훨씬 진단하기 쉽습니다.
| 매크로 | 동작 | 언제 쓰나 |
|---|---|---|
UNITY_ACCESS_DOTS_INSTANCED_PROP |
인스턴스 데이터 직접 읽기 | 항상 데이터가 있다고 보장될 때 |
UNITY_ACCESS_DOTS_INSTANCED_PROP_WITH_DEFAULT |
데이터 없으면 0 반환 | 대부분의 경우, 이걸 기본으로 쓰세요 |
UNITY_ACCESS_DOTS_INSTANCED_PROP_WITH_CUSTOM_DEFAULT |
데이터 없으면 지정값 반환 | 0이 아닌 기본값이 필요할 때 |
3단계: SRP Batcher 호환 유지 + 접근 방식 통합
여기가 가장 중요한 부분입니다. DOTS 경로와 비-DOTS 경로(SRP Batcher)를 모두 지원하려면 UnityPerMaterial cbuffer와 DOTS 블록을 동시에 선언하고, #define 트릭으로 셰이더 코드를 통합합니다.
// SRP Batcher용 cbuffer — 반드시 있어야 SRP Batcher 호환
CBUFFER_START(UnityPerMaterial)
float4 _BaseMap_ST;
float4 _BaseColor;
float _Smoothness;
CBUFFER_END
// DOTS 인스턴싱 경로에서는 cbuffer 대신 GPU 버퍼에서 읽도록 오버라이드
#ifdef UNITY_DOTS_INSTANCING_ENABLED
UNITY_DOTS_INSTANCING_START(UserPropertyMetadata)
UNITY_DOTS_INSTANCED_PROP(float4, _BaseColor)
UNITY_DOTS_INSTANCED_PROP(float, _Smoothness)
UNITY_DOTS_INSTANCING_END(UserPropertyMetadata)
// 셰이더 본문 코드는 그대로 두고, 전처리기가 읽기 경로를 교체
#define _BaseColor UNITY_ACCESS_DOTS_INSTANCED_PROP_WITH_DEFAULT(float4, _BaseColor)
#define _Smoothness UNITY_ACCESS_DOTS_INSTANCED_PROP_WITH_DEFAULT(float, _Smoothness)
#endif
#define 재정의 덕분에 버텍스/프래그먼트 셰이더 본문은 _BaseColor를 그냥 쓰면 됩니다. 어느 경로로 컴파일되느냐에 따라 전처리기가 알아서 cbuffer 읽기 또는 GPU 버퍼 읽기로 전환합니다.
4단계: Instance ID 전파
구조체와 함수에 아래 매크로를 추가합니다.
struct Attributes
{
float4 positionOS : POSITION;
float2 uv : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID // 버텍스 입력에 인스턴스 ID 추가
};
struct Varyings
{
float4 positionCS : SV_POSITION;
float2 uv : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID // 프래그먼트에서도 DOTS 프로퍼티 접근 시 필요
};
Varyings Vert(Attributes input)
{
Varyings output;
UNITY_SETUP_INSTANCE_ID(input); // unity_InstanceID 초기화
UNITY_TRANSFER_INSTANCE_ID(input, output); // 프래그먼트로 전달
// ...
}
half4 Frag(Varyings input) : SV_Target
{
UNITY_SETUP_INSTANCE_ID(input); // 프래그먼트에서 DOTS 프로퍼티 읽기 전 필수
return _BaseColor; // #define에 의해 DOTS 버퍼 또는 cbuffer에서 읽힘
}
완성된 최소 URP Unlit 셰이더
위 내용을 모두 합친 실제 작동하는 셰이더입니다.
Shader "Custom/UnlitDotsInstanced"
{
Properties
{
_BaseMap ("Base Texture", 2D) = "white" {}
_BaseColor ("Base Color", Color) = (1, 1, 1, 1)
}
SubShader
{
Tags { "RenderPipeline"="UniversalPipeline" "Queue"="Geometry" }
Pass
{
Name "Forward"
Tags { "LightMode"="UniversalForward" }
HLSLPROGRAM
#pragma exclude_renderers gles gles3 glcore
#pragma target 4.5
#pragma vertex Vert
#pragma fragment Frag
#pragma multi_compile_instancing
#pragma instancing_options renderinglayer
#pragma multi_compile _ DOTS_INSTANCING_ON
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
struct Attributes
{
float4 positionOS : POSITION;
float2 uv : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct Varyings
{
float4 positionCS : SV_POSITION;
float2 uv : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
CBUFFER_START(UnityPerMaterial)
float4 _BaseMap_ST;
float4 _BaseColor;
CBUFFER_END
#ifdef UNITY_DOTS_INSTANCING_ENABLED
UNITY_DOTS_INSTANCING_START(UserPropertyMetadata)
UNITY_DOTS_INSTANCED_PROP(float4, _BaseColor)
UNITY_DOTS_INSTANCING_END(UserPropertyMetadata)
#define _BaseColor UNITY_ACCESS_DOTS_INSTANCED_PROP_WITH_DEFAULT(float4, _BaseColor)
#endif
TEXTURE2D(_BaseMap);
SAMPLER(sampler_BaseMap);
Varyings Vert(Attributes input)
{
Varyings output;
UNITY_SETUP_INSTANCE_ID(input);
UNITY_TRANSFER_INSTANCE_ID(input, output);
output.positionCS = TransformObjectToHClip(input.positionOS.xyz);
output.uv = TRANSFORM_TEX(input.uv, _BaseMap);
return output;
}
half4 Frag(Varyings input) : SV_Target
{
UNITY_SETUP_INSTANCE_ID(input);
half4 tex = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv);
return tex * _BaseColor;
}
ENDHLSL
}
}
}
Project Settings에서 반드시 확인해야 할 항목
구현을 다 끝냈는데도 빌드에서만 문제가 생긴다면 십중팔구 이 설정 때문입니다.
Edit > Project Settings > Graphics > Shader Stripping > BatchRendererGroup Variants
이 값이 Strip All 또는 Strip Unused로 되어 있으면, 빌드 시 DOTS_INSTANCING_ON 변형이 전부 제거됩니다. 에디터에서는 멀쩡하다가 빌드에서만 에러가 나는 전형적인 패턴이 여기서 발생합니다.
반드시 Keep All로 설정하세요.
호환성 점검 방법
Frame Debugger로 배칭 확인
Window > Analysis > Frame Debugger를 열어서 드로우 콜 목록을 확인합니다. GPU Resident Drawer가 정상 작동 중인 오브젝트의 드로우 콜은 “Hybrid Batch Group” 이름으로 묶여 보입니다. 커스텀 셰이더를 쓴 오브젝트가 여기 포함되지 않는다면 BRG 처리에서 제외된 겁니다.
Rendering Debugger로 GPU 오클루전 컬링 확인
Window > Analysis > Rendering Debugger > GPU Resident Drawer 탭
Occlusion Test Overlay를 활성화하면 씬/게임 뷰에 오버레이가 그려지며 어떤 오브젝트가 GPU 오클루전 컬링 대상인지 시각적으로 확인할 수 있습니다. 커스텀 셰이더 오브젝트가 이 오버레이에서 보이지 않는다면 BRG에서 제외된 상태입니다.
Inspector SRP Batcher 호환 패널
머티리얼을 Inspector에서 선택하면 하단에 “SRP Batcher: compatible” 또는 “SRP Batcher: not compatible” 표시가 나타납니다. DOTS 인스턴싱 셰이더는 SRP Batcher 호환도 동시에 충족해야 하므로, 여기서 “not compatible”이 뜨면 UnityPerMaterial cbuffer 선언을 다시 확인해야 합니다.
실무에서 자주 빠지는 함정들
오랫동안 커스텀 셰이더를 다뤄오면서, 그리고 클라이언트 프로젝트에서 GPU Resident Drawer 도입을 도우면서 반복적으로 목격한 패턴들입니다.
pragma를 일부 패스에만 추가
Forward 패스에는 추가했는데 ShadowCaster, DepthOnly 패스에 빠뜨리는 경우가 많습니다. 에러 메시지에 패스 이름이 명시되니, 에러가 나면 어느 패스인지 먼저 확인하세요.
MaterialPropertyBlock 사용
스크립트에서 renderer.SetPropertyBlock(...)을 호출하는 순간 그 Renderer는 GPU Resident Drawer에서 통째로 제외됩니다. 개별 프로퍼티 하나만 바꿔도 마찬가지입니다. 인스턴스별 프로퍼티가 필요하다면 DOTS 인스턴싱 버퍼를 통해 전달해야 합니다.
ShaderGraph 사용
ShaderGraph의 Sprite Lit / Sprite Unlit 타겟은 DOTS_INSTANCING_ON 변형을 생성하지 않습니다. 반드시 URP Lit 또는 URP Unlit 타겟을 사용해야 합니다.
UNITY_INSTANCING_BUFFER_START로 프로퍼티 선언
#pragma multi_compile_instancing과 #pragma multi_compile _ DOTS_INSTANCING_ON은 같은 셰이더에 공존할 수 있습니다. 문제는 pragma가 아니라 UNITY_INSTANCING_BUFFER_START로 인스턴싱 프로퍼티를 선언할 때입니다. 이렇게 하면 UnityPerMaterial cbuffer 구조가 깨져 SRP Batcher 호환성이 사라집니다. 프로퍼티 선언은 반드시 DOTS 방식(UNITY_DOTS_INSTANCING_START)으로만 해야 합니다.
Light Probe Proxy Volume 사용
LPPV를 사용하는 오브젝트는 GPU Resident Drawer 대상에서 제외됩니다. 그 이유는 앞선 GPU Resident Drawer 동작 원리 완전 분석 포스팅에서 다루었습니다.
마무리
간단한 커스텀 셰이더인데도 GPU Resident Drawer가 적용이 안되어 한참을 헤맨 기억이 납니다. 구조 자체는 단순합니다. pragma 선언, 프로퍼티 블록, #define 재정의 — 이 흐름을 한 번 이해하고 나면 기존 셰이더에 붙이는 작업은 생각보다 빠릅니다.
다만 MaterialPropertyBlock 의존도가 높은 프로젝트라면 이 부분을 먼저 정리해야 합니다. GPU Resident Drawer의 성능 이점을 제대로 얻으려면 인스턴스 데이터를 DOTS 인스턴싱 방식으로 전달하는 흐름 전체를 이해하고 있어야 합니다.
GPU Resident Drawer 자체의 동작 원리가 궁금하시다면, 이전 포스팅 GPU Resident Drawer 동작 원리 완전 분석을 먼저 보시는 걸 권합니다.
← 이전 편: GPU Resident Drawer 동작 원리 완전 분석
더 깊이 공부하려면
이 글을 쓰면서 직접 확인한 자료들입니다.
-
DOTS Instancing shaders in URP — Unity 6 Manual
DOTS 인스턴싱의 전체 개념과 BRG와의 관계를 정리한 공식 페이지. 이 글의 “무엇인가” 섹션 배경 자료. -
Declare DOTS Instancing properties in a custom shader — Unity 6 Manual
UNITY_DOTS_INSTANCING_START블록 이름 세 가지(BuiltinPropertyMetadata/MaterialPropertyMetadata/UserPropertyMetadata)의 차이를 확인한 자료. -
Access DOTS Instancing properties in a custom shader — Unity 6 Manual
UNITY_ACCESS_DOTS_INSTANCED_PROP계열 매크로 세 가지의 동작 차이를 정리한 페이지.WITH_DEFAULT가 왜 안전한지 여기서 확인했습니다. -
Support DOTS Instancing in a custom shader in URP — Unity 6 Manual
URP 환경에서 SRP Batcher와 DOTS 인스턴싱을 동시에 충족하는 방법을 다룬 페이지.#define재정의 패턴의 공식 근거. -
Best practice for DOTS Instancing shaders in URP — Unity 6 Manual
Shader Stripping 설정, MaterialPropertyBlock 제한 등 “빌드에서만 망가지는” 케이스들을 정리한 페이지. -
Enable the GPU Resident Drawer in URP — Unity 6 Manual
GPU Resident Drawer 활성화 조건과 제외 대상(LPPV 등)을 확인한 자료. -
Writing custom shaders for the BatchRendererGroup API — Unity 6 Manual
BRG 레벨에서 셰이더가 어떻게 동작하는지 이해하고 싶을 때. GPU Resident Drawer 내부 원리에 관심 있다면 이전 포스팅과 함께 읽기 좋습니다.