1. Flexible Vertex Formats
Lesson 3에서 배운대로 vertex는 위치 정보 + 속성 정보인데 속성 정보도 결국은 숫자로 나타내는 어떤 값일 것이다.
이러한 정보 표현과 저장을 위해서 Direct3D는 Flexible Vertex Format (FVF)를 사용한단다. 바로 이런 기법을 이용하여 정보를 저장하고 읽어 오는데 어떻게 동작하는지 살펴보자.
vertex는 구조체다. 이미지를 디스플레이 하기 위해서는 이 정보를 Video Ram으로 복사해오고 D3D는 이 정보를 Back buffer에 복사 해서 넣게 된다.
하지만 만약 사용자가 vertex가 될만한 (사실상 모든 vertex를 말하는거 겠죠) 모든 정보 (vertices)를 보낸다면 아래 그림처럼 Video RAM에서 많은 공간을 복사하여 사용할 것이다.
Image 4.1 - A Vertex Format Containing All Possible Data
하지만, 이렇게 하면 vertex가 무지 많은 3D 이미지의 경우 얼마나 많은 복사와 정보의 공간이 필요할지 예측이 된다. 따라서 좀더 현명하게 저 vertex의 구조체에서 필요한 부분만 잘라와서 Video RAM에 복사해 넣는 것이 나을것이다. (아래 그림처럼)
Image 4.2 - A Flexible Vertex Format Goes Faster
근데, 상식적으로 한개의 vertex 구조체에는 좌표 정보 뿐 아니라 각종 속성도 들어가 있으니 당연히 아래 그림처럼 하지 않을까 하는게 내 예측이긴 한데...
아무튼, 바로 이것이 우리가 Flexible Vertex Format을 사용할때 생기는 동작의 원리란다.
FVF Codes
Direct3D에서는 각각의 vertex는 pre-set vertex format으로 부터 만들어지게 된다는데 이것은 혹시 추상 클래스의 개념이 아닐까? 이것은 흔희 API 에서 사용하는 방식대로 OR연산자 | 를 이용하여 플래깅을 표시하여 내가 원하는 형태의 vertex format을 미리 선언해 주는 것 같다.
다음과 같이 선언을 하게 되면 location 정보와 diffuse 색상을 한개의 vertex format에 포함시키겠다는 의미다.
다음 테이블이 바로 플래그 테이블인데,
Flag | Description | Types Included |
---|---|---|
D3DFVF_XYZ | Indicates that the vertex format includes the X, Y and Z coordinates of an untransformed vertex. Untransformed means that the vertex has not yet been translated into screen coordinates. | float, float, float |
D3DFVF_XYZRHW | Indicates that the vertex format includes the X, Y and Z coordinates as well as an additional RHW value of a transformed vertex. This means that the vertex is already in screen coordinates. The Z and the RHW are used when building software engines, which we will not get into. | float, float, float, float |
D3DFVF_DIFFUSE | Indicates that the vertex format contains a 32-bit color code for a vertex, used for the color of diffuse lighting. | DWORD |
D3DFVF_SPECULAR | Indicates that the vertex format contains a 32-bit color code for a vertex, used for the color of specular highlighting. | DWORD |
D3DFVF_TEX0 through D3DFVF_TEX8 |
Indicates that the vertex format contains the coordinates for any textures that will be applied to a model. |
물론 더 많은 flag들이 있지만 튜토리얼에서는 이것들 위주로 다룬다.
Creating Vertices
자, 이제 vertices를 만들어내야 하는데 만들때 우리가 선언한 새로운 포멧 (FVF)를 사용해야 한단다. 말이 조금 애매한데 밑에 있는 예를 보면 조금 이해가 된다. 새로운 vertices를 선언하기 위해서 추가로 특별한 함수를 쓰는게 전혀 없고 단지 구조체를 선언하는 것 뿐이다.
위에서 CUSTOMFVF를 선언할때 두가지 타입을 썼는데 위의 테이블을 보면 타입이 나와있다. 바로 float과 DWORD이다. 고로, 이 형식에 맞추어 구조체를 작성하는데,
{
FLOAT x, y, z, rhw; // from the D3DFVF_XYZRHW flag
DWORD color; // from the D3DFVF_DIFFUSE flag
}
이렇게 해주면된단다. 상당히 충격적이다. -_-
이렇게 구조체를 선언했으니 하나의 타입이고 이 타입 형을 이용하여 드디어 점을 선언해보자.
바로 이렇게 한단다. 물론 array를 선언하고 싶다면 다음과 같이,
{
{320.0f, 50.0f, 1.0f, 1.0f, D3DCOLOR_XRGB(0, 0, 255),},
{520.0f, 400.0f, 1.0f, 1.0f, D3DCOLOR_XRGB(0, 255, 0),},
{120.0f, 400.0f, 1.0f, 1.0f, D3DCOLOR_XRGB(255, 0, 0),},
};
해줘도 된다. OpenGL에 비해서 상당히 놀라운 한가지의 차이점이 있는데 바로 'Flexible'이라는 단어로 요약가능 한것 같다. OpenGL에서는 사용자가 사용하기 원하는 정보만 사용할수 있는 기능이 없다. 하지만 DX에서는 사용자가 원하는 정보만 Video RAM으로 전달할수 있는 말그대로 Flexible한 정보 저장이 가능하다. 바로 이것이 속도의 차이에 한 가지 원인인가?
2. Vertex Buffers
자, 앞서 우리는 두단계를 했다. 1. FVF 코드 작성 (원하는 정보 지정) 2. 삼각형 형태의 점을 지정. 그렇다면 이제 필요한 것은 무엇인가? 바로, D3D가 이 삼각형을 그려내도록 하게 하는 것인데 이때 필요한 것이 Vertex Buffer이다.
Vertex Buffer는 단순한 인터페이스 인데 이것은 시스템 혹은 비디오 메모리의 한 부분에 내가 사용중인 게임에서의 데이터 (vertex/ model data)를 저장한다. 이 인터페이스는 CreateVertexBuffer()라는 명령을 이용해서 생성 가능하다. 이 함수의 원형을 자세히 살펴보면,
UINT Length,
DWORD Usage,
DWORD FVF,
D3DPOOL Pool,
LPDIRECT3DVERTEXBUFFER9 ppVertexBuffer,
HANDLE* pSharedHandle);
이함수의 인자들은 쉽게 설명하기 어려운데 하나하나 살펴보면,
UINT Length : 이 인자는 만들어질 버퍼의 크기를 지정한다. 이 값은 한개 vertex의 사이즈에 버퍼에 저장될 vertex의 총 갯수를 곱해서 구할수 있다. 만약, 삼각형의 경우 3개의 vertex가 필요하니까 이 인자는 3 * sizeof(CUSTOMVERTEX) 가 되는 셈이다.
DWORD Usage : 이것은 vertices를 handle하는 DX의 특별한 방식을 지정하는 것인데 현재의 튜토리얼에서는 사용하지 않는다. 어쨌든 지금은 0으로 셋 해놓자.
DWORD FVF : 이것은 우리가 이전에 만든 FVF 코드를 넣는 인자다. 고로 CUSTOMFVF를 넣으면 되겠네.
D3DPOOL Pool : 이 인자는 D3D에게 어디다 버퍼를 만들지 그리고 어떻게 만들것인지를 지정하게 된다. 다음 이어지는 테이블이 올수 있는 플래그를 설명하는데 함 보자. 오... 몇가지 플래그 들이 있는데 상당히 의미 있는 플래그 들이다.
[Table 4.2 - D3DPOOL Values]
Value | Description |
---|---|
D3DPOOL_DEFAULT | This flag indicates that the buffer should be created in the most appropriate memory for what settings and resources are available. This however, imposes some limits which are not always good for games. |
D3DPOOL_MANAGED | This indicates that the buffer will be located in the video RAM. |
D3DPOOL_SYSTEMMEM | This indicates that the buffer will be located in the system memory. Vertex buffers located here cannot usually be accessed by the Direct3D Device, but can be accessed by other, more advanced means. |
D3DPOOL_SCRATCH | This also indicates the buffer will be located in system memory, however, there is no way for the video RAM to access this. This type is useful for storing graphics information that is not currently being used (but will be used later), such as graphics belonging to other maps a player hasn't reached yet, but might in the near future. |
[Close Table] |
LPDIRECT3DVERTEXBUFFER9 ppVertexBuffer : 오 열라 긴 이름이다. 우리가 만드는 vertex buffer interface에 대한 포인터란다. 이 곳에 빈 포인터 변수를 넣어주면 이 함수가 알아서 리턴시에 그 포인터 주소의 값에다가 값을 대입하나 부다.
HANDLE* pSharedHandle : 이건 걍 NULL이란다... 헐... Mic Soft가 그렇게 쓰라고 했단다. 나중에 사용할 인자인가 보다.
자 이제 이것을 선언하는 방법을 보자.
d3ddev->CreateVertexBuffer(3*sizeof(CUSTOMVERTEX),
0,
CUSTOMFVF,
D3DPOOL_MANAGED,
&t_buffer,
NULL);
포인터 변수를 선언해주고 인자로 주는 것외에는 위에서 본 그대로 이네. 자 이제 vertex buffer를 맹글었으니 이제 여기다 vertex들을 넣어줘야 하지 않겠나?
이것은 간단하게 memcpy()를 이용한다는데 상당히 충격적이다. -_-; 왜 memcpy()인가?
너무 단순한 함수를 쓰는것이 맘에 걸리기는 하지만, 어쨌든 그렇단다. 하지만 중요한 것이있으니 바로 buffer에 접근 시에는 먼저 locking을 해야 한다는 사실.
우선 locking을 해야 하는 이유로, 다중 접속하는 프로세스일 경우 당연히 consistence 문제가 생긴다는 것과 Viedo RAM에게 그 영역은 터치 하지 말라고 경고하는 목적도 있다.
잠그기 위해서는 Lock 함수를 사용하는데 다음과 같다.
UINT SizeToLock,
VOID** ppbData,
DWORD Flags);
UINT OffsetToLock, UINT SizeToLock : 만약 특별한 부분만 locking 하고 싶다면 이 것을 셋팅 해주면 된다. 말그대로 offset과 얼마나 locking 할지 사이즈를 알려주는 인자.
VOID** ppbData : 이 인자는 C++를 잘모르면 상당히 겁나는 것 처럼 보이지만 사실 이중 포인터 게다가 void 형을 쓰는 이유는 두번을 거치면 다양한 작업이 가능하기 때문이고 void를 주는 이유는 우리가 사용하고자 하는 타입이 하나로 고정된 것이 아닌 구조체이기 때문일 것이다. (FVF 니까)
DWORD Flags : 이것은 고급 플래그 설정 인자인데 튜토리얼에서는 다루지 않는다. They basically provide special ways to handle the locked memory. If you are truly interested, they can be researched in the DirectX documentation. For now, we will just set it to 0.
자 이제 어떻게 쓰는지 함 보자.
t_buffer->Lock(0, 0, (void**)&pVoid, 0); // locks t_buffer, the buffer we made earlier
우선 void * 형 변수 선언후 lock 함수를 호출하는데 이 다음은 memcpy를 이용해 vertex들을 직접 복사하는 과정이다.
그 다음은 해제다. 해제는 쉽다. 걍 아래와 같이 해주면 끝~
자 이모든 것들을 하나로 묶에서 함수에 담으니 바로 init_graphic()라는 함수다.
{
// create three vertices using the CUSTOMVERTEX struct built earlier
CUSTOMVERTEX t_vert[] =
{
{ 320.0f, 50.0f, 0.5f, 1.0f, D3DCOLOR_XRGB(0, 0, 255), },
{ 520.0f, 400.0f, 0.5f, 1.0f, D3DCOLOR_XRGB(0, 255, 0), },
{ 120.0f, 400.0f, 0.5f, 1.0f, D3DCOLOR_XRGB(255, 0, 0), },
};
// create the vertex and store the pointer into t_buffer, which is created globally
d3ddev->CreateVertexBuffer(3*sizeof(CUSTOMVERTEX),
0,
CUSTOMFVF,
D3DPOOL_MANAGED,
&t_buffer,
NULL);
VOID* pVoid; // the void pointer
t_buffer->Lock(0, 0, (void**)&pVoid, 0); // lock the vertex buffer
memcpy(pVoid, t_vert, sizeof(t_vert)); // copy the vertices to the locked buffer
t_buffer->Unlock(); // unlock the vertex buffer
return;
}
헐헐 모르고 보면 상당히 어려워 보이지만 알고 보면 참 쉽다.
3. Drawing the Primitive
화면에 출력하기에 앞서 몇가지 간단한 것을 해줘야 하는데, 살펴보면
SetFVF()
이 함수는 D3D에게 어떤 FVF 코드를 현재 사용할것인지를 알려 준다. 사용자는 물론 여러가지 형태의 vertex를 선택적으로 사용할 수 있으니 반드시 그리기 전에 무엇을 사용하는지 알려줘야 하겠다.
SetStreamSource()
그 다음 함수가 바로 요놈인데 D3D에게 어떤 vertex buffer를 그리는데 사용할지 알려 주는 함수다.
LPDIRECT3DVERTEXBUFFER9 pStreamData,
UINT OffsetInBytes,
UINT Stride);
첫번째 인자는 스트림 소스의 번호인데 이것은 차차 공부할 것이고 지금은 일단 0으로 지정한다. 왜냐면 한개의 vertex buffer 만을 사용하기 때문이란다.
두번째는 vertex buffer로의 포인터이고, 세번째는 시작할 바이트 오프셋인데 당연히 우리는 0을 쓸것이고 네번째는 각각의 vertex의 크기이니 당연히 sizeof(CUSTOMVETEX)가 되겠따.
DrawPrimitive()
고 다음이 이놈인가본데, 지금까지 우리는 D3D에게 어떤 종류의 vertices를 우리가 사용하고 있으며 어디서 그것들을 얻을수 있는지 말해주었다. 이제 필요한 것은 그리라고 명령하는 것이다. 요 함수가 바로 선택된 primitives를 화면에 그리라는 명령이다.
UINT StartVertex,
UINT PrimitiveCount);
첫번째 인자가 어떤 타입의 primitive를 사용하는지 알려주는 플래그 이다.
[Table 4.3 - D3DPRIMITIVETYPE Values]
Value | Description |
---|---|
D3DPT_POINTLIST | Shows a series of points. |
D3DPT_LINELIST | Shows a series of separated lines. |
D3DPT_LINESTRIP | Shows a series of connected lines. |
D3DPT_TRIANGLELIST | Shows a series of separated triangles. |
D3DPT_TRIANGLESTRIP | Shows a series of connected triangles. |
D3DPT_TRIANGLEFAN | Shows a series of triangles with one shared corner. |
[Close Table] |
이것은 Lesson 3에서 배운것들이네요. :)
두번째 인자는 몇번째 vertex부터 화면에 그릴까 인데 사용자가 원하면 중간에서 부터 뽑아내서 그릴수 있다. 하지만 우리는 0에서부터 시작해서 다 그릴거라는것.
세번째 인자는 우리가 그리고자 하는 vertex 숫자이다. 포인트를 이용할 경우 3개 라인을 이용할 경우도 3개인데 우리가 삼각형 형태를 사용하면 1이된다.
자 이제 이것들을 다 모아서 render_frame() 함수에 다 넣어보자.
void render_frame(void)
{
d3ddev->Clear(0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0, 0, 0), 1.0f, 0);
d3ddev->BeginScene();
// select which vertex format we are using
d3ddev->SetFVF(CUSTOMFVF);
// select the vertex buffer to display
d3ddev->SetStreamSource(0, t_buffer, 0, sizeof(CUSTOMVERTEX));
// copy the vertex buffer to the back buffer
d3ddev->DrawPrimitive(D3DPT_TRIANGLELIST, 0, 1);
d3ddev->EndScene();
d3ddev->Present(NULL, NULL, NULL, NULL);
return;
}
자 모르는 놈 몇개가 있기는 한데 일단 다른 중요한 것들부터 보자.
4. Releasing Vertex Buffers
바로 해제다. 프로그램 종료 전에 반드시 해제해야 한다.
void cleanD3D(void)
{
t_buffer->Release(); // close and release the vertex buffer
d3ddev->Release(); // close and release the 3D device
d3d->Release(); // close and release Direct3D
return;
}
5. Finished Program
자, 전체 코드를 보면 되는데, 실행해보니 잘된다. :)
#include <windows.h>
#include <windowsx.h>
#include <d3d9.h>
// define the screen resolution and keyboard macros
#define SCREEN_WIDTH 640
#define SCREEN_HEIGHT 480
#define KEY_DOWN(vk_code) ((GetAsyncKeyState(vk_code) & 0x8000) ? 1 : 0)
#define KEY_UP(vk_code) ((GetAsyncKeyState(vk_code) & 0x8000) ? 0 : 1)
// include the Direct3D Library file
#pragma comment (lib, "d3d9.lib")
// global declarations
LPDIRECT3D9 d3d; // the pointer to our Direct3D interface
LPDIRECT3DDEVICE9 d3ddev; // the pointer to the device class
LPDIRECT3DVERTEXBUFFER9 t_buffer = NULL; // the pointer to the vertex buffer
// function prototypes
void initD3D(HWND hWnd); // sets up and initializes Direct3D
void render_frame(void); // renders a single frame
void cleanD3D(void); // closes Direct3D and releases memory
void init_graphics(void); // 3D declarations
struct CUSTOMVERTEX {FLOAT X, Y, Z, RHW; DWORD COLOR;};
#define CUSTOMFVF (D3DFVF_XYZRHW | D3DFVF_DIFFUSE)
// the WindowProc function prototype
LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
// the entry point for any Windows program
int WINAPI WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
HWND hWnd;
WNDCLASSEX wc;
ZeroMemory(&wc, sizeof(WNDCLASSEX));
wc.cbSize = sizeof(WNDCLASSEX);
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = (WNDPROC)WindowProc;
wc.hInstance = hInstance;
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
// wc.hbrBackground = (HBRUSH)COLOR_WINDOW; // not needed any more
wc.lpszClassName = L"WindowClass";
RegisterClassEx(&wc);
hWnd = CreateWindowEx(NULL,
L"WindowClass",
L"Our Direct3D Program",
WS_EX_TOPMOST | WS_POPUP, // fullscreen values
0, 0, // the starting x and y positions should be 0
SCREEN_WIDTH, SCREEN_HEIGHT, // set the window to 640 x 480
NULL,
NULL,
hInstance,
NULL);
ShowWindow(hWnd, nCmdShow);
// set up and initialize Direct3D
initD3D(hWnd);
// enter the main loop:
MSG msg;
while(TRUE)
{
DWORD starting_point = GetTickCount();
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
if (msg.message == WM_QUIT)
break;
TranslateMessage(&msg);
DispatchMessage(&msg);
}
render_frame();
// check the 'escape' key
if(KEY_DOWN(VK_ESCAPE))
PostMessage(hWnd, WM_DESTROY, 0, 0);
while ((GetTickCount() - starting_point) < 25);
}
// clean up DirectX and COM
cleanD3D();
return msg.wParam;
}
// this is the main message handler for the program
LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch(message)
{
case WM_DESTROY:
{
PostQuitMessage(0);
return 0;
} break;
}
return DefWindowProc (hWnd, message, wParam, lParam);
}
// this function initializes and prepares Direct3D for use
void initD3D(HWND hWnd)
{
d3d = Direct3DCreate9(D3D_SDK_VERSION);
D3DPRESENT_PARAMETERS d3dpp;
ZeroMemory(&d3dpp, sizeof(d3dpp));
d3dpp.Windowed = FALSE;
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
d3dpp.hDeviceWindow = hWnd;
d3dpp.BackBufferFormat = D3DFMT_X8R8G8B8;
d3dpp.BackBufferWidth = SCREEN_WIDTH;
d3dpp.BackBufferHeight = SCREEN_HEIGHT;
// create a device class using this information and the info from the d3dpp stuct
d3d->CreateDevice(D3DADAPTER_DEFAULT,
D3DDEVTYPE_HAL,
hWnd,
D3DCREATE_SOFTWARE_VERTEXPROCESSING,
&d3dpp,
&d3ddev);
init_graphics(); // call the function to initialize the triangle
return;
}
// this is the function used to render a single frame
void render_frame(void)
{
d3ddev->Clear(0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0, 0, 0), 1.0f, 0);
d3ddev->BeginScene();
// select which vertex format we are using
d3ddev->SetFVF(CUSTOMFVF);
// select the vertex buffer to display
d3ddev->SetStreamSource(0, t_buffer, 0, sizeof(CUSTOMVERTEX));
// copy the vertex buffer to the back buffer
d3ddev->DrawPrimitive(D3DPT_TRIANGLELIST, 0, 1);
d3ddev->EndScene();
d3ddev->Present(NULL, NULL, NULL, NULL);
return;
}
// this is the function that cleans up Direct3D and COM
void cleanD3D(void)
{
t_buffer->Release(); // close and release the vertex buffer
d3ddev->Release(); // close and release the 3D device
d3d->Release(); // close and release Direct3D
return;
}
// this is the function that puts the 3D models into video RAM
void init_graphics(void)
{
// create the vertices using the CUSTOMVERTEX struct
CUSTOMVERTEX t_vert[] =
{
{ 320.0f, 50.0f, 0.5f, 1.0f, D3DCOLOR_XRGB(0, 0, 255), },
{ 520.0f, 400.0f, 0.5f, 1.0f, D3DCOLOR_XRGB(0, 255, 0), },
{ 120.0f, 400.0f, 0.5f, 1.0f, D3DCOLOR_XRGB(255, 0, 0), },
};
// create a vertex buffer interface called t_buffer
d3ddev->CreateVertexBuffer(3*sizeof(CUSTOMVERTEX),
0,
CUSTOMFVF,
D3DPOOL_MANAGED,
&t_buffer,
NULL);
VOID* pVoid; // a void pointer
// lock t_buffer and load the vertices into it
t_buffer->Lock(0, 0, (void**)&pVoid, 0);
memcpy(pVoid, t_vert, sizeof(t_vert));
t_buffer->Unlock();
return;
}
결과화면은 다음과 같다.
Image 4.3 - The Drawn Triangle
'프로그래밍 > DirectX' 카테고리의 다른 글
[DX Tutorial] Lesson 6: Rendering Depth (0) | 2008.02.12 |
---|---|
[DX Tutorial] Lesson 5: Transforming Vertices (0) | 2008.02.07 |
[DX Tutorial] Lesson 3: An Overview of the Third Dimension (0) | 2008.02.06 |
[DX Tutorial] Lesson2: Going Fullscreen (0) | 2008.02.02 |
[DX Tutorial] The Basic Direct3D Program (0) | 2008.02.01 |