home lecture link bbs blame

◈Introduction to Shader Programming◈

1 쉐이더 개요

Shader(쉐이더) 프로그래밍은 GPU(Graphic Processing Unit)에 대한 3D 장면 처리를 고정 기능 파이프라인을 이용하지 않고 Assembly 언어와 같은 저수준 언어 또는 C언어와 같은 고수준 언어(HLSL: High Level Shading Language)로 작성된 프로그램이라 할 수 있습니다.

비디오 제어기(디스플레이 제어기) CPU에서 처리한 픽셀 데이터를 화면에 출력하는 단순한 역할에서 메모리의 가격이 낮아지고 컴퓨터의 하드웨어 성능이 발전하면서 화면에 출력할 픽셀의 데이터 일부를 CPU 대신 처리하게 되어 단순 기능의 비디오 제어기 대신 디스플레이 프로세서(Display Processer)로 발전하게 되었습니다. 이 디스플레이 프로세서가 라이팅, 정점 변환, 픽셀 샘플링 등을 CPU 대신 처리하게 되었는데 이것을 CPU와 대응되는 개념으로 그래픽 출력을 전담하는 장치를 GPU(Graphic Processing Unit)라고 합니다.

처음에 GPU는 기술적인 한계와 비용에 의해서 그래픽에 대한 모든 처리 과정이 고정(Fixed) 되어 있었습니다. 이것을 고정 기능 파이프라인(Fixed Function Pipeline)으로 부릅니다. GPU도 발전하면서 고정 기능 파이프라인의 일부를 사용자가 프로그램을 작성할 수 있도록 구성되게 되었는데 이것을 프로그램 가능한 파이프라인(Programmable Pipeline)으로 부르게 되었으며 쉐이더는 GPU가 제공하는 Programmable Pipeline의 정점과 Pixel의 처리를 사용자의 프로그램으로 처리하는 것입니다.

 

C/C++ 같은 고급 언어는 inline Assembly를 이용해서 저급 언어도 일부 지원하고 있습니다.

 

#include <stdio.h>

char*   s1 = "Hello";

char*   s2 = "World";

char*   f  = "%s %s\n";

_asm {

        mov eax, s2;

        push eax;

        mov ebx, s1;

        push ebx;

        mov ecx, f;

        push ecx;

        call printf;

 

만약 여러분이 Microsoft DirectX Shader를 사용하면 C언어와 유사한 문법으로 쉐이더 프로그램을 쉽게 작성할 수 있습니다. 또한 DirectX의 쉐이더는 고급 언어뿐만 아니라 어셈블리어 형태의 저급 언어 문법이 지원이 되어 동시에 활용할 수도 있습니다.

 

우리가 C/C++ 등으로 작성하는 프로그램은 실제로 CPU가 처리하는 명령어들입니다. 그런데 우리는 CPU를 완벽하게 이해하지 않고도 프로그램을 작성할 수 있습니다. 이것은 컴파일러라는 도구에 의해서 사람과 가까운 형태의 언어를 CPU가 처리할 수 있는 기계 코드로 바꾸어 주기 때문입니다.

GPU도 제조하는 회사마다 고유의 명령어들이 있을 수 있어서 같은 계열이 아니면 다른 문법 또는 다른 명령어로 프로그램을 작성해야 합니다. 그런데 DirectX Shader를 지원하는 그래픽 카드에 대해서는 여러분이 쉐이더 프로그램을 작성할 때 그래픽 카드의 특성과 세부적인 내용을 생각하지 않고 동일한 문법으로 GPU를 프로그램 할 수 있습니다.

 

고정 기능 파이프라인을 가지고 게임 프로그램을 만들 때 발생하는 문제들은 단순히 값을 바꾸어서 해결되는 경우도 있습니다. 하지만 쉐이더는 이렇게 단순히 값을 바꾸는 반복 작업으로 문제 해결이 잘 안됩니다. 이것은 쉐이더는 GPU의 처리 과정을 직접 작성하는 것이라서 각 단계의 처리 절차를 알고 있지 않으면 문제의 원인과 해결점을 찾을 수 없기 때문입니다.

여러분이 능숙하게 쉐이더를 사용할 수 있는 능력을 키우는 가장 확실한 방법은 먼저 GPU의 처리 과정을 분명하게 이해 하는 것입니다. GPU의 처리 과정만 이해해도 여러분은 올바른 쉐이더 프로그램을 작성할 수 있을 뿐만 아니라 다른 사람이 작성한 것도 쉽게 활용할 수 있습니다.

또한 선형 대수(Linear Algebra) 정도의 수학적 지식이 있어야 합니다. DirectX Shader에서 제공 되는 함수들은 거의 기본적인 함수들 밖에 없습니다. 여러분은 이 기본적으로 제공되는 함수들을 조합해서 정점의 변환, 조명 효과, 픽셀 처리 등을 작성해야 합니다. 예를 들어 조명 효과의 퐁 반사는 쉐이더로 작성한다면 불과 몇 줄도 안되지만 여러분이 벡터의 내적과 power() 함수를 알지 못하면 그림의 떡일 수 밖에 없습니다. 따라서 수학적인 능력이 부족하다 느끼면 틈틈이 게임에 관련된 수학을 찾아보고 연습하기 바랍니다.

 

GPU의 처리 과정을 이해하는 여러 방법 중에서 고정 기능 파이프라인에서 작성한 것을 쉐이더로 프로그램 가능한 파이프라인에 맞게 바꾸어 보는 것이 가장 쉽고 빠릅니다. 고정 기능 파이프라인의 처리 과정을 간단히 복습하면 3D 장면을 연출하기 위해 그래픽 파이프라인에 입력된 정점 데이터는 최초로 정점 처리(Vertex Processing)을 진행 합니다. 정점 처리 과정은 정점의 월드변환 → 정점 블렌딩 → 카메라 공간으로 뷰 변환 → 포그 결합 → 조명 & 재질 결합 → 정규 변환(장치 독립의 정규좌표 변환) → 뷰 포트(View port) 변환 → 래스터라이징(Rasterizing) 변환 입니다.

 

래스터라이징을 거치면 정점의 데이터는 픽셀로 바뀌게 됩니다. 이러한 데이터는 또다시 픽셀 처리(Pixel Processing)을 거치며 픽셀 처리는 샘플링(Sampling) Multi texturing Alpha Test Depth Test Stencil Test Pixel fog Alpha Blending 등을 거처 최종적으로 백 버퍼에 저장이 됩니다.

 

<고정 기능 파이프라인과 프로그램 가능한 파이프라인 비교>

 

이 중에서 정점의 월드변환에서 래스터라이징 전까지에 해당하는 과정을 프로그램 하는 것을 정점 쉐이더(Vertex Shader) 프로그래밍이라 합니다. 그리고 샘플링에서 멀티텍스처링까지 픽셀 처리에 대한 프로그램을 픽셀 쉐이더(Pixel Shader) 프로그래밍이라 합니다. , 정점 쉐이더와 픽셀 쉐이더는 정점 처리와 픽셀 처리의 일부 만을 프로그램 하는 것을 의미합니다.

또한 정점 쉐이더로 처리하는 과정과 픽셀 쉐이더로 처리하는 과정은 독립적으로 진행을 합니다. 이것은 또한 고정 기능 파이프라인의 픽셀 처리와 프로그램 가능한 파이프라인의 픽셀 처리에서 입력되는 정점 처리 후의 데이터가 정점 쉐이더로 처리되었던 결과인지 아니면 고정 기능 파이프라인으로 처리되었던 결과인지를 구분하지 않는다는 것입니다. 이러한 이유로 때로는 정점 처리는 고정 기능 파이프라인으로 처리하고 픽셀 처리는 픽셀 쉐이더를 사용하거나 정점 쉐이더를 사용하고 픽셀 처리는 고정 기능 파이프라인으로 처리하기도 합니다.

 

같은 개수의 명령어를 처리하는 것이라면 고정 기능 파이프라인이 더 빠를 수 있다고 할 수 있지만 쉐이더를 사용하더라도 느려지지 않는다.”라는 것입니다. 우리가 쉐이더를 사용하는 가장 큰 이유는 고정 기능 파이프라인에서 처리하지 못하는 내용을 쉐이더 프로그램을 통해 극복하는 것입니다.

 

지금까지 대충 쉐이더의 역할과 내용을 살펴보았습니다. 이제 본격적으로 정점 쉐이더와 픽셀 쉐이더를 공부해봅시다.

 

 

2 정점 쉐이더(Vertex Shader)

2.1 간단한 정점 쉐이더

정점 쉐이더 프로그래밍은 정점 처리 과정을 프로그램 가능한 파이프라인에 대한 프로그램입니다. 쉐이더가 처음 만들어졌을 때는 GPU에서 지원되는 기능이 많지 않아서 Assembly 형태의 언어로 작성해야만 했습니다.

다음 코드는 C언어의 "Hello world"에 해당하는 가장 간단한 정점 쉐이더 코드 입니다.

 

"vs_1_1                \n"

"dcl_position  v0      \n"

"mov oPos, v0          \n"

 

쉐이더를 어셈블리 형태의 저급 언어로 작성할 때 제일 먼저 여러분은 쉐이더 컴파일 버전을 명시해야 합니다. 첫 번째 줄의 vs_1_1(또는 vs.1.1)은 정점 쉐이더 버전 1.1 로 쉐이더를 작성했다는 의미이며 이 버전을 이용해서 이후의 쉐이더 코드를 컴파일 합니다.

정점 쉐이더 버전은 1.1, 1.2, 1.3, 2.0, 2.x 3.0 등이 있으며 쉐이더 명령어는 버전마다 명령어들이 조금씩 다르기 때문에 이것을 어떤 쉐이더 버전을 사용하고 있는지 꼭 명시해야 합니다.

두 번째 줄의 "dcl_" 으로 시작되는 문장은 GPU에 전달되는 정점 데이터가 처리되기 전에 임시로 머무는 입력 값을 선언하고 저장하는 레지스터(v로 시작)를 지정하는 것입니다.

앞의 "dcl_position v0" GPU에 입력된 정점 데이터의 위치를 v0 레지스터에 지정한다 의미입니다. 입력 값을 저장하는 레지스터는 v0~v12까지 있습니다.

세 번째 줄의 mov는 데이터의 복사를 지시하는 명령어 입니다. "mov oPos, v0" v0에 저장된 x, y, z, w 출력 레지스터 oPos에 저장하라는 의미입니다. 여기서 중요한 것은 정점 쉐이더에서 처리한 후의 위치에 대한 결과 값은 반드시 출력 레지스터에 복사를 해야 한다는 것입니다. 만약 여러분이 출력 레지스터에 어떤 값도 지정하지 않으면 쉐이더 프로그램은 컴파일이 완료되지 않습니다.

 

 

<간단한 정점 쉐이더. s0v_01_vertex01.zip>

 

앞의 쉐이더 코드는 입력레지스터에 저장된 정점 위치를 그대로 출력 레지스터에 저장하도록 했습니다. 만약 여러분이 정점의 좌표를 [-1, -1, 0] ~ [1, 1, 1] 범위로 작성했다면 앞의 그림과 같은 삼각형을 출력 할 수 있습니다. 대부분의 그래픽 카드는 색상을 지정하지 않으면 흰색을 출력하는데 간혹 앞의 오른쪽 그림과 같이 검정색 삼각형이 출력 될 수도 있습니다.

s0v_01_vertex01.zipCShaderEx::Create() 함수에서 삼각형 출력에 대한 정점 쉐이더 작성 예를 볼 수 있습니다.

 

이번에는 이전의 코드에 색상이 출력되도록 쉐이더 코드를 작성해 봅시다. 고정기능 파이프라인에서는 정점에 색상이 있어야 되지만 쉐이더는 직접 색상을 지정할 수 있습니다. 이렇게 직접 쉐이더 색상을 지정하려면 상수 레지스터를 이용해야 합니다. 상수 레지스터의 값을 설정하는 방법은 쉐이더 코드에서 지정하는 방법과 외부에서 지정하는 방법 2가지가 있습니다. 다음은 쉐이더 코드 내부에서 상수 레지스터에 색상을 지정하고 출력하는 예입니다.

 

"vs_1_1                \n"

"def c10, 1, 1, 0, 1   \n"

"dcl_position    v0    \n"

"mov oPos, v0          \n"

"mov oD0, c10          \n"

 

두 번째 줄의 def는 상수 레지스터에 값을 지정하는 명령어로 " def c10, 1, 1, 0, 1"은 상수 레지스터 c10 r, g, b, a = (1, 1, 0, 1)로 저장하라는 명령어 입니다.

상수 레지스터는 c로 시작을 하며 c constant의 첫 글자입니다. 마지막의 "mov oD0, c10"은 출력 레지스터 oD0에 상수레지스터에 저장된 값을 복사를 지시하는 것입니다.

 

<상수 레지스터를 사용한 색상 출력. s0v_01_vertex02.zip>

 

이 번에는 정점이 색상을 가지고 있다고 가정하고 정점의 색상을 출력해 봅시다. 여러분은 이전 쉐이더 코드에서 상수 레지스터의 값 대신 입력 레지스터에 정점 색상을 지정하고 이것을 출력 레지스터 oD0에 복사하면 됩니다.

 

"vs_1_1              \n"

"dcl_position    v0  \n"

"dcl_color       v1  \n"

"mov oPos, v0        \n"

"mov  oD0, v1        \n"

 

세 번째 줄의 "dcl_color v1" 정점의 Diffuse 값을 입력 레지스터 v0에 저장을 지시하는 것이며 마지막 "mov oD0, v1"은 출력 레지스터 oD0 v1의 값 복사를 지시하는 것입니다.

 

<정점의 Diffuse 출력. s0v_01_vertex03.zip>

 

 

2.2 저 수준 정점 쉐이더 작성 방법

어셈블리어 형태의 저 수준 쉐이더 문법은 간단하게 구성되어 있어서 일정한 순서와 명령어들을 익히면 쉽게 쉐이더 코드를 작성할 수 있습니다. 저 수준 쉐이더 코드 작성은 다음과 같이 총 5단계로 나누어서 작성됩니다.

 

1. 정점 쉐이더 버전을 선언한다.

2. 필요하다면 상수 레지스터의 값을 지정한다.

3. 입력 레지스터를 선언하고 지정한다.

4. 정점의 위치, 법선 벡터, 색상, 안개 효과 값들에 대한 연산을 한다.

5. 연산한 결과를 출력 레지스터에 저장한다.

 

첫 번째 단계의 쉐이더 버전 선언은 "vs"로 시작을 하며 vs_1_1(또는 vs.1.1), vs_1_2(또는 vs.1.2), vs_1_3, vs_2_0, vs_3_0 등으로 작성 합니다.

2 번째 단계인 상수 레지스터에 값을 설정하는 것은 "def"로 시작합니다. 쉐이더는 float 4개를 하나의 데이터의 단위로 처리합니다. 따라서 상수 레지스터에 값을 지정할 때는 float형 값 4개를 지정해야 합니다.

 

def c0,  1.0, 3.0, 0.0, 2   ; 상수 레지스터 c0 (1, 3, 0, 2) 값 저장

def c12, 1.0, 0.0, 0.0, 1   ; 상수 레지스터 c12 (1, 0, 0, 1) 값 저장

 

상수 레지스터는 c로 시작을 하며 상수레지스터의 개수는 쉐이더 버전 마다 다르며 상수 레지스터에 저장된 값은 쉐이더 코드 처리 과정에서 수정할 수 없는 읽기 전용입니다.

 

저 수준 쉐이더 문법에서 주석은 Assembly 언어의 주석 ";" C 언어의 주석"//", "/* */" 이 있습니다.

 

3 번째 단계는 입력 레지스터를 선언하고 지정하는 것으로 "dcl_"로 시작합니다. dcl declare를 의미합니다. 가장 많이 사용되는 정점의 위치, 법선 벡터, Diffuse, 텍스처 좌표를 지정할 때는 다음과 같이 작성합니다.

 

dcl_position  v0       ; 입력 정점의 위치를 v0 레지스터로 선언(저장)

dcl_normal    v1       ; 입력 정점 법선을 v1 레지스터로 선언(저장)

dcl_color     v2       ; 정점의 diffuse 색상을 v2 레지스터로 선언(저장)

dcl_texcoord  v3       ; 정점의 텍스처 좌표를 v3 레지스터로 선언(저장)

 

이렇게 "dcl_" 로 입력 레지스터를 지정하면 정점 데이터는 위치(position), 법선(normal), Diffuse(color, color0), 텍스처 좌표(texcoord, texcoord0~7) 등으로 분리되어 "dcl_"에서 지정된 레지스터에 저장 됩니다. "dcl_…" 끝에는 숫자가 붙을 수 있습니다. 숫자 0은 숫자가 없을 때와 동등합니다. , dcl_color0 dcl_color는 같은 내용이며 Diffuse를 지정할 때 사용합니다. 정점의 스페큘러(Specular) dcl_color1로 합니다. 텍스처 좌표도 여러 개를 가지면 각각 dcl_texcoord0~7를 사용하면 됩니다. 때로는 정점을 구성하는 위치 또는 법선 벡터를 여러 개 둘 수 있습니다. 마찬가지 방법으로 dcl_position[#], dcl_normal[#]으로 숫자를 붙여서 사용합니다.

 

"dcl_" 다음에 붙는 키워드는 D3DDECLUSAGE 열거 형에서 "D3DDECLUSAGE_" 제외한 나머지를 사용하면 됩니다.

 

4 번째 단계에서는 입력 레지스터 값과 상수 레지스터 값을 이용해서 연산을 수행합니다. 연산의 문법은 Assembly와 거의 같으며 명령어는 Mnemonic 형태 입니다.

 

operator dest Register, src1 Register, src2 Register, src3 Register

 

dest는 임시(r#), 출력(o)레지스터만 가능하며 상수(c#), 입력(v#)는 읽기 전용이기 때문에 허용하지 않습니다. operator의 모든 연산은 float4 x, y, z, w 또는 r, g, b, a로 나누어서 연산이 가능합니다. 또한 xyzw 순서를 바꾸거나 yyxx와 같이 성분의 순서와 요소를 섞을 수 있으며 이것을 swizzling이라 합니다. swizzling source에서만 가능합니다.

연산을 하면서 결과를 임시로 저장할 수 있습니다. 이 경우에 임시 레지스터(Temporary Register)를 사용할 수 있습니다. 임시 레지스터는 "r"로 시작을 합니다. 다음은 swizzling으로 임시 레지스터 "r0"에 입력 레지스터 "v1"의 값을 복사하는 예입니다.

 

mov r0, v1.zy

 

이렇게 하면 r0(x, y, z, w) (v1.z, v1.y, v1.y, v1.y) 값이 저장 됩니다.

 

5 번째 단계에서는 출력 레지스터로 결과를 저장합니다. 출력 레지스터는 소문자 "o"로 시작을 하며 여러분은 출력 레지스터 oPos에 값을 반드시 저장해야 합니다.

Diffuse 값은 oD0, 스페큘러 값은 oD1에 저장을 하고, 텍스처 좌표는 oT0~ 0T7에 저장합니다. 안개 값은 oFog, Point의 크기는 oPts에 저장합니다.

 

mov oPos, r0

mov oDo , v2

mov oT0 , v3

 

이렇게 사용자가 작성한 쉐이더 코드는 다음 그림과 같이 프로그램 가능한 파이프라인에서 정점 데이터가 입력이 되면 쉐이더 코드의 명령어 순서에 따라 각각의 동작을 수행 합니다.

 

<정점 쉐이더 레이아웃>

 

정점 데이터와 쉐이더 코드의 관계는 그림처럼 입력 레지스터 선언에 의해서 데이터가 분해되어 입력 레지스터에 저장됩니다. 이 입력 레지스터에 저장된 값과 미리 설정된 상수 레지스터에 저장된 값들을 가지고 연산을 합니다. 이 연산의 결과를 임시 레지스터에 저장하거나 직접 출력 레지스터로 복사되고 출력 레지스터 내용은 Rasterizing 또는 Pixel Processing에서 처리 됩니다.

 

지금까지 간단한 정점 쉐이더와 쉐이더 작성 방법을 살펴 보았습니다. 다음으로 정점 쉐이더를 사용해서 정점의 처리 과정을 살펴봅시다.

 

 

2.3 정점 쉐이더 객체와 정점 선언자 객체

프로그램 가능한 파이프라인에 우리가 작성한 정점 쉐이더 코드를 적용하려면 정점 쉐이더(Vertex Shader) 객체와 정점 선언(Vertex Declaration) 객체 2개가 필요합니다.

정점 쉐이더 객체는 컴파일 한 쉐이더 코드를 적재한 객체이고, 정점 선언 객체는 프로그램 가능한 파이프라인에 정점 데이터의 형식을 알리는 객체입니다. 이것은 고정기능 파이프라인의 SetFVF()함수를 대신한다고 할 수 있습니다.

정점 쉐이더 객체를 생성하기 위해서 먼저 정점 쉐이더 코드를 컴파일 해야 합니다. D3DXAssembleShader() 함수는 저수준 언어로 작성된 쉐이더 코드를 컴파일 하며, 컴파일 과정에서 발생한 문법 오류 등을 검사해 줍니다.

 

DWORD dwFlags = 0;

#if defined( _DEBUG ) || defined( DEBUG )

        dwFlags |= D3DXSHADER_DEBUG;

#endif

 

LPD3DXBUFFER   pShd    = NULL;

LPD3DXBUFFER   pErr    = NULL;

INT            iLen    = strlen(sShader);

 

hr = D3DXAssembleShader(sShader, iLen, NULL, NULL, dwFlags, &pShd, &pErr);

 

만약 파일에서 작성한 저 수준 쉐이더를 컴파일 하려면 D3DXAssembleShaderFromFile() 함수를 다음과 같이 사용합니다.

 

hr = D3DXAssembleShaderFromFile("파일 이름", NULL, NULL, dwFlags, &pShd, &pErr);

 

앞의 예처럼 D3DXAssembleShader() 함수의 마지막에 오류 정보를 저장할 ID3DXBuffer 형 인수를 넣어주면 컴파일 과정에서 발생한 에러 내용과 위치()을 문자열로 저장해 줍니다. 우리는 이것을 다음의 (char*)pErr->GetBufferPointer()와 같이 문자열로 캐스팅에서 문제점을 파악할 수 있습니다.

 

if ( FAILED(hr) )

{

        if(pErr)

        {

               char* sErr = (char*)pErr->GetBufferPointer();

               MessageBox( hWnd, sErr, "Err", MB_ICONWARNING);

               pErr->Release();

        }

        return -1;

}

 

D3DXAssembleShader() 함수는 컴파일만 담당하기 때문에 컴파일 한 쉐이더 명령어를 사용하기 위해서 결과를 메모리에 적재해야 합니다. 정점 쉐이더 코드가 적재된 객체를 정점 쉐이더(IDirect3DVertexShader9) 객체라 하는데 이 객체는 다음과 같이 D3D 디바이스의 CreateVertexShader() 함수에 컴파일 한 결과를 인수로 전달하면 생성 됩니다.

 

IDirect3DVertexShader9*       m_pVs;  // 정점 쉐이더 객체

hr = m_pDev->CreateVertexShader( (DWORD*)pShd->GetBufferPointer() , &m_pVs);

if ( FAILED(hr) )

        return -1;

 

프로그램 가능한 파이프라인에서는 고정 기능 파이프라인의 FVF(Flexible Vertex Format) 상수를 사용할 수 없습니다. 그 대신 정점 선언() 객체를 사용하는데 정점 선언 객체를 생성하기 위해서 먼저 D3DVERTEXELEMENT9 구조체 변수를 MAX_FVF_DECL_SIZE 만큼 배열로 선언하고 manual로 값을 채웁니다.

 

D3DVERTEXELEMENT9 vertex_decl[MAX_FVF_DECL_SIZE] =

{

{ 0, 0,  D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_POSITION, 0 },

{ 0, 12, D3DDECLTYPE_D3DCOLOR, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_COLOR, 0 },

D3DDECL_END()

};

 

구조체를 채울 때 마지막은 "D3DDECL_END()"을 사용해서 더 이상 추가가 없음을 지시합니다. 이렇게 manualD3DVERTEXELEMENT9의 값을 설정하는 것도 좋겠지만 처음 하는 분들에게는 부담이 됩니다. DXSDKD3DXDeclaratorFromFVF() 함수는 이미 알려진 FVF 값에서 D3DVERTEXELEMENT9 구조체 변수를 채우는 함수로 이 함수를 사용하는 것이 가장 무난합니다.

manual로 작성하는 경우는 고정 기능 파이프라인에서 제공 되지 않는 FVF를 사용할 때, 예를 들어 정점의 위치가 2개 이상 이거나, 여러 개의 법선 벡터를 사용하게 되면 직접 D3DVERTEXELEMENT9 값을 작성해야 하며 쉐이더 코드 작성에서도 dcl_position 또는 dcl_normal 끝에 숫자를 붙여서 작성해야 합니다.

 

IDirect3DVertexDeclaration9*  m_pFVF;        // 정점 선언 객체

D3DVERTEXELEMENT9 vertex_decl[MAX_FVF_DECL_SIZE] ={0};

D3DXDeclaratorFromFVF(VtxD::FVF, vertex_decl);

if(FAILED(m_pDev->CreateVertexDeclaration( vertex_decl, &m_pFVF )))

        return -1;

 

이렇게 프로그램 가능한 파이프라인을 사용하기 위해서 정점 쉐이더 객체와 정점 선언 객체를 만들었습니다. 이들을 이용해서 렌더링 하는 순서는 다음과 같습니다.

 

1. 정점 쉐이더와 정점 선언 객체 사용 명시함으로써 프로그램 가능한 파이프라인 사용을 알린다.

2. 정점 선언 객체로 정점 데이터의 형식을 파이프라인에 알린다.

3. 필요하면 상수 레지스터 값을 설정한다.

4. 정점을 그린다.

5. 정점 쉐이더 객체 사용을 해제해서 고정 기능 파이프라인 사용으로 돌아 온다.

 

프로그램 가능한 파이프라인을 사용하려면 정점 쉐이더 사용을 다음과 같이 명시해야 합니다.

 

m_pDev->SetVertexShader(m_pVs);

 

이렇게 하면 GPU는 고정 기능 파이프라인 대신에 프로그램 가능한 파이프라인으로 정점 데이터를 전달합니다.

다음으로 정점 선언 객체를 지정해서 입력된 정점 데이터의 형식을 파이프라인에 알립니다.

 

m_pDev->SetVertexDeclaration( m_pFVF );

 

필요한 상수의 전달입니다. CPU에서 데이터의 처리는 int 형 이듯이 쉐이더는 float4 (float *4) 형 입니다. 파이프라인에 상수를 전달하는 함수는 디바이스의 SetVertexShader()함수입니다. 이 함수의 사용은 다음과 같습니다.

 

m_pDev->SetVertexShaderConstantF( 0, (FLOAT*)&mtVP , 4);

 

SetVertexShaderConstantF() 함수의 F FLOAT 형 데이터 전달을 의미합니다. 함수의 첫 번째 인수는 상수 레지스터 이름의 c를 제외한 나머지로 앞의 코드처럼 "0"으로 지정하면 c0 상수 레지스터에 값을 지정하는 것입니다. 함수의 두 번째 인수는 상수에 설정할 값의 주소를 지정합니다.

세 번째 인수는 float4형의 개수를 의미하며 앞의 코드는 float4 형이 4개 전달되므로 총 16개의 float 형 데이터를 상수 레지스터에 지정하는 것입니다. 앞서 쉐이더의 레지스터는 float4형이 기본이라고 했습니다. 그런데 데이터가 16개가 전달되어서 c0 float 4개가 적재되고 나머지 12개의 데이터는 c1, c2, c3 레지스터에 순차적으로 기록이 됩니다.

상수 연결에서 주의할 내용은 float 형 데이터 하나를 설정 하더라도 쉐이더는 float4가 기본이어서 데이터를 float4로 늘린 후에 값을 전달해야 합니다. 예를 들어 상수 레지스터 c10에 정점의 위치 vcP(=float2: x, y)으로 구성된 데이터를 연결한다 하더라도 다음과 같이 코딩 해야 합니다.

 

FLOAT p[4] = {vcP.x, vcP.y, 0, 0};

m_pDev->SetVertexShaderConstantF( 10, (FLOAT*)&p , 1);

 

저 수준에서 행렬 데이터를 상수 레지스터에 복사할 경우에 행렬의 값을 전치(Transpose) 시켜야 합니다. 이것은 쉐이더에서 행렬의 연산 과정이 마치 오른손 좌표계 연산과 동일해서 왼손 좌표를 사용하는 DirectX의 행렬 값을 전치해야 올바를 결과를 만들 수 있습니다.

 

D3DXMATRIX mtVP        = mtWld * mtViw * mtPrj;

D3DXMatrixTranspose( &mtVP , &mtVP );

m_pDev->SetVertexShaderConstantF( 0, (FLOAT*)&mtVP , 4);

 

고 수준 언어인 HLSL을 사용하게 되면 행렬을 전치하지 않고 그대로 사용할 수 있습니다.

상수 레지스터는 float형 이외에 정수형과 bool형을 지정할 수 있습니다. 정수형은 SetVertexShaderConstantI() 함수를 사용하고 bool 형은 SetVertexShaderConstantB() 함수를 사용합니다.

상수 레지스터 지정 후에 DrawPrimitive()함수 등을 호출해서 렌더링을 하며, 렌더링을 끝내고 고정 파이프라인으로 돌아오기 위해서 다음과 같이 SetVertexShader()함수에 NULL 을 설정합니다.

 

m_pDev->SetVertexShader( NULL);

 

s0v_02_shader_string.zip는 문자열로 작성된 쉐이더에 대한 예제이고 s0v_02_shader_file.zip 은 파일에서 작성한 쉐이더 예제 입니다. 이 둘의 CShaderEx 클래스는 프로그램 가능한 파이프라인을 이용해서 월드 공간에서 삼각형을 렌더링 합니다.

 

<프로그램 가능한 파이프라인에서 삼각형 출력:

s0v_02_shader_string.zip, s0v_02_shader_file.zip>

 

지금까지 정점 쉐이더 객체와 정점 선언 객체를 생성하고 프로그램 가능한 파이프라인을 이용하고 상수 레지스터 설정 방법을 간단히 살펴보았습니다. 정점 쉐이더를 사용과 관련된 함수들을 정리한다면 다음과 같습니다.

 

1. 파일 또는 문자열로 어셈블리어 형식의 저 수준 쉐이더 명령어들을 작성한다.

2. 저 수준 쉐이더 명령어들을 컴파일 한다. D3DXAssembleShader() 함수

3. 컴파일 된 쉐이더 명령어로 정점 쉐이더 객체를 생성한다. pDevice->CreateVertexShader()

4. 정점 형식 객체를 생성한다. pDevice-> CreateVertexDeclaration()

5. GPU에게 프로그램 가능한 파이프라인 사용을 알린다. pDevice->SetVertexShader()

5. 파이프라인에 입력된 정점 데이터 형식을 알린다. pDevice->SetVertexDeclaration()

6. 파이프라인의 상수 레지스터를 설정한다. pDevice->SetVertexShaderConstant{F|B|I|}()

7. 장면을 그린다. pDevice->DrawPrimitive ()

8. 정점 쉐이더 사용을 해제한다. pDevice->SetVertexShader(NULL)

 

 

2.4 변환(Transform)

3D 기초 과정으로 돌아서 고정 기능 파이프라인의 정점 처리 과정을 보면 크게 정점의 변환, 조명 효과 적용, 안개 효과 적용 입니다.

 

<쉐이더 프로그램 대상인 고정 기능 파이프라인의 정점 처리 과정>

 

정점의 변환은 다시 월드 변환, 뷰 변환, 정규 변환으로 나눌 수 있으며 수식으로 다음과 같이 정리할 수 있습니다.

 

정점의 정규 변환 위치' = 정점의 위치 * (월드 행렬 * 뷰 행렬 * 투영 행렬)

 

이 수식을 다음과 같이 s0v_02_shader_string.zip, s0v_02_shader_file.zip 에 구현하고 있습니다.

 

vs_1_1

dcl_position   v0

dcl_color      v1

m4x4 oPos, v0, c0

mov  oD0, v1

 

4 번째 줄의 m4x4는 벡터와 행렬의 곱셈 연산을 지시하는 명령어로 "m4x4 oPos, v0, c0" 상수 레지스터 c0, c1, c2, c3를 행으로 구성하는 행렬과 입력 레지스터 v0를 곱해서 oPos에 저장하는 것이며 수식으로 표현하면 다음과 같습니다.

 

 

m4x4는 또한 다음과 같이 4번의 내적(dp4: 4차원 벡터의 내적)를 수행하는 것과 같습니다.

 

m4x4 r0, r1, c0  ó

dp4   r0.x, r1, c0

dp4   r0.y, r1, c1

dp4   r0.z, r1, c2

dp4   r0.w, r1, c3

 

m4x4이외에 m4x3, m3x3도 있는데 m3x3은 주어진 행렬의 3 3열만 곱셈 연산에 적용이 됩니다.

 

c0 "월드 행렬 * 뷰 행렬 * 투영 행렬" 값을 전달하기 위해서 s0v_02_shader_file.zipCShaderEx::Render() 함수를 보면 다음과 같이 행렬 값들을 설정하고 SetVertexShaderConstantF() 함수로 상수 레지스터 c0에 설정하고 있음을 볼 수 있습니다.

 

D3DXMATRIX     mtWld;         // 월드 행렬

D3DXMATRIX     mtViw;         // 뷰 행렬

D3DXMATRIX     mtPrj;         // 투영 행렬

D3DXMATRIX     mtVP    = mtWld * mtViw * mtPrj;

D3DXMatrixTranspose( &mtVP , &mtVP );

m_pDev->SetVertexShaderConstantF( 0, (FLOAT*)&mtVP , 4);

 

지금은 정점 변환에 관한 행렬을 전부 곱해서 전달하고 있고 각각 월드 변환, 뷰 변환, 정규 변환을 쉐이더에서 단계적으로 구현하기 위해서 먼저 다음과 같이 순서와 수식을 정리합니다.

 

정점의 위치' = 입력 정점의 위치

정점의 월드 변환 위치' = 정점의 위치' * 월드 행렬

정점의 뷰 변환 위치' = 정점의 월드 변환 위치' * 뷰 행렬

정점의 정규 변환 위치' = 정점의 월드 변환 위치' * 투영 행렬

 

이것을 쉐이더 코드로 전환해야 하는데 월드 행렬, 뷰 행렬, 투영 행렬은 외부에서 설정되는 값이므로 상수 레지스터로 설정합니다. 그런데 행렬은 float 16개가 필요하기 때문에 만약 월드 행렬을 c0에 설정하게 되면 c0~c3까지 상수 레지스터가 월드 행렬 값으로 설정이 되고, 뷰 행렬이 c4에 설정되면 c4~c7 레지스터가 뷰 행렬 값으로 설정 됩니다. 투영 행렬이 c8에 설정되면 c8~c11까지 투영 행렬 값이 설정 됩니다.

수식의 왼쪽에 있는 정점의 위치', 정점의 월드 변환 위치, 뷰 변환 위치, 정규 변환 위치는 쉐이더 내부에서 임시(Temporary)로 사용되는 값 입니다. 따라서 이들은 임시 레지스터 r0, r1, r2, r3에 저장 시킬 수 있습니다.

이 내용을 가지고 저 수준 쉐이더 코드로 전환하면 다음과 같습니다.

 

vs_1_1                  // 쉐이더 버전

dcl_position    v0      // 위치 설정

mov r0, v0              // 정점의 위치를 임시 레지스터에 복사

m4x4 r1, r0, c0         // 월드 행렬 변환 후 r1에 저장

m4x4 r2, r1, c4         // 뷰 행렬 변환 후 r2에 저장

m4x4 r3, r2, c8         // 투영 행렬 변환 후 r3에 저장

mov oPos, r3            // r3 값을 출력 레지스터 oPos에 복사

 

만약 정점의 색상을 출력하려면 "dcl_color" 또는 "dcl_color0"를 이용해서 정점의 Diffuse 값에 대해서 레지스터를 선언합니다. 만약 정점의 스페큘러(Specular) 값을 얻으려면 "dcl_color1"을 이용해야 합니다.

 

dcl_color       v1      // 색상 레지스터

mov  oD0, v1            // 정점의 색상을 직접 복사

 

색상을 출력하기 위해서는 oD0 oD1을 사용합니다. oD0 Diffuse , oD1은 스페큘러 값에 대한 출력 레지스터입니다.

상수 레지스터 c0, c4, c8에 값을 설정하기 위해서 우리는 다음과 같이 먼저 행렬을 전치(Transpose) 시키고, 디바이스의 SetVertexShaderConstantF() 함수에 "상수 레지스터 인덱스", 행렬 주소, 4 등을 인수로 전달하면 됩니다.

 

D3DXMATRIX     mtT;

D3DXMatrixTranspose( &mtT , &mtWld );

m_pDev->SetVertexShaderConstantF( 0, (FLOAT*)&mtT , 4);

D3DXMatrixTranspose( &mtT , &mtViw );

m_pDev->SetVertexShaderConstantF( 4, (FLOAT*)&mtT , 4);

D3DXMatrixTranspose( &mtT , &mtPrj );

m_pDev->SetVertexShaderConstantF( 8, (FLOAT*)&mtT , 4);

 

<SetVertexShaderConstantF()함수의 인덱스와 상수 레지스터 관계>

 

s0v_04_transform.zip은 정점의 월드 변환, 뷰 변환, 정규 변환을 저 수준으로 구현한 예제 입니다.

 

<정점의 변환: s0v_04_transform.zip>

 

 

2.5 정점 쉐이더 가상 머신(Vertex Shader Virtual Machine)

쉐이더가 익숙해지기 위해서 정점 쉐이더의 레이아웃을 알아보았고 정점의 변환을 연습해보았습니다. 정신 없이 쉐이더 강의를 진행 했는데 다음 단계를 위해서 차분하게 정점 쉐이더의 구조를 다시 살펴 보도록 합시다.

 

<정점 쉐이더 가상 머신>

 

그림은 정점 쉐이더의 가상 머신을 표현한 것입니다. 이 가상 머신은 크게 입력 레지스터, 출력 레지스터, 상수 레지스터, 임시 레지스터, 그리고 산술과 논리 연산을 담당하는 ALU로 구성되어 있습니다.

보통 마이크로 프로세서 내부에서 처리를 돕기 위해 만들어진 작은 기억 공간을 레지스터라 합니다. GPU도 하나의 "처리기(Processor)" 이므로 GPU 내부에서 처리를 보조하는 기억 공간도 레지스터라 부르는 것입니다.

ALU Arithmetic-Logic Unit로 정점 ALU는 정점 데이터에 대한 산술 연산과 논리 동작을 담당합니다. ALU는 기본적인 사칙 연산부터 벡터 * 행렬의 연산, 내적, 제곱근, 승수, exp, log 등의 수학 함수들과 조건 문 등을 처리할 수 있습니다. 정점 쉐이더 코드를 작성한다는 것을 단순하게 바라본다면 정점 ALU가 처리하는 과정을 순서대로 나열한 것이라 할 수 있습니다.

그림을 보면 정점 쉐이더 가상머신의 흐름이 노란색 화살표로 표시되어 있고 입력된 정점 버퍼의 데이터는 입력 레지스터에 "dcl_" 문장에 의해서 정점의 위치, 법선, 색상, 텍스처 좌표 등등이 각각 분리되어 입력 레지스터 v0~v15에 적재 됩니다.

정점 ALU는 입력 레지스터의 값을 받아서 연산을 하며 필요하다면 상수 레지스터와 임시 레지스터를 사용하기도 합니다.

우리가 정점 쉐이더를 컴파일하고 파이프라인에 적용하게 되면 동작을 바꾸어 볼 수 있는 것은 정점 버퍼와 상수 레지스터뿐이라는 것을 기억해야 합니다. 따라서 다양한 기능을 정점 쉐이더에 적용하려면 많은 변수들을 상수 레지스터로 구성해야 합니다. 이로 인해 GPU 내부에서 가장 큰 레지스터를 구성하는 것이 상수 레지스터입니다.

상수 레지스터의 값은 "def" 문장으로 쉐이더 코드 내부에서 지정하거나 아니면 외부에서 SetVertexShaderConstant{F|B|I}() 함수로 설정합니다. 같은 레지스터의 값을 지정할 때는 "def"로 명명한 우선 순위가 쉐이더 내부에서 정한 값이 먼저입니다.

 

우리는 연산의 과정에서 일시적으로 값을 저장할 필요가 있습니다. 이 때 임시 레지스터를 이용합니다. 입력 레지스터와 상수 레지스터는 읽기 전용 이지만 임시 레지스터는 읽기/쓰기가 가능한 레지스터 이며 임시 레지스터에 값을 설정할 때는 "mov" 명령어를 이용해야 합니다.

정점에 관한 모든 처리를 끝냈으면 출력 레지스터에 결과를 저장해야 합니다. 출력 레지스터는 소문자 "o"(Out)로 시작을 하는 레지스터로 위치, 색상, 텍스처 좌표, 안개, 점 크기 5종류가 있습니다. 출력 레지스터의 위치는 oPos에 저장하고, 색상에 대한 Diffuse Specular 값은 oD0, oD1에 저장합니다. 텍스처 좌표는 oT0~0T7까지 사용할 수 있습니다. 안개는 oFog, Point의 크기는 oPts에 저장합니다. 때로는 픽셀 쉐이더에서 정점 쉐이더의 입력 값 또는 결과 값을 사용하고자 할 때가 있습니다. 프로그래머들은 이 경우에 출력 레지스터 중에서 텍스처 좌표를 저장하는 oT0~0T7을 사용하며 끝 번호(oT7)부터 주로 이용합니다.

 

dcl_position   v0      // 정점 위치

dcl_normal     v1      // 정점 법선

mov  oT7, v0           // 정점 위치를 0T7에 저장

mov  oT6, v1           // 정점 법선 벡터를 0T6에 저장

 

<상수 레지스터, 임시 레지스터, 출력 레지스터 사용 예. s0v_05_const.zip>

 

GPU 레지스터의 크기는 float * 4로 구성되어 있습니다. 또한 하나의 레지스터는 x, y, z, w r, g, b, a를 구분하지 않고 x, y, z, w 순서 또는 r, g, b, a 순으로 저장 됩니다. 이것은 색상도 벡터에서 적용되는 연산이 가능하며 또한 벡터도 색상으로 출력할 수 있다는 의미도 됩니다.

 

쉐이더는 각 버전 마다 전체 명령에 대한 제한이 있습니다. 따라서 필요한 명령어 이외의 쉐이더 코드는 GPU의 불필요한 동작을 지시하는 것이기 때문에 쉐이더 작성에서 한 줄이라도 명령문을 줄이는 것이 좋습니다. s0v_05_const.zip "data/ shader.vsh" 은 강의를 목적으로 작성된 것이므로 이렇게 작성하는 것은 좋지 않으며 가급적이면 필요한 부분만 남겨 놓거나 줄여서 사용하는 것이 바람직합니다.

 

지금까지 정점의 변환과 색상을 가지고 프로그램을 작성해 보았는데 텍스처, 조명등을 살펴보겠습니다.

 

 

2.6 텍스처 적용

텍스처에 관련된 중심 내용은 픽셀 쉐이더에 있습니다. 보통 정점 쉐이더에서 텍스처 적용은 단순히 좌표 전달 정도의 용도로 활용 되는 것이 대부분입니다. 이 경우에 쉐이더 코드는 다음과 같이 작성합니다.

 

1. 정점의 텍스처 좌표에 대해서 "dcl_texcoord"로 입력 레지스터의 텍스처 좌표를 설정한다.

2. 필요에 따라 텍스처 좌표를 변환한다.

3. 출력 레지스터 oT0~0T7에 복사한다.

 

입력 레지스터에 대한 정점의 텍스처 좌표 설정은 다음 예제와 같이 dcl_texcoord 명령어로 설정합니다.

 

dcl_texcoord   v3

 

입력 텍스처 좌표는 총 8개까지 설정이 가능합니다. 여러 개의 텍스처 좌표를 입력 레지스터에 설정할 때는 dcl_texcoord0~ dcltexcoord7을 사용합니다. dcl_texcoord dcl_texcoord0과 같습니다.

 

텍스처 좌표도 위치이므로 정점의 위치와 같이 변환(Transform)을 할 수 있습니다. 여기서는 텍스처 좌표의 변환이 없다고 가정하고 최종 단계인 출력 레지스터에 직접 복사하도록 합시다.

 

mov oT0, v3

 

oT0 Output Texture 0-address coordinate를 의미하며 DirectX는 총 8장의 멀티 텍스처를 하나의 그래픽 파이프라인에서 사용이 가능하므로 사용자는 oT0 ~oT7까지 출력 레지스터에 복사를 할 수 있습니다.

s0v_06_tex.zips0v_06_tex_earth.zip"data/shader.vsh"는 은 단순히 텍스처 좌표를 출력 레지스터로 복사하는 예제입니다.

 

 

<텍스처 출력. s0v_06_tex.zip, s0v_06_tex_earth.zip>

 

반사 또는 굴절에 대한 환경 매핑(Environment Mapping)은 정점의 좌표와 법선 벡터를 텍스처 좌표로 전환하는 것입니다. 이 내용은 3D 기초 시간에 약간의 복잡한 과정을 거쳐 살펴보았는데 환경 매핑은 쉐이더를 사용하면 쉽게 구현할 수 있습니다.

 

반사라는 것은 다음과 같이 주어진 그림에서 붉은 색 화살표의 텍스처 좌표를 설정하는 것이고, 이 붉은 색 화살표는 정점의 법선 벡터를 이용하면 됩니다.

또한 반사는 카메라의 위치에 의해 결정되기 때문에 정점의 법선 벡터를 카메라 공간으로 변환하면 카메라의 움직임에 대해서도 반사효과를 만들어 낼 수 있습니다. 이것을 수식으로 정리하면 다음과 같습니다.

 

텍스처 좌표' = 정점의 법선 벡터 * 뷰 행렬

텍스처 좌표.x =  텍스처 좌표'.x * 0.5 + 0.5

텍스처 좌표.y = -텍스처 좌표'.y * 0.5 + 0.5

 

텍스처 좌표에 0.5를 곱하고 더한 것은 법선 벡터의 범위가 [-1, 1]이기 때문에 텍스처 중심으로 좌표를 [0, 1] 범위로 만들기 위해서 입니다.

이 수식을 쉐이더로 구현하면 다음과 같습니다.

 

#define Nor     r0

#define Tex     r1

def     c32, 0.4, 0.5, 0.0, 1.0

dcl_normal     v1                     // 정점의 법선 벡터 레지스터 선언

m3x3 Nor, v1c4                     // 법선 벡터의 카메라 공간 변환

mov Tex.xyz, Nor.xyz                  // 변환된 법선 벡터를 텍스처 좌표에 복사

mov Tex.w, c32.w                      // 텍스처 좌표의 w 값 설정

mov Tex.y, -Tex.y                     // y 좌표를 반전시킴

 

// Tex = Tex * 0.4F + 0.5F

mad Tex, Tex, c32.x, c32.y            // 텍스처의 중심으로 이동과 범위[0,1]로 설정

mov oT0, Tex

 

저 수준 쉐이더에서는 전처리 문이 지원이 되며 "#define" 키워드로 매크로를 정의할 수 있습니다.

카메라 움직임에 대해서 반사 효과가 적용되어야 하기 때문에 정점의 법선 벡터를 뷰 행렬에 곱합니다. 이 때 법선 벡터의 크기는 변하지 않아야 하므로 회전 변환만 적용시키기 위해서 m3x3을 사용합니다.

법선 벡터가 mad를 이용해서 텍스처 중심 좌표 (0.5, 0.5)에서 [0, 1] 범위로 이동하는 계산을 한 번에 처리하기 위해서 y 값을 반전시켜 놓습니다. mad 명령어는 다음과 같이 두 변수의 곱에 세 번째 변수를 합한 값입니다.

 

mad r3, r0, r1, r2

r3.x  = r0.x * r1.x  + r2.x;

r3.y  = r0.y * r1.y  + r2.y;

r3.z  = r0.z * r1.z  + r2.z;

r3.w = r0.w * r1.w + r2.w;

 

따라서 "mad Tex, Tex, c32.x, c32.y"는 다음과 같은 의미이며 여기서 0.5F 대신 0.4F를 사용한 것은 환경 매핑의 경계에서 해상도가 낮기 때문입니다.

 

Tex.x = Tex.x * 0.4F + 0.5F, Tex.y = Tex.y * 0.4F + 0.5F, …

 

이 내용은 s0v_06_tex_env.zip "data/shader.vsh" 파일에 구현되어 있습니다. 실행하면 다음과 같은 화면을 볼 수 있습니다.

 

 

<환경 매핑: s0v_06_tex_env.zip, 정점 위치로 구현된 텍스처 좌표: s0v_06_tex_vtx.zip>

 

3D 기초에서 정점 좌표를 텍스처 좌표로 사용한 예도 있었는데 구현이 간단하므로 s0v_06_tex_vtx.zip 예제를 참고 하기 바랍니다.

 

 

2.7 Lighting

지금까지 정점 쉐이더에서 변환, 색상, 텍스처 좌표 설정 등을 살펴 보았습니다. 정점 쉐이더에서 가장 비중이 있는 영역은 조명(Lighting) 입니다. 특히, 조명 원리를 잘 알고 있으면 NPR(Non Photo-realistic Rendering)의 대표인 Toon(Cartoon) Shading을 쉽게 구현할 수 있습니다.

조명 효과를 구현하기 위한 모델은 여러 가지가 있습니다. 이 중에서 우리는 3D 기초 시간에 다루었던 Lambert 확산과 Phone 반사를 각각 쉐이더로 구현해 보겠습니다. 다음으로 Phong 반사를 개선한 Blinn-Phong 반사를 알아보고 Lambert 확산과 Blinn-Phong 반사를 동시에 렌더링에 적용해 보도록 하겠습니다.

 

 

2.7.1 Lambert 확산

Lambert 확산은 그림과 같이 정점의 법선 벡터와 빛의 방향 벡터의 내적을 반사의 세기(Intensity)로 사용하는 것입니다.

 

<Lambert 확산(반사)의 세기>

 

렌더링 오브젝트는 월드 변환할 수 있습니다. 월드 변환에서 법선 벡터는 회전만 적용해야 합니다. 회전을 법선벡터에 적용하려면 월드 행렬이 크기 변환이 없는 경우에 대해서 "m3x3"을 이용합니다 m3x3은 주어진 행렬의 3 3열만 연산에 적용됩니다.

월드 변환을 하는 오브젝트에 대한 반사의 세기를 쉐이더 코드로 작성하면 다음과 같습니다.

 

#define Nor     r0

#define Lgt    -c8

 

vs_1_1

dcl_normal     v1      // 정점 법선 벡터를 입력 레지스터 v1에 선언

m3x3 Nor, v1c4      // 법선 벡터는 회전만 적용

dp3  r1.w, Nor, Lgt    // Light 방향과 내적으로 정점의 밝기를 설정

 

dp3 Dot Product(내적)으로 "dp" 다음의 숫자는 차원을 나타냅니다. 3이면 xyz만 수행하고 4 xyzw 성분에 대해서 내적을 구합니다. 만약 "dp3 r2, r0, r1"와 같이 결과를 저장하는 장소를 명시하지 않으면 dp3 r2 xyz 성분에 같은 내적 값을 기록합니다. 앞의 코드는 내적의 결과를 r1.w에 저장하고 있습니다.

 

반사의 세기 I는 내적에 의해 범위가 [-1, 1] 가 됩니다. 그런데 밝기는 (-)가 없으므로 0보다 작으면 0으로 만드는 Saturation을 사용하거나 아니면 전체 밝기에 1을 더한 다음 다시 0.5를 곱해서 [0, 1] 범위로 조정할 수 있습니다. 여기서 전체 밝기에 1을 더하고 0.5를 곱하는 것을 적용해 봅시다.

"I = (I + 1)/2 "에 대해서 쉐이더를 사용하면 add mul연산이 필요합니다. 그런데 mad를 사용하면 한 번에 처리가 됩니다. "I = (I + 1)/2" "I * 0.5 + 0.5"와 같으므로 다음과 같이 mad를 이용해서 쉐이더 코드를 작성합니다.

 

def c24, 1, 0.5, 0.1, 1. 0

mad     r1, r1.w, c24.y, c24.y

 

반사의 세기를 계산했습니다. 만약 광원의 색상이 있으면 이 값을 반사의 세기에 곱한 후에 출력 레지스터 0D0에 복사합니다.

 

mov     r1.w, c24.w

mul     oD0, r1, c10   // 최종 색상 = Lambert 반사 * 빛의 색상

 

<Lambert 확산. s0v_07_1lambert.zip>

 

 

2.7.2 Phong 반사

(Phong) 반사는 렌더링 오브젝트의 정 반사(Specular) 효과를 표현한 조명 모델입니다. 퐁 반사의 세기는 정점에서 카메라의 위치에 대한 시선 벡터와 반사 벡터의 내적에 멱승 (Power)을 적용해서 구합니다.

렌더링 오브젝트는 크기, 회전, 이동에 대한 월드 변환이 적용되므로 시선 벡터를 구하기 전에 먼저 정점의 위치에 대해서 월드 변환을 적용하며, 법선 벡터는 Lambert 확산에서처럼 회전만 적용합니다.

 

<퐁 반사 모델>

 

반사 벡터는 빛의 방향 벡터와 정점의 법선 벡터를 이용해서 구합니다. 반사 벡터를 빠르게 구하는 방법은 먼저 법선 벡터와 빛의 방향 벡터를 이용해서 법선 벡터 방향으로 를 만듭니다.

반사 벡터 을 더하면 이 되므로 을 쉽게 구할 수 있습니다.

 

<반사 벡터 계산>

 

 

퐁 반사를 저 수준 쉐이더 언어로 직접 작성하는 것은 쉬운 일이 아닙니다. 좀 더 편리한 방법은 다음과 같은 의사(Pseudo) 코드를 먼저 작성하고 이것을 쉐이더 언어로 변경하는 것입니다.

 

변환 위치 = 정점 위치 * 월드 행렬

변환 법선 = 정점 법선 * 월드 행렬의 회전 행렬

시선 벡터 = normalize(카메라 위치 - 변환 위치)

반사 벡터 = 2 * dot(빛의 방향, 변환 법선) * 변환 법선 - 빛의 방향

퐁 반사의 세기 = dot(시선 벡터, 반사 벡터)^Power

 

쉐이더 코드 작성을 편리하게 하기 위해서 다음과 같이 전 처리문을 사용해서 레지스터 이름을 정의합니다.

 

#define Pos     r0

#define Nor     r1

#define Eye     r2

#define Rfc     r3

#define Phn     r4

#define Lgt    -c8

#define Cam    c16

#define Pow    c16.w

 

'변환 위치' 월드 변환 행렬을 그대로 적용하면 되지만 '변환 법선'은 회전만 적용해야 합니다. 만약 크기 변환 행렬이 적용 안된 월드 변환 행렬이라면 '변환 법선'은 회전만 적용하면 되므로 m3x3으로 '변환 법선'을 구할 수 있습니다. 다음은 '변환 위치', '변환 법선'을 구하는 쉐이더 코드입니다.

 

dcl_position   v0

dcl_normal     v1

m4x4 Pos, v0c4             // 월드 변환

m3x3 Nor, v1c4             // 법선 벡터는 회전만 적용

 

시선 벡터는 normalize(카메라의 위치 - 변환 위치) 입니다. 먼저 카메라의 위치에서 변환 위치를 뺍니다.

 

sub Eye, Cam, Pos

 

시선 벡터를 단위 벡터로 만들어야 하는데 일반 벡터를 단위 벡터로 만드는 방법은 자신의 크기로 나누는 것입니다.

 

 

저 수준 쉐이더에서 rsq는 입력된 t의 크기의 역수  를 반환하는 연산자 입니다.

와 동등 하므로 먼저 시선 벡터를 dp3로 내적을 구하고 rsq를 사용하면 1/(벡터 크기)와 동등해집니다. 이 값을 다시 시선 벡터에 곱하면 시선 벡터가 단위 벡터가 됩니다.

 

dp3 Eye.w, Eye, Eye

rsq Eye.w, Eye.w

mul Eye.xyz, Eye.xyz, Eye.www

 

"반사 벡터 = 2 * dot(빛의 방향, 변환 법선) * 변환 법선 - 빛의 방향" 를 구하기 위해서 변환 법선과 빛의 방향 벡터의 내적을 먼저 구합니다. 내적 값에 2를 곱해야 하는데 같은 내적 값을 add를 하면 2를 곱한 결과와 동일합니다. 다시 변환 법선 벡터를 곱한 다음 빛의 방향 벡터를 빼주면 반사 벡터를 구하게 됩니다.

 

dp3 Rfc.w, Nor, Lgt           // dot(N, L)

add Rfc.w, Rfc.w, Rfc.w               // * 2

mul Rfc.xyz, Nor, Rfc.www     // * N

sub Rfc, Rfc, Lgt             // - L

 

시선 벡터, 반사 벡터를 구했으니 퐁 반사의 세기를 구할 수 있습니다.

 

dp3     Phn.w, Eye, Rfc               // dot(E, R)

 

퐁 반사의 세기도 내적이므로 Lambert 때와 마찬가지로 [0, 1] 범위로 조정합니다.

 

def     c24, 1, .5, 0.1, 1.

add     Phn.w, Phn.w, c24.x    // limit [0, 1]

mul     Phn.w, Phn.w, c24.y

 

마지막으로 멱승(Power)을 적용합니다. 쉐이더 2.0 이후에는 pow 연산자가 있지만 1.1은 없기 때문에 다음과 같은 수식을 exp log 함수로 만듭니다.

 

 

B 대신 외부에서 Specular Power에 대한 Pow 변수로 바꾸고 A 대신 내적을 적용하면

log ( dot(E, R) ) * Pow 가 되어 다음과 같은 쉐이더 코드를 만들 수 있습니다.

 

log     Phn.w, Phn.w

mul     Phn.w, Phn.w, Pow

exp     Phn.w, Phn.w

 

마지막으로 빛의 색상을 곱하고 퐁 반사의 w 값을 1로 설정한 다음 oD0 레지스터에 복사합니다.

 

mul     Phn, c10, Phn.w               // Color = 퐁 반사 * 빛의 색상

mov     Phn.w, c24.w           // Color.w = 1.f;

mov     oD0, Phn               // Output Diffuse Color

 

<퐁 반사: s0v_07_2phong.zip>

 

 

2.7.3 Blinn-Phong 반사

<거의 수평으로 입사한 빛의 퐁 반사>

퐁 반사는 정면으로 반사하는 빛에 대해서 현실 세계를 잘 표현하지만 그림과 같이 거의 수평면으로 입사되는 빛에 대해서 더 넓은 영역의 하이라이트(Highlight)를 만들고 반사의 경계를 만들어 냅니다.

또한 실 세계에서 거의 수평면으로 입사한 빛은 오히려 더 강한 Specular를 만들고, 이를 표현하려면 시선 벡터 방향으로 반사 벡터를 좀 더 움직여야 합니다. 이것을 "off-specular peak" 이라 합니다.

Blinn-Phong 반사는 퐁 반사 모델을 수정해서 좀 더 현실 세계의 정 반사 효과를 표현한 조명 모델이라 할 수 있습니다. Blinn-Phong 반사는 다음 그림과 같이 퐁 반사의 반사 벡터 대신 Half 벡터를 사용합니다.

 

<Blinn-Phong 반사 모델>

 

Half 벡터는 시선 벡터와 빛의 방향 벡터의 합으로 다음과 같이 간단하게 계산합니다.

Blinn-Phong 반사 세기를 구하는 과정은 퐁 반사에서 반사 벡터 대신 Half 벡터를 구하고, Half와 변환된 법선 벡터의 내적을 반사의 세기로 설정합니다.

 

변환 위치 = 정점 위치 * 월드 행렬

변환 법선 = 정점 법선 * 월드 행렬의 회전 행렬

시선 벡터 = normalize(카메라 위치 - 변환 위치)

Half 벡터 = normalize(시선 벡터 + 빛의 방향 벡터)

Blinn-Phong 반사 세기 = dot(법선 벡터, Half 벡터)^Power

 

다음은 시선 벡터와 빛의 방향 벡터를 이용해서 Half를 구하는 쉐이더 코드입니다.

 

add Hlf, Eye, Lgt                            // H = E + L

dp3 Hlf.w, Hlf, Hlf                          // Normalize H

rsq Hlf.w, Hlf.w

mul Hlf.xyz, Hlf.xyz, Hlf.www

 

이후 Blinn-Phong 반사 세기를 구하는 내용은 퐁 쉐이딩에서 Half 벡터와 변환된 법선 벡터의 내적만 다르고 나머지는 같습니다.

 

dp3     Bln.w, Hlf, Nor               // dot(H, N)

log     Bln.w, Bln.w           // pow(Blinn, Power)

mul     Bln.w, Bln.w, Pow

exp     Bln.w, Bln.w

 

mul     Bln, c10, Bln.w               // Blinn-Phong 반사 * 빛의 색상

mov     Bln.w, c24.w           // Color.w = 1.f;

mov     oD0, Bln               // Output Diffuse Color

 

Blinn-Phong 반사는 퐁 반사보다 반사의 영역이 넓지만 그림과 같이 거의 수평으로 반사되는 빛에 대해서도 잘 표현 됩니다. 반사 영역을 좁히는 것은 Power 값을 증가시키면 됩니다.

 

 

<Blinn-Phong 반사: s0v_07_3blinn.zip>

 

 

2.7.4 Lighting

지금까지 퐁 반사와 퐁 반사를 개선한 Blinn-Phong 반사를 살펴보았습니다. 게임에서는 Lambert 확산과 함께 이 들을 결합해서 조명 효과를 만듭니다.

 

 

<Lambert+Phong: s0v_07_4lambert+phong.zip,

Lambert+Blinn-Phong: s0v_07_5lambert+blinn.zip>

 

이들 조명 효과는 다음과 같은 공식으로 출력 레지스터 oD0의 값을 설정했습니다.

 

출력 색상(oD0) = Lambert * 정점 색상 + Blinn 또는

출력 색상(oD0) = Lambert * 정점 색상 * Blinn

 

그런데 이 방법 대로 조명 효과를 만들고 텍스처를 적용하기 위해서 쉐이더 코드를 추가하고,

 

dcl_color      v2

dcl_texcoord   v3

mad oD0, Lmb, v2, Bln

mov  oT0, v3

 

화면에 출력하면 조명 효과의 Specular 적용이 우리가 원하는 형태로 되고 있지 않음을 볼 수 있습니다.

 

<Specular 효과 적용이 미미한 예. s0v_07_6texture1.zip>

 

이것은 출력 레지스터 oD0 Diffuse 값을 저장하는 용도로 사용되고, 고정 기능 파이프라인에서 픽셀 처리를 하게 되면 이 oD0의 색상 범위를 [0, 1]로 정규화 해서 사용하기 문입니다.

아직까지 우리는 픽셀 쉐이더를 사용하지 않기 때문에 고정 기능 파이프라인에서 이것을 개선하도록 합시다. 먼저 쉐이더 코드를 oD0에는 Lambert * 정점 색상 결과를 출력하고 oD1 Blinn 또는 Phong 반사의 Specular 값을 출력합니다.

 

mul oD0, Lmb, v2              // Output Diffuse: Lambert * Vertex Diffuse

mov oD1, Bln                  // Output Specular: Blinn-Phong Reflectance

 

다중 텍스처 처리(Multi-Texturing)의 단계에서 색상 혼합 방법을 D3DTOP_MULTIPLYADD로 정합니다. MULTIPLYADD Arg0 + Arg1 * Arg2 연산을 수행하기 때문에 색상 인수 0번은 Specular(oD1)으로 설정하고 Arg1은 텍스처를 Arg2 Diffuse(oD0)로 설정합니다.

 

m_pDev->SetTextureStageState(0, D3DTSS_COLORARG0, D3DTA_SPECULAR);

m_pDev->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);

m_pDev->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_DIFFUSE);

m_pDev->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MULTIPLYADD);

 

<Specular 효과. s0v_07_6texture2.zip>

 

 

2.8 Vertex Blending

정점 블렌딩(Vertex Blending)은 정점의 위치에 비중을 추가해서 최종 위치를 구하는 방법으로 대표적인 예가 스키닝(Skinning)입니다. 정점 블렌딩은 간단하게 다음과 같이 간단한 공식으로 표현합니다.

 

 

대부분 정점에 행렬이 적용되어 이것을 일반적으로 표현하면  이 되며 모든 가 같고 이면 스키닝이 됩니다.

 

 è

 

정점 쉐이더를 이용해서 정점 블렌딩을 구현하기 위해서 3D 기초 과정에서 구현했던 태극기 예제의 폴리곤을 이용해 보겠습니다.

 

먼저 다음과 같이 2개의 비중(Weight)이 있는 정점 구조체를 선언합니다.

 

struct VtxBlend

{

        D3DXVECTOR3    p;      // 위치

        FLOAT          g;      // 비중(Weight)

        DWORD          d;      // Diffuse

        FLOAT          u, v;   // 텍스처 좌표

enumFVF = (D3DFVF_XYZB1 | D3DFVF_DIFFUSE | D3DFVF_TEX1),};

};

 

이 정점 구조체로 만들어진 정점 데이터의 비중을 정점 쉐이더에서 사용하기 위해서 입력 레지스터를 선언해야 합니다.

 

dcl_blendweight                v1      // 행렬 비중

 

하나의 정점에 두 개의 행렬이 적용된 변환에서 블렌딩 정점의 위치는 다음과 같이 계산 됩니다.

 

블렌딩 정점 위치 = 정점 위치 * 행렬0 * 비중 + 정점 위치 * 행렬1 * (1 - 비중)

 

이것을 쉐이더로 표현하면 외부에서 두 개의 월드 행렬을 적용해서 정점의 위치를 각각 저장한 후에 행렬0으로 변환된 위치에는 "비중(Weight)"을 곱하고 "1-비중"을 계산해서 행렬1으로 변환된 정점에 곱한 다음 둘을 더해서 정점 블렌딩을 구현합니다.

 

m4x4 r0, v0, c12              // 월드 행렬 0에 의한 정점 변환

m4x4 r1, v0, c16              // 월드 행렬 1에 의한 정점 변환

mul r0, r0, v1.x              // r0 = r0 * weight

 

add r2, c0.x, -v1.x            // r2.xyzw = 1 - weight

mad r2, r1, r2, r0            // pos = (1-weight)*v1 + v0*weight

 

계산을 빠르게 적용하기 위해 mad를 이용했습니다. 마지막으로 뷰 변환, 투영 변환을 적용하고 출력 레지스터에 복사합니다.

 

m4x4 r0, r2, c4                       // 뷰 변환

m4x4 oPos, r0, c8             // 투영 변환

 

<정점 블렌딩: s0v_08_vertex_blending.zip>

 

대부분 스키닝은 4개의 행렬 인덱스를 포함한 정점으로 구현됩니다.

 

struct VtxBlend

{

        D3DXVECTOR3    p;

        FLOAT          g[3];      // 비중(Weight)

        BYTE           m[4];      // 행렬의 인덱스

        enumFVF = (D3DFVF_XYZB4 | D3DFVF_LASTBETA_UBYTE4 |…),   };

};

 

정점 쉐이더는 비중과 인덱스를 처리하기 위한 입력 레지스터 선언이 필요합니다. 정점 데이터에서 인덱스를 가져와야 하는데 인덱스의 데이터 타입은 정수형입니다. 정수형 데이터를 저장하려면 "mova"와 같은 명령어를 이용해야 하고 이를 위해서 정점 쉐이더 버전은 2.0이상이 필요합니다. 또한 행렬은 4x4 float형입니다. 따라서 인덱스에 4를 곱해야만 외부에서 스키닝에 적용할 행렬 배열을 상수 레지스터에 복사했을 때 이 상수 레지스터에 저장된 행렬 값을 제대로 가져올 수 있게 됩니다.

 

vs_2_0

 

def     c0, 4.0, 0.0, 0.0, 1.0

 

dcl_position           v0      // 위치

dcl_blendweight               v1      // 행렬 비중

dcl_blendindices       v2      // 행렬 인덱스

mul r0, v2, c0.x       // 상수 레지스터의 위치를 정하기 위해 인덱스에 4를 곱한다.

mova a0, r0            // 상수 레지스터 위치 저장

 

쉐이더의 레지스터는 [] 연산자를 이용해서 데이터를 가져올 수 있습니다. 상수 레지스터에 복사된 행렬 값을 []연산자로 가져와서 4개의 인덱스에 대한 변환을 구현 합니다.

 

#define MATRIX_OFFSET  12

m4x4 r0, v0, c[MATRIX_OFFSET + a0.x]

m4x4 r1, v0, c[MATRIX_OFFSET + a0.y]

m4x4 r2, v0, c[MATRIX_OFFSET + a0.z]

m4x4 r3, v0, c[MATRIX_OFFSET + a0.w]

 

모든 비중의 합은 1입니다. 그런데 정점 구조체의 비중은 "float g[3]"으로 되어 있어서 입력 레지스터는 3개의 비중만 전달될 것이므로 입력 레지스터에서 비중을 먼저 복사하고 마지막 비중을 "1- (v1.x + v1.y + v1.z)"으로 계산합니다.

 

def     c0, 4.0, 0.0, 0.0, 1.0

mov     r4, v1

add     r4.w, r4.x, r4.y

add     r4.w, r4.w, r4.z

add     r4.w, c0.w,-r4.w

 

변환된 정점의 위치에 각각 비중을 곱하고 이 위치들을 전부 더합니다.

 

// 변환된 정점의 위치에 각 비중을 곱한다.

mul r0, r0, r4.x

mul r1, r1, r4.y

mul r2, r2, r4.z

mul r3, r3, r4.w

 

// 비중이 곱해진 각 위치를 더한다.

add     r0, r0, r1

add     r0, r0, r2

add     r0, r0, r3

 

뷰 변환, 투영 변환 행렬을 적용하고 출력 레지스터에 복사하면 스키닝이 완성됩니다.

 

m4x4 r1, r0, c4               // 뷰 변환

m4x4 oPos, r1, c8      // 투영 변환

 

<정점 쉐이더 스키닝: s0v_08_vertex_skinning.zip>

 

 

2.9 안개 효과(Fog)

정점 쉐이더 중에서 흥미로운 부분이 포그(Fog) 입니다. 포그는 정점의 위치를 가지고 결정합니다. 고정 기능 파이프라인에서의 포그는 카메라의 거리 또는 카메라의 z 축에 대한 값에 의존하기 때문에 높은 산에 올라가면 구름이 발 밑에 걸리는 높이 포그(Layered Fog) 같은 것들은 표현이 불가능 합니다.

쉐이더는 공식만 정해지면 포그에 대해서 아주 쉽게 구현할 수 있습니다. 예를 들어 고정 기능 파이프라인과 동일한 카메라의 Z 축 방향의 거리에 의존하는 선형 포그(Linear)의 계수는 다음과 같이 계산합니다.

 

Fog Factor = 뷰 변환 후 정점의 z /(포그 끝 값 포그 시작 값)

 

'뷰 변환 후의 정점 z'을 사용하는 이유는 뷰 행렬을 월드 변환한 정점에 곱하면 카메라 공간의 z축에 대한 값으로 계산 되기 때문입니다.

 

뷰 변환 후 정점 z = dot( (월드 변환 정점 - 카메라 위치), 카메라 z축 벡터)

 

공식들이 간단해서 어렵지 않게 쉐이더를 작성할 수 있습니다.

 

dcl_position   v0

m4x4 r0, v0, c4               // 뷰 변환 후 정점 z

 

뷰 변환 후의 정점 z를 구했고 다음으로 Fog Factor를 구합니다.

 

#define FogBgn c12.x   // 포그 시작

#define FogEnd c12.y   // 포그

#define FogDsR c12.w   // 1/(포그 - 포그 시작 )

sub r0.z, FogEnd, r0.z // (fog end - distance)

mul r0.x, r0.z, FogDsR // Fog Factor = distance/(end - begin)

 

Fog Factor [0, 1] 범위에 있도록 하기 위해서 min, max 를 이용합니다.

 

def c14, 1.0, 0.0, 0.0, 0.0

min r0.x, r0.x, c14.x  // 1 보다 큰 값 제거

max r0.x, r0.x, c14.y  // 음수 값 제거

 

마지막으로 Fog Factor를 출력 레지스터 oFog에 복사 합니다.

 

mov oFog, r0.x

 

<선형 포그(Linear Fog): s0v_09_fog1_range.zip>

 

고정 기능 파이프라인의 Range Fog는 뷰 변환 후의 정점 z에 의존하는 포그입니다. 높이 포그(Layered Fog)는 월드 변환 후의 정점 y에 대한 포그이기 때문에 Fog Factor 공식은 아주 간단합니다.

 

Layered Fog Factor = 월드 변환 후 정점의 y /(포그 끝 값 포그 시작 값)

 

월드 변환이 없는 정점의 경우에서는 Fog Factor를 쉐이더로 작성하는 것은 어려운 일이 아닙니다.

 

#define FogBgn c12.x

#define FogEnd c12.y

#define FogDsR c12.w   // 1/(FogEnd-FogBgn)

def c14, 1.0, 0.0, 0.0, 0.0

dcl_position   v0

mul r0.x, v0.y, FogDsR // Output FogFactor = height/(end - begin)

min r0.x, r0.x, c14.x  // 1 보다 큰 값 제거

max r0.x, r0.x, c14.y  // 음수 값 제거

mov oFog, r0.x         // 포그 출력 레지스터에 저장

 

<높이 포그(Layered Fog): s0v_09_fog2_height.zip>

 

지금까지 Fog Factor를 계산하고 출력 레지스터 oFog에 복사했습니다. 이런 방식은 렌더링에서 디바이스의 포그를 활성화(D3DRS_FOGENABLE, TRUE) 해야 합니다. 또한 높이 포그는 D3DRS_-FOGTABLEMODE D3DFOG_NONE으로 설정해야 합니다.

쉐이더 버전 3.0 이상에서는 oFog를 사용할 수 없어서 포그를 직접 구현 해야 합니다. DXSDK의 도움말을 보면 포그가 적용될 때 고정 기능 파이프라인에서 정점의 최종 색상은 포그 색상, Fog Factor, 조명과 정점의 색상 혼합으로 만들어진 Diffuse 값을 선형 보간 형식으로 결정됩니다.

 

정점의 최종 색상 = Fog 색상 * Fog Factor + Diffuse * (1 - Fog Factor)

 

높이 포그의 예제를 수정해서 쉐이더를 적용해 봅시다. Fog Factor 계산을 mad 연산자로 한 번에 처리하기 위해서 공식을 풀어줍니다.

 

Fog Factor = (포그 끝 값 - 월드 변환 후 정점의 y )/(포그 끝 값 포그 시작 값)

        = (포그 끝 값) /(포그 끝 값 포그 시작 값)

         - 월드 변환 후 정점의 y /(포그 끝 값 포그 시작 값)

Fog Factor= - 월드 변환 후 정점의 y /(포그 끝 값 포그 시작 값)

        + 포그 끝 값 /(포그 끝 값 포그 시작 값)

 

외부에서 포그 끝 값 /(포그 끝 값 포그 시작 값)를 계산한다고 가정하고 다음과 같은 매크로를 정의 합니다.

 

#define FogFct c12.z   //  FogEnd/(FogEnd - Begin)

#define FogDsR c12.w   // 1.0/(FogEnd-FogBgn)

 

이렇게 정의된 매크로가 있으면 y 값에 의존하는 높이 포그의 Fog Factor r0.x mad로 한 번에 계산 됩니다.

 

mad     r0.x, -v0.y, FogDsR, FogFct

 

min, max 연산자로 [0, 1] 범위로 만들고 정점의 최종 색상을 선형 보간 형식으로 만들고 출력 레지스터 oD0에 복사 합니다.

 

mul r1, FogColor, r0.x

add r2, c14.x, -r0.x   // (1-r0.x) <== (1-w)

mad oD0, v2, r2, r1    // Diffuse *(1-FogFactor) + FogColor * FogFactor

 

이렇게 되면 고정 기능 파이프라인의 어떤 설정도 필요 없이 순수한 쉐이더로 포그를 구현할 수 있습니다.

 

<완전한 쉐이더로 구현된 높이 포그: s0v_09_fog3_shader.zip>

 

 

2.10 Toon Shading

정점 쉐이더의 응용 중에 하나가 툰 쉐이딩(Toon: cartoon Shading) 입니다. 툰 쉐이딩을 간단하게 설명하면 정점에 의한 반사 밝기를 선형적으로 처리하지 않고 양자화 단위의 단계적 처리를 의미합니다.

고정파이프 라인에서 만약 정점 a의 라이팅에 대한 반사의 세기가 0.3 이고 정점 b의 라이팅에 세기가 0.7이면 중간 밝기는 선형적인 계산을 통해 보간합니다. 하지만 툰 효과는 반사의 밝기를 선형적으로 처리하지 않고 마치 계단처럼 특정한 범위 내에서는 같은 밝기로 처리합니다.

만약 고정 기능 파이프라인에서 툰 효과를 만들기 위해서는 렌더링의 여러 패스를 거쳐 가야 하지만 정점 쉐이더를 사용하면 아주 간단하고 쉽게 처리할 수 있습니다.

 

정점의 조명 효과에 대한 밝기를 oD0에 출력했습니다. 그런데 밝기의 세기는 [0, 1] 범위이며 이 것을 oD0에 출력하지 않고 텍스처 좌표로 출력하면 픽셀 처리 과정에서 다음 그림과 같이 연속적으로 변하는 텍스처에서 색상을 샘플링 하게 되면 샘플링 된 색상은 곧, 조명의 밝기와 동등한 결과가 됩니다.

 

<밝기가 0, [0, 1] 범위, 1에서의 샘플링 위치>

 

쉐이더는 값을 정하지 않으면 0 또는 1이 되기 때문에 조명의 밝기를 텍스처에서 가져올 때 쉐이더에서 y는 대부분 설정을 안 합니다.

Lambert 확산에 대해서 밝기의 계산은 dot(정점 법선 벡터, 빛의 방향 벡터)입니다.

 

#define Lgt     -c8

def c24, 1.0, 0.5, 0.1, .9

 

dcl_normal     v1             // 정점 법선

m3x3 r0, v1, c4                        // 법선 벡터에 대한 변환

dp3  r1, r0, Lgt               // 밝기 = Dot(변환된 법선 벡터, 빛의 방향 벡터)

 

mad r1.x, r1.x, c24.y, c24.y  // (Dot + 1)* 0.5 = Dot * 0.5 + 0.5

 

Lambert 확산에 대한 반사 세기는  "dot(정점 법선 벡터, 조명 방향 벡터)" 가 되어 전체 크기는 cos θ에 비례하고 값의 범위는 [-1, 1]이 됩니다. 이 값의 범위를 [0, 1]으로 하기 위해서 "mad" 연산자와 0.5 값을 이용했습니다.

 

계산된 반사의 밝기를 출력 레지스터 oD0에 복사하는 대신 oT0.x에 저장합니다.

 

mov oT0.x, r1.x                // 결과를 텍스처 좌표 x에 저장.

 

픽셀 처리를 고정 파이프라인을 이용하려면 다중 텍스처를 Texture Diffuse의 혼합으로 설정하고 폴리곤을 렌더링 합니다.

 

m_pDev->SetTextureStageState( 0 , D3DTSS_COLORARG1 , D3DTA_TEXTURE);

m_pDev->SetTextureStageState( 0 , D3DTSS_COLORARG2 , D3DTA_DIFFUSE);

m_pDev->SetTextureStageState( 0 , D3DTSS_COLOROP , D3DTOP_MODULATE);

 

<Lambert 확산을 텍스처 좌표로 사용. s0v_10_toon1_texture_lighting.zip>

 

툰 쉐이딩은 연속적으로 변화하는 텍스처 대신 다음과 같이 밝기가 양자화된 텍스처를 사용합니다.

 

 

그림에서 조명의 밝기 차이로 인해 파란 색 점과 붉은 색 점의 텍스처 좌표의 위치는 다르지만 텍스처에서 샘플링 하는 픽셀의 색상은 동등합니다.

이전의 Lambert 확산에 대해서 연속적으로 변하는 텍스처 대신 양자화된 텍스처를 적용하면 다음 그림과 같이 툰 쉐이딩(Toon Shading)이 적용된 장면을 볼 수 있습니다.

 

<툰 쉐이딩(Toon Shading): s0v_10_toon1_texture_toon.zip>

 

툰 쉐이딩에서 사용하는 텍스처는 x만 사용하기 때문에 높이가 필요 없습니다. 1차원 텍스처는 높이가 하나의 픽셀로 구성하면 됩니다. 1차원 텍스처 만드는 것은 간단해서 그래픽 툴을 이용하는 것 보다 프로그램에서 실시간으로 만드는 것이 정보 보호를 위해서 이점이 있습니다.

D3DXCreateTexture() 함수를 사용하면 실시간으로 텍스처를 만들 수 있으며 텍스처 객체의 LockRect() 함수를 사용해서 픽셀 데이터를 가져와서 수정 할 수 있습니다.

 

hr = D3DXCreateTexture(m_pDev , 512, 1 , 0, 0

                       , D3DFMT_X8R8G8B8, D3DPOOL_MANAGED, &m_pTex );

if ( FAILED(hr) )

        return hr;

 

D3DLOCKED_RECT pRect;

m_pTex->LockRect(0, &pRect, NULL, 0);

 

DWORD*  pColor  = (DWORD*)pRect.pBits;

for(INT i = 0 ; i < 512; ++i)

{

        FLOAT   c = 0;

 

        if(i<10)       c = 0;

        else if(i<100) c= 0.2f;

        else if(i<200) c= 0.4f;

        else if(i<300) c= 0.6f;

        else if(i<400) c= 0.8f;

        else           c= 1.0;

 

        pColor[i]      = D3DXCOLOR(c,c,c,1);

}

 

m_pTex->UnlockRect(0);

 

<1차원 툰 쉐이딩 텍스처: s0v_10_toon2_1D_texture.zip>

 

툰 쉐이더용 텍스처를 사용하지 않고 직접 조건 문을 사용해서 툰 쉐이딩을 구현하는 방법도 있습니다. 그런데 저 수준으로 약간 난이도 있는 조건 문을 작성하는 것보다 고 수준 언어의 조건 문이 훨씬 간단 하므로 이후 HLSL에서 텍스처 없이 툰 쉐이딩을 구현해 보도록 하겠습니다.

 

조명을 툰 쉐이딩으로 처리하고 Diffuse용 텍스처와 다중 텍스처 처리로 혼합을 하게 되면 툰 쉐이딩의 기본적인 내용은 마무리가 됩니다.

 

<툰 쉐이딩 + Diffuse Map: s0v_10_toon2_diffuse+toon.zip>

 

s0v_10_toon2_diffuse+toon.zip의 멀티 텍스처 처리 방식은

 

최종 색상 = Diffuse Map * Diffuse Color + Toon Shading - 0.5

 

이를 고정 기능 파이프라인에서 다중 텍스처 처리를 다음과 같이 작성했습니다.

 

m_pDev->SetTextureStageState( 0 , D3DTSS_COLORARG1 , D3DTA_TEXTURE);

m_pDev->SetTextureStageState( 0 , D3DTSS_COLORARG2 , D3DTA_DIFFUSE);

m_pDev->SetTextureStageState( 0 , D3DTSS_COLOROP , D3DTOP_MODULATE);

 

m_pDev->SetTextureStageState( 1 , D3DTSS_COLORARG1 , D3DTA_CURRENT);

m_pDev->SetTextureStageState( 1 , D3DTSS_COLORARG2 , D3DTA_TEXTURE);

m_pDev->SetTextureStageState( 1 , D3DTSS_COLOROP , D3DTOP_ADDSIGNED);

m_pDev->SetTexture( 0, m_pTxDif );

m_pDev->SetTexture( 1, m_pTxToon );

 

 

2.11 윤곽선(Edge)

윤곽선을 만드는 방법은 변환한 정점의 깊이 값, 정점의 ID, 법선 벡터 등을 이용해서 만드는데 간단하게 윤곽선을 만들고자 할 때는 정점의 법선 벡터를 이용하는 것이 편리합니다.

 

모델 좌표계에서 윤곽선에 해당하는 정점 위치는 다음과 같이 만들 수 있습니다.

 

윤곽선 정점 위치 = 정점 위치 + 법선 벡터 * 크기

 

렌더링은 2번 진행 합니다. 먼저 일반적인 정점을 렌더링하고 다음으로 윤곽선 정점의 위치를 렌더링 합니다.

고정 기능 파이프라인에서는 윤곽선 정점 위치에 대해서 정점 버퍼를 새로 만들어야 하지만 쉐이더를 사용하면 기존에 있는 정점 버퍼를 그대로 사용하고 대신 쉐이더에서 정점의 법선 벡터와 크기 값을 이용해서 윤곽선 정점 위치를 설정합니다.

 

#define Scl c27

def c25, 0.0, 0.0, 0.0, 1.0

 

dcl_position   v0      // 정점 위치 벡터 레지스터 선언 v0

dcl_normal     v1      // 정점 법선 벡터 레지스터 선언 v1

 

mov r0, v1             // 법선 벡터

mad r0, r0, Scl, v0    // 윤곽선 위치' = 법선 벡터 * 스케일 + 위치

mov r0.w, c25.w                // 윤곽선 위치' w = 1.0

m4x4 oPos, r0, c0      // 변환

 

같은 정점 버퍼를 가지고 두 번 그리는데 첫 번째에서는 CCW로 렌더링 합니다.

// Toon Shading Process

m_pDev->SetRenderState(D3DRS_CULLMODE, D3DCULL_CCW);

m_pDev->SetTexture( 0, m_pTxToon );

m_pDev->DrawIndexedPrimitive( D3DPT_TRIANGLELIST, 0, 0, m_nVtx, 0, m_nFce);

 

두 번째는 CW로 그립니다. 이렇게 하면 CCW로 그린 것이 앞쪽에 나오게 됩니다.

 

// Edge Process

m_pDev->SetRenderState(D3DRS_CULLMODE, D3DCULL_CW);

m_pDev->SetTexture( 0, NULL);

m_pDev->DrawIndexedPrimitive( D3DPT_TRIANGLELIST, 0, 0, m_nVtx, 0, m_nFce);

 

<윤곽선: 위치 + 법선 벡터* 크기. s0v_10_toon3_edge1.zip>

 

정확한 윤곽선 대신 대충 만드는 윤곽선도 있습니다. 이 윤곽선은 카메라의 (-)z축과 법선 벡터의 내적 결과를 툰 쉐이딩과 같이 특정 텍스처의 좌표로 설정하는 것입니다.

 

윤곽선 텍스처 좌표 = dot(변환된 법선 벡터, 카메라 (-)z)

 

<윤곽선 텍스처>

 

지금까지 내용을 가지고 DX Tiny의 모델에 대해서 Diffuse Map, 툰 쉐이딩, 윤곽선 텍스처 적용을 쉐이더로 작성해 봅시다.

 

쉐이더 프로그램 작성을 편리하게 하기 위해서 다음과 같이 매크로를 정의합니다.

 

#define Nor     r2

#define Lgt     -c8

#define CamZ    -c16

 

입력 레지스터는 위치, 법선 벡터, Diffuse, 텍스처 좌표들로 선언하고 지정합니다.

 

dcl_position   v0      // 정점 위치 벡터 레지스터 선언 v0

dcl_normal     v1      // 정점 법선 벡터 레지스터 선언 v1

dcl_color0     v2      // 정점 디퓨즈 색상

dcl_texcoord0  v3      // 정점 텍스처 좌표

 

먼저 정점 위치 변환과 정점의 Diffuse , Diffuse Map 텍스처 좌표를 출력 레지스터에 복사합니다.

 

m4x4 oPos, v0, c0      // 출력 위치

mov oD0, v2            // 정점 색상

mov oT0, v3            // Diffuse Map 텍스처 좌표

 

툰 쉐이딩에 대한 텍스처 좌표를 구현하고 oT1에 복사합니다.

 

def c24, 1.0, 0.5, 0.1, 0.9

m3x3 Nor, v1, c4       // 법선 벡터에 대한 변환

dp3 r1, Nor, Lgt      // 정점 밝기 계산

mad r1.x, r1.x, c24.y, c24.y

mov oT1.x, r1.x               // 결과를 텍스처 좌표 x에 저장

 

윤곽선에 대한 텍스처 좌표를 구하고 이 결과를 oT2에 복사합니다. 텍스처 좌표는 카메라의 -z축과 변환된 정점의 법선 벡터의 내적으로 계산됩니다.

 

dp3 r3.x, Nor, CamZ    // Edge = dot(N, (-)CameraZ)

mov oT2.x, r3.x

 

고정 기능 파이프라인에서 다중 텍스처 처리 연산은 "정점 Diffuse" * "Diffuse Map" * "Toon Shading" * "윤곽선"으로 계산합니다. 이를 구현하면 다음과 같습니다.

 

m_pDev->SetTextureStageState(0, D3DTSS_COLORARG1D3DTA_TEXTURE);

m_pDev->SetTextureStageState(0, D3DTSS_COLORARG2D3DTA_DIFFUSE);

m_pDev->SetTextureStageState(0, D3DTSS_COLOROPD3DTOP_MODULATE);

 

m_pDev->SetTextureStageState(1, D3DTSS_COLORARG1D3DTA_TEXTURE);

m_pDev->SetTextureStageState(1, D3DTSS_COLORARG2D3DTA_CURRENT);

m_pDev->SetTextureStageState(1, D3DTSS_COLOROPD3DTOP_MODULATE);

 

m_pDev->SetTextureStageState(2, D3DTSS_COLORARG1D3DTA_TEXTURE);

m_pDev->SetTextureStageState(2, D3DTSS_COLORARG2D3DTA_CURRENT);

m_pDev->SetTextureStageState(2, D3DTSS_COLOROPD3DTOP_MODULATE);

 

<Diffuse * Toon * Edge. s0v_10_toon3_edge2.zip>

 

 

 

2.9 Depth Encoding

백 버퍼에 저장되는 깊이 값은 때로는 볼륨 포그나 투영 그림자 처리에서 종종 이용되기도 합니다. 정점이 그래픽 파이프라인의 변환을 거치면 Z축의 값은 [0, 1]의 값을 가지게 되어서 이것을 색상으로 사용해도 되지만 변환 후에 z는 실제로 1.0 근처에 몰려 있습니다. 따라서 적당한 값을 정규 변환 후에 z값에 곱하고 이 것을 색상으로 사용하는 것이 좋습니다.

 

정점의 변환 값을 색상으로 사용하기 때문에 임시 레지스터에 월드, , 투영 행렬의 곱으로 구성된 행렬에 정점의 위치를 곱한 값을 저장합니다.

 

dcl_position   v0             // 정점 위치를 입력 레지스터 v0에 선언

m4x4 r0, v0, c0                       // 정점의 변환: 입력 위치 * c0에 입력된 행렬

 

다음으로 출력 레지스터 oPos에 임시 레지스터에 저장된 변환된 정점 위치를 복사합니다.

 

mov  oPos, r0                 // 출력 위치 = r0, z=[0,1]

 

Depth Range 값을 저장한 상수 레지스터의 값과 임시 레지스터에 저장된 변환된 정점 위치의 z값을 곱하고 이것을 출력 레지스터 oD0에 복사합니다.

 

#define DepthRange     c26

mul oD0, r0.z, DepthRange.z   // 출력 디퓨즈 색상 = 변환 후 정점의 z * DepthRange.z

 

<깊이 값 렌더링. s0v_11_depth.zip>

 

 

2.10 Vertex Shader Effect

쉐이더에서 행렬을 정점 쉐이더의 상수 레지스터에 설정할 때 매번 전치(Transpose) 하는 것이 종종 개발자를 혼란하게 만들거나 때로는 전치를 하지 않고 연결하는 실수를 종종 합니다. 또한 SetVertexShaderConstantF() 함수에서 Float , Vector , Matrix 형 등의 다른 타입을 하나의 함수에 설정하다 보니 float4형의 개수를 인수로 전달해야 합니다.

이것은 D3D 디바이스가 저 수준을 지원하기 때문에 인터페이스를 늘리지 않기 위해서 어쩔 수 없이 만들어진 형태입니다.

 

만약 다음과 같이 저 수준 정점 쉐이더의 인터페이스를 만든다면 쉐이더 사용을 좀 더 편리하게 사용할 수 있습니다.

 

interface ILcShader

{

        virtual INT    Begin()=0;

        virtual INT    End()=0;

 

        virtual INT     SetFVF(void* pFVF)=0;

        virtual INT    SetMatrix(INT nRegister, const D3DXMATRIX* v, INT Count=1)=0;

        virtual INT    SetVector(INT nRegister, const D3DXVECTOR4* v)=0;

        virtual INT    SetColor(INT nRegister, const D3DXCOLOR* v)=0;

        virtual INT    SetFloat(INT nRegister, const FLOAT* v)=0;

};

 

s0v_12_ShaderEffect.zipILcShader ILcShader를 상속 받은 CLcShader 클래스가 구현되어 있습니다.

Begin()/End() 함수는 정점 처리를 프로그램 가능한 파이프라인에서 처리를 하거나 해제하는 함수로 다음과 같이 구현되어있습니다.

 

INT CLcShader::Begin()

{

        return m_pDev->SetVertexShader(m_pShd);

}

 

INT CLcShader::End()

{

        m_pDev->SetVertexDeclaration(NULL);

        return m_pDev->SetVertexShader(NULL);

}

 

SetFVF()는 고정 기능 파이프라인의 FVF()함수 호출과 유사한 기능을 하기 때문에 이 이름이 붙었습니다.

 

INT CLcShader::SetFVF(void* pFVF)

{

        return m_pDev->SetVertexDeclaration((PDVD)pFVF);

}

 

D3D 디바이스의 SetVertexConstantF() 함수를 분리해서 Matrix, Vector, Color, Float 형에 대해서 처리하도록 각 기능에 대한 명세를 분명히 하는 것이 프로그램 응집성에 도움이 됩니다.

 

INT CLcShader::SetMatrix(INT uReg, const D3DXMATRIX* v, INT nCount)

{

        HRESULT hr;

        for(int i=0; i<nCount; ++i)

        {

               D3DXMATRIX     t;

               D3DXMatrixTranspose(&t, &v[i] );

               hr = m_pDev->SetVertexShaderConstantF( uReg + i*4, (FLOAT*)&t, 4);

               if(FAILED(hr))

                       return -1;

        }

        return 0;

}

 

INT CLcShader::SetVector(INT uReg, const D3DXVECTOR4* v)

{

        return m_pDev->SetVertexShaderConstantF( uReg , (FLOAT*)v , 1);

}

 

INT CLcShader::SetColor(INT uReg, const D3DXCOLOR* v)

{

        return m_pDev->SetVertexShaderConstantF( uReg , (FLOAT*)v , 1);

}

 

INT CLcShader::SetFloat(INT uReg, const FLOAT* v)

{

        return m_pDev->SetVertexShaderConstantF( uReg , (FLOAT*)v , 1);

}

 

이렇게 구성된 ILcShader 인터페이스는 다음과 같이 간단하게 객체를 생성하고 사용할 수 있습니다.

 

ILcShader*     m_pVs;

 

// 함수를 통한 객체 생성

LcDev_CreateVertexShaderFromFile(&m_pVs, m_pDev, "data/Shader.vsh");

 

// 쉐이더 사용

m_pVs->Begin();

m_pVs->SetFVF(m_pFVF);

 

D3DXVECTOR4 DepthScalers(1.0f, 0.004F, 0.0f, 1.0f);

// 상수 연결

m_pVs->SetMatrix( 0, &(m_mtWld * mtViw * mtPrj));

m_pVs->SetVector(26, &DepthRange);

m_pDev->DrawPrimitive(…);

 

// 쉐이더 해제

m_pVs->End();

 

전체 코드는 s0v_12_ShaderEffect.zip를 참고하기 바랍니다.

 

 

 


3. Pixel Shader

지금까지 우리는 정점 쉐이더 사용법을 살펴보았습니다. 정점 쉐이더는 변환, 조명, 안개 효과 등 래스터라이징 이전까지의 처리 과정을 프로그램 가능한 파이프라인을 이용하는 것입니다.

Rasterizing 이후 만들어진 픽셀 데이터는 픽셀 처리 과정(Pixel Processing)으로 넘어갑니다. 픽셀 처리 과정은 샘플링→ 다중 텍스처 처리 → 알파 테스트 → 깊이 테스트 → 스텐실 테스트 → 픽셀 포그 → 알파 블렌딩 순으로 진행되고 마지막에 후면 버퍼를 갱신하는 과정입니다.

픽셀 쉐이더는 이러한 픽셀 처리 과정 중에서 텍스처에서 색상을 추출하는 샘플링(Sampling)과 입력된 색상을 혼합하는 다중 텍스처 처리(Multi-Texturing)를 고정 기능 파이프라인이 아닌 프로그램 가능한 파이프라인으로 처리하는 것입니다.

 

<픽셀 처리 과정>

 

하나의 삼각형을 구성하기 위해서 세 개의 정점이 필요하지만 이 세 정점으로 구성된 삼각형의 픽셀을 채우는 작업은 화면 해상도와 카메라의 위치에 따라 달라집니다. , 정점 처리보다 픽셀 처리가 훨씬 많은 작업이 필요합니다.

 

<정점 쉐이더와 픽셀 쉐이더 역할 비교>

 

이와 같은 기술적 문제들과 픽셀 처리에 필요한 하드웨어의 구성에 대한 비용 문제로 인해서 픽셀 쉐이더보다 정점 쉐이더를 지원하는 GPU가 먼저 출시되었습니다. 또한 초창기 GPU 제조사들은 저마다 자신들의 저 수준 픽셀 쉐이더 언어를 표준으로 정하려고 했기 때문에 정점 쉐이더와는 다르게 명령어들이 일관성이 부족했습니다.

 

기술 보다 이론이 앞서 있는 상황에서 아직까지 1.x 버전에서 작성한 많은 예제들이 있지만 픽셀 쉐이더는 버전 2.0에 와서 많은 명령어들이 정리되었기 때문에 저수준으로 작성하면 각 버전마다 다른 명령어를 사용해야 하지만 고 수준으로 작성하면 거의 같은 함수를 사용할 수 있어서 여러분은 최소한 2.0이상 버전에서 고 수준으로 작성하는 것이 사용의 편리와 유지 보수를 위해서 좋습니다.

 

<픽셀 쉐이더 가상머신>

 

픽셀 쉐이더 가상 머신은 정점 쉐이더 가상 머신처럼 입력 레지스터, 출력 레지스터, 상수 레지스터, 임시 레지스터, 그리고 산술과 논리 연산을 담당하는 ALU로 구성되어 있습니다.

입력 레지스터는 Rasterizing 을 거친 픽셀 데이터에 대해서 v로 시작을 합니다. 색상 레지스터(Color Register) v# v0 v1이 있고, v0 Diffuse, v1 Specular에 해당합니다.

t으로 시작하는 입력 레지스터 t#은 픽셀 쉐이더 버전 마다 차이가 있습니다. 1.x 버전에서는 텍스처 좌표에 의해 샘플링 된 픽셀이지만 2.0 이후에는 텍스처 좌표 자체입니다.

s로 시작하는 입력 레지스터 s#은 색상을 추출하는 샘플러(Sampler) 객체입니다.

임시 레지스터 r#은 정점 쉐이더의 임시 레지스터와 같은 기능을 수행하며 연산의 결과를 저장하는 용도로 사용되는 읽기, 쓰기 레지스터입니다.

상수 레지스터 c# 역시 정점 쉐이더의 상수 레지스터와 같은 기능을 수행하고 쉐이더 내부에서는 읽기만 가능하고 값의 설정은 외부에서 디바이스의 함수를 사용하거나 "def" 명령어로 미리 정해야 합니다.

출력 레지스터는 쉐이더 버전 마다 크게 차이가 있습니다. 1.x 버전에서는 임시 레지스터 r0가 출력 레지스터입니다. 2.x 버전에서는 Multi-element texture가 가능해서 oC0~ oC3이 있고, oDepth도 있습니다. 보통 oC0로 출력하는데 oC0는 후면 버퍼에 해당합니다.

픽셀 ALU는 픽셀 데이터에 대한 산술 연산과 논리 동작을 담당하며 기본적인 사칙 연산부터 픽셀에 대해서 내적, 제곱근, 승수, exp, log 등의 수학 함수들과 조건 문 등을 처리합니다.

 

<픽셀 쉐이더 레이아웃>

 

저 수준 픽셀 쉐이더 코드와 입력된 텍스처, 래스터 과정에 의해 만들어진 픽셀의 관계는 그림처럼 먼저 입력 레지스터 선언에 따라 픽셀은 v#, 텍스처 좌표 또는 텍스처는 t#, 샘플러 객체는 #s 입력 레지스터에 저장됩니다. 이 입력 레지스터에 저장된 값과 미리 설정된 상수 레지스터 c#에 저장된 값들을 가지고 픽셀 ALU는 연산을 합니다. 또한 명령어에 따라 연산의 결과를 임시 레지스터를 이용하고, 마지막으로 출력 레지스터에 복사 합니다.

 

100번 보는 것보다 한 번 작성해 것이 훨씬 이해가 빠르기 때문에 픽셀 쉐이더 연습을 통해서 하나하나 배워봅시다.

 

 

3.1 간단한 픽셀 쉐이더

3.1.1 Diffuse 출력

저 수준 픽셀 쉐이더의 명령어들과 문법은 정점 쉐이더와 거의 비슷합니다. 따라서 픽셀 쉐이더 코드를 컴파일 하고, 쉐이더 객체를 생성하고, 렌더링에서 상수 설정 등에 대한 기본 동작은 정점 쉐이더와 부분적으로 함수 이름만 다를 뿐 거의 같은 형식으로 되어 있습니다.

그런데 픽셀 쉐이더 1.x 에서 각 버전 마다 명령어들의 차이가 정점 쉐이더 보다 훨씬 심합니다. 예를 들어 ATI에서 ps_1_4 버전이 지원이 되지만 NVIDIA 계열에서는 ps_1_4를 지원 안 합니다. 따라서 여러분은 간단한 픽셀 쉐이더의 경우 1.1 버전을 사용하거나 아니면 2.0이상 버전으로 사용하는 것이 좋습니다.

여기서는 픽셀 쉐이더 버전 1.1 2.0을 중심으로 강의를 하겠습니다.

 

간단하게 색상이 있는 사각형을 출력하는 예를 만들어 봅시다. 이를 위해 다음과 같은 정점 구조체를 사용해야 합니다.

 

struct VtxD

{

        D3DXVECTOR3    p;      // 정점 위치

        DWORD          d;      // 정점 색상

        enum {FVF = (D3DFVF_XYZ|D3DFVF_DIFFUSE),};

};

 

이 구조체로 사각형을 출력하도록 4개의 정점을 만들고 위치와 색상을 설정합니다.

 

VtxD    m_pVtx[4];             // 정점 데이터

m_pVtx[0] = VtxD(-0.9F0.9F0, D3DXCOLOR(1,0,0,1));

m_pVtx[1] = VtxD( 0.9F0.9F0, D3DXCOLOR(0,1,0,1));

m_pVtx[2] = VtxD( 0.9F, -0.9F0, D3DXCOLOR(0,0,1,1));

m_pVtx[3] = VtxD(-0.9F, -0.9F0, D3DXCOLOR(1,0,1,1));

 

고정 기능 파이프라인에서는 디바이스의 DrawPrimitive…() 함수를 호출하면 바로 출력되었습니다. 우리는 픽셀 쉐이더를 이용해서 출력하는 것이 목표이기 때문에 다음과 같이 쉐이더 코드를 작성합니다.

 

ps_1_1                 // 픽셀 쉐이더 버전 선언

mov r0, v0             // 출력 레지스터에 복사

 

v0는 래스터 처리를 거친 Diffuse 값입니다. 픽셀 쉐이더 버전 1.x에서는 임시 레지스터로 사용되는 r0가 출력 레지스터 이기도 합니다. 버전 1.1에서는 임시 레지스터를 r0 r1 2개 정도만 사용할 수 있습니다.

이와 동등한 코드를 픽셀 쉐이더 2.0로 작성하면 다음과 같습니다.

 

ps_2_0                 // 픽셀 쉐이더 버전 선언

dcl v0                 // Diffuse를 입력 레지스터 v0 선언

mov oC0, v0            // 출력 레지스터에 복사

 

dcl은 입력 레지스터를 선언할 때 사용되며 v#는 색상, 텍스처 좌표는 t#, 샘플러는 s#으로 지정합니다.

 

이 픽셀 쉐이더 명령어를 컴파일하고 픽셀 쉐이더 객체를 생성하는 코드는 다음과 같습니다.

 

DWORD dwFlags = 0;

#if defined( _DEBUG ) || defined( DEBUG )

        dwFlags |= D3DXSHADER_DEBUG;

#endif

 

LPD3DXBUFFER pShd = NULL;     LPD3DXBUFFER pErr = NULL;

hr = D3DXAssembleShaderFromFile"data/Shader.psh"

                       , NULL, NULL, dwFlags, &pShd, &pErr);

 

if( FAILED(hr) )

{

        if(pErr)

        {

               MessageBox( hWnd, (char*)pErr->GetBufferPointer(), "Err", MB_ICONWARNING);

               pErr->Release();

        }

        else

        {

               MessageBox( hWnd, "File is Not exist", "Err", MB_ICONWARNING);

        }

        return -1;

}

 

hr = m_pDev->CreatePixelShader( (DWORD*)pShd->GetBufferPointer() , &m_pPs);

pShd->Release();

 

if ( FAILED(hr) )

        return -1;

 

정점 쉐이더와 마찬가지로 쉐이더 컴파일은 문자열은 D3DXAssembleShader()함수를 사용하고 파일은 D3DXAssembleShaderFromFile() 함수를 사용합니다. 픽셀 쉐이더 객체는 디바이스의 CreatePixelShader() 함수를 이용해서 픽셀 쉐이더 객체를 생성합니다.

이렇게 만든 픽셀 쉐이더 객체를 렌더링에서 사용하기 위해서는 정점 쉐이더와 유사하게 픽셀 처리를 프로그램 가능한 파이프라인 사용을 디바이스에 알려야 합니다.

 

m_pDev->SetPixelShader(m_pPs);

 

이후 과정은 렌더링은 고정 기능 파이프라인과 같습니다.

 

m_pDev->SetFVF(…);

m_pDev->DrawPrimitive();

 

프로그램 가능한 파이프라인 사용이 끝나면 이 또한 디바이스에게 알립니다.

 

m_pDev->SetPixelShader( NULL);

 

이와 관련한 전체 코드는 s1p_01_basic1.zip ShaderEx.cpp "data/shader.psh" 파일을 참고 하기 바랍니다.

 

<정점 색상 출력 픽셀 쉐이더. s1p_01_basic1.zip>

 

3.1.2 상수 레지스터 설정

픽셀 쉐이더에서 처리되는 데이터 또한 정점 쉐이더와 마찬가지로 float4형 이고 색상에 대해서 r, g, b, a 또는 x, y, z, w 로 접근할 수 있으며 swizzling 또한 가능합니다.

상수 설정도 float4형으로 전달해야 하며 SetPixelShaderConstantF() 함수를 사용합니다. 이 함수는 SetVertexConstantF()와 비슷하게 상수 레지스터의 값을 설정하는 함수로 첫 번째 인수는 상수 레지스터 번호를 지정하고, 두 번째 인수는 float형 데이터의 배열을 설정합니다. 마지막 세 번째 인수는 float4형의 개수를 정합니다.

상수 레지스터 설정의 우선 순위는 쉐이더 내부에서 정한 코드입니다. 정점 쉐이더와 마찬 가지로 외부에서 SetPixelShaderConstantF() 함수로 상수 레지스터의 값을 설정해도 쉐이더 내부에서 정의되어 있으면 이 정의된 값으로 상수 레지스터가 정해집니다.

 

만약 Diffuse와 두 개의 색상을 곱해서 최종 색상 = Diffuse * 색상 0 * 색상 1으로 정하는 것을 픽셀 쉐이더로 작성하면 다음과 같습니다.

 

ps_1_1

def c0, 2, 2, 2, 1     // 상수 레지스터 c0.rgba =(2,2,2,1)

mul r0, c0, v0         // 상수 값과 색상 혼합

mul r0, r0, c1         // 외부에서 정한 상수 값과 색상 혼합

 

또는

 

ps_2_0

def c0, 2, 2, 2, 1     // 상수 레지스터 c0.rgba =(2,2,2,1)

dcl v0                 // Diffuse를 입력 레지스터 v0 선언

mul r0, c0, v0         // 상수 값과 Diffuse

mul r0, r0, c1         // 외부에서 정한 상수 값과 색상 혼합

 

mov oC0, r0            // 출력 레지스터에 복사

 

쉐이더 내부에서 상수 레지스터 c0가 설정되었습니다. 상수 레지스터를 외부에서 설정하려면 SetPixelShaderConstantF()함수로 다음과 같이 작성합니다.

 

D3DXCOLOR color0(0, 0, 0, 1);

m_pDev->SetPixelShaderConstantF(0, (float*)&color0, 1);      // 상수 레지스터 값 설정

 

D3DXVECTOR4 color1(0.5f, 1, 1, 1);

m_pDev->SetPixelShaderConstantF(1, (float*)&color1, 1);      // 상수 레지스터 값 설정

 

색상에 대해서 D3DXVECTOR4 구조체를 사용할 수 있지만 D3DXCOLOR 구조체를 사용하는 것이 보기 좋습니다.

외부에서 c0를 설정하고 있지만 c0는 쉐이더 내부에서 "def"로 이미 설정되어 있어 이 값이 렌더링에 적용됩니다.

 

<픽셀 쉐이더 상수 레지스터 설정. s1p_01_basic2_const.zip>

 

 

3.1.3 색상 반전(Invert)

색상 밝기의 반전(Invert)는 다음과 같이 계산 합니다.

 

반전 색상 = 1.0 - 색상

 

고정 기능 파이프라인에서 색상을 반전하려면 전체 픽셀을 복사해서 이 공식을 적용해야 하는데 픽셀 쉐이더를 사용하면 다음과 같이 간단한 코드로 사용하면 입력된 Diffuse에 대해서 색상을 반전 시킬 수 있습니다.

 

ps_1_1

def c0, 1, 1, 1, 0

sub r0, c0, v0

 

또는

 

ps_2_0

def c0, 1, 1, 1, 0

dcl v0

sub r0, c0, v0

mov oC0, r0

 

<색상 반전(Invert): s1p_01_basic3_invert.zip>

 

 

3.1.4 색상 단색(Monotone)

R, G, B 색상을 하나의 색상으로 만드는 단색화(Monotone)도 픽셀 쉐이더를 사용하면 편리합니다.

 

단색화 색상 = (R + G + B)/3

 

으로 정의할 수 있지만 사람의 눈은 Green Red에 더 민감하다고 합니다. 경험적인 데이터에 의해 단색화 색상은 다음과 같이 정의 합니다.

 

단색화 색상 = R * 0.299 + G * 0.587 + B * 0.114

 

RGB 색상을 3차원 벡터로 생각하면 단색화 색상 공식은 내적을 이용한 것과 동일합니다.

 

단색화 색상 = dot( (R, G, B), (0.299, 0.587, 0.114) )

 

쉐이더 코드를 구성할 때 같은 기능이라면 쉐이더에서 제공하는 함수를 사용하고, 저 수준의 경우 한 줄이라도 덜 작성하는 것이 효율이 높다고 합니다. 픽셀 쉐이더에서도 내적에 대한 dp3, dp4 연산자가 있어서 단색화 색상을 다음과 같이 작성할 수 있습니다.

 

ps_1_1

def c0, 0.299, 0.587, 0.114, 0

dp3 r0, c0, v0

 

또는

 

ps_2_0

def c0, 0.299, 0.587, 0.114, 0

dcl v0

dp3 r0, c0, v0

mov oC0, r0

 

<단색화(Monotone): s1p_01_basic4_mono.zip>

 

단색화는 게임에서 자주 사용되는 기술 이므로 꼭 기억하기 바랍니다.

 

 

3.2 Texturing & Multi-Texturing

픽셀 쉐이더에서 텍스처 색상을 처리하는 방법은 다른 픽셀 처리(Pixel Processing)과 동일하며 텍스처에서 색상을 가져오는 샘플링(Sampling)만 추가될 뿐입니다. 그런데 픽셀 쉐이더 1.x 은 텍스처 색상을 가져오는 명령어들이 버전마다 차이가 있고 제조사 마다 지원이 되지 않는 버전도 존재했었는데 이것은 픽셀 처리량이 정점 쉐이더 보다 많아 GPU를 만드는 가격과 기술의 문제가 있었기 때문이며 현재 대부분의 그래픽 카드는 픽셀 쉐이더 2.0 이상 지원 되고 있어서 이 버전부터 사용 방법과 명령어 구성의 일관성이 유지되고 있어서 이 버전 이상에서 작업하는 것이 좋습니다.

 

픽셀 쉐이더 1.1에서 텍스처 색상을 가져올 때는 tex 명령어와 t# 레지스터를 사용해서 "tex t#"으로 작성합니다.

 

ps_1_1

tex t0         // stage 0의 텍스처 좌표에서 샘플링

tex t1         // stage 1의 텍스처 좌표에서 샘플링

 

tex는 텍스처의 샘플링 명령어로 "tex t0"는 다중 텍스처 처리(Multi-Texturing) 0 번째 Stage 텍스처 좌표에 해당하는 픽셀 R, G, B, A를 가져와 t0에 저장합니다. "tex t1" 1 번째 Stage 텍스처 좌표의 픽셀을 t1에 저장합니다.

 

ATI 계열만 지원되는 픽셀 쉐이더 버전 1.4의 경우 texld를 사용합니다. texld는 두 개의 Operand가 필요합니다.

 

ps_1_4

texld r0, t0   // stage 0의 텍스처 좌표 t0에서 샘플링, r0에 저장

texld r1, t1   // stage 1의 텍스처 좌표 t1에서 샘플링, r1에 저장

 

texld의 첫 번째 Operand 임시 레지스터 r#은 픽셀의 저장 장소이고, 두 번째 Operand t#은 텍스처 좌표에 해당합니다.

앞의 "texld r0, t0" t0(0 번째 Stage 텍스처 좌표)에 해당되는 픽셀을 r0에 저장하고, "texld r1, t1" 1 번째 텍스처 좌표(t1)의 픽셀을 r1에 저장하는 것입니다.

 

픽셀 쉐이더 2.0부터 텍스처 색상을 추출하는 샘플러 객체 "s#"와 임시 레지스터 r0 대신 출력 레지스터 "oC#"이 추가되었습니다. 그리고 문법 체계도 바뀌어서 정점 쉐이더 문법과 비슷하게 입력 레지스터를 모두 선언해야 합니다.

다음은 샘플러와 입력 레지스터를 선언하고 텍스처에서 색상을 추출하는 예입니다.

 

ps_2_0

dcl_2d  s0              // 2차원 텍스처에 대한 샘플러 선언

dcl     t0.xy           // 0 Stage에 대한 2차원 xy 텍스처 좌표 선언

texld   r0, t0, s0     // 샘플러를 사용한 텍스처 색상 추출

mov     oC0, r0        // 출력 레지스터에 복사

 

샘플러 선언은 1차원 텍스처는 "dcl_1d s#", 2차원 텍스처는 "dcl_2d s#", 입방체(cube) 텍스처는 "dcl_cube s#", 볼륨 텍스처는 "dcl_volume s#"을 선언할 때 사용 합니다. 텍스처 좌표 지정은 "dcl t#"로 합니다. 1차원 텍스처 좌표는 "dcl t#.x", 2차원 텍스처 좌표는 "dcl t#.xy" t# 레지스터 다음에 각 차원에 해당하는 좌표계만큼 x, y, z, w 순서대로 적습니다.

샘플러 레지스터의 번호는 고정 기능 파이프라인에서 디바이스의 SetTexture() 함수 첫 번째 인수의 Stage에 해당합니다.

텍스처에서 색상을 가져오는 샘플링 명령은 texld를 사용합니다. 2.0에서 texld 3개의 Operand를 사용합니다. 첫 번째 Operand는 추출한 색상을 저장 장소이고, 두 번째 Operand는 텍스처 좌표 레지스터 t#입니다. 세 번째 Operand는 샘플러 레지스터 s#입니다.

이렇게 샘플러, 텍스처 좌표 등이 분리되어 있어서 한 개의 텍스처 좌표가 입력되더라도 이것을 변화시켜가면서 샘플링 할 수 있어서 다중 텍스처 처리(Multi-Texturing), Post Effect 등에서 픽셀 쉐이더 2.0 이상을 사용하는 것이 작업하기에 편리합니다.

D3D 픽셀 쉐이더 2.0은 픽셀의 결과에 대해서 Multi-element에 대한 출력이 가능합니다. 출력 레지스터 oC# oC0 ~ 0C3까지 있으며 최소한 oC0 레지스터 출력이 설정되어야 쉐이더 컴파일이 됩니다.

 

지금까지 쉐이더 버전마다 텍스처 샘플링에 대해서 살펴보았습니다. 쉐이더를 사용한 다중 텍스처 처리는 색상의 연산이므로 샘플링에 대한 방법만 알면 간단하게 만들 수 있습니다.

다음은 텍스처의 색상을 그대로 화면에 출력하는 쉐이더 코드 입니다.

 

ps_1_1

tex t0         // 텍스처 좌표 t0에 대한 샘플링

mov r0, t0     // 출력 레지스터에 복사

 

또는

 

ps_2_0

dcl_2d  s0             // 2D 샘플러 선언

dcl     t0.xy          // 2차원 텍스처 좌표 선언

texld   r0, t0, s0     // 샘플링

mov     oC0, r0        // 출력 레지스터에 복사

 

<단순 텍스처 색상 출력: s1p_02_1tex1.zip>

 

대부분 정점 처리의 Diffuse 또는 Specular 값과 혼합해서 출력합니다. 픽셀 쉐이더에서 정점 처리의 Diffuse Specular는 입력 레지스터 v0, v1으로 선언하고 텍스처 좌표는 t#으로 선언해서 사용합니다. 정점 구조체와 정점 쉐이더 출력 레지스터, 픽셀 쉐이더 입력 레지스터의 관계를 비고 하면 다음 그림과 같습니다.

 

<정점 구조체, 정점 쉐이더 출력 레지스터, 픽셀 쉐이더 입력 레지스터와의 관계>

 

간단하게 다중 텍스처 처리의 색상 연산 MODULATE4X 를 쉐이더로 구현해봅시다. MODULATE4X ARG1 ARG2를 곱하고 다시 4배를 합니다.

 

MODULATE4X 최종 색상 = 정점 처리 Diffuse * 텍스처 색상 * 4

 

ps_1_1

tex t0                 // 0번 째 Stage의 텍스처를 t0에 샘플링

mul_x4 r0, t0, v0      // 출력 색상 = 텍스처 * Diffuse * 4

 

또는

 

ps_2_0

def     c31, 4, 0, 0, 0

dcl     v0             // Diffuse 선언

dcl_2d  s0             // 2D 샘플러

dcl     t0.xy          // 0 Stage의 텍스처 좌표

 

texld r0, t0, s0       // 샘플링

mul r0, r0, v0         // 색상 = 텍스처 * Diffuse

mul r0, r0, c31.x      // 색상 *= 4

mov oC0, r0            // 출력

 

1.X 버전에서는 수정자(modifier) modifier가 있어서 명령문을 줄일 수 있습니다. 앞의 mul_x4 mul 연산과 이 연산의 결과에 4배를 지시하는 것입니다. 나누기는 _d2, _d4, _d8 등이 있지만 사용을 잘 안 합니다. 또한 결과의 범위를 [0, 1]로 한정하는 _sat(Saturation)도 있습니다. 그런데 아쉽게도 2.0부터 이들 수정자는 사용할 수 없고 상수를 설정해서 직접 계산해야 합니다.

 

<멀티 텍스처 MODULATE4X: s1p_02_1tex2.zip>

 

고정 기능 파이프라인은 색상 처리 방식이 몇 가지로 한정되어 있지만 픽셀 쉐이더를 사용하면 처리 방식이 자유롭고 쉽게 코드로 만들 수 있습니다. 예를 들어 다음과 같이 최종 색상을 만든다고 합시다.

 

최종 색상 = 외부 색상 * Diffuse * 텍스처 색상 + 베이스 색상

 

이것을 고정 기능 파이프라인에서 구현하려면 3개 이상의 다중 처리(Multi-Texturing)을 거쳐야 합니다. 하지만 이것을 쉐이더 코드로 작성하면 다음과 같이 간단하게 만들 수 있습니다.

 

ps_1_1

 

def c0, 0.2, 0.2, 0.2, 0      // 베이스 색상

 

tex t0                 // 0 Stage의 텍스처를 t0에 샘플링

 

mul r0, c1, v0         // 외부 상수 값과 Diffuse 곱셈

mul r1, r0, t0         // r1 = Diffuse * c1 * texture 색상

add r0, r1, c0         // 출력= r1 + c0

 

또는

 

ps_2_0

 

def c0, 0.2, 0.2, 0.2, 0

 

dcl     v0

dcl_2d  s0             // 2D 샘플러

dcl     t0.xy          // 0 Stage의 텍스처 좌표

 

texld r0, t0, s0       // 샘플링

 

mul r1, c1, v0         // r1 = 외부 상수 값* Diffuse

mul r1, r1, r0         // r1 = Diffuse * c1 * texture 색상

add r0, r1, c0         // 색상 = r1 + c0

mov oC0, r0            // 출력

 

<다중 텍스처 처리(Multi-Texturing): s1p_02_1tex3.zip>

 

두 개의 텍스처의 혼합도 쉐이더를 사용하면 무척 편리합니다. 입력된 두 텍스처의 색상을 더하고 Diffuse와 곱해서 최종 색상을 출력하도록 쉐이더를 작성하면 다음과 같이 1.1에서 총 5줄이면 충분합니다.

 

ps_1_1

tex t0                  // 0번 째 Stage 텍스처 샘플링

tex t1                  // 1번 째 Stage 텍스처 샘플링

add r0, t0, t1         // 두 텍스처 색상을 더함

mul r0, r0, v0         // 더한 색상에 Diffuse를 곱하고 출력 레지스터 r0에 복사

 

만약 1 Stage에 대한 텍스처 좌표를 0 Stage의 좌표를 사용하려면 다중 처리 상태를 다음과 같이 설정합니다.

 

m_pDev->SetTextureStageState(1, D3DTSS_TEXCOORDINDEX, 0);

 

1.4 버전은 다음과 같이 texld를 사용하고 텍스처 좌표를 지정할 수 있어서 같은 텍스처 좌표를 사용할 때 1.1 버전처럼 디바이스의 다중 텍스처 처리 상태를 설정할 필요가 없습니다.

 

ps_1_4

texld r0, t0           // 0 번째 텍스처 좌표에 해당하는 0번 째 텍스처 샘플링

texld r1, t0           // 0 번째 텍스처 좌표에 해당하는 1번 째 텍스처 샘플링

add r2, r0, r1         // 두 텍스처 색상을 더함

mul r0, r2, v0         // 더한 색상에 Diffuse를 곱하고 출력 레지스터 r0에 복사

 

2.0 Diffuse, 샘플러, 텍스처 좌표를 전부 선언해야만 사용할 수 있기 때문에 이 부분에 대해서만 코드가 길어질 뿐입니다.

 

ps_2_0

dcl     v0

dcl_2d  s0             // 2D 샘플러 0

dcl_2d  s1             // 2D 샘플러 1

dcl     t0.xy          // Texture Coordinate at stage 0

texld   r0, t0, s0     // 0번 텍스처 좌표에 해당하는 0번 텍스처 샘플링

texld   r1, t0, s1     // 0번 텍스처 좌표에 해당하는 1번 텍스처 샘플링

add     r2, r0, r1     // 두 텍스처 색상을 더함

mul     r0, r2, v0     // 더한 색상에 Diffuse를 곱함

mov     oC0, r0        // 출력

 

<Multi-Texturing: s1p_02_2tex_multi.zip>

 

 

3.3 단색 화면(Monotone Effect)

만약 쉐이더를 사용하지 않고 출력 색상을 단색으로 만들고자 한다면 여러분은 디바이스의 후면 버퍼를 구성하는 색상버퍼에서 픽셀을 가져와 단색으로 만들어야 합니다.

 

IDirect3DSurface9*     pSrc;

hr = m_pd3dDevice->GetBackBuffer(0, 0, D3DBACKBUFFER_TYPE_MONO, &pSrc);

hr = pSrc->LockRect(&rc, NULL, 0);

DWORD*  pColor = (DWORD*)rc.pBits;

 

for(int i=0; i<dsc.Width* dsc.Height; ++i)

{

        D3DXCOLOR      color = pColor[i];

        FLOAT   d = color.r * 0.299f + color.g * 0.587f + color.b * 0.114f;

        pColor[i] = D3DXCOLOR(d,d,d,1);

}

 

<화면 전체에 대한 단색화: s1p_03_mono0_fixed.zip>

 

전체 화면을 단색으로 멋지게 처리했지만 가장 큰 문제는 프레임 속도 입니다. 이 방식의 문제는 어떤 하드웨어 가속도 지원이 되지 않는 방식이라서 렌더링 속도가 빨라야 ~10 FPS 정도여서 게임에서 사용하기는 부적합니다.

 

쉐이더를 사용하면 렌더링 속도의 저하 없이 화면 전체를 단색으로 만들 수 있습니다. 방식은 다음 그림처럼 3D 장면을 텍스처에 저장하고 이 텍스처를 화면 영역과 동일한 4개의 정점에 매핑 한 다음 다시 디바이스에 출력하는 것입니다.

 

<화면 단색화 방법>

 

픽셀을 하나의 색상으로 만드는 단색화 방법은 이전에 살펴 보았고 이것을 다음과 같은 공식으로 표현할 수 있음을 우리는 알고 있습니다.

 

단색화 색상 = dot( (R, G, B), (0.299, 0.587, 0.114) )

 

이것을 텍스처에 적용하면 다음과 같이 작성 할 수 있습니다.

 

ps_2_0

def c0, 0.299, 0.587, 0.114, 0

dcl     v0

dcl_2d  s0             // 2D 샘플러 0

dcl     t0.xy          // 텍스처 좌표

texld   r0, t0, s0     // 샘플링 0

dp3     r0, r0, c0     // 내적으로 단색 색상 생성

mov     oC0, r0        // 출력

 

 

<텍스처 단색화 출력: s1p_03_mono1.zip, s1p_03_mono2.zip>

 

다음으로 화면 전체를 실시간으로 만든 텍스처에 렌더링 해야 하는데 이것은 서피스 효과 강좌에서 만들었던 IrenderTarget 객체를 이용하겠습니다. IrenderTarget 사용은 다음과 같습니다.

 

IrenderTarget* m_pTrnd;

 

CMain::Init()

// 렌더 타깃 용 텍스처 생성

if(FAILED(LcD3D_CreateRenderTarget(…)))

        return -1;

 

 

CMain::FrameMove()

// 렌더 타깃에 장면 그리기

m_pTrnd->BeginScene();

        this->RenderScene();

m_pTrnd->EndScene();

 

CMain::Render()

// 쉐이더 실행, 전체 화면에 다시 그리기

LPDIRECT3DTEXTURE9     pTx = (LPDIRECT3DTEXTURE9)m_pTrnd->GetTexture();

m_pShader->SetSceneTexture(pTx);

SAFE_RENDER(   m_pShader      );

 

색상을 곱하면 어두워지므로 곱셈의 단색화 색상에 대한 색상 비중 값을 좀 더 밝게 올리고 외부에서 색상을 조정할 수 있도록 쉐이더를 작성하면 다음과 같습니다.

 

ps_1_1

def c0, 0.8, 0.9, 0.4, 0      // 색상 비중 값

tex t0                 // 0 번 스테지 텍스처 샘플링

dp3 r0, t0, c0         // 내적으로 간단히 단색 만듦

mul r0, r0, c1         // 외부 상수와 곱해서 최종 색상 출력

 

또는

 

ps_2_0

def c0, 0.8, 0.9, 0.4, 0

dcl     v0

dcl_2d  s0             // 2D 샘플러 0

dcl     t0.xy          // 텍스처 좌표 0

 

texld   r0, t0, s0     // 샘플링

 

dp3 r0, r0, c0         // 내적으로 단색 색상 생성

mul r0, r0, c1         // 외부 상수와 곱해서 최종 색상 출력

mov oC0, r0            // 출력

 

 

<단색화 화면: s1p_03_mono3_shader.zip>

 

 

3.4 Blur 효과

흐림(Blur) 효과는 단색 효과와 마찬가지로 픽셀 쉐이더를 사용하는 대표적인 예 입니다. 흐림 효과는 Gaussian blur를 많이 사용하지만 여기서는 단순하게 픽셀에 인접한 좌, , , 하 픽셀들과 자기 자신을 더한 값의 평균을 최종 색상으로 정하는 방식을 구현해 보겠습니다.

샘플링은 자신을 포함해서 좌, , , 하 총 5번 진행되기 때문에 좌표 또한 정점의 구조체는 총 5개의 텍스처 좌표를 가지고 있어야 합니다.

 

struct VtxDUV1

{

        VEC3    p;

        DWORD   d;

        FLOAT   u0,v0;

        FLOAT   u1,v1;

        FLOAT   u2,v2;

        FLOAT   u3,v3;

        FLOAT   u4,v4;

};

 

이 구조체에 각각의 uv 좌표를 설정한 다음과 같이 쉐이더 코드를 작성합니다.

 

ps_1_4

def c0, 0, 0, 0, 0.2

texld r0, t0

texld r1, t1

texld r2, t2

texld r3, t3

texld r4, t4

add r5, r0, r1

add r5, r5, r2

add r5, r5, r3

add r5, r5, r4

mul r1, r5, c0.wwww

mov r0, r1

 

렌더링에서 같은 텍스처를 여러 Stage에 연결합니다.

 

for(i=0; i<5; ++i)

        m_pDev->SetTexture(i, m_pTx);

 

이렇게 설정한 다음 렌더링 하면 흐림 효과를 만들어 낼 수 있으며 전체 코드는 s1p_04_blur1.zip를 참고 하기 바랍니다.

 

픽셀 쉐이더 2.0이상을 사용하면 텍스처 좌표를 변화해 가며 샘플링 할 수 있습니다. 다음의 쉐이더 코드에서 상수 값은 3.5f/800.f, 3.5f/256.f 값으로 바로 인접한 픽셀이 아닌 3.5만큼 떨어져 있는 픽셀을 샘플링 하기 위한 값입니다.

 

ps_2_0

def c0, 0.0,-0.004375f, 0.0, 0.2

def c1, 0.0, 0.004375f, 0.0, 0.0

def c2, -0.005833f, 0.0, 0.0, 0.0

def c3, -0.005833f, 0.0, 0.0, 0.0

 

dcl_2d  s0                     // 2D 샘플러 0

dcl     t0.xyzw                // 텍스처 좌표 0

add   r0, c0, t0              // 텍스처 좌표를 왼쪽(0, -3.5)으로 이동

texld r0, r0, s0              // Sampling texcoord (0, -3.5)

add   r1, c1, t0

texld r1, r1, s0              // Sampling texcoord (0, +3.5)

add   r2, c2, t0

texld r2, r2, s0              // Sampling texcoord (-3.5, 0)

add   r3, c3, t0

texld r3, r3, s0              // Sampling texcoord (+3.5, 0)

texld r4, t0, s0              // Sampling texcoord (0, 0)

add r0, r0, r1

add r0, r0, r2

add r0, r0, r3

add r0, r0, r4

mul r0, r0, c0.wwww

mov oC0, r0                   // 출력

 

쉐이더 코드가 조금 길어졌는데 흐림 효과는 저 수준으로 작성하는 것보다 HLSL을 사용하는 것이 편리합니다. 저 수준은 "이렇게 하는 방법도 있구나" 하는 정도로만 기억하기 바랍니다.

 

<흐림 효과: s1p_04_blur1.zip, s1p_04_blur2.zip>

 

 

3.5 Shader Effect

앞서 정점 쉐이더를 Wrapping 하기 위해서 ILcShader를 만들었습니다. 이 클래스를 픽셀 쉐이더에서도 사용할 수 있도록 수정해 봅시다. 클래스의 인터페이스 모양은 변하지 않고, 생성 함수에서 정점 쉐이더와 픽셀 쉐이더를 구분할 수 있도록 인수를 추가합니다.

 

// sCmd: Vertex Shader: "vs", Pixel Shader: "ps"

INT LcDev_CreateShaderFromFile(char* sCmd

               , ILcShader** pData, void* pDevice, char* sFile);

 

INT LcDev_CreateShaderFromString(char* sCmd

               , ILcShader** pData, void* pDevice, char* sString, INT iLen);

 

CreateShaderFromFile() 함수에서 정점 쉐이더 또는 픽셀 쉐이더 타입을 결정하고 ILcShader 객체를 생성합니다.

 

INT LcDev_CreateShaderFromFile(char* sCmd, ILcShader** pData, void* pDevice, char* sFile)

        if( 0 == _stricmp(sCmd, "vs"))

               p->SetShaderType(CLcShader::ELC_VS);

        else if( 0 == _stricmp(sCmd, "ps"))

               p->SetShaderType(CLcShader::ELC_PS);

        else{   delete p; return -1;   }

 

        if(FAILED(p->Create(pDevice, sFile)))

        {

               delete p; return -1;

        }

 

        *pData = p;

        return 0;

}

 

함수의 구현에서는 sCmd 값을 가지고 정점 쉐이더와 픽셀 쉐이더를 구분해서 객체를 생성하는 CLsShader::Create() 함수를 정점 쉐이더 또는 픽셀 쉐이더 객체를 생성할 수 있도록 다음과 같이 변경합니다.

 

INT CLcShader::Create(void* p1, void* p2, void* p3, void* p4)

        // Vertex Shader

        if(ELC_VS == m_nShader)

                hr = m_pDev->CreateVertexShader( (DWORD*)pShd->GetBufferPointer()

                        , &m_pVs);

 

        // Pixel Shader

        else if(ELC_PS == m_nShader)

                hr = m_pDev->CreatePixelShader( (DWORD*)pShd->GetBufferPointer()

                        , &m_pPs);

 

쉐이더 상수를 설정하는 SetMatrix(), SetVector(), SetColor(), SetFloat() 함수도 정점 쉐이더와 픽셀 쉐이더 둘 다 지원할 수 있도록 수정합니다.

 

INT CLcShader::SetVector(INT uReg, const D3DXVECTOR4* v)

{

        if(ELC_VS == m_nShader)

               return m_pDev->SetVertexShaderConstantF( uReg , (FLOAT*)v , 1);

        return m_pDev->SetPixelShaderConstantF( uReg , (FLOAT*)v , 1);

}

 

INT CLcShader::SetColor(INT uReg, const D3DXCOLOR* v)

{

        if(ELC_VS == m_nShader)

               return m_pDev->SetVertexShaderConstantF( uReg , (FLOAT*)v , 1);

        return m_pDev->SetPixelShaderConstantF( uReg , (FLOAT*)v , 1);

}

 

INT CLcShader::SetFloat(INT uReg, const FLOAT* v)

{

        if(ELC_VS == m_nShader)

               return m_pDev->SetVertexShaderConstantF( uReg , (FLOAT*)v , 1);

        return m_pDev->SetPixelShaderConstantF( uReg , (FLOAT*)v , 1);

}

 

Begin()/End() 함수도 같은 방식으로 정점 쉐이더, 픽셀 쉐이더 지원이 되도록 수정합니다. 전체 코드는 다음 예제를 참고하기 바랍니다.

 

s1p_05_IShaderEffect.zip

 

 

3.6 Shader Effect

앞서 픽셀 쉐이더 2.0 이상은 샘플러, 텍스처 좌표 등이 분리되어 있어서 쉐이더 코드에서 같은 텍스처 좌표에 편차(Deviation)를 주어 흐림 효과 등을 만들 수 있다고 했습니다. 흐림 효과 등 인접 픽셀을 처리할 때 미리 계산된 값들을 주변 픽셀에 가중치(또는 비중: Weight)을 주어 이 가중치에 각 픽셀을 곱하고 곱해진 픽셀들을 다시 더해서 최종 색상을 결정합니다. 인접한 픽셀에 가중치를 설정하는 것을 픽셀 마스킹이라 합니다. 이 마스킹 값에 따라 흐림 효과 같은 적분 형태가 될 수 있고, 색의 변화 부분에서 날카롭게 만드는 미분 형태도 존재합니다. 특히 미분 형태 중에서 장면의 외곽선 추출은 게임 프로그램에서 자주 응용되는 기술입니다.

 

이전에 구현했던 흐림 효과에 대해서 마스킹 테이블을 구성하면 다음과 같습니다.

<흐림 효과 마스킹 테이블>

 

때로는 주변 픽셀의 가중 값을 음수(-) 값으로 설정 할 수 있습니다. 결과적으로 픽셀 사이의 뺄셈이 수행되는데 이러한 형태는 미분과 흡사하며 외곽선 등 픽셀의 변화가 큰 부분을 표현할 때 사용됩니다.

 

<외곽선 마스킹 테이블>

 

이 외곽선 마스킹 테이블을 가지고 야간 투시경(Night Scope)을 구현해 봅시다. 가장 먼저 구현해야 할 것은 화면 전체 장면을 픽셀에 저장하는 것입니다. 이것은 이전의 단색화 과정에서 만들어 보았으므로 생략하겠습니다.

다음으로 다음과 같은 스코프 이미지가 필요합니다.

 

<Scope 이미지>

 

화면 전체를 저장한 픽셀은 마스킹 테이블의 값에 따라 총 9번 텍스처 좌표를 변화해 가면서 샘플링 하고 가중치를 곱한 이 값들을 더해서 색상을 만듭니다. 그리고 최종 출력 픽셀을 스코프 이미지와 곱셈으로 결정하면 야간 투시경이 만들어 집니다.

이를 쉐이더 코드로 작성하면 다음과 같습니다.

 

ps_2_0

def c10, 0.0, 1.0, 0.8, 0     // Min, Max, 전체 밝기

def c11, 0.7, 0.9, 0.3, 0     // 색상 비중 값

 

dcl     t0             // t0 텍스처 좌표 선언

dcl_2d  s0             // 0-stage 샘플러 객체 선언

dcl_2d  s1             // 1-stage 샘플러 객체 선언

 

// Circle Image

mov r0, t0              // 샘플링 1-stage with 0 stage texture coordinate

texld r0, r0, s1

 

// Render Target Image Sampling

mov r1, t0

add r1, r1, c0

texld r1, r1, s0

 

mov r2, t0

add r2, r2, c0

texld r2, r2, s0

// Multiple Masking Value

mul r1, r1, c20.x

mul r2, r2, c20.y

// Addl all Pixel

mov r10, r1

add r10, r10, r2

add r10, r10, r3

add r10, r10, r4

max r10, r10, c10.r    // (-)제거

 

dp3 r10, r10, c11       // 내적으로 간단히 단색 만듦

mul r10, r10, c16       // 외부 상수와 곱해서 최종 색상 출력

mul r10, r0, r10        // Multiple r0, r10

mul r10, r10, c10.zzz   // 전체 밝기 조정

mov oC0, r10

 

이 쉐이더 코드는 s1p_05_scope.zip "data/shader.psh"에 구현 되어 있고 실행하면 다음과 같은 화면을 볼 수 있습니다.

 

<저 수준 쉐이더 응용 - Night Scope: s1p_05_scope.zip>

 

저 수준 작성이 여러모로 힘이 많이 드니까 여러분은 HLSL을 이용하기 바랍니다.



Copyright (c) 2004 3dapi.com All rights reserved.

Creative Commons License