想要开发一个好用的渲染引擎,加载/编辑模型和使用骨骼动画的功能是必不可少的,由于市面上的模型格式过多并且标准也互不统一,所以想要自己写出读取模型的算法是十分困难的,因此可以借助好用的模型加载库来完成这个工作,Assimp就是一个开源且支持格式较多的模型加载库,官方声称Assimp可以加载的模型格式有40种之多,但是实际上在加载一些模型格式时会时常出现错误,经本人测试部分fbx,部分3ds和所有obj模型能够成功加载的,由于fbx拥有骨骼动画的功能,所以我基本以使用fbx为主(但想要加载全部fbx模型还是只能用FBX SDK完成)。
Assimp架构
Assimp库使用的架构是典型的树状结构,而这个架构在模型加载中也是比较好用的,因为它足够清晰,也便于理清模型当中部件的父子关系,下图简单地呈现了Assimp的架构:
Struct
Content
aiScene
mRootNode,mMeshes,mMaterials,mAnimations,mTextures,mLights,mCameras
aiNode
mTransformation,mParent,mChildren,mMeshes(索引)
aiMesh
mVertices,mNormals,mTangents,mBitangents,mColors,mTextureCoords,mFaces,mBones,mAnimMeshes
aiFace
mIndices
aiMaterial
GetTextureCount(…),GetTexture(…)
aiAnimation
mDuration,mTicksPerSecond,mChannels,mMeshChannels
aiNodeAnim
mNodeName,mPositionKeys,mRotationKeys,mScalingKeys,mPostState
Model类
Model类装载了加载模型的所有方法,后面我们的所有代码都会添加至这个类中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 struct MaterialInfo { UINT diffuseMaps; }; class Mesh {public : struct RenderInfo { std::vector<Vertex> vertices; std::vector<UINT> indices; }; std::vector<Vertex> vertices; std::vector<UINT> indices; UINT materialIndex; Mesh (std::vector<Vertex> vertices, std::vector<UINT> indices, UINT materialIndex) { this ->vertices = vertices; this ->indices = indices; this ->materialIndex = materialIndex; } }; class Model {public : Model (std::string path); std::vector<Mesh> meshes; std::vector<MaterialInfo> materials; std::vector<Mesh::RenderInfo> renderInfo; std::vector<std::string> texturePath; private : std::string directory; void ProcessNode (const aiScene* scene, aiNode* node) ; Mesh ProcessMesh (const aiScene* scene, aiMesh* mesh) ; UINT SetupMaterial (std::vector<UINT> diffuseMaps) ; void SetupRenderInfo () ; std::vector<UINT> LoadMaterialTextures (aiMaterial* mat, aiTextureType type) ; };
获取Scene
在所有的工作开始之前,必然是使用Assimp将模型从本机读取出来,代码如下:
1 2 3 4 5 6 7 Model::Model (std::string path) { Assimp::Importer importer; const aiScene* scene = importer.ReadFile (path, aiProcess_Triangulate | aiProcess_GenNormals | aiProcess_FlipUVs | aiProcess_CalcTangentSpace); directory = path.substr (0 , path.find_last_of ('\\' )) + '\\' ; ProcessNode (scene, scene->mRootNode); SetupRenderInfo (); }
ReadFile方法常用的几个参数意义如下:
aiProcess
意义
aiProcess_Triangulate
以三角形图元读取模型网格
aiProcess_GenNormals
自动生成法线
aiProcess_FlipUVs
翻转UV(是否使用视情况而定)
aiProcess_CalcTangentSpace
自动计算切线空间
aiProcess_FlipWindingOrder
反转绕序
aiProcess_JoinIdenticalVertices
合并相同顶点
遍历节点
从Scene中获取根节点后,就可以从根节点开始,不断递归遍历其下的子节点,最终做到处理完所有节点拥有的数据,代码如下:
1 2 3 4 5 6 7 8 9 10 void Model::ProcessNode (const aiScene* scene, aiNode* node) { for (size_t i = 0 ; i < node->mNumMeshes; i++) { aiMesh* mesh = scene->mMeshes[node->mMeshes[i]]; meshes.push_back (ProcessMesh (scene, mesh)); } for (size_t i = 0 ; i < node->mNumChildren; i++) { ProcessNode (scene, node->mChildren[i]); } }
如果在该节点下找到挂载的Mesh,就直接处理该Mesh数据并将其添加至meshes容器当中,需要注意的是aiNode仅仅保存Mesh对应的索引,所以需要真正获得aiMesh还需要到Scene中去查找。
处理网格
从Scene中获取到aiMesh后,就可以加载出aiMesh中的数据,包括模型的顶点,索引,法向量,切向量,纹理坐标,材质索引相关信息,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 Mesh Model::ProcessMesh (const aiScene* scene, aiMesh* mesh) { std::vector<Vertex> vertices; std::vector<UINT> indices; for (size_t i = 0 ; i < mesh->mNumVertices; i++) { Vertex vertex; vertex.position.x = mesh->mVertices[i].x; vertex.position.y = mesh->mVertices[i].y; vertex.position.z = mesh->mVertices[i].z; vertex.normal.x = mesh->mNormals[i].x; vertex.normal.y = mesh->mNormals[i].y; vertex.normal.z = mesh->mNormals[i].z; vertex.tangent.x = mesh->mTangents[i].x; vertex.tangent.y = mesh->mTangents[i].y; vertex.tangent.z = mesh->mTangents[i].z; if (mesh->mTextureCoords[0 ]) { vertex.texCoord.x = mesh->mTextureCoords[0 ][i].x; vertex.texCoord.y = mesh->mTextureCoords[0 ][i].y; } else { vertex.texCoord = { 0.0f , 0.0f }; } vertices.push_back (vertex); } for (size_t i = 0 ; i < mesh->mNumFaces; i++) { aiFace face = mesh->mFaces[i]; for (size_t j = 0 ; j < face.mNumIndices; j++) indices.push_back (face.mIndices[j]); } std::vector<UINT> diffuseMaps; if (mesh->mMaterialIndex >= 0 ) { aiMaterial* material = scene->mMaterials[mesh->mMaterialIndex]; diffuseMaps = LoadMaterialTextures (material, aiTextureType_DIFFUSE); } UINT materialIndex = SetupMaterial (diffuseMaps); return Mesh (vertices, indices, materialIndex); }
需要注意的是,索引并非直接存储在aiMesh中,而是存储在aiMesh下的aiFace中,由于在导入模型时使用了aiProcess_Triangulate,这里的Face也就基本是三角形图元,拥有3个Indices,将它们读取出来添加进总的索引即可。
读取材质
每一个Mesh可以有唯一个mMaterialIndex材质索引,同样的,可以使用这个材质索引在Scene中查询到其对应的aiMaterial,里面存储了材质信息,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 std::vector<UINT> Model::LoadMaterialTextures (aiMaterial* mat, aiTextureType type) { std::vector<UINT> textures; for (UINT i = 0 ; i < mat->GetTextureCount (type); i++) { aiString str; mat->GetTexture (type, 0 , &str); bool skip = false ; std::string texturePath = directory + str.C_Str (); for (UINT j = 0 ; j < this ->texturePath.size (); j++) { if (this ->texturePath[j] == texturePath) { textures.push_back (j); skip = true ; break ; } } if (!skip) { textures.push_back (this ->texturePath.size ()); this ->texturePath.push_back (texturePath); } } return textures; }
想要从aiMaterial中获取纹理贴图,首先必须明确自己想要的贴图种类,Assimp中用aiTextureType规定了一些常见的贴图种类,有漫反射贴图,镜面反射贴图,法线贴图,光照贴图,高度图等等,如果只使用漫反射贴图的话指定aiTextureType_DIFFUSE即可。
用aiMaterial下的GetTextureCount方法获取纹理贴图数量后,使用GetTexture方法获取对应纹理贴图的相对路径,这里我为了避免相同贴图的重复加载,设定一个将贴图路径与已经添加过的贴图路径进行比较的算法,做到所有贴图只会被添加一次。
返回的textures则代表了Mesh对应贴图在所有贴图中的索引位置,确保之后绘制Mesh的时候使用正确的纹理贴图。
SetupMaterial使用了类似的方法来确保不会有相同的Material被重复创建,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 UINT Model::SetupMaterial (std::vector<UINT> diffuseMaps) { MaterialInfo material; if (diffuseMaps.size () > 0 ) material.diffuseMaps = diffuseMaps[0 ]; else material.diffuseMaps = 0 ; UINT materialIndex; bool skip = false ; for (UINT i = 0 ; i < materials.size (); i++) { if (CompareMaterial (materials[i], material)) { materialIndex = i; skip = true ; break ; } } if (!skip) { materialIndex = materials.size (); materials.push_back (material); } return materialIndex; } bool CompareMaterial (MaterialInfo dest, MaterialInfo source) { bool isSame = false ; if (dest.diffuseMaps == source.diffuseMaps) isSame = true ; else isSame = false ; return isSame; }
使用Material而非Texture在仅仅有一个漫反射贴图的时候似乎看不出意义,因为两者几乎等同,但当一个Mesh需要同时使用多个贴图时(例如同时使用漫反射贴图和法线贴图),使用Material来存储Texture就显得十分有必要,同时这也方便我们之后对于材质内容的扩展。
提取渲染数据
在之前,我们已经获取了所有Mesh的顶点和索引,接下来要做的就是将它们全部提取出来,便于之后使用DrawCall绘制:
1 2 3 4 5 6 7 8 9 10 11 12 13 void Model::SetupRenderInfo () { renderInfo.resize (materials.size ()); for (UINT i = 0 ; i < meshes.size (); i++) { UINT index = meshes[i].materialIndex; UINT indexOffset = renderInfo[index].vertices.size (); for (UINT j = 0 ; j < meshes[i].indices.size (); j++) renderInfo[index].indices.push_back (meshes[i].indices[j] + indexOffset); renderInfo[index].vertices.insert (renderInfo[index].vertices.end (), meshes[i].vertices.begin (), meshes[i].vertices.end ()); } }
我们默认一个mesh使用一个材质,同时用材质作为区分模型部件的标准,也就是说,有多少个材质我们就使用多少个DrawCall,这确保了DrawCall数量的最小化,使得绘制效率尽可能高。
SkinnedModel类
藉由上面的Model类作为一个跳板,接下来我们就可以考虑添加骨骼与蒙皮动画到我们的模型当中了,我将这个新的类命名为SkinnedModel类,当然这个新的类会比原来的庞大许多:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 class SkinnedMesh {public : struct RenderInfo { std::vector<SkinnedVertex> vertices; std::vector<UINT> indices; }; std::vector<SkinnedVertex> vertices; std::vector<UINT> indices; UINT materialIndex; SkinnedMesh (std::vector<SkinnedVertex> vertices, std::vector<UINT> indices, UINT materialIndex) { this ->vertices = vertices; this ->indices = indices; this ->materialIndex = materialIndex; } }; class SkinnedModel {public : struct BoneData { UINT boneIndex[NUM_BONES_PER_VERTEX]; float weights[NUM_BONES_PER_VERTEX]; void Add (UINT boneID, float weight) { for (size_t i = 0 ; i < NUM_BONES_PER_VERTEX; i++) { if (weights[i] == 0.0f ) { boneIndex[i] = boneID; weights[i] = weight; return ; } } MessageBox (NULL , L"bone index out of size" , L"Error" , NULL ); } }; struct BoneInfo { bool isSkinned = false ; glm::mat4x4 boneOffset; glm::mat4x4 defaultOffset; int parentIndex; }; SkinnedModel (std::string path); std::vector<SkinnedMesh> meshes; std::vector<MaterialInfo> materials; std::vector<SkinnedMesh::RenderInfo> renderInfo; std::vector<std::string> texturePath; void GetBoneMapping (std::unordered_map<std::string, UINT>& boneMapping) { boneMapping = this ->boneMapping; } void GetBoneOffsets (std::vector<glm::mat4x4>& boneOffsets) { for (size_t i = 0 ; i < boneInfo.size (); i++) boneOffsets.push_back (boneInfo[i].boneOffset); } void GetNodeOffsets (std::vector<glm::mat4x4>& nodeOffsets) { for (size_t i = 0 ; i < boneInfo.size (); i++) nodeOffsets.push_back (boneInfo[i].defaultOffset); } void GetBoneHierarchy (std::vector<int >& boneHierarchy) { for (size_t i = 0 ; i < boneInfo.size (); i++) boneHierarchy.push_back (boneInfo[i].parentIndex); } void GetAnimations (std::unordered_map<std::string, AnimationClip>& animations) { animations.insert (this ->animations.begin (), this ->animations.end ()); } private : std::string directory; void ProcessNode (const aiScene* scene, aiNode* node) ; SkinnedMesh ProcessMesh (const aiScene* scene, aiMesh* mesh) ; UINT SetupMaterial (std::vector<UINT> diffuseMaps) ; void SetupRenderInfo () ; std::vector<UINT> LoadMaterialTextures (aiMaterial* mat, aiTextureType type) ; std::vector<BoneInfo> boneInfo; std::unordered_map<std::string, UINT> boneMapping; std::unordered_map<std::string, AnimationClip> animations; void LoadBones (const aiMesh* mesh, std::vector<BoneData>& bones) ; void ReadNodeHierarchy (const aiNode* node, int parentIndex) ; void LoadAnimations (const aiScene* scene) ; }; SkinnedModel::SkinnedModel (std::string path) { Assimp::Importer importer; const aiScene* scene = importer.ReadFile (path, aiProcess_Triangulate | aiProcess_GenNormals | aiProcess_FlipUVs | aiProcess_CalcTangentSpace); directory = path.substr (0 , path.find_last_of ('\\' )) + '\\' ; ReadNodeHierarchy (scene->mRootNode, -1 ); ProcessNode (scene, scene->mRootNode); SetupRenderInfo (); LoadAnimations (scene); }
SkinnedModel类下有两个用于辅助骨骼加载的结构体,一个是BoneData,与顶点相对应,指定了每个顶点所对应的绑骨(骨骼索引,蒙皮权重),一个是BoneInfo,与骨骼相对应,指定了每个骨骼的父子关系和变换。boneMapping是一个用骨骼名字来查找骨骼索引的无序映射表,可以方便查找boneInfo。
梳理树状层级
在不使用骨骼蒙皮动画的时候,模型自身部件的父子关系显得可有可无,因为我们实际并不需要用到,但一旦使用骨骼蒙皮动画,由于父子关系会涉及到层级之间的(矩阵)变换关系,因此梳理一遍树状层级就显得不可缺少:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 void SkinnedModel::ReadNodeHierarchy (const aiNode* node, int parentIndex) { BoneInfo boneInfo; UINT boneIndex = this ->boneInfo.size (); boneMapping[node->mName.C_Str ()] = boneIndex; boneInfo.boneOffset = glm::mat4 (1.0f ); boneInfo.parentIndex = parentIndex; boneInfo.defaultOffset = { node->mTransformation.a1, node->mTransformation.b1, node->mTransformation.c1, node->mTransformation.d1, node->mTransformation.a2, node->mTransformation.b2, node->mTransformation.c2, node->mTransformation.d2, node->mTransformation.a3, node->mTransformation.b3, node->mTransformation.c3, node->mTransformation.d3, node->mTransformation.a4, node->mTransformation.b4, node->mTransformation.c4, node->mTransformation.d4 }; this ->boneInfo.push_back (boneInfo); for (size_t i = 0 ; i < node->mNumChildren; i++) ReadNodeHierarchy (node->mChildren[i], boneIndex); }
这里同样使用递归来遍历整个树状层次,并将aiNode下的mTransformation存储为了骨骼的默认变换以便之后使用。
加载骨骼蒙皮数据
在ProcessMesh中,我加入了以下方法以便获取aiMesh下的mBones数据:
1 2 std::vector<BoneData> vertexBoneData (mesh->mNumVertices) ;LoadBones (mesh, vertexBoneData);
LoadBones方法会为每个顶点生成所需用的骨骼数据,从而得到顶点与骨骼的对应关系,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 void SkinnedModel::LoadBones (const aiMesh* mesh, std::vector<SkinnedModel::BoneData>& boneData) { for (size_t i = 0 ; i < mesh->mNumBones; i++) { UINT boneIndex = 0 ; std::string boneName (mesh->mBones[i]->mName.C_Str()) ; if (boneMapping.find (boneName) == boneMapping.end ()) { } else boneIndex = boneMapping[boneName]; boneMapping[boneName] = boneIndex; if (!boneInfo[boneIndex].isSkinned) { aiMatrix4x4 offsetMatrix = mesh->mBones[i]->mOffsetMatrix; boneInfo[boneIndex].boneOffset = { offsetMatrix.a1, offsetMatrix.b1, offsetMatrix.c1, offsetMatrix.d1, offsetMatrix.a2, offsetMatrix.b2, offsetMatrix.c2, offsetMatrix.d2, offsetMatrix.a3, offsetMatrix.b3, offsetMatrix.c3, offsetMatrix.d3, offsetMatrix.a4, offsetMatrix.b4, offsetMatrix.c4, offsetMatrix.d4 }; boneInfo[boneIndex].isSkinned = true ; } for (size_t j = 0 ; j < mesh->mBones[i]->mNumWeights; j++) { UINT vertexID = mesh->mBones[i]->mWeights[j].mVertexId; float weight = mesh->mBones[i]->mWeights[j].mWeight; boneData[vertexID].Add (boneIndex, weight); } } }
boneMapping已经在之前遍历树状层级的时候完成了构建,在这里我们只需要用boneName完成查找即可。aiBone下存有骨骼自身的偏移矩阵,将其存储在BoneInfo中;同样aiBone下还存有顶点蒙皮权重,暂时将其存储在BoneData中。
在读取顶点坐标/法向量/切向量的同时将蒙皮权重放进Vertex里,骨骼蒙皮权重规定了骨骼对一个顶点的造成的影响占比:
1 2 3 vertex.boneWeights.x = vertexBoneData[i].weights[0 ]; vertex.boneWeights.y = vertexBoneData[i].weights[1 ]; vertex.boneWeights.z = vertexBoneData[i].weights[2 ];
顶点着色器
修改顶点着色器的输入,在顶点数据中添加上骨骼蒙皮数据:
1 2 3 4 5 6 7 8 9 struct VertexIn { float3 position; float2 texCoord; float3 normal; float3 tangent; float3 boneWeights; uint4 boneIndices; };
由于需要考虑到骨骼蒙皮对顶点产生的变换,所以我们需要在顶点着色器中添加一些计算步骤,HLSL代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 float3 posL = float3 (0.0f , 0.0f , 0.0f ); float3 normalL = float3 (0.0f , 0.0f , 0.0f ); float3 tangentL = float3 (0.0f , 0.0f , 0.0f ); float weights[4 ] = { input.boneWeights.x, input.boneWeights.y, input.boneWeights.z, 1.0f - input.boneWeights.x - input.boneWeights.y - input.boneWeights.z };for (int i = 0 ; i < 4 ; i++) { posL += weights[i] * mul (boneTransforms[input.boneIndices[i]], float4 (input.position, 1.0f )).xyz; normalL += weights[i] * mul ((float3x3)boneTransforms_inv_trans[input.boneIndices[i]], input.normal); normalL += weights[i] * mul ((float3x3)boneTransforms[input.boneIndices[i]], input.normal); }
这里的计算也并不复杂,分别将各个骨骼的蒙皮权重乘以骨骼变换矩阵再乘以初始坐标(向量),最后再逐个相加,就能得到需要的结果,这可以放在MVP矩阵变换之前执行。
骨骼动画
一些基本数据结构和功能,会在加载骨骼动画时使用到,贴在下面方便查看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 struct VectorKey { float timePos; glm::vec3 value; }; struct QuatKey { float timePos; glm::qua<float > value; }; class BoneAnimation {public : float GetStartTime () const ; float GetEndTime () const ; void Interpolate (float t, glm::mat4x4& M) ; std::vector<VectorKey> translation; std::vector<VectorKey> scale; std::vector<QuatKey> rotationQuat; glm::mat4x4 defaultTransform; private : glm::vec3 LerpKeys (float t, const std::vector<VectorKey>& keys) ; glm::qua<float > LerpKeys (float t, const std::vector<QuatKey>& keys) ; }; class AnimationClip {public : float GetClipStartTime () const ; float GetClipEndTime () const ; void Interpolate (float t, std::vector<glm::mat4x4>& boneTransform) ; std::vector<BoneAnimation> boneAnimations; }; class SkinnedData {public : UINT GetBoneCount () const { return boneHierarchy.size (); } float GetClipStartTime (const std::string& clipName) const ; float GetClipEndTime (const std::string& clipName) const ; void Set (std::vector<int >& boneHierarchy, std::vector<glm::mat4x4>& boneOffsets, std::unordered_map<std::string, AnimationClip>& animations) ; void GetFinalTransform (const std::string& clipName, float timePos, std::vector<glm::mat4x4>& finalTransforms) ; private : std::vector<int > boneHierarchy; std::vector<glm::mat4x4> boneOffsets; std::unordered_map<std::string, AnimationClip> animations; }; float BoneAnimation::GetStartTime () const { float t0 = 0.0f ; float t1 = 0.0f ; float t2 = 0.0f ; if (translation.size () != 0 ) t0 = translation.front ().timePos; if (scale.size () != 0 ) t1 = scale.front ().timePos; if (rotationQuat.size () != 0 ) t2 = rotationQuat.front ().timePos; float timePos = t0 < t1 ? t0 : t1; timePos = timePos < t2 ? timePos : t2; return timePos; } float BoneAnimation::GetEndTime () const { float t0 = 0.0f ; float t1 = 0.0f ; float t2 = 0.0f ; if (translation.size () != 0 ) t0 = translation.back ().timePos; if (scale.size () != 0 ) t1 = scale.back ().timePos; if (rotationQuat.size () != 0 ) t2 = rotationQuat.back ().timePos; float timePos = t0 > t1 ? t0 : t1; timePos = timePos > t2 ? timePos : t2; return timePos; } float AnimationClip::GetClipStartTime () const { float t = FLT_MAX; for (UINT i = 0 ; i < boneAnimations.size (); i++) t = t < boneAnimations[i].GetStartTime () ? t : boneAnimations[i].GetStartTime (); return t; } float AnimationClip::GetClipEndTime () const { float t = 0.0f ; for (UINT i = 0 ; i < boneAnimations.size (); i++) t = t > boneAnimations[i].GetEndTime () ? t : boneAnimations[i].GetEndTime (); return t; } void AnimationClip::Interpolate (float t, std::vector<glm::mat4x4>& boneTransform) { for (UINT i = 0 ; i < boneAnimations.size (); i++) boneAnimations[i].Interpolate (t, boneTransform[i]); } float SkinnedData::GetClipStartTime (const std::string& clipName) const { auto clip = animations.find (clipName); return clip->second.GetClipStartTime (); } float SkinnedData::GetClipEndTime (const std::string& clipName) const { auto clip = animations.find (clipName); return clip->second.GetClipEndTime (); } void SkinnedData::Set (std::vector<int >& boneHierarchy, std::vector<glm::mat4x4>& boneOffsets, std::unordered_map<std::string, AnimationClip>& animations) { this ->boneHierarchy = boneHierarchy; this ->boneOffsets = boneOffsets; this ->animations = animations; }
加载模型动画
Assimp的模型动画存储在aiScene下的aiAnimation中,在我们加载Scene的同时就可以将模型动画同时加载进来,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 void SkinnedModel::LoadAnimations (const aiScene* scene) { for (size_t i = 0 ; i < scene->mNumAnimations; i++) { AnimationClip animation; std::vector<BoneAnimation> boneAnims (boneInfo.size()) ; aiAnimation* anim = scene->mAnimations[i]; float ticksPerSecond = anim->mTicksPerSecond != 0 ? anim->mTicksPerSecond : 25.0f ; float timeInTicks = 1.0f / ticksPerSecond; for (size_t j = 0 ; j < anim->mNumChannels; j++) { BoneAnimation boneAnim; aiNodeAnim* nodeAnim = anim->mChannels[j]; for (size_t k = 0 ; k < nodeAnim->mNumPositionKeys; k++) { VectorKey keyframe; keyframe.timePos = nodeAnim->mPositionKeys[k].mTime * timeInTicks; keyframe.value.x = nodeAnim->mPositionKeys[k].mValue.x; keyframe.value.y = nodeAnim->mPositionKeys[k].mValue.y; keyframe.value.z = nodeAnim->mPositionKeys[k].mValue.z; boneAnim.translation.push_back (keyframe); } for (size_t k = 0 ; k < nodeAnim->mNumScalingKeys; k++) { VectorKey keyframe; keyframe.timePos = nodeAnim->mScalingKeys[k].mTime * timeInTicks; keyframe.value.x = nodeAnim->mScalingKeys[k].mValue.x; keyframe.value.y = nodeAnim->mScalingKeys[k].mValue.y; keyframe.value.z = nodeAnim->mScalingKeys[k].mValue.z; boneAnim.scale.push_back (keyframe); } for (size_t k = 0 ; k < nodeAnim->mNumRotationKeys; k++) { QuatKey keyframe; keyframe.timePos = nodeAnim->mRotationKeys[k].mTime * timeInTicks; keyframe.value.x = nodeAnim->mRotationKeys[k].mValue.x; keyframe.value.y = nodeAnim->mRotationKeys[k].mValue.y; keyframe.value.z = nodeAnim->mRotationKeys[k].mValue.z; keyframe.value.w = nodeAnim->mRotationKeys[k].mValue.w; boneAnim.rotationQuat.push_back (keyframe); } boneAnims[boneMapping[nodeAnim->mNodeName.C_Str ()]] = boneAnim; } animation.boneAnimations = boneAnims; std::string animName (anim->mName.C_Str()) ; animName = animName.substr (animName.find_last_of ('|' ) + 1 , animName.length () - 1 ); animations[animName] = animation; } }
从aiAnimation下的mChannels就可以得到每个节点对应的骨骼动画,因此我们同样按照boneMapping的索引存储在boneAnims下,这样就不会变得混乱。
骨骼动画也是逐帧动画的一种呈现形式,所以依旧依赖关键帧来记录信息,Assimp中每个关键帧都由mPositionKeys(位置),mScalingKeys(缩放),mRotationKeys(旋转)三个形式记录变换,其中位置和缩放用向量记录,旋转用四元数记录。
除了关键帧的变换我们需要知道以外,关键帧的时间点我们也需要知道,Assimp中用mTicksPerSecond来规定时间标准,求它的倒数得到timeInTicks(单位时间的长度),再用关键帧的mTime乘以timeInTicks得到标准化的关键帧时间点。
得知了关键帧的变换和时间点之后,就可以使用这些关键帧构建完整的骨骼动画了。
关键帧插值
通过关键帧插值推得过渡帧的变换数据,关键帧插值分为向量线性插值和四元数球面线性插值,用当前时刻在两个关键帧之间的百分比位置进行线性插值,直接使用GLM库简化掉了计算的细节,这里不再详细讲解Lerp和Slerp的方法,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 glm::vec3 BoneAnimation::LerpKeys (float t, const std::vector<VectorKey>& keys) { glm::vec3 res = glm::vec3 (0.0f ); if (t <= keys.front ().timePos) res = keys.front ().value; else if (t >= keys.back ().timePos) res = keys.back ().value; else { for (size_t i = 0 ; i < keys.size () - 1 ; i++){ if (t >= keys[i].timePos && t <= keys[i + 1 ].timePos){ float lerpPercent = (t - keys[i].timePos) / (keys[i + 1 ].timePos - keys[i].timePos); res = keys[i].value * lerpPercent + keys[i + 1 ].value * (1 - lerpPercent); break ; } } } return res; } glm::qua<float > BoneAnimation::LerpKeys (float t, const std::vector<QuatKey>& keys) { glm::qua<float > res = glm::qua <float >(); if (t <= keys.front ().timePos) res = keys.front ().value; else if (t >= keys.back ().timePos) res = keys.back ().value; else { for (size_t i = 0 ; i < keys.size () - 1 ; i++) { if (t >= keys[i].timePos && t <= keys[i + 1 ].timePos) { float lerpPercent = (t - keys[i].timePos) / (keys[i + 1 ].timePos - keys[i].timePos); res = glm::slerp (keys[i].value, keys[i + 1 ].value, lerpPercent); break ; } } } return res; }
将插值与矩阵变换综合在一起,得到当前帧的变换矩阵,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void BoneAnimation::Interpolate (float t, glm::mat4x4& M) { if (translation.size () == 0 && scale.size () == 0 && rotationQuat.size () == 0 ) { M = defaultTransform; return ; } glm::vec3 T = glm::vec3 (0.0f ); glm::vec3 S = glm::vec3 (0.0f ); glm::qua<float > R = glm::qua <float >(); if (translation.size () != 0 ) T = LerpKeys (t, translation); if (scale.size () != 0 ) S = LerpKeys (t, scale); if (rotationQuat.size () != 0 ) R = LerpKeys (t, rotationQuat); M = glm::translate (glm::mat4 (1.0f ), T) * glm::mat4_cast (R) * glm::scale (glm::mat4 (1.0f ), S); }
最终变换矩阵
刚刚我们所作的都是对于一根骨头而言的,但是一个硕大的模型不可能只有一根骨头,而是拥有完整的骨架,并且骨骼之间相互具有父子关系,父物件的变换会对子物件造成影响,因此我们需要根据树状层级计算出一根骨头真正的变换矩阵,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 void SkinnedData::GetFinalTransform (const std::string& clipName, float timePos, std::vector<glm::mat4x4>& finalTransforms) { UINT numBones = boneOffsets.size (); std::vector<glm::mat4x4> toParentTransforms (numBones) ; auto clip = animations.find (clipName); clip->second.Interpolate (timePos, toParentTransforms); std::vector<glm::mat4x4> toRootTransforms (numBones) ; toRootTransforms[0 ] = toParentTransforms[0 ]; for (UINT i = 1 ; i < numBones; i++){ int parentIndex = boneHierarchy[i]; toRootTransforms[i] = toRootTransforms[parentIndex] * toParentTransforms[i]; } for (UINT i = 0 ; i < numBones; i++) { finalTransforms[i] = toRootTransforms[i] * boneOffsets[i]; } }
获得最终变换矩阵的原理非常简单,就是把自身的变换矩阵乘以父物体的变换矩阵,得到自身对于模型根部的变换矩阵,除此以外,还需要乘以boneOffset即骨骼自身的偏移。
在渲染循环的每一帧更新骨骼动画,并获取最终变换矩阵,由着色器读取完成计算:
1 2 3 4 5 6 7 8 9 void UpdateSkinnedAnimation (float deltaTime) { timePos += deltaTime; if (timePos > skinnedInfo.GetClipEndTime (clipName)) timePos = 0.0f ; skinnedData.GetFinalTransform (clipName, timePos, finalTransforms); }