본문 바로가기

Graphics/Ronja's Unity Shader tutorials

HLSL

 

HLSL

HLSL? Hlsl is the language the “juicy” parts of unity shaders are written in. The parts that contain custom logic and eventually decide what is drawn where on screen. It’s the language Microsoft designed to work with their Direct3D API to write gpu p

www.ronja-tutorials.com

Ronja 님의 허락을 받고 번역한 튜토리얼입니다. 원문은 위 링크에서 확인하실 수 있습니다.
몇몇 부분은 생략·추가하였습니다.
의역과 오역이 넘쳐날 수 있으니 편하게 봐주시고 잘못된 부분은 알려주시면 감사하겠습니다!

 

 

목차

  • HLSL?
  • 빌트인 타입
    •  스칼라
    • 벡터
    • 행렬
    • 텍스처
  • 수학 함수
  • 커스텀 타입
  • 변수
  • 함수
  • 제어 흐름
    • if 문
    • 반복문

 


 

HLSL?

HLSL은 유니티 셰이더의 중요한 부분들에서 쓰여진 언어로, Direct3D API와 연계해 GPU 프로그램을 작성할 수 있도록 마이크로소프트가 설계한 그래픽스 언어 입니다. 그 부분은 커스텀 로직을 포함하며, 최종적으로 무엇이 화면 어디에 그려질지 결정합니다.

엄밀히 말하면 대부분의 유니티 셰이더들은 CG(C for Graphics 의 준말)로 작성된 것으로 태그되어있지만, CG는 2012년부터는 사용되지 않기도 하고, HLSL과 대부분의 구문과 기능을 공유하기 때문에 CG를 HLSL로 바꾸어 검색하는 게 다른 대부분의 경우에 비해 더 좋은 결과가 나올 것입니다.
이론상으로는 유니티는 OpenGL을 위해 설계된 GLSL 셰이더 작성도 지원하지만, HLSL을 위한 예제가 더 많기 때문에 HLSL을 사용할 것입니다. 최종적으로는 우리가 출시할 플랫폼에서 필요한 그래픽스 언어로 변환되기 때문에 크게 상관 없기도 하고요.

하지만 유니티 셰이더를 배울 때 추천하는 건, 여기서 당신의 프로그래밍 여정을 시작하지 말라는 것입니다.
셰이더는 디버깅하기 까다롭고, 할 수 있는 것이 다소 제한되어 있습니다. 그리고 대부분의 경우 일반적인 프로그래밍 문맥과는 약간 다른 방향으로 작업을 해야 합니다. 그래서 저는 당신이 타입, 변수, 클래스, 메소드와 반복문, if 문과 같은 기본적인 부분들을 알 것이라고 가정하고 설명하려고 합니다.

 


 

빌트인 타입

아래가 셰이더를 만들 때 사용할 빌트인 타입들입니다.

 

스칼라

유니티 HLSL 안의 숫자 자료형에는 실수를 나타내는 fixed, half, float 와 정수를 나타내는 int, uint 가 있습니다.

모바일 GPU에서 fixed는 정밀도가 1/256인 -2.0과 2.0 사이의 숫자, half는 16비트 부동소수점 숫자, float는 32비트 부동소수점 숫자 자료형입니다.
데스크탑 GPU에서는 float, half, fixed 값 모두 32비트의 부동소수점 수로 연산되므로, 편의상 모든 곳에서 float를 사용하는 것을 자주 보게 될 것입니다. 하지만 이는 나중에 최적화를 위해 사용할 수 있는 좋은 도구이기도 합니다.

정수 자료형에서 int는 양수와 음수 모두를 가질 수 있고, uint형은 양수만 가질 수 있으므로 약간의 성능상 이점을 가질 수 있습니다.

또한, 참이냐 거짓이냐의 값을 가지고 있는 단순한 데이터 타입 bool이 있습니다. 여기에 조건을 체크한 결과를 저장할 수도 있습니다. 만약 bool 형 값을 숫자와 (다른 숫자에 더하거나 곱하는 식으로) 사용할 경우, 값이 거짓일 경우 0으로, 참일 경우엔 1로 동작하게 됩니다.

(유니티 셰이더 그래프에선 bool 형이 사용가능하지만, 셰이더 코드 내에선 불가능하다고 역자는 알고 있기는 합니다.
다만, a = b > c 와 같이 참과 거짓을 1하고 0으로 반환하는 조건 연산자는 사용 가능합니다.)

 

벡터

변수를 선언할 때 float2, half3, fixed4와 같이 스칼라 값의 끝 부분에 숫자를 추가하는 것으로 최대 4개의 값을 가지는 벡터 타입으로 선언할 수 있습니다. 텍스처 좌표, 색과 포지션을 저장할 때 이러한 벡터 값을 사용합니다.

벡터 내부의 한 값에 접근할 때, 좌표의 경우에는 각각 vector.x, vector.y, vector.z, vector.w로, 색상의 경우에는 vector.r, vector.g, vector.b, vector.a 순으로 사용합니다. 또는 vector[2] 와 같이 벡터 내부의 값에 접근할 수도 있습니다. (0을 기준으로 하므로, 4차원 벡터의 4개 값은 각각 0, 1, 2, 3이 됩니다.)

벡터에서 여러개의 값을 얻어서 다른 벡터에 그 값을 넣어줘야 할 때, 스위즐링(Swizzling)을 통해서 새로운 벡터를 만들 필요 없이 사용할 수 있습니다. 스위즐링은 아래와 같이 점 뒤에 벡터의 하위값들을 작성하는 것을 말합니다.

- vector.xy : 벡터의 첫 두 값(x, y)을 가져와 2차원 벡터에 넣습니다.
- vector.zyx : 벡터의 첫 세 값(x, y, z)을 가져와 순서를 반전해줍니다.
- vector.xxxx : 벡터의 첫 한개 값(x)을 가져와 모든 성분값이 해당 값으로 구성된 4차원 벡터를 만듭니다.

 

행렬

벡터가 한 방향에서 확장된 타입인 것처럼, 행렬은 두 방향에서 확장된 타입입니다. 행렬은 float4x4, half3x2 그리고 bool2x4와 같이 스칼라 값의 끝 부분에 '숫자 x 숫자'를 추가해 선언합니다. 행렬의 멤버 값에 접근하는 것은 matrix[3][2] 와 같이 대괄호를 통해서 동작하는데, 첫 번째 숫자는 행을, 두 번째 숫자는 열을 의미합니다.

또는 _m32와 같은 접근자를 통해서도 가능합니다. 이 접근자들은 스위즐링에도 사용될 수 있습니다. matrix._m03_m13_m23과 같이 접근자를 사용하면, 행렬의 마지막 열의 첫 행 세개의 값(matrix[0][3], matrix[1][3], matrix[2][3])을 3차원 벡터(x, y, z)로 작성합니다.
대괄호를 사용할 때, 대괄호 한 쌍만  사용한다면 해당 행을 정의하는 벡터를 얻을 수 있습니다.

다행히 행렬 내의 값에 접근할 일은 거의 없으므로, 접근자는 지금 이해해야 하는 것이라기 보다는 그저 도구에 지나지 않습니다.

 

텍스처

HLSL은 텍스처를 표현하는 데이터 타입 또한 가지고 있습니다. 대부분의 경우 데이터 타입을 벗어나지는 않겠지만, 텍스처의 픽셀을 tex2D(texture, coordinate) 함수를 통해서 읽어오는 식으로 사용해볼 수도 있습니다.

곧 텍스처에 관해서 더 배우게 될 것입니다. 약속합니다.

 


 

수학 함수

HLSL은 간단한 연산을 위한 일반적인 =, -, *, / 연산자와 비교를 위한 >, <, ==, !=, !, >=, <=, &&, || 연산자 외에도,
내장 함수로 abs, dot, lerp, pow, min, atan2 와 같은 여러가지 수학 함수들을 제공합니다.
(여기↗에서 모든 내장 함수들을 찾아볼 수 있습니다.)

+=, *=, -=, /= 같은 변수를 수정하고 다시 값을 할당해주는 대입 연산자도 있고,
1을 변수에 더하거나 빼주는 var++ 와 var--  같은 증감 연산자도 있습니다.

스칼라 타입과 벡터 타입의 곱은 벡터의 모든 성분에 스칼라 값이 곱해진 동일한 차원의 벡터를 반환합니다.
그래서 float2(2, 7) * 3 은 float2(6, 21) 와 같습니다.

벡터를 변환하기 위해 행렬과 벡터를 곱할 때는 mul 함수를 사용합니다. 처음에는 이게 이해하기 어려운 흑마술 같을 수도 있습니다. 그래서 저는 행렬 곱연산이 사용되는 케이스를 코드에 붙여넣는 것을 배우는 것을 추천합니다.

 


 

커스텀 타입

빌트인 타입 외에도, 우리만의 자체 타입을 추가할 수도 있습니다.
자체 타입을 추가하기 위한 구문은 아래와 같습니다. (끝 부분에 세미콜론 넣는 거 중요합니다!)

 

struct typeName
{
    float variable;
    float2 otherVariable;
};

 

이론적으론 class 키워드를 사용할 수도 있고, 상속, 멤버 함수와 인터페이스까지도 사용할 수 있지만, 저는 셰이더에 이를 사용해 본 적이 없기 때문에 여기에서 설명하지는 않을 것입니다. 만약 관련한 내용이 추후에 나온다면, 그 때가 설명할 시간이 될 것입니다.
이 기능을 사용하고 싶은 경우를 위해 말씀드리자면, 문법은 C++ / C#과 동일합니다.
HLSL - 인터페이스와 클래스 ↗

벡터 타입과 마찬가지로, instance.variable 혹은 멤버 변수의 하위 값에 접근해야 하는 경우에는 instance.otherVariable.x 식으로 점을 사용해서 커스텀 타입의 멤버 변수에 접근합니다.

 


 

변수

HLSL의 데이터 타입들은 모두 값 타입입니다. 값을 가지면 바로 그 값을 변경할 수 있고, new 와 같은 키워드를 통해 생성되지 않아도 된다는 것을 의미합니다.

벡터 타입을 생성하고 싶다면 함수를 호출하듯이 하면 됩니다. 이러한 경우 선언할 타입의 하위 값은 타겟 타입 내의 값으로 넣어야 합니다. float4를 생성한다면 float2 두 개를 값으로 넣거나, 또는 float 두 개와 float2 값 한 개를 넣거나, 아니라면 또 다른 float4 값 하나를 넣을 수 있습니다.

그렇게 아래와 같이 데이터 타입을 만들 수 있습니다.

 

typeName instance;
instance.variable = 3.14;
instance.otherVariable = float2(3, 1.4);

 

우리가 선언한 변수들은 함수 내에 위치할수도 있는데, 이 경우 선언한 변수는 해당 함수 내에서 변수 선언 이후에 오는 다른 부분에서만 접근할 수 있습니다. (대부분의 프로그래밍 언어가 그렇듯이)
또는 함수 바깥에 있을 수 있는데, 이 경우에는 셰이더 내 모든 함수에서 선언한 순서와 관계없이 변수에 접근할 수 있습니다. (하지만 변수를 쉽게 찾을 수 있도록 코드의 상단 부분에서 변수를 선언하는 것이 일반적입니다.)

 


 

함수

HLSL 내 대부분의 함수는 전역함수입니다. 이는 함수가 데이터 타입에 속해있지 않고, 어디에서나 호출될 수 있다는 것을 의미합니다.

함수는 다수의 인자를 가질 수 있고 (혹은 인자가 없거나) 값을 반환할 수 있습니다.
만약 값을 반환하지 않는 함수라면, 반환형을 void 로 선언해야 합니다.

일반적인 함수 구문은 아래와 같이 생겼습니다.

 

returnType functionName(argType arg1, otherArgType arg2)
{
    // 몇 가지 작업을 수행하고 returnValue를 연산해줍니다.

    return returnValue;
}

 

함수를 호출하기 위해서는 그냥 함수 이름과 괄호를 써주면 되고, 필요한 경우에는 인자 값을 괄호 안에 적습니다.
만약 같은 이름이지만 인자값이 다른 함수들이 여럿 있다면, HLSL은 함수를 호출할 때 사용한 인자의 타입과 일치하는 함수를 자동으로 찾아줍니다.

 


 

제어 흐름

많은 셰이더의 경우, 명령어를 놓치거나 반복하지 않고 명령어를 하나씩 실행하는 것으로 충분합니다. 그 중에서 if문과 for문 중 하나를 선택하는 것도 중요합니다.

셰이더 안에서 제어 흐름을 사용하는게 해롭다는 것은 기본적인 지식입니다. 특히 모바일 GPU 상에서는 성능에 영향을 미칠 것입니다. 대신 step과 같은 함수를 여러번 반복해서 사용하는 식으로 처리해야 합니다.

GPU가 분기의 양 쪽 모두를 계산하고 그 중 사용하지 않는 하나를 버려야 할 수도 있는 것은 맞지만, 다른 방식을 사용할 때에도 마찬가지이므로 이론적인 이유로 if문을 자제하기 보다는 코드를 예쁘게 만드는 것을 더 중요하게 생각하기 바랍니다.

 

if 문

if 문은 아래와 같이 생겼는데, 조건이 참이라면 if { } 부분이 실행되고, 아니라면 else { } 부분이 실행됩니다. 둘 다 실행되지는 않습니다.

 

if(condition)
{
	// 조건이 참이라면 실행됩니다.
}
else 
{
	// 조건이 거짓일 때 실행됩니다.
}

 

조건 주변의 괄호 if() 는 필수적이고, else 구문은 선택적인 부분입니다. 만약 구문에서 중괄호를 사용하지 않을 경우, 구문은 다음 하나의 줄에만 영향을 미칩니다. (다음 세미콜론까지이고, 다음 새로운 줄까지는 아닙니다.) 그 다음 줄에는 영향을 미치지 않습니다.

조건은 boolean 일수도, 숫자(이 경우 0은 거짓이고, 음수를 포함한 다른 모든 값들은 참)거나 둘 중 하나를 반환하는 연산일 수도 있습니다.

약간의 초심자의 팁은, 만약 당신이 원하는 if문의 조건과 반대되는 값을 가지고 있다면 (코드를 실행하고 싶을 때는 조건 값이 거짓이고, 실행하지 않을 때는 참일 때), 느낌표를 앞에 붙이면 본래 값이 거짓으로, 거짓 값이 참으로 뒤집힌다는 것입니다.

 

반복문

반복문은 어떤 코드가 실행되고 실행되지 않을지를 제어하는 또 다른 방법입니다.

while 문은 단순한 종류의 반복문입니다. 정의한 조건이 참이 아닌 한 계속해 실행됩니다. 만약 처음부터 조건이 참이 아니라면 아예 실행되지 않습니다.

while 문은 아래와 같이 생겼습니다.

 

while(condition)
{
	// 반복되는 작업을 수행합니다.
}

 

반복되는 작업을 수행하는 부분에서 조건이 참이  아니게 되도록 조건에 해당하는 변수를 바꾸는 것이 중요합니다. 그렇지 않다면 반복문은 영원히 실행될 것이고, 에디터 전체에서 충돌이 발생할 수 있습니다. (다행히, 에디터를 열지 않고도 셰이더를 고칠 수 있습니다.)

 

또 다른 반복문은 for 문으로, 반복되고 세어지기 위해 문법적 설탕(synactic sugar)가 추가됩니다.
for 문은 아래와 같이 정의됩니다.

 

for(beforeLoopLogic; condition; inLoopLogic)
{
	// 반복되는 작업을 수행합니다.
}

 

일반적으로 사용되는 index 변수가 maxValue-1 까지 계산되는 반복문의 예는 아래와 같습니다.

 

for(uint index=0; index<maxValue; index++)
{
	// 반복되는 작업을 수행합니다.
}

 

위 코드는 while 문을 사용하는 아래 코드와 동일하게 동작합니다.

 

uint index = 0;
while(index < maxValue)
{
    // 반복되는 작업을 수행합니다.

    index++;
}

 

두 반복문 모두 break 와 continue 키워드를 지원합니다.
break 문이 반복문 내에 존재한다면, 코드가 break 문에 도달했을 때 루프를 종료합니다. 이는 무한으로 반복되는 while 문을 탈출하는 것을 가능하게 합니다.
cotinue 문이 있다면, 루프가 다음 루프의 시작으로 점프합니다.

 

 

'Graphics > Ronja's Unity Shader tutorials' 카테고리의 다른 글

기본 투명 셰이더  (0) 2024.01.21
유니티 서피스 셰이더 기본  (0) 2023.11.19
기본 셰이더  (0) 2023.08.06
변수  (0) 2023.04.22
유니티 셰이더 구조  (0) 2022.11.09