home lecture link bbs blame

◈HLSL 기초◈

HLSL(High Level Shading Language)은 단어 그대로 쉐이더에 대한 고 수준 언어입니다. 어셈블리 명령어 형식으로 작성한 정점 쉐이더와 픽셀 쉐이더 코드는 사용자에게 언어의 사용, 이해와 더불어 가독성과 융통성에 많은 부담을 줍니다. HLSL은 저 수준 언어인 쉐이더 명령어를 C나 파스칼과 같이 고 수준의 언어로 대신하게 되어 저 수준 언어의 어려운 부분들을 해소시켜줍니다.

고수준 언어로 작성된 쉐이더 프로그램은 컴파일 과정에서 필요한 명령어들을 컴파일러가 자동으로 추가합니다. 마이크로소프트에서 HLSL으로 작성한 고 수준 언어를 컴파일해서 만든 명령어와 저 수준 쉐이더 명령어 만으로 작성한 명령어들을 비교한 자료를 발표한 적이 있는데 이 발표에 의하면 HLSL을 사용하더라도 둘의 차이가 크지 않음을 보인다고 합니다. 따라서 HLSL을 사용한 것과 직접 쉐이더 명령어를 사용한 결과는 차이가 없다라고 볼 수 있으므로 사용자는 HLSL을 이용하는 것이 가독성, 편리성 등 여러모로 유리한 점이 많이 있습니다.

 

<저 수준으로 작성한 정점 쉐이더>

vs_1_1

dcl_position   v0

dcl_color      v1

m4x4 oPos, v0, c0

mov  oD0, v1

 

<HLSL로 작성한 정점 쉐이더>

float          m_Bright;

float4x4       m_mtWVP;

struct SvsOut

{

        float4 Pos : POSITION;

        float4 Dif : COLOR0;

};

SvsOut VtxPrc(float3 Pos : POSITION, float4 Dif : COLOR0 )

{

        SvsOut Out = (SvsOut)0;

        Out.Pos = mul(float4(Pos, 1), m_mtWVP);

        Out.Dif = Dif * m_Bright;

        return Out;

}

 

두 코드를 비교해 보면 어셈블리 형식의 저 수준으로 작성한 쉐이더 코드가 훨씬 간결한 것을 알 수 있습니다. 하지만 HLSL로 작성한 것이 가독성이 빠르고 사용이 편리해 보입니다. 여러분이 저 수준 형식을 사용한다면 연산이 많아질수록 오히려 손이 더 가는 형태가 되어 불편합니다. 예를 들어 상수 설정이 많아지면 c0, c1 이러한 이름을 사용하는 것보다 “m_mtWVP”와 같이 사용자가 직관적으로 정의한 변수를 사용하는 것이 좋고, 또한 함수도 만들어서 사용 할 수 있어서 저수준 형식보다 HLSL이 장점이 많습니다. 그리고 이후에 알게 되겠지만 저 수준으로 작성하는 것이 꼭 코드의 길이가 짧아지는 것은 아니라는 것을 알게 될 것입니다.

 

여러분이 HLSL을 빨리 익히는 방법은 이전 강의에서 저 수준 언어로 작성된 코드들를 HLSL로 바꾸어 보는 것이 가장 무난합니다. 그렇다고 반드시 저 수준 언어를 알아야 할 필요는 없습니다. 보통 HLSL 100 Line 정도에서 작성되므로 만약 여러분이 C언어에 익숙하다면 HLSL에 대한 문법과 함수들을 연습하면 몇일 이내로 배울 수 있습니다. HLSL 함수 또한 걱정할 것 없는 것이 생각만큼 많은 숫자도 아니고 대부분 수학과 관련된 함수들이고 직관적으로 이해가 되기 때문에 HLSL로 장면에 대한 간단한 표현부터 연습하면 몇 일 이내로 쉽게 배울 수 있습니다.

 

 

1. HLSL 연습

1.1 간단한 HLSL

HLSL은 저 수준 작성과 마찬가지로 문자열 또는 파일로 작성할 수 있습니다. 예를 들어 다음과 같이 정점의 위치를 출력하는 쉐이더 코드를 저 수준과 HLSL로 작성해 봅시다.

 

<저 수준>

"vs_1_1                \n"

"dcl_position v0       \n"

"mov oPos, v0          \n"

 

<HLSL>

"float4 VtxPrc(float3 Pos : POSITION) : POSITION \n"

"{                            \n"

"    return float4(Pos, 1);   \n"

"}                            \n"

 

저 수준 언어를 기계어로 번역하는 것을 Assemble이라 하고 고 수준 언어는 Compile이라 합니다. 저 수준으로 작성한 쉐이더는 D3DXAssembleShader() 함수를 사용했습니다. 이와 대응되는 HLSL 함수는 D3DXCompileShader() 함수 입니다. HLSL 문법으로 작성한 쉐이더 코드는 다음과 같이 컴파일합니다.

 

LPD3DXBUFFER   pShd    = NULL;

LPD3DXBUFFER   pErr    = NULL;

hr = D3DXCompileShader(sHlsl, iLen

               , NULL, NULL

               , "VtxPrc"     // 시작 함수

               , "vs_1_1"     // 쉐이더 버전

               , dwFlags

               , &pShd         // 컴파일 쉐이더

               , &pErr         // Error

               , &m_pTbl      // 상수 테이블

               );

 

C언어로 작성한 응용프로그램은 int main() 함수가 필요합니다. HLSL은 사용자 함수를 지원하기 때문에 D3DXCompileShader() 함수의 4 번째 문자열 인수에 쉐이더 실행의 시작 함수를 지정해야 합니다. 또한 정점 쉐이더 또는 픽셀 쉐이더 버전을 설정해야 하며, 상수 레지스터에 데이터 연결을 담당하는 상수 테이블을 생성할 수 있도록 상수 테이블 주소를 지정합니다. 파일에서 작성한 HLSL D3DXCompileShaderFromFile()함수를 사용 합니다.

D3DXCompileShader() 함수는 단지 HLSL로 작성한 쉐이더 코드를 컴파일하거나 실패에 대한 문법 에러를 검사하는 기능만 수행하기 때문에 컴파일 한 결과를 메모리에 적재하기 위해서 정점 쉐이더 또는 픽셀 쉐이더 객체를 생성합니다. 이 부분은 저 수준과 동일합니다.

 

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

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

 

HLSL은 쉐이더 작성을 고급 언어로만 하겠다는 뜻이므로 저 수준과 마찬가지로 렌더링 파이프라인에 적용되는 정점 스트림에 대한 정점 선언자(Vertex Declarator)를 만들어야 합니다.

 

D3DVERTEXELEMENT9 vertex_decl[MAX_FVF_DECL_SIZE]={0};

D3DXDeclaratorFromFVF(Vtx::FVF, vertex_decl);

m_pDev->CreateVertexDeclaration( vertex_decl, &m_pFVF );

 

렌더링에서 프로그램 가능한 파이프라인을 사용하는 방법은 저 수준과 거의 동일하며 쉐이더의 전역 변수 설정은 컴파일할 때 생성한 상수 테이블 객체를 이용합니다.

 

1. 정점 쉐이더 또는 픽셀 쉐이더 사용을 명시함으로써 프로그램 가능한 파이프라인 사용을 알린다.

2. 정점 선언자를 디바이스에 연결한다.

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

4. 상수 테이블을 사용해서 쉐이더의 전역 변수 값을 설정한다.

5. 정점을 그린다.

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

 

m_pDev->SetVertexShader(…);

m_pDev->SetPixelShader(…);

m_pDev->SetVertexDeclaration(…);

 

m_pDev->DrawPrimitiveUP(…);

 

m_pDev->SetVertexDeclaration(NULL);

 

m_pDev->SetVertexShader(NULL);

m_pDev->SetPixelShader(NULL);

 

<간단한 HLSL: hs1_basic1.zip>

 

 

1.2 HLSL 기초

Assembly 형식의 저 수준 쉐이더 언어는 사용법은 약간 까다롭지만 문법이 간단해서 프로그래머가 기억해야할 내용이 많지 않았습니다. 하지만 HLSL C언어와 같은 고급 언어로 구성되어 있어서 문법에 대한 지식이 필요합니다.

 

 

1.2.1 자료 형(Data Type)

저 수준 쉐이더에서 모든 데이터는 저장 장소 레지스터 이름을 그대로 사용해서 자료형이라는 것이 필요하지 않았지만 HLSL은 프로그래머가 사용하는 변수 또는 상수를 컴파일러가 적절히 번역할 수 있도록 자료형을 사용 하며 HLSL의 기본 자료 형(Data Type)과 복합 형(Complex Type)으로 구분합니다.

 

기본 자료 형은 크게 Scalar Type(), Vector , Matrix 형으로 나눕니다.

Scalar Type는 단일 자료로 형태로 true/false만 가지는 bool , 32bit int , 16비트 부동 소수점 half, 32비트 float , 64비트 부동소수점 double 형이 있습니다. 이중에서 가장 많이 사용되는 타입은 float 형과 int 형입니다. 때로는 GPU의 특성에 따라 half, double은 지원이 안될 수 있으므로 타입 결정이 어려우면 float 형을 사용하는 것이 가장 무난합니다.

Scalar 형을 사용하는 방법은 다음과 같습니다.

 

선언: float f;

선언과 동시에 초기화: float f = 3.1f;

 

문법이 C언어와 같습니다. Scalar형은 배열을 만들어 사용할 수 있습니다. 다음은 float형 배열을 만들고 초기화 방법과 데이터 접근에 대한 예입니다.

 

배열: float f[3];

배열 선언과 동시에 초기화: float f[3] = {1.f, 2.f, 3.f};

배열 접근: f[2] = 10.f;

 

Scalar Type의 연산은 C언어의 산술 연산 +, -, *, /, {+|-|*|/}= 등이 가능하고 정수 형의 경우 단항 연산 ++, --을 사용할 수 있습니다.

 

Vector Type은 자료를 표현하기 위해서 2개 이상의 자료가 결합된 형태로 가장 기본적인 형태는 vector이며 vector float 4개가 하나의 자료인 float4 형입니다. vector 형은 자료를 접근하는 방법이 x, y, z, w 또는 r, g, b, a를 이용하거나 임의 접근 연산자 []를 사용합니다.

만약 vector의 전체 성분을 교체할 때는 vector 생성자를 사용합니다.

 

vector 선언: vector v;

vector 선언과 동시에 초기화: vector v  = {1,1,0,1};

vector 접근1: v.y = 3.0f; or v.g = 5.0f;

vector 접근2: v[2]=1.0f;

vector 생성자: v= vector (1, 0, 1, 1);

 

Vector TypeScalar 형에 숫자를 붙여 차원을 붙여서 사용하기도 합니다. 예를 들어 float 형에 대해서 float2, float3, float4 형이 있고, int 형에 대해서 int2, int3, int4 형이 있습니다. 이들은 vector<type, dimension>형태와 동등합니다.

 

float2 vector<float, 2>

float3 vector<float, 3>

float4 vector<float, 4>

int3 vector<int, 3>

int4 vector<int, 4>

 

선언과 동시에 초기화: vector<double, 4> v = {1., 2., 3., 4.};

성분 접근1: v[2]= 3.4;

 

이들 숫자가 붙은 자료형도 Vector Type 이므로 vector와 마찬가지로 vector 생성자를 이용하거나 자신의 Type에 대한 생성자를 사용합니다.

 

v = vector<float, 4>(1, 1, 0, 1);

v = float4(1, 1, 0, 1);

 

주의할 것은 Vector Type Scalar 변수 또는 상수로 설정하면 변수의 모든 값은 Scalar 값으로 설정이 됩니다.

 

float k =0.3f;

float4  v;

v  = k;        // v.x = v.y = v.z = v.w = k

v  = 1. 0f;    // v.x = v.y = v.z = v.w = 1.0f

 

Vector Typeswizzling 가능합니다. , xyzw rgba의 혼용은 허용이 안됩니다. 다음은 Vector Type에서 swizzling이 안되는 예입니다.

 

v.xg, v.yr

 

기본 자료 형인 Matrix Type는 행렬을 표현한 자료 형입니다. 행렬 자료 형은 matrix 키워드를 사용해서 행과 열의 수로 표현합니다.

 

matrix <type, row, column>: type: int, float

3x3 float 형 행렬: matrix<float, 3, 3>

 

vector와 마찬가지로 숫자로 행과 열을 표현하기도 합니다.

 

int{row}x{column}: int2x2, int3x3, int4x4, int4x3

float{row}x{column}: float2x2, float3x3, float4x4, float4x3

 

행렬의 성분 접근에 대해서 행 우선 지정은 "row_major" 키워드를 사용하고 열 우선 지정의 경우는 "col_major" 키워드를 사용합니다. 이들은 GPU 내부에서 처리되는 방식을 결정하는 것이기 때문에 연산의 결과에는 영향을 주지 않습니다. 만약 이들 지정이 없으면 default로 행 우선 순위 방식으로 행렬에 대해서 연산이 진행이 되고 행렬과 벡터의 연산에서 벡터를 좌측에 놓는 벡터 * 행렬 형태를 취하기 때문에 HLSL의 행렬형은 "col_major"가 됩니다. 또한 저 수준 언어와 다르게 미리 행렬 변수 값을 Transpose를 할 필요가 없습니다.

행렬의 성분 접근은 행과 열의 인덱스를 이용한 방법 "_m+인덱스 숫자"를 이용한 방법, [] 연산자를 이용한 방법 등 3가지가 있습니다.

 

._m{row}{col}  → ._m00 ~ ._m33

._{row}{col}   → ._11 ~ . _44

.[{row}][{col}] → mtTM[0][0] ~ mtTM[3][3]

 

행렬은 C언어와 유사하게 1차원 배열 또는 2차원 배열을 이용하거나 성분 접근으로 초기화 합니다.

 

float2x2 v = {1,2,3,4};       → v._11=1, v._12=2, v._21=3, v._22 = 4;

float2x2 v = float2x2({1,2}, {3,4});

 

행렬도 swizzling이 가능하며 _m{row}{col} _{row}{col}을 혼용하지 않으면 됩니다. 예를 들어 v._m00_11 과 같은 swizzling은 허용이 안됩니다.

 

복합 형(Complex Type)은 구조체(Struct), 사용자 정의(User Define), 텍스처 객체, 샘플러 객체), Shader 객체 등이 있습니다.

구조체 형은 C언어의 구조체(struct)와 유사하며 데이터의 입력(레지스터) 또는 출력(레지스터)를 지정할 때 사용됩니다.

 

struct Svertex

{

    float3 position;

    float3 normal;

};

 

struct 형 변수의 초기화는 전체를 0으로 초기할 때는 상수 "0"을 해당 구조체의 타입으로 캐스팅합니다. 개별적인 데이터 초기화는 {}를 사용합니다. 성분 접근은 "." 연산자를 사용합니다.

 

초기화: struct Svertex v={ {1, 2, 3}, {4, 5, 6}};

0으로 초기화: Svertex v = (Svertex)0;

성분 접근 - 갱신: v.normal = float3(1, 0, 1);

성분 접근 - 복사: float3 t = v.position

 

사용자 정의는 C언어와 동일하게 typedef 키워드를 사용합니다.

 

typdef vector<float, 3> point;

point v;

 

텍스처 객체는 texture 키워드 사용하며 다음은 가장 기본적인 텍스처 객체 생성입니다.

 

texture tex0;

 

때로는 쉐이더 프로그램에는 전혀 영향을 주지 않고 응용 프로그램에서 텍스처 객체의 이름 등을 참고하고자 할 때 "<>"를 사용해서 주해(Annotation)를 지정합니다.

 

texture tex0 <string name ="MyTexture.bmp">;

 

샘플러 객체는 텍스처에서 픽셀을 가져오는 샘플링을 담당하는 객체입니다. 샘플러 타입은 1차원 텍스처에 대한 sampler1D, 2차원 텍스처에 대한 sampler2D, 3차원 텍스처에 대한 sampler3D, 입방체(Cube) 텍스처에 대한 samplerCUBE 등이 있습니다.

단순한 형태의 샘플러 타입 객체를 생성은 sampler 키워드를 사용합니다.

 

sampler SampDif;

 

샘플링에서 필터링(Filtering), 어드레싱(Addressing)은 항상 같이 설정해야합니다. 샘플러 타입의 객체는 sampler state를 사용해서 이들을 지정할 수 있습니다.

 

sampler3D MySampler = sampler_state

{

        MinFilter = LINEAR;

        MaxFilter = LINEAR;

        MipFilter = LINEAR;

        AddressU = Wrap;

        AddressV = Wrap;

};

 

하나의 샘플러를 가지고 여러 텍스처에 적용하는 것도 좋지만 대부분의 프로그래머들은 각 텍스처마다 샘플러 객체 하나씩 결합하는 형태로 쉐이더를 작성합니다. 다음은 샘플러 객체에 텍스처 객체를 지정하는 예입니다.

 

texture tex0;

sampler SampNor = sampler_state

{

        Texture = <tex0>;

        MinFilter = LINEAR;

        AddressU = Wrap;

        AddressV = Wrap;

};

 

정점 쉐이더, 픽셀 쉐이더를 통합한 ID3DXEffect쉐이더 객체는 저 수준 혹은 고 수준 쉐이더 언어를 전부 사용할 수 있습니다. 따라서 각 경우에 맞게 컴파일 옵션을 설정해야하는데 고 수준 쉐이더 언어의 컴파일을 저 수준 Assemble 컴파일할 때 버전을 지정하거나 C언어의 inline assembly와 같이 HLSL 내부에서 저 수준으로 쉐이더를 작성할 때 사용하는 객체가 쉐이더 객체 입니다.

정점 쉐이더 객체 지정은 "VertexShader vs" 으로 시작하고 픽셀 쉐이더 객체 지정은 "PixelShader ps" 으로 시작합니다.

만약 여러분이 저 수준 언어로 쉐이더 객체를 생성한다면 asm 키워드가 필요합니다.

 

VertexShader vs = asm {"저수준 언어"};

 

고 수준으로 작성된 쉐이더 코드는 함수로 구성되어 있으므로 compile 키워드, 대상 쉐이더 버전, 시작 함수를 지정합니다.

 

VertexShader vs = compile "쉐이더 버전" "시작 함수()";

 

다음은 픽셀 쉐이더 객체에 대한 저 수준, 고 수준 생성에 대한 예입니다.

 

PixelShader ps = asm

{

    ps_2_0

}

 

PixelShader ps = compile ps_2_0 PixelProc();//PixelProc()함수를 ps_2_0으로 컴파일

 

주해(Annotation)ID3DXEffect 객체를 사용할 때 파라미터에 사용자 정보를 추가하는 용도로 사용되며 응용프로그램에서 Lookup에 대한 질의 함수를 통해서 정보를 가져옵니다. HLSL자체에는 영향을 주지 않습니다.

사용 방법은 "<", ">" 안에 데이터 타입, 변수 이름, 등호, 데이터 값 등을 ";" 으로 구분해서 작성합니다.

 

texture t< string name="MyTexture.bmp";>;

 

technique MyTech

    int MyInt = 12;

    string InputArray = "MyLookup";

 

이렇게 HLSL에 작성된 주해는 응용 프로그램에서 질의 함수를 가지고 정보를 가져올 수 있습니다.

 

LPCSTR sName;

hAnnotation = m_pEffect->GetAnnotationByName(hTech, "InputArray");

m_pEffect->GetString( hAnnotation, &sName);

// 주해에서 InputArray 변수의 이름인 MyLookup을 가져옴

 

HLSLstring 타입은 HLSL에 영향을 주지 않고 질의(Query)용도로 응용 프로그램에서 이용할 수 있도록 만든 자료 형입니다.

 

 

1.2.2 기억 장소(Storage Class)

Storage Class는 변수의 수식어로 변수의 수명(Life Time)과 접근에 대한 범위(Scope)를 결정하며 static, extern, uniform, shard등이 있습니다.

정적 변수 staticC언어의 static과 유사하며 전역 변수에 설정이 되면 응용 프로그램에서 접근 불가능하고 HLSL 코드 내에서만 유효 합니다. 또한 지역 변수에 설정이 되면 함수가 종료 되어도 값이 유지가 됩니다. 이것은 저 수준에서 쉐이더 내부에서 상수 레지스터를 설정할 것과 비슷합니다.

변수가 전역에 있고 어떤 수식어도 안 붙이면 외부 변수 extern이 됩니다. extern 변수는 응용프로그램에서 값을 변경할 수 있습니다. 여러분이 변환 행렬, 빛의 방향, 색상 등을 쉐이더에 전달할 경우 변수는 extern이 되어야 합니다.

 

extern 변수는 상수 테이블을 이용해서 값을 변경합니다. 상수 테이블은 HLSL 코드를 컴파일 할 때 사용한 함수 D3DXCompileShader() 의 인수 리스트에서 상수 테이블인 ID3DXConstantTable* 의 주소를 얻을 수 있습니다.

이 상수 테이블은 쉐이더의 기본 데이터 형에 대한 변수 설정 함수 SetVector(), SetMatrix(), SetInt(), SetFloat() 등이 있으며 변수를 변경하기 위해 변수 이름을 사용하는 방법과 변수 이름의 핸들을 이용한 방법 2가지가 있습니다.

 

// HLSL 코드

float4 Dif = float4(1,1,0,1);

 

// 변수 이름을 이용한 방법

D3DXCOLOR      color(1,0,1,1);

m_pTbl->SetVector(m_pDev, "Dif", (D3DXVECTOR4*)&color);

 

핸들을 사용하면 데이터 접근 속도에 이득이 있다고 합니다. 변수의 핸들을 얻는 것은 GetConstantByName() 함수에 이름을 전달합니다.

 

D3DXHANDLE     m_hDif; // Diffuse Constant Handle

// 핸들을 이용한 방법

m_hDif = m_pTbl->GetConstantByName(NULL, "Dif");

m_pTbl->SetVector(m_pDev, m_hDif, (D3DXVECTOR4*)&color);

 

<전역 변수 설정: hs1_basic2_const.zip>

 

정점 쉐이더와 픽셀 쉐이더는 varing uniform, 두 종류의 입력 데이터를 받습니다. 정점 쉐이더에서 varing 데이터는 정점의 위치, 법선, 색상 등과 같이 정점 스트림에서 온 데이터입니다. 이와 대비되는 uniform 데이터는 저 수준으로 비교하면 상수 레지스터에 저장된 데이터라 할 수 있습니다.

만약 여러분이 정점 처리에 대한 함수를 다음과 같이 쉐이더를 작성하면 컴파일할 때 에러가 발생합니다.

 

void VtxPrc( in  float3 iPos: POSITION

        , out float4 oPos: POSITION

        , out float4 oD0 : COLOR

        , float4 Dif   // float4 앞에 uniform을 붙이면 에러 없어짐

)

{

        Dif = float4(0,1,1,1);

        oPos = float4(iPos, 1);

        oD0  = Dif;

}

 

이것은 VtxPrc() 함수가 정점 처리의 void main 함수이기 때문에 입/출력을 명시적으로 지정해야하는데 Dif 변수는 입력(in) 또는 출력(out)으로 지정되지 않았기 때문입니다. 그런데 uniform을 사용해서 "uniform float4 Dif"와 같이 작성하면 쉐이더 컴파일러는 Dif 변수를 상수 레지스터에 저장된 데이터라 여기고 컴파일을 완성합니다.

uniform 사용에서 지역 변수에는 uniform 선언이 안되고, 전역 변수와 함수의 인수에 사용되는데, D3D에서 전역 변수에 대한 uniform 선언은 extern과 거의 같습니다.

참고로 OpenGL 쉐이더 GLSL의 경우 외부에서 쉐이더의 상수 값 설정을 하기위해서 전역 변수에 uniform으로 선언하며 위치, 색상, 법선 등의 입력 값에 대한 변수에는 varing을 이용합니다.

 

<Uniform : hs1_basic3_uniform.zip>

 

 

1.2.3 Semantic

저 수준 쉐이더 작성에서 우리는 레지스터를 직접적으로 다루었습니다. HLSL에서 변수는 Semantic(시맨틱: 의미)을 설정할 수 있는데 Semantic은 변수의 입력과 출력을 확인 또는 지정하거나 데이터의 출처와 역할에 대한 분명한 의미를 부여하기 위해 함수, 변수, 인수 뒤에 선택적으로 붙여서 서술하는 것입니다. 종류는 Vertex Shader Semantic, Pixel Shader Semantic, Explicit Register Binding (명시적 레지스터 바인딩) 등이 있습니다.

Semantic 설정 방법은 전역 변수, 인수, 함수 등의 끝에 콜론(":")을 붙이고 "SEMANTIC 이름"을 붙입니다.

 

float4x4 m_mtWldViw : WORLDVIEW;

float4 pos: POSITION;

 

Semantic은 다음과 같이 함수의 입력 변수 또는 함수의 끝에 붙일 수 있습니다.

 

float4 MyFunc( float3 pos: POSITION) : POSITION

{

        return float4(pos, 1);

}

 

만약 이 함수가 시작 함수로 설정이 되면 입력 변수에 설정된 Semantic은 저 수준의 "dcl" 선언과 같아져 앞의 "pos" 변수는 "dcl_position v0"와 비슷한 의미가 됩니다. 또한 함수의 뒤에 붙은 Semantic은 결과를 oPos에 복사하는 것과 같은 의미가 됩니다.

 

구조체 변수 다음에도 Semantic을 붙일 수 있습니다.

struct T

{

    float3 pos : POSITION;

};

 

<Semantic: hs1_basic4_semantic.zip>

 

이렇게 구조체 변수에 Semantic을 부여하고 함수의 입력 변수의 타입과, 출력의 타입으로 설정하면 프로그램하기가 무척 편리합니다.

저 수준 쉐이더의 "dcl"과 대응되는 입력에 대한 Semantic은 다음과 같습니다.

 

POSITION[n]: 정점의 위치. POSITION, POSITION0, POSITION1, ….

BLENDWEIGHT[n]: 정점 블렌딩 비중 값

BLENDINDICES[n]: 정점 블렌딩 인덱스 값

NORMAL[n]: 정점 법선 벡터

PSIZE[n]: Point Size

COLOR[n]: 정점 Diffuse(COLOR, COLOR0), Specular(COLOR1)

TEXCOORD[n]: 텍스처 좌표

TANGENT[n]: 정점 접선 벡터

BINORMAL[n]: 정점 종법선 벡터

TESSFACTOR[n]: tessellation factor

 

정점 쉐이더의 출력 레지스터 지정하는 저 수준 명령어 oXXX와 대응되는 정점 쉐이더 출력 Semantic은 다음과 같습니다.

 

POSITION: 정점의 출력 위치 → oPos

PSIZE: Point Size oPts

FOG: 정점 포그 값 → oFog

COLOR[0,1]: Diffuse, Specular oD0, oD1

TEXCOORD[0~7]: 텍스처 좌표 → oT0~T7

 

D3D에서 사용자가 만든 데이터는 출력 레지스터가 없기 때문에 이 데이터를 픽셀 쉐이더로 전달하기 위한 경우 TEXCOORD 출력 시멘틱을 가장 많이 사용하며 TEXCOORD0, TEXCOORD1은 대부분 사용하기 때문에 보통 TEXCOORD7부터 TEXCOORD6, TEXCOORD5, … 등을 이용합니다.

 

다음은 정점 쉐이더 Semantic을 구조체에 적용해서 사용한 예 입니다.

 

// 정점 입력에 대한 구조체

struct SvtxIn

{

        float3 Pos : POSITION;

        float3 Nor : NORMAL;

        float4 Dif : COLOR;

        float2 Tex : TEXCOORD;

};

 

// 정점 출력에 대한 구조체

struct SvtxOut

{

        float4 Pos : POSITION;

        float4 Dif : COLOR;

        float2 Tex : TEXCOORD0;

        float4 Nor : TEXCOORD7;       // 법선 벡터는 출력 레지스터가 없으므로 TEXCOORD 이용

};

SvtxOut VtxPrc(SvtxIn pVtx)

{

        SvtxOut Out=(SvtxOut)0;

        // 쉐이더 연산

        return Out;

}

 

<Struct: hs1_basic5_struct.zip>

 

픽셀 쉐이더의 Semantic 문법은 정점 쉐이더 Semantic과 동일하게 데이터의 출처에 대한 식별을 위해 사용되며 시멘틱 위치는 구조체 멤버 뒤, 함수의 인수 뒤, 함수 뒤 등에 붙여서 사용합니다. 이 픽셀 쉐이더 SemanticPixel Shader Input Semantic Pixel Shader Output Semantic 두 종류가 있습니다.

픽셀 쉐이더 입력 Semantic은 픽셀 쉐이더의 입력 레지스터 지정하고 Sampler 객체는 따로 지정해서 사용합니다.

픽셀 쉐이더 입력 Semantic COLOR, TEXCOORD 두 종류가 있습니다.

 

COLOR, COLOR[0, 1]: 정점 처리 과정에서 만들어진 Diffuse(0)Specular(1)

TEXCOORD, TEXCOORD[0~7]: 텍스처 좌표

 

픽셀 쉐이더 출력 Semantic은 픽셀 쉐이더의 출력 레지스터 지정하는 것으로 저 수준 명령어의 oXXXX와 대응됩니다.

COLOR[n] 색상 → oC[n], COLOR =COLOR0

TEXCOORD[n]: → 텍스처 좌표

DEPTH[n]: oDepth[n], DEPTH = DEPTH0

 

Explicit Register Binding은 레지스터 이름을 명시적으로 지정하는 것으로 저 수준과 혼용해 사용하거나 아니면 고정 기능 파이프라인의 렌더링 상태 머신에 설정된 값을 이용하고자 할 때 사용합니다.

명시적 레지스터 바인딩은 다음과 같이 Register 키워드와 저 수준 레지스터 이름을 사용합니다.

 

타입 + 변수 + ":" + register( "저 수준 레지스터 이름");

 

예를 들어 고정기능 파이프라인의 다중 처리(Multi-Texturing)의 샘플러 0번에 대해서 쉐이더 코드를 다음과 같이 작성할 수 있습니다.

 

sampler SmpDif : register(s0);    // Sampler 객체 SmpDif register s0에 바인딩

 

이렇게 되면 디바이스의 SetTexture(0, pTex); 에 바인딩된 pTex 텍스처를 쉐이더에서 사용할 수 있습니다.

 

때로는 상수 레지스터를 설정할 수도 있습니다.

 

float4x4 mtWorld : register(c0);   // 전역 변수 mtWorld를 상수 레지스터 c0에 바인딩

float4   vcLight : register(c10);  // 전역 변수 vcLight를 상수 레지스터 c10에 바인딩

 

상수 테이블을 통해서 전역 변수 값을 설정하지만 이와 같이 레지스터를 명시적으로 바인딩하면 상수 테이블 대신 디바이스의 SetVertexShaderConstantF() 함수로 c0, c10 레지스터 값을 변경할 수 있게 되어 결과적으로 mtWorld, vcLight를 수정한 것과 동일한 효과를 발휘합니다.

 

 

1.2.3 사용자 정의 함수

HLSL의 함수는 C언어의 함수와 거의 같으며 기본적인 형태는 다음과 같습니다.

 

"return type" "Function_Name"({"argument type" "argument name"}) { : Sementic}

{

        // function body

        return …;

}

 

만약 함수가 정점 처리 또는 픽셀 처리의 main 함수 경우에는 함수의 반환 형은 정점 쉐이더/픽셀 쉐이더 Semantic을 따라야 합니다.

 

픽셀 쉐이더 main 함수 예)

float4 PxlPrc(float2 vTex : TEXCOORD0) : COLOR0

{

        return tex2D(DiffuseSampler, vTex);

}

 

또한 함수의 반환 형이 void 형이면 함수의 인수 형 앞에 in/out 키워드를 넣어서 입/출력을 지정할 수 있습니다.

 

정점 쉐이더 main 함수 예)

void VtxPrc( in  float4 vPos : POSITION,     // 입력 레지스터 위치

              in  float2 vTex : TEXCOORD0,    // 입력 레지스터 텍스처 좌표 0

              out float4 oPos : POSITION,     // 출력 레지스터 위치

              out float2 oTex : TEXCOORD0     // 출력 레지스터 텍스처 좌표 0

        )

{

        …

}

 

<함수 사용: hs1_basic6_function.zip>

 

함수의 인수 값 변동이 쉐이더 내부에서 불허할 경우 uniform 키워드 이용합니다.

 

 

1.2.4 HLSL 내장 함수, 기타

여러분이 HLSL 내장 함수를 적절히 사용하려면 쉐이더 버전이 2.0이상으로 컴파일 하는 것이 좋습니다. 대부분 쉐이더의 함수는 정점 처리의 변환과 조명 효과 그리고 픽셀 처리에 관련된 함수로 수학 함수와 샘플링 함수가 90% 이상입니다.

 

Math 함수:

- 삼각 함수: sin, cos, tan, acos, asin, cot

- 지수 함수: exp: 자연대수 e의 승수, exp2(): 2의 승수, pow(x,y)=x^y

- 로그 함수: log, log2, log10

- 벡터 함수: dot(), cross(), distance(), length(), len(), sqrt(), rsqrt(),

        normalize(), reflect(), refract()

- 행렬 함수: determinant(), transpose()

- 산술 함수: abs(), ceil(), floor(), round(), fmod(), lerp(), saturate()

 

샘플링 함수:

- tex{n}D(s,t): n차원 샘플러 s t 좌표에 대한 색상 추출. tex1D, tex2D, texCUBE(s,t)

- tex{n}Dproj(s,t): 정점의 깊이(z,w)를 저장한 텍스처에 대한 n차원 투영 샘플링.

        t= 4D임에 주의 샘플링 전에 t t.w로 나누어짐

- tex{n}Dbias(s,t): 보정된 n차원 텍스처에 대한 Sampliing

 

HLSL Keyword 중에 Technique이 있습니다. 이것은 정점 쉐이더와 픽셀 쉐이더를 통합한 ID3DXEffect 객체를 사용할 때 이용되는 키워드로 렌더링의 단위인 passpass 안에 렌더링 상태 머신의 옵션 지정, 정점 쉐이더, 픽셀 쉐이더 객체의 컴파일 방법 등을 지정할 수 있습니다.

 

technique T

{

        pass P0

        {

               FogEnable      = FALSE;

               AlphablendEnable= FALSE;

               VertexShader = compile vs_2_0 VtxPrc();

               PixelShader = compile ps_2_0 PxlPrc();

        }

 

        pass P1

        {

        }

}

 

 

1.3 정점 처리 HLSL

지금까지 HLSL의 중요한 문법들을 살펴보았습니다. 이제 정점 처리과정부터 픽셀 처리 과정을 간단한 연습을 통해서 HLSL을 익혀보도록 합시다.

 

1.3.1 정점 변환, 텍스처

쉐이더 기초 강의 때부터 계속 이야기 했듯이 HLSL의 정점 처리 적용 범위는 변환과 조명입니다. 변환을 수식으로 표현하면 "행렬 * 벡터" 또는 "벡터 * 행렬"입니다. HLSL d3d와 일치하도록 쉐이더가 구성되어 있어서 "벡터 * 행렬" 식을 사용합니다. 이들 곱셈은 내장 함수 "mul"을 이용합니다.

쉐이더에 입력되는 정점의 위치는 특별한 경우가 아니면 대부분 float3 형입니다. 그런데 파이프라인은 float4 형을 사용하므로 입력 데이터를 float4 형으로 늘리고 마지막 w=1로 합니다.

이렇게 해서 월드 변환, 뷰 변환, 정규 변환의 연산을 mul() 함수를 통해서 진행합니다.

만약 뷰 변화, 정규 변환에 대해서 특별하게 처리할 내용이 없다면 외부에서 뷰 행렬과 투영 행렬을 미리 곱한다음 쉐이더에 적용하는 것이 일반적입니다.

 

float4x4       m_mtWld;       // 월드 변환 행렬

float4x4       m_mtViwPrj;    // * 투영 변환 행렬

float3         vcInput;       // 정점 데이터의 입력 위치

float4         vcOut;          // 변환된 출력 위치

 

vcOut   = float4(vcIn,1);      // float4형으로 늘리고 w=1로 설정

vcOut   = mul(vcOut, m_mtWld); // 월드 변환

vcOut   = mul(vcOut, m_mtViwPrj); // * 투영 변환

return vcOut

 

<정점 변환 1: hs2_vtx1_transform1.zip>

 

여러분은 앞으로 월드 변환, 뷰 변환, 투영 변환에 대해서 HLSL로 작성할 일이 많을 것입니다. 이들을 각각 처리해 보는 것도 아주 중요합니다.

 

float4x4       mtWorld;       // 월드 변환 행렬

float4x4       mtView;        // 뷰 변환 행렬

float4x4       mtProj;        // 정규 변환 행렬

float3         vcInput;       // 정점 데이터의 입력 위치

float4         vcOut;         // 변환된 출력 위치

vcOut = float4(vcInput, 1);

vcOut = mul(vcOut, mtWorld);

vcOut = mul(vcOut, mtView);

vcOut = mul(vcOut, mtProj);

return vcOut;

 

<정점 변환2: hs2_vtx1_transform2.zip>

 

정점 처리에 대한 쉐이더 함수를 작성할 때 색상, 텍스처 좌표 등도 작성해야 합니다. 보통 텍스처의 좌표는 그림자, 환경 매핑 등과 같이 어느 특정한 대상에 대한 변환을 제외한다면 있는 그대로 출력하는 것이 대부분이며 이 경우에 대한 출력 구조체는 간단하게 만들 수 있습니다.

 

struct SvsOut

{

        float4 Pos : POSITION  ;       // 출력 위치

        float4 Dif : COLOR0    ;       // 출력 Diffuse

        float2 Tx0 : TEXCOORD0 ;       // 출력 텍스처 좌표

};

SvsOut VtxPrc( float3 Pos : POSITION  // 입력 위치

        ,       float4 Dif : COLOR0    // 입력 Diffuse

        ,       float4 Tx0 : TEXCOORD0 // 입력 텍스처 좌표

)

{

        SvsOut Out = (SvsOut)0;

        Out.Dif = Dif;

        Out.Tx0 = Tx0;

        return Out;

}

 

 

<텍스처 좌표와 정점 Diffuse 출력: hs2_vtx2_tex.zip, hs2_vtx2_tex_earth.zip>

 

 

1.3.2 색상, 안개

정점의 위치 이외에 색상, 텍스처 좌표, 안개 효과 등을 하나의 함수에서 출력하기 위해서 구조체 사용이 필요합니다. 간단하게 정점의 색상을 출력하기 위한 구조체와 함수는 다음과 같이 작성할 수 있습니다.

 

struct SvsOut

{

        float4 Pos : POSITION; // 출력 위치

        float4 Dif : COLOR0;   // 출력 Diffuse

};

 

SvsOut VtxProc( float3 Pos : POSITION, float4 Dif : COLOR)

{

        SvsOut Out = (SvsOut)0;

        // 정점 변환

       

        // 색상 출력

        Out.Dif = Dif;

        return Out;

}

 

정점의 색상을 출력할 때 주의할 것은 색상에 대한 입력값이 float4 형입니다. 보통 D3D는 색상을 32비트로 취급하지만 쉐이더는 r, g, b, a에 대해서 float형으로 처리하기 때문에 float4형으로 선언해야 합니다. 색상 처리를 float형으로 정의하고 색상의 범위를 [0, 1]로 정하면 색상의 가산연산에 대해서는 덧셈을, 감산 연산은 곱셈을 적용할 수가 있습니다.

 

<정점 Diffuse 출력: hs2_vtx3_diffuse.zip>

 

우리는 이전에 저 수준 정점 쉐이더에서 선형 포그(Linear Fog)를 직접 계산해서 레지스터 oFog에 복사하지않고 정점의 색상 출력 oD0에 출력해 보았습니다. 코드의 가독성을 위해서 전처리문을 이용해 레지스터의 이름을 우리가 생각한 이름으로 만들었지만 여전히 저 수준 명령문은 익숙해지기가 어렵습니다. HLSL을 사용하면 여러분은 선형 포그, 지수 포그, 높이 포그 등을 쉽게 구현할 수 있습니다.

선형 변화에 대한 안개 효과에 대한 공식을 출력 색상으로 지정하는 방법은 다음과 같습니다.

 

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

출력 Diffuse 색상 = Fog 색상 * Fog Factor + 정점 Diffuse * (1 - Fog Factor)

 

이 공식들을 쉐이더로 바꾸는 일은 그리 큰 어려움은 없습니다.

 

// 안개 효과 변수

float4  m_FogColor;    // Fog 색상

float   m_FogEnd;      // Fog 끝 값

float   m_FogBgn;      // Fog 시작 값

SvsOut VtxProc( float3 Pos : POSITION, float4 Dif : COLOR)

{

        float FogFactor;

        // 정점의 변환: 월드,

        vcOut = float4(vcInput, 1);

        vcOut = mul(vcOut, m_mtWorld);

        vcOut = mul(vcOut, m_mtView);

 

        // 정점의 뷰 변환 과정의 z/(Fog 끝 값 - Fog 시작 값)Fog Factor로 저장

        FogFactor = vcOut.z/(m_FogEnd - m_FogBgn);

 

        // 정점의 정규 변환

        vcOut = mul(vcOut, m_mtProj);

 

        // 출력 Diffuse를 정점 Diffuse와 포그 색상과 혼합

        float4 Fog = m_FogColor * FogFactor + In.Dif * (1-FogFactor);

 

        // 혼합 값을 출력 색상으로 지정

        Out.Dif = Fog;

 

        return Out;

}

 

<안계 효과: hs2_vtx3_fog.zip>

 

앞의 Fog Factor는 선형적으로 안개가 변화되기 때문에 Linear Fog 입니다. 쉐이더의 내장 함수 exp() 함수와 pow()를 사용해서 D3DFOG_EXP, _EXP2를 만들 수 있습니다.

 

Fog Factor EXP = 1/exp(뷰 변환 후 정점의 z * Fog Density)

Fog Factor EXP2 = 1/exp( pow(뷰 변환 후 정점의 z * Fog Density, 2) )

 

지금은 정점의 Diffuse 색상에 안개 효과를 적용하고 있어서 텍스처가 적용되면 제대로 표현하지 못합니다. 따라서 제대로된 안개 표현은 픽셀 쉐이더에서 처리하는 것이 가장 좋은 방법이며 쉐이더 Effect에서 구현해 보도록 하겠습니다.

 

 

1.3.3 분산 조명

저 수준보다 HLSL을 이용하는 장점 중에 하나가 내장된 수학 함수들이 많고 이들을 적절하게 이용할 수 있다는 것입니다. 여러분이 램버트 확산 또는 퐁의 반사의 세기(Intensity)를 구하기 위해서 저 수준으로 작성하게 되면 매번 모든 처리 과정을 작성해야 했습니다. 물론 쉐이더 코드의 길이가 적어서 copy paste가 익숙해지면 별로 문제될 것이 없다고 생각하는 분들도 있지만 같은 내용이면 D3D에서 지원되는 것을 사용하라고 대부분 GPU를 만드는 회사들이 권장하고 있는데 우리는 D3D의 쉐이더가 지원하는 함수를 이용해서 분산 조명(램버트 확산) 효과와 스페큘러(퐁 반사) 효과를 구현해 보겠습니다.

 

분산 조명은 램버트 확산에 기초를 두고 있으며 반사되는 빛의 세기(Intensity)를 빛의 방향과 정점의 법선 벡터와의 내적을 통해서 구합니다.

 

반사 밝기 = Dot(N, L)

 

이 공식을 그대로 적용하게 되면 최종 색상의 범위는 -1.0 ~ +1.0 가 됩니다. 간단하게 음수 값을 제거하기 위해서 saturation을 적용할 수 있습니다.

 

반사 밝기 = saturate( Dot(N, L) )

 

이 공식은 HLSL dot(), saturate()함수를 사용해서 거의 그대로 바꿀 수 있습니다.

 

float3  m_vcLgt;       // 빛의 방향 벡터

VtxProc(…)

{

        float3  vcNor;          // 정점의 법선 벡터

        …

        float4  ReflectIntensity = saturate( dot(vcNor, m_vcLgt) );

 

만약 렌더링 오브젝트가 회전 변환을 한다면 정점의 법선 벡터는 내적을 구하는 dot() 함수에 적용되기 전에 회전 변환해야 합니다.

 

float3x3       m_mtRot;       // 회전 행렬

vcNor = mul(vcNor, m_mtRot);

saturate()함수로 [0,1] 범위 값만 갖게 하는 것이 가장 간단하지만 현실 세계에서 빛은 공기 때문에 산란이 생기고 이로 인해서 90가 넘어도 약간의 반사가 있습니다. 이것을 물리적으로 구현하는 것이 가장 좋지만 다음과 같이 간단하게 처리할 수도 있습니다.

 

반사 밝기 = (Dot(N, L) + 1) /2

 

이 공식에 의해서 반사의 세기는 이전과 동일하게 [0, 1] 범위를 갖지만 법선 벡터와 빛의 방향 벡터의 각도가 90가 넘어도 반사 효과가 만들어 집니다. 이 공식을 일반화 시키면 다음과 같습니다.

 

반사 밝기 = saturate ( a * Dot(N, L) + b )

 

이것을 HLSL로 바꾸는 것은 어렵지 않습니다.

 

static float   m_Sat_A = 0.5f;        // Saturation Flag A

static float   m_Sat_B = 0.5f;        // Saturation Flag B

 

float3  m_vcLgt;       // 빛의 방향 벡터

VtxProc()

{

        float3  vcNor = In.Nor;               // 정점의 법선 벡터

        float4  ReflectIntensity = saturate(m_Sat_A * dot(vcNor, m_vcLgt) + m_Sat_B);

 

외부에서 Saturation 변수의 값을 바꾸고 싶으면 static 키워드를 해제하면 됩니다.

 

<Lambert: hs2_vtx4_lgt_diffuse.zip>

 

 

1.3.4 스페큘러 조명

퐁 반사를 사용한 스페큘러 반사 세기는 정점의 법선 벡터(N)에 의해 반사되는 광원의 반사 벡터(R), 정점에서 바라보는 카메라에 대한 시선 벡터(E)의 내적의 결과에 하이라이트(Sharpness) 세기를 멱승(Power)으로 구합니다. 여러분은 먼저 반사 벡터(R: Reflection vector)를 정점의 법선 벡터(N)와 빛의 방향 벡터(L)을 사용해서 먼저 구하고 반사의 세기를 결정합니다.

 

R = 2 * dot(N, L) * N – L

퐁 반사 밝기 = power(Dot(R, E), Sharpness)

 

반사 벡터를 구해주는 내장 함수는 reflect()함수 이며, 이 함수를 사용할 때는 빛의 방향을 반대 방향으로 설정합니다.

 

reflect(L, N) = L - 2 * dot(L, N) * N

반사 벡터 R = reflect(-L, N)

 

멱승(Power)을 구하는 HLSL 함수는 pow() 입니다. 이들 내장 함수 등을 이용해서 퐁 반사의 세기를 HLSL로 간단하게 작성할 수 있습니다.

 

float3 m_vcLgt;        // 빛의 방향 벡터

float3 vcNor;  // 정점의 법선 벡터

float3 vcEye;  // 정점에서 카메라 위치를 바라본 시선 방향 벡터

float3  vcR = reflect(-m_vcLgt, vcNor);

float4  Phong = pow( dot(vcR,  vcEye), m_fSharp);

 

그런데 렌더링 오브젝트는 월드 변환이 필요하므로 반사의 세기를 구하기 전에 정점에서 카메라의 위치를 바라본 시선 방향 벡터와 반사벡터에 영향을 주는 정점의 위치와 법선 벡터를 각각 월드 변환과 회전 변환을 적용해야 하고 카메라의 위치를 외부에서 받아와서 시선 방향 벡터를 구한다음 반사의 세기를 구합니다.

 

float4x3 m_mtWld;      // 월드 행렬

float3x3 m_mtRot;      // 회전 행렬

float3  m_vcCam;      // 카메라 위치

float3  m_vcLgt;      // 빛의 방향 벡터

VtxProc()

{

        float3 vcPos = mul(float4(In.Pos, 1), m_mtWld);      // 위치의 월드 변환

        float3 vcNor = mul(In.Nor, m_mtRot);         // 법선의 회전 변환

        float3 vcEye = normalize( m_vcCam - vcPos);  // 시선 벡터의 정규화

        float3 vcRfc = reflect(-m_vcLgt, vcNor);     // 반사 벡터

        float4 Phong = pow( dot(vcRfc, vcEye), m_fSharp); // 퐁 반사 세기

 

<퐁 반사: hs2_vtx4_lgt_phong.zip>

 

Blinn-Phong 반사의 세기는 퐁 반사의 반사 벡터와 시선 벡터를 사용하지 않고 Half 벡터와 정점의 법선 벡터를 이용합니다.

 

Half 벡터 = normalize(E + L)

Blinn-Phong 반사 세기 = Dot(N, H)^Sharpness

 

VtxProc()

{

        float3 vcEye = normalize(m_vcCam - vcPos);   // 시선 벡터의 정규화

        float3 vcHlf = normalize(vcEye + m_vcLgt);   // Half 벡터

        float4 Blinn = pow( dot(vcNor, vcHlf), m_fSharp); // Blinn-Phong 반사 세기

 

<블린-퐁 반사: hs2_vtx4_lgt_blinn.zip>

 

 

1.4 픽셀 처리(Pixel Processing) HLSL

1.4.1 간단한 픽셀 처리 HLSL

프로그램 가능한 픽셀 파이프라인은 픽셀의 샘플링(Sampling)과 다중 텍스처 처리(Multi-Texturing) 입니다. 픽셀 파이프라인에 입력되는 데이터는 정점 처리 과정의 Rastering을 통해서 만들어진 픽셀, 텍스처 좌표, 그리고 텍스처입니다.

픽셀 처리의 결과는 색상이기 때문에 HLSL 함수를 작성하면 출력은 float4형으로 정하고 Semantic COLOLOR로 합니다. 간단하게 정점 처리 과정에서 만들어진 색상을 그대로 출력하는 HLSL 함수를 다음과 같이 작성할 수 있습니다.

 

float4 PxlPrc(float4 iDif: COLOR) : COLOR

{

        return iDif;

}

 

void형 함수를 사용하는 경우라면 in/out 키워드를 이용해서 다음과 같이 작성합니다.

 

void PxlPrc(   in float4 iDif : COLOR0               // From Vertex Processing

        ,       out float4 oDif: COLOR0               // Output oC0

)

{

        oDif = iDif;

}

 

정점 처리의 HLSL과 마찬가지로 이들 함수 또한 D3DXCompileShader() 함수를 사용해서 컴파일을 해야 합니다.

 

LPD3DXBUFFER   pShd    = NULL;

LPD3DXBUFFER   pErr    = NULL;

hr = D3DXCompileShader(sHlsl, iLen

               , NULL, NULL

               , "PxlPrc"     // 시작 함수

               , "ps_1_1"     // 쉐이더 버전

               , dwFlags

               , &pShd         // 컴파일 쉐이더

               , &pErr         // Error

               , &m_pTbl      // 상수 테이블

               );

 

D3DXCompileShader() 함수는 HLSL로 작성한 정점 쉐이더, 픽셀 쉐이더 등을 컴파일하며 이후의 절차적 텍스처에 대한 쉐이더 또한 컴파일을 합니다.

간단히 출력하는 쉐이더 코드는 ps.1.1 으로도 충분하지만 현재 대부분의 그래픽 카드는 2.0 이상지원이 되므로 D3DXCompileShader() 함수의 쉐이더 버전 인수에 ps_2_0 이상으로 설정하는 것이 좋습니다.

D3DXCompileShader() 함수는 단순하게 쉐이더만 컴파일하므로 컴파일 결과를 가지고 픽셀 쉐이더를 생성해야 합니다.

 

LPDIRECT3DPIXELSHADER9 m_pPs;         // Pixel Shader

D3DXCompileShader(…, &pShd,…);

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

 

픽셀 쉐이더 사용, , 프로그램 가능한 픽셀 처리 파이프라인 이용은 저 수준과 동일하며 사용이 끝나면 NULL 인수를 이용해서 사용 해지를 알립니다.

 

m_pDev->SetPixelShader(m_pPs);        // Pixel Shader 사용

m_pDev->DrawPrimitive(…);            // Rendering

m_pDev->SetPixelShader( NULL);        // Pixel Shader 해제

 

<픽셀 쉐이더 기초: hs3_pxl1_basic.zip>

 

 

1.4.2 픽셀 처리 사용자 함수

저 수준은 서로 다른 처리 rootin을 하나로 작성하는 것이 쉽지가 않았습니다. 하지만 HLSL을 사용하면 if 문과 함수로 여러 처리에 대해서 쉽게 작성할 수 있습니다. 이에 대한 예로 색상의 반전, 단색화 등을 HLSL로 작성해 봅시다.

 

int g_nPxlPrc = 2;                    // 픽셀 처리 타입

 

float4  PxlInverse(float4 Input)      // 색상 반전

{

        return 1 - Input;

}

 

float4  PxlMonotone(float4 InColor)   // 단색화

{

        float4 Out = 0.f;

        float4 d = float4(0.299, 0.587, 0.114, 0);

        float4 t = InColor;

       

        Out = dot(d, t);

        Out.w = 1;

        return Out;

}

 

// 픽셀 main 처리 함수

float4 PxlPrc( in float4 iDif : COLOR0               // From Vertex Processing

) : COLOR0

{

        float4 Out=float4(0,0,0,1);

 

        if(1== g_nPxlPrc)

               Out = PxlInverse(iDif);

        else if(2 == g_nPxlPrc)

               Out = PxlMonotone(iDif);

        else

               Out = iDif;

 

        return Out;

}

 

색상의 반전은 1- 색상으로 처리하며 PxlInverse() 함수를 이를 구현하고 있습니다. 단색화는 r, g, b에 대해서 적절한 값을 곱해주고 더해서 이 값을 동일하게 r, g, b에 적용하는 것으로 PxlMonotone()함수가 이를 처리하고 있습니다. PxlPrc() 함수는 픽셀 처리의 main 함수 이며 색상 처리 종류(g_nPxlrc)에 따라서 원래의 색상, 반전, 단색화를 해당 함수를 불러서 처리하고 있습니다.

 

  

<색상 반전, 단색화: hs3_pxl2_inv_mono.zip>

 

 

1.4.3 텍스처 처리

텍스처에서 샘플링을 담당하는 샘플러를 픽셀 쉐이더에서 다음과 같이 지정합니다.

 

sampler smpDif;

 

이렇게 샘플러를 지정하고 나서 tex2D() 와 같은 샘플링 함수에 텍스처 좌표를 입력하면 텍스처에서 색상을 가져오게 됩니다.

 

float4 PxlPrc( in float4 Tx0 : TEXCOORD0 /* 텍스처 좌표 */) : COLOR0

{

        return  tex2D(smpDif, Tx0);    // 샘플링

}

 

<샘플러 register 선언: hs3_pxl3_sampler1.zip>

 

픽셀 쉐이더의 색상 처리를 연습하는 여러가지 방법 중의 하나가 Post Effect에서 사용되는 단색화(Monotone)와 흐림 효과(Blur Effect)를 구현해 보는 것입니다.

이전에 단색화는 색상의 r, g, b에 적당한 비중 값을 곱한 후에 이 값을 다시 r, g, b에 설정하는 것이라 했습니다. 또한 r, g, b에 대한 비중 값과 픽셀의 r, g, b를 벡터로 생각하고 내적(Dot Product)를 하면 곧 단색화의 색상이 된가도 했습니다.

단색화 값 = 단색화 비중(r, g, b), 픽셀 색상(r, g, b)

 

이 단색화 값에 정해진 색상을 곱하게 되면 화면에 특정한 색상으로 장면을 연출합니다. 단색화의 HLSL 구현을 위해서 이전에 저 수준으로 만들었던 예제를 HLSL로 바꾸면 다음과 같습니다.

 

sampler SampDif : register(s0);

float4  Out=0.0F;

float4  MonoColor ={0.5F, 1.0F, 2.0F, 1.0F};         // 단색화 색상

float4  MonoWeight={0.299F, 0.587F, 0.114F, 0.0F};   // 단색화 비중

 

Out = tex2D( SampDif, Tx0 );  // 샘플링

Out = dot(Out, MonoWeight);   // 내적(dot)로 단색화

Out *= MonoColor;             // 단색화 색상을 곱함

return Out;

 

 

<단색화: hs4_pxl1_mono.zip>

 

흐림 효과(Blur Effect)는 인접한 픽셀에 비중 값을 곱하고 더해서 최종 색상을 만는 것입니다. 특히 Gaussian Blur은 인접한 픽셀까지의 거리를 가지고 비중 값을 exp()함수로 결정하며 수식으로 표현하면 다음과 같습니다.

 

최종 색상 =

 

HLSL exp() 함수를 지원합니다. 인접한 픽셀은 텍스처의 좌표를 변화시킨 후에 tex2D() 와 같은 샘플링 함수를 가지고 얻으며 이것을 반복적으로 for 문 등을 이용해서 적용합니다. 다음은 Gaussian Blur HLSL로 구현한 예 입니다.

 

sampler SampDif : register(s0);

float4  Out=0.0F;

for(x, )

{

        float2 T = Tx0;

        T.x += (2.f * x)/1024.f;

        Out += tex2D( SampDif, T ) * exp( (-x*x)/8.f);

}

Out *= 0.24F// 전체 명도를 낮춤

return Out;

 

<Blur 효과: hs4_pxl2_blur.zip>

 

단색화와 흐림 효과는 그 자체만으로 의미가 있지만 코드의 길이가 길지 않기 때문에 이 둘을 합쳐서 표현하는 것도 좋습니다.

 

sampler SampDif : register(s0);

float4  Out=0.0F;

for(int x=-4; x<=4; ++x)

{

        float2 T = Tx0;

        T.x += (2.f * x)/1024.f;      // 텍스처 좌표를 변화시킨다

        Out += tex2D( SampDif, T ) * exp( (-x*x)/8.f);

}

Out = dot(Out, MonoWeight);           // 단색화

Out *= MonoColor;                      // 단색에 적용할 색상을 곱함

return Out;

 

<단색화 + 흐림 효과: hs4_pxl3_mono+blur.zip>

 

흐림 효과와 단색화를 합쳐 놓으니까 더 멋진 장면을 만들어 냈습니다. 게임의 장면들은 이렇게 작은 부분을 결합해서 만드는 경우도 많이 있으니 꾸준히 연습하기 바라며 다음으로 다중 처리를 HLSL로 구현해 보도록 하겠습니다.

 

다중 텍스처 처리(Multi-Texturing)를 하려면 샘플러를 샘플러 레지스터에 명시적으로 선언을 해야 합니다. 이 때 register() 함수를 이용합니다.

 

sampler smp0 : register(s0);

sampler smp1 : register(s1);

sampler smp2 : register(s2);

이렇게 샘플러를 선언하면 pDevice->SetTexure(StageIndex, pTexture)와 같은 문장을 실행 할 때 Stage Index에 해당하는 레지스터에 텍스처 포인터가 자리잡고 이 텍스처를 샘플러가 샘플링합니다.

텍스처 샘플링에 대한 내장 함수는 tex1D, tex2D, tex3D, tex2Dproj, tex3Dproj, texCUBE 등이 있으며 가장 많이 사용하는 함수는 2차원 텍스처에 대한 tex2D() 함수입니다. 모든 샘플링 함수는 샘플러와 텍스처 좌표를 인수로 받습니다. 따라서 픽셀 처리의 main 함수는 정점 처리에서 만들어진 색상뿐만 아니라 텍스처 좌표도 같이 받아야 합니다.

Rasterizing 으로 만들어진 색상과 텍스처의 색상을 modulate하는 과정을 다음과 같이 작성할 수 있습니다.

 

sampler smp0 : register(s0);

float4 PxlPrc(float4 iDif: COLOR, float2 Tex0: TEXCOORD0) : COLOR

{

        return iDif * tex2D(smp0, Tex0);

}

 

만약 modulate2x 를 구현하고자 하면 출력의 결과에 2를 곱하면 됩니다.

 

float4 Out;

Out = tex2D(smp0, Tex0);

Out *= iDif;

Out *= 2;

return Out;

 

<MODULATE2X: hs3_pxl3_sampler2_earth.zip>

 

HLSL을 사용하면 고정 기능 파이프라인의 MODULATE4X, ADD, SUBSTRACT, ADDSIGNED 등의 FLAG로 다중 처리를 지시한 내용을 간단한 산술 연산으로 구현할 수 있어서 고정 파이프라인에서 처리하는 것보다 여러모로 잇점이 많습니다.

고정 기능 파이프라인에서 다중 처리에 대한 텍스처 샘플링의 상태 값을 설정하는 것은 간단하지만 D3D FLAG 값들을 기억하기가 어려웠습니다. 그런데 HLSL을 사용하면 각 샘플러에 대해서 필터링과 어드레싱을 설정할 수가 있습니다. 샘플러의 상태를 설정하기 위해서 sampler_state를 사용합니다.

 

sampler smpDif0 : register(s0) = sampler_state

{

    MinFilter = POINT;        // Filtering

    MagFilter = POINT;

    MipFilter = POINT;

    AddressU = Wrap;          // Addressing

    AddressV = Wrap;

};

 

sampler smpDif1 : register(s1) = sampler_state

{

    MinFilter = NONE;

    MagFilter = NONE;

    MipFilter = NONE;

    AddressU = Wrap;

    AddressV = Wrap;

};

<Multi-Texturing: hs5_pxl4_multi_tex1.zip>

 

앞서 산술 연산으로 Multi-Texturing을 쉽게 구현할 수 있다고 했습니다. 다음의 HLSL 코드는 2개의 텍스처를 여러 상황에 대해서 간단한 산술 연산으로 구현한 예입니다.

 

sampler SampDif0 : register(s0) = sampler_state

sampler SampDif1 : register(s1) = sampler_state

int     m_nMulti;

float4 PxlPrc(float4 Tx0 : TEXCOORD0) : COLOR0

{

        float4 Out= 0;

        float4 t0 = tex2D( SampDif0, Tx0 );          // Sampling m_TxDif0

        float4 t1 = tex2D( SampDif1, Tx0 );          // Sampling m_TxDif1

 

        if(0 == m_nMulti)      Out = t0;

        else if(1 == m_nMulti) Out = t1;

        else if(2 == m_nMulti) Out = t0 * t1;         // Modulate

        else if(3 == m_nMulti) Out = t0 * t1 * 2;     // Modulate 2x

        else if(4 == m_nMulti) Out = t0 * t1 * 4;     // Modulate 4x

        else if(5 == m_nMulti) Out = t0 + t1;         // Add

        else if(6 == m_nMulti) Out = t0 + t1 - .5;    // Add signed

        else if(7 == m_nMulti) Out =(t0 + t1 - .5)*2; // Add signed

        else if(8 == m_nMulti) Out = t0 + t1 - t0*t1; // add smooth

        else if(9 == m_nMulti) Out = t0 - t1;         // sub

        else if(10== m_nMulti) Out = t1 - t0;         // sub

        else if(11== m_nMulti) Out = 1 - t0;          // Inverse t0

        else if(12== m_nMulti) Out = 1 - t1;          // Inverse t1

        else if(13== m_nMulti) Out = 1 - (t0 + t1);   // Inverse (t0+t1)

 

        return Out;

}

 

<Multi-Texturing 연습: hs5_pxl4_multi_tex2.zip>

 

 

1.4.4 Procedural Texture

HLSL의 흥미로운 점은 쉐이더로 작성한 픽셀 처리를 텍스처에 저장할 수가 있습니다. 이 때 사용되는 함수가 D3DXFillTextureTX() 함수 입니다. 텍스처의 픽셀을 어떤 알고리듬(Algorithm)에 의해서 컴퓨터의 처리에의해 만들어진 텍스처를 절차적 텍스처(Procedural Texture)라 합니다. HLSL Algorithm이고 이를 적용하게 되면 절차적 텍스처가 됩니다.

절차적 텍스처에 대한 쉐이더를 작성할 때 픽셀의 좌표는 [0, 1] 범위가 되며 픽셀 처리에 대한 main 함수의 입력 Semantic은 텍스처 좌표 TEXCOORD가 아닌 위치 POSITION으로 합니다. Position 값은 뷰포트의 영역을 [0, 1]로 바라본 값입니다. 또한 쉐이더가 픽셀의 색상을 결정하므로 함수의 반환 값은 float4 형으로 하고 Semantic COLOR로 합니다.

 

float4 TxlPrc(float2 In : POSITION) : COLOR0

{

        float4  Out;

        …

        return Out;

}

 

절차적 텍스처에 대한 쉐이더를 HLSL로 작성하게 되면 정점 쉐이더, 픽셀 쉐이더와 마찬가지로 D3DXCompileShaderFromFile() 함수를 이용해서 쉐이더를 컴파일하고 쉐이더 버전에 텍셀 버전을 입력합니다.

 

D3DXCompileShaderFromFile("쉐이더 파일"

                       , NULL, NULL

                       , "TxlPrc"     // 텍셀 main 함수

                       , "tx_1_0"     // 텍셀 버전

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

 

절차적 텍스처는 D3D Device CreateTexture() 함수 또는 D3DXCreateTexture() 함수롤 이용해서 생성합니다.

 

D3DXCreateTexture(m_pDev, 128, 128, 1, 0, D3DFMT_UNKNOWN, D3DPOOL_MANAGED, &m_pTx);

 

텍스처의 픽셀에대해서 텍셀 쉐이더를 적용하기 위해 D3DXFillTextureTX() 함수를 호출하면 절차적 텍스처가 완성이 됩니다.

 

D3DXFillTextureTX(m_pTx, (DWORD*)pShd->GetBufferPointer(), NULL, 0);

 

<Procedural 텍스처: hs5_pxl4_procedual1.zip

 

화면 잡음과 같은 Post Effect Normal Mapping 등에서는 정점의 텍스처 좌표에서 공식에 따른 위치에 텍스처 좌표를 다시 설정하고 샘플링 작업을 진행하는 경우가 있습니다. 이러한 경우에 공식을 직접 적용하는 방법도 있지만 때로는 공식의 결과 값을 텍스처에 저장해서 처리 속도를 빠르게 합니다.

hs4_pxl4_procedual2.zip Noise 텍스처를 생성하는 예제로 random 값을 만들기 위해서 noise() 함수를 사용하고 있습니다.

 

static float m_Delta = 2000.0F;

float4 TxlPrc(float2 Pos : POSITION) : COLOR0

{

        float4 Out = (float4)0;

        Out.r = noise((Pos+0) * m_Delta);

        Out.g = noise((Pos+1) * m_Delta);

        Out.b = noise((Pos+2) * m_Delta);

        Out = normalize(Out);

        Out = (Out + 1) * 0.5F;

        Out.w = 1;

        return Out;

}

 

 

<Procedural Noise 텍스처: hs5_pxl4_procedual2.zip m_Delta = 4, m_Delta = 2000>

 

Post Effect 연습에서 사각형, 은행잎, 직소 등의 패턴 효과에서 일정한 영역에 대해서 같은 픽셀을 적용합니다. 영역 중심에서 벗어나는 텍스처 좌표는 중심 쪽으로 상대적으로 이동할 수 있도록 해야하는데 이 값(Delta)을 미리 텍스처에 저장합니다.

텍스처에서 픽셀을 가져와서 새로운 좌표로 다음의 공식을 이용합니다.

 

New(U, V) = Old(U, V) + Delta( r-0.5, g-0.5)

 

hs5_pxl4_procedual3.zip 예제는 텍스처의 중앙에서 샘플링이 가능하도록 Delta 값을 저장하는 예입니다.

 

<Procedural Delta 텍스처: hs5_pxl4_procedual3.zip>

 

이 텍스처를 사용하는 예제는 Post Effect의 은행잎, 직소 등을 살펴보기 바랍니다.

 

 

1.5 ILcEffect 만들기

저 수준 쉐이더에서는 하나의 모듈은 하나의 파일 또는 단일 문자열에서 처리했었습니다. 그런데 HLSL은 정점과 픽셀 처리에 대한 main 함수들을 하나의 파일에 작성할 수 있고 D3DXCompileShaderFromFile() 에서 컴파일 할 때 원하는 main 함수를 지정하면 해당 함수와 관련된 내용만 컴파일 합니다.

 

<정점 쉐이더, 픽셀 쉐이더를 하나의 파일에 작성한 예: hs6_vtxpxl.zip>

 

이것은 여러가지 장점이 있습니다. D3D가 구조상 정점과 픽셀 쉐이더가 분리되어 있다하더라도 게임에서는 이 둘을 한꺼번에 사용되는 경우가 많이 있으므로 이 둘을 하나의 모듈 안에 구현되어 있으면 편리하며 우리는 이를 위해서 고 수준 언어로 작성된 정점과 픽셀 쉐이더를 ILcEffect로 추상화 해 봅시다.

HLSL 과 저 수준 쉐이더는 언어의 형식만 다르기 때문에 저 수준에서 추상화한 방식이 거의 그대로 활용될 수 있습니다. 변화된 것은 쉐이더의 상수 설정에 대한 구현이 상수 테이블을 사용하고 정점 쉐이더와 픽셀 쉐이더의 main 함수를 지정하는 것입니다.

 

interface ILcEffect

{

        virtual INT    Create(void* =NULL,void* =NULL,void* =NULL,void* =NULL)=0;

        virtual void   Destroy()=0;

        virtual INT    Begin()=0;

        virtual INT    End()=0;

 

        virtual INT    SetupDecalarator(DWORD dFVF)=0;

        virtual INT    SetMatrix(char* sName, D3DXMATRIX* v)=0;

        virtual INT    SetVector(char* sName, D3DXVECTOR4* v)=0;

        virtual INT    SetColor(char* sName, D3DXCOLOR* v)=0;

        virtual INT    SetFloat(char* sName, FLOAT v) =0;

};

 

ILcEffect 객체를 생성하는 함수는 다음과 같습니다.

 

int LcHlsl_CreateShader(char* sCmd, ILcEffect** pData, …);

 

ILcEffect를 구현한 CHlslEffect은 지정된 쉐이더 함수를 컴파일하고 정점 선언자, 정점 쉐이더, 픽셀 쉐이더 객체를 생성합니다. 또한 상수 레지스터 설정을 상수 테이블로 구현합니다.

 

LPD3DXCONSTANTTABLE    m_pTbl; // 정점 쉐이더를 위한 상수 테이블

INT CHlslEffect::SetMatrix(char* sName, D3DXMATRIX* v)

{

        return m_pTbl->SetMatrix(m_pDev, sName, v);

}

INT CHlslEffect::SetVector(char* sName, D3DXVECTOR4* v)

{

        return m_pTbl->SetVector(m_pDev, sName, v);

}

INT CHlslEffect::SetColor(char* sName, D3DXCOLOR* v)

{

        return m_pTbl->SetVector(m_pDev, sName, (D3DXVECTOR4*)v);

}

INT CHlslEffect::SetFloat(char* sName, FLOAT v)

{

        return m_pTbl->SetFloat(m_pDev, sName, v);

}

 

좀 더 정교하게 작성하려면 정점 쉐이더에 대한 상수 테이블뿐만 아니라 픽셀 쉐이더의 상수 테이블도 같이 구현하는 것이 좋습니다. hs6_ILcEffect.zipILcEffect를 구현하고 Glow 효과를 보여주고 있습니다.

 

<ILcEffect의 구현과 Glow 효과: hs6_ILcEffect.zip>



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

Creative Commons License