#아드리

[Unity] 셰이더 입문자를 위한 HLSL 설명서 본문

Unity /Graphics

[Unity] 셰이더 입문자를 위한 HLSL 설명서

아두리두리 2025. 4. 15. 12:55

 

셰이더 공부 전 꼭 알아야 할 것들

Unity로 셰이더를 처음 접하려는 분들이라면, 아마 HLSL, CG, GLSL 같은 단어들이 헷갈리게 느껴질 거예요. 이 글에서는 그중에서도 가장 많이 쓰이는 HLSL에 대해 설명해볼게요.

 

HLSL이란?

**HLSL (High-Level Shading Language)**는 GPU에서 동작하는 프로그램을 작성할 때 사용하는 언어입니다. 마이크로소프트가 Direct3D API용으로 만든 언어이고, Unity 셰이더의 핵심 로직을 작성할 때도 이 HLSL을 사용하게 돼요.

"어디에 어떤 색을 칠할지", "빛이 닿으면 어떤 식으로 보일지", 이런 그래픽 처리의 진짜 핵심 부분을 담당합니다.

 

CG와 HLSL의 차이

Unity의 전통적인 셰이더 코드를 보면 CGPROGRAM이라는 태그로 시작하는 경우가 많습니다. 이건 **CG(C for Graphics)**라는 NVIDIA가 만든 셰이더 언어를 의미해요. 그런데 중요한 건,

  • CG와 HLSL은 문법이 거의 똑같고,
  • CG는 사실 2012년에 공식 지원이 종료됐습니다.
  • 그런데도 Unity에서는 여전히 CG를 사용하는 것처럼 보이죠. 이건 사실상 HLSL 문법을 사용하고 있다고 봐도 무방합니다.

그래서 실무나 학습에서는 HLSL이라는 이름으로 검색하는 게 더 많은 예제를 찾을 수 있어요.

 

GLSL은 왜 안 써요?

Unity는 GLSL이라는 OpenGL용 셰이더 언어도 지원하긴 합니다. 하지만…

  • GLSL은 Unity에서 잘 쓰이지 않고,
  • 관련 자료도 적고,
  • 결국 Unity는 셰이더 코드를 내보내는 플랫폼에 맞게 자동으로 변환해 주기 때문에,

우리는 그냥 HLSL 하나에 집중하는 게 훨씬 효율적입니다.

 

셰이더를 배우기 전에 꼭 알아야 할 것들

사실 셰이더는 일반적인 C#이나 파이썬 같은 언어와는 좀 달라요.
처음 접하면 이렇게 느낄 수 있어요:

"디버깅은 왜 이렇게 어려워…?"
"변수를 찍어볼 수도 없고, 뭔가 로직이 이상한데 확인할 방법이 없어!"
"왜 좌표랑 숫자가 이렇게 많이 나와…?"

 

그도 그럴 게, 셰이더는 GPU에서 병렬로 실행되고, 성능을 위해 많은 제약이 있는 환경에서 동작해요. 그래서 프로그래밍 경험이 어느 정도 쌓인 후 도전하는 걸 추천해요.

 

이런 사람에게 적합해요!

아래 항목에 해당된다면 셰이더 공부 시작해도 괜찮아요:

  • 변수, 자료형, 조건문, 반복문 같은 프로그래밍 기본 문법에 익숙하다
  • 클래스와 함수 개념을 알고 있다
  • 3D 공간, 벡터(Vector), UV 좌표 등에 대한 개념이 있다
  • 수학을 어느 정도 좋아하거나, 거부감이 적다

 

Unity HLSL 셰이더를 위한 기본 타입 정리 ✨

 

셰이더 코드를 처음 작성할 때 가장 헷갈릴 수 있는 것 중 하나는 바로 자료형입니다. Unity의 HLSL에서 자주 사용되는 빌트인 타입들을 정리해봤습니다. 이걸 알면 셰이더 코드를 훨씬 더 자유롭게 다룰 수 있을 거에요 :)

 

🔹 스칼라 값 (Scalar Values)

모바일 GPU에서는 fixed는 -2부터 2 사이의 값을 1/256 정밀도로 표현하며, half는 16비트 부동 소수점, float는 32비트 부동 소수점입니다. 데스크탑 GPU에서는 보통 이들 모두를 32비트 float으로 처리하기 때문에, 간단하게 float를 쓰는 경우가 많습니다.
그러나 최적화가 필요할 때는 각 타입을 적절히 구분해 사용하는 것이 좋습니다. 정수 타입 중 int는 양수와 음수 모두 저장할 수 있고, uint는 양수만 저장할 수 있습니다. 이러한 제한은 성능 이점으로 작용할 수도 있습니다. 또한 bool 타입도 있으며, true/false 값을 가질 수 있습니다. 숫자 연산과 함께 사용할 경우 false는 0, true는 1로 동작합니다. 정리하자면,

 

float, half, fixed

  • float: 32비트 부동소수점 숫자 (가장 일반적)
  • half: 16비트 부동소수점 (모바일 최적화에 유리)
  • fixed: -2~2 사이의 값, 1/256 정밀도 (모바일에서만 의미 있음)

팁: 데스크탑에서는 모두 32비트로 처리되므로 대부분 float를 사용해도 무방하지만, 모바일 GPU 최적화할 때는 half나 fixed가 도움이 됩니다.

 

int, uint

  • int: 정수, 음수와 양수 모두 가능
  • uint: unsigned int, 양수만 저장 가능 (성능상 약간의 이점 있음)

bool

  • 참/거짓 저장. 수치 연산 시 false=0, true=1로 간주됩니다.

 

 

🔹 벡터 값 (Vector Values)

스칼라 값을 확장하면 여러 숫자를 담는 벡터 타입이 됩니다. 벡터는 스칼라 타입 뒤에 숫자를 붙여서 생성합니다. 

예:

  • float2, half3, fixed4 등

주로 색상, 위치, 텍스처 좌표 등을 표현하는 데 사용됩니다.

예:

  • float2 → UV 좌표
  • float3 → 3D 위치
  • float4 → 색상(RGBA)

벡터 요소 접근

벡터의 개별 요소에 접근할 때는 보통 다음과 같이 씁니다:

  • 좌표: vector.x, vector.y, vector.z, vector.w
  • 색상: vector.r, vector.g, vector.b, vector.a
  • 배열처럼: vector[0], vector[1]

스위즐(Swizzling)

벡터의 특정 값만 골라 새 벡터로 만들 수 있습니다.

예:

vector.xy    // 앞 두 요소로 2D 벡터 생성
vector.zyx   // 순서를 바꿔서 새로운 벡터 생성
vector.xxxx  // 첫 번째 요소로만 구성된 4D 벡터 생성

 

🔹 행렬 (Matrix Values)

벡터가 1차원이라면, **행렬(Matrix)**은 2차원이죠. 타입 표기법은 float4x4, half3x2,  bool2x4 처럼 씁니다.

 

행렬 값 접근법

  • 배열 접근: matrix[row][column] -> matrix[3][2] (3번째 행, 2번째 열)
  • 축약 표현: _m32 (3행 2열)
  • 스위즐도 가능: matrix._m03_m13_m23

대부분의 경우 행렬의 값을 직접 다룰 일은 적어요. 나중에 변환할 때 유용하니 참고만 해도 좋아요.

 

🔹 텍스처 (Textures)

HLSL에는 텍스처 관련 타입도 있으며, 가장 흔하게 쓰는 것은 tex2D(texture, coordinate) 함수입니다.

예:

  • sampler2D, Texture2D 등

픽셀 읽기:

tex2D(texture, uv);

 

더 깊은 내용은 나중에 다룰게요. 😉

 

🔹 수학 연산 (Math)

기본 연산자 외에도 다양한 내장 수학 함수들이 있어요:

  • 기본 연산: +, -, *, /
  • 비교 연산: ==, !=, <, >, <=, >=
  • 논리 연산: &&, ||, !
  • 기타 함수: abs, dot, lerp, pow, min, atan2 등

연산 축약형

  • x += 1 → x = x + 1 "=: 변수 값을 수정하고 그 값을 다시 할당
  • x++, x-- 도 사용 가능 : 변수 값을 각각 1씩 증가시키거나 감소시킴

벡터와 스칼라 곱

부동 소수점 숫자와 벡터를 곱할 때는 벡터의 각 요소에 해당 숫자가 곱해진 결과를 얻을 수 있습니다.

float2(2, 7) * 3 // → float2(6, 21)

 

행렬 곱은?

mul(matrix, vector);

 

행렬과 벡터의 곱셈은 mul() 함수를 사용하여 벡터를 변환하는 데 사용됩니다. 행렬 곱셈을 잘 이해하는 것이 중요하지만, 처음에는 기존 예제를 따라하며 익히는 것이 좋습니다.

 

🔹 사용자 정의 타입 (struct)

HLSL에서는 내장된 기본 타입 외에도 사용자가 정의한 타입을 만들 수 있습니다. 구조체를 사용하여 새로운 타입을 정의할 수 있습니다:

struct typeName {
  float variable;
  float2 otherVariable;
};

 

클래스나 상속과 같은 고급 기능도 사용이 가능하지만, 셰이더 코드에서 자주 사용되지 않으므로 이번에는 다루지 않겠습니다. 필요할 경우 그때 그때 설명할 예정입니다.

사용자 정의 타입의 변수에는 . 연산자를 사용하여 접근할 수 있습니다. 예를 들어:

instance.variable
instance.otherVariable.x

 

🔹 변수 선언과 초기화

HLSL에서 모든 데이터 타입은 값 타입(value type)입니다. 즉, 값을 생성한 후 이를 변경할 수 있습니다. new 키워드 없이도 바로 값을 할당할 수 있습니다.

벡터 타입은 함수처럼 호출하여 생성할 수 있습니다. 예를 들어 float4 타입은 다음과 같이 생성할 수 있습니다:

float4 color = float4(1.0, 0.5, 0.2, 1.0);

 

변수는 함수 안에 선언할 수도 있고, 함수 바깥에 선언하여 여러 함수에서 사용할 수 있습니다. 함수 안에 선언된 변수는 해당 함수 내에서만 접근 가능합니다.

 

🔹 함수 (Functions)

HLSL에서 함수는 대부분 전역 범위로 정의됩니다. 즉, 데이터 타입에 속하지 않으며 어디서든 호출할 수 있습니다. 함수는 여러 인자를 받을 수 있으며, 값을 반환할 수도 있습니다. 반환 값이 없다면 void를 사용하여 반환 타입을 명시합니다.

returnType functionName(argType arg1, otherArgType arg2) {
  // 작업 수행
  return returnValue;
}

 

함수를 호출할 때는 함수 이름 뒤에 괄호를 쓰고, 필요한 인자를 넣습니다. 동일한 함수 이름으로 여러 가지 인자 타입을 처리할 수 있으며, HLSL은 자동으로 맞는 버전을 찾아 호출합니다.

  • 반환값이 없다면 void 사용
  • 오버로딩도 지원돼요 (동일한 이름, 다른 인자)

 

🔹 흐름 제어 (Control Flow)

많은 셰이더에서는 명령이 순차적으로 실행되지만, 조건에 따라 다른 경로로 코드를 분기하는 것이 중요할 때도 있습니다. 제어 흐름을 사용하면 셰이더의 효율성을 높일 수 있습니다. 셰이더에서 조건문과 루프를 사용할 때 성능에 미치는 영향을 고려해야 하며, 특히 모바일 GPU에서는 성능 저하가 발생할 수 있습니다. 그러나 이를 적절히 사용하면 코드의 가독성이 좋아집니다.

 

if문

if 문은 조건이 참일 경우 특정 블록을 실행하고, 그렇지 않으면 다른 블록을 실행합니다. 기본 문법은 다음과 같습니다:

if (condition) {
  // 참일 때 실행
} else {
  // 거짓일 때 실행
}

 

  • 괄호 필수!
  • !를 쓰면 반대로 만들 수 있어요.

조건이 true일 경우 첫 번째 블록이 실행되고, false일 경우 두 번째 블록이 실행됩니다. 조건은 boolean 값이거나 숫자(0은 false, 그 외의 값은 true)가 될 수 있습니다.

 

 

반복문 (loops)

셰이더에서 루프는 코드의 반복을 제어하는 데 사용됩니다. while 루프는 조건이 true일 동안 계속해서 실행됩니다.

 

while문

while(condition) {
  // 조건이 참일 때 반복
}

 

주의: 조건이 처음부터 false라면 루프는 한 번도 실행되지 않습니다. 주의할 점은 루프 내에서 조건을 만족시키지 않으면 무한 루프가 발생할 수 있습니다.

 

for문

루프는 반복 횟수를 지정할 때 유용합니다.

'for' 루프는 다음과 같이 정의됩니다.

for(beforeLoopLogic; condition; inLoopLogic){
  //do things
}

 

이것은 보통 필요한 것보다 더 많은 자유를 제공하므로, 보통 덜 혼란스러운 사용 사례는 변수 index가 0부터 maxValue-1까지 카운팅하는 것입니다:

for(uint index = 0; index < maxValue; index++){
  // 작업 수행
}

 

이 코드는 대신 while 루프를 사용하는 다음 코드와 동일합니다:

 

uint index = 0;
while(index < maxValue){
  //do things

  index++;
}

 

두 루프 모두 break와 continue 키워드를 지원합니다.

 

break 문을 코드에 포함시키면 코드가 다음 루프의 끝으로 점프하게 됩니다. 이로 인해 무한 루프에서 벗어날 수도 있습니다.

continue 문은 루프가 다음 반복의 시작으로 점프하도록 만듭니다.

 

break & continue

  • break: 루프 종료
  • continue: 다음 반복으로 건너뜀

 

마무리

처음엔 낯설 수 있지만, 이 타입들과 흐름 제어를 이해하면 HLSL 셰이더를 훨씬 자유롭게 다룰 수 있습니다. 복잡한 개념은 천천히, 일단은 자주 쓰이는 것부터 익혀보아요!

 

반응형

'Unity > Graphics' 카테고리의 다른 글

[DEMO] Unity 아티스트를 위한 HDRP 안내서  (3) 2018.09.27
Comments