셰이더 ## 연산자

Mazeline·2025년 10월 15일
0

TA 가이드

목록 보기
1/6
post-thumbnail

학습 목표: 초급 테크니컬 아티스트가 셰이더에서 ## 연산자(토큰 결합 연산자)를 완전히 이해하고 실무에 활용할 수 있도록 돕습니다.


들어가며

셰이더 개발을 하다 보면 비슷한 패턴의 변수나 함수를 반복적으로 작성해야 하는 경우가 많습니다. 예를 들어 _Metallic0, _Metallic1, _Metallic2... 처럼 숫자만 증가하는 변수명을 다루거나, 각 파츠에 대해 동일한 속성 접근 코드를 반복 작성해야 할 때입니다.

이러한 반복 코드는 유지보수를 어렵게 만들고 타이핑 오류의 원인이 됩니다. ## 연산자는 이 문제를 해결하는 전처리기 기능으로, 토큰(변수명)을 컴파일 전에 자동으로 결합해줍니다.

이 글에서는 ## 연산자의 동작 원리를 분석하고, 실무에서 자주 사용되는 활용 패턴을 소개합니다.


## 연산자란?

기본 개념

연산자는 C/C++ 전처리기(Preprocessor)에서 제공하는 토큰 결합 연산자(Token Pasting Operator)입니다. 두 개의 토큰을 하나로 연결하여 새로운 식별자를 생성합니다.

동작 시점

중요한 점은 ## 연산자가 컴파일 이전의 전처리 단계에서 작동한다는 것입니다. 이는 런타임이 아닌 텍스트 치환 단계에서 변수명이 결정됨을 의미합니다.

// 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;

이 방식은 다음과 같은 문제가 있습니다:

  • 변수가 추가될 때마다 수작업으로 입력해야 함
  • 타이핑 오류 가능성
  • 변수명 규칙 변경 시 전체 수정 필요
  • 코드 가독성 저하

구현 방법

1. 매크로 정의

#define METALLIC(idx) _Metallic##idx

이 한 줄의 매크로가 모든 _Metallic 계열 변수에 접근할 수 있게 해줍니다.

2. 실제 사용

float helmetMetallic = METALLIC(0);   // _Metallic0
float armorMetallic = METALLIC(1);    // _Metallic1
float weaponMetallic = METALLIC(2);   // _Metallic2

3. 배열 초기화 활용

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

장점

  • 변수명 입력 오타 방지
  • 일관된 네이밍 패턴 유지
  • 파츠 추가 시 빠른 확장

추가 활용 패턴

1. 여러 속성 동시 관리

// 메탈릭과 러프니스를 동시에
#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);

2. 복합 매크로

// 여러 토큰 결합
#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



주의사항

1. 런타임 변수로 인덱스 전달 불가

// 작동하지 않음
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;
}

2. 루프에서 직접 사용 불가

// 작동하지 않음
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];  // 정상 작동
}

3. 존재하지 않는 변수 참조 주의

#define TEX(idx) _Texture##idx

// Properties에 _Texture5가 정의되지 않은 상태
float4 color = SAMPLE_TEXTURE2D(TEX(5), ...);  // 런타임 에러

Best Practices

권장 사항

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일

대상 독자: 초급 테크니컬 아티스트

난이도: 초급

profile
테크아트 컨설팅 전문 회사 "메이즈라인" 입니다.

2개의 댓글

comment-user-thumbnail
2025년 10월 15일

글을 읽어봐도 예시주신 Tiling/Offset 에서 SampleDetailMap 함수를 idx 수만큼 늘려서 사용해야 하면 이점이 있는 건지 잘 모르겠네요.. ##연산자에 관해서는 잘 배우고 갑니다.

1개의 답글