3차원 메시들이 단순히 화면에 떠있기만 해서는 별로 쓸모가 없다.

이들이 상호작용하여 움직일 수 있어야 비로소 진정한 3D가 아닌가? 허허허
애니메이션 기법에는 여러가지가 있지만, 이번에는 가장 필요한 기법만 확실하게 알아두자

일반적으로 애니메이션 기법에는 다섯개가 있다.

1. 정점 애니메이션
2. 계층적 애니메이션
3. 뼈대 애니메이션
4. 스키닝
5. 역운동학

의미가 중복되는것도 있고, 실제 애니메이션과는 상관없는 것들도 있찌만, 대체로 위의 다섯가지가 애니메이션 관련 개발시에 많이 마주치게 되는 것들이다.

애니메이션 기법 중 가장 흔하면서 가장 강력한 기법인 키프레임 애니메이션 기법에 대해서만 알아보자.




키프레임 애니메이션
키프레임 애니메이션이란 전체 애니메이션 중에서 중요한 몇개의 프레임에 애니메이션 키값을 등록하고, 나머지 값은 자동 생성하는 애니메이션 방식을 말한다.
여기서 프레임이란 애니메이션에서 출력될 한 장면, 한 장면을 가리킨다고 생각하면 된다.

예를들어보자

 

전체 6프레임짜리 애니메이션이다.

첫프레임은 (-10, 0, 0) 여섯째프레임은 (10, 0, 0)

그리고, 첫번째와 여섯번째 프레임에 키값이 등록되어있다고 되어있다.

키값 : 그 지점의 좌표값


그렇타면, 2, 3, 4, 5프레임은 어떻게 구성이 되어야 할까?

당연히 중간값은 그림처럼 선형보간되는 것이다.


이렇게 전체 애니메이션에서 중요한 몇개의 프레임에 애니메이터가 키값을 넣어두면 나머지는 프로그램이 생성하는 방식을 키프레임 애니메이션 방식이라고 한다.

중간 프레임을 생성하는 방법은 다양하며, 요고를 보간한다고 이야기한다. 인터폴레이션(interpoloation)

보간법에는 여러가지가 있으며, 가장 쉬운 방식은 사원수에서 밝힌 선형보간 방식이고, 이거말고도 뉴턴보간법, 스플라인 보간법 등 다양한 기법이 있어요


요 키프레임 문제를 선형보간법으로 풀면 아래와 같다

a * (1 - t) + b * t (단, 0 <= t <= 1)


t는 첫프레임을 0, 끝프레임을 1로 잡고 그 사이값들은 소수점대로 나눈 것

a는 첫프레임, b는 끝프레임


ㅇㅋ..


키 프레임 애니메이션의 핵심은 바로 보간이다

방금의 예제에서는 위치값만을 보간의 대상으로 삼았기 때문에 간단하게 선형보간으로 해결했지만, 게임 프로그래밍에서는 위치, 회전, 크기를 가장 중요한 3대 키값으로 보고, 이들에 대한 보간을 지원하는 것이 대부분이다.

크기는 선형보간으로 해결하지만 회전은 사원수를 사용하게 된다.


사원수가 보간에 사용되는 기법이 구면선형보간(spherical linear interpolation, slerp)이라고 했다.

이에 해당하는 함수는 D3DXQuaternionSlerp()로 다음과 같이 선언된다

D3DXQUATERNION *D3DXQuaternionSlerp(D3DXQUATERNION *pOut, CONST D3DXQUATERNION *pQ1, CONST D3DXQUATERNION *pQ2, FLOAT t);


Q1과 Q2사이를 구면선형보간 하는데, t값을 0~1 사이의 값으로 주면 우리가 원하는 결과를 얻을 수 있다.


그럼 실제 구현을 해보자



실제구현

이 예제에는 2개의 위치키와 2개의 회전키를 가지고 중간값을 보간하는 프로그램이다.




#include <d3d9.h>

#include <d3dx9.h>


LPDIRECT3D9 g_pD3D = NULL;

LPDIRECT3DDEVICE9 g_pd3dDevice = NULL;

LPDIRECT3DVERTEXBUFFER9 g_pVB = NULL;

LPDIRECT3DINDEXBUFFER9 g_pIB = NULL;


D3DXMATRIXA16 g_matTMParent; //부모의 TM

D3DXMATRIXA16 g_matRParent; //부모의 회전행렬


D3DXMATRIXA16 g_matTMChild; //자식의 TM

D3DXMATRIXA16 g_matRChild; //자식의 회전행렬


float g_fRot = 0.0f;


struct CUSTOMVERTEX

{

FLOAT x, y, z;

DWORD color;

};


#define D3DFVF_CUSTOMVERTEX (D3DFVF_XYZ|D3DFVF_DIFFUSE)


struct MYINDEX

{

WORD _0, _1, _2; // 일반적으로 인덱스는 16비트의 크기를 갖는다

};


D3DXVECTOR3 g_aniPos[2]; // 위치 키 값

D3DXQUATERNION g_aniRot[2]; // 회전 키 값


HRESULT InitD3D( HWND hWnd )

{

    /// 디바이스를 생성하기위한 D3D객체 생성

    if( NULL == ( g_pD3D = Direct3DCreate9( D3D_SDK_VERSION ) ) )

        return E_FAIL;


    D3DPRESENT_PARAMETERS d3dpp;                /// 디바이스 생성을 위한 구조체

    ZeroMemory( &d3dpp, sizeof(d3dpp) );        /// 반드시 ZeroMemory()함수로 미리 구조체를 깨끗이 지워야 한다.

    d3dpp.Windowed = TRUE;                      /// 창모드로 생성

    d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;   /// 가장 효율적인 SWAP효과

    d3dpp.BackBufferFormat = D3DFMT_UNKNOWN;    /// 현재 바탕화면 모드에 맞춰서 후면버퍼를 생성

d3dpp.AutoDepthStencilFormat = D3DFMT_D16; /// 복잡한 오브젝트를 그릴것이라 Z버퍼가 필요하다

d3dpp.EnableAutoDepthStencil = TRUE;


    /// 디바이스를 다음과 같은 설정으로 생성한다.

    if( FAILED( g_pD3D->CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd,

                                      D3DCREATE_SOFTWARE_VERTEXPROCESSING,

                                      &d3dpp, &g_pd3dDevice ) ) )

    {

        return E_FAIL;

    }


/// 컬링 기능을 끈다.

g_pd3dDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_CCW);

/// Z버퍼를 켠다

g_pd3dDevice->SetRenderState(D3DRS_ZENABLE, TRUE);


/// 정점에 색깔값이 없으므로 광원기능을 끈다

g_pd3dDevice->SetRenderState(D3DRS_LIGHTING, FALSE);


    return S_OK;

}


HRESULT InitVB()

{

CUSTOMVERTEX vertices[] = {

{ -1, 1, 1, 0xffff0000 }, // v0

{ 1, 1, 1, 0xff00ff00 }, // v1

{ 1, 1, -1, 0xff0000ff }, // v2

{ -1, 1, -1, 0xffffff00 }, // v3


{ -1, -1, 1, 0xff00ffff }, // v4

{ 1, -1, 1, 0xffff00ff }, // v5

{ 1, -1, -1, 0xff000000 }, // v6

{ -1, -1, -1, 0xffffffff }, // v7

};


if(FAILED(g_pd3dDevice->CreateVertexBuffer(8*sizeof(CUSTOMVERTEX), 0, D3DFVF_CUSTOMVERTEX,

D3DPOOL_DEFAULT, &g_pVB, NULL)))

{

return E_FAIL;

}


VOID* pVertices;

if(FAILED(g_pVB->Lock(0, sizeof(vertices), (void**)&pVertices, 0)))

return E_FAIL;

memcpy(pVertices, vertices, sizeof(vertices));

g_pVB->Unlock();


return S_OK;

}


HRESULT InitIB()

{

// 상자를 렌더링하기위해 12개의 면 선언

MYINDEX indices[] = 

{

{ 0, 1, 2 }, { 0, 2, 3 }, //윗면

{ 4, 6, 5 }, { 4, 7, 6 }, //아랫면

{ 0, 3, 7 }, { 0, 7, 4 }, //왼쪽면

{ 1, 5, 6 }, { 1, 6, 2 }, //오른쪽면

{ 3, 2, 6 }, { 3, 6, 7 }, //앞면

{ 0, 4, 5 }, { 0, 5, 1 } //뒷면

};


// 인덱스 버퍼 생성

// D3DFMT_INDEX16은 인덱스의 단위가 16비트 라는 것

// 우리는 MYINDEX구조체에서 WORD형으로 선언했으므로, 

// D3DFMT_INDEX16을 사용한다.


if(FAILED(g_pd3dDevice->CreateIndexBuffer(12 * sizeof(MYINDEX), 0, D3DFMT_INDEX16,

D3DPOOL_DEFAULT, &g_pIB, NULL)))

{

return E_FAIL;

}


// 인덱스 버퍼를 값으로 채운다

// 인덱스 버퍼의 Lock()함수를 호출하여 포인터를 얻어온다

VOID* pIndices;

if(FAILED(g_pIB->Lock(0, sizeof(indices), (void**)&pIndices, 0)))

return E_FAIL;


memcpy(pIndices, indices, sizeof(indices));

g_pIB->Unlock();


return S_OK;

}


void InitAnimation()

{

g_aniPos[0] = D3DXVECTOR3( 0, 0, 0 ); /// 위치 키(0,0,0)

g_aniPos[1] = D3DXVECTOR3( 5, 5, 5 ); /// 위치 키(5,5,5)


    FLOAT Yaw = D3DX_PI * 90.0f / 180.0f; /// Y축 90도 회전

    FLOAT Pitch = 0;

    FLOAT Roll = 0;

D3DXQuaternionRotationYawPitchRoll( &g_aniRot[0], Yaw, Pitch, Roll ); /// 사원수 키(Y축90도)


    Yaw = 0;

    Pitch = D3DX_PI * 90.0f / 180.0f; /// X축 90도 회전

    Roll = 0;

D3DXQuaternionRotationYawPitchRoll( &g_aniRot[1], Yaw, Pitch, Roll ); /// 사원수 키(X축90도)

}


float Linear(float v0, float v1, float t)

{

return v0 * (1.0f - t) + v1 * t;

// return v0 + t * (v1 - v0); // 똑같아

}

선형보간 함수

void SetupCamera()

{

/// 월드행렬 설정

    D3DXMATRIXA16 matWorld;

D3DXMatrixIdentity( &matWorld );

    g_pd3dDevice->SetTransform( D3DTS_WORLD, &matWorld );


    /// 뷰행렬을 설정

    D3DXVECTOR3 vEyePt( 0.0f, 10.0f,-20.0f );

    D3DXVECTOR3 vLookatPt( 0.0f, 0.0f, 0.0f );

    D3DXVECTOR3 vUpVec( 0.0f, 1.0f, 0.0f );

    D3DXMATRIXA16 matView;

    D3DXMatrixLookAtLH( &matView, &vEyePt, &vLookatPt, &vUpVec );

    g_pd3dDevice->SetTransform( D3DTS_VIEW, &matView );


    /// 프로젝션 행렬 설정

    D3DXMATRIXA16 matProj;

    D3DXMatrixPerspectiveFovLH( &matProj, D3DX_PI/4, 1.0f, 1.0f, 100.0f );

    g_pd3dDevice->SetTransform( D3DTS_PROJECTION, &matProj );

}


VOID Animate()

{

static float t = 0;

float x, y, z;

D3DXQUATERNION quat;


if(t > 1.0f) t = 0.0f;


// 위치값의 선형보간

x = Linear(g_aniPos[0].x, g_aniPos[1].x, t);

y = Linear(g_aniPos[0].y, g_aniPos[1].y, t);

z = Linear(g_aniPos[0].z, g_aniPos[1].z, t);

// 이동행렬을 구한다

D3DXMatrixTranslation(&g_matTMParent, x, y, z);

// 위의 네줄은 다음의 세줄로 바꿀 수 있다

// D3DXVECTOR3 v;

// D3DXVec3Lerp(&v, &g_aniPos[0], &g_aniPos[1], t);

// D3DXMatrixTranslation(&g_matTMParent, v.x, v.y, v.z);


// 회전값의 구면선형보간

D3DXQuaternionSlerp(&quat, &g_aniRot[0], &g_aniRot[1], t);

D3DXMatrixRotationQuaternion(&g_matRParent, &quat); // 사원수를 회전행렬값으로 변환

t += 0.005f;

// 자식 메시의 Z축 회전행렬

D3DXMatrixRotationZ(&g_matRChild, GetTickCount()/500.0f);

// 자식 메시는 원점으로부터 (3,3,3)거리에 있음

D3DXMatrixTranslation(&g_matTMChild, 3, 3, 3);

}

키값을 보간하여 애니메이션 행렬을 만든다.

위치키와 회전키를 보간하는 방법 잘 봐두자

주석부분은 D3DX 함수를 써서 더 간결하게 한거야


VOID CleanUp()

{

if(g_pVB != NULL)

g_pVB->Release();


if(g_pIB != NULL)

g_pIB->Release();


if(g_pd3dDevice != NULL)

g_pd3dDevice->Release();


if(g_pD3D != NULL)

g_pD3D->Release();

}


VOID DrawMesh(D3DXMATRIXA16* pMat)

{

g_pd3dDevice->SetTransform(D3DTS_WORLD, pMat);

g_pd3dDevice->SetStreamSource(0, g_pVB, 0, sizeof(CUSTOMVERTEX));

g_pd3dDevice->SetFVF(D3DFVF_CUSTOMVERTEX);

g_pd3dDevice->SetIndices(g_pIB);

g_pd3dDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, 0, 8, 0, 12);

}


HRESULT InitGeometry()

{

if(FAILED(InitVB())) return E_FAIL;

if(FAILED(InitIB())) return E_FAIL;


InitAnimation();


return S_OK;

}


VOID Render()

{

D3DXMATRIXA16 matWorld;

    /// 후면버퍼와 Z버퍼를 지운다

    g_pd3dDevice->Clear( 0, NULL, D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB(0,0,255), 1.0f, 0 );

    

Animate();

    /// 렌더링 시작

    if( SUCCEEDED( g_pd3dDevice->BeginScene() ) )

    {

matWorld = g_matRParent * g_matTMParent;

DrawMesh(&matWorld); // 부모상자 그리기


matWorld = g_matRChild * g_matTMChild * g_matRParent * g_matTMParent;

//matWorld = g_matRChild * g_matTMChild * matWorld; // 위와 같은 결과


DrawMesh(&matWorld); // 자식상자 그리기

g_pd3dDevice->EndScene();

}


    /// 후면버퍼를 보이는 화면으로!

    g_pd3dDevice->Present( NULL, NULL, NULL, NULL );

}


#define ROT_DELTA 0.1f

LRESULT WINAPI MsgProc( HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam )

{

    switch( msg )

    {

        case WM_DESTROY:

            CleanUp();

            PostQuitMessage( 0 );

            return 0;

case WM_KEYDOWN:

if(wParam == VK_LEFT) g_fRot -= ROT_DELTA;

if(wParam == VK_RIGHT) g_fRot += ROT_DELTA;

break;

    }


    return DefWindowProc( hWnd, msg, wParam, lParam );

}


INT WINAPI WinMain( HINSTANCE hInst, HINSTANCE, LPSTR, INT )

{

    /// 윈도우 클래스 등록

    WNDCLASSEX wc = { sizeof(WNDCLASSEX), CS_CLASSDC, MsgProc, 0L, 0L, 

                      GetModuleHandle(NULL), NULL, NULL, NULL, NULL,

                      "BasicFrame", NULL };

    RegisterClassEx( &wc );


    /// 윈도우 생성

    HWND hWnd = CreateWindow( "BasicFrame", "Hierarchy", 

                              WS_OVERLAPPEDWINDOW, 100, 100, 500, 500,

                              GetDesktopWindow(), NULL, wc.hInstance, NULL );


    /// Direct3D 초기화

    if( SUCCEEDED( InitD3D( hWnd ) ) )

    { 

/// 정점버퍼 초기화

if(SUCCEEDED(InitGeometry())){


SetupCamera();


/// 윈도우 출력

ShowWindow( hWnd, SW_SHOWDEFAULT );

UpdateWindow( hWnd );


/// 메시지 루프

MSG msg; 

ZeroMemory(&msg, sizeof(msg));


while(msg.message!=WM_QUIT)

{

if(PeekMessage(&msg, NULL, 0U, 0U, PM_REMOVE)){

TranslateMessage( &msg );

DispatchMessage( &msg );

}

else

Render();

}

}

    }


    /// 등록된 클래스 소거

    UnregisterClass( "D3D Tutorial", wc.hInstance );

    return 0;

}



 


'Graphics > DirectX' 카테고리의 다른 글

Direct X - 지형 처리 기법  (0) 2014.07.31
Direct X - 스키닝  (0) 2014.07.30
Direct X - 계층구조  (3) 2014.07.28
Direct X - 법선맵핑  (0) 2014.07.25
Direct 3D - 빌보드  (0) 2014.07.24
Posted by 긍정왕오킹