학습 목표: 초급 테크니컬 아티스트가 셰이더에서 ## 연산자(토큰 결합 연산자)를 완전히 이해하고 실무에 활용할 수 있도록 돕습니다.
셰이더 개발을 하다 보면 비슷한 패턴의 변수나 함수를 반복적으로 작성해야 하는 경우가 많습니다. 예를 들어 _Metallic0
, _Metallic1
, _Metallic2
... 처럼 숫자만 증가하는 변수명을 다루거나, 각 파츠에 대해 동일한 속성 접근 코드를 반복 작성해야 할 때입니다.
이러한 반복 코드는 유지보수를 어렵게 만들고 타이핑 오류의 원인이 됩니다. ## 연산자는 이 문제를 해결하는 전처리기 기능으로, 토큰(변수명)을 컴파일 전에 자동으로 결합해줍니다.
이 글에서는 ## 연산자의 동작 원리를 분석하고, 실무에서 자주 사용되는 활용 패턴을 소개합니다.
중요한 점은 ## 연산자가 컴파일 이전의 전처리 단계에서 작동한다는 것입니다. 이는 런타임이 아닌 텍스트 치환 단계에서 변수명이 결정됨을 의미합니다.
// 1단계: 작성한 코드
#define METALLIC(idx) _Metallic##idx
float value = METALLIC(1);
// 2단계: 전처리기 처리 후
float value = _Metallic1;
// 3단계: 컴파일 및 실행
캐릭터 셰이더에서 파츠별로 메탈릭 값을 조절해야 하는 경우를 생각해봅시다.
기존 방식의 한계
Properties {
_Metallic0("헬멧 메탈릭", Range(0,1)) = 0.5
_Metallic1("갑옷 메탈릭", Range(0,1)) = 0.8
_Metallic2("무기 메탈릭", Range(0,1)) = 0.9
}
// 셰이더 코드
float helmetMetallic = _Metallic0;
float armorMetallic = _Metallic1;
float weaponMetallic = _Metallic2;
이 방식은 다음과 같은 문제가 있습니다:
#define METALLIC(idx) _Metallic##idx
이 한 줄의 매크로가 모든 _Metallic
계열 변수에 접근할 수 있게 해줍니다.
float helmetMetallic = METALLIC(0); // _Metallic0
float armorMetallic = METALLIC(1); // _Metallic1
float weaponMetallic = METALLIC(2); // _Metallic2
float metallics[3] = {
METALLIC(0),
METALLIC(1),
METALLIC(2)
};
Step 1: 매크로 정의 인식
#define TEXTURE(idx) _MainTex##idx
// ↑매크로명 ↑파라미터 ↑결합 규칙
Step 2: 매크로 호출 발견
TEXTURE(2)
Step 3: 토큰 결합 수행
_MainTex + 2 → _MainTex2
Step 4: 최종 코드 생성
float4 color = TEXTURE2D_SAMPLE(_MainTex2, sampler_MainTex2, uv);
캐릭터의 헬멧, 갑옷, 무기가 각각 다른 메탈릭과 러프니스 값을 가져야 합니다.
기존 방식
Properties {
_Metallic0("헬멧 메탈릭", Range(0,1)) = 0.8
_Metallic1("갑옷 메탈릭", Range(0,1)) = 0.5
_Metallic2("무기 메탈릭", Range(0,1)) = 0.9
}
// 각 파츠마다 일일이 변수명 입력
float helmetMetal = _Metallic0;
float armorMetal = _Metallic1;
float weaponMetal = _Metallic2;
매크로 정의
#define METALLIC(idx) _Metallic##idx
전처리 결과
METALLIC(0) → _Metallic0
METALLIC(1) → _Metallic1
METALLIC(2) → _Metallic2
개선된 코드
float helmetMetal = METALLIC(0); // _Metallic0
float armorMetal = METALLIC(1); // _Metallic1
float weaponMetal = METALLIC(2); // _Metallic2
// 메탈릭과 러프니스를 동시에
#define METALLIC(idx) _Metallic##idx
#define ROUGHNESS(idx) _Roughness##idx
Properties {
_Metallic0("헬멧 메탈릭", Range(0,1)) = 0.8
_Roughness0("헬멧 러프니스", Range(0,1)) = 0.3
_Metallic1("갑옷 메탈릭", Range(0,1)) = 0.5
_Roughness1("갑옷 러프니스", Range(0,1)) = 0.6
}
// 사용
float hMetal = METALLIC(0);
float hRough = ROUGHNESS(0);
// 여러 토큰 결합
#define PART_PROP(part, prop) _Part##part##_##prop
Properties {
_Part0_Metallic("파츠 0 메탈릭", Range(0,1)) = 0.8
_Part0_Roughness("파츠 0 러프니스", Range(0,1)) = 0.3
_Part1_Metallic("파츠 1 메탈릭", Range(0,1)) = 0.5
}
// 사용
float metallic = PART_PROP(0, Metallic); // _Part0_Metallic
float roughness = PART_PROP(0, Roughness); // _Part0_Roughness
// 작동하지 않음
int index = GetDynamicIndex(); // 런타임 값
float value = METALLIC(index); // 에러
// 이유: ## 연산자는 전처리 단계에서 작동
// index는 런타임에 결정되는 값
해결 방법: 조건문 사용
float GetMetallic(int index)
{
if(index == 0) return METALLIC(0);
else if(index == 1) return METALLIC(1);
else if(index == 2) return METALLIC(2);
else return 0;
}
// 작동하지 않음
for(int i = 0; i < 3; i++)
{
float m = METALLIC(i); // 에러
}
// 해결: 배열 미리 초기화
float metallics[3] = {
METALLIC(0),
METALLIC(1),
METALLIC(2)
};
for(int i = 0; i < 3; i++)
{
float m = metallics[i]; // 정상 작동
}
#define TEX(idx) _Texture##idx
// Properties에 _Texture5가 정의되지 않은 상태
float4 color = SAMPLE_TEXTURE2D(TEX(5), ...); // 런타임 에러
1. 명확한 네이밍 규칙
// 좋은 예
#define LAYER_ALBEDO(idx) _Layer##idx##_Albedo
#define LAYER_NORMAL(idx) _Layer##idx##_Normal
// 나쁜 예
#define TEX(a, b) _##a##b // 의도 파악 어려움
2. 주석으로 사용 예시 명시
// 사용법: METALLIC(0) → _Metallic0
#define METALLIC(idx) _Metallic##idx
3. 범위 검증
float GetSafeMetallic(int idx)
{
if(idx < 0 || idx > 2) return 0;
if(idx == 0) return METALLIC(0);
else if(idx == 1) return METALLIC(1);
else return METALLIC(2);
}
1. 과도한 중첩
// 가독성 저하
#define A(x) _Prop##x
#define B(x) A(x##_Sub)
#define C(x) B(x##_Detail)
2. 매직 넘버 사용
// 나쁜 예
float m0 = METALLIC(0);
float m1 = METALLIC(1);
// 좋은 예
#define METALLIC_HELMET 0
#define METALLIC_ARMOR 1
float m0 = METALLIC(METALLIC_HELMET);
다만 전처리 단계에서 작동한다는 특성을 이해하고, 런타임 동적 제어가 필요한 경우 조건문을 활용하는 등 적절한 패턴을 적용해야 합니다.
이번 글에서 소개한 패턴들은 실제 프로덕션 환경에서 검증된 방법들이며, 프로젝트의 요구사항에 맞게 응용할 수 있습니다.
작성일: 2025년 10월 15일
대상 독자: 초급 테크니컬 아티스트
난이도: 초급
글을 읽어봐도 예시주신 Tiling/Offset 에서 SampleDetailMap 함수를 idx 수만큼 늘려서 사용해야 하면 이점이 있는 건지 잘 모르겠네요.. ##연산자에 관해서는 잘 배우고 갑니다.