커스텀 셰이더 라이브러리를 개발하던 중 이 에러 메시지를 마주했습니다. 분명히 제대로 정의한 구조체인데 왜 모호하다는 걸까요? 원인을 추적해보니 URP 내부에도 동일한 이름의 구조체가 존재했습니다. 이 글에서는 Include Guard 활용법을 공유합니다.
배경: 커스텀 셰이더 라이브러리 개발
고객사 프로젝트에서 URP 기반의 커스텀 셰이더 라이브러리를 개발하고 있었습니다. 표준 URP의 SurfaceData 구조체에는 없는 추가 필드들이 필요했기 때문에, 자체적인 SurfaceData 구조체를 정의하여 사용하기로 했습니다. 처음에는 문제없이 잘 동작했지만, 라이브러리 구조를 리팩토링하면서 Include 순서가 바뀌자 갑자기 컴파일 에러가 발생했습니다.
문제 상황: 모호한 심볼 에러
저희 라이브러리에서는 다음과 같은 SurfaceData 구조체를 정의하여 사용하고 있었습니다:
// SurfaceDataLib.hlsl
struct SurfaceData
{
half3 albedo;
half3 specular;
half metallic;
half smoothness;
half occlusion;
half3 normalTS;
half3 tangentTS;
half3 emission;
half alpha;
half horizonFade;
half giOcclusionBias;
};
이 구조체를 초기화하는 함수를 작성하면서 URP Core를 include했는데, 여기서 문제가 발생했습니다:
// InputsLib.hlsl
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/CommonMaterial.hlsl"
// ... 기타 includes
inline void InitializeSurfaceData(float2 uv, out SurfaceData outSurfaceData)
{
// 컴파일 에러: 'SurfaceData' - ambiguous symbol
}
컴파일 결과는 "ambiguous symbol" 에러였습니다. 처음에는 오타나 경로 문제인 줄 알았지만, 원인은 다른 곳에 있었습니다.
원인 분석: 두 개의 SurfaceData
에러 메시지를 단서로 URP 소스 코드를 추적해보았습니다.
URP의 SurfaceData 정의
URP는 Packages/com.unity.render-pipelines.universal/ShaderLibrary/SurfaceData.hlsl에서 자체적인 SurfaceData 구조체를 정의하고 있었습니다. 이 파일은 Core.hlsl을 include하면 자동으로 포함됩니다:
// URP SurfaceData.hlsl (간략화)
#ifndef UNIVERSAL_SURFACE_DATA_INCLUDED
#define UNIVERSAL_SURFACE_DATA_INCLUDED
struct SurfaceData
{
half3 albedo;
half alpha;
half3 normalTS;
half3 emission;
half metallic;
half smoothness;
half occlusion;
// ... URP 표준 멤버들
};
#endif
충돌의 본질
문제는 다음과 같은 상황에서 발생합니다:
InputsLib.hlsl이Core.hlsl을 includeCore.hlsl이 내부적으로SurfaceData.hlsl을 include- URP의
SurfaceData구조체가 정의됨 - 이후 커스텀
SurfaceData구조체가 정의되면 이름 충돌 발생
컴파일러 입장에서는 동일한 이름의 구조체가 두 개 존재하므로, 어떤 것을 사용해야 할지 결정할 수 없습니다.
해결 방법: Include 순서 조정 + 매크로 차단
몇 가지 해결 방법을 검토한 끝에, 커스텀 SurfaceData를 먼저 정의하고 URP의 정의를 차단하는 방식을 선택했습니다.
왜 구조체 이름을 유지했는가
가장 단순한 해결책은 커스텀 구조체 이름을 SGESurfaceData 등으로 변경하는 것입니다. 하지만 이 방법은 선택하지 않았습니다.
첫째, 이미 프로젝트 전반에서 SurfaceData라는 이름을 사용하고 있었습니다. 이름을 변경하면 Lit, Unlit, Shadow 등 모든 셰이더 패스에서 해당 구조체를 참조하는 코드를 전부 수정해야 합니다. 단순한 찾기-바꾸기로 해결될 문제가 아니라, 각 패스별로 정상 동작 여부를 확인하는 작업까지 필요합니다.
둘째, 커스텀하지 않은 표준 URP 패스와의 호환성 문제가 있습니다. 일부 패스는 커스텀 라이브러리를 사용하지 않고 URP 표준 코드를 그대로 사용하는데, 구조체 이름이 다르면 이런 패스에서 데이터를 주고받을 때 타입 불일치로 인한 문제가 발생할 수 있습니다.
결론적으로, 기존 코드 수정을 최소화하면서 URP 표준 패스와의 호환성을 유지하기 위해 구조체 이름은 그대로 두고 Include 순서로 해결하는 방식을 택했습니다.
Step 1: SurfaceDataLib.hlsl에 Include Guard 및 차단 매크로 추가
// SurfaceDataLib.hlsl
#ifndef SGE_SURFACE_DATA_LIB_INCLUDED
#define SGE_SURFACE_DATA_LIB_INCLUDED
// 핵심: URP SurfaceData 정의 차단
// 이 매크로가 정의되면 URP의 SurfaceData.hlsl은 자체 구조체를 정의하지 않음
#define UNIVERSAL_SURFACE_DATA_INCLUDED
struct SurfaceData
{
half3 albedo;
half3 specular;
half metallic;
half smoothness;
half occlusion;
half3 normalTS;
half3 tangentTS;
half3 emission;
half alpha;
half horizonFade;
half giOcclusionBias;
};
SurfaceData EmptyFill()
{
SurfaceData data = (SurfaceData)0;
data.normalTS = half3(0.0h, 0.0h, 1.0h);
data.tangentTS = half3(1.0, 1.0, 0.0);
data.occlusion = 1.0h;
return data;
}
#endif // SGE_SURFACE_DATA_LIB_INCLUDED
Step 2: InputsLib.hlsl에서 Include 순서 조정
// InputsLib.hlsl
// 핵심: SGE SurfaceData를 먼저 정의하여 URP SurfaceData와 충돌 방지
#include "../Data/SurfaceDataLib.hlsl"
// 이제 Core.hlsl을 include해도 URP SurfaceData는 정의되지 않음
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/CommonMaterial.hlsl"
#include "SurfaceInputsLib.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DBuffer.hlsl"
#include "PropertiesLib.hlsl"
#include "TextureSampleLib.hlsl"
// 이제 SurfaceData는 오직 커스텀 정의만 참조
inline void InitializeSurfaceData(float2 uv, out SurfaceData outSurfaceData)
{
outSurfaceData = EmptyFill();
// ... 나머지 초기화 로직
}
작동 원리 상세 설명
이 해결책의 핵심은 URP의 Include Guard를 역이용하는 것입니다.
Include Guard의 역할
URP의 SurfaceData.hlsl은 다음과 같은 Include Guard를 사용합니다:
#ifndef UNIVERSAL_SURFACE_DATA_INCLUDED
#define UNIVERSAL_SURFACE_DATA_INCLUDED
// ... 구조체 정의
#endif
우리가 SurfaceDataLib.hlsl에서 미리 #define UNIVERSAL_SURFACE_DATA_INCLUDED를 선언하면, 나중에 URP의 SurfaceData.hlsl이 include될 때 조건문이 거짓이 되어 URP의 구조체 정의가 스킵됩니다.
실행 순서
InputsLib.hlsl파싱 시작SurfaceDataLib.hlslincludeSGE_SURFACE_DATA_LIB_INCLUDED정의UNIVERSAL_SURFACE_DATA_INCLUDED정의 ← 핵심!- 커스텀
SurfaceData구조체 정의
Core.hlslinclude- 내부에서
SurfaceData.hlslinclude 시도 UNIVERSAL_SURFACE_DATA_INCLUDED가 이미 정의되어 있음- URP
SurfaceData정의 스킵
- 내부에서
- 나머지 코드에서
SurfaceData는 오직 커스텀 정의만 참조
주의사항
1. 멤버 호환성 확인
커스텀 SurfaceData가 URP 내부 함수들과 함께 사용된다면, 해당 함수들이 기대하는 멤버들이 모두 포함되어 있는지 확인해야 합니다. 예를 들어, URP의 일부 라이팅 함수는 특정 멤버를 요구할 수 있습니다.
2. Include 순서 일관성
프로젝트 전체에서 SurfaceDataLib.hlsl이 항상 URP Core보다 먼저 include되어야 합니다. 어느 한 곳이라도 순서가 바뀌면 충돌이 발생할 수 있습니다.
3. 버전 호환성
URP 버전이 업데이트되면서 include guard 매크로 이름이 변경될 수 있습니다. 업그레이드 시 확인이 필요합니다.
대안적 해결 방법
앞서 언급한 방법 외에도 몇 가지 대안을 검토했습니다. 프로젝트 상황에 따라 더 적합한 방법이 있을 수 있습니다.
방법 1: 구조체 이름 변경
가장 안전하고 충돌 위험이 없는 방법은 커스텀 구조체의 이름 자체를 변경하는 것입니다:
struct SGESurfaceData // SurfaceData 대신
{
// ...
};
이 방법은 URP와의 충돌을 완전히 피할 수 있지만, 프로젝트 전체에서 구조체 이름을 변경해야 하는 번거로움이 있습니다.
방법 2: URP SurfaceData 직접 확장
URP의 SurfaceData를 그대로 사용하고, 추가 필드만 별도 구조체로 분리하는 방법입니다:
struct SurfaceDataExtension
{
half horizonFade;
half giOcclusionBias;
// 추가 필드들
};
이 방법은 URP 호환성을 유지하면서도 커스텀 기능을 추가할 수 있지만, 코드가 다소 복잡해질 수 있습니다.
결론
Unity URP 기반 커스텀 셰이더 라이브러리를 개발하다 보면 SurfaceData처럼 URP 내부에서 이미 사용 중인 이름과 충돌하는 경우가 종종 있습니다. 이번 경험을 통해 Include 순서 조정과 매크로 차단을 활용하면 URP의 기존 코드를 수정하지 않고도 깔끔하게 문제를 해결할 수 있다는 것을 확인했습니다.
핵심 포인트를 정리하면 다음과 같습니다:
- 커스텀
SurfaceData를 URP Core보다 먼저 include UNIVERSAL_SURFACE_DATA_INCLUDED매크로를 미리 정의하여 URP 정의 차단- 프로젝트 전체에서 일관된 Include 순서 유지
이 패턴은 SurfaceData 외에도 URP와 이름이 겹치는 다른 구조체나 함수에도 동일하게 적용할 수 있습니다. 비슷한 문제를 겪고 계신 분들께 도움이 되길 바랍니다.
'UNITY3D' 카테고리의 다른 글
| Unity URP Edge Fusion 간단 소개 및 분석 (0) | 2025.11.01 |
|---|---|
| Greedy Meshing(그리디 메싱) 이론, 처리 구조, 활용 이유 및 Voxel Chunk(복셀 청크) (0) | 2025.09.27 |
| Voxel 게임의 최적화 기술 및 최신 Voxel 렌더링 최적화 트렌드 분석 (0) | 2025.09.26 |
| URP 기술적 개선 제안 (2025.09.15 추가) (0) | 2025.09.15 |
| Unity 6.2.1 HDRP vs URP 셰이더 코드 비교 연구 (1) | 2025.09.07 |