1 /** 2 * All classes dealing with using 3D skeletal animation 3 */ 4 module dash.components.animation; 5 import dash.core.properties; 6 import dash.components; 7 import dash.utility; 8 9 import derelict.assimp3.assimp; 10 import std..string: fromStringz; 11 import std.conv: to; 12 13 mixin( registerComponents!() ); 14 15 /** 16 * Animation object which handles all animation specific to the gameobject 17 */ 18 class Animation : Component 19 { 20 private: 21 /// Asset animation that the gameobject is animating based off of 22 @ignore 23 AnimationData _animationData; 24 /// Current animation out of all the animations in the asset animation 25 @ignore 26 int _currentAnim; 27 /// Current time of the animation 28 @ignore 29 float _currentAnimTime; 30 /// Bone transforms for the current pose 31 @ignore 32 mat4f[] _currBoneTransforms; 33 /// If the gameobject should be animating 34 @ignore 35 bool _animating; 36 37 /// Animation to return to if _animateOnce is true 38 @ignore 39 int _returnAnimation; 40 /// If the animation is animating once, then returning to _returnAnimation 41 @ignore 42 bool _animateOnce; 43 44 public: 45 /// Bone transforms for the current pose (Passed to the shader) 46 mixin( Property!_currBoneTransforms ); 47 48 this() 49 { 50 _currentAnim = 0; 51 _currentAnimTime = 0.0f; 52 _animating = true; 53 } 54 55 /** 56 * Create animation object based on asset animation 57 */ 58 this( AnimationData assetAnimation ) 59 { 60 this(); 61 _animationData = assetAnimation; 62 } 63 64 /** 65 * Updates the animation, updating time and getting a pose based on time 66 */ 67 override void update() 68 { 69 if( _animating ) 70 { 71 // Update currentanimtime based on deltatime and animations fps 72 _currentAnimTime += Time.deltaTime * 24.0f; 73 74 if( _currentAnimTime >= _animationData.animationSet[ _currentAnim ].duration * 24 - 1 ) 75 { 76 _currentAnimTime = 0.0f; 77 78 if( _animateOnce ) 79 { 80 _animateOnce = false; 81 _currentAnim = _returnAnimation; 82 } 83 } 84 } 85 86 // Calculate and store array of bonetransforms to pass to the shader 87 currBoneTransforms = _animationData.getTransformsAtTime( _currentAnim, _currentAnimTime ); 88 } 89 90 /** 91 * Continue animating. 92 */ 93 void play() 94 { 95 _animating = true; 96 } 97 /** 98 * Pause the animation 99 */ 100 void pause() 101 { 102 _animating = false; 103 } 104 /** 105 * Stops the animation, moving to the beginning 106 */ 107 void stop() 108 { 109 _animating = false; 110 _currentAnimTime = 0.0f; 111 } 112 /** 113 * Switches the current animation 114 */ 115 void changeAnimation( int animNumber, int startAnimTime ) 116 { 117 if( animNumber < _animationData.animationSet.length ) 118 { 119 _currentAnim = animNumber; 120 _currentAnimTime = startAnimTime; 121 } 122 else 123 warning( "Could not change to new animation, the animation did not exist." ); 124 } 125 /** 126 * Runs an animation once, then returns 127 */ 128 void runAnimationOnce( int animNumber, int returnAnimNumber ) 129 { 130 if( animNumber < _animationData.animationSet.length ) 131 { 132 _animateOnce = true; 133 _currentAnim = animNumber; 134 _returnAnimation = returnAnimNumber; 135 _currentAnimTime = 0; 136 } 137 else 138 warning( "Could not change to new animation, the animation did not exist." ); 139 } 140 141 /** 142 * Shutdown the gameobjects animation data 143 */ 144 override void shutdown() 145 { 146 147 } 148 } 149 150 /** 151 * Stores the animation skeleton/bones, stores the animations poses, and makes this information accessible to gameobjects 152 */ 153 class AnimationData : Asset 154 { 155 private: 156 /// List of animations, containing all of the information specific to each 157 AnimationSet[] _animationSet; 158 /// Amount of bones 159 int _numberOfBones; 160 /// Hierarchy of bones for this animation 161 Bone boneHierarchy; 162 bool _isUsed; 163 164 public: 165 /// List of animations, containing all of the information specific to each 166 mixin( Property!_animationSet ); 167 /// Amount of bones 168 mixin( Property!_numberOfBones ); 169 /// Whether or not the material is actually used. 170 mixin( Property!( _isUsed, AccessModifier.Package ) ); 171 172 /** 173 * Create the assetanimation, parsing all of the animation data 174 * 175 * Params: 176 * animations = List of assimp animation/poses object 177 * numAnimations = Number of 178 * mesh = Assimp mesh/bone object 179 * nodeHierarchy = Hierarchy of bones/filler nodes used for the animation 180 */ 181 this( Resource res, const(aiAnimation**) animations, int numAnimations, const(aiMesh*) mesh, const(aiNode*) nodeHierarchy ) 182 { 183 super( res ); 184 185 for( int i = 0; i < nodeHierarchy.mNumChildren; i++) 186 { 187 string name = nodeHierarchy.mChildren[ i ].mName.data.ptr.fromStringz().to!string; 188 if( findBoneWithName( name, mesh ) != -1 ) 189 { 190 boneHierarchy = makeBonesFromHierarchy( mesh, nodeHierarchy.mChildren[ i ] ); 191 } 192 } 193 194 for( int ii = 0; ii < numAnimations; ii++) 195 addAnimationSet( animations[ ii ], 24 ); 196 } 197 198 /** 199 * Returns the animation as an addible component. 200 */ 201 Animation getComponent() 202 { 203 return new Animation( this ); 204 } 205 206 /** 207 * Recurse the node hierarchy, parsing it into a usable bone hierarchy 208 * 209 * Params: 210 * animation = Assimp animation/poses object 211 * mesh = Assimp mesh/bone object 212 * currNode = The current node checking in the hierarchy 213 * 214 * Returns: The bone based off of the currNode data 215 */ 216 Bone makeBonesFromHierarchy( const(aiMesh*) mesh, const(aiNode*) currNode ) 217 { 218 //NOTE: Currently only works if each node is a Bone, works with bones without animation b/c of storing nodeOffset 219 //NOTE: Needs to be reworked to support this in the future 220 Bone bone; 221 string name = currNode.mName.data.ptr.fromStringz().to!string; 222 int boneNumber = findBoneWithName( name, mesh ); 223 224 if( boneNumber != -1 ) 225 { 226 bone = new Bone( name, boneNumber ); 227 bone.offset = convertAIMatrix( mesh.mBones[ bone.boneNumber ].mOffsetMatrix ); 228 bone.nodeOffset = convertAIMatrix( currNode.mTransformation ); 229 _numberOfBones++; 230 } 231 232 for( int i = 0; i < currNode.mNumChildren; i++ ) 233 { 234 string childName = currNode.mChildren[ i ].mName.data.ptr.fromStringz().to!string; 235 int childBoneNumber = findBoneWithName( childName, mesh ); 236 237 // Ensure end nodes are bones, otherwise do not keep 238 if( boneNumber != -1 && childBoneNumber != -1 ) 239 bone.children ~= makeBonesFromHierarchy( mesh, currNode.mChildren[ i ] ); 240 else if( childBoneNumber != -1 ) 241 return makeBonesFromHierarchy( mesh, currNode.mChildren[ i ] ); 242 } 243 244 return bone; 245 } 246 /** 247 * Get a bone number by matching name bones in mesh 248 * 249 * Params: 250 * name = Name searching for 251 * mesh = Mesh containing bones to check 252 * 253 * Returns: Bone number of desired bone 254 */ 255 int findBoneWithName( string name, const(aiMesh*) mesh ) 256 { 257 for( int i = 0; i < mesh.mNumBones; i++ ) 258 { 259 if( name == mesh.mBones[ i ].mName.data.ptr.fromStringz ) 260 { 261 return i; 262 } 263 } 264 265 return -1; 266 } 267 268 void addAnimationSet( const(aiAnimation*) animation, int fps ) 269 { 270 AnimationSet newAnimSet; 271 newAnimSet.duration = cast(float)animation.mDuration; 272 newAnimSet.fps = fps; 273 for( int i = 0; i < _numberOfBones; i++) 274 { 275 newAnimSet.bonePoses ~= new BonePose(); 276 } 277 addPoses( animation, boneHierarchy, newAnimSet ); 278 _animationSet ~= newAnimSet; 279 } 280 void addPoses( const(aiAnimation*) animation, Bone currBone, AnimationSet newAnimSet ) 281 { 282 if( currBone.boneNumber != -1 ) 283 { 284 assignAnimationData( animation, currBone.name, newAnimSet.bonePoses[ currBone.boneNumber ] ); 285 } 286 287 // Add to children 288 for( int i = 0; i < currBone.children.length; i++ ) 289 { 290 addPoses( animation, currBone.children[ i ], newAnimSet ); 291 } 292 } 293 /** 294 * Access the animation channels to find a match with the bone, then store its animation data 295 * 296 * Params: 297 * animation = Assimp animation/poses object 298 * boneToAssign = Bone to assign the animation keys/poses 299 */ 300 void assignAnimationData( const(aiAnimation*) animation, string boneName, BonePose poseToAssign ) 301 { 302 for( int i = 0; i < animation.mNumChannels; i++) 303 { 304 if( animation.mChannels[ i ].mNodeName.data.ptr.fromStringz == boneName ) 305 { 306 // Assign the bone animation data to the bone 307 poseToAssign.positionKeys = convertVectorArray( animation.mChannels[ i ].mPositionKeys, 308 animation.mChannels[ i ].mNumPositionKeys ); 309 poseToAssign.scaleKeys = convertVectorArray( animation.mChannels[ i ].mScalingKeys, 310 animation.mChannels[ i ].mNumScalingKeys ); 311 poseToAssign.rotationKeys = convertQuat( animation.mChannels[ i ].mRotationKeys, 312 animation.mChannels[ i ].mNumRotationKeys ); 313 } 314 } 315 } 316 317 /** 318 * Called by gameobject animation components to get an animation pose 319 * 320 * Params: 321 * time = The current animations time 322 * 323 * Returns: The boneTransforms, returned to the gameobject animation component 324 */ 325 mat4f[] getTransformsAtTime( int animationNumber, float time ) 326 { 327 mat4f[] boneTransforms = new mat4f[ _numberOfBones ]; 328 329 // Check shader/model 330 for( int i = 0; i < _numberOfBones; i++) 331 { 332 boneTransforms[ i ] = mat4f.identity; 333 } 334 335 fillTransforms( animationSet[ animationNumber ].bonePoses, boneTransforms, boneHierarchy, time, mat4f.identity ); 336 337 return boneTransforms; 338 } 339 /** 340 * Recurse the bone hierarchy, filling up the bone transforms along the way 341 * 342 * Params: 343 * transforms = The boneTransforms to fill up 344 * bone = The current bone checking 345 * time = The animations current time 346 * parentTransform = The parents transform (which effects this bone) 347 */ 348 void fillTransforms( BonePose[] bonePoses, mat4f[] transforms, Bone bone, float time, mat4f parentTransform ) 349 { 350 BonePose bonePose = bonePoses[ bone.boneNumber ]; 351 mat4f finalTransform; 352 if( bonePose.positionKeys.length == 0 && bonePose.rotationKeys.length == 0 && bonePose.scaleKeys.length == 0 ) 353 { 354 finalTransform = parentTransform * bone.nodeOffset; 355 transforms[ bone.boneNumber ] = finalTransform * bone.offset; 356 } 357 else 358 { 359 mat4f boneTransform = mat4f.identity; 360 361 if( bonePose.positionKeys.length > cast(int)time ) 362 { 363 boneTransform = boneTransform.translation( bonePose.positionKeys[ cast(int)time ].vector[ 0 ], 364 bonePose.positionKeys[ cast(int)time ].vector[ 1 ], 365 bonePose.positionKeys[ cast(int)time ].vector[ 2 ] ); 366 } 367 if( bonePose.rotationKeys.length > cast(int)time ) 368 { 369 boneTransform = boneTransform * bonePose.rotationKeys[ cast(int)time ].toMatrix!4; 370 } 371 if( bonePose.scaleKeys.length > cast(int)time ) 372 { 373 boneTransform = boneTransform.scale( bonePose.scaleKeys[ cast(int)time ].vector[ 0 ], 374 bonePose.scaleKeys[ cast(int)time ].vector[ 1 ], 375 bonePose.scaleKeys[ cast(int)time ].vector[ 2 ] ); 376 } 377 378 finalTransform = parentTransform * boneTransform; 379 transforms[ bone.boneNumber ] = finalTransform * bone.offset; 380 } 381 382 // Check children 383 for( int i = 0; i < bone.children.length; i++ ) 384 { 385 fillTransforms( bonePoses, transforms, bone.children[ i ], time, finalTransform ); 386 } 387 } 388 389 /** 390 * Converts a aiVectorKey[] to vec3f[]. 391 * 392 * Params: 393 * quaternions = aiVectorKey[] to be converted 394 * numKeys = Number of keys in vector array 395 * 396 * Returns: The vectors in vector[] format 397 */ 398 vec3f[] convertVectorArray( const(aiVectorKey*) vectors, int numKeys ) 399 { 400 vec3f[] keys; 401 for( int i = 0; i < numKeys; i++ ) 402 { 403 aiVector3D vector = vectors[ i ].mValue; 404 keys ~= vec3f( vector.x, vector.y, vector.z ); 405 } 406 407 return keys; 408 } 409 /** 410 * Converts a aiQuatKey[] to quat[]. 411 * 412 * Params: 413 * quaternions = aiQuatKey[] to be converted 414 * numKeys = Number of keys in quaternions array 415 * 416 * Returns: The quaternions in quat[] format 417 */ 418 quatf[] convertQuat( const(aiQuatKey*) quaternions, int numKeys ) 419 { 420 quatf[] keys; 421 for( int i = 0; i < numKeys; i++ ) 422 { 423 aiQuatKey quaternion = quaternions[ i ]; 424 keys ~= quatf( quaternion.mValue.w, quaternion.mValue.x, quaternion.mValue.y, quaternion.mValue.z ); 425 } 426 427 return keys; 428 } 429 /** 430 * Converts an aiMatrix to a mat4 431 * 432 * Params: 433 * aiMatrix = Matrix to be converted 434 * 435 * Returns: The matrix in mat4 format 436 */ 437 mat4f convertAIMatrix( aiMatrix4x4 aiMatrix ) 438 { 439 mat4f matrix = mat4f.identity; 440 441 matrix[0][0] = aiMatrix.a1; 442 matrix[0][1] = aiMatrix.a2; 443 matrix[0][2] = aiMatrix.a3; 444 matrix[0][3] = aiMatrix.a4; 445 matrix[1][0] = aiMatrix.b1; 446 matrix[1][1] = aiMatrix.b2; 447 matrix[1][2] = aiMatrix.b3; 448 matrix[1][3] = aiMatrix.b4; 449 matrix[2][0] = aiMatrix.c1; 450 matrix[2][1] = aiMatrix.c2; 451 matrix[2][2] = aiMatrix.c3; 452 matrix[2][3] = aiMatrix.c4; 453 matrix[3][0] = aiMatrix.d1; 454 matrix[3][1] = aiMatrix.d2; 455 matrix[3][2] = aiMatrix.d3; 456 matrix[3][3] = aiMatrix.d4; 457 458 return matrix; 459 } 460 461 /** 462 * Shutdown the animation bone/pose data 463 */ 464 override void shutdown() 465 { 466 467 } 468 469 /** 470 * A single animation track, storing its bones and poses 471 */ 472 struct AnimationSet 473 { 474 float duration; 475 float fps; 476 BonePose[] bonePoses; 477 } 478 /** 479 * All the bone transforms/rotations/scales for a sc 480 */ 481 class BonePose 482 { 483 vec3f[] positionKeys; 484 quatf[] rotationKeys; 485 vec3f[] scaleKeys; 486 } 487 /** 488 * A bone in the animation, storing everything it needs 489 */ 490 class Bone 491 { 492 this( string boneName, int boneNum ) 493 { 494 name = boneName; 495 boneNumber = boneNum; 496 } 497 498 string name; 499 int boneNumber; 500 Bone[] children; 501 502 mat4f offset; 503 mat4f nodeOffset; 504 } 505 }