Skeletal Animation 理論與實踐
今天要學習的是遊戲開發中,特別是gameplay開發中非常重要的部分n– 骨骼動畫。
nn
首先會學習一些原理,還有動畫製作的pipeline,實踐方面會包括動畫載入,GPU Skinning,animation blend, Addittive animation等等。
nnTool: VS2015 + blender 2.78 + OpenGL + SDL2
Keyframenanimation 和Skeletalnanimation
nn
Keyframe定義:在關鍵的幾個時間點定義主體的姿態,稱為關鍵幀,
nnnn中間的部分用插值(Linear/Spline)得出
nnnn
在遊戲中,KeyFrame就是指的是一系列Mesh的頂點位置,Mesh的當前位置信息是通過上一個keyframe和下一個keyframe插值而來。在Uncharted4裡面,有使用Keyframe animation來做人群動畫的。
nnnn相對於Keyframe animation ,Skeletal animation的思想是將有動畫的物體分為兩個部分:用於渲染的Mesh 和n用與運動的骨骼(通常稱為skeleton 或者 rig)。Mesh上的頂點和骨骼通常存在著一對多的對應關係,當骨骼發生transform的變化的時候,Mesh上的頂點會根據對應關係得出新的位置。
nnnn在遊戲引擎架構這本書中,一個很有用的思想就是將skeletal animation視為一種數據壓縮技術。選擇動畫技術的目的,是能夠提供最佳壓縮而又不會產生不能接受的視覺瑕疵,KeyFrameAnimation中的動畫數據當然是相當巨大的,骨骼動畫就能提供最佳的壓縮,因為每個關節的移動會擴大至多個頂點的移動。
nnnnCharacternCreation Pipeline
nn
在深入技術之前,我們先來了解Character Creation Pipeline。一下一個常規的Character製作的pipeline大概是這樣
nnnn這些階段有些是互相依賴的,有些又是可以並行的,每個階段的詳細說明可以參考這裡。
nnnn建模就不說了,和動畫相關的主要是後面的三個個部分,第一個部分為Rigging,主要是根據模型還有角色可能要做的動作製作出一套對應的骨骼,這套骨骼包含了一些joints和bones,同時還定義了joints的自由度,約束,還包括了一些IK等等。通常在建模的時候,建模師會將角色擺成一個Binding Pos,也叫T pos,目的是方便做Rig的人,因為這樣放joints和bones的時候會更方便。
Skinng就是就Mesh的頂點綁定到對應的骨骼的上,當骨骼運動的時候,Mesh會根據綁定的骨骼運動到相應的位置。關於Binding Pos更詳細的解釋可以參考:What Is a Binding Pose in Character Animation?
nn在完成Rigging和Skinning之後,動畫師就可以去k動畫了,也可能通過一些高級手段,比如動作捕捉來製作動畫。
nnMd5格式說明
nnMd5是ID software所推出的一種動畫格式。一個包含動畫的md5資源包含了兩個文件:
nn.md5mesh 文件:定義了mesh和材質還有骨骼信息.
nn.md5anim文件: 定義了一段對應於md5mesh文件的動畫.
nn注意,兩個文件中的骨骼信息必須一致。
nn
對於這兩個格式的詳細ie
nn一個md5mesh的格式如下
nnMD5Version <int:version>ncommandline <string:commandline>nnnumJoints <int:numJoints>nnumMeshes <int:numMeshes>nnjoints {n<string:name> <int:parentIndex> ( <vec3:position> ) ( <vec3:orientation> )n...n}nnmesh {nshader <string:texture>nnnumverts <int:numVerts>nvert <int:vertexIndex> ( <vec2:texCoords> ) <int:startWeight> <int:weightCount>n...nnnumtris <int:numTriangles>ntri <int:triangleIndex> <int:vertIndex0> <int:vertIndex1> <int:vertIndex2>n...nnnumweights <int:numWeights>nweight <int:weightIndex> <int:jointIndex> <float:weightBias> ( <vec3:weightPosition> )n...nn}n
有點像obj文件,但是還包含了joint的信息和每個頂點的權重信息。
nn一個md5anim文件如下
nnMD5Version <int:version>ncommandline <string:commandline>nnumFrames <int:numFrames>nnumJoints <int:numJoints>nframeRate <int:frameRate>nnumAnimatedComponents <int:numAnimatedComponents>nhierarchy {n<string:jointName> <int:parentIndex> <int:flags> <int:startIndex>n...n}nbounds {n( vec3:boundMin ) ( vec3:boundMax )n...n}nbaseframe {n( vec3:position ) ( vec3:orientation )n...n}nframe <int:frameNum> {n<float:frameData> ...n}n
其中Hierarchy定義了骨骼的父子結構。Bounds定義了每一幀mesh的aabb。Baseframe定義了骨骼的初始位置。Frame定義了每一幀的骨骼信息。
nn文件的載入可以自己去寫paser,這裡為了方便起見就直接用assimp來處理。但是assimp載入還是有很多隱晦的默認規則,比如
nn1) 如果load(」soldier.md5mesh 「),默認會把載入一個soldier.md5anim的文件,所以如果要載入多個anim文件,就要手動去指定;
nn2) 如果shader那一行制定的貼圖沒有擴展名,則默認為name_d.tga為diffuse貼圖。
骨骼和矩陣數學
nn
一個帶骨骼的mesh如下圖
骨骼可理解為一個坐標空間,關節可理解為骨骼坐標空間的原點。關節的位置由它在父骨骼坐標空間中的位置描述。
nn一個bone可以定義如下
nnclass Bonen{ntpublic:ntBone(const SkeletonNodeData& node);nntstring m_name;ntunsigned int m_parentID;ntvector<unsigned int> m_childID;ntglm::mat4 m_transform;n};n
骨骼就是坐標空間,骨骼層次就是嵌套的坐標空間。關節只是描述骨骼的位置即骨骼自己的坐標空間原點在其父空間中的位置,繞關節旋轉是指骨骼坐標空間(包括所有子空間)自身的旋轉。
nn直接看一下Gpunskinning 的 vertex shader
nn#version 330 coren//skinningShader.vertnn const int MAX_BONES = 100;nn uniform mat4 modelMatrix;n uniform mat4 projModelViewMatrix;t// (projection x view x model) matrixn uniform mat3 normalMatrix;n uniform mat4 lightProjModelViewMatrix;nn uniform mat4 boneMatrix[MAX_BONES];nnn in vec3 in_position;n in vec3 in_normal;n in vec2 in_textCoord;nn in ivec4 in_boneIDs;n in vec4 in_weights;nn out Datan {ntvec2 textCoord;ntvec3 position;ntvec3 normal;ntvec4 lightVertexPosition; //position of vertex in light space.n } n DataOut; nnnvoid main() n{ nn mat4 boneTransform = boneMatrix[in_boneIDs[0]] * in_weights[0];n boneTransform += boneMatrix[in_boneIDs[1]] * in_weights[1];n boneTransform += boneMatrix[in_boneIDs[2]] * in_weights[2];n boneTransform += boneMatrix[in_boneIDs[3]] * in_weights[3];n//轉換到bone 空間下n vec4 vertex4 = boneTransform * vec4(in_position, 1.0); n n DataOut.position = vec3(boneTransform * modelMatrix * vertex4);n DataOut.normal = normalize( mat3(boneTransform) * normalMatrix * in_normal);n DataOut.textCoord = in_textCoord;n//轉換到世界空間下n DataOut.lightVertexPosition = lightProjModelViewMatrix * vertex4;n gl_Position = projModelViewMatrix * vertex4;n}n
簡直超級簡單!就是
nn最終位置n= 原始位置 * 求和(矩陣* 權重)
nn這個過程就是GPUnSkinning。這裡注意到GPU Skinning的一個小小的限制,就是頂點的權重數不超過4,這對於一般的遊戲來說已經足夠了。(或者傳兩個vec4來處理多於4個權重數的情況)
nn
空間變化應當是:
nnModel space -> Bone Space ->World Space
nnShader中boneMatrixn就是經插值之後得到的matrix矩陣了。
nn再看下keyframe,每一個keyframe其實就是一堆骨骼的position和rotation的組合,動畫中的每一秒都有24個這樣的keyframe
nnclass KeyFramen{ntpublic:ntKeyFrame(double time,nttt const vector<glm::vec3>& boneTranslation, nttt const vector<glm::quat>& boneRotation);nntinline double getTime() const { return m_time; }ntinline unsigned int getBoneCount() const { return m_boneTranslation.size(); }nntinline const glm::vec3& getBoneTranslation(unsigned int boneID) const nt{nttreturn m_boneTranslation[boneID];nt}nntinline const glm::quat& getBoneRotation(unsigned int boneID) const nt{nttreturn m_boneRotation[boneID];nt}nnntprivate:ntdouble m_time;ntvector<glm::vec3> m_boneTranslation;ntvector<glm::quat> m_boneRotation;n};n
在讀取的時候MD5anim文件的的時候,每一幀中每個骨骼對應的是六個數字,
nn( vec3:position ) ( vec3:orientationn)
nn對應的分別是vector3的位置,還有一個四元素表示旋轉,其中旋轉只給了三個分量,最後一個需要再載入的時候計算出來。
nnvoid ComputeQuatW( glm::quat& quat )n{n float t = 1.0f - ( quat.x * quat.x ) - ( quat.y * quat.y ) - ( quat.z * quat.z );n if ( t < 0.0f )n {n quat.w = 0.0f;n }n elsen {n quat.w = -sqrtf(t);n }n}n
看一下插值計算的過程,注意,這裡的計算一般是放在Cpu處理的.為了計算出某個時刻骨骼最終的變換矩陣,我們需要根據當前時間對關鍵幀之間三個變換數組進行插值,並將這些變換組合成一個矩陣。這些完成之後我們需要在骨骼樹種找到對應的骨骼節點並遍歷其父節點,之後我們對它的每個父節點都做同樣的插值處理,並將這些變換矩陣乘起來即可。對應的函數實現如下
nn
void AnimatedMeshGL::getBoneTransformation(double timeEllasped, const Skeleton& skeleton, AnimationComponent& animComponent, vector<glm::mat4>& finalTransformList)n{nt//獲取當前的動畫序列ntauto& anim = skeleton.getAnimation(animComponent.m_animationIndex);ntunsigned int keyFrameCount = anim.getKeyFrameCount();ntdouble ticksPerSecond = anim.getTicksPerSecond() != 0 ? anim.getTicksPerSecond() : 25.0f;nt//總共運行的幀數ntdouble timeInTicks = timeEllasped * ticksPerSecond;nt//取余操作ntdouble animationTime = fmod(timeInTicks, anim.getDuration());nnt//算出當前動畫的上一幀和下一幀,後面用ntfor (unsigned int i = animComponent.m_startFrameIndex; i < keyFrameCount - 1; i++)nt{nttif (animationTime < anim.getKeyFrame(i + 1).getTime()) {ntttanimComponent.m_startFrameIndex = i;ntttbreak;ntt}nt}ntanimComponent.m_endFrameIndex = (animComponent.m_startFrameIndex + 1) % keyFrameCount;nnt//計算要插值的時間點ntdouble deltaTime = (anim.getKeyFrame(animComponent.m_endFrameIndex).getTime() - anim.getKeyFrame(animComponent.m_startFrameIndex).getTime());ntanimComponent.m_remainingTime = (animationTime - anim.getKeyFrame(animComponent.m_startFrameIndex).getTime()) / deltaTime;nntglm::mat4 identity(1.0f);nttransformBone(0, identity, skeleton, animComponent);nnt//將計算的結果保存一下ntfor (unsigned int i = 0, maxBones = m_boneFinalTransform.size(); i < maxBones; i++)nttfinalTransformList[i] = m_boneFinalTransform[i];nnt//最後一幀和第一幀重合ntif (animComponent.m_startFrameIndex == keyFrameCount - 2)nttanimComponent.m_startFrameIndex = 0;n}n
關鍵函數transformBone
nnvoid AnimatedMeshGL::transformBone(unsigned int boneID, const glm::mat4& parentTransform, const Skeleton& skeleton, const AnimationComponent& animComponent)n{nt//從0號bone開始遞歸計算,初始parentTransform為單位陣ntconst Bone& node = skeleton.getBoneNode(boneID);nt//取出上一幀和下一幀的信息ntconst Animation& animation = skeleton.getAnimation(animComponent.m_animationIndex);ntconst KeyFrame& startKeyframe = animation.getKeyFrame(animComponent.m_startFrameIndex);ntconst KeyFrame& endKeyframe = animation.getKeyFrame(animComponent.m_endFrameIndex);nt//取出根骨的offset matrixntconst glm::mat4& inverseTransform = skeleton.getInverseTransform();nt//骨骼的當前位置ntglm::mat4 nodeTransform = node.m_transform;nntunsigned int boneIndex = animation.getBoneIndex(node.m_name);ntif (boneIndex != ~0)nt{ntt//m_remainingTime作為插值的參數nttfloat factor = float(animComponent.m_remainingTime);nntt//四元素的slerp對旋轉進行插值nttauto rotQuat = glm::slerp(startKeyframe.getBoneRotation(boneIndex),ntttendKeyframe.getBoneRotation(boneIndex), factor);ntt//Vector3的插值得到位置nttauto posVec = BlendVec3(startKeyframe.getBoneTranslation(boneIndex),ntttendKeyframe.getBoneTranslation(boneIndex), factor);nntt//組合出boone矩陣nttglm::mat4 translation = glm::translate(glm::mat4(), posVec);nttnodeTransform = translation * glm::toMat4(rotQuat);nt}nnt//和父骨骼的矩陣相乘ntglm::mat4 localFinalTransform = parentTransform * nodeTransform;nnt//計算最終的transformntauto iter2 = m_boneNameToIndex.find(node.m_name);ntif (iter2 != m_boneNameToIndex.end())nt{ntt//boneOffset是每個骨骼的offetMatrix 後面有說明nttglm::mat4 boneOffset = glm::make_mat4(m_bone[iter2->second].m_offsetMatrix.m_m16);nttm_boneFinalTransform[iter2->second] = inverseTransform * localFinalTransform * boneOffset;nt}nnt//遞歸處理child bonentfor (unsigned int i = 0; i < node.m_childID.size(); i++)ntttransformBone(node.m_childID[i], localFinalTransform, skeleton, animComponent);n}n
注意其中的插值是分別對position,rotation進行插值。
nn由於骨骼的 Transform Matrix (作用是將頂點從骨骼空間變換到上層空間)是基於其父骨骼空間的,只有根骨骼的 Transform 是基於世界空間的,所以要通過自下而上一層層 Transform 變換(如果使用行向量右乘矩陣,這個 Transform 的累積過程就是C=Mbone * Mfather * Mgrandpar *... *Mrootn) , 得到該骨骼在世界空間上的變換矩陣 - Combined Transform Matrix ,即通過這個矩陣可將頂點從骨骼空間變換到世界空間。那麼這個矩陣的逆矩陣就可以將世界空間中的頂點變換到某塊骨骼的骨骼空間。由於 Mesh 實際上就是定義在世界空間了,所以這個逆矩陣就是Bone Offset Matrix 。即Bone OffsetMatrix 就是骨骼在初始位置(沒有經過任何動畫改變)時將 bone 變換到世界空間的矩陣( CombinedTransformMatrix )的逆矩陣。
nn從結構上看, skeletal Animation的輸入主要有:動畫數據,骨骼數據,包含nSkin info 的 Mesh 數據,以及 Bone Offset Matrix 。
nn從過程上看,載入階段:載入並建立骨骼層次結構,計算或載入nBone Offset Matrix ,載入nMesh 數據和 Skin info (具體的實現 不同的引擎中可能都不一樣)。運行階段:根據時間從動畫數據中獲取骨骼當前時刻的nTransform Matrix ,調用nUpdateBoneMatrix 計算出各骨骼的nCombinedMatrix ,對於每個頂點根據 Skin info 進行 Skinning 計算出頂點的世界坐標,最終進行模型的渲染。
nn最終效果(假裝在動)
調整播放速度
nn
每個動畫片段都有一個局部時間線,裡面有動畫開始播放的時間,時間縮放比例R。通過調整時間比例R,就可以達到控制動畫播放縮率的效果。
nn
具體來說,對於每個AnimationnClip都有一個Animation Component用於記錄播放的信息
nnstruct AnimationComponentn{ntAnimationComponent(unsigned int animationIndex, unsigned int startFrameIndex);ntunsigned int m_animationIndex;ntunsigned int m_startFrameIndex;ntunsigned int m_endFrameIndex;ntdouble m_StartTimeMark;ntdouble m_PlaySpeed;ntdouble m_remainingTime;n};n
再採樣動畫的時候,將全局的時間映射到局部的時間
nndouble ticksPerSecond = anim.getTicksPerSecond() != 0 ? anim.getTicksPerSecond() : 25.0f;ndouble timeInTicks = timeEllasped * ticksPerSecond* animComponent.m_PlaySpeed;n
以0.25倍的速度播放相同的動畫效果如下
nnAnimationnblending
nn
動畫混合是把兩個或更多的輸入姿勢結合,產生骨骼的輸出姿勢,這樣就可以再不添加新動畫的情況下產生一些新的動畫,所需要付出的只是CPU上一些消耗。原理就是插值。對於兩個輸入姿勢Pa和Pb的情況,最終姿勢
nnP = (1-b)Pa + bPb
nn其中b為混合百分比,取值為(0,1).這裡所說的姿勢插值主要是一個4*4的矩陣進行插值,矩陣當然不能直接插值,所以要對位移和旋轉分別進行插值(有些引擎還會有scale插值),位置的插值用的Vector3::Lerp,旋轉就用四元素的SLerp。
nn下面以兩個Animation blending簡單的應用來實踐一下。
nn
Animation Crossfade
nn動畫的淡入淡出通常會應用在兩個動畫的切換,
nn關鍵函數貼一下
void AnimatedMeshGL::transformBoneBlend(unsigned int boneID, const glm::mat4& parentTransform, const Skeleton& skeleton, const AnimationComponent& animComponent1, const AnimationComponent& animComponent2, float blendFactor)n{ntconst Bone& node = skeleton.getBoneNode(boneID);nntconst Animation& animation1 = skeleton.getAnimation(animComponent1.m_animationIndex);ntconst KeyFrame& startKeyframe1 = animation1.getKeyFrame(animComponent1.m_startFrameIndex);ntconst KeyFrame& endKeyframe1 = animation1.getKeyFrame(animComponent1.m_endFrameIndex);nntconst Animation& animation2 = skeleton.getAnimation(animComponent2.m_animationIndex);ntconst KeyFrame& startKeyframe2 = animation2.getKeyFrame(animComponent2.m_startFrameIndex);ntconst KeyFrame& endKeyframe2 = animation2.getKeyFrame(animComponent2.m_endFrameIndex);nntconst glm::mat4& inverseTransform = skeleton.getInverseTransform();ntglm::mat4 nodeTransform = node.m_transform;nntunsigned int boneIndex = animation1.getBoneIndex(node.m_name);ntif (boneIndex != ~0)nt{nttfloat factor1 = float(animComponent1.m_remainingTime);nnttauto rotQuat1 = glm::slerp(startKeyframe1.getBoneRotation(boneIndex),ntttendKeyframe1.getBoneRotation(boneIndex), factor1);nnttauto posVec1 = BlendVec3(startKeyframe1.getBoneTranslation(boneIndex),ntttendKeyframe1.getBoneTranslation(boneIndex), factor1);nnttfloat factor2 = float(animComponent2.m_remainingTime);nttauto rotQuat2 = glm::slerp(startKeyframe2.getBoneRotation(boneIndex),ntttendKeyframe2.getBoneRotation(boneIndex), factor2);nnttauto posVec2 = BlendVec3(startKeyframe2.getBoneTranslation(boneIndex),ntttendKeyframe2.getBoneTranslation(boneIndex), factor2);nnttauto posVec = BlendVec3(posVec1, posVec2, blendFactor);nttauto rotQuat = glm::slerp(rotQuat1, rotQuat2, blendFactor);nnttglm::mat4 translation = glm::translate(glm::mat4(), posVec);nttnodeTransform = translation * glm::toMat4(rotQuat);nt}nntglm::mat4 localFinalTransform = parentTransform * nodeTransform;nntauto iter2 = m_boneNameToIndex.find(node.m_name);ntif (iter2 != m_boneNameToIndex.end())nt{nttglm::mat4 boneOffset = glm::make_mat4(m_bone[iter2->second].m_offsetMatrix.m_m16);nttm_boneFinalTransform[iter2->second] = inverseTransform * localFinalTransform * boneOffset;nt}nntfor (unsigned int i = 0; i < node.m_childID.size(); i++)ntttransformBoneBlend(node.m_childID[i], localFinalTransform, skeleton, animComponent1, animComponent2, blendFactor);n}n
nn
相比於之前單個動畫播放的函數只是再對應的地方添加了插值處理。看一下結果。
nn現在要實現行走到下蹲的blend,再沒有blend處理的情況下,效果是這樣的
可以看在動畫切換的時候,右腳被直接掰回來了,顯得很不自然。加了blend的動作會通過插值生成中間的動畫,比如下面的第二幀圖片
nnnn在第三人稱角色控制中經常會遇到的一個問題就是走路和跑步的動作blend,假設走路的動畫是一個3s的循環,跑步的是一個2s的循環,那麼他們兩個之間的blend就沒那麼簡單了,需要考慮的問題有1)走路的過程中需要隨時可以切換到run的動畫 2)blend的時候,要保證出的腳是一致的。要做到這兩點就要用到歸一化時間的概念。
nn對於一個AnimationnClip,無論它的時常T是多長,u = 0代表動畫開始,u=1代表動畫結束。在blend的時候,將walk的歸一化時間和run的歸一化時間匹配上,就可以做到完美的匹配。
nn
Additivenanimation
nn首先看下定義,對於兩個輸入片段S(SourceClip)和參考片段R(ReferenceClip),可以通過減法得到區別片段D(DifferenceClip),有D = S-R.
nn得出差別動畫之後,就可以將D按一定百分比混合到任意的不相干的動畫片段上,而不僅限於原來的參考片段。比如參考片段是角色征程跑步,而來源片段是疲憊下跑步,那麼區別片段只含有角色在疲憊的動畫,若將此片段應用至步行,結果會是一個疲憊下步行的結果。
nn下圖是神海2中將兩個動畫通過Additive blend 的方式生成新的Idle動畫。
nnnn在blend中隨便製做個前俯後仰的動畫,
這裡我們用一個最簡單的方法來處理S和R – 導出的動畫就是S,R就是動畫的第一幀。
nnUnity貌似也是這樣的處理方法
nnAdditive animations in Unity are always relative to thenfirst frame of the animation. This means that you will sometimes need to usenmore animations, or do a little more scripting than you would otherwise havendone, but you should always be able to obtain the same results in the end as ifnyou could have specified any frame as the reference.
nn實現上其實非常簡單,n
nnfloat factor1 = float(currentComponent.m_remainingTime);nnauto rotQuat1 = glm::slerp(startKeyframe1.getBoneRotation(boneIndex),ntendKeyframe1.getBoneRotation(boneIndex), factor1);nnauto posVec1 = BlendVec3(startKeyframe1.getBoneTranslation(boneIndex),ntendKeyframe1.getBoneTranslation(boneIndex), factor1);nnglm::vec3 sourceVec = sourceFrame.getBoneTranslation(boneIndex);nglm::quat sourceRot = sourceFrame.getBoneRotation(boneIndex);nnfloat factor2 = float(addComponent.m_remainingTime);nglm::quat rotQuat2 = glm::slerp(startKeyframe2.getBoneRotation(boneIndex),ntendKeyframe2.getBoneRotation(boneIndex), factor2);nnauto posVec2 = BlendVec3(startKeyframe2.getBoneTranslation(boneIndex),ntendKeyframe2.getBoneTranslation(boneIndex), factor2);nnglm::quat rotDiff = rotQuat2 * glm::inverse(sourceRot);nglm::vec3 posDiff = posVec2 - sourceVec;nauto rotQuat = rotQuat1* rotDiff;nauto posVec = posVec1 + posDiff;n
nn
效果
nnnn同樣還有使用additive animation的應用有tps遊戲里的瞄準混合等等。
nn和additive blend類似有一個混合方式是Partial blend,指的是身體的不同部位通過mask標記播放不同的動畫,比如各種情況下的揮手動作。Unity中對應的是Animation 中 Layermask 的使用。Partial blend 有兩個比較明顯的缺點:
1)兩個部分的動畫因為混合因子改變劇烈(0,1)動畫會看上去很突兀。
nn2)現實中人體的動作並不是完全獨立的,即晃動手臂的同時,其他的骨骼也會有相應的動畫。
nn所以通常會混合使用多種blend來處理角色動畫。
nn下面是uncharted2中是動畫層
nnnn小結
nn動畫作為GamePlay的基石,在國內的遊戲開發中通常得不到太大的重視,然後在3A遊戲的製作中,通常都會有一個專門的動畫團隊,甚至有專門負責動畫的TA,所以如果想把gameplay這塊的東西做好,細節做到位,建議還是認真了解下動畫的原理,製作的pipeline。
nn寫這篇東西花了很長的時間,一方面是最近事情比較多,另一方面,動畫這塊的東西實在非常之多,上面所寫的內容只是動畫裡面非常小的一些東西,很多內容,比如數據壓縮,Procedual Animation 都沒有提及,對於想深入了解引擎內部Animation實現的同學,非常建議認真讀一下Game Engine Architecture 中關於骨骼動畫的內容,書的作者就是做動畫系統出身的。最後放個彩蛋
參考
nnFast Skinning March 21st 2005 J.M.P. van Waveren ?n2005, Id Software, Inc.ipeLibne
nnFast Skinning March 21st 2005 J.M.P. van Waveren ?n2005, Id Software, Inc.ipeLibne
Game Engine Architecture
Animationsnin lwjglMD5模型的格式、導入與頂點蒙皮式骨骼動畫II
http://www.zwqxin.com/archives/opengl/model-md5-format-import-animation-2.htmlGraphicsnfor Games
MAKINGnDOOM 3 MODS : THE CODE
Doomnmd5 Model Loader
Loadingnand Animating MD5 Models with OpenGLKeyframe Animationn
Skeletal AnimationWhatnis Rigging?Optimizingnthe Rendering Pipeline of Animated Models Using the Intel Streaming SIMDnExtensionsopen source project
nnjulienr/scalamd5推薦閱讀:
※從零開始手敲次世代遊戲引擎(一)
※「千萬別學我!」之遊戲開發者的囧事(上)
※遊戲開發匯總 (Gu Lu's Blog)
※An Unnormal Normal Approach to 3D Modeling
※webgame為什麼總是倚重冷卻時間這個要素?