콘텐츠로 이동

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를 사용합니다.

Assets/Shaders/CustomFunctions/SampleNoise.hlsl
// 이 파일을 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 Graph
2. 슬롯 추가:
- 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:

Assets/Shaders/SubGraphs/WetnessEffect.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);
}

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");
}
}

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

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);
}
}
}

Shader Graph 최적화 체크리스트:
Texture 샘플링:
[ ] 불필요한 Texture Sample 노드 제거
[ ] 동일 텍스처를 여러 번 샘플링하지 않았는가
[ ] 노멀 맵: Normal Unpack 전 채널 재사용
연산 최소화:
[ ] Vertex Stage에서 처리 가능한 연산을 Fragment로 옮기지 않았는가
[ ] half 정밀도 사용 가능한 부분은 half로 처리했는가
[ ] Custom Function 내 조건 분기(if) 최소화
Variant 관리:
[ ] Keyword가 많으면 셰이더 변형 수 폭발적 증가 주의
[ ] Local Keyword 우선 사용 (Global은 모든 셰이더에 영향)
[ ] ShaderVariantCollection으로 필요한 변형만 사전 컴파일

기능용도주의사항
Custom Function NodeHLSL 직접 삽입_float/_half 오버로드 모두 필요
Sub Graph셰이더 로직 모듈화/재사용과도한 중첩 시 컴파일 시간 증가
Keyword런타임 셰이더 분기Keyword 수에 비례해 변형 수 증가
Vertex Stage메시 변형물리 연산 대신 셰이더로 처리 가능

Shader Graph와 HLSL을 적절히 혼용하면 노드 에디터의 생산성과 코드의 표현력을 함께 얻을 수 있습니다.