├── LICENSE ├── README.md └── src ├── cesium_model_animation_player.js └── cesium_model_animation_player.ts /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Prominent Edge 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cesium-ModelAnimationPlayer 2 | An animation player for use with Cesium entities to play glTF animations independent of the standard timeline. 3 | 4 | ## Installation 5 | Currently the simplest way to install the code is to copy the `cesium_model_animation_player.js` file and incorporate it into your project's source directory 6 | 7 | ## Examples 8 | ### Basic usage 9 | First load the animation set from the glTF file. Currently only .glb format with embedded asset data is supported. 10 | ``` 11 | let animation_set = await AnimationParser.parseAnimationSetFromFile('../assets/my_model.glb'); 12 | ``` 13 | Next instantiate the AnimationPlayer, passing it the animation set and the Cesium entity to animate, along with desired playback FPS. Default playback is set to "clamp", but looping is supported as illustrated below. 14 | ``` 15 | let player = new AnimationPlayer(animation_set, entity, 30); 16 | player.loop_type = LOOP_TYPE.LOOP; 17 | player.play("animation_name"); 18 | ``` 19 | You can set the playback speed (multiplier) as well, negative values will cause the animation to play in reverse. 20 | ``` 21 | player.speed = 2.0; 22 | ``` 23 | 24 | ### Manual control 25 | Instead of calling `play()`, you can update the player manually as well. The argument is the current time in seconds you want to set the animation to. Make sure the player is stopped first! 26 | ``` 27 | player.stop(); 28 | player.setAnimation("current_animation_name"); 29 | player.setTime(current_time); 30 | ``` 31 | You can also update the player by setting the animation based on a percentage of its duration. 32 | ``` 33 | player.setPercent(0.5); 34 | ``` 35 | 36 | ### Debugging tips 37 | You can access information about the animations and the nodes from the animation player directly. 38 | ``` 39 | // check current animation duration 40 | player.current_animation.duration 41 | 42 | // print names of animations associated with this player 43 | for(var i = 0; i < player.animations.length; i++) { 44 | console.log(player.animations[i].name); 45 | } 46 | 47 | // get keyframe information for the current animation (or any animation) 48 | for(track in player.current_animation.tracks) { 49 | console.log(track.translation_keys); 50 | console.log(track.rotation_keys); 51 | console.log(track.scale_keys); 52 | } 53 | ``` 54 | 55 | ## Notes and TODOs 56 | * Models and animations must conform to glTF 2.0 spec 57 | * In order for exported models/animations to be compatible with the player all nodes must be named. 58 | * As mentioned previously, currently only the .glb format is compatible with this system with the animation data embedded in the binary. If there is an urgent need for supporting standard glTF format please create an issue and let us know. 59 | * While the glTF format allows for byte, short, int, and float component types for rotations (`Vec4`), the parser currently assumes floats only. 60 | * Currently the `play()` method operates on the main thread via `setInterval`. In the future this should be re-worked to make use of web workers. 61 | * If you find yourself needing web workers for true concurrency of animation playback, remember that you can still accomplish this by making use of either the `setTime` or `setPercent` methods -------------------------------------------------------------------------------- /src/cesium_model_animation_player.js: -------------------------------------------------------------------------------- 1 | const Cesium = require('cesium/Cesium'); 2 | 3 | export const LOOP_TYPE = Object.freeze({"CLAMP":1, "LOOP":2}); 4 | export const PLAY_STATE = Object.freeze({"PLAY":1, "STOP":2, "PAUSE":3}); 5 | 6 | export class AnimationKey { 7 | constructor(time, value) { 8 | this.time = time; 9 | this.value = value; 10 | } 11 | }; 12 | 13 | export class AnimationTrack { 14 | constructor() { 15 | this.translation_keys = []; 16 | this.rotation_keys = []; 17 | this.scale_keys = []; 18 | } 19 | }; 20 | 21 | export class Animation { 22 | constructor(name) { 23 | this.name = name; 24 | this.duration = 0; 25 | this.tracks = {}; // a dictionary whose keys are node names 26 | } 27 | }; 28 | 29 | export class AnimationSet { 30 | constructor(animations, nodes) { 31 | this.animations = animations; 32 | this.nodes = nodes; 33 | } 34 | }; 35 | 36 | export class AnimationPlayer { 37 | constructor(animation_set, entity, fps) { 38 | this.loop_type = LOOP_TYPE.CLAMP; 39 | this.play_state = PLAY_STATE.STOP; 40 | this.animation_set = animation_set; 41 | this.entity = entity; 42 | if(this.animation_set.animations.length > 0) { 43 | this.current_animation = this.animation_set.animations[0]; 44 | } 45 | else { 46 | this.current_animation = ""; 47 | } 48 | 49 | // set initial node positions for Cesium entity 50 | let cesium_nodes = {}; 51 | 52 | for(var node_name in this.animation_set.nodes) { 53 | if(typeof this.entity.model.nodeTransformations != "undefined" && 54 | typeof this.entity.model.nodeTransformations[node_name] != "undefined"){ 55 | cesium_nodes[node_name] = this.entity.model.nodeTransformations[node_name]; 56 | } else { 57 | cesium_nodes[node_name] = { 58 | translation: new Cesium.Cartesian3(0, 0, 0), 59 | rotation: new Cesium.Cartesian4(0, 0, 0, 1), 60 | scale: new Cesium.Cartesian3(1, 1, 1) 61 | } 62 | } 63 | } 64 | 65 | this.entity.model.nodeTransformations = cesium_nodes; 66 | this.interval_id = -1; 67 | this.current_time = 0; 68 | this.speed = 1; 69 | this._frame_duration = 1.0/fps; 70 | } 71 | 72 | setAnimation(animation_name) { 73 | for(var i = 0; i < this.animation_set.animations.length; i++) { 74 | if(animation_name === this.animation_set.animations[i].name) { 75 | this.current_animation = this.animation_set.animations[i]; 76 | return; 77 | } 78 | } 79 | console.error("Can't set current animation: " + animation_name + " does not exist"); 80 | } 81 | 82 | setFPS(fps) { 83 | this._frame_duration = 1.0/fps; 84 | } 85 | 86 | play(animation_name) { 87 | if(typeof animation_name === 'undefined') { 88 | if(this.play_state === PLAY_STATE.PLAY) { 89 | return; 90 | } else if(this.play_state === PLAY_STATE.PAUSE) { 91 | this.play_state = PLAY_STATE.PLAY; 92 | } else if(this.play_state === PLAY_STATE.STOP) { 93 | this.play_state = PLAY_STATE.PLAY; 94 | this.interval_id = window.setInterval(() => this._update(), this._frame_duration * 1000); 95 | } 96 | return; 97 | } 98 | 99 | let animations = this.animation_set.animations; 100 | for(var i = 0; i < animations.length; i++) { 101 | if(animations[i].name === animation_name) { 102 | this.current_animation = animations[i]; 103 | if(this.play_state === PLAY_STATE.PLAY) { 104 | return; 105 | } else if(this.play_state === PLAY_STATE.PAUSE) { 106 | this.play_state = PLAY_STATE.PLAY; 107 | } else if(this.play_state === PLAY_STATE.STOP) { 108 | this.play_state = PLAY_STATE.PLAY; 109 | this.interval_id = window.setInterval(() => this._update(), this._frame_duration * 1000); 110 | } 111 | return; 112 | } 113 | } 114 | console.error("Can't play animation: " + animation_name + " does not exist"); 115 | } 116 | 117 | _clearUpdateInterval() { 118 | clearInterval(this.interval_id); 119 | this.interval_id = -1; 120 | } 121 | 122 | _update() { 123 | if(this.play_state === PLAY_STATE.PLAY) 124 | this.setTime(this.current_time + this._frame_duration * this.speed); 125 | } 126 | 127 | setPercent(percent) { 128 | if(percent < 0.0) { 129 | percent = 0.0; 130 | } 131 | else if(percent > 1.0) { 132 | percent = 1.0; 133 | } 134 | let time = this.current_animation.duration * percent; 135 | this.setTime(time); 136 | } 137 | 138 | setTime(current_time) { 139 | this.current_time = current_time; 140 | if(this.speed > 0) { 141 | if(this.current_time > this.current_animation.duration) { 142 | if(this.loop_type === LOOP_TYPE.CLAMP) { 143 | this.current_time = this.current_animation.duration; 144 | } else if(this.loop_type === LOOP_TYPE.LOOP) { 145 | this.current_time = 0; 146 | } 147 | } 148 | } else if(this.speed < 0) { 149 | if(this.current_time < 0) { 150 | if(this.loop_type === LOOP_TYPE.CLAMP) { 151 | this.current_time = 0; 152 | } else if(this.loop_type === LOOP_TYPE.LOOP) { 153 | this.current_time = this.current_animation.duration; 154 | } 155 | } 156 | } 157 | 158 | 159 | for(var track_name in this.current_animation.tracks) { 160 | let track = this.current_animation.tracks[track_name]; 161 | let node = this.animation_set.nodes[track_name]; 162 | let curr_trans_keys = this.getKeysAtTime(track.translation_keys, this.current_time); 163 | let curr_rot_keys = this.getKeysAtTime(track.rotation_keys, this.current_time); 164 | let curr_scale_keys = this.getKeysAtTime(track.scale_keys, this.current_time); 165 | 166 | //-------------------------- 167 | // Translation 168 | //-------------------------- 169 | if(typeof curr_trans_keys !== 'undefined' && curr_trans_keys.length > 0) { 170 | let orig_trans = node.translation; 171 | let invMat = node.inv_rotation_matrix; 172 | 173 | if(curr_trans_keys[0].time == curr_trans_keys[1].time) { 174 | let result = new Cesium.Cartesian3(curr_trans_keys[0].value[0] - orig_trans[0], curr_trans_keys[0].value[1] - orig_trans[1], curr_trans_keys[0].value[2] - orig_trans[2]); 175 | //get the result expressed in local node space 176 | Cesium.Matrix3.multiplyByVector(invMat, result, result); 177 | this.entity.model.nodeTransformations[track_name].translation = result; 178 | } else { 179 | let keyDelta = curr_trans_keys[1].time - curr_trans_keys[0].time; 180 | let timeDelta = this.current_time - curr_trans_keys[0].time; 181 | let t = timeDelta/keyDelta; 182 | let start = new Cesium.Cartesian3(curr_trans_keys[0].value[0], curr_trans_keys[0].value[1], curr_trans_keys[0].value[2]); 183 | let end = new Cesium.Cartesian3(curr_trans_keys[1].value[0], curr_trans_keys[1].value[1], curr_trans_keys[1].value[2]); 184 | 185 | //interpolate the translation keys 186 | let result = new Cesium.Cartesian3(); 187 | Cesium.Cartesian3.lerp(start, end, t, result); 188 | 189 | //account for delta / relative offset from original translation 190 | result.x -= orig_trans[0]; 191 | result.y -= orig_trans[1]; 192 | result.z -= orig_trans[2]; 193 | 194 | //get the result expressed in local node space 195 | Cesium.Matrix3.multiplyByVector(invMat, result, result); 196 | 197 | this.entity.model.nodeTransformations[track_name].translation = result; 198 | } 199 | } 200 | 201 | //-------------------------- 202 | // Rotation 203 | //-------------------------- 204 | if(typeof curr_rot_keys !== 'undefined' && curr_rot_keys.length > 0) { 205 | 206 | let orig_inv = node.inv_rotation; 207 | let invMat = node.inv_rotation_matrix; 208 | 209 | if(curr_rot_keys[0].time == curr_rot_keys[1].time) { 210 | let result = new Cesium.Quaternion(curr_rot_keys[0].value[0], curr_rot_keys[0].value[1], curr_rot_keys[0].value[2], curr_rot_keys[0].value[3]); 211 | 212 | //isolate the axis 213 | let resultAxis = new Cesium.Cartesian3(1,0,0); 214 | let resultAngle = Cesium.Quaternion.computeAngle(result); 215 | if(Math.abs(resultAngle) > Cesium.Math.EPSILON5) 216 | Cesium.Quaternion.computeAxis(result, resultAxis); 217 | 218 | //transform to local node space 219 | Cesium.Matrix3.multiplyByVector(invMat, resultAxis, resultAxis); 220 | 221 | //get the new quaternion expressed in local node space 222 | Cesium.Quaternion.fromAxisAngle(resultAxis, resultAngle, result); 223 | //calc the rotation delta/difference 224 | Cesium.Quaternion.multiply(result, orig_inv, result); 225 | this.entity.model.nodeTransformations[track_name].rotation = result; 226 | } else { 227 | let keyDelta = curr_rot_keys[1].time - curr_rot_keys[0].time; 228 | let timeDelta = this.current_time - curr_rot_keys[0].time; 229 | let t = timeDelta/keyDelta; 230 | let start = new Cesium.Quaternion(curr_rot_keys[0].value[0], curr_rot_keys[0].value[1], curr_rot_keys[0].value[2], curr_rot_keys[0].value[3]); 231 | let end = new Cesium.Quaternion(curr_rot_keys[1].value[0], curr_rot_keys[1].value[1], curr_rot_keys[1].value[2], curr_rot_keys[1].value[3]); 232 | 233 | //slerp the rotation keys 234 | let result = new Cesium.Quaternion(); 235 | Cesium.Quaternion.slerp(start, end, t, result); 236 | 237 | //isolate the axis 238 | let resultAxis = new Cesium.Cartesian3(1,0,0); 239 | let resultAngle = Cesium.Quaternion.computeAngle(result); 240 | if(Math.abs(resultAngle) > Cesium.Math.EPSILON5) 241 | Cesium.Quaternion.computeAxis(result, resultAxis); 242 | 243 | //transform to local node space 244 | Cesium.Matrix3.multiplyByVector(invMat, resultAxis, resultAxis); 245 | 246 | //get the new quaternion expressed in local node space 247 | Cesium.Quaternion.fromAxisAngle(resultAxis, resultAngle, result); 248 | 249 | //calc the rotation delta/difference 250 | Cesium.Quaternion.multiply(result, orig_inv, result); 251 | 252 | this.entity.model.nodeTransformations[track_name].rotation = result; 253 | } 254 | } 255 | 256 | //-------------------------- 257 | // Scale 258 | //-------------------------- 259 | if(typeof curr_scale_keys !== 'undefined' && curr_scale_keys.length > 0) { 260 | let orig_scale = this.animation_set.nodes[track_name].scale; 261 | 262 | if(curr_scale_keys[0].time == curr_scale_keys[1].time) { 263 | let result = new Cesium.Cartesian3(curr_scale_keys[0].value[0] / orig_scale[0], curr_scale_keys[0].value[1] / orig_scale[1], curr_scale_keys[0].value[2] / orig_scale[2]); 264 | this.entity.model.nodeTransformations[track_name].scale = result; 265 | } else { 266 | let keyDelta = curr_scale_keys[1].time - curr_scale_keys[0].time; 267 | let timeDelta = this.current_time - curr_scale_keys[0].time; 268 | let t = timeDelta/keyDelta; 269 | let start = new Cesium.Cartesian3(curr_scale_keys[0].value[0], curr_scale_keys[0].value[1], curr_scale_keys[0].value[2]); 270 | let end = new Cesium.Cartesian3(curr_scale_keys[1].value[0], curr_scale_keys[1].value[1], curr_scale_keys[1].value[2]); 271 | let result = new Cesium.Cartesian3(); 272 | Cesium.Cartesian3.lerp(start, end, t, result); 273 | 274 | //account for delta / relative offset from original scale 275 | result.x /= orig_scale[0]; 276 | result.y /= orig_scale[1]; 277 | result.z /= orig_scale[2]; 278 | this.entity.model.nodeTransformations[track_name].scale = result; 279 | } 280 | } 281 | } 282 | } 283 | 284 | getKeysAtTime(keys, time) { 285 | let result = []; 286 | if(keys.length == 0) 287 | return result; 288 | 289 | //we need to return some value even if the first key for this track isn't reached quite yet 290 | if(keys[0].time > time) { 291 | result.push(keys[0]); 292 | result.push(keys[0]); 293 | return result; 294 | } 295 | 296 | //just clamp the last key if we are at the end 297 | if(time > keys[keys.length-1].time) { 298 | result.push(keys[keys.length-1]); 299 | result.push(keys[keys.length-1]); 300 | return result; 301 | } 302 | 303 | for(var i = 0; i < keys.length-1; i++) { 304 | if(keys[i].time <= time && keys[i+1].time >= time) { 305 | result.push(keys[i]); 306 | result.push(keys[i+1]); 307 | return result; 308 | } 309 | } 310 | } 311 | 312 | stop() { 313 | this.play_state = PLAY_STATE.STOP; 314 | this.current_time = 0; 315 | //reset the node transforms on the entity to the default pose 316 | let cesium_nodes = {}; 317 | for(var node_name in this.animation_set.nodes) { 318 | cesium_nodes[node_name] = { 319 | translation: new Cesium.Cartesian3(0, 0, 0), 320 | rotation: new Cesium.Cartesian4(0, 0, 0, 1), 321 | scale: new Cesium.Cartesian3(1, 1, 1) 322 | } 323 | } 324 | this.entity.model.nodeTransformations = cesium_nodes; 325 | this._clearUpdateInterval(); 326 | 327 | } 328 | 329 | pause() { 330 | //no need to pause if we are not playing 331 | if(this.play_state === PLAY_STATE.PLAY) 332 | this.play_state = PLAY_STATE.PAUSE; 333 | this._clearUpdateInterval(); 334 | } 335 | } 336 | 337 | export class AnimationParser { 338 | static _readFileAsync(file) { 339 | return new Promise((resolve, reject) => { 340 | let reader = new FileReader(); 341 | 342 | reader.onload = () => { 343 | resolve(reader.result); 344 | }; 345 | 346 | reader.onerror = reject; 347 | 348 | reader.readAsArrayBuffer(file); 349 | }); 350 | } 351 | 352 | static _getResourceAsync(uri) { 353 | return new Promise((resolve, reject) => { 354 | var req = new Request(uri); 355 | 356 | fetch(req).then(function(response) { 357 | if (!response.ok) { 358 | reject(new Error(response.statusText)); 359 | } 360 | return response; 361 | }).then(function(response) { 362 | resolve(response.arrayBuffer()); 363 | }); 364 | }); 365 | } 366 | 367 | static parseAnimationNodesFromArrayBuffer(array_buffer) { 368 | // get the length of the JSON data starting at 12 byte offset according to gltf standard 369 | let dv = new DataView(array_buffer, 12, 4); 370 | // don't forget to set little-endian = true when parsing from data view (gltf standard!) 371 | let json_chunk_length = dv.getUint32(0, true); 372 | console.log("gltf JSON length: " + json_chunk_length + " bytes"); 373 | 374 | // get the actual JSON data itself 375 | let json_data_chunk = array_buffer.slice(20, 20+json_chunk_length); 376 | let decoder = new TextDecoder('UTF-8'); 377 | let json_text = decoder.decode(json_data_chunk); 378 | let gltf_json = JSON.parse(json_text); 379 | console.log("gltf JSON loaded successfully:"); 380 | 381 | // store links to parent nodes 382 | for(var i = 0; i < gltf_json.nodes.length; i++) { 383 | if(typeof gltf_json.nodes[i].children != 'undefined') { 384 | for(var k = 0; k < gltf_json.nodes[i].children.length; k++) { 385 | gltf_json.nodes[gltf_json.nodes[i].children[k]].parent = gltf_json.nodes[i].name; 386 | } 387 | } 388 | } 389 | 390 | return gltf_json.nodes; 391 | } 392 | 393 | static parseAnimationsFromArrayBuffer(array_buffer) { 394 | let animations = []; 395 | 396 | // get the length of the JSON data starting at 12 byte offset according to gltf standard 397 | let dv = new DataView(array_buffer, 12, 4); 398 | // don't forget to set little-endian = true when parsing from data view (gltf tandard!) 399 | let json_chunk_length = dv.getUint32(0, true); 400 | console.log("gltf JSON length: " + json_chunk_length + " bytes"); 401 | 402 | // get the actual JSON data itself 403 | let json_data_chunk = array_buffer.slice(20, 20+json_chunk_length); 404 | let decoder = new TextDecoder('UTF-8'); 405 | let json_text = decoder.decode(json_data_chunk); 406 | let gltf_json = JSON.parse(json_text); 407 | console.log("gltf JSON loaded successfully:"); 408 | console.log(gltf_json); 409 | 410 | // get the length of the gltf embedded binary data 411 | let bin_offset = 20+json_chunk_length; 412 | dv = new DataView(array_buffer, bin_offset, 4); 413 | let bin_chunk_length = dv.getUint32(0, true); 414 | console.log("gltf bin length: " + bin_chunk_length + " bytes"); 415 | 416 | // get the actual binary data, we add 8 to get past the "type" and "chunk length" headers 417 | let bin_data_chunk = array_buffer.slice(bin_offset + 8, bin_offset + 8 + bin_chunk_length); 418 | 419 | //-------------------------------------------------- 420 | // get and process all animations 421 | //-------------------------------------------------- 422 | if(typeof gltf_json.animations === 'undefined') 423 | return []; 424 | for(var i = 0; i < gltf_json.animations.length; i++) { 425 | let anim_name = gltf_json.animations[i].name; 426 | if(typeof anim_name == 'undefined' || anim_name == "") 427 | anim_name = "animation_" + i; 428 | let curr_animation = new Animation(anim_name); 429 | console.log("processing animation: " + anim_name); 430 | 431 | for(var k = 0; k < gltf_json.animations[i].channels.length; k++) { 432 | let channel = gltf_json.animations[i].channels[k]; 433 | 434 | // the following will be either "translation, rotation, or scale" 435 | let dof_type = channel.target.path; 436 | 437 | let node = gltf_json.nodes[channel.target.node]; 438 | if(typeof node == 'undefined') { 439 | console.warn("node is undefined for channel " + k); 440 | continue; 441 | } 442 | 443 | let node_name = node.name; 444 | if(typeof node_name == 'undefined' || node.name == "") { 445 | node_name = "node_" + channel.target.node; 446 | } 447 | 448 | // add a new track to this animation for the node if it does not exist already 449 | if(typeof curr_animation.tracks[node_name] == 'undefined') 450 | curr_animation.tracks[node_name] = new AnimationTrack(); 451 | 452 | let sampler = gltf_json.animations[i].samplers[channel.sampler]; 453 | 454 | //-------------------------------------------------- 455 | // get input accessor (keyframe times for this channel's sampler) and process the data for it 456 | //-------------------------------------------------- 457 | let input = gltf_json.accessors[sampler.input]; 458 | //console.log("min: " + input.min + " max: " + input.max); 459 | 460 | let input_accessor_byte_offset = (typeof input.byteOffset == 'undefined' ? 0 : input.byteOffset); 461 | if(input.componentType != 5126) 462 | console.warn("input component type is not a float!"); 463 | 464 | // each element (keyframe timestamp) is a 4 byte float 465 | let input_element_size = 4; 466 | 467 | //use the buffer view and accessor to offset into the binary buffer to retrieve our data 468 | let input_bufferView = gltf_json.bufferViews[input.bufferView]; 469 | let input_accessor_data_offset = input_bufferView.byteOffset + input_accessor_byte_offset; 470 | let input_bin = bin_data_chunk.slice(input_accessor_data_offset, input_accessor_data_offset + input_element_size * input.count); 471 | let input_dv = new DataView(input_bin); 472 | 473 | // parse and store each timestamp out of the buffer 474 | let timestamps = []; 475 | for(var j = 0; j < input.count; j++) { 476 | let timestamp = input_dv.getFloat32(j*4, true); 477 | if(timestamp > curr_animation.duration) { 478 | curr_animation.duration = timestamp; 479 | } 480 | timestamps.push(timestamp); 481 | } 482 | 483 | //-------------------------------------------------- 484 | // get output accessor (keyframe values for this channel's sampler) and process the data for it 485 | //-------------------------------------------------- 486 | let output = gltf_json.accessors[sampler.output]; 487 | 488 | let output_accessor_byte_offset = (typeof output.byteOffset == 'undefined' ? 0 : output.byteOffset); 489 | 490 | // we only care about VEC3 and VEC4 since we are only dealing with rotation, scale, and translation, 491 | // and we are going to assume they are floating point (componentType = 5126 according to gltf spec) 492 | if(output.componentType != 5126) 493 | console.warn("output component type is not a float!"); 494 | 495 | let output_component_count = (output.type == "VEC3" ? 3 : 4); 496 | // 4 byte floats in according to gltf spec 497 | let output_element_size = output_component_count * 4; 498 | 499 | //use the buffer view and accessor to offset into the binary buffer to retrieve our value data 500 | let output_bufferView = gltf_json.bufferViews[output.bufferView]; 501 | let output_accessor_data_offset = output_bufferView.byteOffset + output_accessor_byte_offset; 502 | let output_bin = bin_data_chunk.slice(output_accessor_data_offset, output_accessor_data_offset + output_element_size * output.count); 503 | let output_dv = new DataView(output_bin); 504 | 505 | // parse and store each value 506 | let values = []; 507 | for(var j = 0; j < output.count * output_component_count; j += output_component_count) { 508 | let value = []; 509 | for(var l = 0; l < output_component_count; l++) { 510 | value.push(output_dv.getFloat32(j*4 + l*4, true)); 511 | } 512 | values.push(value); 513 | } 514 | 515 | if(dof_type == "translation") { 516 | for(var j = 0; j < output.count; j++) { 517 | curr_animation.tracks[node_name].translation_keys.push(new AnimationKey(timestamps[j], values[j])); 518 | } 519 | } else if(dof_type == "rotation") { 520 | for(var j = 0; j < output.count; j++) { 521 | curr_animation.tracks[node_name].rotation_keys.push(new AnimationKey(timestamps[j], values[j])); 522 | } 523 | } else if(dof_type == "scale") { 524 | for(var j = 0; j < output.count; j++) { 525 | curr_animation.tracks[node_name].scale_keys.push(new AnimationKey(timestamps[j], values[j])); 526 | } 527 | } 528 | } 529 | animations.push(curr_animation); 530 | } 531 | return animations; 532 | } 533 | 534 | static async parseAnimationSetFromUri(glb_uri) { 535 | let array_buffer = await this._getResourceAsync(glb_uri); 536 | return this._parseAnimationSetFromArrayBuffer(array_buffer); 537 | } 538 | 539 | static async parseAnimationSetFromFile(glb_file) { 540 | let array_buffer = await this._readFileAsync(glb_file); 541 | return this._parseAnimationSetFromArrayBuffer(array_buffer); 542 | } 543 | 544 | static _parseAnimationSetFromArrayBuffer(array_buffer) { 545 | let animation_nodes = AnimationParser.parseAnimationNodesFromArrayBuffer(array_buffer); 546 | // convert nodes to dictionary format 547 | let nodes_dict = {}; 548 | for(var i = 0; i < animation_nodes.length; i++) { 549 | nodes_dict[animation_nodes[i].name] = animation_nodes[i]; 550 | 551 | //if the node defines its TRS info as a matrix, we need to capture that (see glTF 2.0 spec) 552 | if(typeof animation_nodes[i].matrix !== 'undefined') { 553 | let mat = new Cesium.Matrix4(); 554 | Cesium.Matrix4.fromColumnMajorArray(animation_nodes[i].matrix, mat); 555 | nodes_dict[animation_nodes[i].name].matrix = mat; 556 | } 557 | 558 | //set default values for translation rotation and scale if they do not exist 559 | if(typeof nodes_dict[animation_nodes[i].name].translation === 'undefined') 560 | nodes_dict[animation_nodes[i].name].translation = [0,0,0]; 561 | 562 | if(typeof nodes_dict[animation_nodes[i].name].rotation === 'undefined') { 563 | nodes_dict[animation_nodes[i].name].rotation = [0,0,0,1]; 564 | nodes_dict[animation_nodes[i].name].inv_rotation_matrix = Cesium.Matrix3.IDENTITY; 565 | nodes_dict[animation_nodes[i].name].inv_rotation = new Cesium.Quaternion(0,0,0,1); 566 | } 567 | else { 568 | //compute and store the inverse rotation matrix and quaternion for future calculations 569 | let orig_rot = nodes_dict[animation_nodes[i].name].rotation; 570 | let orig_quat = new Cesium.Quaternion(orig_rot[0], orig_rot[1], orig_rot[2], orig_rot[3]); 571 | let orig_quat_inv = new Cesium.Quaternion(); 572 | Cesium.Quaternion.inverse(orig_quat, orig_quat_inv); 573 | let invMat = new Cesium.Matrix3(); 574 | Cesium.Matrix3.fromQuaternion(orig_quat_inv, invMat); 575 | nodes_dict[animation_nodes[i].name].inv_rotation_matrix = invMat; 576 | nodes_dict[animation_nodes[i].name].inv_rotation = orig_quat_inv; 577 | } 578 | 579 | if(typeof nodes_dict[animation_nodes[i].name].scale === 'undefined') 580 | nodes_dict[animation_nodes[i].name].scale = [0,0,0]; 581 | } 582 | 583 | let animations = AnimationParser.parseAnimationsFromArrayBuffer(array_buffer); 584 | console.log(nodes_dict); 585 | return new AnimationSet(animations, nodes_dict); 586 | } 587 | }; 588 | -------------------------------------------------------------------------------- /src/cesium_model_animation_player.ts: -------------------------------------------------------------------------------- 1 | const Cesium = require('cesium/Cesium'); 2 | 3 | export enum LOOP_TYPE {CLAMP = 1, LOOP = 2}; 4 | export enum PLAY_STATE {PLAY = 1, STOP = 2, PAUSE = 3}; 5 | 6 | export class AnimationKey { 7 | time : number; 8 | value : any; 9 | constructor(time : number, value : any) { 10 | this.time = time; 11 | this.value = value; 12 | } 13 | }; 14 | 15 | export class AnimationTrack { 16 | translation_keys : AnimationKey[]; 17 | rotation_keys : AnimationKey[]; 18 | scale_keys : AnimationKey[]; 19 | constructor() { 20 | this.translation_keys = []; 21 | this.rotation_keys = []; 22 | this.scale_keys = []; 23 | } 24 | }; 25 | 26 | export class Animation { 27 | name : string; 28 | duration : number; 29 | tracks : Object; 30 | constructor(name) { 31 | this.name = name; 32 | this.duration = 0; 33 | this.tracks = {}; // a dictionary whose keys are node names 34 | } 35 | }; 36 | 37 | export class AnimationSet { 38 | animations : Animation[]; 39 | nodes : Object; 40 | constructor(animations, nodes) { 41 | this.animations = animations; 42 | this.nodes = nodes; 43 | } 44 | }; 45 | 46 | export class AnimationPlayer { 47 | 48 | loop_type : LOOP_TYPE; 49 | play_state : PLAY_STATE; 50 | animation_set : AnimationSet; 51 | current_animation : Animation; 52 | entity : Cesium.Entity; 53 | interval_id : number; 54 | current_time : number; 55 | speed : number; 56 | 57 | private _frame_duration : number; 58 | 59 | constructor(animation_set, entity, fps) { 60 | this.loop_type = LOOP_TYPE.CLAMP; 61 | this.play_state = PLAY_STATE.STOP; 62 | this.animation_set = animation_set; 63 | this.current_animation = this.animation_set.animations[0]; 64 | this.entity = entity; 65 | this.interval_id = -1; 66 | this.current_time = 0; 67 | this.speed = 1; 68 | this._frame_duration = 1.0/fps; 69 | 70 | // set initial node positions for Cesium entity 71 | let cesium_nodes = {}; 72 | 73 | for(var node_name in this.animation_set.nodes) { 74 | if(typeof this.entity.model.nodeTransformations != "undefined" && 75 | typeof this.entity.model.nodeTransformations[node_name] != "undefined"){ 76 | cesium_nodes[node_name] = this.entity.model.nodeTransformations[node_name]; 77 | } else { 78 | cesium_nodes[node_name] = { 79 | translation: new Cesium.Cartesian3(0, 0, 0), 80 | rotation: new Cesium.Cartesian4(0, 0, 0, 1), 81 | scale: new Cesium.Cartesian3(1, 1, 1) 82 | } 83 | } 84 | } 85 | 86 | this.entity.model.nodeTransformations = cesium_nodes; 87 | 88 | } 89 | 90 | setAnimation(animation_name) { 91 | for(var i = 0; i < this.animation_set.animations.length; i++) { 92 | if(animation_name === this.animation_set.animations[i].name) { 93 | this.current_animation = this.animation_set.animations[i]; 94 | return; 95 | } 96 | } 97 | console.error("Can't set current animation: " + animation_name + " does not exist"); 98 | } 99 | 100 | setFPS(fps) { 101 | this._frame_duration = 1.0/fps; 102 | } 103 | 104 | play(animation_name) { 105 | if(typeof animation_name === 'undefined') { 106 | if(this.play_state === PLAY_STATE.PLAY) { 107 | return; 108 | } else if(this.play_state === PLAY_STATE.PAUSE) { 109 | this.play_state = PLAY_STATE.PLAY; 110 | } else if(this.play_state === PLAY_STATE.STOP) { 111 | this.play_state = PLAY_STATE.PLAY; 112 | this.interval_id = window.setInterval(() => this._update(), this._frame_duration * 1000); 113 | } 114 | return; 115 | } 116 | 117 | let animations = this.animation_set.animations; 118 | for(var i = 0; i < animations.length; i++) { 119 | if(animations[i].name === animation_name) { 120 | this.current_animation = animations[i]; 121 | if(this.play_state === PLAY_STATE.PLAY) { 122 | return; 123 | } else if(this.play_state === PLAY_STATE.PAUSE) { 124 | this.play_state = PLAY_STATE.PLAY; 125 | } else if(this.play_state === PLAY_STATE.STOP) { 126 | this.play_state = PLAY_STATE.PLAY; 127 | this.interval_id = window.setInterval(() => this._update(), this._frame_duration * 1000); 128 | } 129 | return; 130 | } 131 | } 132 | console.error("Can't play animation: " + animation_name + " does not exist"); 133 | } 134 | 135 | _clearUpdateInterva() { 136 | clearInterval(this.interval_id); 137 | this.interval_id = -1; 138 | } 139 | 140 | _update() { 141 | if(this.play_state === PLAY_STATE.PLAY) 142 | this.setTime(this.current_time + this._frame_duration * this.speed); 143 | } 144 | 145 | setPercent(percent) { 146 | if(percent < 0.0) { 147 | percent = 0.0; 148 | } 149 | else if(percent > 1.0) { 150 | percent = 1.0; 151 | } 152 | let time = this.current_animation.duration * percent; 153 | this.setTime(time); 154 | } 155 | 156 | setTime(current_time) { 157 | this.current_time = current_time; 158 | if(this.speed > 0) { 159 | if(this.current_time > this.current_animation.duration) { 160 | if(this.loop_type === LOOP_TYPE.CLAMP) { 161 | this.current_time = this.current_animation.duration; 162 | } else if(this.loop_type === LOOP_TYPE.LOOP) { 163 | this.current_time = 0; 164 | } 165 | } 166 | } else if(this.speed < 0) { 167 | if(this.current_time < 0) { 168 | if(this.loop_type === LOOP_TYPE.CLAMP) { 169 | this.current_time = 0; 170 | } else if(this.loop_type === LOOP_TYPE.LOOP) { 171 | this.current_time = this.current_animation.duration; 172 | } 173 | } 174 | } 175 | 176 | 177 | for(var track_name in this.current_animation.tracks) { 178 | let track = this.current_animation.tracks[track_name]; 179 | let node = this.animation_set.nodes[track_name]; 180 | let curr_trans_keys = this.getKeysAtTime(track.translation_keys, this.current_time); 181 | let curr_rot_keys = this.getKeysAtTime(track.rotation_keys, this.current_time); 182 | let curr_scale_keys = this.getKeysAtTime(track.scale_keys, this.current_time); 183 | 184 | //-------------------------- 185 | // Translation 186 | //-------------------------- 187 | if(typeof curr_trans_keys !== 'undefined' && curr_trans_keys.length > 0) { 188 | let orig_trans = node.translation; 189 | let invMat = node.inv_rotation_matrix; 190 | 191 | if(curr_trans_keys[0].time == curr_trans_keys[1].time) { 192 | let result = new Cesium.Cartesian3(curr_trans_keys[0].value[0] - orig_trans[0], curr_trans_keys[0].value[1] - orig_trans[1], curr_trans_keys[0].value[2] - orig_trans[2]); 193 | //get the result expressed in local node space 194 | Cesium.Matrix3.multiplyByVector(invMat, result, result); 195 | this.entity.model.nodeTransformations[track_name].translation = result; 196 | } else { 197 | let keyDelta = curr_trans_keys[1].time - curr_trans_keys[0].time; 198 | let timeDelta = this.current_time - curr_trans_keys[0].time; 199 | let t = timeDelta/keyDelta; 200 | let start = new Cesium.Cartesian3(curr_trans_keys[0].value[0], curr_trans_keys[0].value[1], curr_trans_keys[0].value[2]); 201 | let end = new Cesium.Cartesian3(curr_trans_keys[1].value[0], curr_trans_keys[1].value[1], curr_trans_keys[1].value[2]); 202 | 203 | //interpolate the translation keys 204 | let result = new Cesium.Cartesian3(); 205 | Cesium.Cartesian3.lerp(start, end, t, result); 206 | 207 | //account for delta / relative offset from original translation 208 | result.x -= orig_trans[0]; 209 | result.y -= orig_trans[1]; 210 | result.z -= orig_trans[2]; 211 | 212 | //get the result expressed in local node space 213 | Cesium.Matrix3.multiplyByVector(invMat, result, result); 214 | 215 | this.entity.model.nodeTransformations[track_name].translation = result; 216 | } 217 | } 218 | 219 | //-------------------------- 220 | // Rotation 221 | //-------------------------- 222 | if(typeof curr_rot_keys !== 'undefined' && curr_rot_keys.length > 0) { 223 | 224 | let orig_inv = node.inv_rotation; 225 | let invMat = node.inv_rotation_matrix; 226 | 227 | if(curr_rot_keys[0].time == curr_rot_keys[1].time) { 228 | let result = new Cesium.Quaternion(curr_rot_keys[0].value[0], curr_rot_keys[0].value[1], curr_rot_keys[0].value[2], curr_rot_keys[0].value[3]); 229 | 230 | //isolate the axis 231 | let resultAxis = new Cesium.Cartesian3(1,0,0); 232 | let resultAngle = Cesium.Quaternion.computeAngle(result); 233 | if(Math.abs(resultAngle) > Cesium.Math.EPSILON5) 234 | Cesium.Quaternion.computeAxis(result, resultAxis); 235 | 236 | //transform to local node space 237 | Cesium.Matrix3.multiplyByVector(invMat, resultAxis, resultAxis); 238 | 239 | //get the new quaternion expressed in local node space 240 | Cesium.Quaternion.fromAxisAngle(resultAxis, resultAngle, result); 241 | //calc the rotation delta/difference 242 | Cesium.Quaternion.multiply(result, orig_inv, result); 243 | this.entity.model.nodeTransformations[track_name].rotation = result; 244 | } else { 245 | let keyDelta = curr_rot_keys[1].time - curr_rot_keys[0].time; 246 | let timeDelta = this.current_time - curr_rot_keys[0].time; 247 | let t = timeDelta/keyDelta; 248 | let start = new Cesium.Quaternion(curr_rot_keys[0].value[0], curr_rot_keys[0].value[1], curr_rot_keys[0].value[2], curr_rot_keys[0].value[3]); 249 | let end = new Cesium.Quaternion(curr_rot_keys[1].value[0], curr_rot_keys[1].value[1], curr_rot_keys[1].value[2], curr_rot_keys[1].value[3]); 250 | 251 | //slerp the rotation keys 252 | let result = new Cesium.Quaternion(); 253 | Cesium.Quaternion.slerp(start, end, t, result); 254 | 255 | //isolate the axis 256 | let resultAxis = new Cesium.Cartesian3(1,0,0); 257 | let resultAngle = Cesium.Quaternion.computeAngle(result); 258 | if(Math.abs(resultAngle) > Cesium.Math.EPSILON5) 259 | Cesium.Quaternion.computeAxis(result, resultAxis); 260 | 261 | //transform to local node space 262 | Cesium.Matrix3.multiplyByVector(invMat, resultAxis, resultAxis); 263 | 264 | //get the new quaternion expressed in local node space 265 | Cesium.Quaternion.fromAxisAngle(resultAxis, resultAngle, result); 266 | 267 | //calc the rotation delta/difference 268 | Cesium.Quaternion.multiply(result, orig_inv, result); 269 | 270 | this.entity.model.nodeTransformations[track_name].rotation = result; 271 | } 272 | } 273 | 274 | //-------------------------- 275 | // Scale 276 | //-------------------------- 277 | if(typeof curr_scale_keys !== 'undefined' && curr_scale_keys.length > 0) { 278 | let orig_scale = this.animation_set.nodes[track_name].scale; 279 | 280 | if(curr_scale_keys[0].time == curr_scale_keys[1].time) { 281 | let result = new Cesium.Cartesian3(curr_scale_keys[0].value[0] / orig_scale[0], curr_scale_keys[0].value[1] / orig_scale[1], curr_scale_keys[0].value[2] / orig_scale[2]); 282 | this.entity.model.nodeTransformations[track_name].scale = result; 283 | } else { 284 | let keyDelta = curr_scale_keys[1].time - curr_scale_keys[0].time; 285 | let timeDelta = this.current_time - curr_scale_keys[0].time; 286 | let t = timeDelta/keyDelta; 287 | let start = new Cesium.Cartesian3(curr_scale_keys[0].value[0], curr_scale_keys[0].value[1], curr_scale_keys[0].value[2]); 288 | let end = new Cesium.Cartesian3(curr_scale_keys[1].value[0], curr_scale_keys[1].value[1], curr_scale_keys[1].value[2]); 289 | let result = new Cesium.Cartesian3(); 290 | Cesium.Cartesian3.lerp(start, end, t, result); 291 | 292 | //account for delta / relative offset from original scale 293 | result.x /= orig_scale[0]; 294 | result.y /= orig_scale[1]; 295 | result.z /= orig_scale[2]; 296 | this.entity.model.nodeTransformations[track_name].scale = result; 297 | } 298 | } 299 | } 300 | } 301 | 302 | getKeysAtTime(keys, time) { 303 | let result = []; 304 | if(keys.length == 0) 305 | return result; 306 | 307 | //we need to return some value even if the first key for this track isn't reached quite yet 308 | if(keys[0].time > time) { 309 | result.push(keys[0]); 310 | result.push(keys[0]); 311 | return result; 312 | } 313 | 314 | //just clamp the last key if we are at the end 315 | if(time > keys[keys.length-1].time) { 316 | result.push(keys[keys.length-1]); 317 | result.push(keys[keys.length-1]); 318 | return result; 319 | } 320 | 321 | for(var i = 0; i < keys.length-1; i++) { 322 | if(keys[i].time <= time && keys[i+1].time >= time) { 323 | result.push(keys[i]); 324 | result.push(keys[i+1]); 325 | return result; 326 | } 327 | } 328 | } 329 | 330 | stop() { 331 | this.play_state = PLAY_STATE.STOP; 332 | this.current_time = 0; 333 | //reset the node transforms on the entity to the default pose 334 | let cesium_nodes = {}; 335 | for(var node_name in this.animation_set.nodes) { 336 | cesium_nodes[node_name] = { 337 | translation: new Cesium.Cartesian3(0, 0, 0), 338 | rotation: new Cesium.Cartesian4(0, 0, 0, 1), 339 | scale: new Cesium.Cartesian3(1, 1, 1) 340 | } 341 | } 342 | this.entity.model.nodeTransformations = cesium_nodes; 343 | this._clearUpdateInterva(); 344 | } 345 | 346 | pause() { 347 | //no need to pause if we are not playing 348 | if(this.play_state === PLAY_STATE.PLAY) 349 | this.play_state = PLAY_STATE.PAUSE; 350 | this._clearUpdateInterva(); 351 | } 352 | } 353 | 354 | export class AnimationParser { 355 | static _readFileAsync(file) { 356 | return new Promise((resolve, reject) => { 357 | let reader = new FileReader(); 358 | 359 | reader.onload = () => { 360 | resolve(reader.result); 361 | }; 362 | 363 | reader.onerror = reject; 364 | 365 | reader.readAsArrayBuffer(file); 366 | }); 367 | } 368 | 369 | static _getResourceAsync(uri) { 370 | return new Promise((resolve, reject) => { 371 | var req = new Request(uri); 372 | 373 | fetch(req).then(function(response) { 374 | if (!response.ok) { 375 | reject(new Error(response.statusText)); 376 | } 377 | return response; 378 | }).then(function(response) { 379 | resolve(response.arrayBuffer()); 380 | }); 381 | }); 382 | } 383 | 384 | static parseAnimationNodesFromArrayBuffer(array_buffer) { 385 | // get the length of the JSON data starting at 12 byte offset according to gltf standard 386 | let dv = new DataView(array_buffer, 12, 4); 387 | // don't forget to set little-endian = true when parsing from data view (gltf standard!) 388 | let json_chunk_length = dv.getUint32(0, true); 389 | console.log("gltf JSON length: " + json_chunk_length + " bytes"); 390 | 391 | // get the actual JSON data itself 392 | let json_data_chunk = array_buffer.slice(20, 20+json_chunk_length); 393 | let decoder = new TextDecoder('UTF-8'); 394 | let json_text = decoder.decode(json_data_chunk); 395 | let gltf_json = JSON.parse(json_text); 396 | console.log("gltf JSON loaded successfully:"); 397 | 398 | // store links to parent nodes 399 | for(var i = 0; i < gltf_json.nodes.length; i++) { 400 | if(typeof gltf_json.nodes[i].children != 'undefined') { 401 | for(var k = 0; k < gltf_json.nodes[i].children.length; k++) { 402 | gltf_json.nodes[gltf_json.nodes[i].children[k]].parent = gltf_json.nodes[i].name; 403 | } 404 | } 405 | } 406 | 407 | return gltf_json.nodes; 408 | } 409 | 410 | static parseAnimationsFromArrayBuffer(array_buffer) { 411 | let animations = []; 412 | 413 | // get the length of the JSON data starting at 12 byte offset according to gltf standard 414 | let dv = new DataView(array_buffer, 12, 4); 415 | // don't forget to set little-endian = true when parsing from data view (gltf tandard!) 416 | let json_chunk_length = dv.getUint32(0, true); 417 | console.log("gltf JSON length: " + json_chunk_length + " bytes"); 418 | 419 | // get the actual JSON data itself 420 | let json_data_chunk = array_buffer.slice(20, 20+json_chunk_length); 421 | let decoder = new TextDecoder('UTF-8'); 422 | let json_text = decoder.decode(json_data_chunk); 423 | let gltf_json = JSON.parse(json_text); 424 | console.log("gltf JSON loaded successfully:"); 425 | console.log(gltf_json); 426 | 427 | // get the length of the gltf embedded binary data 428 | let bin_offset = 20+json_chunk_length; 429 | dv = new DataView(array_buffer, bin_offset, 4); 430 | let bin_chunk_length = dv.getUint32(0, true); 431 | console.log("gltf bin length: " + bin_chunk_length + " bytes"); 432 | 433 | // get the actual binary data, we add 8 to get past the "type" and "chunk length" headers 434 | let bin_data_chunk = array_buffer.slice(bin_offset + 8, bin_offset + 8 + bin_chunk_length); 435 | 436 | //-------------------------------------------------- 437 | // get and process all animations 438 | //-------------------------------------------------- 439 | if(typeof gltf_json.animations !== 'undefined') { 440 | for(var i = 0; i < gltf_json.animations.length; i++) { 441 | let anim_name = gltf_json.animations[i].name; 442 | if(typeof anim_name == 'undefined' || anim_name == "") 443 | anim_name = "animation_" + i; 444 | let curr_animation = new Animation(anim_name); 445 | console.log("processing animation: " + anim_name); 446 | 447 | for(var k = 0; k < gltf_json.animations[i].channels.length; k++) { 448 | let channel = gltf_json.animations[i].channels[k]; 449 | 450 | // the following will be either "translation, rotation, or scale" 451 | let dof_type = channel.target.path; 452 | 453 | let node = gltf_json.nodes[channel.target.node]; 454 | if(typeof node == 'undefined') { 455 | console.warn("node is undefined for channel " + k); 456 | continue; 457 | } 458 | 459 | let node_name = node.name; 460 | if(typeof node_name == 'undefined' || node.name == "") { 461 | node_name = "node_" + channel.target.node; 462 | } 463 | 464 | // add a new track to this animation for the node if it does not exist already 465 | if(typeof curr_animation.tracks[node_name] == 'undefined') 466 | curr_animation.tracks[node_name] = new AnimationTrack(); 467 | 468 | let sampler = gltf_json.animations[i].samplers[channel.sampler]; 469 | 470 | //-------------------------------------------------- 471 | // get input accessor (keyframe times for this channel's sampler) and process the data for it 472 | //-------------------------------------------------- 473 | let input = gltf_json.accessors[sampler.input]; 474 | //console.log("min: " + input.min + " max: " + input.max); 475 | 476 | let input_accessor_byte_offset = (typeof input.byteOffset == 'undefined' ? 0 : input.byteOffset); 477 | if(input.componentType != 5126) 478 | console.warn("input component type is not a float!"); 479 | 480 | // each element (keyframe timestamp) is a 4 byte float 481 | let input_element_size = 4; 482 | 483 | //use the buffer view and accessor to offset into the binary buffer to retrieve our data 484 | let input_bufferView = gltf_json.bufferViews[input.bufferView]; 485 | let input_accessor_data_offset = input_bufferView.byteOffset + input_accessor_byte_offset; 486 | let input_bin = bin_data_chunk.slice(input_accessor_data_offset, input_accessor_data_offset + input_element_size * input.count); 487 | let input_dv = new DataView(input_bin); 488 | 489 | // parse and store each timestamp out of the buffer 490 | let timestamps = []; 491 | for(var j = 0; j < input.count; j++) { 492 | let timestamp = input_dv.getFloat32(j*4, true); 493 | if(timestamp > curr_animation.duration) { 494 | curr_animation.duration = timestamp; 495 | } 496 | timestamps.push(timestamp); 497 | } 498 | 499 | //-------------------------------------------------- 500 | // get output accessor (keyframe values for this channel's sampler) and process the data for it 501 | //-------------------------------------------------- 502 | let output = gltf_json.accessors[sampler.output]; 503 | 504 | let output_accessor_byte_offset = (typeof output.byteOffset == 'undefined' ? 0 : output.byteOffset); 505 | 506 | // we only care about VEC3 and VEC4 since we are only dealing with rotation, scale, and translation, 507 | // and we are going to assume they are floating point (componentType = 5126 according to gltf spec) 508 | if(output.componentType != 5126) 509 | console.warn("output component type is not a float!"); 510 | 511 | let output_component_count = (output.type == "VEC3" ? 3 : 4); 512 | // 4 byte floats in according to gltf spec 513 | let output_element_size = output_component_count * 4; 514 | 515 | //use the buffer view and accessor to offset into the binary buffer to retrieve our value data 516 | let output_bufferView = gltf_json.bufferViews[output.bufferView]; 517 | let output_accessor_data_offset = output_bufferView.byteOffset + output_accessor_byte_offset; 518 | let output_bin = bin_data_chunk.slice(output_accessor_data_offset, output_accessor_data_offset + output_element_size * output.count); 519 | let output_dv = new DataView(output_bin); 520 | 521 | // parse and store each value 522 | let values = []; 523 | for(var j = 0; j < output.count * output_component_count; j += output_component_count) { 524 | let value = []; 525 | for(var l = 0; l < output_component_count; l++) { 526 | value.push(output_dv.getFloat32(j*4 + l*4, true)); 527 | } 528 | values.push(value); 529 | } 530 | 531 | if(dof_type == "translation") { 532 | for(var j = 0; j < output.count; j++) { 533 | curr_animation.tracks[node_name].translation_keys.push(new AnimationKey(timestamps[j], values[j])); 534 | } 535 | } else if(dof_type == "rotation") { 536 | for(var j = 0; j < output.count; j++) { 537 | curr_animation.tracks[node_name].rotation_keys.push(new AnimationKey(timestamps[j], values[j])); 538 | } 539 | } else if(dof_type == "scale") { 540 | for(var j = 0; j < output.count; j++) { 541 | curr_animation.tracks[node_name].scale_keys.push(new AnimationKey(timestamps[j], values[j])); 542 | } 543 | } 544 | } 545 | animations.push(curr_animation); 546 | } 547 | } 548 | return animations; 549 | } 550 | 551 | static async parseAnimationSetFromUri(glb_uri) { 552 | let array_buffer = await this._getResourceAsync(glb_uri); 553 | return this._parseAnimationSetFromArrayBuffer(array_buffer); 554 | } 555 | 556 | static async parseAnimationSetFromFile(glb_file) { 557 | let array_buffer = await this._readFileAsync(glb_file); 558 | return this._parseAnimationSetFromArrayBuffer(array_buffer); 559 | } 560 | 561 | static _parseAnimationSetFromArrayBuffer(array_buffer) { 562 | let animation_nodes = AnimationParser.parseAnimationNodesFromArrayBuffer(array_buffer); 563 | // convert nodes to dictionary format 564 | let nodes_dict = {}; 565 | for(var i = 0; i < animation_nodes.length; i++) { 566 | nodes_dict[animation_nodes[i].name] = animation_nodes[i]; 567 | 568 | //if the node defines its TRS info as a matrix, we need to capture that (see glTF 2.0 spec) 569 | if(typeof animation_nodes[i].matrix !== 'undefined') { 570 | let mat = new Cesium.Matrix4(); 571 | Cesium.Matrix4.fromColumnMajorArray(animation_nodes[i].matrix, mat); 572 | nodes_dict[animation_nodes[i].name].matrix = mat; 573 | } 574 | 575 | //set default values for translation rotation and scale if they do not exist 576 | if(typeof nodes_dict[animation_nodes[i].name].translation === 'undefined') 577 | nodes_dict[animation_nodes[i].name].translation = [0,0,0]; 578 | 579 | if(typeof nodes_dict[animation_nodes[i].name].rotation === 'undefined') { 580 | nodes_dict[animation_nodes[i].name].rotation = [0,0,0,1]; 581 | nodes_dict[animation_nodes[i].name].inv_rotation_matrix = Cesium.Matrix3.IDENTITY; 582 | nodes_dict[animation_nodes[i].name].inv_rotation = new Cesium.Quaternion(0,0,0,1); 583 | } 584 | else { 585 | //compute and store the inverse rotation matrix and quaternion for future calculations 586 | let orig_rot = nodes_dict[animation_nodes[i].name].rotation; 587 | let orig_quat = new Cesium.Quaternion(orig_rot[0], orig_rot[1], orig_rot[2], orig_rot[3]); 588 | let orig_quat_inv = new Cesium.Quaternion(); 589 | Cesium.Quaternion.inverse(orig_quat, orig_quat_inv); 590 | let invMat = new Cesium.Matrix3(); 591 | Cesium.Matrix3.fromQuaternion(orig_quat_inv, invMat); 592 | nodes_dict[animation_nodes[i].name].inv_rotation_matrix = invMat; 593 | nodes_dict[animation_nodes[i].name].inv_rotation = orig_quat_inv; 594 | } 595 | 596 | if(typeof nodes_dict[animation_nodes[i].name].scale === 'undefined') 597 | nodes_dict[animation_nodes[i].name].scale = [0,0,0]; 598 | } 599 | 600 | let animations = AnimationParser.parseAnimationsFromArrayBuffer(array_buffer); 601 | console.log(nodes_dict); 602 | return new AnimationSet(animations, nodes_dict); 603 | } 604 | }; 605 | --------------------------------------------------------------------------------