Unity Shader Graph 심화
Shader Graph는 노드 기반 셰이더 편집 도구이지만, 실전 프로젝트에서는 노드만으로 표현하기 어려운 로직이 반드시 등장합니다. 이 글에서는 Custom Function Node, Sub Graph, Keyword, 그리고 HLSL 직접 혼용 방법을 다룹니다.
1. Custom Function Node — HLSL 직접 삽입
섹션 제목: “1. Custom Function Node — HLSL 직접 삽입”Shader Graph에서 순수 HLSL 코드를 실행하려면 Custom Function Node를 사용합니다.
// 이 파일을 Custom Function Node의 Source > File 에 연결
void SampleNoise_float(float2 uv, float scale, out float noise){ float2 scaled = uv * scale; // 간단한 값 노이즈 float2 i = floor(scaled); float2 f = frac(scaled); f = f * f * (3.0 - 2.0 * f); // smoothstep
float a = frac(sin(dot(i, float2(127.1, 311.7))) * 43758.5453); float b = frac(sin(dot(i + float2(1,0), float2(127.1, 311.7))) * 43758.5453); float c = frac(sin(dot(i + float2(0,1), float2(127.1, 311.7))) * 43758.5453); float d = frac(sin(dot(i + float2(1,1), float2(127.1, 311.7))) * 43758.5453);
noise = lerp(lerp(a, b, f.x), lerp(c, d, f.x), f.y);}
// half 정밀도 오버로드 (모바일 최적화)void SampleNoise_half(half2 uv, half scale, out half noise){ SampleNoise_float((float2)uv, (float)scale, (float)noise);}노드 설정:
- Type: File
- Name:
SampleNoise - Inputs:
UV (Vector2),Scale (Float) - Output:
Noise (Float)
함수 이름은 반드시
_float/_half두 오버로드를 모두 제공해야 Graph Settings의 정밀도 설정과 충돌하지 않습니다.
2. Sub Graph — 재사용 가능한 셰이더 모듈
섹션 제목: “2. Sub Graph — 재사용 가능한 셰이더 모듈”복잡한 노드 그룹은 Sub Graph로 캡슐화해 여러 셰이더에서 재사용합니다.
Sub Graph 생성 절차:1. Project 창 우클릭 → Create → Shader Graph → Sub Graph2. 슬롯 추가: - Properties 패널에서 Input 추가 - Output Node에서 출력 타입 지정3. 로직 구현 후 저장4. 메인 셰이더에서 Sub Graph 노드로 드래그
예시 Sub Graph: PBR Wetness Effect Inputs: Albedo(Color), Wetness(Float), Normal(Vector3) Outputs: ModifiedAlbedo(Color), ModifiedSmoothness(Float), ModifiedNormal(Vector3)Sub Graph 내부에서 처리하는 습윤 효과 HLSL:
void ApplyWetness_float( float4 albedo, float wetness, float3 normal, out float4 outAlbedo, out float outSmoothness, out float3 outNormal){ // 젖은 표면: 어두워지고 반사율 증가 outAlbedo = lerp(albedo, albedo * 0.6, wetness); outSmoothness = lerp(0.0, 0.95, wetness);
// 젖은 표면의 노멀 감쇠 (표면이 평탄해짐) outNormal = lerp(normal, float3(0, 0, 1), wetness * 0.5); outNormal = normalize(outNormal);}3. Keyword — 런타임 셰이더 분기
섹션 제목: “3. Keyword — 런타임 셰이더 분기”Shader Graph Keyword를 사용하면 동일 셰이더에서 여러 변형을 만들 수 있습니다.
Keyword 설정: Type: Boolean (또는 Enum) Reference: _USE_DETAIL_MAP Definition: Material (Inspector에서 체크박스로 제어) Scope: Local (이 셰이더에서만 사용)C#에서 런타임 제어:
using UnityEngine;
public class MaterialKeywordController : MonoBehaviour{ [SerializeField] private Material targetMaterial;
// 디테일 맵 활성화 public void EnableDetailMap() { targetMaterial.EnableKeyword("_USE_DETAIL_MAP"); }
public void DisableDetailMap() { targetMaterial.DisableKeyword("_USE_DETAIL_MAP"); }
// Enum Keyword — 여러 모드 전환 public void SetRenderMode(int mode) { // Enum keyword는 _RENDERMODE_OFF, _RENDERMODE_ADD, _RENDERMODE_MULTIPLY 등 targetMaterial.SetFloat("_RenderModeInt", mode); }
// 글로벌 Keyword (모든 셰이더에 적용) public void SetGlobalWetWeather(bool isWet) { if (isWet) Shader.EnableKeyword("_WET_WEATHER"); else Shader.DisableKeyword("_WET_WEATHER"); }}4. Vertex Stage 활용 — 지면 변형
섹션 제목: “4. Vertex Stage 활용 — 지면 변형”Shader Graph에서 Vertex 단계를 수정해 메시를 변형합니다.
// 바람에 흔들리는 풀 셰이더 — Vertex Displacement// Custom Function Node: WindSway
void WindSway_float( float3 positionOS, // Object Space Position float3 normalOS, float2 uv, float time, float windStrength, float windFrequency, out float3 displacedPosition){ // UV.y = 0이면 뿌리(고정), 1이면 끝(최대 흔들림) float rootMask = uv.y; // 뿌리를 고정시키는 마스크
// 바람 방향: X축 위주로 흔들림 float sineWave = sin(time * windFrequency + positionOS.x * 0.5); float cosWave = cos(time * windFrequency * 0.7 + positionOS.z * 0.5);
float3 windOffset = float3( sineWave * windStrength * rootMask, cosWave * windStrength * 0.1 * rootMask, 0.0 );
displacedPosition = positionOS + windOffset;}5. URP와 Shader Graph 통합
섹션 제목: “5. URP와 Shader Graph 통합”URP에서 Shader Graph 셰이더에 CustomRenderPass 결과를 전달하는 방법:
using UnityEngine;using UnityEngine.Rendering;using UnityEngine.Rendering.Universal;
public class DepthNormalFeature : ScriptableRendererFeature{ private DepthNormalPass _pass;
public override void Create() { _pass = new DepthNormalPass(); _pass.renderPassEvent = RenderPassEvent.AfterRenderingPrePasses; }
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) { renderer.EnqueuePass(_pass); }
private class DepthNormalPass : ScriptableRenderPass { private static readonly int DepthNormalTexID = Shader.PropertyToID("_CameraDepthNormalsTexture");
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) { // Shader Graph의 Custom Function에서 _CameraDepthNormalsTexture 샘플링 // 이 pass에서 텍스처를 생성해 글로벌 셰이더 파라미터로 설정 CommandBuffer cmd = CommandBufferPool.Get("DepthNormal"); // ... 실제 렌더링 로직 context.ExecuteCommandBuffer(cmd); CommandBufferPool.Release(cmd); } }}6. 성능 고려 사항
섹션 제목: “6. 성능 고려 사항”Shader Graph 최적화 체크리스트:
Texture 샘플링:[ ] 불필요한 Texture Sample 노드 제거[ ] 동일 텍스처를 여러 번 샘플링하지 않았는가[ ] 노멀 맵: Normal Unpack 전 채널 재사용
연산 최소화:[ ] Vertex Stage에서 처리 가능한 연산을 Fragment로 옮기지 않았는가[ ] half 정밀도 사용 가능한 부분은 half로 처리했는가[ ] Custom Function 내 조건 분기(if) 최소화
Variant 관리:[ ] Keyword가 많으면 셰이더 변형 수 폭발적 증가 주의[ ] Local Keyword 우선 사용 (Global은 모든 셰이더에 영향)[ ] ShaderVariantCollection으로 필요한 변형만 사전 컴파일| 기능 | 용도 | 주의사항 |
|---|---|---|
| Custom Function Node | HLSL 직접 삽입 | _float/_half 오버로드 모두 필요 |
| Sub Graph | 셰이더 로직 모듈화/재사용 | 과도한 중첩 시 컴파일 시간 증가 |
| Keyword | 런타임 셰이더 분기 | Keyword 수에 비례해 변형 수 증가 |
| Vertex Stage | 메시 변형 | 물리 연산 대신 셰이더로 처리 가능 |
Shader Graph와 HLSL을 적절히 혼용하면 노드 에디터의 생산성과 코드의 표현력을 함께 얻을 수 있습니다.