Bizonyos játékokban szerepelnek ilyen nevek, hogy reanimated corpse. Én mindig azt hittem, hogy azért ez a nevük, mert az első animációt elrontották és egy patchben újraanimálták (és nem szégyellik bevallani).
Később rájöttem, hogy a szó valami olyasmit jelent amikor nem természetes módon mozog a hulla (azaz nem marad nyugalmi állapotban, mint egyébként).
Újranyúzott csontváz-állat
A webfordítás szépségei után nézzük meg hogyan kell egy ilyen modellt betölteni. Eddig az egész betöltés egy függvényhívás volt, ami még most is igaz, de hozzá kell venni egy csomó varacskolást is. A függvény ami a betöltést elvégzi a
D3DXLoadMeshHierarchyFromX. Ennek paraméterben meg kell adni egy ID3DXAllocateHierarchy típusú pointert. Najó de az hol van? Sehol, neked kell megírni (pontosabban származtatni abból az interfészből).
CODE
class AnimatedMesh : public ID3DXAllocateHierarchy
{
public:
STDMETHOD(CreateMeshContainer)(
LPCSTR Name, const D3DXMESHDATA* meshData, const D3DXMATERIAL* materials,
const D3DXEFFECTINSTANCE* effectInstances, DWORD numMaterials,
const DWORD* adjacency, LPD3DXSKININFO skinInfo,
LPD3DXMESHCONTAINER* retNewMeshContainer );
STDMETHOD(CreateFrame)(LPCSTR Name, LPD3DXFRAME* retNewFrame);
STDMETHOD(DestroyFrame)(LPD3DXFRAME frameToFree);
STDMETHOD(DestroyMeshContainer)(LPD3DXMESHCONTAINER meshContainerToFree);
};
Ezt a négy metódust kell kötelező jelleggel leimplementálni. A DirectX frame-ekbe szervezi a csontokat és ezekből a háttérben egy fát épít, amit rekurzívan kell bejárni minden rajzoláshoz és updatehez. A kirajzolandó geometria adat a frame-en belül egy meshcontainer objektumban van. A metódusok implementációja nem igényel különösebb gondolkozást (copy-paste), azt viszont fontos megérteni, hogy hogyan működik ez az egész. CPU skinning esetében a vertexeket újra kell számolni, DE. Az .x fájlok esetében az animáció abszolút transzformációkat ad meg, tehát mindig az eredeti vertexekre kell elvégezni azt. Ezért az eredeti mesh-t meg kell tartani és egy másik bufferbe végezni el a transzformációt. GPU skinning esetében erre nyilván nincsen szükség; a vertex shaderben fogom elvégezni a transzformációkat. Ekkor viszont meg kell hívni a ConvertToIndexedBlendedMesh metódust, ami súlyokat (BLENDWEIGHT) és csont indexeket (BLENDINDICES) generál a mesh-nek. Ami mindkét esetre igaz, hogy kétfajta mesh-t különböztetünk meg: normál- és skinned mesh(container)-t. Előbbit teljesen átlagos módon kell kirajzolni, de a transzformációját a hierarchiában elfoglalt helye határozza meg. Utóbbira viszont alkalmazni kell n darab (itt vertexenként 4) csont transzformációját (ettől lesz "skin"). CPU-s esetben a UpdateSkinnedMesh metódus végzi ezt el, GPU-s esetben a vertex shader. A betöltés hívási fája nagyvonalakban az alábbi:
Rajzoláskor pedig az alábbi metódusok hívódnak meg:
Ezek után a rajzolás meghívja a rekurzív DrawFrame függvényt, ami bejárja a fát és kirajzolja a mesh-eket a DrawMeshContainer hívással. Ha normál mesh akkor kirajzolja, ha meg skinned, akkor elvégzi a transzformációkat (avagy beállítja a shader konstansokat) és úgy rajzolja ki. A felszabadítás hasonlóan történik mint a létrehozás, a D3DXFrameDestroy rekurzív függvény meghívásával.
Előkészületek
A fenti műveletek egyszerű elvégzéséhez az alap D3DXFRAME és D3DXMESHCONTAINER struktúrák nem biztosítanak elég tagváltozót, ezért ki kell bőviteni azokat. A frame-hez az alábbi két membert vettem hozzá:
CPU skinning
Ebben az esetben a GenerateSkinnedMesh metódus lemásolja a container eredeti mesh-ét a fenti extra változóként felvett
exSkinMesh-be. Érdekesebb a rajzolás, ugyanis lock-olni kell mind a két mesh-t, majd az eredetiből áttranszformálni a vertexeket a másikba:
CODE
// végső csont transzformációk kiszámolása
DWORD Bones = meshContainer->pSkinInfo->GetNumBones();
for( DWORD i = 0; i < Bones; ++i )
{
D3DXMatrixMultiply(&bonetransforms[i], &meshContainer->exBoneOffsets[i],
meshContainer->exFrameCombinedMatrixPointer[i]);
}
// vertexek transzformálása a megfelelő csontokkal
void* srcPtr = 0;
void* destPtr = 0;
meshContainer->MeshData.pMesh->LockVertexBuffer(D3DLOCK_READONLY, (void**)&srcPtr);
meshContainer->exSkinMesh->LockVertexBuffer(0, (void**)&destPtr);
{
meshContainer->pSkinInfo->UpdateSkinnedMesh(bonetransforms, NULL, srcPtr, destPtr);
}
meshContainer->exSkinMesh->UnlockVertexBuffer();
meshContainer->MeshData.pMesh->UnlockVertexBuffer();
// rajzolás
D3DXMATRIX id(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
device->SetTransform(D3DTS_WORLD, &id);
// ...
Fontos, hogy mivel kézzel transzformáltuk a mesh-t, a world mátrixot az identitásra kell állítani. Ez a kód arra az esetre vonatkozik, amikor skinned mesh-t kell rajzolni, a normál mesheknél természetesen a hozzá tartozó mátrixot kell beállítani world-nek. A subseteket mindkét esetben a material-ok határozzák meg.
GPU skinning
Szemben a fentivel itt a subsetek a felhasznált csontok szerint vannak csoportosítva, tehát a rajzolás is más. A mesh-t itt is subsetenként rajzoljuk, de
minden subsethez beállítjuk shader konstansként azt az k darab csont mátrixot ami a vertexeknél szóba jöhet.
Fontos a különbség: egy meshhez tartozhat akár 30 csont mátrix (matBones) de ebből egy vertexre (itt) legfeljebb 4 csont hathat (numBones illetve a BLENDINDICES vertex attribútum).
CODE
LPD3DXBONECOMBINATION pBoneComb = reinterpret_cast<LPD3DXBONECOMBINATION>(
meshContainer->exBoneCombinationBuff->GetBufferPointer());
for( DWORD iAttrib = 0; iAttrib < meshContainer->exNumAttributeGroups; ++iAttrib )
{
for( DWORD iPaletteEntry = 0;
iPaletteEntry < meshContainer->exNumBoneMatrices;
++iPaletteEntry )
{
DWORD iMatrixIndex = pBoneComb[iAttrib].BoneId[iPaletteEntry];
if( iMatrixIndex != UINT_MAX )
{
D3DXMatrixMultiply(
&bonetransforms[iPaletteEntry],
&meshContainer->exBoneOffsets[iMatrixIndex],
meshContainer->exFrameCombinedMatrixPointer[iMatrixIndex]);
}
}
Effect->SetMatrixArray("matBones", bonetransforms, meshContainer->exNumBoneMatrices);
Effect->SetInt("numBones", meshContainer->exNumInfl - 1);
// ...
}
A transzformációt a vertex shader végzi el. A súlyok összege mindig 1 kell legyen, tehát az utolsó = (1 - előzők_összege), sőt, ha esetleg néhány súlyt kihagysz (MD5-nél lazán előfordulhat), akkor is ezt kell tenni, hiába van érvényes értéked az utolsó súlyra. CODE
float4x3 matBones[MAX_MATRICES];
float4x4 matViewProj;
int numBones;
void vs_main(
in out float4 pos : POSITION,
in float4 weights : BLENDWEIGHT,
in float4 indices : BLENDINDICES,
in float3 norm : NORMAL,
in out float2 tex : TEXCOORD0,
uniform int bones)
{
float3 bpos = 0.0f;
float3 bnorm = 0.0f;
// kompatibilitás bizonyos kártyákkal
int4 ind = D3DCOLORtoUBYTE4(indices);
int blendindices[4] = (int[4])ind;
float blendweights[4] = (float[4])weights;
float last = 0;
for( int i = 0; i < bones - 1; ++i )
{
last += blendweights[i];
bpos += mul(pos, matBones[blendindices[i]]) * blendweights[i];
bnorm += mul(norm, matBones[blendindices[i]]) * blendweights[i];
}
// az utolsót mindig így kell
last = 1 - last;
bpos += mul(pos, matBones[blendindices[bones - 1]]) * last;
bnorm += mul(norm, matBones[blendindices[bones - 1]]) * last;
pos = mul(float4(bpos.xyz, 1), matViewProj);
}
A shaderben egy érdekes dolgot csinál az SDK-s példa: a vertexekre ható csontok száma szerint más vertex shadert hív meg. Ezt úgy lehet megtenni, hogy egy külső paraméter szerint többször lefordítod a vertex shadert, azaz négy program van, amik csak abban különböznek, hogy hányszor fut le a ciklus. CODE
vertexshader vsarray[4] =
{
compile vs_2_0 vs_main(1),
compile vs_2_0 vs_main(2),
compile vs_2_0 vs_main(3),
compile vs_2_0 vs_main(4)
};
technique skinning
{
pass p0
{
vertexshader = vsarray[numBones];
pixelshader = compile ps_2_0 ps_main();
}
}
A pixel shadert mindenki el tudja képzelni: kiadja a textúra színét. Megjegyezném, hogy nem csak négy súly hathat egy vertexre, de négynél többet körülményes kezelni (viszont van BLENDWEIGHTn és BLENDINDICESn szemantika).
Blending két animáció között
Maga az animálás egy egyszerű controller->AdvanceTime() hívás ezért arról külön nem írok semmit.
A kontrollernek viszont van egy rakás egyéb metódusa amit nem árt letisztázni.
CODE
LPD3DXANIMATIONSET set = NULL;
UINT newtrack = (currenttrack == 0 ? 1 : 0);
double transitiontime = 0.25f;
controller->GetAnimationSet(index, &set);
controller->SetTrackAnimationSet(newtrack, set);
set->Release();
controller->UnkeyAllTrackEvents(currenttrack);
controller->UnkeyAllTrackEvents(newtrack);
// sima átmenet két animáció között
controller->KeyTrackEnable(currenttrack, false, currenttime + transitiontime);
controller->KeyTrackSpeed(currenttrack, 0.0f, currenttime, transitiontime, D3DXTRANSITION_LINEAR);
controller->KeyTrackWeight(currenttrack, 0.0f, currenttime, transitiontime, D3DXTRANSITION_LINEAR);
controller->SetTrackEnable(newtrack, true);
controller->KeyTrackSpeed(newtrack, 1.0f, currenttime, transitiontime, D3DXTRANSITION_LINEAR);
controller->KeyTrackWeight(newtrack, 1.0f, currenttime, transitiontime, D3DXTRANSITION_LINEAR);
currenttrack = newtrack;
currentanim = index;
Egy animációnak az objektum egy animáció halmazt feleltet meg (aminek az elemei nyilván az animáció lépései). Ezt el kell kérni és beállitani az új track-re. A sima átmenetet pedig úgy lehet elérni, hogy a régi és az új track-re beállítunk kulcsokat, nevezetesen a régire egy olyat ami mostantól számítva transitiontime idő alatt az aktuális sebességböl (KeyTrackSpeed) és súlyból (KeyTrackWeight) 0-ba megy át (lineáris interpoláció!), az újra pedig ugyanez de 1-ig. Nyilván az átmenet után a régi tracket ki kell kapcsolni (KeyTrackEnable). Ennek az az eredménye, hogy például az átmenetidő felénél félig a régi animáció érvényesül, félig meg az új. Ennél korábban még a régi dominál, utána meg már az új (és a súlyok összege végig 1). Ez általában jó, de előfordulhat olyan eset amikor pont úgy blendelődik össze két animáció, hogy a karakter mindkét lába a levegőben van és ettől ugrik egyet. Tehát nem teljesen mindegy, hogy mikor blendelsz.
Animált objektumok példányosítása
Szebben nem tudom lefordítani az instancingot. Ezt a fogalmat kicsit rendbe kell rakni, mert általában a hardware instancing-ot értik alatta, amit viszont nem támogat minden kártya. Én viszont most a konyhai instancing-ra gondolok,
azaz más world mátrixxal kirajzolod az objektumot mégegyszer. Az animált objektumoknál ez azért probléma, mert minden példány a saját animációs fázisában kell legyen és nem egy globálisban.
CODE
void AnimatedMesh::CloneFrames(LPD3DXFRAME from, LPD3DXFRAME fromparent, LPD3DXFRAME to, LPD3DXFRAME toparent)
{
// ...
if( from->pFrameSibling )
{
LPD3DXFRAME tosibling = NULL;
CreateFrame(from->pFrameSibling->Name, &tosibling);
D3DXFrameAppendChild(toparent, tosibling);
CloneFrames(from->pFrameSibling, fromparent, tosibling, toparent);
}
if( from->pFrameFirstChild )
{
LPD3DXFRAME tochild = NULL;
CreateFrame(from->pFrameFirstChild->Name, &tochild);
D3DXFrameAppendChild(to, tochild);
CloneFrames(from->pFrameFirstChild, from, tochild, to);
}
}
Amit kicsit jobban végig kell gondolni az a meshcontainer-ek klónozása. Az világos, hogy az előkészületekben hozzáadott extra mezőket neked kell lemásolni. Ami nem világos, hogy a normál mezők közül miket kell. Rossz hír, hogy a DirectX ebben nem segít. A következőt csináltam én:
Más modellformátumok
Az .x-en kívül még az MD5-ről tudok írkálni valamit (ez a Doom 3 és Quake 4 modellformátuma), mert azt leimplementáltam az engineben (de ebben a cikkben nincs benne a kód).
Kicsit máshogy közelíti meg a dolgot: a csontokat joint-nak hívja és mátrix helyett kvaterniókat használ. Ami egy érdekesség, hogy nem kell külön vertex buffert
lefoglalni a CPU skinning-hez, mert egy weight buffer-ben van eltárolva a kezdő pozíció. Kvaterniók esetében a transzformáció az alábbi módon történik:
Nyilván × itt kvaterniószorzást jelent, -1 pedig konjugáltat. Amit picit nehéz ezzel megcsinálni az a GPU skinning, ugyanis vertexenként több súly is lehet, sőt ezeknek már pozíciója is van. Egy másik probléma, hogy a csontok nem subsetenként vannak megadva, hanem egyben, tehát egy külön algoritmussal kell újrasubsetelni a mesh-t, hogy 110 helyett elég legyen mondjuk 30 csontot beállitani egy subset rajzolásához. Egy külön cikkben részletezem majd, hogy hogyan lehet átkonvertálni az itt leírt megközelítésre. Más animációt támogató formátumok még az .fbx, ami zárt forrású tehát az FBX SDK-n kivül mással nem lehet betölteni, cserébe viszont jóldefiniált és közvetlenül tudja exportálni/importálni a 3ds MAX. Ezen kívül van még a COLLADA, ami egy XML alapú formátum, viszont nem konkrétan modelleket, hanem egy egész jelenetet ír le, ezért az implementációja egy pain in the ass (hacsak nem hagysz ki minden mást).
Summarum
Az eddigiekhez képest ez egy elég nehéz része a DirectX-nek, amit fejből sokkal nehezebb megírni (szemben az előző fejezetekben látott dolgokkal).
Éppen ezért nem is írtam túl sok kódrészletet, mivel nem túl informatívak (és sok részt én is az SDK-ból copyztam ki).
A kód letölthetö itt. Höfö:
|