주저리
라이브러리 파트
이처럼, 프레임 계층구조를 하나 만들어 두고, N개의 애니메이션 컨트롤러를 준비해서, N개의 애니메이션을 독립적으로 동시에 표현 할수있는 아키텍쳐가 이 라이브러리의 핵심이다. "동시에 표현되는 N개의 애니메이션" 이라는 말을 잘 생각해 보자. 눈치 빠른 사람이라면, 이 구조를 N개의 캐릭터로 확장 시킬수 있다는 것을 알것이다. 캐릭터 마다 계층구조 프레임을 각자 독립적으로 가질 수도 있지만, 쓸데없이 메모리를 낭비 하는 셈이므로, 이런 컨셉으로 가는게 바람직하다.
| 클래스 | 하는일 |
|---|---|
| CMultiAnim |
- x 파일을 로드하여, 계층구조 메시를 캡슐화 한다. - fx 파일을 로드하여 셰이더 관련 프로세스를 처리한다. - CAnimInstance(애니메이션 메시 인스턴스)를 생성하고 관리한다. |
| CAnimInstance |
- 애니메이션 기본 인스턴스 유닛. - 고유한 애니메이션 컨트롤러를 가지고, 독립적으로 애니메이션을 수행한다. - 계층구조 프레임들을 돌면서 메시들을 렌더링 한다. (스키닝) |
| CMultiAnimAllocateHierarchy |
- 계층구조 메시를 생성, 릴리즈 한다. (x 파일 로딩중에 일어남) - 메시에 사용되는 텍스쳐를 생성한다. - 스키닝 정보를 가지고 메시의 뼈대 옵셋을 준비한다. - 스키닝을 위한 팔레트 크기를 계산한다. - 블렌딩 가중치와 뼈대 인덱스를 가지는 스킨 메시를 생성한다. |
| MultiAnimFrame |
- D3DXFRAME 확장판, 이 예제에서는 확장된것이 없다. - 트리 형태의 계층구조를 위해 자식 프레임과 형제 프레임을 포인트한다. - 노드 데이터로 변환 매트릭스와, 메시 컨테이너를 가지고 있다. |
| MultiAnimMC | - D3DXMESHCONTAINER 확장판. - 텍스쳐, 워킹메시, 뼈대옵셋, 뼈대포인터, 팔레트, 등등을 확장한다. |
[표 1. 라이브러리 파트의 클래스 & 스트럭쳐]
다음으로 서비스 코드인, 애플리케이션 파트를 살펴 보자.
애플리케이션 파트
예제에서 사용하는 "tiny_4anim.x" 파일에는 4개의 애니메이션셋이 있다. "Loiter", "Walk", "Jog", "Wave" 라는 이름을 가지는 셋이다. 이중 "Wave"는 사용되지 않는다. 애니메이션을 선택하고 컨트롤 하려면, 애니메이션 컨트롤러의 해당 애니메이션셋 인덱스를 알아야 하는데, CTiny::GetAnimIndex 함수가 준비되어 있다. 이 함수를 호출해서 각각의 셋 인덱스를 뽑아둔다(m_dwAnimIdxLoiter, m_dwAnimIdxWalk, m_dwAnimIdxJog). 이 인덱스로 애니메이션셋을 뽑아서 트랙에 올려놓고 두 트랙사이를 부드럽게 전환하는 과정이(예: 걷다가 뛰기) CTiny::SetMoveKey 함수에 준비되어 있다. 이 함수는 CTiny의 속도값(m_fSpeed)을 판단하여, 새로운 트랙에 걷거나(m_dwAnimIdxWalk), 뛰는(m_dwAnimIdxJog) 애니메이션셋을 올려놓고, 현재 재생중인 트랙을 비활성화 시킨후, 새로 만든 트랙을 현재 트랙으로 재설정한다. 이렇게 하면, 두 트랙 사이가 보간된다. 내부적으로 애니메이션 컨트롤러가 두 트랙 사이를 보간한 프레임 매트릭스를 생성한다.
CTiny::SetMoveKey 함수가 걷거나 뛰도록 다음 애니메이션셋을 설정하는 함수인 반면, 비슷한 함수인 CTiny::SetIdleKey는 멀뚱멀뚱 서서 빈둥거리는 애니메이션셋("Loiter") 으로 설정하는 함수이다. 코드 내용은 비슷하다. 단지 m_dwAnimIdxWalk, m_dwAnimIdxJog 대신에 m_dwAnimIdxLoiter 인덱스로 애니메이션 셋을 얻어내어 트랙에 올리는 것이 다를 뿐이다.
애니메이션을 업데이트하고 그리는 부분은 좀 뒤에서 천천히 코드를 보면서 분석해 볼 것이다.
| 클래스 | 하는일 |
|---|---|
| CTiny |
- 멤버로 가지고 있는 CAnimInstance를 이용해서 애니메이션을 수행한다. - 빈둥거리기, 걷기, 뛰기 트랙을 컨트롤한다. - 각 트랙이 바뀔때 부드럽게 보간해서 애니메이션한다. - N개의 CTiny 목록을 관리한다. (업데이트, 그리기) - 캐릭터간의 충돌을 처리한다. - 애니메이션 콜백 핸들러를 관리한다. |
| CBHandlerTiny |
- ID3DXAnimationCallbackHandler 확장판, 발자국 소리를 플레이한다. - 애니메이션 컨트롤러에 의해 호출된다. |
| CallbackDataTiny | - CBHandlerTiny의 콜백함수로 전달할 데이터. - 애니메이션 컨트롤러에 이 데이터를 추가하면, 콜백시 전달된다. - 이 값을 기반으로 발자국 소리를 조절한다. |
[표 2. 애플리케이션 파트의 클래스 & 스트럭쳐]
클래스 다이어그램
CMultiAnimAllocateHierarchy는 멤버로 CMultiAnim을 포인트 하고 있는데, 메시 컨테이너를 만들때(CMultiAnimAllocateHierarchy::CreateMeshContainer), 스키닝 팔레트 테이블 정보를 얻거나 올바른 팔레트 정보를 재설정 하기 위해 이 멤버를 사용한다. 어쨌든, 이렇게 생성된 프레임 계층구조의 루트 노드(MultiAnimFrame)는 CMultiAnim::m_pFrameRoot로 포인트 되어 사용할 수 있게된다. 이 루트 노드는 D3DXLoadMeshHierarchyFromX 함수로 얻어내고 있다.
CMultiAnim 클래스는 CAnimInstance를 생성하고 인스턴스 목록을 배열(vector<CAnimInstance> m_v_pAnimInstances)로 가지고 관리한다. 전체 애니메이션을 업데이트하거나, 그릴 때, 이 배열을 사용한다. CAnimInstance 에서도 역시 CMultiAnim 인스턴스를 포인트 하고 있으므로 계층구조 프레임을 참조하여 자신의 애니메이션을 그릴수 있다. 애니메이션은 CAnimInstance 클래스 인스턴스 마다 독립적으로 존재하는 애니메이션 컨트롤러(m_pAC)에 의해서 수행된다.
CTiny 클래스는 캐릭터 오브젝트를 대변하는 클래스이다. CAnimInstance를 생성하고 멤버(m_pAI)로 가지고 있으므로, 캐릭터 독립적으로 애니메이션이 가능하다. 생성하는 캐릭터들은 외부 글로벌 배열인 vector<CTiny*> g_v_pCharacters 에 저장되고, 이 배열은 CTiny::m_pv_pChars 에서 포인트 하므로, CTiny 내부에서 캐릭터 목록의 접근이 가능하다. 예를들어, 캐릭터가 다른 캐릭터들과 충돌했는지를 테스트 할때, 유용하게 사용될 수 있겠다.
[그림 1. 클래스 다이어그램]
프레임 계층구조 만들기
CMultiAnimAllocateHierarchy AH;
AH.SetMA(&g_MultiAnim);
g_MultiAnim.SetTechnique("Skinning20");
CMultiAnim 클래스를 준비하는 CMultiAnim::Setup 메소드로 들어가 보자. sFxFile(fx 파일)을 매개변수로 받아서 셰이더에 접근할수 있는 이펙트 오브젝트(m_pEffect)를 만들어낸다. 이때, 지원되는 셰이더 버젼을 기반으로 매트릭스 팔레트 테이블의 크기를 결정하는데. skin.vsh 파일에 기본값으로, #define MATRIX_PALETTE_SIZE_DEFAULT 26 로 명시되어 있지만, 버텍스 셰이더 버전이 1.1 이상이면 35개로 재설정 한다는 것이다.
D3DXMACRO mac[2] =
{
{"MATRIX_PALETTE_SIZE_DEFAULT", "35"},
{NULL, NULL}
};
D3DCAPS9 caps;
D3DXMACRO* pmac = NULL;
m_pDevice->GetDeviceCaps(&caps);
if (caps.VertexShaderVersion > D3DVS_VERSION(1, 1))
pmac = mac;
DWORD dwShaderFlags = D3DXFX_NOT_CLONEABLE;
또한, sXFile(x 파일)을 매개변수로 받아서 메시를 로드하고, 프레임 계층구조를 만들기 위해 D3DXLoadMeshHierarchyFromX API를 호출한다. 이때 4번째 매개변수로 pAH(CMultiAnimAllocateHierarchy)를 넘겨주어 계층구조가 만들어질때 콜백하도록 한다. 트리형 프레임 계층구조가 다 만들어지면, 6번째 매개변수m_pFrameRoot(MultiAnimFrame)로 루트 프레임 노드를 반환한다. 우리는 이 노드로 트리를 탐색 할 것이다.
D3DXLoadMeshHierarchyFromX(
wszPath, 0, m_pDevice, pAH, pLUD, (LPD3DXFRAME*)&m_pFrameRoot, &m_pAC);
여기까지 왔는데, CMultiAnimAllocateHierarchy 콜백을 살펴보지 않으면 섭하다. 들어가보자. 이 클래스는 좀전에 말한대로 D3DXLoadMeshHierarchyFromX API가 수행되면서 계층구조 프레임 노드를 생성할때 콜백되는 클래스이다. 이때, 우리의 입맛에 맛는 커스텀 프레임이나 메시 컨테이너를 생성 하는 것으로 기능을 확장 시킬수 있다. 그 입맛에 맛는 커스텀 프레임이 MultiAnimFrame 이고, 메시 컨테이너가 MultiAnimMC 이다. 먼저 프레임이 생성될때 호출되는 콜백 함수인 CMultiAnimAllocateHierarchy::CreateFrame 함수를 보자. 아래 코드를 보면, 크게 하는것 없이 MultiAnimFrame 인스턴스를 하나 생성하고, 프레임 이름을 설정한후 출력 매개변수인 ppFrame으로 포인트 하고 있음을 볼 수 있다. 이제 프레임 노드로 LPD3DXFRAME 대신에 우리가 새로 정의한 MultiAnimFrame이 사용될 것이다. 그런데, 뭐하나 확장된게 없다. 그냥 일 예라고 보면 될듯.
HRESULT CMultiAnimAllocateHierarchy::CreateFrame(THIS_ LPCSTR Name, LPD3DXFRAME* ppFrame)
{
assert(m_pMA);
if (pFrame == NULL)
{
DestroyFrame(pFrame);
return E_OUTOFMEMORY;
}
{
pFrame->Name = (CHAR*)HeapCopy((CHAR*)Name);
}
else
{
pFrame->Name = (CHAR*)HeapCopy("<no_name>");
}
{
DestroyFrame(pFrame);
return E_OUTOFMEMORY;
}
return S_OK;
}
사실 CMultiAnimAllocateHierarchy 콜백 클래스에서 가장 중요한 부분은 메시 컨테이너를 생성하는 부분이다. CMultiAnimAllocateHierarchy::CreateMeshContainer 함수인데, 코드양도 꽤 된다. 일단 보자. 프레임과 마찬가지로 역시나 MultiAnimMC의 인스턴스를 하나 생성하고 ppNewMeshContainer로 포인트 하고 있다. 그러면 이제부터 메시 컨테이너로 D3DXMESHCONTAINER가 아닌 우리의 커스텀 MultiAnimMC 가 사용될 것이다. 메시 컨테이너는 프레임이 멤버로 가지고 있으므로(D3DXFRAME::m_pMeshContainer) 나중에 이값을 참조해서 접근하면 되겠다. 아무튼, 다음으로 이것저것 메시 컨테이너의 정보를 설정하는데, 헉! 스킨 정보가 없는 스태틱 메시를 처리하지 않는다. 아무리 예제이지만, 이건 좀 바꿔야 겠다. 나중에...
// 스태틱 메시는 처리하지 않는다. 우린 스킨드 메시에만 관심이 있다.
if (pSkinInfo == NULL)
{
hr = S_OK;
goto e_Exit;
}
메시 이름, 타입, 메시데이터, 재질, 텍스쳐, 버텍스인접정보, 등등을 새로 생성한 메시 컨테이너에 복사한다. 텍스쳐의 경우 이 시점에 로드가 된다. m_apTextures 배열 멤버에 재질 갯수만큼 로드되니 나중에 렌더링 할때 재질별로 DrawSubset 하면 되겠다.
{
CopyMemory(pMC->pMaterials, pMaterials, NumMaterials * sizeof(D3DXMATERIAL));
for (DWORD i = 0; i < NumMaterials; ++i)
{
if (pMC->pMaterials[i].pTextureFilename)
{
WCHAR sNewPath [MAX_PATH];
WCHAR wszTexName [MAX_PATH];
if (MultiByteToWideChar(
CP_ACP, 0, pMC->pMaterials[i].pTextureFilename, -1, wszTexName, MAX_PATH) == 0)
{
pMC->m_apTextures[ i ] = NULL;
}
else if (SUCCEEDED(DXUTFindDXSDKMediaFileCch(sNewPath, MAX_PATH, wszTexName)))
{
if (FAILED(D3DXCreateTextureFromFile(
m_pMA->m_pDevice, sNewPath, &pMC->m_apTextures[i])))
pMC->m_apTextures[ i ] = NULL;
}
else
pMC->m_apTextures[ i ] = NULL;
}
else
pMC->m_apTextures[ i ] = NULL;
}
}
아래 코드를 제대로 이하하기 위해서는 [뼈대 옵셋 매트릭스], [뼈대 매트릭스], [팔레트 테이블] 등의 개념부터 이해 해야한다. 개념이 없다면, Skinned Mesh 예제를 먼저 읽어보는게 좋을듯 하다. 어쨌든, 다음 작업은, 스키닝 정보를 메시 컨테이너에 복사 하고, 스킨 정보에서 뼈대 옵셋 관련 정보를 추출한 후 m_amxBoneOffsets 멤버에 저장해 둔다. 이 매트릭스는 나중에 셰이더에게 보낼 팔레트를 계산할때 사용될 것이다.
pMC->pSkinInfo = pSkinInfo;
pSkinInfo->AddRef();
for (DWORD i = 0; i < pSkinInfo->GetNumBones(); ++i)
pMC->m_amxBoneOffsets[i] = *(D3DXMATRIX*)pSkinInfo->GetBoneOffsetMatrix(i);
UINT iPaletteSize = 0;
m_pMA->m_pEffect->GetInt("MATRIX_PALETTE_SIZE", (INT*)&iPaletteSize);
pMC->m_dwNumPaletteEntries = min(iPaletteSize, pMC->pSkinInfo->GetNumBones());
pSkinInfo 로부터 뼈대 옵셋 매트릭스를 받아낸 후에, 애니메이션과 렌더링을 위한 계층구조 메시를 셋업하기위해 ID3DXSkinInfo::ConvertToIndexedBlendedMesh 메소드를 호출한다. 이 메소드는 첫번째 인자로 넘겨받은 원본 메시(pMC->MeshData.pMesh)를 스키닝 정보를 기반으로 가중치와 인덱스를 가지는 스킨 메시를 만들어 낸다. 또한 부가적으로 이것저것 정보들을 출력하는데, 생소하게도, 뼈대 컴비네이션(LPD3DXBONECOMBINATION) 이라는 놈이 보인다. 뭐, 별건 아니고 메시 속성별로 뼈대 인덱스를 관리 하는 자료구조 정도로 이해하면 되겠다. 뼈대 매트릭스 인덱스 = 뼈대 컴비네이션[속성별].BoneId[팔레트엔트리] 이렇게 표현 하면 될까? 중요한 것은 이러한 구조가 왜 필요한가를 이해 하는 것이다. 하나의 메시를 속성별로 쪼개야 하는 경우가 언제 있을까? 물론, 재질이 다른경우가 있겠지만, 그런 당연한것 말고, 우리는 스키닝을 위해 버텍스 셰이더를 사용할 것이다. 그런데 이 버텍스 셰이더는 최대 상수 갯수가 제한 되어 있으므로 무작정 팔레트 테이블의 크기를 크게 할 수 없다. 즉 한 메시에 사용되는 총 뼈대 테이블이 팔레트 테이블 크기보다 크다면, 속성 그룹으로 뼈대 테이블을 나누고 나눈 갯수만큼 DrawSubset으로 렌더를 수행해야 한다.
이 예제에서는 셰이더 기반의 인덱스 스키닝을 위한 메시를 생성하고 있는데, 다양한 방식의 스키닝 기법이 존재 하므로 Skinned Mesh 예제를 참조하면 도움이 될것이다.
pMC->pSkinInfo->ConvertToIndexedBlendedMesh(
pMC->MeshData.pMesh, // [입력] 원본 메시
D3DXMESH_MANAGED | D3DXMESHOPT_VERTEXCACHE, // [입력] 옵션
pMC->m_dwNumPaletteEntries, // [입력] 뼈대 매트릭스 팔레트 테이블 갯수
pMC->pAdjacency, // [입력] 버텍스 인접 정보
NULL, // [출력] 버텍스 인접 정보
NULL, // [출력] 면(리맵) 정보
NULL, // [출력] 버텍스(리맵) 정보
&pMC->m_dwMaxNumFaceInfls, // [출력] 한 버텍스에 영향을 주는 뼈대의 최대 갯수
&pMC->m_dwNumAttrGroups, // [출력] 뼈대 컴비네이션에 있는 속성 그룹 갯수
&pMC->m_pBufBoneCombos, // [출력] 뼈대 컴비네이션
&pMC->m_pWorkingMesh); // [출력] 스킨 메시
CMultiAnim::m_amxWorkingPalette 는 CAnimationInstance에서 메시 프레임을 그릴때 작업용 공용 버퍼 테이블로 사용할 놈이다. 그런데 이 크기가 스키닝 작업을 처리하기에 너무 작으면 안되므로, 여기서 그 크기를 업데이트 해 준다.
if (m_pMA->m_dwWorkingPaletteSize < pMC->m_dwNumPaletteEntries)
{
if (m_pMA->m_amxWorkingPalette)
delete [] m_pMA->m_amxWorkingPalette;
m_pMA->m_dwWorkingPaletteSize = pMC->m_dwNumPaletteEntries;
m_pMA->m_amxWorkingPalette = new D3DXMATRIX [m_pMA->m_dwWorkingPaletteSize];
if (m_pMA->m_amxWorkingPalette == NULL)
{
m_pMA->m_dwWorkingPaletteSize = 0;
hr = E_OUTOFMEMORY;
goto e_Exit;
}
}
그외에, 메시를 원하는 포맷으로 컨버팅 하는 과정과, GeForce3 를 위한 커스터 마이징, 등의 과정이 남아있는데, 코드가 직관적이다. 사실 맥스에서 메시 소스를 만들때, 애플리케이션에서 원하는 포맷으로 제작하면, 메시 포맷을 컨버팅 하는 과정(CloneMeshFVF)을 거치지 않으므로 속도가 조금 향상 될 것으로 보인다. 자, 이제 CMultiAnimAllocateHierarchy 콜백에서 일단 나오자. 지겹다.
살펴본대로 D3DXLoadMeshHierarchyFromX 호출로 트리형태의 계층구조 프레임이 만들어졌다. 이제 우리는 m_pFrameRoot 라는 루트노드를 가지게 되었다. 이 루트노드를 가지고 트리를 순회하면서 작업할 꺼리가 하나 생겼다. 바로, 모든 계층구조 메시의 뼈대 테이블을 셋업하는 과정이다. CMultiAnim::SetupBonePtrs 함수가 트리를 순회 하면서 pSkinInfo가 있는 모든 메시 컨테이너에게 MultiAnimMC::SetupBonePtrs(m_pFrameRoot)를 호출하는 놈이다. CMultiAnimAllocateHierarchy 콜백에서 수행하지 못하고, 지금 이 작업을 해야 하는 이유는, 메시컨테이너 생성 콜백시 뼈대 매트릭스가 있는 프레임 노드가 아직 만들어지지 않았기 때문이다. 프레임은 메시뿐 아니라 뼈대 프레임도 존재하는 것이다.
HRESULT MultiAnimMC::SetupBonePtrs(D3DXFRAME* pFrameRoot)
{
if (pSkinInfo)
{
if (m_apmxBonePointers)
delete [] m_apmxBonePointers;
if (m_apmxBonePointers == NULL)
return E_OUTOFMEMORY;
{
MultiAnimFrame* pFrame =
(MultiAnimFrame*) D3DXFrameFind(pFrameRoot, pSkinInfo->GetBoneName(i));
if (pFrame == NULL)
return E_FAIL;
}
}
}
오케! 이제 프레임 계층구조가 완벽히 준비되었다. 이 구조를 가지고 애니메이션을 업데이트 하거나 그리면 되는 것이다. 마지막으로 정리할겸, "tiny_4anim.x" 프레임 계층구조가 어떠한 모양으로 구성되어 있는지 한번 보자. 참고로 손가락(finger) 노드들이 있지만, 복잡해 보여서 그리진 않았다.
메시 데이터를 가지고 있는 노드는 단 한개 밖에 없다. 그외의 것들은 모두 뼈대로써 자체 변환 매트릭스를 가지고 있다. 메시의 한 정점 위치를 결정 짓는것은 바로 이들 뼈대 매트릭스와 가중치 값인 것이다.
캐릭터 인스턴스 만들기
if (g_v_pCharacters.size() == 0)
{
CTiny* pTiny = new CTiny;
if (pTiny == NULL)
return E_OUTOFMEMORY;
pTiny->SetSounds(g_bPlaySounds);
}
긴말 할것 없이, CTiny::Setup 함수속으로 들어가 보자. 일단, 애니메이션 인스턴스(CAnimInstance)를 생성하기 위해 CMultiAnim::CreateNewInstance 함수를 호출하고, 애니메이션 인스턴스를 멤버(m_pAI)에 저장해 둔다.
m_pMA->CreateNewInstance(&m_dwMultiAnimIdx);
m_pAI = m_pMA->GetInstance(m_dwMultiAnimIdx);
CMultiAnim 클래스는 애니메이션 인스턴스를 생성한 후, 자체 관리 배열에 등록해둔다. CMultiAnim::CreateNewInstance 메소드가 그러한 처리를 담당한다.
HRESULT CMultiAnim::CreateNewInstance(DWORD* pdwNewIdx)
{
CAnimInstance* pAI;
HRESULT hr = CreateInstance(&pAI);
if (FAILED(hr))
goto e_Exit;
{
m_v_pAnimInstances.push_back(pAI);
}
catch (...)
{
hr = E_OUTOFMEMORY;
goto e_Exit;
}
}
애니메이션은 캐릭터 독립적으로 수행 되어야 하므로, 애니메이션 인스턴스가 가지고 있는 애니메이션 컨트롤러는 복제 생성되어야 한다. CMultiAnim::CreateInstace 함수중 아래 코드의 CloneAnimationController가 호출되는 부분을 유심히 살펴보자.
HRESULT CMultiAnim::CreateInstance(CAnimInstance** ppAnimInstance)
{
*ppAnimInstance = NULL;
HRESULT hr;
CAnimInstance* pAI = NULL;
m_pAC->GetMaxNumAnimationOutputs(),
m_pAC->GetMaxNumAnimationSets(),
m_pAC->GetMaxNumTracks(),
m_pAC->GetMaxNumEvents(),
&pNewAC);
if (SUCCEEDED(hr))
{
pAI = new CAnimInstance(this);
if (pAI == NULL)
{
hr = E_OUTOFMEMORY;
goto e_Exit;
}
if (FAILED(hr))
goto e_Exit;
}
{
if (pAI)
delete pAI;
pNewAC->Release();
}
}
이렇게 생성된 애니메이션 컨트롤러는 CAnimInstance::Setup 메소드를 통해 멤버(m_pAC)로 저장되고, 컨트롤러의 트랙(2개) 상태를 리셋하게 된다.
HRESULT CAnimInstance::Setup(LPD3DXANIMATIONCONTROLLER pAC)
{
assert(pAC != NULL);
for (i = 0; i < dwTracks; ++i)
m_pAC->SetTrackEnable(i, FALSE);
}
이것으로, 캐릭터가 고유한 애니메이션 컨트롤러를 가지고 생성 되었다. 자, 다시 CTiny::Setup 함수로 돌아가 보자. 다음으로 캐릭터가 다른 캐릭터와 중복된 위치에 서지 않도록 시작위치를 찾아서 설정한다. 여기서는 1000번의 시도를 해서 중복되지 않는 위치를 찾고 있다.
bool bBlocked = true;
DWORD dwAttempts;
for (dwAttempts = 0; dwAttempts < 1000 && bBlocked; ++dwAttempts)
{
ChooseNewLocation(&m_vPos);
bBlocked = IsBlockedByCharacter(&m_vPos);
}
애니메이션셋을 핸들링 하기 위해서 애니메이션셋 인덱스가 필요하다. 아래의 코드는 각 애니메이션셋의 인덱스를 멤버로 저장해 둔다. 자세한 설명은 위 애플리케이션 파트 에서 소개 했으므로 코드만 보고 넘어가자.
m_dwAnimIdxLoiter = GetAnimIndex("Loiter");
m_dwAnimIdxWalk = GetAnimIndex("Walk");
m_dwAnimIdxJog = GetAnimIndex("Jog");
발자국 소리를 내기 위해서 콜백을 설정하는 부분이 나오는데, 내 관심 대상이 아니므로 패스한다. 코드가 직관적이므로 그냥 보면 되겠다. 다음 부분을 보자. 캐릭터의 기본 월드 TM(m_mxOrientation)중 스케일 * 회전 매트릭스를 계산하는 부분이다. 먼저 스케일 매트릭스를 구해서 적절한 크기를 셋팅하고, x축으로 -90도 회전한다. 그다음 y축으로 90도 회전시킨다. 90도 회전 시키는 이유는, 쉽게 말해서, y축으로 세우고, x축으로 바라 보도록 기본 위치를 설정하기 위함이다. (맞나 -_-?)
D3DXMATRIX mx;
float fScale = 1.f / m_pMA->GetBoundingRadius() / 7.f;
D3DXMatrixScaling(&mx, fScale, fScale, fScale);
m_mxOrientation = mx;
D3DXMatrixRotationX(&mx, -D3DX_PI / 2.0f);
D3DXMatrixMultiply(&m_mxOrientation, &m_mxOrientation, &mx);
D3DXMatrixRotationY(&mx, D3DX_PI / 2.0f);
D3DXMatrixMultiply(&m_mxOrientation, &m_mxOrientation, &mx);
SetSeekingState();
ComputeFacingTarget();
위 코드중 SetSeekingState 함수는 캐릭터가 다다를 목적지 위치(m_vPosTarget)를 설정하고, 걷기 또는 달리기중에 한 애니메이션셋을 랜덤하게 선택하는 부분이다. 목적지 위치 찾는 함수인 SetNewTarget은 캐릭터의 초기 위치를 잡는 방식과 동일하다. 단, 캐릭터의 위치는 m_vPos 멤버에, 목적지 위치는 m_vPosTarget에 저장됨을 기억하자. 이렇게 목적지 위치를 구했으면, 현 위치에서 목적지 위치로 가는(걷든, 뛰든) 캐릭터의 방향을 결정 해야한다. 그 코드가 ComputeFacingTarget 함수이다. 별건없고, 방향 단위 벡터를 구한후, 각도를 라디안 값으로 m_fFacingTarget 멤버에 구해 놓는다. 나중에 이 값으로 캐릭터의 동작을 처리할 것이다. 자세한 설명은 좀 뒤에서 [캐릭터 동작 처리하기] 섹션에서 알아 볼테니, 일단 그렇다는 것만 알아두자.
void CTiny::SetSeekingState()
{
m_bIdle = false;
m_bWaiting = false;
m_fSpeed = m_fSpeedWalk;
else
m_fSpeed = m_fSpeedJog;
}
void CTiny::SetMoveKey()
{
DWORD dwNewTrack = (m_dwCurrentTrack == 0 ? 1 : 0);
LPD3DXANIMATIONCONTROLLER pAC;
LPD3DXANIMATIONSET pAS;
m_pAI->GetAnimController(&pAC);
pAC->GetAnimationSet(m_dwAnimIdxWalk, &pAS);
else
pAC->GetAnimationSet(m_dwAnimIdxJog, &pAS);
pAS->Release();
pAC->UnkeyAllTrackEvents(m_dwCurrentTrack);
pAC->UnkeyAllTrackEvents(dwNewTrack);
pAC->KeyTrackEnable(m_dwCurrentTrack, FALSE, m_dTimeCurrent + MOVE_TRANSITION_TIME);
pAC->KeyTrackSpeed(
m_dwCurrentTrack, 0.0f, m_dTimeCurrent, MOVE_TRANSITION_TIME, D3DXTRANSITION_LINEAR);
pAC->KeyTrackWeight(
m_dwCurrentTrack, 0.0f, m_dTimeCurrent, MOVE_TRANSITION_TIME, D3DXTRANSITION_LINEAR);
pAC->SetTrackEnable(dwNewTrack, TRUE);
pAC->KeyTrackSpeed(
dwNewTrack, 1.0f, m_dTimeCurrent, MOVE_TRANSITION_TIME, D3DXTRANSITION_LINEAR);
pAC->KeyTrackWeight(
dwNewTrack, 1.0f, m_dTimeCurrent, MOVE_TRANSITION_TIME, D3DXTRANSITION_LINEAR);
m_dwCurrentTrack = dwNewTrack;
}
마지막으로, CTiny::Setup 함수를 호출할때 포인터로 넘겨 주었던 애플리케이션 글로벌 배열인 g_v_pCharacters(pv_pChars)에 CTiny 인스턴스(this)가 추가 되었으니, 이 배열을 순회 하면서 캐릭터를 대상으로 이것저것 장난 칠 수 있게 되었다.
try
{
pv_pChars->push_back(this);
}
catch( ... )
{
return E_OUTOFMEMORY;
}
캐릭터 동작 처리하기
void CALLBACK OnFrameMove(IDirect3DDevice9* pd3dDevice, double fTime, float fElapsedTime)
{
vector<CTiny*>::iterator itCur, itEnd = g_v_pCharacters.end();
for (itCur = g_v_pCharacters.begin(); itCur != itEnd; ++itCur)
(*itCur)->Animate(fTime - g_fLastAnimTime);
}
CTiny::Animate 메소드를 좀더 자세히 살펴보자. 먼저 캐릭터의 현재 상태를 판단하여, 캐릭터가 사용자에 의해 조작중이면, AnimateUserControl 함수를, 빈둥 거리는 상태이면, AnimateIdle 함수를, 특정 목적지까지 걷거나 뛰는 중이라면, AnimateMoving 함수를 호출한다. 다음에 호출되는 SmoothLoiter 함수는 빈둥거리는 애니메이션 트랙이 한 애니메이션 주기를 다 재생 했을때, 루프백 해서 처음으로 돌아가게 하는데, 이때 부드럽게 끝과 처음이 애니메이션 되도록 한다. 마지막으로, 캐릭터의 월드 변환 매트릭스를 캐릭터 방향각(m_fFacing)으로 회전 시키고, 캐릭터 위치(m_vPos)로 설정하여 애니메이션 인스턴스에 저장해둔다. 나중에 캐릭터 오브젝트를 렌더할 때 월드 변환 매트릭스로 사용될 것이다.
void CTiny::Animate(double dTimeDelta)
{
if (m_bUserControl)
AnimateUserControl(dTimeDelta);
else if (m_bIdle)
AnimateIdle(dTimeDelta);
else
AnimateMoving(dTimeDelta);
SmoothLoiter();
D3DXMatrixTranslation(&mx, m_vPos.x, m_vPos.y, m_vPos.z);
D3DXMatrixMultiply(&mxWorld, &mxWorld, &mx);
D3DXMatrixMultiply(&mxWorld, &m_mxOrientation, &mxWorld);
m_pAI->SetWorldTransform(&mxWorld);
}
AnimateIdle, AnimateMoving, SmoothLoiter 함수에 대해서 좀더 자세히 알아보겠다. 먼저 AnimateIdle 함수는 빈둥거리는 시간(4초)만큼이 지나가면, SetSeekingState 함수를 호출해서 새로운 목적지로 달리거나 뛰도록 한다. SetSeeking 함수는 위에서 이미 설명 했으므로, 기억이 가물가물하면 다시 읽어보면 되겠다. SetSeeking 함수에서 리마인드 해야 할 부분은 캐릭터의 위치는 m_vPos 멤버에, 이동할 목적지 위치는 m_vPosTarget에 업데이트 된다는 것이다.
void CTiny::AnimateIdle(double dTimeDelta)
{
if (m_dTimeIdling > 0.0)
m_dTimeIdling -= dTimeDelta;
if (m_dTimeIdling <= 0.0)
SetSeekingState();
}
AnimateMoving 함수의 역할을 간단히 요약하자면, 캐릭터를 돌아 다니게 하는 핵심 데이터들을 업데이트 하고. 애니메이션 트랙을 준비하는 함수라고 할 수 있다. 즉, 목적지방향(m_vFacingTarget), 캐릭터방향(m_vFacing) 그리고, 이동속도(m_fSpeed)를 기반으로, 캐릭터의 현재위치(m_vPos)를 계산하여 캐릭터의 월드 매트릭스를 업데이트 하는 것이다. 또한, 이동을 시작하도록 애니메이션 트랙을 설정(SetMoveKey)하거나, 이동을 멈추기 위해 트랙을 설정(SetIdleKey)하고 있다. 사실 애니메이션 동작 시나리오 로직 코드가 구조화 되어 있지 않고, 좀 지저분하다. 상태를 bool 멤버로 정의하고, 해당 상태의 시간 딜레이를 double 멤버로 지정하고 있으므로, 코드를 살펴보기전에 아래 테이블을 보고, 이들 변수들이 어떤 상태에 사용되는지 이해하고 넘어가자. 또한, 이 함수는 캐릭터가 돌아다니는 인공지능(?) 시나리오를 처리하고 있으므로, 일일이 분석하기 보다는 중요한 몇몇 부분만 살펴 보려 한다.
| m_bIdle m_dTimeIdle |
빈둥거리는 애니메이션 트랙이 활성화 되었을때. 빈둥거리는 애니메이션 시간 (4초). |
| m_bWaiting m_dTimeWaiting |
어딘가에 막혀서 잠시 대기 하는 상태. 최대 대기 상태 시간 (4초). |
| m_dTimeBlocked | 진행경로가 막혔을때 잠시 멈짖 하는 시간 (1초). |
[표 2. 캐릭터 상태와 시간]
첫번째로 살펴볼 부분은 캐릭터가 이동할 거리를 계산해보고 목적지까지 다 도달 했는지 체크하는 부분이다. 만약 도착 했다면, 캐릭터 위치를 목적지 위치로 설정하고, 빈둥거리는 상태로 변경한다. 목적지 까지 도달 했는지 계산하는 로직이 (거리 = 속력 * 시간) 이런 공식으로 부터 나온것 같다. 캐릭터의 속도(m_fSpeed)와 시간(dTimeDelta)으로 이동 했을때의 거리가 남은 거리보다 크면, 대충 목적지 까지 왔다고 판단하고, 현재 위치(m_vPos)를 목적지 위치(m_vPosTarget)로 업데이트 한다. 코드에는, 목적지 까지의 남은 거리를 구할때 D3DXVec3Length 대신에 D3DXVec3LengthSq 함수를 사용하고, 계산시에 제곱 해주고 있는데, 왜 이렇게 하는지는 솔직히 잘 모르겠다. 퍼포먼스가 올라가나?
D3DXVec3Subtract(&vSub, &m_vPos, &m_vPosTarget);
fDist = D3DXVec3LengthSq(&vSub);
else if (m_fSpeed * dSpeedScale * dTimeDelta * m_fSpeed * dSpeedScale * dTimeDelta >= fDist)
{
m_vPos = m_vPosTarget;
SetIdleState();
}
두번째로, 살펴볼 부분은 캐릭터의 이동에 관련된 부분이다. GetFacing 함수를 호출해서 캐릭터 방향각(m_fFacing)을 이용한 방향 단위 벡터를 만들어 낸다. 그다음, 이동할 거리만큼 벡터 길이를 스케일 하고, 현재 위치에 더한다. 이렇게 계산된 vMovePos가 이동 할 위치이다.
GetFacing(&vMovePos);
D3DXVec3Scale(&vMovePos, &vMovePos, float(m_fSpeed * dSpeedScale * dTimeDelta));
D3DXVec3Add(&vMovePos, &vMovePos, &m_vPos);
계산한 이동 할 위치를 가지고, 범위를 벗어났거나, 다른 캐릭터에 막혔거나, 뱅글뱅글 돌기만 하고 도착하지 못할 위치면, 이동하지 않도록 bCanMove를 false로 마크해둔다. 솔직히, 도착하지 못할 위치를 판단하는 코드는 쉽사리 이해가 안됐다. 하지만, 스타크래프트의 터렛 위의 옵저버 버그를 생각하니 조금은 이해가 왔다. 즉, 목적지 위치가 너무 가까운데 이동 속도에 비해 회전 속도가 너무 작아서 도달할 수 없는 경우이다. 그런데 (남은거리 <= 이동속도 / 회전속도) 이 공식은 어떻게 이해를 해야 하나 -_-;
어쨌든, 이렇게 구해진 상태(bCanMove, bWaiting)에 따라 다음 애니메이션을 계속하거나 바꾼다. 즉, (bCanMove && m_bWaiting) 조건은 이전에 대기중 상태 였는데 이번엔 움직일 수 있는 상태이니 빈둥거리는 애니메이션에서 이동 애니메이션으로 전환 하는것이고, (!bCanMove && !m_bWaiting) 조건은 이동하고 있었는데, 이번엔 이동할 수 없으므로 빈둥거리는 애니메이션으로 전환하는 것이다. 이때, 잠시 멈짖 하도록 m_dTimeBlocked 딜레이 값을 1 로 설정하고 있다. 마지막으로, 이동하는 상태인 경우 캐릭터의 현재 위치(m_vPos)를 업데이트 한다.
bool bOrbit = false;
if (IsOutOfBounds(&vMovePos))
bCanMove = false;
if (IsBlockedByCharacter(&vMovePos))
bCanMove = false;
if ((m_fFacing != m_fFacingTarget) &&
(fDist <= ((m_fSpeed * m_fSpeed) / (m_fSpeedTurn * m_fSpeedTurn))))
{
bOrbit = true;
bCanMove = false;
}
if (bCanMove && m_bWaiting)
{
SetMoveKey();
m_bWaiting = false;
}
{
SetIdleKey(false);
m_bWaiting = true;
if (!bOrbit)
m_dTimeBlocked = 1.0;
}
m_vPos = vMovePos;
세번째로, 살펴볼 부분은, 현재 캐릭터가 바라보는 방향(m_fFacing)과 새로 설정된 목적지 방향(m_fFacingTarget)이 다른경우 캐릭터가 회전하도록 캐릭터 방향(m_fFacing)을 업데이트 하는 코드이다. 당연한 얘기겠지만, 한번에 탁! 바뀌는게 아니라, dTimeDelta 만큼 조금씩 회전하게된다. 이때, m_fFacing 에서 m_fFacingTarget 으로의 회전 각도가 작은쪽을 선택해서 회전하게 된다. 즉 시계방향(cw) 인지, 반시계방향(ccw) 인지에 따라 캐릭터 방향을 업데이트 하는 코드가 갈리게 된다. 캐릭터가 이동하는 도중 회전하면, 멋진 궤적을 그리며 이동 할 것이다.
if (m_fFacingTarget != m_fFacing)
{
float fFacing = m_fFacingTarget;
if (m_fFacingTarget > m_fFacing)
fFacing -= 2 * D3DX_PI;
if (fDiff < D3DX_PI) // cw turn
{
if (m_fFacing - m_fSpeedTurn * dTimeDelta <= fFacing)
m_fFacing = m_fFacingTarget;
else
m_fFacing = float(m_fFacing - m_fSpeedTurn * dTimeDelta);
}
else // ccw turn
{
if (m_fFacing + m_fSpeedTurn * dTimeDelta - 2 * D3DX_PI >= fFacing)
m_fFacing = m_fFacingTarget;
else
m_fFacing = float(m_fFacing + m_fSpeedTurn * dTimeDelta);
}
}
뭐, 코드의 의미야 대충 이해 하겠지만, 원리에 대해서 좀 알아보자. 알고리즘 말이다. 미안한 말이지만, 난 수학을 정말 못한다. 그래서 기하학적으로 접근하겠다. 수학 못하는 애가 설명하는 거니까 이해하기는 빠를 것이다. 일단 [회전 방향을 결정하는 조건]부터 알아보자.
이 말의 의미를 해석하기 전에, 먼저 [방향각]의 수치 개념부터 정리하자, 목적지 방향각을 결정하는 ComputFacingTarget 함수를 먼저 보는게 좋을듯하다. 위에서도 잠시 언급했던 이 함수는 현위치(m_vPos)에서 목적지(m_vPosTarget)로의 방향 단위 벡터를 구한후, 각도를 라디안 값으로 m_fFacingTarget 멤버에 구해 놓는다.
void CTiny::ComputeFacingTarget()
{
D3DXVECTOR3 vDiff;
D3DXVec3Subtract(&vDiff, &m_vPosTarget, &m_vPos);
D3DXVec3Normalize(&vDiff, &vDiff);
{
if (vDiff.x > 0.f)
m_fFacingTarget = 0.0f;
else
m_fFacingTarget = D3DX_PI;
}
else if (vDiff.z > 0.f)
m_fFacingTarget = acosf(vDiff.x);
else
m_fFacingTarget = acosf(-vDiff.x) + D3DX_PI;
}
위 코드를, 아래 그림으로 이해하자. 편의상 각도를 육십분법으로 표현 했다.
A. 목적지 벡터(m_vPosTarget)에서 시작 벡터(m_vPos)를 뺀 벡터 vDiff는
B. 원점으로 부터 향하는 방향 벡터이다.
C. vDiff.z 값이 0 과 같다면, x축 방향은 0도 이고, -x축 방향은 180도 이다.
D. vDiff.z 값이 0 보다 크다면, acosf(vDiff.x / vDiff길이) 로 각도를 구한다.
E. vDiff.z 값이 0 보다 작다면, acosf(-vDiff.x / vDiff길이) + PI 로 각도를 구한다.
F. vDiff 를 단위 벡터로 만들어(vDiff길이=1) 위 식을 단순화 시켰다.
즉, [방향각] 이란, X-축을 기준으로 반시계 방향으로 측정한 각도를 라디안 값으로 가지고 있는것이다. 사실 이부분을 설명 할까 말까 망설였다. 왜? 당연한 거니까... 그런데, 배울때의 나는 어땠는가를 생각해보고, 그냥 정리해 보기로 했다. 자, 다시 캐릭터 방향(m_fFacing)을 업데이트 하는 코드로 돌아가 보자.
좀전에 설명하다 잠시 뒤로 미루었던, 캐릭터 방향각에서 목적지 방향각으로 회전할때, 시계방향(cw)으로 회전 할지, 반시계방향(ccw)으로 회전 할지 판단하는 조건을 해석해 보자. 위에서 언급했던, [회전 방향을 결정하는 조건]은 다음과 같이 해석 할수 있다.
리마인드를 위해 좀전에 설명하던 코드 스텁을 보면서 이해하자.
float fFacing = m_fFacingTarget;
if (m_fFacingTarget > m_fFacing)
fFacing -= 2 * D3DX_PI;
float fDiff = m_fFacing - fFacing;
if (fDiff < D3DX_PI) // cw turn
...
else // ccw turn
...
코드상에서 애쓰고 있는 부분이 바로 이 사이각을 구하는 로직이다. 이때, 목적지 방향각이 캐릭터 방향각 보다 작은경우 캐릭터 방향각에서 목적지 방향각을 빼면 바로 사이각이 나오지만, 목적지 방향각이 더 큰 경우는 올바른 계산이 안되므로, 목적지 방향각을 거꾸로 한바퀴(2*D3DX_PI) 돌려놓고 계산한다. 거꾸로 한바퀴 돌린다는 말의 의미는, 지금까지 다루었던 회전각의 개념이 X 축을 기준으로 반시계 방향으로 측정한 +값이 였는데, 거꾸로 돌려버리면, X 축을 기준으로 시계 방향으로 측정한 -값의 각도가 되는 것이다. 쉽게 말해서 X 축을 기준으로 반시계 방향으로 측정한 캐릭터 방향각(m_fFacing)과 X 축을 기준으로 시계방향으로 측정한 목적지 방향각(m_fFacingTarget)을 서로 합쳐서 그 사이각을 구한다고 보면 되겠다.
다음 코드는 시계 방향으로 회전 할 때와 반시계 방향으로 회전할 때를 나누어 [캐릭터 방향각]을 업데이트 하고 있다. 어느 쪽으로 회전하든 그 원리는 같으므로 편의상 시계 방향으로 회전하는 경우만 살펴보자. 먼저, 주어진 속도(m_fSpeedTurn)로 [캐릭터 방향각]을 회전시켰을 때의 각도는 (m_fFacing - m_fSpeedTurn * dTimeDelta) 로 표현된다는 것은 직관적으로 알 수 있다. 문제는 너무 회전해서 [목적지 방향각] 보다 작아지는 경우가 있을 수 있는데, 이 경우 강제로 [목적지 방향각]으로 [캐릭터 방향각]을 설정하고 있다.
if (m_fFacingTarget != m_fFacing)
{
...
if (fDiff < D3DX_PI) // cw turn
{
// if we're overturning
if (m_fFacing - m_fSpeedTurn * dTimeDelta <= fFacing)
m_fFacing = m_fFacingTarget;
else
m_fFacing = float(m_fFacing - m_fSpeedTurn * dTimeDelta);
}
else // ccw turn
{
// if we're overturning
if (m_fFacing + m_fSpeedTurn * dTimeDelta - 2 * D3DX_PI >= fFacing)
m_fFacing = m_fFacingTarget;
else
m_fFacing = float(m_fFacing + m_fSpeedTurn * dTimeDelta);
}
ComputeFacingTarget();
}
마지막 부분에 ComputFacingTarget 함수를 호출하고 있는데, 목적지 방향과 캐릭터 방향이 서로 달라서 캐릭터를 회전 시키면서 이동 했다는 얘기는, [목적지 방향각]이 변경 되었단 이야기다. 즉, [목적지 방향각]을 다시 업데이트 하기 위해 ComputFacingTarget 함수를 호출하는 것이다.
애니메이션 업데이트 & 렌더 하기
vector<CTiny*>::iterator itCur, itEnd = g_v_pCharacters.end();
for (itCur = g_v_pCharacters.begin(); itCur != itEnd; ++itCur)
{
(*itCur)->AdvanceTime(fElapsedTime, &vEye);
(*itCur)->Draw();
}
AdvanceTime 호출 도중에 일어나는 잡스러운 것들은 일단 배제하고, 최종적으로 CAnimInstance::AdvanceTime 메소드가 호출 되게 된다. 코드를 보면, 결국, ID3DXAnimationController::AdvanceTime 메소드를 호출하고 있는데, 마지막 호출로부터 얼마만큼의 시간이 진행되었는지를 의미하는 TimeDelta를 넘겨주면, 뼈대 프레임에 있는 애니메이션 변환 매트릭스(D3DXFRAME::TransformationMatrix)를 해당 시간 만큼 업데이트 한다. 사실 이 말의 의미를 제대로 이해 하려면, [키 프레임 애니메이션 기법]에 대한 이해가 있어야 하겠지만, 여기서 설명하기엔 나름 방대하므로, 다음 기회로 미루고, "뼈대를 애니메이션 시킨다" 라고만 이해하면 되겠다. 메시들은 이들 뼈대에 붙어서 렌더링 될 것이다.
HRESULT CAnimInstance::AdvanceTime(DOUBLE dTimeDelta, ID3DXAnimationCallbackHandler * pCH)
{
HRESULT hr;
hr = m_pAC->AdvanceTime(dTimeDelta, pCH);
if (FAILED(hr))
return hr;
UpdateFrames(m_pMultiAnim->m_pFrameRoot, &m_mxWorld);
}
이렇게 준비된 애니메이션 변환 매트릭스는 계층 구조상의 뼈대 변환 매트릭스로 업데이트 되어야 한다(UpdateFrames). 이것도 역시 [뼈대 애니메이션 기법]에 대한 이해가 있어야 하므로 간략히 설명하겠다. 샘플 레퍼런스에 보면 아래와 같은 글이 있다.
"업데이트 된 매트릭스는 프레임 계층구조를 따라 결합된 매트릭스(combined matrix) 입니다. 결합과정은 프레임 자신의 매트릭스와 계층구조 상에 있는 모든 부모 프레임 매트릭스들을 곱한 것이 됩니다. 뼈대는 그들 부모 뼈대로부터 영향을 받고, 부모 뼈대는 조부 뼈대로부터 영향을 받는 것이지요. 이러한 과정은 루트 뼈대 까지 반복됩니다."
번역 실력이 훌륭해서 의미가 바로 와닫지 않는다. 좀더 자세히 설명하자면, 한 프레임의 변환 매트릭스는 그 부모 프레임의 변환 매트릭스를 곱한것이어야 한다. 즉, 뼈대는 부모뼈대의 영향을 받는다는 것이다. [그림 2. tiny_4anim.x 파일의 프레임 계층구조] 그림을 보자. 오른손(Bip01_R_Hand) 프레임의 위치는 오른손 로컬 매트릭스 만으로 이루어 지는게 아니라 아래팔(Bip01_R_ForeArm) 프레임의 위치에 따라 상대적으로 움직이는 프레임이다. 그렇게 하기 위해서 부모 프레임인 아래팔의 변환 매트릭스를 곱해 주어야 한다는 것이다. 이러한 과정은 루트 프레임까지 계속된다. 다시 말하자면, AdvanceTime 메소드의 호출은 각 프레임의 애니메이션을 적용한 로컬 변환 매트릭스를 생성한 것이고, UpdateFrames 메소드의 호출은 루트 프레임부터 트리를 순회 하면서 로컬 변환 매트릭스에 그 부모 변환 매트릭스를 곱해서 뼈대 매트릭스를 완성 한다는 것이다. 지금 까지 설명한 내용을 수식으로 표현하자면 아래와 같다.
Mbone_transform = Mbone_local X Mbone_animation X Mbone_parent
여기에 루트 프레임 변환 매트릭스에 월드 변환 매트릭스 까지 곱해주면, 원하는 공간에 오브젝트를 표현할 수 있게 되는 것이다. 코드를 보자. 트리를 순회하고 업데이트 하는 순서가 루트 프레임으로 부터 깊이를 따라 아래로 리커시브하게 이루어 지고 있다. 최초 호출시에는 루트 프레임과 월드 변환 매트릭스를 넣어서 호출 했지만, 다음 리커시브 호출 시에는 부모 프레임과 부모 프레임의 변환 매트릭스를 받아서 자신의 변환 매트릭스에 곱해주고 있음을 볼수 있겠다.
void CAnimInstance::UpdateFrames(MultiAnimFrame* pFrame, D3DXMATRIX* pmxBase)
{
assert(pFrame != NULL);
assert(pmxBase != NULL);
D3DXMatrixMultiply(&pFrame->TransformationMatrix, &pFrame->TransformationMatrix, pmxBase);
if (pFrame->pFrameSibling)
UpdateFrames((MultiAnimFrame*)pFrame->pFrameSibling, pmxBase);
if (pFrame->pFrameFirstChild)
UpdateFrames((MultiAnimFrame*)pFrame->pFrameFirstChild, &pFrame->TransformationMatrix);
}
이리하여 뼈대의 최종 변환 매트릭스가 준비되었다. 마지막으로 그리기만 하면 되는 것이다. CTiny:Draw 메소드의 호출은 CAnimInstance::Draw 메소드로 전달되고, CAnimInstance::DrawFrames 메소드를 루트 프레임부터 리커시브 하게 호출하고 있다.
HRESULT CAnimInstance::Draw()
{
DrawFrames(m_pMultiAnim->m_pFrameRoot);
}
void CAnimInstance::DrawFrames(MultiAnimFrame* pFrame)
{
if (pFrame->pMeshContainer)
DrawMeshFrame(pFrame);
DrawFrames((MultiAnimFrame*)pFrame->pFrameSibling);
DrawFrames((MultiAnimFrame*)pFrame->pFrameFirstChild);
}
주의깊게 살펴 보아야 할 부분은 메시를 그리는 함수인 DrawMeshFrame 메소드 이다. 당연한 얘기지만, 이 메소드로 처리되는 프레임은 메시 컨테이너를 가진 노드이다. [그림 2. tiny_4anim.x 파일의 프레임 계층구조] 그림을 보면, 메시 컨테이너를 가진 프레임은 한개가 있을 뿐이다. 쓰레기 메시 컨테이너도 하나 더 있긴 하지만, 처리되지 않으므로 1개라고 치자. 메시는 연결된 뼈대의 위치에 의해 그 모양이 결정된다. 달리 말하자면, 메시의 각 정점은 한개 이상의 뼈대에 의해서 그 위치가 결정된다는 것이다. 이때 정점에 명시된 4개의 가중치 값으로 뼈대가 정점에 영향을 미치는 강도를 조절하는 기법이 스키닝이다. 스키닝 구현 기법에는 여러가지 방법이 있지만, 이 예제에서는 셰이더를 이용한 스키닝을 사용하고 있다. 다른 스키닝 기법에 대해 알고 싶으면 Skinned Mesh 샘플을 참조하면 되겠다. 해야 할 일은 많을것 같은데, 코드는 비교적 단순하다. 이유인즉, 스키닝 코드가 샘플 코드에 있지 않고, fx 파일에 들어가 있기 때문이다. 여기서는 단순히 버텍스 셰이더가 fx 파일을 잘 처리할 수 있도록 외부 데이터인 팔레트 정보, 텍스쳐 등을 넘겨주기만 하면 되는 것이다. fx 파일 분석은 다음 기회에 하도록 하겠다.
한참 전에 설명했던 뼈대 컴비네이션(D3DXBONECOMBINATION)이 여기서 사용되었다. 기억을 거슬러 보자. 우리는 스키닝을 위해 버텍스 셰이더를 사용할 것이다. 그런데 이 버텍스 셰이더는 최대 상수 갯수가 제한 되어 있으므로 무작정 팔레트 테이블의 크기를 크게 할 수 없다. 즉 한 메시에 사용되는 총 뼈대 테이블이 셰이더의 팔레트 테이블 크기보다 크다면, 속성 그룹으로 뼈대 테이블을 나누고 나눈 갯수만큼 DrawSubset으로 렌더를 수행해야 한다. 코드를 보면, 속성 그룹 단위로 작업하고 있음을 볼수 있다.
LPD3DXBONECOMBINATION pBC =
(LPD3DXBONECOMBINATION)(pMC->m_pBufBoneCombos->GetBufferPointer());
DWORD dwAttrib, dwPalEntry;
{
DWORD dwMatrixIndex = pBC[dwAttrib].BoneId[dwPalEntry];
...
pMC->m_pWorkingMesh->DrawSubset(dwAttrib);
...
}
버텍스 셰이더에게 전달해야 할 데이터는 [뼈대 팔레트 테이블], [텍스쳐], [버텍스에 영향을 미치는 뼈대의 최대 갯수]가 있다. 아래 코드는 이들 데이터를 만들고 버텍스 셰이더에게 전달 한다.
for (dwPalEntry = 0; dwPalEntry < pMC->m_dwNumPaletteEntries; ++dwPalEntry)
{
DWORD dwMatrixIndex = pBC[dwAttrib].BoneId[dwPalEntry];
if (dwMatrixIndex != UINT_MAX)
D3DXMatrixMultiply(
&m_pMultiAnim->m_amxWorkingPalette[dwPalEntry],
&(pMC->m_amxBoneOffsets[dwMatrixIndex]),
pMC->m_apmxBonePointers[dwMatrixIndex]);
}
m_pMultiAnim->m_pEffect->SetMatrixArray(
"amPalette", m_pMultiAnim->m_amxWorkingPalette, pMC->m_dwNumPaletteEntries);
마지막으로, 버텍스 셰이더를 설정하고 메시를 그리고 있다.
if (FAILED(m_pMultiAnim->m_pEffect->SetTechnique(m_pMultiAnim->m_sTechnique)))
return;
for (uiPass = 0; uiPass < uiPasses; ++uiPass)
{
m_pMultiAnim->m_pEffect->BeginPass(uiPass);
pMC->m_pWorkingMesh->DrawSubset(dwAttrib);
m_pMultiAnim->m_pEffect->EndPass();
}
m_pMultiAnim->m_pEffect->End();
마치며
이 예제는 애니메이션 오브젝트를 여러개 올려놓고 서로 독립적으로 어떻게 애니메이션 시킬 수 있는지를 설명하는 예제이다. 코드를 단순히 하려고 그랬는진 몰라도 스키닝 처리가 좀 부실하다. 버텍스 셰이더 기법만 사용하고 있는데, 사실 하드웨어 지원을 받을 수 없는 저사양 PC에서는 소프트웨어 스키닝 기법을 사용해야 한다. 셰이더를 사용할 수 없기 때문이다. Skinned Mesh 샘플에 다양한 스키닝 기법이 소개되어 있으므로, 이를 토대로 확장 시켜보는것도 좋을 듯 싶다.
SkinnedMesh Sample
SkinnedMesh 예제는 D3DX를 사용한 스키닝 메시 애니메이션을 설명하고 있습니다. 스키닝이란 계층구조 뼈대 메시에 들어가있는 데이터를 사용해서 메시 버텍스를 변경하기 위해 기하 블렌딩(geometry blending)을 적용하는 애니메이션 기술을 말합니다. 기하 블렌딩(geometry blending)은 부드러운 표면을 만들어냅니다.
The SkinnedMesh sample illustrates mesh animation with skinning using D3DX. Skinning is an animation technique that takes data organized in a skeletal-mesh hierarchy and applies geometry blending to transform mesh vertices. The geometry blending generates smooth surfaces with fewer artifacts.
Path
| Source: | (SDK root)\Samples\C++\Direct3D\SkinnedMesh |
| Executable: | (SDK root)\Samples\C++\Direct3D\Bin\x86 or x64\SkinnedMesh.exe |
Sample Overview
Direct3D와 D3DX는 메시들을 애니메이션 시키기 위해 사용될수 있습니다. 애플리케이션은 애니메이션 데이터를 가진 x파일을 로드할 수 있고, 애니메이트된 메시를 제어하고 렌더할 수 있습니다. 이 예제는 5가지 스키닝 기법(고정함수 비인덱스, 고정함수 인덱스, 소프트웨어, 어셈블리 셰이더, HLSL 세이더)을 보여줍니다. 각각의 기법은 장점과 단점을 가지고 있으니, 애플리케이션 개발자는 잘 생각해보고, 각자 자신의 애플리케이션에 맞는 적절한 방법을 선택해야 합니다.
Direct3D and D3DX can be used to animate meshes. Applications can load X files containing animation data, then control and render the animated meshes. This sample demonstrates five skinning techniques: fixed-function non-indexed, fixed-function indexed, software, assembly shader, and HLSL shader. Each technique has its advantages and disadvantages, and application developers should weigh these and choose the appropriate techniques for the needs of their application.
사람의 몸과 같은 모델을 애니메이션 시킬때, 스키닝은 널리 사용되는 기법입니다. 스키닝은 계층구조 형태를 가지는 서로 연결된 뼈대 셋을 사용합니다. 뼈대를 이동시키거나 회전시키면, 메시의 표면도 이동되거나 회전됩니다. 메시 표면은 사람의 스킨 처럼 보입니다. 메시 표면의 버텍스들 각각은 여러개의 뼈대와 연결되어 있습니다. 버텍스의 위치는 연결된 뼈대에 의해서 결정되게 됩니다. 예를들어 사람의 팔을 표현한 <그림 1>에 있는 계층구조를 유심히 살펴보세요.
Skinning is a popular animation technique that is modeled like a human body. Skinning uses a set of interconnected bones (or frames) that form a hierarchy. Moving or rotating the bones cause the mesh surface to move or rotate. The mesh surface is analagous to the skin of the human body. Each point (or vertex) on the mesh surface is associated with a number of bones. Its position is determined by the position of the associated bones. For instance, consider the hierarchy in figure 1 that describes a human limb:
쇄골
윗팔
아래팔
손
손가락0
손가락1
손가락2
손가락3
손가락4
그림 1. 인간의 팔 계층구조.
팔꿈치에 있는 스킨은 윗팔 뼈대와 아래팔 뼈대에 의해 영향을 받습니다. 윗팔이 움직이지않고, 아래팔이 움직인다면, 팔꿈치 스킨 위치가 영향을 받게 됩니다. 아래팔이 움직이지 않고 윗팔이 움직인다해도 마찬가지 입니다. 이러한 사실은 팔꿈치 스킨이 윗팔과 아랫팔 뼈대에 영향을 받는다는 것을 의미합니다. 여러개 매트릭스를 가진 버텍스를 변환하는 것은 기하 블렌딩(geometry blending)으로 계산할 수 있습니다. 각각의 매트릭스는 0~1의 블렌딩 가중치(blending weight)를 가집니다. 참고로 모든 블렌딩 가중치의 합은 1이 되어야 합니다. 블렌딩 연산은 각각의 매트릭스를 가지고 버텍스를 변환 하는 작업입니다. 그런다음, 이들 좌표는 해당하는 블렌딩 가중치에 의해서 보간됩니다. 버텍스들은 같은 뼈대로부터 영향을 받는다 해도 서로 다른 블렌딩 가중치를 가질수 있습니다. 이러한 기하 블렌딩은 애니메이트될때 부드러운 표면들을 가져다 줄 것입니다.
The skin at the elbow is influenced by the upper arm bone and the forearm bone. If the upper arm remains stationary and the forearm moves, the elbow skin position is affected; the same is true when holding the forearm stationary and moving the upper arm. It can be concluded that the skin at the elbow is associated with both the upper arm and forearm bones. Transforming vertices with multiple matrices are done with geometry blending. Each matrix has a blending weight ranging from 0 to 1, with the sum of all blending weights equal to 1. The blending operation is carried out by first transforming the vertex with each matrix, yielding several transformed vertex coordinates. Then, these coordinates are interpolated based on the corresponding blending weights. Vertices can have different blending weights even if they are influenced by the same bones. This geometry blending allows surfaces to stay smooth when animated.
게다가, 부모 뼈대에 의한 이동도 스킨 위치에 영할을 줄 것입니다. 예를들어, 윗팔과 아랫팔 뼈대가 고정된 채로, 쇄골 뼈대가 이동된다면, 윗팔과 아랫팔 사이에 있는 스킨은 위치가 변경되어야 합니다. 이러한 사실은, 왜 자식 뼈대가 종속성을 가지는 계층구조로 모델링 되어야 하는지를 말해주고 있습니다.
Furthermore, any movement by an ancestor bone may affect the skin position. For instance, if both the upper arm and forearm bones remain stationary (that is, no rotation), and the clavicle bone moved, the skin between the upper arm bone and the forearm bone should change position. This explains why the child bones are modeled in a hierarchy to generate this dependency.
그림 2. 캐릭터의 왼쪽 아래팔 뼈대 옵셋 변환 이펙트
- 왼쪽그림: 디스크에 저장된 캐릭터.
- 가운데그림: 뼈대 옵셋 변환이 버텍스 좌표를 메시 공간에서 뼈대 공간으로 변경시킵니다.
- 오른쪽그림: 이제 버텍스들이 뼈대 공간에 있고, 애니메이션 변환이 수행될수 있습니다. 이 경우엔, 아래팔의 회전이 적용되었습니다.
How the Sample Works
메시를 로딩하는 첫번째 단계는 D3DXLoadMeshHierarchyFromXInMemory 함수를 호출하는 것입니다. 이 호출은 D3DXLoadMeshHierarchyFromX 호출과 유사합니다. 둘다 x파일을 메모리에 로드하는 함수 입니다. 하지만, D3DXLoadMeshHierarchyFromXInMemory 함수는 뼈대로 구성된 계층구조 트리 형태의 메시를 반환 한다는 점이 다릅니다. 이러한 형태는 메시가 x파일에 어떻게 구성되는지를 매치시켜줍니다. D3DX 에서는, 애니메이션 가능한 메시가 프레임 계층구조로 구성됩니다. 맨 위는, 루트 프레임 입니다. 루트 프레임은 한개 또는 그 이상의 자식 프레임을 가지고, 각각의 자식 프레임은 자신들만의 고유한 또 다른 자식 프레임들을 가지고, 또 그 자식들도 마찬가지 입니다.
The first step in loading a mesh is to call D3DXLoadMeshHierarchyFromXInMemory. This call is similar to D3DXLoadMeshHierarchyFromX, in that they both load a mesh from an X file into memory. However, D3DXLoadMeshHierarchyFromXInMemory returns the mesh in the form of a hierarchy tree composed of bones (or frames). This form matches how the mesh is structured in the X file. In D3DX, an animatable mesh is composed from a frame hierarchy. At the very top, there is a root frame. The root frame has one or more child frames, each child frame has its own child frames, and so forth.
계층구조의 프레임은 D3DXFRAME을 상속받은 구조체로 표현됩니다. 애플리케이션은 D3DXFRAME 계층구조 노드의 포맷을 상속받아서 각각의 프레임에 비공개 정보를 저장 할수 있습니다. 그림 3 에서 볼수 있듯, D3DXFRAME은 계층구조에 필요한 최소한의 데이터만을 제공합니다. 각각의 프레임은 이름과, 변환 매트릭스, 메시 컨테이너(기하 데이터)를 가집니다. 또한 각각의 프레임은 형제, 자식 프레임과 링크 되어 있습니다.
A frame in the hierarchy is represented by a structure derived from a D3DXFRAME. An application may store private information with each frame by deriving the format of each hierarchy node from D3DXFRAME. D3DXFRAME, as shown in figure 3, provides the bare minimum data required by the hierarchy. Each frame has a name, a transformation matrix, and a mesh container for the geometry data. Each frame also contains a link to a sibling and a child frame.
typedef struct _D3DXFRAME {
LPSTR Name;
D3DXMATRIX TransformationMatrix;
LPD3DXMESHCONTAINER pMeshContainer;
struct _D3DXFRAME *pFrameSibling;
struct _D3DXFRAME *pFrameFirstChild;
} D3DXFRAME, *LPD3DXFRAME;
Figure 3. D3DXFRAME은 메시 계층구조의 빌딩 블럭 입니다.
SkinnedMesh 예제는, 애니메이션을 수행하기 위해, D3DXFRAME을 상속받은 combination matrix라 불리는 매트릭스 구조를 확장하고 있습니다.
The SkinnedMesh sample derives D3DXFRAME and augments the struct with another matrix named a combination matrix which will be used when advancing the animation.
계층구조에서, 몇몇 프레임들은 메시 컨테이너 값을 가질 것입니다. 이 메시 컨테이너는 메시 기하 데이터를 가지고 있습니다. 메시를 렌더링 할때, 이들 컨테이너는 계층구조 메시의 자체 위치와 상관없이 그려집니다. 프레임도 마찬가지로, 그림 4에서 보여지는것 처럼 D3DXMESHCONTAINER 에서 파생된 프레임 고유의 메시 컨테이너 타입을 정의해야 합니다.
In a hierarchy, some frames will have valid container values, which are the mesh geometry data. When rendering the mesh, the container is drawn regardless of its location in the mesh hierarchy. Similar to a frame, an application should define its own mesh container type by deriving from a D3DXMESHCONTAINER, shown in figure 4.
typedef struct _D3DXMESHCONTAINER {
LPSTR Name;
D3DXMESHDATA MeshData;
LPD3DXMATERIAL pMaterials;
LPD3DXEFFECTINSTANCE pEffects;
DWORD NumMaterials;
DWORD *pAdjacency;
LPD3DXSKININFO pSkinInfo;
struct _D3DXMESHCONTAINER *pNextMeshContainer;
} D3DXMESHCONTAINER, *LPD3DXMESHCONTAINER;
Figure 4. 스탠다드 메시 컨테이너.
D3DXMeshContainer의 모든 데이터 필드는 Name과 pSkinInfo를 제외하고는 일반적인 메시를 위해 존재합니다. Name은 메시 계층구조 상에 있는 메시 컨테이너를 식별하는 유니크 문자열 입니다. pSkinInfo는 뼈대 매트릭스 정보를 가지고 있습니다. 이 정보는 각각의 버텍스에 영향을 주는 뼈대들로 구성되어 있습니다.
All data fields in a D3DXMeshContainer also exist for an ordinary mesh, except for Name and pSkinInfo. Name is a unique string that identifies the mesh container in the hierarchy and pSkinInfo holds bone matrices information. This information consists of the bones by which each vertex is influenced, and is important when setting up the mesh into attribute groups.
추가로, 계층구조 메시를 반환 하기위해 D3DXLoadMeshHierarchyFromX 함수는 메시 프레임들과 연관되어있는 ID3DXAnimationController 인터페이스를 반환합니다. 이 애니메이션 컨트롤러는 애니메이션이 가능한 메시들을 제어합니다. (이놈은 재생을 위한 애니메이션 시퀀스가 무엇인지, 각각의 시퀀스가 얼마나 빨리 재생되는지를 나열 하는데도 사용됩니다)
In addition to returning a mesh hierarchy, D3DXLoadMeshHierarchyFromX returns an ID3DXAnimationController interface associated with the mesh frames. The animation controller controls the flow of the animation available to the mesh; use it specify what animation sequences to play and how fast each sequence plays.
프레임 계층구조의 두 기본적인 구조체가 애플리케이션에서 자주 파생되고, 정의되기 때문에, 프레임과 메시 컨테이너의 할당과 해제를 처리하는 함수를 애플리케이션에서 만들어 두는게 좋습니다. ID3DXAllocateHierarchy는 어떻게 그러한 할당, 해제 함수를 애플리케이션이 정의 할수 있는지 설명하는 인터페이스 입니다. 요놈을 사용하기 위해서 애플리케이션은 먼저 ID3DXAllocateHierarchy를 상속받은 클래스를 정의해야 합니다. 그런다음 인터페이스에 선언된 아래의 4가지 메소드를 구현해야 합니다.
ID3DXAllocateHierarchy::CreateFrame,
ID3DXAllocateHierarchy::DestroyFrame,
ID3DXAllocateHierarchy::CreateMeshContainer,
ID3DXAllocateHierarchy::DestroyMeshContainer
생성 메소드는 프레임이나 메시 컨테이너의 새 인스턴스를 할당하고, 필요한 다른 리소스들도 할당하고, 필드를 초기화 합니다. 그런다음 호출한 놈한테 오브젝트 인스턴스를 반환합니다.
Because the two fundamental structures of a frame hierarchy are often derived and defined by the application itself, the application has to define functions that handle the allocation and deallocation of the frames and mesh containers. ID3DXAllocateHierarchy is an interface that describes how the application can define such allocation and deallocation functions. To use it, an application first defines a class derived from ID3DXAllocateHierarchy. Then, implement the four methods declared by the interface: ID3DXAllocateHierarchy::CreateFrame, ID3DXAllocateHierarchy::DestroyFrame, ID3DXAllocateHierarchy::CreateMeshContainer, and ID3DXAllocateHierarchy::DestroyMeshContainer. The create methods allocate a new instance of frame or mesh container struct, allocate other resources as necessary, initialize the struct fields, and return the object instance back to the caller.
프레임이나 메시의 컨테이너가 생성되는 동안 할당 했었던 모든 리소스들은 파괴 메소드가 해제해야 합니다. 프레임이나 메시 컨테이너 인스턴스 그 자체도 마찬가지로 해제해야 합니다. SkinnedMesh 예제에서, CreateMeshContainer 함수는 메시 컨테이너 구조체에 있는 애플리케이션 정의 필드를 초기화 합니다. 이 구조체는 <그림 5> 에서 보여주고 있습니다.
The destroy methods should free all resources allocated during the creation of the frame or mesh container, including the frame or mesh container instance itself. In the SkinnedMesh sample, CreateMeshContainer also initializes the application-defined fields in the mesh container structure. The structure is shown in figure 5.
struct D3DXMESHCONTAINER_DERIVED: public D3DXMESHCONTAINER
{
LPDIRECT3DTEXTURE9* ppTextures;
// SkinMesh info
LPD3DXMESH pOrigMesh;
LPD3DXATTRIBUTERANGE pAttributeTable;
DWORD NumAttributeGroups;
DWORD NumInfl;
LPD3DXBUFFER pBoneCombinationBuf;
D3DXMATRIX** ppBoneMatrixPtrs;
D3DXMATRIX* pBoneOffsetMatrices;
DWORD NumPaletteEntries;
bool UseSoftwareVP;
DWORD iAttributeSW;
};
Figure 5. 파생된 메시 컨테이너.
pSkinInfo로부터 뼈대 옵셋 매트릭스를 받아낸 후에, CreateMeshContainer함수는 애니메이팅과 렌더링을 위한 계층구조 메시를 셋업하기위해 GenerateSkinnedMesh 함수를 호출합니다. GenerateSkinnedMesh 함수는 메시버텍스를 그룹으로 분리합니다. 이 그룹은 최적의 상태로 메시를 렌더링 하기위해 사용되는 스키닝 메소드를 기반으로 합니다.
After retrieving the bone offset matrices from pSkinInfo, CreateNeshContainer calls GenerateSkinnedMesh to set up the mesh hierarchy for animating and rendering. GenerateSkinnedMesh divides the mesh vertices into groups based on the skinning method used, which makes rendering the mesh optimal.
D3DXLoadMeshHierarchyFromX 함수가 리턴된 후에, SetupBoneMatrixPointers 함수를 호출하고 있습니다. 이 함수는 메시 컨테이너를 찾기위해 프레임 계층구조를 순회합니다. 메시 컨테이너를 찾아내면, 메시 컨테이너의 ppBoneMatrixPtrs 멤버를 초기화 하기위해 SetupBoneMatrixPointersOnMesh 함수를 호출합니다. 이 멤버는 매트릭스 포인터 배열입니다. 배열의 각 요소는 특정한 프레임 매트릭스를 가리킵니다. 이 배열은 프레임 인덱스로 프레임 매트릭스를 빠르게 찾아낼 수 있게 합니다. 계층구조는 트리 형태로 구조가 잡혀있기 때문에, index-to-matrix 검색은 단순무식한 선형 프로세스가 아닙니다. 이 매트릭스는 효율적인 처리를 위한 헬퍼 룩업 테이블을 제공합니다.
After D3DXLoadMeshHierarchyFromX returns, the sample calls SetupBoneMatrixPointers. This function traverses the frame hierarchy to search for mesh containers. When it finds a mesh container, it calls SetupBoneMatrixPointersOnMesh to initialize the ppBoneMatrixPtrs member of the mesh container. This member is an array of matrix pointers, where each element in the array points to a particular frame matrix. This array provides a quick lookup for frame matrices by frame index. Because the hierarchy is structured as a tree, performing an index-to-matrix lookup is not a straightforward process. This matrix serves as a helper lookup table for efficiency.
Rendering the Mesh
메시 애니메이션을 렌더링 하는 과정은 두 단계 프로세스(매트릭스 셋업하기, 실제로 렌더링하기) 입니다. 매트릭스를 셋업하기 위해서는 우선, ID3DXAnimationController::AdvanceTime 함수를 호출합니다. 이 메소드에 마지막 호출로부터 얼마만큼의 시간이 진행되었는지를 의미하는 TimeDelta를 넘겨주면, 해당 시간 만큼 뼈대 위치에 해당하는 계층구조 변환 매트릭스를 업데이트 합니다. 이러한 매트릭스는 그들 부모 프레임들을 고려한 변환(스케일, 회전, 이동 변환)을 가지고 있습니다. 다음으로, UpdateFrameMatrices 함수를 호출합니다. 이 함수는 변환 매트릭스를 업데이트 합니다. 업데이트 된 매트릭스는 프레임 계층구조를 따라 결합된 매트릭스(combined matrix) 입니다. 결합과정은 프레임 자신의 매트릭스와 계층구조 상에 있는 모든 부모 프레임 매트릭스들을 곱한 것이 됩니다. 뼈대는 그들 부모 뼈대로부터 영향을 받고, 부모 뼈대는 조부 뼈대로부터 영향을 받는 것이지요. 이러한 과정은 루트 뼈대 까지 반복됩니다. 모든 영향받는 매트릭스를 결합 하는것은 렌더링 하기에 보다 적당한, 월드에 절대적인 혹은 상대적인, 결합된 변환 매트릭스를 만들어냅니다. UpdateFrameMatrices가 바로 이러한 일련의 작업들을 리커시브한 계층구조 트리 순회를 통해 수행하는 함수입니다. <계속 비슷한 내용이라 생략>
Rendering the animated mesh is a two-step process: setting up the matrices and the actual rendering. To set up the matrices, ID3DXAnimationController::AdvanceTime is called first. This method takes a TimeDelta parameter that indicates how much to advance since the last call, then it updates the frame hierarchy's transformation matrices with matrices that correspond to the bone positions at that instance of time. These matrices contain transformation with respect to their parent frames, consisting of a scaling plus rotation plus translation transformation. Next, the sample calls UpdateFrameMatrices. This function updates the frame hierarchy's combined transformation matrix. The combined transformation matrix holds the product of all of the ancestor frames' matrices, including the frame's own matrix. Recall that bones are influenced by their parent bones, and the parent bones are influenced by the grandparent bones, and so forth. Combining all influencing matrices makes the combined transformation matrix absolute, or relative to the world, which is more suitable for rendering. UpdateFrameMatrices achieves this by recursively traversing the hierarchy tree. For each frame, it writes the product of the transformation matrix and the parent's matrix to the combined transformation matrix. Then it calls UpdateFrameMatrices on its sibling and first child nodes, passing its parent matrix to the sibling and passing its own combined transformation matrix as the parent matrix for the children.
다른 예제들과 마찬가지로 실제 렌더링은 OnFrameRender 함수 내에서 시작됩니다. 메시의 렌더링은 DrawFrame 함수에 의해서 수행됩니다. 이 함수는 프레임 노드에 대한 포인터를 가지고 프레임의 메시를 그립니다. 그런다음 리커시브하게 프레임의 형제 그리고 자식을 다시 호출합니다. 그 결과 프레임 계층구조에 있는 메시 컨테이너들은 최상위 DrawFrame 함수가 리턴될때 모두 다 그려질 것입니다. 프레임이 유효한 메시 컨테이너를 가지고 있을때, DrawFrame은 DrawMeshContainer를 호출합니다.
The actual rendering, like all other samples, starts in OnFrameRender. The rendering of the mesh is done by the DrawFrame. This function takes a pointer to a frame node, draws the frame's mesh if one exists, then recursively calls itself again with the frame's sibling and children. The result is that all mesh containers in the frame hierarchy will be drawn when the top DrawFrame returns. When a frame holds a valid mesh container, DrawFrame calls the DrawMeshContainer, which is where all the mesh rendering takes place.
DrawMeshContainer 함수는 아래와 같은 다른 방식으로 메시를 렌더합니다.
DrawMeshContainer renders the mesh in different ways:
Rendering with Fixed-Function Non-Indexed Skinning
이 스키닝 방식을 사용하면, 디바이스는 한번에 4개의 월드 매트릭스를 로드할 수 있습니다. 그리고, 이들 4개의 매트릭스에 의해 영향을 받는 버텍스들을 변환시킬 수 있습니다. 이들 버텍스가 렌더되고 난후, 다른 매트릭스 셋과, 버텍스(두번째 매트릭스 셋에 영향을 받는 렌더될수 있는 버텍스들)을 로드 할 수 있습니다. 이 렌더링 프로세스는 메시의 모든 면이 렌더 될때까지 계속됩니다. GenerateSkinnedMesh 함수는 ID3DXSkinInfo::ConvertToBlendedMesh(메시 면들을 속성 그룹으로 분리하는 함수)를 호출합니다. 속성 그룹 각각은 4개 매트릭스의 특정 셋에 영향을 받는 버텍스가 어떤 것인지를 구분합니다. 렌더시에, 속성그룹 각각은 하나의 IDirect3DDevice9::DrawIndexedPrimitive 함수로 렌더될 수 있습니다.
When this skinning technique is active, the device can load up to four world matrices at a time and transform the vertices influenced by those four matrices. After these vertices are rendered, a different set of matrices can be loaded, and vertices influenced by the second set of matrices can be rendered. This rendering process continues until all faces of the mesh are rendered. GenerateSkinnedMesh calls ID3DXSkinInfo::ConvertToBlendedMesh which divides a mesh's faces into attribute groups. Each attribute group identifies geometry influenced by a particular set of four matrices. At render time, each attribute group can be rendered with a single IDirect3DDevice9::DrawIndexedPrimitive.
비-인덱스 스키닝 방식을 위해서, DrawMeshContainer 함수는 메시 안의 속성그룹 각각을 돌면서, 한번에 한개의 속성 그룹을 렌더합니다. 메시는 속성그룹 각각이 4개 매트릭스에 의해 영향을 받을수 있도록 셋업합니다. 버텍스들을 디바이스에 보내기 전에, 영향을 미치는 매트릭스들을 먼저 디바이스에 보내야 합니다. 이들 매트릭스들을 설정하기 위해, IDirect3DDevice9::SetTransform 함수가 호출됩니다. 이 함수에 넘기는 변환 타입에는 D3DTS_WORLDMATRIX(i) 매크로를 사용합니다. i 값의 범위는 최소 0 부터 최대 3 입니다. 디바이스에 보낼 매트릭스들은 메시 컨테이너의 pBoneCombinationBuf로 얻어냅니다. 이 멤버는 D3DXBONECOMBINATION 타입의 배열을 담은 버퍼를 포인트 하고 있습니다. D3DXBONECOMBINATION 각각은 이 속성 그룹에 영향을 미치는 뼈대 매트릭스를 나타냅니다. 이 정보를 가지고, 디바이스에 로드할 4개 매트릭스를 알 수 있습니다. 하지만, 매트릭스를 디바이스에 세팅 하기전에, 뼈대 옵셋 매트릭스를 가져와야 할 필요가 있습니다. 뼈대 옵셋 매트릭스를 리콜 하는것은 파일에 있는 메시의 기본 형태로부터 뼈대의 프레임으로 버텍스들을 변환하는 것입니다. 그러므로, 디바이스에 보낼 매트릭스는 뼈대 매트릭스와 프레임에 있는 결합된(combined) 변환 매트릭스를 곱한것이 됩니다. 이 매트릭스를 로딩한 후에, 속성 그룹의 면들은 ID3DXBaseMesh::DrawSubset 함수의 호출로 그려질수 있습니다.
For non-indexed skinning, DrawMeshContainer loops through each attribute group in the mesh and renders one attribute group at a time. The mesh is set up so that each attribute group is influenced by up to four matrices. Before the vertices can be sent to the device, the influencing matrices first need to be sent to the device. To set the matrices, IDirect3DDevice9::SetTransform is called with the transformation type D3DTS_WORLDMATRIX(i), where i ranges from a minimum of 0 to a maximum of 3. The matrices that the function sends to the device for a particular attribute group are obtained from pBoneCombinationBuf of the mesh container. This member points to a buffer containing an array of type D3DXBONECOMBINATION. Each D3DXBONECOMBINATION identifies the bone matrices influencing this attribute group. With this information, the sample knows which four matrices to load on the device to properly render an attribute group. Before actually setting the matrices, however, the matrices need to have the bone offset matrices applied to them. Recall that bone offset matrices transform the vertices from the mesh's default pose on the disk to the parent bone's frame of reference. Therefore, a matrix that gets sent to the device is the product of the bone matrix and the frame's combined transformation matrix. After loading the matrices, the faces of the attribute group can be rendered by calling ID3DXBaseMesh::DrawSubset.
Rendering with Fixed-Function Indexed Skinning
고정함수 인덱스 스키닝 방식에서, 디바이스는 한개의 매트릭스 팔레트를 가집니다. 팔레트로 설정할 수 있는 크기는 CAPS 정보로부터 알아낼수 있습니다. 애플리케이션은 팔레트 형태의 많은 매트릭스를 로드할수 있습니다. 메시 안에 각각의 버텍스는 팔레트에 있는 매트릭스를 식별하기 위해 4개 인덱스를 가집니다. 이 예제 에서는 최대 12개 매트릭스 팔레트가 사용됩니다. 더 큰 팔레트를 사용하면, 보다 효율적이지만, 작은 크기의 팔레트를 사용하면, 보다 많은 디바이스에 호환되는 코드를 만들 수 있습니다. 인덱스 스키닝을 위해 적당한 메시를 얻기 위해서, GenerateSkinnedMesh 함수는 ID3DXSkinInfo::ConvertToIndexedBlendedMesh 함수를 호출합니다. 이 함수는 팔레트 크기를 가지고, 매트릭스 팔레트로 작업하기 위한 속성 그룹으로 메시 버텍스들을 분리 합니다. 속성 그룹 각각은 팔레트에 있는 하나의 특정 매트릭스 셋에 의해 영향을 받고, 하나의 IDirect3DDevice9::DrawIndexedPrimitive 함수로 그려질수 있습니다.
With the fixed-function indexed skinning, the device has a matrix palette. The size of the palette can be obtained from the device capability information. The application can load as many matrices as the palette could hold. Each vertex in the mesh has up to four indices to identify the matrices in the palette that influence the vertex. In the sample, the code uses a maximum 12-matrix palette. Using a larger palette is more efficient, but using a smaller palette makes the code compatible with more devices. To obtain a mesh suitable for indexed skinning, GenerateSkinnedMesh calls ID3DXSkinInfo::ConvertToIndexedBlendedMesh (which is similar to ID3DXSkinInfo::ConvertToBlendedMesh) but it takes a palette size and divides the mesh vertices into attribute groups to work with the matrix palette. Each attribute group is influenced by one specific set of matrices that can fit in the palette, and can be drawn with a single IDirect3DDevice9::DrawIndexedPrimitive.
인덱스 스키닝을 위한 렌더 코드는 아주 조금은 비-인덱스 방식의 경우와 유사하게 보입니다. GenerateSkinnedMesh 함수는 속성 그룹을 돌면서, 디바이스에 보내기위해 매트릭스들을 셋업합니다. 여기까지만 비-인덱스 방식과 같습니다. 각각의 속성 그룹은 팔레트에 있는 특정 매트릭스 셋에 의해 영향을 받은 버텍스들을 식별합니다. D3DXBONECOMBINATION 배열 안의 각각의 엘리먼트는 뼈대 매트릭스들을 식별합니다 (속성 그룹을 렌더링할때 팔레트를 로드하기 위함). 매트릭스들이 설정된 후에, 이 속성 그룹의 면들은 ID3DXBaseMesh::DrawSubset 함수로 렌더합니다.
The render code for indexed skinning looks extremely similar to the non-indexed case. GenerateSkinnedMesh loops through the attribute groups and sets up the matrices to send to the device, just as it would for non-indexed skinning. Here, each attribute group identifies the vertices influenced by a particular set of matrices in the palette. In the D3DXBONECOMBINATION array, each element identifies the bone matrices with which to load the palette when rendering this attribute group. After the matrices are set, this attribute group's faces are rendered with a call to ID3DXBaseMesh::DrawSubset.
Rendering with Shader-Based Skinning
개념상, 셰이더 기반 스키닝은 고정함수 인덱스 스키닝과 매우 유사합니다. 하드웨어가 지원하는 매트릭스 팔레트를 사용하는것 대신에, 매트릭스를 로드하는데 셰이더를 사용합니다. 그런다음, 버텍스 셰이더는 매트릭스 값을 읽을 수 있고, 스키닝 변환을 수행합니다. 이 샘플은 두가지 형태의 셰이더 기반 스키닝을 지원합니다. 어셈블리 와 HLSL 방식이 그것인데요. 사실상, 두 형태의 유일한 차이점은 셰이더가 어떻게 작성 되었느냐 입니다. 고정함수 인덱스 스키닝 방식과 비슷하게, GenerateSkinnedMesh 함수는 팔레트를 위해 필요한 상수 갯수를 계산합니다. 그런다음 팔레트에의해 속성값이 정렬된 메시를 얻기위해 ID3DXSkinInfo::ConvertToBlendedMesh 함수를 호출합니다. 팔레트에 있는 각각의 매트릭스가 3개 셰이더 상수만 필요합니다. 왜냐하면, 4번째 컬럼은 항상 (0, 0, 0, 1)이고, 제대로 저장될 필요도 없기 때문입니다.
Shader-based skinning is, in concept, similar to fixed-function indexed skinning. Instead of using the hardware-supported matrix palette, the application uses shader constants to load the matrices. Then, the vertex shader can read in the matrix values and perform the skinning transformation. The sample supports two types of shader-based skinning: assembly and HLSL. Virtually, the only difference between the two is how the shader is written. Similar to fixed-function indexed skinning, GenerateSkinnedMesh computes the number of constants required for the palette, then calls ID3DXSkinInfo::ConvertToBlendedMesh to obtain a mesh that is attribute-sorted by palette. It is worth noting that each matrix in the palette only requires three shader constants because the fourth column is always (0, 0, 0, 1) and does not need to be explicitly stored.
셰이더 기반 스키닝은 인덱스 스키닝 방식의 형태를 띕니다. 매트릭스들을 로딩하기 위해 사용되는 IDirect3DDevice9::SetTransform 함수 대신에, 셰이더 상수로 로드되어 집니다. 이 예제는 디바이스에 보내기 위해 고정함수 인덱스의 경우와 같은 방법으로 매트릭스를 계산합니다. 매트릭스를 보낼때, 어셈블리 셰이더를 사용하는 경우 IDirect3DDevice9::SetVertexShaderConstantF 함수를 호출합니다. HLSL 셰이더를 사용하는 경우는 ID3DXBaseEffect::SetMatrixArray 함수를 사용합니다. 이 작업이 끝나고 셰이더가 셋업되면, 속성 그룹은 DrawSubset으로 렌더될수 있습니다.
Shader-based skinning is a form of indexed skinning. Instead of loading the matrices with IDirect3DDevice9::SetTransform, they are loaded as shader constants. The sample computes the matrices to send to the device in exactly the same manner as in the fixed-function indexed case. When sending the matrices, the sample calls IDirect3DDevice9::SetVertexShaderConstantF when using an assembly shader, or ID3DXBaseEffect::SetMatrixArray when using an HLSL shader. After this is done and the shader is set up, the attribute group can be rendered with DrawSubset.
Rendering with Software Skinning
소프트웨어 스키닝은, 이름 그대로, 완전히 소프트웨어 적으로 스키닝 작업을 수행한는 방식입니다. 입력 메시와 뼈대 매트릭스를 가지고, 변환과 블렌딩을 수행한후, 다른 메시로 결과를 출력합니다. 이 최종 기하정보는 표현 하려고 하는 애니메이션을 가지고 있고, 렌더링 하기 위해 디바이스에 보내질수 있습니다. 소프트웨어 스키닝의 가장 큰 약점은, 퍼포먼스가 낮다는 점입니다. 소프트웨어 스키닝은 효율적인 하드웨어 대신에 소프트웨어적으로 수행되기 때문에 느릴수 밖에 없습니다. 하지만, 소프트웨어 스키닝은 디바이스의 성능에 상관없이 항상 성공적으로 수행될수 있습니다. 소프트웨어 스키닝을 셋업하기 위해서 이 샘플은 소프트웨어 매트릭스 팔레트 기능을 하는 매트릭스 배열을 할당하고 있습니다. 렌더링 하는 동안, 이 매트릭스 배열은 스키닝 매트릭스로 채워지고, 메시를 에니메이트 하는데 사용될 것입니다.
Software skinning, as the name suggests, performs the skinning work completely in software. The process takes an input mesh and the bone matrices, carries out the transformation and blending, then writes the result into another mesh. This resulting geometry represents the animation pose desired and can be sent to the device for rendering. The biggest drawback of software skinning is performance. Software skinning is inherently slow because it is done by software instead of the efficient hardware. However, software skinning is also the only technique that is guaranteed to work regardless of the device on which the application runs. (Note that other skinning techniques can also work on all devices by using software vertex processing. However, software skinning can be useful in certain situations, such as performing a hit test on the animated mesh.) To set up for software skinning, the sample allocates an array of matrices to function as the software matrix palette. During rendering, this matrix array will be filled with skinning matrices and used to animate the mesh.
본질적으로, 소프트웨어 스키닝은 아주 큰 팔레트(시스템 메모리가 허용 할때까지)로 동작하는 인덱스 스키닝이라고 생각할 수 있습니다. 모든 매트릭스는 렌더링과 동시에 로드 될 수 있습니다. 이 예제에서, DrawMeshContainer 함수는 메시에 대한 모든 매트릭스를 계산하고, 매트릭스 배열에 그 결과를 저장합니다. ID3DXSkinInfo::UpdateSkinnedMesh 함수는 변환 매트릭스를 거친 버텍스 원본 한개로 소프트웨어 스키닝을 구현합니다. 함수가 리턴될때, 메시 컨테이너의 MeshData 멤버는 스키닝된, 애니메이트된 메시를 가지게 됩니다. 전체 메시는 이제 DrawSubset 함수로 그려질 수 있습니다. 메시가 여러개 재질을 가지고 있는경우 한번에 각각의 속성 그룹을 그려야 합니다.
Essentially, software skinning can be thought of as the indexed skinning with an infinitely large palette (or as large as the system memory allows) done completely in software. All matrices can be loaded simultaneously for rendering. In the sample, DrawMeshContainer computes all matrices for the mesh and stores the result in a matrix array. ID3DXSkinInfo::UpdateSkinnedMesh implements software skinning on a set of source vertices using the passed transformation matrices and fills a set of output vertices with animated coordinates. When it returns, the MeshData member of the mesh container holds a skinned and animated mesh; the entire mesh can now be drawn by calling DrawSubset. The code still has to draw each attribute group at a time if the mesh contains multiple materials.
MultiAnimation Sample
이 예제는 HLSL 스키닝과 D3DX 애니메이션 컨트롤러를 사용한 다중 애니메이션 메시를 설명하고 있습니다. 애니메이션 컨트롤러는 하나의 애니메이션에서 다른 애니메이션으로 이동할때, 부드럽게 변환하도록 애니메이션셋을 블렌딩 해서 만듭니다.
This sample demonstrates mesh animation with multiple animation sets using high-level shader language (HLSL) skinning and the D3DX animation controller. The animation controller blends animation sets together to ensure a smooth transition when moving from one animation to another.
Path
| Source: | (SDK root)\Samples\C++\Direct3D\MultiAnimation |
| Executable: | (SDK root)\Samples\C++\Direct3D\Bin\x86 or x64\MultiAnimation.exe |
Overview
이 예제는 D3DX 애니메이션 지원 기능을 이용하여 3D 애니메이션을 어떻게 렌더링 할수 있는지 보여줍니다. D3DX는 다중 애니메이션의 블렌딩 뿐만아니라 애니메이션 메시를 로딩하는 API 들을 가지고 있습니다. 애니메이션 컨트롤러는 애니메이션 트랙을 지원하고, 하나의 애니메이션에서 다른 애니메이션으로 부드럽게 전환되도록 합니다. 이 예제는 애니메이션 클래스 라이브러리와 애플리케이션 두개 파트로 나뉘어져 있습니다.
This sample shows how an application can render 3D animation utilizing D3DX animation support. D3DX has APIs that handle the loading of the mesh to be animated, as well as the blending of multiple animations. The animation controller supports animation tracks for this purpose, and allows transitioning from one animation to another smoothly. The sample is divided into two parts: the animation class library and the application.
애니메이션 클래스 라이브러리는 애플리케이션과 D3DX 사이에 위치하는 제너럴한 라이브러리 입니다. 이 라이브러리는 x파일로부터 메시를 로딩하는 것들과, 버텍스 셰이더와 매트릭스 팔레트(인덱스 스키닝을 위해 필요)를 사용하여 애니메이션 메시를 렌더링 하는것, 계층구조 프레임을 다루는 것들을 캡슐화 하고 있습니다. 이 라이브러리는 재사용 가능하고, 커스터마이징 할수 있도록 설계 되었습니다.
The animation class library is a general-purpose library that sits between the application and D3DX. It encapsulates the details of loading the mesh from a .x file and manipulating its frame hierarchy to prepare it for rendering, as well as rendering the animated mesh using a vertex shader and a matrix palette for indexed skinning. It is designed to be reusable and customizable.
애플리케이션 부분은 이 예제에 종속적인 코드들을 가지고 있습니다. 이 부분에서는 Tiny 캐릭터의 인스턴스를 생성하기위해 애니메이션 클래스 라이브러리를 사용하고 있습니다. Tiny 캐릭터는 자체 동작 정보를 가지고 애니메이션되는 놈 입니다. 각각의 인스턴스는 사용자나, 샘플 프로그램에 의해서 컨트롤될 수 있습니다. 또한, Tiny 인스턴스들 사이에서 충돌이 일어났는지, 범위를 벗어났는지를 처리하고, 컨트롤되는 인스턴스들에 대한 동작 관리도 처리합니다.
The application portion contains code specific only to this sample. It uses the animation class library to create instances of the Tiny character, which are animated according to the action they perform. Each instance can be controlled by either the user or the sample. This portion also handles collision detection among instances of Tiny, out of bound detection, and behavior management for instances that are controlled by the sample.
The Application
이 예제의 애플리케이션 부분은 두개 파트로 구성되어 있습니다. 첫번째 파트는 CTiny 클래스 입니다. 이놈은 tiny_4anim.x 파일로부터 로드한 애니메이션 메시의 동작을 처리합니다. 두번째 파트는 CMyD3DApplication과 DirectX 애플리케이션 위자드가 생성한 일반적인 코드들 모두를 가지고 있습니다.
The application portion of this sample consists of two parts. The first part is the CTiny class. It handles the behavior of the animated mesh in the sample loaded from the tiny_4anim.x file. The second part contains CMyD3DApplication and all of the code that is normally generated by the DirectX application wizard.
이 예제에서, Tiny는 사용자나 애플리케이션(디폴트)에 의해서 컨트롤될 수 있습니다. Tiny가 애플리케이션에 의해 컨트롤될 때, 다음과 같은 일들이 일어납니다. 첫번째로, 바닥위에 랜덤한 위치가 선택되고, 이동 속도(걷거나 뛰거나)가 결정됩니다. 다음으로, 현재 위치에서 새로운 위치로 돌고 이동 합니다. 새로운 위치로의 이동이 끝나면, 또 다른 위치로의 이동이 선택되기 전에 잠시 쉽니다. 이러한 과정 전체가 계속 반복됩니다. Tiny 인스턴스는 여러개가 생성될수 있는데요. 이들 인스턴스들이 각각 이동할때 부딛히면 멈출수 있도록 충돌체크가 일어납니다.
In this sample, Tiny can be controlled by either the user or the application (default). When Tiny is controlled by the application, the following happens: First, a random location on the floor is chosen and a move speed (run or walk) is determined. Next, Tiny is turned at the current position toward the new destination and is moved in that direction. After Tiny arrives at the new destination, there is a brief period of idle time before another location is selected where to move Tiny, and the whole process repeats. There can be more than one instance of Tiny. Collision detection is done for the instances, so they can block each other's movement.
CTiny 클래스는 애니메이션 라이브러리에 정의된 CAnimInstance 멤버를 하나 가지고 있습니다. CTiny는 이 멤버와 애니메이션을 수행 하기 위한 자체 애니메이션 컨트롤러를 사용합니다. 이 예제에서, Tiny는 3가지 애니메이션 셋(어슬렁거리기, 걷기, 뛰기)을 가지고 있습니다. Tiny 애니메이션 컨트롤러는 두개 애니메이션 트랙을 지원합니다. 대부분의 시간에, 이 두 트랙중 하나는 애니메이션 셋으로 활성화 됩니다. Tiny의 액션이 애니메이션을 바꾸기위해 호출될때(어슬렁거리다가 걷기 또는 뛰다가 멈추기), 두번째 트랙이 사용가능하게 되고, 첫번째 트랙으로부터 두번째 트랙으로의 변화를 완료하기위한 시간 간격을 설정합니다. 그런다음 시간이 좀 지나면, 애니메이션 컨트롤러는 두개 트랙 사이를 부드럽게 보간한 프레임 매트릭스를 생성할 것입니다. 변환 간격이 지난후에, 첫번째 트랙은 비활성화 되고, 두번째 트랙이 새로운 애니메이션으로 플레이 됩니다.
The CTiny class has a CAnimInstance member, a class defined by the animation library. CTiny uses this member and its animation controller to perform the animation tasks necessary. In this sample, Tiny has three sets of animation: Loiter, Walk, and Run. The animation controller of Tiny supports two animation tracks. Most of the time, one of these two tracks is active with an animation set. When Tiny's action calls for a change of animation (for instance, going from loitering to walking, or stopping from running), enable the second track with the new animation set to play and set a transition period to complete the transition from the first track to the second. Then, as time is advanced, the animation controller will generate the correct frame matrices that reflect the transition smoothly by interpolating between the two tracks. After the transition period passes, the first track is disabled, and the second track plays the new animation in which Tiny is seen.
CTiny는 또한 애니메이션중에 있는 특정한 인스턴스에 대해 발굽 소리를 플레이 할수 있는 콜백 시스템을 지원합니다. 초기화 단계에서 CTiny는 CallbackDataTiny 구조체를 설정합니다. 이놈은 콜백 핸들러를 통해 전달할 데이터를 담고있습니다. 이 구조체는 세가지 멤버를 가지고 있습니다. m_dwFoot 멤버는 어떤 발이 소리를 내는지를 나타냅니다. m_pvCameraPos 콜백이 호출될때의 카메라 위치 좌표를 나타냅니다. m_pvTinyPos는 Tiny의 월드 좌표계 위치를 나타냅니다. 이 데이터는 어떤 소리가 플레이 될지(왼쪽인지 오른쪽인지를), 얼마 만큼의 소리로 플레이 할지 결정하는데 사용됩니다. 이 샘플에서는 애니메이션 컨트롤러에 의해 호출되는 콜백 핸들러 함수를 가진 ID3DXAnimationCallbackHandler를 상속받은 CBHandlerTiny라는 클래스를 정의하고 있습니다. 콜백 핸들러 함수인 HandleCallback은 CallbackDataTiny 구조체로 넘겨진 데이터를 인자로 받습니다. 그러고 나서 CallbackDataTiny 구조체에 있는 값을 기반으로 계산된 적절한 볼륨으로 DirectSound 버퍼를 플레이 합니다.
CTiny also takes advantage of the callback system to play the footstep sound at appropriate instances in the animation. At initialization time, CTiny sets up a structure, CallbackDataTiny, containing the data to pass to the callback handler. The structure has three members: m_dwFoot indicates which foot is triggering the sound, m_pvCameraPos points to the camera position when the callback is made, and m_pvTinyPos points to the world space coordinates of Tiny. This data is used to determine which sound to play (left or right), and how loud it should be. The sample defines a class called CBHandlerTiny, derived from ID3DXAnimationCallbackHandler, that contains the callback handler function to be called by the animation controller. The callback handler function, HandleCallback, retrieves the data passed as a CallbackDataTiny structure then plays a DirectSound buffer with the appropriate volume based on the values in the CallbackDataTiny structure.
프레임 이동시, CTiny 인스턴스 어레이를 돌면서, 각각의 인스턴스에 대해 Animate를 호출합니다. 이 과정은 모든 인스턴스의 동작을 업데이트 하도록 합니다. (바뀔 필요가 있으면)
In FrameMove, the sample iterates through the array of CTiny instances and calls Animate on each one of them. This updates the behaviors of all instances, changing them if needed.
렌더링 코드는 비교적 간단합니다. 우선, 카메라 위치를 기반으로한 뷰와 프로젝션 매트릭스를 설정하고, 바닥위에 보이도록 메시 오브젝트를 렌더합니다. 다음으로, Tiny 인스턴스 어레이를 돌면서, AdvanceTime함수와 Draw 함수를 호출합니다. AdvanceTime 함수는 애니메이션 컨트롤러가 계층구조 프레임 매트릭스를 업데이트 하도록 만듭니다. Draw 함수는 이 업데이트된 매트릭스를 사용하여 메시 인스턴스를 렌더합니다. 마지막으로 필요한 텍스트를 렌더 합니다.
The rendering code of the application is relatively simple. It first sets up the view and projection matrices based on the camera position and orientation, then it renders the mesh object representing the floor. Next, it goes through the array of Tiny instances and calls AdvanceTime and Draw on each one of them. AdvanceTime makes the animation controller update the frame hierarchy's matrices, then Draw renders the instance using the updated matrices. Finally, the code renders the informational text as necessary.
The Animation Class Library
라이브러리는 아래의 구조체와 클래스들로 구성되어 있습니다.
The library consists of the following structures and classes:
CMultiAnim
이 클래스는 라이브러리의 심장부 입니다. 이놈은 x 파일로부터 로드된 계층구조 메시를 캡슐화 합니다. 또한 계층구조 메시를 공유하는 애니메이션 메시 인스턴스(CAnimInstance)를 생성할수 있습니다. 이들 애니메이션 인스턴스는 그들을 생성한 CMultiAnim 오브젝트들과 연동되어 있습니다.
This class is the heart of the library. Its function is to encapsulate the mesh hierarchy loaded from a .x file. It can also create instances of the animating mesh, of the type CAnimInstance, that share the mesh hierarchy from the .x file. These instances are associated with the CMultiAnim object that creates them.
CMultiAnim::Setup 초기화 메소드에서, CMultiAnim은 버텍스 셰이더 버전에 따른 매트릭스 팔레트의 최대 크기를 결정합니다. 그런다음 x 파일로부터 계층구조 메시를 로드하고, fx 파일로부터 이펙트 오브젝트를 생성합니다. fx 파일은 메시를 렌더하기 위한 버텍스 셰이더를 가지고 있습니다. 계층구조 메시 프레임들과 애니메이션 컨트롤러는 이러한 프로세스에서 생성됩니다.
In its initialization method, CMultiAnim::Setup, CMultiAnim determines the maximum size of the matrix palette based on the version of the vertex shader present. It then loads the mesh hierarchy from the given .x file and creates an effect object from the given .fx file that contains the vertex shader with which the mesh is rendered. The frames of the mesh hierarchy and the animation controller are created in this process.
프레임들은 계층구조 뼈대를 표현하는 트리형태의 구조체와, 메시 오브젝트들로 구성되어 있습니다. 이 구조체는 모든 관련된 인스턴스들과 공유됩니다.
The frames consist of a tree structure representing the hierarchy of the bones, as well as the mesh objects. This structure is shared by all associated instances.
애니메이션 컨트롤러는 계층구조 메시 프레임들과 관련이 있습니다. CMultiAnim의 멤버로 존재하는 애니메이션 컨트롤러는 실제 애니메이션을 위해 사용되지는 않습니다만, 새로운 애니메이션 인스턴스가 생성될 때, 애니메이션 컨트롤러는 복제(clone)되고, 복제된 컨트롤러는 새로운 애니메이션 인스턴스가 가지게 됩니다. 애플리케이션은 인스턴스(CAnimInstance) 고유의 애니메이션 컨트롤러(CAnimInstance::m_pAC)를 애니메이션을 컨트롤하는데 사용합니다. 각각의 인스턴스가 그 자신만의 고유한 애니메이션 컨트롤러의 복사본을 가지고 있기때문에 다른 인스턴스들과 독립적으로 애니메이트 될수 있습니다.
The animation controller is associated with the mesh hierarchy frames. The animation controller that CMultiAnim holds is not used for animation. Instead, when a new animation instance is created, the animation controller is cloned and the new animation controller is owned by the new instance. The application uses an instance's own animation controller to control its animation. Because each instance has its own copy of animation controller, it can animate independent of all other instances.
애니메이션 인스턴스에 있는 애니메이션 컨트롤러의 ID3DXAnimationController::AdvanceTime 메소드가 호출될때, 애니메이션 컨트롤러는 계층구조 메시에 있는 각각의 프레임에 대한 변환 매트릭스를 업데이트 할 것입니다. 이후에, 이들 프레임은 인스턴스를 렌더링 하는데 사용됩니다.
When the ID3DXAnimationController::AdvanceTime method of an animation controller in an animation instance is called, the animation controller will update the transformation matrix for each frame in the mesh hierarchy. The frames are then used in the rendering of the instance.
CAnimInstance
이 클래스는 애니메이션 엔터티 또는 인스턴스 라는 말로 표현될수 있습니다. 각각의 애니메이션 인스턴스는 각자 고유한 애니메이션 컨트롤러를 가지고 있습니다. 이 말의 의미는 각자 독립적으로 애니메이트 될수 있다는 얘기입니다. 또한 각각의 인스턴스는 관련된 인스턴스들이 가지고 있는 CMultiAnim 오브젝트를 공유합니다. 아래 테이블은 애니메이션을 컨트롤 하기위해 사용되는 애니메이션 컨트롤러의 중요한 메소드들을 보여줍니다. 이들 메소드들은 애니메이트, 렌더링 할때 애플리케이션에 의해 사용됩니다.
This class represents an animation entity, or an animation instance. Each animation instance has its own animation controller, which allows the instances to be animated independent of each other. Each instance is also associated with a CMultiAnim object, which holds the mesh hierarchy that is shared by all associated instances. The following table lists the methods that are the most interesting for animation control and which are used extensively by the application when animating and rendering.
| 메소드 이름 | 설명 |
|---|---|
| GetAnimController |
이 인스턴스의 애니메이션 컨트롤러(type: ID3DXAnimationController)를 반환합니다. 이 애니메이션 컨트롤러를 가지고 애플리케이션은 플레이 하는데 필요한 애니메이션을 설정할 수 있습니다. |
| SetWorldTransform |
이 인스턴스의 최상위 레벨 월드 변환 매트릭스를 설정합니다. 애니메이션 타임이 지나면, 계층구조 메시에 있는 각각의 프레임은 이 월드 변환에 의해 리커시브하게 변환 됩니다. 이러한 변환은 월드 공간내에 원하는 위치로 메시들을 가져오게 합니다. |
| AdvanceTime |
추가적인 콜백 핸들러를 가진 인스턴스에 대한 로컬 애니메이션을 진행합니다. 진행 시간은 이 인스턴스의 뼈대 위치를 업데이트 하기위한 기반 값으로 사용됩니다. 또한, 애니메이션 컨트롤러에 keyed event 꺼리가 있다면, 콜백 핸들러를 인보크 할 것입니다. |
| ResetTime |
이 인스턴스의 로컬 타임을 리셋 합니다. |
| Draw | HLSL 버텍스 셰이더를 가지고 이 인스턴스를 렌더 합니다. 보통, 이 함수는 AdvanceTime 함수 다음에 호출합니다. |
| Method Name | Description |
|---|---|
| GetAnimController | Returns the animation controller for this instance, of type ID3DXAnimationController. With the animation controller, the application can set the animation it needs to play. |
| SetWorldTransform | Sets the top level world transformation matrix for this instance. When the animation time advances, each frame in the mesh hierarchy is recursively transformed by this world transformation to bring it to the correct world space coordinate and orientation. |
| AdvanceTime | Advances the local animation time for this instance with an optional callback handler. The time advancement causes the animation controller to update the bone positions for this instance. The animation controller will also invoke the callback handler if the keyed event is reached. |
| ResetTime | Resets the local time for this instance. |
| Draw | Renders this instance with the HLSL vertex shader. This is normally called after calling AdvanceTime so that the frames are set up properly for rendering. |
애니메이션 인스턴스가 렌더 될때, 버텍스 셰이더가 포함된 렌더링 매개변수를 설정하기 위해, 인스턴스에 연결된 CMultiAnim에 있는 이펙트 오브젝트를 사용합니다. 버텍스 셰이더는 메시를 스키닝 하고, 스크린 공간으로 위치를 변환하고, 메시의 색상을 계산하는 역할을 합니다. 스키닝 부분은 VS_Skin 함수에 의해 동작됩니다. 이 함수는 Skin.vsh 버텍스 셰이더 파일에 있습니다. 이 파일은 스키닝 메시를 위한 매트릭스 팔레트인 float4x3 어레이(amPalette)와 VS_Skin 스키닝 함수를 가지고 있습니다. VS_Skin 함수는 다른 버텍스 셰이더 함수에 의해 인보크 되도록 설계되었습니다. 그러므로 애플리케이션 딴에서 구현한 버텍스 셰이더 내부에서 스키닝 프로세스를 처리하기위해 VS_Skin 함수를 호출할수 있습니다. VS_Skin 함수는 오브젝트 공간 좌표, 법선, 3개의 가중치값(원랜 4개가 필요한데 계산식으로 4번째놈을 구할수 있습니다; 1.0f - 3개가중치합), 그리고 매트릭스 팔레트를 참조하기위한 인덱스 4개를 받습니다. 그런다음 해당되는 매트릭스 팔레트와 가중치값을 가지고 위치와 법선을 변환합니다. 그리고 월드 공간 좌표와 법선을 결과에 함께 합칩니다.
When an animation instance is rendered, it uses the effect object of its associated CMultiAnim to set the rendering parameters, including the vertex shader. The vertex shader function is responsible for skinning the mesh, transforming the position to screen space, and computing the color. The skinning portion is done by the function VS_Skin. This function is located in the vertex shader file Skin.vsh. The file contains an array of float4x3 named amPalette, which represents the matrix palette for skinning meshes, and the VS_Skin skinning function. VS_Skin is designed to be invoked by another vertex shader function, so an application can call it in its own vertex shader function to handle the skinning process. VS_Skin takes the object space position and normal, up to three blending weights (the last blending weight is computed by subtracting the sum of all other weights from 1), and up to four indices for the matrix palette. The function then transforms the position and normal up to four times with the corresponding matrices in the matrix palette and blending weights, and adds the result together to form the world space position and normal.
CMultiAnimAllocateHierarchy
이 클래스는 ID3DXAllocateHierarchy 인터페이스를 상속 받았습니다. 이 클래스는 계층구조 메시를 로딩, 릴리징 하는동안의 리소스 할당과 해제를 핸들링하는 역할을 합니다. 이 예제 에서, CMultiAnimAllocateHierarchy는 새로운 프레임들의 모든 멤버를 초기화 하고, 계층구조 메시가 생성되는 동안 호출되는 CreateFrame과 CreateMeshContainer 안에서 메시 컨테이너들의 모든 멤버를 초기화 합니다. DestroyFrame, DestroyMeshContainer 내부에서는, CreateFrame과 CreateMeshContainer로 할당된 모든 리소스들을 각각 해제 합니다.
This class inherits from ID3DXAllocateHierarchy. Its purpose is to handle the allocation and deallocation of resources during the loading and releasing of a mesh hierarchy. In this sample, CMultiAnimAllocateHierarchy initializes all members of new frames and mesh containers in CreateFrame and CreateMeshContainer called during the mesh hierarchy creation. In DestroyFrame and DestroyMeshContainer, it frees up all resources allocated in CreateFrame and CreateMeshContainer, respectively.
CreateMeshContainer는 단순히 할당, 복사하는것 보다 좀더 많은 일을 합니다. 이놈은 아래의 일들을 합니다.
- 메시에서 사용되는 모든 텍스쳐를 생성합니다.
- 주어진 pSkinInfo를 가지고 메시 컨테이너의 뼈대 옵셋을 초기화 합니다.
- 메시가 동작하기 위해 필요한 팔레트 크기를 확보하고, 팔레트 크기에 맞는 메시를 생성합니다.
CreateMeshContainer does a little more work than simply allocating and copying. It also has to do the following:
- Create all of the textures used by the mesh.
- Initialize the mesh container's bone offset array, given by pSkinInfo.
- Obtain the palette size that the mesh has to work with, and call ConvertToIndexedBlendedMesh to create a working mesh that is compatible with the palette size.
MultiAnimFrame Structure
이 스트럭쳐는 D3DXFRAME 으로부터 파생되었습니다. 이 놈은 단일 프레임, 뼈대, 계층구조 메시를 표현합니다. 한개 프레임은 형제, 자식 프레임을 포함할수 있습니다. 계층구조 메시 전체는 트리 구조와 유사하게 구성되어 있습니다. 애플리케이션은 필요한 경우 D3DXFRAME으로부터 파생된 스트럭쳐에 멤버들을 추가할 수 있습니다. 이 예제에서는 추가 멤버가 필요하지 않아서 확장된것이 없습니다.
This structure is derived from D3DXFRAME. It represents a single frame, or bone, in a mesh hierarchy. A frame may contain siblings or children. The entire mesh hierarchy is structured similar to a tree. The application can add members to a structure derived from D3DXFRAME as necessary. In this sample, no additional members are needed.
MultiAnimMC Structure
이 구조체는 D3DXMESHCONTAINER로 부터 파생되었습니다. 이 놈은 적절한 메시 데이터와 함께 계층구조 메시에 붙어있는 메시 오브젝트를 포함하고 있습니다. 계층구조 메시는 얼마든지 메시 컨테이너들을 가질수 있습니다. 이 예제에서 사용된 추가적인 멤버들이 아래 테이블에 정의되어 있습니다.
This structure is derived from D3DXMESHCONTAINER. It contains a mesh object attached to the mesh hierarchy along with relevant data for the mesh. A mesh hierarchy can contain any number of mesh containers. In this sample, additional members are defined as listed in the following table.
| 타입 | 이름 | 설명 |
|---|---|---|
| LPDIRECT3DTEXTURE9 * |
m_apTextures |
메시가 사용하는 Direct3D 텍스쳐 오브젝트 어레이. |
| LPD3DXMESH |
m_pWorkingMesh |
매트릭스 팔레트 사이즈와 호환되는 메시의 복사본. |
| D3DXMATRIX* |
m_amxBoneOffsets |
뼈대의 옵셋 매트릭스 어레이. 이 정보는 pSkinInfo 멤버를 통해 얻어냅니다. |
| D3DXMATRIX** |
m_apmxBonePointers |
매트릭스 포인터 어레이. 요놈은 계층구조 메시의 여러 프레임에 있는 변환 매트릭스를 가리킵니다. 이 어레이로 뼈대 인덱스로부터 뼈대 매트릭스로 쉽게 맵핑할 수 있습니다. |
| DWORD |
m_dwNumPaletteEntries |
렌더링시 이 메시가 사용하는 매트릭스 팔레트의 크기. 이 크기는 버텍스 셰이더의 최대 레지스터 갯수의 제한 때문에 최대 팔레트 크기를 넘을 수 없습니다. 또한 메시의 뼈대 갯수보다 클수 없습니다. |
| DWORD |
m_dwMaxNumFaceInfls |
단일 면에 영향을 미치는 뼈대의 최대 갯수. 이 값은 ID3DXSkinInfo::ConvertToIndexedBlendedMesh로부터 얻어집니다. 버텍스 셰이더는 렌더링시에 마지막 가중치를 언제 계산해야 하는지 알아야 하므로 이 값이 필요합니다. |
| DWORD |
m_dwNumAttrGroups |
작업 메시(working mesh)의 속성 그룹 갯수. 속성 그룹은 메시의 서브셋(한번의 draw 호출로 그려질수 있는 단위) 입니다. 만약 작업 팔레트(working palette) 크기가 메시 전체를 렌더하기에 충분히 크지 않다면, 속성그룹으로 뼈대 테이블 참조자(boneid)를 쪼개야 할 필요가 있습니다. ID3DXSkinInfo::ConvertToIndexedBlendedMesh 로 수행할 수 있습니다. |
| LPD3DXBUFFER | m_pBufBoneCombos | D3DXBONECOMBINATION의 어레이 형태로 뼈대 조합 테이블을 가집니다. 각각의 속성 그룹에는 하나의 D3DXBONECOMBINATION이 있습니다. 이 구조로 메시 서브셋을 식별 합니다. |
| Type | Name | Description |
|---|---|---|
| LPDIRECT3DTEXTURE9 * |
m_apTextures |
Array of Direct3D texture objects used by the mesh. |
| LPD3DXMESH |
m_pWorkingMesh |
A copy of the mesh compatible with the matrix palette size available. |
| D3DXMATRIX* |
m_amxBoneOffsets |
Array of matrices representing the bone offsets. This information is obtained from the pSkinInfo member, copied here for convenience. |
| D3DXMATRIX** |
m_apmxBonePointers |
Array of pointers to matrix. They point to the TransformationMatrix of various frames of this mesh hierarchy. This array provides an easy mapping from bone index to bone matrix. |
| DWORD |
m_dwNumPaletteEntries |
Size of the matrix palette that this mesh uses when rendering. The size cannot exceed the maximum palette size allowed by our vertex shader due to limited registers, and it also cannot be larger than the number of bones in this mesh. |
| DWORD |
m_dwMaxNumFaceInfls |
Maximum number of bones that may affect a single face. This value is obtained from ID3DXSkinInfo::ConvertToIndexedBlendedMesh. The vertex shader needs this value when rendering in order to know when to compute the last weight. |
| DWORD |
m_dwNumAttrGroups |
Number of attribute groups in working mesh. An attribute group is a subset of the mesh that can be drawn in a single draw call. If the working palette size is not large enough to render the mesh in its entirety, it needs to be broken down into separate attribute groups. This is done by ID3DXSkinInfo::ConvertToIndexedBlendedMesh. |
| LPD3DXBUFFER | m_pBufBoneCombos | Contains the bone combination table, in the form of an array of D3DXBONECOMBINATION structures. There is one D3DXBONECOMBINATION for each attribute group. It identifies the subset of the mesh (vertices, faces, and bones) that can be drawn in a single draw call. |
User's Guide
아래의 조작키가 샘플에 정의되어 있습니다.
The following general controls are defined in the sample.
| Key | Action |
|---|---|
| Q | 카메라를 아래로 이동합니다. |
| E | 카메라를 위로 이동합니다. |
| W | 카메라를 앞으로 이동합니다. |
| A | 카메라를 왼쪽으로 이동합니다. |
| S | 카메라를 뒤로 이동합니다. |
| D | 카메라를 오른쪽으로 이동합니다. |
| N | 다음 뷰 모드로 전환합니다. |
| P | 이전 뷰 모드로 전환합니다. |
| R | 카메라를 리셋합니다. |
| F2 | Direct3D 설정 메뉴를 띄웁니다. |
| Alt+Enter | 풀스크린을 토글합니다. |
| Esc | 종료 |
아래의 조작키는 특정한 메시 인스턴스를 보는 뷰 모드일때 사용됩니다.
The following controls are valid when you are viewing a specific mesh instance.
| Key | Action |
|---|---|
| C | 유저 조작 모드로 전환 합니다. |
| W | 앞으로 이동합니다. |
| A,D | 왼쪽 오른쪽으로 회전합니다. |
| W+Shift | 달리기 모드로 고정합니다. |
Pitfalls and Alternatives
이 예제중 일부는 다르게 동작할 수도 있습니다. (왠지 여기부터는 다른 사람이 쓴 글인듯... 어렵다 -_-)
Some of what this sample does can be done differently, with different trade-offs.
모든 Tiny 인스턴스들은 같은 계층구조 매트릭스 프레임을 공유하고 있기 때문에, 특정한 인스턴스의 시간을 진행 시키고, 다른 인스턴스를 이동하기 전에 렌더를 해야만 합니다. 일반적으로 이러한 것은 문제를 발생하지는 않지만, 처음에 모든 인스턴스들에 대해 매트릭스를 업데이트 하고 렌더하기를 원한다면, 복제시에 다른 매트릭스 셋으로 새로운 애니메이션 컨트롤러 포인트를 만들어야 합니다. 이러한 방법은, 각각의 애니메이션 컨트롤러가 자신들 만의 고유한 매트릭스 셋을 가지고 덮어쓰는 방법이 아니기 때문에, 더욱 많은 메모리가 소비됩니다.
Because all instances of Tiny share the same frame hierarchy matrices, the sample must advance time on a specific instance and render it before moving on to another instance. Generally, this does not present a problem. However, if an application wishes to update the matrices for all of the instances first then render them, it can make new animation controllers point to a different set of matrices when cloning. This way, each animation controller has its own set of matrices to work with, and overwriting will not occur, at the expense of more memory usage.
스키닝 버텍스 셰이더에 있는 매트릭스 팔레트가 매트릭스 어레이라는 것도 형편 없습니다. 그리고, 당연한듯 중요한 상수 레지스터 갯수를 정한것도 형편 없습니다. 애플리케이션은 충분하지 않은 상수 레지스터 때문에 문제를 경험하게 될것입니다. 또한 애플리케이션은 보다 작은 매트릭스 팔레트로 작업할 수 있습니다. CMultiAnim 내부에 있는 이펙트 오브젝트를 생성할때 MATRIX_PALETTE_SIZE_DEFAULT 값을 보다 작게 설정하면 되는거죠. 이러한 접근법의 문제는 메시가 보다 많은 서브셋을 가져야 한다는 것입니다. 서브셋은 따로따로 그리기 위해 필요한 것들 입니다. (역주: 서브셋이 많아 진단 얘기는 DrawPrimitive 호출횟수가 늘어난다는 이야기 입니다. 최적화의 기본은 이 호출을 최소화 하는 것이 시작입니다.)
It is also worth noting that the matrix palette in the skinning vertex shader is an array of matrices, and naturally takes up a significant number of constant registers. An application may experience the problem of insufficient constant registers for other use. The application can work with a smaller-sized matrix palette by setting the MATRIX_PALETTE_SIZE_DEFAULT shader #define to a smaller value when creating the effect object in CMultiAnim. The drawback of this approach is that the mesh may contain more subsets that need to be drawn separately.
이 예제는 애니메이션 메시 렌더링 방법을 중점적으로 보여주기위한 예제임을 명심해 주세요. 비록 코드가 확장 가능하다 하더라도 이 예제는 스태틱 메시들을 처리하진 않습니다.
Note that this sample specifically demonstrates rendering animated meshes. It does not handle static meshes, although the code can be extended to do that.
다른 문제점은 Direct3D 디바이스가 릴리즈되고 재생성 될때, 애니메이션 컨트롤러의 상태가 완전히 보존되어야 합니다. 이러한 일은 일반적으로 HAL에서 REF로 넘어가거나, 또는 그 반대거나, 하나의 Direct3D 디바이스가 다른 듀얼모니터 시스템으로 가는 경우가 있을수 있습니다. 멀티 애니메이션시, Direct3D 디바이스가 릴리즈 되면, 모든 애니메이션 컨트롤러들도 역시 릴리즈 됩니다. 그런다음, 새로운 Direct3D 디바이스가 초기화 될때 애니메이션 컨트롤러들은 재 생성됩니다. 이것은 한가지 문제점이 있습니다. 왜냐하면, 애니메이션 컨트롤러들에 저장된 상태값들이 모두 소실되기 때문이죠. 어떤 애니메이션 트랙이 재생 되어야 하는지, 어떤 트랙이 활성화 되었는지, 속도, 가중치, 등에 대한 정보들을 잃어 버리게 됩니다. 이러한 문제점을 해결하기위해서는 Direct3D 오브젝트를 릴리즈 하기전에(DeleteDeviceObjects for MultiAnimation) 애니메이션 컨트롤러의 상태값들을 가져온후, 버퍼에 저장해두어야 합니다. 그런다음, 애니메이션 컨트롤러가 재 생성 된후에(in InitDeviceObjects for MultiAnimation), 버퍼에 저장된 상태값들을 애니메이션 컨트롤러에 넣어서 복구해야 합니다. 애니메이션 컨트롤러 각각에 대해서 아래의 상태가 저장되어야 합니다.
Another task that the sample only does partially is the complete preservation of animation controllers' states when the Direct3D device is released and re-created. This generally happens when the application switches from HAL to REF device (see Device Types), or vice versa, or from one Direct3D device to the other on a dual-monitor system. In MultiAnimation, when the Direct3D device is released, all animation controllers are also released. The animation controllers are then re-created when the sample initializes the new Direct3D device. This presents a problem, because animation states stored in animation controllers are lost. The sample loses the knowledge of what animation each track is playing, what tracks are enabled, and the speed and weight of each track before the old device is released. To remedy this issue, an application should, before releasing Direct3D objects in its cleanup function (DeleteDeviceObjects for MultiAnimation), retrieve the animation controllers' states and save them in a buffer. Then, after it re-creates the animation controllers that it needs (in InitDeviceObjects for MultiAnimation), it should restore the animation controllers' states from the buffer. For each animation controller, the following states should be saved.
- ID3DXAnimationController::GetTime 으로 얻어낸 애니메이션 컨트롤러의 현재 시간.
- 재생 트랙 애니메이션 셋 이름. 이 정보는 ID3DXAnimationController::GetTrackAnimationSet, ID3DXAnimationSet::GetName 을 호출해서 얻어낼 수 있습니다.
- 모든 트랙에 대한 상세설명(description). ID3DXAnimationController::GetTrackDesc 를 호출해서 얻어낼수 있습니다.
- 모든트랙에 대한 현재 이벤트들, 만약 이벤트가 처리중이라면, ID3DXAnimationController::GetCurrentTrackEvent, ID3DXAnimationController::GetEventDesc 함수로 얻어낼 수 있습니다.
- 모든 키 트랙 이벤트들, 모든 이벤트 키를 얻기위해서, 우선 hEvent를 NULL로 설정하고 ID3DXAnimationController::GetUpcomingTrackEvent 함수를 호출합니다. 이 함수는 다음에 발생할 키 이벤트 핸들을 반환합니다. 그런다음, 이 이벤트에 대한 상세설명(description)을 얻기위해, 얻어낸 핸들을 ID3DXAnimationController::GetEventDesc 함수에 넣어서 호출합니다. 그런다음, 이전에 ID3DXAnimationController::GetUpcomingTrackEvent 호출로 반환된 핸들을 가지고, ID3DXAnimationController::GetUpcomingTrackEvent 함수를 다시 호출합니다. 이렇게 하면, 두번째 키 이벤트를 위핸 핸들을 가져올수 있습니다. 모든 키 이벤트를 가져올때까지 이 과정을 반복 하세요.
- ID3DXAnimationController::GetPriorityBlend 함수를 호출해서 애니메이션 컨트롤러에 대한 현재 priority blend를 가져옵니다.
- ID3DXAnimationController::GetCurrentPriorityBlend 함수와 ID3DXAnimationController::GetEventDesc. 함수를 호출해보고, 한놈이 현재 돌아가고 있으면, 현재 priority blend 이벤트도 얻어와야 합니다.
- 모든 키 priority blend 이벤트들. 이것들은 ID3DXAnimationController::GetUpcomingPriorityBlend 함수와 ID3DXAnimationController::GetEventDesc 함수를 호출해서 얻어낼 수 있습니다. 모든 이벤트들을 처리할 수 있는 방법은 트랙에서 키 이벤트들을 처리하는 방식과 유사합니다.
좀 단순화 시키려는 의도로, MultiAnimation 샘플은 모든 애니메이션 컨트롤러의 상태를 보존하진 않습니다. 하지만 오히려, 이놈의 샘플에서는 재생중인 현재 트랙에 대한 애니메이션 셋 만을 저장하고 있습니다. 그 결과, 만약 Direct3D 디바이스 오브젝트가 애니메이션 변환중에 릴리즈되고 재 생성되면, 애니메이션은 변환 전후와 같게 나타나지 않을 것입니다; 변환이 완료되었다면 나타 나겠지만... 게다가, 모든 키 이벤트들은 재 초기화 된 후에 복구되지 않을 것입니다.
For reasons of simplicity, MultiAnimation does not preserve all of its animation controllers' states, but rather it saves only the animation set that its current track is playing. Consequently, if the Direct3D device object is released and re-created during an animation transition, the animation will not appear the same after the transition as before; it will appear as if the transition has been completed. In addition, all keyed events will not be restored after re-initialization.



이올린에 북마크하기