Skeletal-Animation.html (39270B)
1 <div id="content"> 2 <h1 id="content-title">Skeletal Animation</h1> 3 <h1 id="content-url" style='display:none;'>Guest-Articles/2020/Skeletal-Animation</h1> 4 <p>3D Animations can bring our games to life. Objects in 3D world like humans and & 5 animals feel more organic when they move their limbs to do certain things like walking, running & attacking. 6 This tutorial is about Skeletal animation which you all have been waiting for. We will first understand the concept thoroughly and then understand the data 7 we need to animate a 3D model using Assimp. I'd recommend you to finish the <a href="https://learnopengl.com/Model-Loading/Assimp">Model Loading</a> chapter of this saga as this tutorial code continues from there. You can still understand the concept and implement it in your way. So let's get started.</p> 8 9 10 <h3>Interpolation</h3> 11 <p>To understand how animation works at basic level we need to understand the concept of Interpolation. 12 Interpolation can be defined as something happening over time. Like an enemy moving from point A to point B in time T i.e Translation happening over time . 13 A gun turret smoothly rotates to face the target i.e Rotation happening over time and a tree is scaling up from size A to size B in time T i.e Scaling happening over time.</p> 14 <p>A simple interpolation equation used for Translation and Scale looks like this..</p> 15 <p style="text-align: center;"><strong>a = a * (1 - t) + b * t </strong></p> 16 <p>It is known as as Linear Interpolation equation or Lerp. For Rotation we cannot use Vector. 17 The reason for that is if we went ahead and tried to use the linear interpolation equation on a vector of X(Pitch),Y(Yaw) & Z(Roll), the interpolation won't be linear. 18 You will encounter 19 weird issues like The 20 Gimbal Lock(See references section below to learn about it). To avoid this issue we use Quaternion for rotations. 21 Quaternion provides something called The Spherical Interpolation or 22 Slerp equation which gives the same result as Lerp but for two rotations A & B. 23 I won't be able to explain how the equation works because its out of the scope for now. You can surely checkout references section below to 24 understand The Quaternion. 25 </p> 26 <h3>Components of An Animated Model : Skin, Bones and Keyframes</h3> 27 <p>The whole process of an animation starts with the addition of the first component which is The Skin in a software like blender or Maya. 28 Skin is nothing but meshes which add visual aspect to the model to tell the viewer how it looks like. 29 But If you want to move any mesh then just like the real world, you need to add Bones. You can see the images below to understand how it looks in software like blender....</p> 30 <p> </p> 31 <img src="/img/guest/2020/skeletal_animation/skin.png" alt="skin" width="300" height="300" class="clean"> <img src="/img/guest/2020/skeletal_animation/bones.png" alt="bones" width="300" height="300"class="clean"> <img src="/img/guest/2020/skeletal_animation/merged.png" alt="skin and bones" width="300" height="300"class="clean"> 32 <p>These bones are usually added in hierarchical fashion for characters like humans & animals and the reason is pretty obvious. We want parent-child relationship among limbs. 33 For example, If we move our right shoulder then our right bicep, forearm, hand and fingers should move as well. This is how the hierarchy looks like....</p> 34 <p> </p> 35 36 <p> 37 <img src="/img/guest/2020/skeletal_animation/parent_child.png" alt="" width="853" height="425"/></p> 38 39 <p>In the above diagram if you grab the hip bone and move it, all limbs will be affected by its movement.</p> 40 <p>At this point, we are ready to create KeyFrames for an animation. Keyframes are poses at different point of time in an animation. We will interpolate between 41 these Keyframes to go from one pose to another pose smoothly in our code. Below you can see how poses are created for a simple 4 frame jump animation...</p> 42 <p><img src="/img/guest/2020/skeletal_animation/poses.gif" width="300px" class="clean" alt=""/> <img src="/img/guest/2020/skeletal_animation/interpolating.gif" class="clean" width="300px" alt="Interpolation bw frames" hspace="20"/></p> 43 <p> </p> 44 <h3>How Assimp holds animation data</h3> 45 <p>We are almost there to the code part but first we need to understand how assimp holds imported animation data. Look at the diagram below..</p> 46 <p><img src="/img/guest/2020/skeletal_animation/assimp1.jpeg" alt="" width="710" height="800"/></p> 47 <p>Just like in the <a href="https://learnopengl.com/Model-Loading/Assimp">Model Loading</a> chapter, we will start with the <code>aiScene</code> pointer 48 which holds a pointer to the root node and look what do we have here, an array of Animations. 49 This array of <code>aiAnimation</code> contains the general information like duration of an animation represented here as 50 <code>mDuration</code> and then we have a <code>mTicksPerSecond</code> variable, which controls how fast 51 we should interpolate between frames. If you remember from the last section that an animation has keyframes. 52 Similary, an <code>aiAnimation</code> contains an <code>aiNodeAnim</code> array called Channels. 53 This array of contains all bones and their keyframes which are going to be engaged in an animation. 54 55 An <code>aiNodeAnim</code> contains name of the bone and you 56 will find 3 types of keys to interpolate between here, Translation,Rotation & Scale.</p> 57 58 <p>Alright, there's one last thing we need to understand and we are good to go for writing some code.</p> 59 60 <p> </p> 61 <h3>Influence of multiple bones on vertices</h3> 62 <p>When we curl our forearm and we see our biceps muscle pop up. We can also say that forearm bone transformation is affecting vertices on our biceps. 63 Similary, there could be multiple bones affecting a single vertex in a mesh. 64 For characters like solid metal robots all forearm vertices will only be affected by forearm bone but for characters like humans, animals etc, there could be 65 upto 4 bones which can affect a vertex. Let's see how assimp stores that information...</p> 66 <p> </p> 67 <p><img src="/img/guest/2020/skeletal_animation/assimp2.jpeg" alt="" width="760" height="860"/></p> 68 <p> </p> 69 <p>We start with the <code>aiScene</code> pointer again which contains an array of all aiMeshes. 70 Each <code>aiMesh</code> object has an array of <code>aiBone</code> which contains the information like 71 how much influence this <code>aiBone</code> will have on set of vertices on the mesh. 72 aiBone contains the name of the bone, an array of <code>aiVertexWeight</code> which basically 73 tells us how much influence this <code>aiBone</code> will have on what vertices on the mesh. 74 Now we have one more member of <code>aiBone</code> which is offsetMatrix. It's a 4x4 matrix 75 used to transform vertices from model space to their bone space. 76 You can see this in action in images below....</p> 77 78 <img src="/img/guest/2020/skeletal_animation/mesh_space.png" class="clean" alt="Mesh Space" style="width:50%"> 79 <img src="/img/guest/2020/skeletal_animation/bone_space.png" class="clean" alt="Bone Space" style="width:50%"> 80 <p> 81 When vertices are in bone space they will be transformed relative to their bone 82 as they are supposed to. You will soon see this in action 83 in code. 84 </p> 85 86 <h3>Finally! Let's code.</h3> 87 <p>Thank you for making it this far. We will start with directly looking at the end result which is our final vertex 88 shader code. This will give us good sense what we need at the end.. </p> 89 90 <pre><code>#version 430 core 91 92 layout(location = 0) in vec3 pos; 93 layout(location = 1) in vec3 norm; 94 layout(location = 2) in vec2 tex; 95 layout(location = 3) in ivec4 boneIds; 96 layout(location = 4) in vec4 weights; 97 98 uniform mat4 projection; 99 uniform mat4 view; 100 uniform mat4 model; 101 102 const int MAX_BONES = 100; 103 const int MAX_BONE_INFLUENCE = 4; 104 uniform mat4 finalBonesMatrices[MAX_BONES]; 105 106 out vec2 TexCoords; 107 108 void main() 109 { 110 vec4 totalPosition = vec4(0.0f); 111 for(int i = 0 ; i < MAX_BONE_INFLUENCE ; i++) 112 { 113 if(boneIds[i] == -1) 114 continue; 115 if(boneIds[i] >= MAX_JOINTS) 116 { 117 totalPosition = vec4(pos,1.0f); 118 break; 119 } 120 vec4 localPosition = finalBoneMatrices[boneIds[i]] * vec4(pos,1.0f); 121 totalPosition += localPosition * weights[i]; 122 vec3 localNormal = mat3(finalBoneMatrices[boneIds[i]]) * norm; 123 } 124 125 mat4 viewModel = view * model; 126 gl_Position = projection * viewModel * totalPosition; 127 TexCoords = tex; 128 } 129 </code></pre> 130 131 <p> 132 Fragment shader remains the same from the <a href="https://learnopengl.com/Model-Loading/Model">model loading</a> chapter. 133 Starting from the top you see two new attributes layout declaration. 134 First <code>boneIds</code> and second is <code>weights</code>. we also have 135 a uniform array <code>finalBonesMatrices</code> which stores transformations of all bones. 136 <code>boneIds</code> contains indices which are used to read the <code>finalBonesMatrices</code> 137 array and apply those transformation to <code>pos</code> vertex with their respective weights 138 stored in <code> weights </code> array. This happens inside <code> for </code> loop above. 139 Now let's add support in our <code>Mesh</code> class for bone weights first.. 140 </p> 141 142 <pre><code>#define MAX_BONE_INFLUENCE 4 143 144 struct Vertex { 145 // position 146 glm::vec3 Position; 147 // normal 148 glm::vec3 Normal; 149 // texCoords 150 glm::vec2 TexCoords; 151 152 //bone indexes which will influence this vertex 153 int m_BoneIDs[MAX_BONE_INFLUENCE]; 154 155 //weights from each bone 156 float m_Weights[MAX_BONE_INFLUENCE]; 157 }; 158 </code></pre> 159 160 <p> 161 We have added two new attributes for the <code>Vertex</code>, just like we saw in our vertex shader. 162 Now's let's load them in GPU buffers just like other attributes in our <code>Mesh::setupMesh </code> function... 163 </p> 164 165 <pre><code>class Mesh 166 { 167 ... 168 169 void setupMesh() 170 { 171 ... 172 173 // ids 174 <function id='29'><function id='60'>glEnable</function>VertexAttribArray</function>(3); 175 glVertexAttribIPointer(3, 4, GL_INT, sizeof(Vertex), 176 (void*)offsetof(Vertex, m_BoneIDs)); 177 178 // weights 179 <function id='29'><function id='60'>glEnable</function>VertexAttribArray</function>(4); 180 <function id='30'>glVertexAttribPointer</function>(4, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex), 181 (void*)offsetof(Vertex, m_Weights)); 182 183 ... 184 } 185 ... 186 } 187 </code></pre> 188 <p>Just like before, except now we have added 3 and 4 layout location ids for <code>boneIds</code> and <code>weights</code>. One imporant thing to notice here is how we are passing data for <code>boneIds</code>. We are using <code>glVertexAttribIPointer</code> and we passed GL_INT as third parameter. </p> 189 <p>Now we can extract the bone-weight information from the assimp data structure. Let's make some changes in Model class...</p> 190 191 <pre><code>struct BoneInfo 192 { 193 /*id is index in finalBoneMatrices*/ 194 int id; 195 196 /*offset matrix transforms vertex from model space to bone space*/ 197 glm::mat4 offset; 198 199 }; 200 </code></pre> 201 202 <p> This <code> BoneInfo </code> will store our offset matrix and also a unique id which will 203 be used as an index to store it in <code>finalBoneMatrices</code> array we saw earlier in our shader. 204 Now we will add bone weight extraction support in <code>Model</code>... </p> 205 206 <pre><code>class Model 207 { 208 private: 209 ... 210 std::map<string, BoneInfo> m_BoneInfoMap; // 211 int m_BoneCounter = 0; 212 213 ... 214 void SetVertexBoneDataToDefault(Vertex& vertex) 215 { 216 for (int i = 0; i < MAX_BONE_WEIGHTS; i++) 217 { 218 vertex.m_BoneIDs[i] = -1; 219 vertex.m_Weights[i] = 0.0f; 220 } 221 } 222 223 Mesh processMesh(aiMesh* mesh, const aiScene* scene) 224 { 225 vector vertices; 226 vector indices; 227 vector textures; 228 229 for (unsigned int i = 0; i < mesh->mNumVertices; i++) 230 { 231 Vertex vertex; 232 233 SetVertexBoneDataToDefault(vertex); 234 235 vertex.Position = AssimpGLMHelpers::GetGLMVec(mesh->mVertices[i]); 236 vertex.Normal = AssimpGLMHelpers::GetGLMVec(mesh->mNormals[i]); 237 238 if (mesh->mTextureCoords[0]) 239 { 240 glm::vec2 vec; 241 vec.x = mesh->mTextureCoords[0][i].x; 242 vec.y = mesh->mTextureCoords[0][i].y; 243 vertex.TexCoords = vec; 244 } 245 else 246 vertex.TexCoords = glm::vec2(0.0f, 0.0f); 247 248 vertices.push_back(vertex); 249 } 250 ... 251 ExtractBoneWeightForVertices(vertices,mesh,scene); 252 253 return Mesh(vertices, indices, textures); 254 } 255 256 void SetVertexBoneData(Vertex& vertex, int boneID, float weight) 257 { 258 for (int i = 0; i < MAX_BONE_WEIGHTS; ++i) 259 { 260 if (vertex.m_BoneIDs[i] < 0) 261 { 262 vertex.m_Weights[i] = weight; 263 vertex.m_BoneIDs[i] = boneID; 264 break; 265 } 266 } 267 } 268 269 void ExtractBoneWeightForVertices(std::vector& vertices, aiMesh* mesh, 270 const aiScene* scene) 271 { 272 for (int boneIndex = 0; boneIndex < mesh->mNumBones; ++boneIndex) 273 { 274 int boneID = -1; 275 std::string boneName = mesh->mBones[boneIndex]->mName.C_Str(); 276 if (m_BoneInfoMap.find(boneName) == m_BoneInfoMap.end()) 277 { 278 BoneInfo newBoneInfo; 279 newBoneInfo.id = m_BoneCounter; 280 newBoneInfo.offset = AssimpGLMHelpers:: 281 ConvertMatrixToGLMFormat(mesh->mBones[boneIndex]->mOffsetMatrix); 282 m_BoneInfoMap[boneName] = newBoneInfo; 283 boneID = m_BoneCounter; 284 m_BoneCounter++; 285 } 286 else 287 { 288 boneID = m_BoneInfoMap[boneName].id; 289 } 290 assert(boneID != -1); 291 auto weights = mesh->mBones[boneIndex]->mWeights; 292 int numWeights = mesh->mBones[boneIndex]->mNumWeights; 293 294 for (int weightIndex = 0; weightIndex < numWeights; ++weightIndex) 295 { 296 int vertexId = weights[weightIndex].mVertexId; 297 float weight = weights[weightIndex].mWeight; 298 assert(vertexId <= vertices.size()); 299 SetVertexBoneData(vertices[vertexId], boneID, weight); 300 } 301 } 302 } 303 ....... 304 }; 305 </code></pre> 306 307 <p>We start by declaring a map <code>m_BoneInfoMap</code> and a counter <code>m_BoneCounter</code> 308 which will be incremented as soon as we read a new bone. 309 we saw in the diagram earlier that each <code>aiMesh</code> contains all 310 aiBones which are associated with the <code>aiMesh</code>. 311 The whole process of the bone-weight extraction starts from the 312 <code> processMesh </code> 313 function. For each loop iteration we are setting <code>m_BoneIDs</code> and <code>m_Weights</code> to 314 their default values 315 by calling function <code>SetVertexBoneDataToDefault</code>. 316 Just before the <code>processMesh</code> function ends, we call the 317 <code>ExtractBoneWeightData</code>. In the <code>ExtractBoneWeightData</code> we run 318 a for loop for each <code>aiBone</code> and check if this bone already exists in the <code>m_BoneInfoMap</code>. 319 If we couldn't find it then it's considered a new bone and we create new <code>BoneInfo</code> 320 with an id and store its associated <code>mOffsetMatrix</code> to it. Then we store this new <code>BoneInfo</code> 321 in <code>m_BoneInfoMap</code> and then we increment the <code>m_BoneCounter</code> counter to create 322 an id for next bone. In case we find the bone name in <code>m_BoneInfoMap</code> then 323 that means this bone affects vertices of mesh out of 324 its scope. So we take it's Id and proceed further to know which vertices it affects. </p> 325 326 <p> One thing to notice that we are calling <code>AssimpGLMHelpers::ConvertMatrixToGLMFormat</code>. 327 Assimp store its matrix data in different format than GLM so this function just gives us our matrix in GLM format. 328 </p> 329 <p>We have extracted the offsetMatrix for the bone and now we will simply iterate its <code>aiVertexWeight</code>array 330 and extract all vertices indices which will be influenced by this bone along with their 331 respective weights and call <code>SetVertexBoneData</code> to fill up <code>Vertex.boneIds</code> and <code>Vertex.weights</code> with extracted information. </p> 332 333 <p>Phew! You deserve a coffee break at this point. </p> 334 335 <h3>Bone,Animation & Animator classes</h3> 336 <p>Here's high level view of classes..</p> 337 338 <p><img src="/img/guest/2020/skeletal_animation/bird_eye_view.png" class="clean" alt="" width="700"/></p> 339 340 <p> Let us remind ourselves what we are trying to achieve. For each rendering frame we want to interpolate all bones in heirarchy smoothly and get their final transformations matrices which will be supplied to shader 341 uniform <code>finalBonesMatrices</code>. 342 Here's what each class does... 343 344 <p><b>Bone</b> : A single bone which reads all keyframes data from <code>aiNodeAnim</code>. It will also interpolate between its keys i.e Translation,Scale & Rotation based on the current animation time. </p> 345 <p><b>AssimpNodeData</b> : This struct will help us to isolate our <code><b>Animation</b> from Assimp. </code> </p> 346 <p><b>Animation</b> : An asset which reads data from aiAnimation and create a heirarchical record of <code><b>Bone</b></code>s </p> 347 <p><b>Animator</b> : This will read the heirarchy of <code>AssimpNodeData</code>, 348 Interpolate all bones in a recursive manner and then prepare final bone transformation matrices for us that we need. 349 350 </p> 351 352 <p> 353 Here's the code for <code>Bone</code>... 354 355 <pre><code>struct KeyPosition 356 { 357 glm::vec3 position; 358 float timeStamp; 359 }; 360 361 struct KeyRotation 362 { 363 glm::quat orientation; 364 float timeStamp; 365 }; 366 367 struct KeyScale 368 { 369 glm::vec3 scale; 370 float timeStamp; 371 }; 372 373 class Bone 374 { 375 private: 376 std::vector<KeyPosition> m_Positions; 377 std::vector<KeyRotation> m_Rotations; 378 std::vector<KeyScale> m_Scales; 379 int m_NumPositions; 380 int m_NumRotations; 381 int m_NumScalings; 382 383 glm::mat4 m_LocalTransform; 384 std::string m_Name; 385 int m_ID; 386 public: 387 /*reads keyframes from aiNodeAnim*/ 388 Bone(const std::string& name, int ID, const aiNodeAnim* channel) 389 : 390 m_Name(name), 391 m_ID(ID), 392 m_LocalTransform(1.0f) 393 { 394 m_NumPositions = channel->mNumPositionKeys; 395 396 for (int positionIndex = 0; positionIndex < m_NumPositions; ++positionIndex) 397 { 398 aiVector3D aiPosition = channel->mPositionKeys[positionIndex].mValue; 399 float timeStamp = channel->mPositionKeys[positionIndex].mTime; 400 KeyPosition data; 401 data.position = AssimpGLMHelpers::GetGLMVec(aiPosition); 402 data.timeStamp = timeStamp; 403 m_Positions.push_back(data); 404 } 405 406 m_NumRotations = channel->mNumRotationKeys; 407 for (int rotationIndex = 0; rotationIndex < m_NumRotations; ++rotationIndex) 408 { 409 aiQuaternion aiOrientation = channel->mRotationKeys[rotationIndex].mValue; 410 float timeStamp = channel->mRotationKeys[rotationIndex].mTime; 411 KeyRotation data; 412 data.orientation = AssimpGLMHelpers::GetGLMQuat(aiOrientation); 413 data.timeStamp = timeStamp; 414 m_Rotations.push_back(data); 415 } 416 417 m_NumScalings = channel->mNumScalingKeys; 418 for (int keyIndex = 0; keyIndex < m_NumScalings; ++keyIndex) 419 { 420 aiVector3D scale = channel->mScalingKeys[keyIndex].mValue; 421 float timeStamp = channel->mScalingKeys[keyIndex].mTime; 422 KeyScale data; 423 data.scale = AssimpGLMHelpers::GetGLMVec(scale); 424 data.timeStamp = timeStamp; 425 m_Scales.push_back(data); 426 } 427 } 428 429 /* Interpolates b/w positions,rotations & scaling keys based on the curren time of the 430 animation and prepares the local transformation matrix by combining all keys tranformations */ 431 void Update(float animationTime) 432 { 433 glm::mat4 translation = InterpolatePosition(animationTime); 434 glm::mat4 rotation = InterpolateRotation(animationTime); 435 glm::mat4 scale = InterpolateScaling(animationTime); 436 m_LocalTransform = translation * rotation * scale; 437 } 438 439 glm::mat4 GetLocalTransform() { return m_LocalTransform; } 440 std::string GetBoneName() const { return m_Name; } 441 int GetBoneID() { return m_ID; } 442 443 /* Gets the current index on mKeyPositions to interpolate to based on the current 444 animation time */ 445 int GetPositionIndex(float animationTime) 446 { 447 for (int index = 0; index < m_NumPositions - 1; ++index) 448 { 449 if (animationTime < m_Positions[index + 1].timeStamp) 450 return index; 451 } 452 assert(0); 453 } 454 455 /* Gets the current index on mKeyRotations to interpolate to based on the current 456 animation time */ 457 int GetRotationIndex(float animationTime) 458 { 459 for (int index = 0; index < m_NumRotations - 1; ++index) 460 { 461 if (animationTime < m_Rotations[index + 1].timeStamp) 462 return index; 463 } 464 assert(0); 465 } 466 467 /* Gets the current index on mKeyScalings to interpolate to based on the current 468 animation time */ 469 int GetScaleIndex(float animationTime) 470 { 471 for (int index = 0; index < m_NumScalings - 1; ++index) 472 { 473 if (animationTime < m_Scales[index + 1].timeStamp) 474 return index; 475 } 476 assert(0); 477 } 478 private: 479 480 /* Gets normalized value for Lerp & Slerp*/ 481 float GetScaleFactor(float lastTimeStamp, float nextTimeStamp, float animationTime) 482 { 483 float scaleFactor = 0.0f; 484 float midWayLength = animationTime - lastTimeStamp; 485 float framesDiff = nextTimeStamp - lastTimeStamp; 486 scaleFactor = midWayLength / framesDiff; 487 return scaleFactor; 488 } 489 490 /* figures out which position keys to interpolate b/w and performs the interpolation 491 and returns the translation matrix */ 492 glm::mat4 InterpolatePosition(float animationTime) 493 { 494 if (1 == m_NumPositions) 495 return <function id='55'>glm::translate</function>(glm::mat4(1.0f), m_Positions[0].position); 496 497 int p0Index = GetPositionIndex(animationTime); 498 int p1Index = p0Index + 1; 499 float scaleFactor = GetScaleFactor(m_Positions[p0Index].timeStamp, 500 m_Positions[p1Index].timeStamp, animationTime); 501 glm::vec3 finalPosition = glm::mix(m_Positions[p0Index].position, 502 m_Positions[p1Index].position 503 , scaleFactor); 504 return <function id='55'>glm::translate</function>(glm::mat4(1.0f), finalPosition); 505 } 506 507 /* figures out which rotations keys to interpolate b/w and performs the interpolation 508 and returns the rotation matrix */ 509 glm::mat4 InterpolateRotation(float animationTime) 510 { 511 if (1 == m_NumRotations) 512 { 513 auto rotation = glm::normalize(m_Rotations[0].orientation); 514 return glm::toMat4(rotation); 515 } 516 517 int p0Index = GetRotationIndex(animationTime); 518 int p1Index = p0Index + 1; 519 float scaleFactor = GetScaleFactor(m_Rotations[p0Index].timeStamp, 520 m_Rotations[p1Index].timeStamp, animationTime); 521 glm::quat finalRotation = glm::slerp(m_Rotations[p0Index].orientation, 522 m_Rotations[p1Index].orientation, scaleFactor); 523 finalRotation = glm::normalize(finalRotation); 524 return glm::toMat4(finalRotation); 525 } 526 527 /* figures out which scaling keys to interpolate b/w and performs the interpolation 528 and returns the scale matrix */ 529 glm::mat4 Bone::InterpolateScaling(float animationTime) 530 { 531 if (1 == m_NumScalings) 532 return <function id='56'>glm::scale</function>(glm::mat4(1.0f), m_Scales[0].scale); 533 534 int p0Index = GetScaleIndex(animationTime); 535 int p1Index = p0Index + 1; 536 float scaleFactor = GetScaleFactor(m_Scales[p0Index].timeStamp, 537 m_Scales[p1Index].timeStamp, animationTime); 538 glm::vec3 finalScale = glm::mix(m_Scales[p0Index].scale, 539 m_Scales[p1Index].scale, scaleFactor); 540 return <function id='56'>glm::scale</function>(glm::mat4(1.0f), finalScale); 541 } 542 }; 543 </code></pre> 544 545 <p> 546 547 We start by creating 3 structs for our key types. Each struct holds a value and a time stamp. Timestamp tells us at what point of an animation we need to interpolate to its value. 548 <code>Bone</code> has a constructor which reads from <code>aiNodeAnim</code> and stores keys and their timestamps to <code>mPositionKeys, mRotationKeys & mScalingKeys </code>. The main interpolation process 549 starts from <code>Update(float animationTime)</code> which gets called every frame. This function calls respective interpolation functions for all key types and combines all final interpolation results 550 and store it to a 4x4 Matrix <code>m_LocalTransform</code>. The interpolations functions for translation & scale keys are similar but for rotation we are using <code>Slerp</code> to interpolate between quaternions. 551 Both <code>Lerp</code> & <code>Slerp</code> takes 3 arguments. First argument takes last key, second argument takes next key and third argument takes value of range 0-1,we call it scale factor here. 552 Let's see how we calculate this scale factor in function <code>GetScaleFactor</code>... 553 554 <p><img src="/img/guest/2020/skeletal_animation/scale_factor.png" alt="skin"/></p> 555 556 <p>In code...</p> 557 558 <p><b> float midWayLength = animationTime - lastTimeStamp; </b></p> 559 <p><b> float framesDiff = nextTimeStamp - lastTimeStamp;</b></p> 560 <p><b> scaleFactor = midWayLength / framesDiff; </b></p> 561 <p></p> 562 </p> 563 564 Let's move on to <code><b>Animation</b></code> class now... 565 566 <pre><code>struct AssimpNodeData 567 { 568 glm::mat4 transformation; 569 std::string name; 570 int childrenCount; 571 std::vector<AssimpNodeData> children; 572 }; 573 574 class Animation 575 { 576 public: 577 Animation() = default; 578 579 Animation(const std::string& animationPath, Model* model) 580 { 581 Assimp::Importer importer; 582 const aiScene* scene = importer.ReadFile(animationPath, aiProcess_Triangulate); 583 assert(scene && scene->mRootNode); 584 auto animation = scene->mAnimations[0]; 585 m_Duration = animation->mDuration; 586 m_TicksPerSecond = animation->mTicksPerSecond; 587 ReadHeirarchyData(m_RootNode, scene->mRootNode); 588 ReadMissingBones(animation, *model); 589 } 590 591 ~Animation() 592 { 593 } 594 595 Bone* FindBone(const std::string& name) 596 { 597 auto iter = std::find_if(m_Bones.begin(), m_Bones.end(), 598 [&](const Bone& Bone) 599 { 600 return Bone.GetBoneName() == name; 601 } 602 ); 603 if (iter == m_Bones.end()) return nullptr; 604 else return &(*iter); 605 } 606 607 608 inline float GetTicksPerSecond() { return m_TicksPerSecond; } 609 610 inline float GetDuration() { return m_Duration;} 611 612 inline const AssimpNodeData& GetRootNode() { return m_RootNode; } 613 614 inline const std::map<std::string,BoneInfo>& GetBoneIDMap() 615 { 616 return m_BoneInfoMap; 617 } 618 619 private: 620 void ReadMissingBones(const aiAnimation* animation, Model& model) 621 { 622 int size = animation->mNumChannels; 623 624 auto& boneInfoMap = model.GetBoneInfoMap();//getting m_BoneInfoMap from Model class 625 int& boneCount = model.GetBoneCount(); //getting the m_BoneCounter from Model class 626 627 //reading channels(bones engaged in an animation and their keyframes) 628 for (int i = 0; i < size; i++) 629 { 630 auto channel = animation->mChannels[i]; 631 std::string boneName = channel->mNodeName.data; 632 633 if (boneInfoMap.find(boneName) == boneInfoMap.end()) 634 { 635 boneInfoMap[boneName].id = boneCount; 636 boneCount++; 637 } 638 m_Bones.push_back(Bone(channel->mNodeName.data, 639 boneInfoMap[channel->mNodeName.data].id, channel)); 640 } 641 642 m_BoneInfoMap = boneInfoMap; 643 } 644 645 void ReadHeirarchyData(AssimpNodeData& dest, const aiNode* src) 646 { 647 assert(src); 648 649 dest.name = src->mName.data; 650 dest.transformation = AssimpGLMHelpers::ConvertMatrixToGLMFormat(src->mTransformation); 651 dest.childrenCount = src->mNumChildren; 652 653 for (int i = 0; i < src->mNumChildren; i++) 654 { 655 AssimpNodeData newData; 656 ReadHeirarchyData(newData, src->mChildren[i]); 657 dest.children.push_back(newData); 658 } 659 } 660 float m_Duration; 661 int m_TicksPerSecond; 662 std::vector<Bone> m_Bones; 663 AssimpNodeData m_RootNode; 664 std::map<std::string, BoneInfo> m_BoneInfoMap; 665 }; 666 </code></pre> 667 668 <p> Here, creation of an <code>Animation</code> object starts with a constructor. It takes two arguments. First, path to the animation file & second parameter is the <code>Model</code> for this animation. 669 You will see later ahead why we need this <code>Model</code> reference here. We then create an <code>Assimp::Importer</code> to read the animation file, followed by an <code>assert</code> check which will throw 670 an error if animation could not be found. Then we read general animation data like how long is this animation which is <code>mDuration</code> and the animation speed represented by <code>mTicksPerSecond</code>. 671 We then call <code>ReadHeirarchyData</code> which replicates <code>aiNode</code> heirarchy of Assimp and creates heirarchy of <code>AssimpNodeData</code>. 672 </p> 673 674 <p> Then we call a function called <code>ReadMissingBones</code>. I had to write this function because sometimes when I loaded FBX model separately, it had some bones missing and I found those missing bones in 675 the animation file. This function reads the missing bones information and stores their information in <code>m_BoneInfoMap</code> of <code>Model</code> and saves a reference of <code>m_BoneInfoMap</code> locally in 676 the m_BoneInfoMap.</p> 677 678 <p>And we have our animation ready. Now let's look at our final stage, The Animator class...</p> 679 680 <pre><code>class Animator 681 { 682 public: 683 Animator::Animator(Animation* Animation) 684 { 685 m_CurrentTime = 0.0; 686 m_CurrentAnimation = currentAnimation; 687 688 m_FinalBoneMatrices.reserve(100); 689 690 for (int i = 0; i < 100; i++) 691 m_FinalBoneMatrices.push_back(glm::mat4(1.0f)); 692 } 693 694 void Animator::UpdateAnimation(float dt) 695 { 696 m_DeltaTime = dt; 697 if (m_CurrentAnimation) 698 { 699 m_CurrentTime += m_CurrentAnimation->GetTicksPerSecond() * dt; 700 m_CurrentTime = fmod(m_CurrentTime, m_CurrentAnimation->GetDuration()); 701 CalculateBoneTransform(&m_CurrentAnimation->GetRootNode(), glm::mat4(1.0f)); 702 } 703 } 704 705 void Animator::PlayAnimation(Animation* pAnimation) 706 { 707 m_CurrentAnimation = pAnimation; 708 m_CurrentTime = 0.0f; 709 } 710 711 void Animator::CalculateBoneTransform(const AssimpNodeData* node, glm::mat4 parentTransform) 712 { 713 std::string nodeName = node->name; 714 glm::mat4 nodeTransform = node->transformation; 715 716 Bone* Bone = m_CurrentAnimation->FindBone(nodeName); 717 718 if (Bone) 719 { 720 Bone->Update(m_CurrentTime); 721 nodeTransform = Bone->GetLocalTransform(); 722 } 723 724 glm::mat4 globalTransformation = parentTransform * nodeTransform; 725 726 auto boneInfoMap = m_CurrentAnimation->GetBoneIDMap(); 727 if (boneInfoMap.find(nodeName) != boneInfoMap.end()) 728 { 729 int index = boneInfoMap[nodeName].id; 730 glm::mat4 offset = boneInfoMap[nodeName].offset; 731 m_FinalBoneMatrices[index] = globalTransformation * offset; 732 } 733 734 for (int i = 0; i < node->childrenCount; i++) 735 CalculateBoneTransform(&node->children[i], globalTransformation); 736 } 737 738 std::vector<glm::mat4> GetFinalBoneMatrices() 739 { 740 return m_FinalBoneMatrices; 741 } 742 743 private: 744 std::vector<glm::mat4> m_FinalBoneMatrices; 745 Animation* m_CurrentAnimation; 746 float m_CurrentTime; 747 float m_DeltaTime; 748 }; 749 </code></pre> 750 751 <p> 752 753 <code>Animator</code> constructor takes an animation to play and 754 then it proceeds to reset the animation time <code>m_CurrentTime</code> to 0. 755 It also initializes <code>m_FinalBoneMatrices</code> 756 which is a <code>std::vector<glm::mat4></code>. 757 The main point of attention here is <code>UpdateAnimation(float deltaTime)</code> function. 758 It advances the <code>m_CurrentTime</code> with rate of 759 <code>m_TicksPerSecond</code> and then calls the <code>CalculateBoneTransform</code> function. 760 We will pass two arguments in the start, first is the <code>m_RootNode</code> of <code>m_CurrentAnimation</code> 761 and second is an identity matrix passed as <code>parentTransform</code> This function then check if <code>m_RootNode</code>s bone is engaged in this animation by finding it in <code>m_Bones</code> array of <code>Animation</code>. 762 If bone is found then it calls <code>Bone.Update()</code> function which interpolates all bones and return local bone transform matrix to 763 <code>nodeTransform</code>. 764 But this is local space matrix and will move bone around origin if passed in shaders. So we multiply this <code>nodeTransform</code> with <code>parentTransform</code> and 765 we store the result in <code>globalTransformation</code>. This would be enough but vertices are still in default model space. 766 we find offset matrix in <code>m_BoneInfoMap</code> and then multiply it 767 with <code>globalTransfromMatrix</code>. 768 We will also get the id index which will be used to write final transformation of this bone to m_FinalBoneMatrices. 769 </p> 770 771 <p> 772 Finally! we call <code>CalculateBoneTransform</code> for each child nodes of this node and pass <code>globalTransformation</code> as <code>parentTransform</code>. 773 We break this recursive loop when there will no children 774 left to process further. 775 </p> 776 777 </p> 778 </p> 779 780 <h3> Let's Animate</h3> 781 782 <p> 783 Fruit of our hardwork is finally here! Here's how we will play the animation in <code>main.cpp</code> ... 784 </p> 785 786 <pre><code>int main() 787 { 788 ... 789 790 Model ourModel(FileSystem::getPath("resources/objects/vampire/dancing_vampire.dae")); 791 Animation danceAnimation(FileSystem::getPath( 792 "resources/objects/vampire/dancing_vampire.dae"), &ourModel); 793 Animator animator(&danceAnimation); 794 795 // draw in wireframe 796 //<function id='43'>glPolygonMode</function>(GL_FRONT_AND_BACK, GL_LINE); 797 798 // render loop 799 // ----------- 800 while (!<function id='14'>glfwWindowShouldClose</function>(window)) 801 { 802 // per-frame time logic 803 // -------------------- 804 float currentFrame = <function id='47'>glfwGetTime</function>(); 805 deltaTime = currentFrame - lastFrame; 806 lastFrame = currentFrame; 807 808 // input 809 // ----- 810 processInput(window); 811 animator.UpdateAnimation(deltaTime); 812 813 // render 814 // ------ 815 <function id='13'><function id='10'>glClear</function>Color</function>(0.05f, 0.05f, 0.05f, 1.0f); 816 <function id='10'>glClear</function>(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); 817 818 // don't forget to enable shader before setting uniforms 819 ourShader.use(); 820 821 // view/projection transformations 822 glm::mat4 projection = <function id='58'>glm::perspective</function>(<function id='63'>glm::radians</function>(camera.Zoom), 823 (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f); 824 glm::mat4 view = camera.GetViewMatrix(); 825 ourShader.setMat4("projection", projection); 826 ourShader.setMat4("view", view); 827 828 auto transforms = animator.GetFinalBoneMatrices(); 829 for (int i = 0; i < transforms.size(); ++i) 830 ourShader.setMat4("finalBonesTransformations[" + std::to_string(i) + "]", 831 transforms[i]); 832 833 // render the loaded model 834 glm::mat4 model = glm::mat4(1.0f); 835 model = <function id='55'>glm::translate</function>(model, glm::vec3(0.0f, -0.4f, 0.0f)); 836 model = <function id='56'>glm::scale</function>(model, glm::vec3(.5f, .5f, .5f)); 837 ourShader.setMat4("model", model); 838 ourModel.Draw(ourShader); 839 840 // glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.) 841 // ------------------------------------------------------------------------------- 842 <function id='24'>glfwSwapBuffers</function>(window); 843 <function id='23'>glfwPollEvents</function>(); 844 } 845 846 // glfw: terminate, clearing all previously allocated GLFW resources. 847 // ------------------------------------------------------------------ 848 <function id='25'>glfwTerminate</function>(); 849 return 0; 850 } 851 </code></pre> 852 853 <p> 854 855 We start with loading our <code>Model</code> which will setup bone weight data for the shader and then create an <code>Animation</code> by giving it the path. 856 Then we create our <code>Animator</code> object by passing it the created <code>Animation</code>. In render loop we then update our <code>Animator</code>, take the 857 final bone transformations and give it to shaders. Here's the output we all have been waiting for... 858 859 </p> 860 861 <img src="/img/guest/2020/skeletal_animation/output.gif" alt="output"> 862 863 <p> Download the model used <a href="/data/models/vampire.zip">here.</a> Note that animations 864 and meshes are baked in single DAE(collada) file. You can find the full source code <a href="/code_viewer_gh.php?code=src/8.guest/2020/skeletal_animation/skeletal_animation.cpp" target="_blank">here</a>. 865 866 <h3>Further reading</h3> 867 <ul> 868 <li><a href="http://www.songho.ca/math/quaternion/quaternion.html" target="_blank"> 869 Quaternions</a>: An article by songho to understand quaternions in depth.</li> 870 <li><a href="http://ogldev.atspace.co.uk/www/tutorial38/tutorial38.html" target="_blank"> 871 Skeletal Animation with Assimp</a>: An article by OGL Dev.</li> 872 <li><a href="https://youtu.be/f3Cr8Yx3GGA" target="_blank"> 873 Skeletal Animation with Java</a>: A fantastic youtube playlist by Thin Matrix.</li> 874 <li><a href="https://www.gamasutra.com/view/feature/131686/rotating_objects_using_quaternions.php" target="_blank"> 875 Why Quaternions should be used for Rotation</a>: An awesome gamasutra article.</li> 876 877 </ul> 878 879 880 881 <author> 882 <strong>Article by: </strong>Ankit Singh Kushwah<br/> 883 <strong>Contact: </strong><a href="mailto:eklavyagames@gmail.com" target="_blank">e-mail</a> 884 </author> 885 886 </div> 887