버텍스 셰이더

DirextX 8.0의 tutorial10에 버텍스 셰이더를 추가하겠다.

왜 셰이더를 사용할까? 유연한 프로그래밍이 첫째 목적이고, 둘째가 최적화이다.
2가지 이유 모두 아직 머리 속에 맴돌기만 할뿐 감이 오지 않는다.

유연한 프로그래밍이 가능한 이유는 데이터가 고정파이프 라인으로 들어가는 대신에 프로그래밍 가능한 GPU로 들어가기 때문에 이전에 CPU에서나 하던일을 GPU에서 가능하게 되었다.

이런 이유로 최적화에도 도움이 된다. 그래픽 카드로 보내는 데이터량도 줄일 수 있고, GPU는 CPU보다 벡터 연산이 더 빠르기 때문에 거의 대부분 앞선다.

입력 벡터 레지스터:
데이터가 들어오는 레지스터로 v0, v1, v2.......... v15까지 16개의 입력 버퍼가 있다.
셰어더 어셈블러에서는 v0, v1과 같이 사용하고
선언에서 사용될 때는 다음과 d3d8types.h파일에 다음과 같이 정의 되어있다.

	 
	[리스트 1]
	   
	#define D3DVSDE_POSITION 0
	#define D3DVSDE_BLENDWEIGHT 1 
	#define D3DVSDE_BLENDINDICES 2 
	#define D3DVSDE_NORMAL 3 
	#define D3DVSDE_PSIZE 4 
	#define D3DVSDE_DIFFUSE 5 
	#define D3DVSDE_SPECULAR 6 
	#define D3DVSDE_TEXCOORD0 7 
	#define D3DVSDE_TEXCOORD1 8 
	#define D3DVSDE_TEXCOORD2 9 
	#define D3DVSDE_TEXCOORD3 10 
	#define D3DVSDE_TEXCOORD4 11 
	#define D3DVSDE_TEXCOORD5 12 
	#define D3DVSDE_TEXCOORD6 13 
	#define D3DVSDE_TEXCOORD7 14 
	#define D3DVSDE_POSITION2 15 

상수 레지스터:
셰이더에서 읽기 전용으로 사용되는 레지스터로 버텍스 셰이더의 명령이 실행되기 이전에 상수를 바꿀 수 있기 때문에 실제로는 함수를 바꾸는 변수의 용도로 사용된다.
D3DCAP8 구조체의 MaxVertexShaderConst 멤버로 사용 가능한 상수의 최대 갯수를 알 수 있다.

주소 레지스터:
상수들을 인덱스할 수 있게 해 주는 특별한 엔터티이다. 더 이상 설명은 나도 모르기 때문에 무시...

임시 레지스터:
고급언어의 지역변수 처럼 변수값이 보존되지 않는다.
r0, r1, r2............  r11 처럼 불린다.

출력 레지스터:
출력 레지스터로 내 보내지 않으면 화면엔 아무것도 보이지 않을것이다.
출력 레지스터의 사용 용도는 다음과 같다.

출력변수

설명

oPos

변환된 화면 좌표들 안의 출력 위치

oDn

2개의 컬러 출력(oD1과 oD2), 각각은 4D 벡터이다.

oTn

4개의 텍스처 좌표 출력(oT0부터 oT3까지). 각각은 4D 벡터이다.

oFog

출력 포그 값. 이것은 하나의 FLOAT 값으로 취급된다.

oPts    

출력 점 크기 값. 이것은 하나의 FLOAT 값으로 취급된다.

 

DirextX 8.0의 tutorial 10의 소스를 버텍스 셰이더로 수정해보자.
굳이 셰이더로 할 필요없지만 "Hello world!"로 C를 배울 때 처럼 쉬운걸로 시작해보자.
입력레지스터의 값을 상수레지스터의 값으로 연산한 뒤에 출력레지스터로 보내준다는걸 기억하면 된다.

출력 레지스터  = 상수 레지스터 (짬뽕) 입력 레지스터

DirextX 8.0의 tutorial 10에 새롭게 추가된것만 설명하겠다.

1) 셰이더 변수 선언

DWORD                                   g_hVShader = 0;  //버텍스 셰이더의 핸들                                         

HANDLE이 아니라 DWORD로 버텍스 셰이더의 핸들을 선언한다.

2) 셰이더 생성

HRESULT CreateShader()
{
	const char szVertextShader[] =
	"vs.1.1                //버전 정보   \n"
	"dp4 oPos.x, v0, c0    //c0는 x값    \n"
	"dp4 oPos.y, v0, c1    //c1은 y값    \n"
	"dp4 oPos.z, v0, c2    //c2는 z값    \n"
	"dp4 oPos.w, v0, c3    //w값         \n"
	"mov oD0,  v5          //꼭지점 칼라 전달\n";

	ID3DXBuffer* pShaderBuffer;
	ID3DXBuffer* pShaderErrors;

	if (FAILED(D3DXAssembleShader(szVertextShader, sizeof(szVertextShader) - 1, 0, NULL, &pShaderBuffer, &pShaderErrors)))
		return E_FAIL;

	DWORD Declaration[] =
	{
		D3DVSD_STREAM(0),
		D3DVSD_REG(D3DVSDE_POSITION,  D3DVSDT_FLOAT3),
		D3DVSD_REG(D3DVSDE_DIFFUSE,   D3DVSDT_D3DCOLOR),
		D3DVSD_END()
	};
	
	if (FAILED(m_pD3DDevice->CreateVertexShader(Declaration, (DWORD *)pShaderBuffer->GetBufferPointer(),
								   &g_ShaderHandle, 0)))
		return E_FAIL;
	_RELEASE_(pShaderBuffer);
}	    
	    

CreateShader()를 RenderInit(), 즉 버텍스 버퍼 생성 이전에 실행한다.

D3DXAssembleShader과 D3DXAssembleShaderFromFile 2가지가 있는데, 셰이더가 간단하기 때문에 D3DXAssembleShader를 사용하여 셰이더 코드의 초소한 에러체크와 이진코드로 만들를 있다.
pShaderBuffer는 더 이상 필요하지 않기 때문에 해제한다.

셰이더 문법은 마지막에 설명하겠다.

        DWORD Declaration[] =
        {
                D3DVSD_STREAM(0),
                D3DVSD_REG(D3DVSDE_POSITION,  D3DVSDT_FLOAT3),
                D3DVSD_REG(D3DVSDE_DIFFUSE,   D3DVSDT_D3DCOLOR),
                D3DVSD_END()
        };

CreateVertexShader()에 전달해주기 위한 Declaration[]변수를 CUSTOMVERTEX  구조체와 비슷하게 만든다
Declaration는 D3DVSD_STREAM(0)에서 시작하여 D3DVSD_END()으로 끝납니다.
스트림 0에서 시작하고 있습니다.

그 사이에 선언되어 있는 것은 CUSTOMVERTEX 구조체와 같은 순서로 리스트 1의 선언을 적든지, 숫자를 적고,
두 번째는 크기선언을 적으면 된다.

CreateVertexShader()로 디바이스 상에 실제적으로 셰이더 인스턴스를 생성한다.

3) 렌드링

void MoveShip(float fx, float fy)

{
        D3DXMATRIX      mScale1, mRot1, mTran1, mResult1;
        D3DXMATRIX      mScale2, mRot2, mTran2, mResult2;
        D3DXMATRIX      mScale3, mRot3, mTran3, mResult3;

        //본체
        //SRT
        D3DXMatrixScaling( &mScale1, 1.1f, 1.1f, 1.1f );
        D3DXMatrixRotationZ(&mRot1, D3DXToRadian(g_fDegree));
        D3DXMatrixTranslation( &mTran1, fx, fy, 0.0f);
        mResult1 = mScale1 * mRot1 * mTran1;

#if  0
        g_d3d_Device->SetTransform( D3DTS_WORLD, &mResult1);
        g_d3d_Device->DrawPrimitive( D3DPT_TRIANGLESTRIP, 3, 2 );
#else
        D3DXMATRIX ShaderMatrix = mResult1 * g_matCameraView * g_matProj;
        //Get the transpose
        D3DXMatrixTranspose(&ShaderMatrix, &ShaderMatrix);
        //Pass the transposed matrix to the shader
        g_d3d_Device->SetVertexShaderConstant(0, &ShaderMatrix, 4);
        g_d3d_Device->SetVertexShader(g_hVShader);        
        g_d3d_Device->DrawPrimitive( D3DPT_TRIANGLESTRIP, 3, 2 );

        g_d3d_Device->SetVertexShader(D3DFVF_CUSTOMVERTEX);
#endif

        //포탑
        D3DXMatrixScaling( &mScale2, 1.0f, 1.0f, 1.0f );
        D3DXMatrixRotationZ(&mRot2, D3DXToRadian(g_fTurretDegree));
        D3DXMatrixTranslation( &mTran2, 0.0f, -0.7f, 0.0f );

        mResult2 = mScale2 * mRot2 * mTran2 * mResult1;
        g_d3d_Device->SetTransform( D3DTS_WORLD, &mResult2 );
        g_d3d_Device->DrawPrimitive( D3DPT_TRIANGLELIST, 0, 1 );

        //포
        D3DXMatrixScaling( &mScale3, 0.1f, 0.3f, 1.0f );
        D3DXMatrixRotationZ(&mRot3, 0.0f);
        D3DXMatrixTranslation( &mTran3, 0.0f, -0.1f, -0.5f);

        mResult3 = mScale3 * mRot3 * mTran3 * mResult2;
        g_d3d_Device->SetTransform( D3DTS_WORLD, &mResult3 );
        g_d3d_Device->DrawPrimitive( D3DPT_TRIANGLESTRIP, 3, 2 );
}

 

#if  0
        g_d3d_Device->SetTransform( D3DTS_WORLD, &mResult1);
        g_d3d_Device->DrawPrimitive( D3DPT_TRIANGLESTRIP, 3, 2 );
#else
        D3DXMATRIX ShaderMatrix = mResult1 * g_matCameraView * g_matProj;
        //Get the transpose
        D3DXMatrixTranspose(&ShaderMatrix, &ShaderMatrix);
        //Pass the transposed matrix to the shader
        g_d3d_Device->SetVertexShaderConstant(0, &ShaderMatrix, 4);
        g_d3d_Device->SetVertexShader(g_hVShader);        
        g_d3d_Device->DrawPrimitive( D3DPT_TRIANGLESTRIP, 3, 2 );

        g_d3d_Device->SetVertexShader(D3DFVF_CUSTOMVERTEX);
#endif


배의 본체만 셰이더를 사용하고 있다.
#if 절을 주목하자. 우리가 만든 간단한 셰이더 코드에서 이동을 처리해주기 때문에 SetTransform()을 더 이상 호출할 필요가 없다.

대신 월드 메트릭스, 뷰 메트릭스(카메라 뷰 메트릭스), 투영 메트릭스(카메라 투영 메트릭스)를 곱해서 셰이더로 전달해준다.
D3DXMATRIX ShaderMatrix = mResult1 * g_matCameraView * g_matProj;

셰이더 내부에서는 OPEN GL과 같은 열행렬을 사용하고 있기 때문에 셰이더로 전달하기 전에 행 행렬에서 열행렬로 바꾸기 위해 전치행렬을 사용한다.
D3DXMatrixTranspose(&ShaderMatrix, &ShaderMatrix);

모든 변환이 끝났으면 셰이더로 넘겨준다.
첫 번째 파라메터는 상수 레지스터 c0에서 시작해서 세 번째 파라메터 4개의 상수레지스터를 사용한다(여기서는  c3 레지스터까지 사용)
g_d3d_Device->SetVertexShaderConstant(0, &ShaderMatrix, 4);

우리가 프로그래밍한 셰이더로 그리기 위해 셰이더를 설정한다.
g_d3d_Device->SetVertexShader(g_hVShader)

DrawPrimitive()를 해주고 나서 나머지는 고정 파이프라인을 사용하기 때문에, 고정 파이프라인 설정을 다시 해준다.
g_d3d_Device->SetVertexShader(D3DFVF_CUSTOMVERTEX); 

4) 소멸

    if ( g_hVShader != NULL )
    {
        g_d3d_Device->DeleteVertexShader( g_hVShader );
        g_hVShader = NULL;
    }

5) 셰이더 코드 설명

vs.1.1

셰이더 버전을 설정한다.

dp4 oPos.x, v0, c0
dp4 oPos.y, v0, c1
dp4 oPos.z, v0, c2
dp4 oPos.w, v0, c3
 

위코드는 1X4 벡터인 v0와 4X4 벡터 c0를 곱해 뷰, 투영 행렬을 구하여 출력 레지스터 oPos에 넣는 코드이다
이 코드와 같다. ( = m4x4 oPos, v0, c0)

행렬식으로 보면 다음과 같다.

                                                     ┌c0.x c1.x c2.x c3.x ┐
[oPos.x oPos.y oPos.z oPos.w] = [v0.x v0.y v0.z v0.w]|c0.y c1.y c2.y c3.y │
                                                     │c0.z c1.z c2.z c3.z │
                                                     └c0.w c1.w c2.w c3.w ┘

mov oD0,  v5

디퓨즈 색상을 출력한다.

실제적으로 추가된 코드는 모두 네 군데이다. 상수 레지스터를 통하여 전달하면, 입력레지스터의 버텍스와
조합하여 출력레지스터로 내 보내는 구조라는걸 명심하자.

다이렉트 X 9.x에서는 셰이더 사용법이 약간 바뀌었다.

[펌]
DirectX 실시간 렌더링 실전 테크닉 - 정보 문화사
http://www.t-pot.com