콘텐츠로 이동

Unity HLSL Shader 기초

Unity는 세 가지 방식으로 셰이더를 작성합니다.

  • ShaderLab: Unity 고유 선언 언어. Properties, SubShader, Pass 구조를 정의
  • HLSL: GPU에서 실행되는 실제 셰이더 코드
  • Shader Graph: 노드 기반 비주얼 에디터 (코드 불필요)

이 글은 URP(Universal Render Pipeline)에서 HLSL을 직접 작성하는 방법을 다룹니다. 셰이더 그래프로 표현하기 어려운 복잡한 효과나 커스텀 렌더링 패스에서 직접 HLSL을 다뤄야 합니다.


Shader "Custom/MyFirstShader"
{
Properties
{
_BaseColor ("Base Color", Color) = (1, 1, 1, 1)
_BaseMap ("Base Texture", 2D) = "white" {}
_Glossiness("Smoothness", Range(0, 1)) = 0.5
}
SubShader
{
// URP 태그 (필수)
Tags
{
"RenderType" = "Opaque"
"RenderPipeline" = "UniversalPipeline"
"Queue" = "Geometry"
}
Pass
{
Name "ForwardLit"
Tags { "LightMode" = "UniversalForward" }
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
// URP 핵심 헤더
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
// Properties를 HLSL 변수에 연결
CBUFFER_START(UnityPerMaterial)
float4 _BaseColor;
float4 _BaseMap_ST; // 텍스처 타일링/오프셋
float _Glossiness;
CBUFFER_END
TEXTURE2D(_BaseMap);
SAMPLER(sampler_BaseMap);
// 버텍스 입력
struct Attributes
{
float4 positionOS : POSITION; // 오브젝트 공간 위치
float3 normalOS : NORMAL;
float2 uv : TEXCOORD0;
};
// 프래그먼트 입력 (버텍스 출력)
struct Varyings
{
float4 positionHCS : SV_POSITION; // 클립 공간 위치
float3 normalWS : TEXCOORD0; // 월드 공간 법선
float2 uv : TEXCOORD1;
};
// 버텍스 셰이더
Varyings vert(Attributes IN)
{
Varyings OUT;
// 오브젝트 → 클립 공간 변환 (URP 매크로)
OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);
// 법선 변환
OUT.normalWS = TransformObjectToWorldNormal(IN.normalOS);
// UV 타일링/오프셋 적용
OUT.uv = TRANSFORM_TEX(IN.uv, _BaseMap);
return OUT;
}
// 프래그먼트 셰이더
half4 frag(Varyings IN) : SV_Target
{
// 텍스처 샘플링
half4 texColor = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, IN.uv);
// 기본 컬러 × 텍스처
half4 color = texColor * _BaseColor;
return color;
}
ENDHLSL
}
}
}

셰이더에서 좌표 공간은 가장 혼동하기 쉬운 개념입니다.

Object Space (오브젝트 공간)
↓ M (Model Matrix)
World Space (월드 공간)
↓ V (View Matrix)
View Space (카메라 공간)
↓ P (Projection Matrix)
Clip Space (클립 공간)
↓ 원근 나눗셈
NDC (Normalized Device Coordinates)
↓ 뷰포트 변환
Screen Space (화면 공간)
// 오브젝트 → 월드
float3 worldPos = TransformObjectToWorld(IN.positionOS.xyz);
// 오브젝트 → 클립 (가장 자주 사용)
float4 clipPos = TransformObjectToHClip(IN.positionOS.xyz);
// 월드 → 클립
float4 clipPos = TransformWorldToHClip(worldPos);
// 법선 변환 (역전치 행렬 사용)
float3 normalWS = TransformObjectToWorldNormal(IN.normalOS);

// Lighting.hlsl 포함 필요
half4 frag(Varyings IN) : SV_Target
{
// 법선 정규화
float3 normalWS = normalize(IN.normalWS);
// 메인 라이트 가져오기
Light mainLight = GetMainLight();
// Lambert 디퓨즈: N·L
half NdotL = saturate(dot(normalWS, mainLight.direction));
// 라이트 색상 × 강도 × N·L
half3 diffuse = mainLight.color * mainLight.distanceAttenuation * NdotL;
// 텍스처 샘플링
half4 texColor = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, IN.uv);
half3 finalColor = texColor.rgb * _BaseColor.rgb * (diffuse + 0.1); // 0.1: 앰비언트
return half4(finalColor, texColor.a * _BaseColor.a);
}

// 추가 Properties
_NormalMap ("Normal Map", 2D) = "bump" {}
_NormalStrength ("Normal Strength", Range(0, 2)) = 1.0
// 추가 변수
TEXTURE2D(_NormalMap);
SAMPLER(sampler_NormalMap);
// Attributes에 탄젠트 추가
struct Attributes
{
float4 positionOS : POSITION;
float3 normalOS : NORMAL;
float4 tangentOS : TANGENT; // 탄젠트
float2 uv : TEXCOORD0;
};
// Varyings에 TBN 행렬 정보 추가
struct Varyings
{
float4 positionHCS : SV_POSITION;
float3 normalWS : TEXCOORD0;
float3 tangentWS : TEXCOORD1;
float3 bitangentWS : TEXCOORD2;
float2 uv : TEXCOORD3;
};
// 버텍스 셰이더
Varyings vert(Attributes IN)
{
Varyings OUT;
OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);
OUT.normalWS = TransformObjectToWorldNormal(IN.normalOS);
OUT.tangentWS = TransformObjectToWorldDir(IN.tangentOS.xyz);
OUT.bitangentWS = cross(OUT.normalWS, OUT.tangentWS) * IN.tangentOS.w;
OUT.uv = TRANSFORM_TEX(IN.uv, _BaseMap);
return OUT;
}
// 프래그먼트 셰이더
half4 frag(Varyings IN) : SV_Target
{
// 탄젠트 공간 법선 샘플링
half4 normalSample = SAMPLE_TEXTURE2D(_NormalMap, sampler_NormalMap, IN.uv);
float3 normalTS = UnpackNormal(normalSample);
normalTS.xy *= _NormalStrength;
// 탄젠트 → 월드 공간 변환
float3x3 TBN = float3x3(
normalize(IN.tangentWS),
normalize(IN.bitangentWS),
normalize(IN.normalWS)
);
float3 normalWS = normalize(mul(normalTS, TBN));
// 이후 라이팅 계산에 normalWS 사용
// ...
return half4(normalWS * 0.5 + 0.5, 1.0); // 시각화
}

SubShader
{
Tags
{
"RenderType" = "Transparent"
"Queue" = "Transparent"
"RenderPipeline" = "UniversalPipeline"
}
Pass
{
Blend SrcAlpha OneMinusSrcAlpha // 알파 블렌딩
ZWrite Off // 깊이 쓰기 비활성화
// ...HLSL 코드...
}
}

구성 요소역할
CBUFFER_START(UnityPerMaterial)Properties → HLSL 변수 연결
TEXTURE2D / SAMPLER텍스처 선언
TransformObjectToHClip오브젝트 → 클립 공간 변환
TRANSFORM_TEXUV 타일링/오프셋 적용
SAMPLE_TEXTURE2D텍스처 샘플링
GetMainLight()URP 메인 라이트 정보

HLSL 셰이더를 직접 작성하면 셰이더 그래프로 표현하기 어려운 커스텀 라이팅 모델, 특수 효과, 성능 최적화된 렌더링 패스를 구현할 수 있습니다.