서의 공간

HLSL 둘러보기 (번역: 박범준) 본문

Graphics Theory/관련 정보

HLSL 둘러보기 (번역: 박범준)

홍서의 2020. 12. 3. 10:26

이 글은 Catalin의 블로그에 게시된 바 있는 Crash Course in HLSL을 번역한 것입니다.

번역본 링크(구글 docx): HLSL 둘러보기 - Google 문서

역자 - 박범준(mochanpowder@gmail.com)

HLSL 둘러보기

HLSL이 무슨 뜻인가요? 왜 만들었죠? 이펙트(Effect) 파일은 어떻게 생겼나요? 이걸로 뭘 할 수 있죠? float4x4texcoord0, compile ps_3_0이 무슨 뜻입니까? 첫번째 질문의 답은 간단합니다 : HLSL은 고수준 쉐이더 언어(High Level Shader Language)라는 뜻입니다. 그렇다면 자연히 다음 질문이 떠오릅니다. 그에 대한 답을 다음 글에서 차근차근 짚어볼것입니다. 먼저 HLSL의 역사와 존재하게 된 배경에 대해 시작해보겠습니다. 그 후에 HLSL 이펙트 파일의 구조와 이 언어의 여러 구성요소들에 대해 배울 것입니다. 최종적으로 언어의 기본을 살펴본 뒤에 XNA 환경에서의 이펙트 파일 템플릿을 살펴보겠습니다.

HLSL의 역사

C언어가 CPU를 위한 언어라고 보았을 때, HLSL은 GPU를 위한 언어라고 볼 수 있습니다. 이들이 원래 무엇인지 신경쓰지 않고도 우린 이 둘을 잘 사용할 수 있습니다. 하지만 C언어의 경우 원래 무엇인지를 알고 접근한다면 그 특징에 대해 더 잘 이해할 수 있듯이,  컴퓨터 공학도들처럼 GPU의 간략한 역사에 대해 배워볼것입니다.

 

다들 알다시피 PC나 콘솔에는 GPU라는 부품이 있는데, 이 녀석은 단 하나의 목적만을 가지고 있었습니다 : 컴퓨터 그래픽을 처리, 표현하는 것이죠. 현대 GPU의 조상이라 할 수 있는 80년대 PC의 그래픽 칩들은 두 개 이상의 비트맵을 하나의 이미지로 조합하는 기능만을 수행하는 특별한 회로를 가지고 있었습니다. 그러나 그 이후, 1996년에 문제의 3D 가속기가 첫 출시됩니다. 이는 3DFX Voodoo라 불리는 카드로, 여러 3D 기능을 수행할 수 있는 분리된 하드웨어였습니다. 3D 기능은 곧 기존의 2D 비디오 카드와 통합되어 현대 GPU의 할아버지격인 하나의 칩으로 재탄생됩니다. 이 시절의 유명한 그래픽 카드들로 Voodoo, TNT, GeForce, Radeon을 꼽을 수 있습니다.

 

GPU는 트랜스폼과 조명을 하드웨어적으로 지원하기 시작하면서 새로운 단계에 접어듭니다. 이는 그래픽 카드가 3D 공간 상의 기하 정보, 클리핑, 폴리곤의 조명 등을 모두 하드웨어에서 처리할 수 있게 되었음을 의미합니다. 이 시절의 모든 3D 기술들은 고정 함수(Fixed Functions)라 불리는 기능을 통해 제공되었는데(이로 인해 고정 함수 파이프라인이라 불립니다.), 이는 하드웨어에서 제공된 각종 변환과 조명 모델만 사용할 수 있는 것이었습니다. 이로 인해 당시의 많은 게임들이 비슷비슷한 모양을 띄게 되었습니다.

 

컴퓨터 그래픽의 리얼리즘과 유연성을 보다 향상시키기 위한 비디오 카드 제작사들의 시도는 두 갈래로 나뉘어졌습니다. 첫번째는 단순하게 점점 더 많은 고정 함수를 추가하여 프로그래머의 요구를 보다 넓게 충족시키는 것이었습니다. 그러나 이 방법은 회로기판과 API 상의 변수, 명령어 등을 기하급수적으로 팽창시켰습니다. 두번째 방법은 각각의 정점(Vertex), 또는 픽셀마다 수행하게 되는 ‘작은 프로그램’을 작성하는 것이었습니다. 사용자들로서는 다행히도 두번째 방법이 선택되었는데, 이는 개발자에게 더 많은 유연성을 제공하였습니다(물론 하드웨어가 지원하는 범위 내에서).

 

이 ‘작은 프로그램’이 다이렉트 X 8.0에서 소개된 쉐이더입니다. 쉐이더 모델 1로 구분되는 이것은 사용 가능한 기능의 목록이 포함된 명세서를 가지고 있었습니다. 이 버전의 다이렉트 3D에서 프로그래머들은 어셈블리어와 유사한 언어로 쉐이더를 작성해야 했습니다. 예시는 이렇습니다 :

vs_1_1
dcl_position v0
dcl_color v0
m4x4 oPos, v0, c0
mov oD0, v1

어셈블리로 작성된 쉐이더는 읽거나 유지, 보수하기가 어려웠습니다. 게다가 쉐이더 모델 1은 매우 적은 명령어와 제한된 기능만을 가지고 있었습니다. CPU 프로그래밍 쪽이 고수준 프로그래밍 언어의 축복을 받아 보다 쉬운 프로그래밍을 할 수 있었던 것처럼 다이렉트 X 9.0대에 들어서 쉐이더 모델 2.0이 고수준 셰이딩 언어와 함께 공개되었습니다.

 

HLSL은 다이렉트 X에서 GPU연산되는 쉐이더를 작성하는데 사용하는 언어입니다. 이는 문법적으로 C와 유사하지만 독자적인 자료형과 프로그램 구조를 가지고 있었습니다. 이는 변수, 함수, 표현식, 선언, 표준 자료형, 사용자 정의 자료형, 전처리기 등과 같은 고수준 프로그래밍 언어의 요소들을 사용할 수 있게 하여 그래픽 프로그래머의 삶을 보다 쉽게 바꾸어 주었습니다. 이 요소들은 HLSL을 보다 읽거나 유지, 보수하기 쉽게 해주었습니다.

 

HLSL의 구문과 구조에 들어가기 전에 그래픽 처리 파이프라인에 대해 알아보겠습니다.

다이렉트 X 그래픽의 프로세스 파이프라인

각 타입의 쉐이더가 어떻게 동작하는지 이해하려면 먼저 3D 오브젝트가 어떻게 정제되어 표현되는지를 알아야 합니다. 다이렉트 3D 9의 그래픽 파이프라인을 아래의 도식으로 살펴보겠습니다.

 

 

도식의 블록 하나는 렌더링 과정에서 특정한 목표를 지닙니다. 정점 데이터와 기초 데이터, 텍스쳐로부터 데이터를 제공받습니다. 정점 데이터는 변환되지 않은 모델의 정점 정보를 지닙니다. 이는 버텍스 버퍼라는 곳에 저장됩니다. 기초 데이터는 점, 선, 면 같은 기하학 상의 기초정보를 지닙니다. 이들은 정점 데이터의 정점들로 정의되어 인덱스 버퍼에 색인됩니다. 이론상으로 테셀레이션 단계는 높은 우선순위를 가진 기초정보(N-Patches 나 디스플레이스먼트 맵 같은)를 정점으로 변환하여 버텍스 버퍼에 저장합니다. 하지만 적은 수의 GPU들만이 이 기능을 내장하고 있었습니다. 새로운 버전의 다이렉트3D에서는(DX 11) 파이프라인의 이 단계가 보다 멋진 일을 수행하게끔 재탄생되지만, 아직 XNA에서는 가능하지 않습니다.

 

정점 처리 단계는 좀 더 흥미롭습니다. 버텍스 버퍼에 담겨있던 정점들이 입력되어 다양한 변환이 가해진 뒤, 지오메트리 처리단계로 넘어가기 전 상태를 가정해보겠습니다. 여기서 첫번째 타입의 쉐이더가 등장합니다 : 정점(버텍스) 쉐이더. 정점 쉐이더를 작성할 때 쉐이더 프로그래머가 할 일은 아마도, 주어진 오브젝트 좌표에 기반한 정점들을 화면상의 좌표로 변환하는 것입니다. 여러 다른 좌표 공간에 대해서는 Creator’s Club site 의 학습(Education) 섹션에서 배워볼 수 있습니다. 지금은 버텍스 버퍼에서 넘어온 정점이 어떤 매트릭스로 곱연산되면서 2D 화면 상에 투사된다는 것만 기억하셔도 됩니다. 투사된 위치와 함께 색상, 노말(법선), 텍스쳐 좌표 등도 보통 함께 전달됩니다. 

 

지오메트리 처리단계에서는 변환된 정점으로 형성된 기하정보에 여러가지 알고리즘이 적용됩니다. 이 알고리즘이란 클리핑(스크린 바깥의 기하 정보를 지우는 것), 백 페이스 컬링(뒷면이 스크린을 바라보는 기하 정보를 지우는 것), 래스터라이제이션(정점으로 정의된 삼각형을 픽셀 형태로 바꾸는 것)등을 말합니다. 래스터라이저는 삼각형의 세 개의 점과 그 속성값들을 취합하여 삼각형 속의 픽셀들에게 보간하여 분배합니다. 이 값들이 나중에 픽셀 처리단계로 넘어가게 됩니다.

 

픽셀 처리단계는 우리가 배우게 될 두번째 쉐이더인 픽셀 쉐이더의 집과 같은 곳입니다. 픽셀 쉐이더는 래스터라이저로부터 데이터를 입력받습니다. 여기엔 텍스쳐 좌표와 노말, 바이노말과 그외 여러 정보가 포함됩니다. 픽셀 쉐이더 내부에서는 이 데이터와 텍스쳐에서 읽어들인 색상을 활용해 최종적인 색상을 만들어내게 됩니다. 이 색상은 픽셀 렌더링 단계에서 알파 블렌딩, 뎁스, 스텐실 테스트 등을 거쳐 최종적으로 프레임버퍼에 입력됩니다.

 

아직 언급하지 않은 나머지 두 단계는 텍스쳐와 텍스쳐 샘플러입니다. 텍스쳐는 데이터가 저장된 메모리 블럭을 말하며 보통 색상값의 배열을 의미합니다. 픽셀 쉐이더가 어떤 텍스쳐에 접근할 때 텍스쳐 샘플러에 특정 어드레싱 모드와 필터링을 지정하며 그에 접근합니다(가끔은 정점 쉐이더도 접근합니다).

 

각각의 쉐이더가 언제 어디서 동작하는지 이해하려면 지금까지 짚어본 그래픽스 파이프라인의 일반적인 모습을 기억해두는 것이 좋습니다. 이 내용들을 숙지한 채 이미 언급한 바 있는 두가지 쉐이더에 대해 살펴보도록 하겠습니다.

정점 쉐이더(버텍스 쉐이더)

앞에서 본 바와 같이 정점 쉐이더는 정점 처리 단계에 실행됩니다. 그러므로 정점 쉐이더는 다음과 같은 사항들을 주관하게 됩니다.

  • 공간 변환(transformation)을 조직(Coordinate)합니다. 보통 세 가지 변환 정보를 사용하여 구성됩니다. 먼저 월드 좌표계 상 위치 변환과 회전 변환이 있습니다. 시야 변환은 화면에 나타나는 모든 정점을 이동시킵니다. 예를 들어 상대위치를 가지는 카메라 처럼요. 시야 공간상에서 카메라는 시스템의 근본이 됩니다. 마지막 변환은 투사 변환입니다. 이는 시야 공간에 존재하는 3D 삼각형과 폴리곤들을 화면 상에 렌더링할 수 있는 2D 삼각형과 폴리곤으로 바꾸어줍니다.

  • 일부 애니메이션 기법들이 정점 쉐이더에서 사용됩니다. 사실은 월드 변환에 속하지만 따로 언급해둘 필요가 있겠습니다.

  • 빛과 색상 계산 또한 정점 쉐이더에서 행해집니다.

  • 버텍스 쉐이더는 픽셀 쉐이더나 고급 효과들이 사용할 경우에 대비해 모든 변수와 값들을 넘겨줘야 합니다.

정점 쉐이더는 명령어 목록이 들어있는 함수들로 구현됩니다. 쉐이더의 길이 한계는 쉐이더의 버전과 HLSL 코드를 컴파일하여 생성된 어셈블리에 따라 결정됩니다. 또한 쉐이더 버전에 따라 정점 쉐이더 내부에서 몇몇 명령어의 사용 가능 여부가 결정되기도 합니다. 일반적으로 쉐이더 버전이 높아지면 더 많은 기능을 사용할 수 있고, 낮아지면 더 넓은 시스템 환경을 지원할 수 있습니다. 엑스박스 360 쉐이더 버전은 vs_3_0의 상위 집합이라 하는데, 이는 vs_3_0의 기능들을 가지고 있으면서 그 외에 몇가지 기능도 함께 가지고 있음을 의미합니다.

 

다음은 HLSL로 작성된 정점 쉐이더의 가장 간단한 예제입니다.

float4x4 WorldViewProjection;

float4 VertexShaderFunction(float4 inputPosition : POSITION) : POSITION
{
    return mul(inputPosition, WorldViewProjection);
}

 

이 쉐이더는 모델의 정점에 WorldViewProjection 매트릭스를 곱연산하여 투사 공간에 위치시킵니다. 응용프로그램이 이 패러미터를 적합한 값으로 설정하게 됩니다.

 

보이는 바와 같이 C와 비슷하면서 어셈블리 스타일보다 훨씬 더 보기 좋은 코드가 되었습니다. 아직 이해되지 않는 부분이 있더라도 걱정할 필요 없습니다. 나머지 글에서 차차 일부분이 설명될 것이며 나머진 약간의 연습을 통해 깨우치게 될 것입니다. float4x4 자료형과 float4 자료형은 HLSL의 자료형입니다. mul은 고유 함수라 하고, 두 개의 POSITION 인스턴스는 시맨틱(semantic)이라 불립니다. 다른 자료형을 다루는 또 다른 시맨틱도 있습니다만, 차후에 다루도록 하겠습니다. 지금은 우선 픽셀 쉐이더로 넘어가봅시다.

픽셀 쉐이더

정점 쉐이더가 정점에서 동작하듯이 픽셀 쉐이더는 픽셀에서 동작합니다. 픽셀 하나가 프레임 버퍼에 기록되기 전에 먼저 픽셀 쉐이더를 거치게 됩니다.  이를 픽셀 처리 단계라 부릅니다. 규칙상 픽셀 쉐이더는 단 하나의 색상만을 출력하도록 되어있습니다. 이 색상값은 텍스쳐와 환경광(Ambient Light), 방향광(Directional Light), 그림자, 매터리얼(Material) 종류 등을 고려하여 여러가지 방법으로 산출될 수 있습니다. 이에 사용되는 입력값은 응용 프로그램에서 전달받은 정점 텍스쳐, 쉐이더 패러미터들과 텍스쳐 샘플러에서 전달받은 텍스쳐 데이터입니다. 픽셀 쉐이더의 길이와 복잡도 또한 쉐이더 버전에 따라 컴파일 시 길이 제약을 받습니다.

 

아주 간단한 예제 하나를 봅시다.

float4 PixelShaderFunction() : COLOR0
{
    return float4(1, 0, 0, 1);
}

이 아주 간단한 쉐이더는 각각의 픽셀을 빨강으로 칠하게 됩니다. 픽셀의 색상을 나타내는 반환값 float4(1,0,0,1)은 플롯의 배열형으로 저장되어 각각 RGBA(빨강, 초록, 파랑, 투명도)를 가리킵니다. 실제로 사용되는 픽셀 쉐이더들은 대부분 이렇게 단순하지 않습니다. 보통 약간의 입력 패러미터와 텍스쳐, 조명을 고려한 보다 복잡한 연산이 이루어지게 됩니다.

이펙트 파일

지금까지 쉐이더에 대해 이야기했습니다. 정점 및 픽셀 쉐이더를 작성할 수 있는 능력을 습득하게 되면 다음 단계는 이 두 가지를 한곳에 합치는 것입니다. 이 쉐이더들의 조합과 아울러 그래픽 파이프라인 기능을 조작하는 명령들을 이펙트라 부릅니다.

 

그래픽 파이프라인을 다시 한번 살펴보면 이펙트를 도식의 일부 단계들(정점 처리 단계, 기하 처리 단계, 픽셀 처리 단계, 텍스쳐 샘플러, 픽셀 렌더링의 일부)을 대체하거나 제어할 것으로 예상해 볼 수 있습니다. 정점 처리 단계와 픽셀 처리 단계는 쉐이더 프로그램을 작성하여 실행할 수 있습니다. 다른 블록들도 특정 변수에 값을 지정하는 등의 파이프라인 명령으로 제어할 수 있습니다.

 

이펙트는 또한 다양한 하드웨어의 버전에 맞는 쉐이더를 편하게 작성하게 해줍니다. 이를 위해 이펙트엔 ‘테크닉(Techniques)’이라는 개념이 있습니다. 테크닉이란 하나 또는 여러개의 패스(Pass)의 모음입니다. 각각의 패스는 오브젝트를 렌더링하는 특정한 방법을 정의합니다. 이를 위해서 테크닉과 그 패스들은 여러 전역 변수들과 파이프라인 명령, 텍스쳐 샘플러 명령과 쉐이더 명령을 캡슐화합니다. 이 방법으로 우리는 서로 상이한 하드웨어 기능을 사용하는 각기 다른 버전의 쉐이더를 작성하여 테크닉에 캡슐화 할 수 있습니다. 그러면 응용프로그램이 구동되는 하드웨어에 적합한 테크닉을 런타임에 선택할 수 있게 됩니다. 하이-엔드 급의 장치에서는 보다 복잡한 쉐이더를 사용하고 로우-엔드 급의 장치에서는 간단하고 빠른 쪽을 선택하는 것이죠. 정점 쉐이더와 픽셀 쉐이더는 각각의 함수(VertexShader(정점쉐이더) 와 PixelShader(픽셀쉐이더))로 정의되어 두 파이프라인 변수에 할당되어 있습니다. 

 

이펙트 파일의 일반적인 구조는 다음과 같습니다.

//parameter 선언
[…]

//data type 선언
[…]

//function 선언
[…]

//technique 선언
[…]

글의 나머지에서 각각의 요소에 대한 설명을 이어갈 것입니다.

자료형

HLSL에서 사용 가능한 데이터 타입을 간단하게 살펴보겠습니다. 이 다음 단원에서 자료형을 활용해 변수를 선언하고 패러미터를 사용하는 방법을 알아보겠습니다. 가장 단순한 타입은 스칼라(scalar) 타입으로 아래와 같습니다.

Type

Value

bool

true or false

int

signed integer

half

16-bit floating point

float

32-bit floating point

float 형은 GPU 내부에서 사용되는 네이티브 자료형으로 가장 자주 사용하는 형입니다. 이때문에 inthalf, double과 같은 다른 자료형을 지원하지 않는 GPU에서는 float을 모방(emulate)하여 사용합니다. 정수형(Integers)도 대부분의 경우 여기에 속하며, 이 모방으로 인해 32비트 정수형의 모든 범위가 같은 32비트 크기의 float 형으로 커버되지 않습니다.

 

스칼라 타입 이후로는 벡터(vector) 타입을 알아 볼 차례입니다. 벡터에는 한 개에서 네 개의 스칼라 타입이 저장됩니다. 이 스칼라 타입을 몇 개 가지고 있는지가 벡터형의 이름 바로 뒤에 따라붙게 됩니다. 예를 들어 float3, half2, int4, double2와 같은 식으로 표현하는 것이죠. 벡터 형의 장점으로 사용이 쉽다는 것 외에도 여기에 적용되는 모든 연산이 벡터 형이 가진 모든 요소에 동시에 이루어진다는 점을 들 수 있습니다. 예를 들자면, 두 개의 float4 변수를 더하는 것은 단 하나의 명령으로 각각의 대응하는 요소에 한꺼번에 합산이 수행됩니다. 각각의 컴포넌트에 접근하려면 rgbaxyzw라는 두 가지의 접근자를 활용하면 됩니다. var 라는 이름을 가진 float4가 하나 있다고 가정하자면 var.x 또는 var.r 과 같은 식으로 var의 첫번째 요소에 접근할 수 있습니다. 마찬가지로 y 또는 g로 두번째 요소에 접근할 수 있죠. 보통 그 변수를 위치값으로 사용할 때 xyzw 접근자를 사용할 것이고 색상값으로 사용할 때 rgba 접근자를 사용할 것입니다.

 

다음 자료구조는 컴퓨터 그래픽에서 매우 흔히 사용되는 매트릭스(Matrix)라 불리는 형입니다. 매트릭스형은 벡터와 비슷하게 정의되지만 두 개의 축(차원, Dimension)을 지니고 있습니다. 이름에는 스칼라 자료형의 이름 바로 뒤에 열 수와 영문자 x가 붙고 다음으로 행 수가 붙습니다 : int4x2, double1x3, float4x4 이런 식으로요. 보통 매트릭스를 곱셈에 사용할테지만, 매트릭스의 요소에 접근할 수 있는 방법 또한 있습니다. 첫 번째 방법은 제로 기반 표기법으로, 언더스코어(밑줄) 뒤에 영문자 m, 열 번호, 행 번호를 적어서 표현할 수 있고, 1 기반 표기법으로는 언더스코어 뒤에 바로 열 번호와 행 번호를 기재합니다. 마지막으로 2차배열처럼 대괄호 안에 제로 기반 표기법으로 열 번호와 행 번호를 기입하는 방법이 있습니다. 예시로 mvar이라는 float 형 4x4 매트릭스 하나를 가정하고, 여기의 두번째 열, 세번째 행에 접근하는 표기법들을 살펴보겠습니다. : mvar._m12, mvar._23, mvar[1][2]

 

텍스쳐 타입은 texture라는 키워드로 존재하며 텍스쳐 오브젝트를 나타냅니다. 텍스쳐와 연결된 또 다른 자료형으로 샘플링할 텍스쳐, 사용할 필터 방식 등의 명령을 가지고 있는 샘플러(sampler)가 있습니다. 일반적인 샘플러 타입으로 sampler, sampler1D, sampler2D, sampler3DsamplerCube 가 있습니다. 나중에 텍스쳐와 샘플러의 사용에 대해 보다 깊이있는 내용을 다룰 것입니다.

 

마지막으로 중요한 타입으로 구조체(structure)가 있습니다. 구조체는 사용자가 정의한 여러 자료형의 집합입니다. struct라는 키워드 뒤에 사용하고자 하는 이름을 붙여 정의합니다. 구조체의 멤버에 접근할때는 구조체연산자( . )가 사용됩니다. 예시로, 색상과 위치 멤버를 하나씩 포함하는 구조체를 살펴보겠습니다.

struct demoStruct
{
    float3 position;
    float4 color;
}

HLSL을 작성함에 있어 가장 중요한 자료형들을 알아보았습니다.

고유 함수 (Instrinsic Function)

HLSL은 일반적으로 사용되는 기능에 접근하게 해주는 다양한 고유 함수를 가지고 있습니다. 이 함수들의 패러미터는 함수의 기능에 따라 달라지지만 항상 매트릭스나 샘플러, 스칼라 값 또는 벡터값입니다. 모든 고유 함수의 목록은 여기서 볼 수 있습니다.

HLSL 쉐이더 작성하기 

지금까지 쉐이더를 작성할 때 쌓아올릴 블록들을 살펴보았습니다. 이야기한 바 있지만, 두 종류의 쉐이더가 있습니다. 정점 쉐이더(vertex shader)는 정점을 이동시키고, 픽셀 쉐이더는 화면상에 표현될 최종 색상을 결정하지요. HLSL에서는 다양한 컴퓨터 그래픽적 목표를 위해 만들어진 여러 데이터형을 사용할 수 있습니다. 이 언어는 또한 여러가지 상황에 쓸모있는 함수 목록도 가지고 있습니다. 이제는 이 블록들을 어떻게 쌓아올리는지 알아보도록 합시다.

변수의 선언

다른 프로그래밍 언어들처럼, 변수를 선언하려면 먼저 자료형을 결정하고 그 뒤에 이름을 붙여줘야 합니다. 자료형은 앞서 알아본 어떤것도 될 수 있습니다. 배열은 대괄호([,])를 사용하면 됩니다. 선언과 동시에 초기화할 수도 있습니다. 아래, 선언의 예시를 살펴봅시다.

float float_variable1;
float float_variable2 = 3.4f;
float3 position = float3(0,1,0);
float4 color = 1.0f;
float4x4 BoneMatrices[58];

저장소 클래스 그리고/또는 형 전환자(type modifier)가 변수 선언의 앞에 옵니다. 저장소 클래스 전환자는 변수의 범위(scope)와 수명(lifetime)을 지정합니다.

 

저장소 클래스 전환자는:

  • extern - 전역변수는 쉐이더 외부로부터의 입력입니다; 전역은 기본적으로 extern으로 간주됩니다.

  • noninterpolation - 정점쉐이더로부터의 출력을 픽셀 쉐이더로 전달할 때 보간(interpolate)을 하지 않습니다.

  • shared - 여러 이펙트들 사이에서 공유됩니다.

  • static - 이 지역(local) 변수가 한번 초기화되면 여러 함수 호출간에도 그 값을 유지합니다.

  • uniform - 쉐이더의 실행 전반에 걸쳐 유지되는 상수값을 가집니다;전역 변수들은 기본적으로 uniform으로 간주됩니다.

  • volatile - 자주 바뀌는 값을 말합니다; 지역 변수에만 적용됩니다.

형 전환자는:

  • const - 쉐이더가 이 변수를 수정할 수 없습니다. 선언 시에 초기화되어야 합니다.

  • row_major - 요소 4개가 한 행에 저장됩니다; 하나의 상수 레지스터에 저장됩니다.

  • column_major - 요소 4개가 한 열에 저장됩니다; 매트릭스 연산 최적화

시맨틱과 주석은 차후에 다루도록 하겠습니다.

쉐이더 입력

쉐이더가 결과값을 만들어내기 위해선 입력값이 있어야 합니다. 이 데이터는 고정형(uniform)인 것과 비정형(varying)인 것 두 가지의 형태를 가질 수 있습니다.

고정형 입력(Uniform Inputs)

고정형 입력은 여러 차례에 걸친 쉐이더의 실행에도 변하지 않는 값들로 구성됩니다. 일반적으로 고정형 입력은 매터리얼 색상, 텍스쳐, 월드 변환(transformation)등을 가집니다.

 

고정형 입력을 명시하는 두 가지 방법이 있습니다. 첫번째는 전역 변수를 통하는 방법으로, 가장 많이 쓰입니다. 쉐이더 함수 바깥에서 이들을 선언한 뒤 함수 안에서 사용하는 것이죠. 두번째 방법은 입력 패러미터값에 uniform 저장소 클래스 전환자를 붙이는 것입니다.

float4x4 WorldMatrix : WORLD; //WORLD라는 시맨틱이 붙음으로써 전역변수가 됩니다

float4 AFunction(PSIn input, uniform another_var)
{
   //여러번 실행될 동안 값 WorldMatrix와 another_var는 변화하지 않는 상수값을 가집니다.
}

위 코드에서 우린 another_var 를 uniform 전환자로 선언했습니다. 이는 another_var 를 전역변수처럼 동작하게 합니다.

 

uniform 변수들은 상수 테이블에 저장되어 응용프로그램에서 접근할 수 있습니다. 우린 여전히 XNA에서 EffectParameters를 활용해 접근할 수 있습니다. 이펙트 클래스는 패러미터 멤버 안에 패러미터의 컬렉션을 가집니다. 그로 인해 우린 전역변수에 일련번호, 이름, 시맨틱을 통해 접근할 수 있습니다. 우리가 이펙트 하나를 effect라는 변수로 불러왔다고 가정하고 아래 코드를 봅시다. WorldMatrix 변수에 접근하는 세가지 방법을 볼 수 있습니다(위의 예시에서).

effect.Parameters[0]
effect.Parameters[“WorldMatrix”]
effect.Parameters.GetParameterBySemantic(“WORLD”)

응용프로그램으로부터 전달된 패러미터값을 수정하려면 SetValue 함수를 사용합니다. 인자값은 해당 쉐이더의 변수와 동일한 자료형을 사용합니다.

//맞는 예시
effect.Parameters[“WorldMatrix”].SetValue(Matrix.CreateTranslation(10,0,10));

//잘못된 예시 – runtime error를 일으킵니다
effect.Parameters[“WorldMatrix”].SetValue(Vector3.Zero);

GetValueXXX 함수를 사용해 변수의 값을 읽을 수 있긴 하지만 퍼포먼스상 사용하지 않기를 추천합니다. XXX 부분은 읽어들일 타입을 말합니다.

effect.Parameters[“WorldMatrix”].GetValueMatrix();

비정형 입력(Varying Inputs)

비정형 입력은 각 실행마다 달라지는 값을 나타냅니다. 예를 들어 정점 쉐이더가 매번 실행될 때마다 위치값과 텍스쳐 좌표등의 입력값이 달라집니다.

 

비정형 입력 패러미터들은 쉐이더 함수에서 입력 패러미터들로 정의됩니다. 쉐이더를 컴파일하기 위해 각각의 패러미터는 시맨틱 표시가 되어야 합니다. 패러미터에 시맨틱이나 uniform 전환자가 붙어있지 않다면 컴파일이 실패합니다.

 

그럼 시맨틱이란 무엇일까요? 시맨틱이란, 데이터가 그래픽 파이프라인을 어떻게 지나다니는지를 명시하는 이름입니다. 예를 들어 POSITION0 시맨틱은 어떤 변수가 정점의 위치를 나타내는 데이터를 가지고 있음을 명시합니다. 정확히 어떻게 데이터 덩어리 속에서 이 데이터를 추출해 적합한 시맨틱을 가진 변수에 할당하는지는 이 글의 뒷부분, 정점 선언(Vertex Declarations)) 단원에서 다시 다룰것입니다. 정점(버텍스) 쉐이더는 응용프로그램에서 전달받은 버텍스 버퍼의 데이터를 연결하기 위해 시맨틱을 사용하는 반면, 픽셀 쉐이더는 정점 쉐이더의 출력값 데이터를 연결하기 위해 시맨틱을 사용합니다.

 

그래픽 파이프라인이 데이터를 어떻게 이동시킬지 알리기 위해 시맨틱을 입력값과 출력값 양쪽에 명시해야 합니다. 변수에 시맨틱을 할당하기 위해선 두가지 방법이 사용됩니다 : 시맨틱을 콜론( : )으로 변수의 선언 뒤에 붙이는 것, 또는 데이터 구조체의 각각의 멤버 뒤에 시맨틱을 붙여 정의하는 것. 함수의 출력값에도 아래와 같이 시맨틱을 붙여 명시할 수 있습니다.

float4 PixelShaderFunction(float2 TexCoords:TEXCOORD0) : COLOR0
{}

위의 코드에서 TEXCOORD0 시맨틱은 변수 TexCoords 가 텍스쳐 좌표의 첫번째 채널을 포함해야 한다고 명시하고 있습니다. 함수에 붙어있는 시맨틱 COLOR0 는 이 함수의 반환값이 해당 픽셀의 색상이 될 것임을 의미합니다. 만약 함수의 패러미터 리스트에서 출력값을 명시하고 싶다면 외부 변환자를 붙여야 합니다. 여러분은 앞으로 종종 아래와 같은 구조체로 명시된 입력값과 마주칠 것입니다.

struct VertexShaderInput
{
    float4 Position : POSITION0;
    float3 Normal : NORMAL0;
    float2 TexCoord : TEXCOORD0;
};

struct VertexShaderOutput
{
    float4 Position : POSITION0;
    float2 TexCoord : TEXCOORD0;
    float3 Normal : TEXCOORD1;
};

VertexShaderInput 구조체는 세 개의 변수를 가지는데, 각각은 입력 버텍스 스트림의 특정 데이터에 링크됩니다 : 정점의 위치와 노말(법선), 텍스쳐 좌표이지요. 만약 시맨틱으로 명시된 요소들이 버텍스 선언과 버텍스 버퍼에 전부 포함되어있지 않다면 해당 변수들은 0값으로 세팅될 것입니다. VertexShaderOutput 구조체는 쉐이더로부터 데이터를 출력하고 픽셀 쉐이더가 읽어들일 수 있게 합니다.

이 구조체들을 사용하는 버텍스 쉐이더는 아래와 같이 선언합니다.

VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
    VertexShaderOutput output;
    [...]//입력값에 기반해 출력값들을 계산합니다.
    return output;
}

정점 쉐이더에 의해 출력된 데이터는 픽셀 쉐이더에 입력값으로 제공되기 전에 보간 과정을 거칩니다. 정점 쉐이더의 출력값에서 다른 값은 생략하더라도 위치(Position) 값만은 POSITION 시맨틱을 사용해 무조건 출력되어야 합니다.

 

위의 데이터를 사용하기 위해 픽셀 쉐이더는 다음과 같은 선언을 가질 수 있습니다.

float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0​

이 함수 내에서 여러분은 입력된 구조체의 멤버들 중 위치값을 제외한 나머지를 사용할 수 있습니다. 위치값(Position)은 픽셀 쉐이더 상에서 숨겨져있으며 이를 사용하려 하면 컴파일 에러에 걸리게 됩니다. 픽셀 쉐이더는 언제나 COLOR0 시맨틱의 float4 를 출력해야 합니다.

 

여기까지 어떻게 데이터가 응용프로그램에서 전달되어 정점 쉐이더와 픽셀 쉐이더에 도달하는 지 알아보았습니다. 화면에 오브젝트를 그릴 때 우린 항상 텍스쳐를 활용하려 하죠. 다음 단원에서는 텍스쳐와 샘플러를 어떻게 쉐이더의 입력값으로 사용하는지 알아보겠습니다.

텍스쳐와 샘플러

텍스쳐에서 데이터를 읽어들이려면 텍스쳐 형의 멤버를 두는 것 만으로는 부족합니다. 여기에 샘플러(Sampler)와 샘플러 인스트럭션(Sampler Instruction)이 필요하죠. 샘플러는 어떤 텍스쳐에서 어떻게 읽어들일지를 명시하고 샘플러 인스트럭션은 주어진 좌표에 대해 실제 읽기를 수행합니다.

 

샘플러 선언은 샘플러 상태가 포함되어야 합니다. 가장 중요한 샘플러의 요소들을 나열해 보겠습니다.

  • AddressU - 텍스쳐 좌표상의 U 값을 명시합니다.

  • AddressV - 텍스쳐 좌표상의 V 값을 명시합니다.

  • MagFilter - 확대 샘플링 시 적용할 필터를 명시합니다.

  • MinFilter - 축소 샘플링 시 적용할 필터를 명시합니다.

  • MipFilter - 밉맵에서 샘플링 할 때 적용할 필터를 명시합니다.

아래에서 샘플러 선언의 예시를 살펴보겠습니다.

Texture DiffuseTexture;

sampler TextureSampler = sampler_state
{
    Texture = (DiffuseTexture);
    MinFilter = Linear;
    MagFilter = Linear;
    MipFilter = Linear;
    AddressU = Wrap;
    AddressV = Wrap;
};

이 샘플러는 DiffuseTexture 텍스쳐에서 데이터를 읽어오며, 선형(Linear) 필터링을 사용하고 0~1 범위 바깥의 텍스쳐 좌표는 바둑판식 반복을 적용시키고 있습니다.

 

이제 텍스쳐와 샘플러가 있으니 샘플러를 사용해 텍스쳐의 데이터를 읽어봅시다. 이를 위해 샘플링 함수를 사용합니다(앞서 ‘고유 함수(Instrinsic functions)’ 단원에서 살펴보았습니다). tex2D, tex3D, texCUBE 가 가장 보편적입니다. 이들은 샘플러 변수와 텍스쳐 좌표를 패러미터로 삼습니다. tex2D 의 경우 float2, tex3DtexCUBE 의 경우 float3 가 사용됩니다. TextureSampler 샘플러와 정점 쉐이더로부터 넘겨받은 텍스쳐 좌표를 활용해 2D 텍스쳐에서 값을 읽어오는 예시를 살펴봅시다.

float4 PixelShaderFunction(float2 TexCoords : TEXCOORD0) : COLOR0
{
    return tex2D(TextureSampler, TexCoords);
}

흐름 제어(Flow Control)

쉐이더 모델에 따라서는 HLSL도 분기문과 루프문 같은 흐름 제어 명령어를 지원합니다. 

 

비디오 하드웨어를 위한 가장 간단한 형태의 분기문은 정적 분기(Static branching) 입니다. 이는 특정 코드 블록이 연산될지 말지를 일부 쉐이더 상수에 근거해 결정합니다. 각각의 드로우 콜 사이에서 해당하는 상수값을 지정해 쉐이더가 각각의 모델을 다르게 그리게 할 수 있습니다. 그러나 코드 블록의 연산 여부는 오브젝트 하나 당 한번씩만 결정하게 됩니다.

 

CPU에서 분기문을 다루어 본 프로그래머들에게는 분기문이 친숙할 수 있습니다. 비교 조건은 런타임 시 각각의 픽셀 또는 정점에 대해 수행됩니다. 따라서 모델의 각 부위에서 서로 다른 코드를 사용할 수도 있습니다. 이를 동적 분기(Dynamic branching)라고 합니다. 동적 분기는 더 유연하고 편리한데 반해 성능 저하를 일으킬 수 있으며, 지원되지 않는 하드웨어가 있을 수 있습니다.

시맨틱과 주석 소개

앞서서 변수를 정의하기 위해 시맨틱을 덧붙이고 그래픽 파이프라인에서 입력과 출력을 연결하는 것을 살펴보았습니다. 시맨틱은 언어의 맥락과 관련이 있으며 일부 변수에 필수적입니다.

 

여러분은 다른 쉐이더의 다른 변수를 설정하는 방법을 시맨틱으로 통합할 수 있습니다.  예를 들어 어떤 쉐이더가 WorldMatrix 를 변수명 wrld, world_mat 등등으로 사용하고 있을 때 WORLD 시맨틱을 사용해 해당하는 변수에 접근할 수 있습니다.

 

시맨틱에 더해 HLSL은 주석 개념또한 제공합니다. 주석은 테크닉(Techniques)이나 패스(Pass), 패러미터에 붙일 수 있는 사용자 정의 데이터입니다. 주석은 HLSL 컴파일러와는 상관이 없습니다. 최종 쉐이더 코드에도 영향이 없으면서 유연하게 패러미터에 정보를 기술할 수 있는 방법입니다. 응용프로그램은 어떤 방식으로도 이 데이터를 읽고 사용할 수 있습니다. 주석 선언은 꺾쇠 괄호로 범위가 지정됩니다.

 

아래 예시에서 텍스쳐에 주석을 달아 이 텍스쳐에 로드되어야 하는 파일을 명시했습니다.

texture Texture <string filename = "myTexture.bmp">

주석이란 그저 꾸밈새에 지나지 않습니다. 파일 ‘myTexture.bmp’는 자동으로 변수에 로드되지 않습니다. 그러나 이 주석을 사용함으로써 우린 이 정보를 응용프로그램에 전달할 수 있습니다. EffectParameter 클래스의 Annotations 멤버에 접근해 주석을 읽을 수 있습니다. 아래 코드에서 텍스쳐 변수에 붙은 주석을 어떻게 사용하는지를 묘사하고 있습니다.

Effect effect;

Texture2D texture;
//주석을 읽어들입니다.
String filename = effect.Parameters["Texture"].Annotations["filename"].GetValueString();
[...]//텍스쳐 변수에 명시된 파일 명으로 텍스쳐를 불러옵니다.
//텍스쳐를 이펙트 패러미터로 지정합니다.
effect.Parameters["Texture"].SetValue(texture);

이런 식으로 다른 이펙트 파일들과도 상호작용할 수 있습니다. 게임에서도, 다른 툴에서도요.

테크닉(Techniques)과 패스(Pass)

이펙트 파일 하나는 하나 또는 그 이상의 테크닉을 포함할 수 있습니다. 각각의 테크닉은 렌더링 방식을 정의하는 모든 정보를 캡슐화하여 가지고 있으며, 하나 또는 그 이상의 패스를 가질 수 있습니다. techniques 키워드를 사용해 테크닉을 선언합니다. 

technique Technique_0
{
    //패스(pass)의 목록
}

technique Technique_1
{
    //패스(pass)의 목록
}

XNA 코드에서 테크닉은 Techniques 멤버로 접근할 수 있으며 현재 활성화된 테크닉은 CurrentTechnique 로 접근할 수 있습니다. Techniques 은 때로 다른 쉐이더 모델을 위해 상이한 쉐이더를 제공할 때에도 사용됩니다. 하드웨어 설정에 따라 이펙트를 초기화한 시점으로부터 이런 식으로 테크닉을 사용할 수 있습니다.

 

앞서 언급했듯이 테크닉은 하나 또는 그 이상의 패스로 구성됩니다. 하나의 패스는 렌더에 필요한 상태 할당(state assignments)을 가지고 있습니다. 여기엔 정점 쉐이더와 픽셀 쉐이더, 렌더 상태가 포함됩니다. 패스는 대부분 모델 또는 오브젝트의 최종 렌더링이 추가적인 처리를 거쳐 통합될 필요가 있을 때 사용됩니다. 예를 들어 방향광(directional lighting)을 고려하는 패스와 지점광(Spotlights)을 고려하는 패스가 따로 있을 수 있고, 최종적으로 이들의 결과를 통합할 수 있습니다. 그러나 단순히 테크닉 안에 여러 패스를 선언하는 것만으로는 이들을 자동적으로 실행시킬 수 없습니다. 응용프로그램의 코드에서 테크닉 안의 패스 목록 전체를 반복하여야 하고 각각의 패스마다 드로우 콜 선언을 해야 합니다.

 

아래 코드는 여러 패스를 가지고 있는 테크닉의 모습을 보여줍니다.

technique example_technique
{
    pass Pass0
    {
        ...
    }

    pass Pass1
    {
        ...
    }
}

XNA 응용프로그램은 아래와 같은 방법으로 패스들을 반복, 실행할 것입니다.

effect.Begin();
foreach (EffectPass pass in effect.CurrentTechnique.Passes
{
    pass.Begin();
    //드로우 콜 선언
    pass.End();
}
effect.End();

정점 선언(Vertex Declarations)

앞서 시맨틱이 어떤 파이프라인 단계에서 다음 단계로 어떻게 데이터를 링크시키는지 살펴보았습니다. 하나 분명하지 못했던 점은 “특정 변수에 연결될 버텍스 스트림의 값을 그래픽 파이프라인은 어떻게 찾아내는가” 였습니다. 예를들어 우린 VertexShaderInput.Position 은 정점의 위치값을 가지고 있을 것으로 알고 있습니다만, 그래픽카드가 정점값이랍시고 전송받은 N개의 바이트들 중 어디가 포지션인지, 노말이나 텍스쳐 좌표같은 나머지 정보들은 어디인지 알아차릴 방법은 없습니다.

 

여기서 정점 선언이 등장합니다. 정점 선언은 응용프로그램 코드로부터 초기화된 구조체로, 그래픽 장치에 설정되어 바이트 스트림이 어떻게 구성될지를 지시합니다.

 

정점 선언은 정점 요소(VertexElement)의 배열로 표현됩니다. 정점 요소는 정점의 한 요소를 정의하는 기본 구조입니다. 예를 들어 위치, 노말 및 텍스쳐 좌표에 대한 정보가 포함된 정점은 세 개의 정점 요소를 가질 것입니다. 하나는 포지션, 하나는 노말, 나머지 하나는 텍스쳐 좌표이겠지요. 아래에서 정점 요소의 속성을 살펴봅시다.

  • stream - 사용하는 스트림 인덱스

  • offset - 첫번째 데이터로부터의 오프셋(이격값)

  • element format - 정점 데이터 타입과 크기를 정의

  • element method - 테셀레이션 방식 정의

  • element usage - 데이터의 쓰임새 정의

  • usage index - 동일한 usage 를 가진 여러개의 데이터가 존재할 수 있습니다. usage index는 그들 각자를 구분하기 위해 사용됩니다.

앞부분의 세 속성으로 이 요소의 데이터가 바이트 스트림의 어디에 어떻게 위치하고 있는지 알 수 있습니다. usage usage index 는 데이터의 논리적 의미를 명시하며 시맨틱으로 직접적으로 매핑됩니다. 아래 선언을 살펴봅시다.

VertexElement texCoords2 = new VertexElement(0,12,
	VertexElementFormat.Vector2,VertexElementMethod.Default,
	VertexElementUsage.TextureCoordinate,2);

이 코드는 스트림의 12번째 바이트에서 시작하는 정점 요소를 만들어냅니다. 두 개의 float이 텍스쳐 좌표를 나타내고 usage index 로는 2번을 사용합니다. 이는 TEXCOORD2로 해석됩니다. 정점 선언이 장치에서 이런 정점 요소를 가지고 있을 때 그래픽 하드웨어는 스트림의 12번째 자리에서 8바이트(float 두 개) 만큼 읽어들여, 쉐이더에서  TEXCOORD2 시맨틱을 붙여놓은 변수가 존재할 경우 거기에 할당합니다.

 

XNA 는 여러 종류의 정점 타입들이 언어에 사전정의 되어있고, 여기 첨부된 정점 요소 배열에 VertexElements 멤버를 통해 접근할 수 있습니다. 그래서 정점 선언을 만들때 다음과 같이 VertexPositionColorTexture 형으로 정점들을 명시합니다.

VertexDeclaration vdecl = new VertexDeclaration(
       GraphicsDevice,
       VertexPositionColorTexture.VertexElements);

때론 여러분 스스로의 정점 구조체와 정점 요소의 배열들을 어떻게 정의하는지 알아야 할수도 있지만 그런 경우는 매우 드물것입니다.

XNA 이펙트(Effect) 템플릿 설명

이제 이론적 배경이 충족되었으니 XNA Game Studio 3.1에서 제공하는 이펙트 파일의 템플릿을 살펴봅시다. 템플릿을 확인하기 위해 먼저 XNA 프로젝트를 만들고 Content에 새로운 이펙트 파일을 추가합시다. 아래와 비슷한 코드를 볼 수 있을것입니다.

float4x4 World;
float4x4 View;
float4x4 Projection;

// TODO: 이펙트의 페러미터를 넣으세요.

struct VertexShaderInput
{
    float4 Position : POSITION0;
    // TODO: 텍스쳐 좌표나 정점 컬러같은 입력 채널을 적으세요.
    // 
};

struct VertexShaderOutput
{
    float4 Position : POSITION0;
    // TODO: 색상이나 텍스쳐 좌표같은 정점 쉐이더 출력을 적으세요.
    // 이 값들은 자동으로 삼각형으로 보간되어 
    // 픽셀 쉐이더에 전달될 것입니다.
};

VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
    VertexShaderOutput output;
    float4 worldPosition = mul(input.Position, World);
    float4 viewPosition = mul(worldPosition, View);
    output.Position = mul(viewPosition, Projection);
    // TODO: 정점 쉐이더 코드를 적으세요.
    return output;
}

float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
    // TODO: 픽셀 쉐이더 코드를 적으세요.
    return float4(1, 0, 0, 1);
}

technique Technique1
{
    pass Pass1
    {
        // TODO: 렌더 명령을 설정하세요.
        VertexShader = compile vs_1_1 VertexShaderFunction();
        PixelShader = compile ps_1_1 PixelShaderFunction();
    }
}

이펙트 파일의 처음에는 세 개 패러미터의 정의가 담겨있습니다. 이 패러미터들은 float4x4 형이고 각각 월드와 뷰, 프로젝션 매트릭스를 나타냅니다. 그 아래로 두 가지의 구조체 정의가 뒤따르는데, VertexShaderInput 은 정점 쉐이더로 들어가는 입력값을 정의하고 VertexShaderOutput 은 정점 쉐이더에서 나오는 출력값을 정의합니다. 이 각각의 구조체는 GPU에게 그 의미를 알리기 위한 POSITION0 시맨틱이 붙어있는 위치값 (Position)이라는 채널을 포함하고 있습니다.

 

다음으로 정의된 함수는 정점 쉐이더입니다. 이야기했듯이 이 함수는 VertexShaderInput 을 입력값으로 받고, VertexShaderOutput 을 반환합니다. 입력값으로 주어진 위치값과 앞서 언급한 세 개의 매트릭스가 서로 곱연산된 값이 출력값으로 사용됩니다. 이 곱셈과정은 모든 정점 쉐이더에서 가장 흔하게 행해집니다.

 

픽셀 쉐이더가 될 운명인 그 다음 함수는 결과값으로 COLOR0 시맨틱이 붙은 float4 값을 반환합니다. 이 함수는 VertexShaderOutput 구조체를 입력값으로 사용합니다. 이 외에도 여러 방법으로 작성될 수 있지만, 이와같이 데이터를 정리하면 깔끔하고 유지보수가 쉬운 코드가 됩니다. 이 픽셀 쉐이더는 기본적으로 빨간색만을 반환하게 되어있습니다.

 

마지막으로 테크닉이 정의되어있습니다. 이 테크닉은 위의 두 함수를 버텍스 쉐이더와 픽셀 쉐이더로 사용하는 하나의 패스를 가지고 있습니다.

 

이 템플릿을 당신의 이펙트 파일을 작성할 때 시작점으로 사용할 수 있습니다.

맺음말

부디 이 짧은 소개글이 HLSL와 쉐이더 프로그래밍을 배우려는 이들에게 도움이 되길 바랍니다.

 

(역자 주 : 오역, 의미 불명, 용어 혼동, 발번역 등에 대해서는 mochanpowder@gmail.com 으로 연락주세요)

Comments