cesium-examples/map/libs/cesium_model_animation_play...

588 lines
23 KiB
JavaScript

const Cesium = require('cesium/Cesium');
export const LOOP_TYPE = Object.freeze({"CLAMP":1, "LOOP":2});
export const PLAY_STATE = Object.freeze({"PLAY":1, "STOP":2, "PAUSE":3});
export class AnimationKey {
constructor(time, value) {
this.time = time;
this.value = value;
}
};
export class AnimationTrack {
constructor() {
this.translation_keys = [];
this.rotation_keys = [];
this.scale_keys = [];
}
};
export class Animation {
constructor(name) {
this.name = name;
this.duration = 0;
this.tracks = {}; // a dictionary whose keys are node names
}
};
export class AnimationSet {
constructor(animations, nodes) {
this.animations = animations;
this.nodes = nodes;
}
};
export class AnimationPlayer {
constructor(animation_set, entity, fps) {
this.loop_type = LOOP_TYPE.CLAMP;
this.play_state = PLAY_STATE.STOP;
this.animation_set = animation_set;
this.entity = entity;
if(this.animation_set.animations.length > 0) {
this.current_animation = this.animation_set.animations[0];
}
else {
this.current_animation = "";
}
// set initial node positions for Cesium entity
let cesium_nodes = {};
for(var node_name in this.animation_set.nodes) {
if(typeof this.entity.model.nodeTransformations != "undefined" &&
typeof this.entity.model.nodeTransformations[node_name] != "undefined"){
cesium_nodes[node_name] = this.entity.model.nodeTransformations[node_name];
} else {
cesium_nodes[node_name] = {
translation: new Cesium.Cartesian3(0, 0, 0),
rotation: new Cesium.Cartesian4(0, 0, 0, 1),
scale: new Cesium.Cartesian3(1, 1, 1)
}
}
}
this.entity.model.nodeTransformations = cesium_nodes;
this.interval_id = -1;
this.current_time = 0;
this.speed = 1;
this._frame_duration = 1.0/fps;
}
setAnimation(animation_name) {
for(var i = 0; i < this.animation_set.animations.length; i++) {
if(animation_name === this.animation_set.animations[i].name) {
this.current_animation = this.animation_set.animations[i];
return;
}
}
console.error("Can't set current animation: " + animation_name + " does not exist");
}
setFPS(fps) {
this._frame_duration = 1.0/fps;
}
play(animation_name) {
if(typeof animation_name === 'undefined') {
if(this.play_state === PLAY_STATE.PLAY) {
return;
} else if(this.play_state === PLAY_STATE.PAUSE) {
this.play_state = PLAY_STATE.PLAY;
} else if(this.play_state === PLAY_STATE.STOP) {
this.play_state = PLAY_STATE.PLAY;
this.interval_id = window.setInterval(() => this._update(), this._frame_duration * 1000);
}
return;
}
let animations = this.animation_set.animations;
for(var i = 0; i < animations.length; i++) {
if(animations[i].name === animation_name) {
this.current_animation = animations[i];
if(this.play_state === PLAY_STATE.PLAY) {
return;
} else if(this.play_state === PLAY_STATE.PAUSE) {
this.play_state = PLAY_STATE.PLAY;
} else if(this.play_state === PLAY_STATE.STOP) {
this.play_state = PLAY_STATE.PLAY;
this.interval_id = window.setInterval(() => this._update(), this._frame_duration * 1000);
}
return;
}
}
console.error("Can't play animation: " + animation_name + " does not exist");
}
_clearUpdateInterval() {
clearInterval(this.interval_id);
this.interval_id = -1;
}
_update() {
if(this.play_state === PLAY_STATE.PLAY)
this.setTime(this.current_time + this._frame_duration * this.speed);
}
setPercent(percent) {
if(percent < 0.0) {
percent = 0.0;
}
else if(percent > 1.0) {
percent = 1.0;
}
let time = this.current_animation.duration * percent;
this.setTime(time);
}
setTime(current_time) {
this.current_time = current_time;
if(this.speed > 0) {
if(this.current_time > this.current_animation.duration) {
if(this.loop_type === LOOP_TYPE.CLAMP) {
this.current_time = this.current_animation.duration;
} else if(this.loop_type === LOOP_TYPE.LOOP) {
this.current_time = 0;
}
}
} else if(this.speed < 0) {
if(this.current_time < 0) {
if(this.loop_type === LOOP_TYPE.CLAMP) {
this.current_time = 0;
} else if(this.loop_type === LOOP_TYPE.LOOP) {
this.current_time = this.current_animation.duration;
}
}
}
for(var track_name in this.current_animation.tracks) {
let track = this.current_animation.tracks[track_name];
let node = this.animation_set.nodes[track_name];
let curr_trans_keys = this.getKeysAtTime(track.translation_keys, this.current_time);
let curr_rot_keys = this.getKeysAtTime(track.rotation_keys, this.current_time);
let curr_scale_keys = this.getKeysAtTime(track.scale_keys, this.current_time);
//--------------------------
// Translation
//--------------------------
if(typeof curr_trans_keys !== 'undefined' && curr_trans_keys.length > 0) {
let orig_trans = node.translation;
let invMat = node.inv_rotation_matrix;
if(curr_trans_keys[0].time == curr_trans_keys[1].time) {
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]);
//get the result expressed in local node space
Cesium.Matrix3.multiplyByVector(invMat, result, result);
this.entity.model.nodeTransformations[track_name].translation = result;
} else {
let keyDelta = curr_trans_keys[1].time - curr_trans_keys[0].time;
let timeDelta = this.current_time - curr_trans_keys[0].time;
let t = timeDelta/keyDelta;
let start = new Cesium.Cartesian3(curr_trans_keys[0].value[0], curr_trans_keys[0].value[1], curr_trans_keys[0].value[2]);
let end = new Cesium.Cartesian3(curr_trans_keys[1].value[0], curr_trans_keys[1].value[1], curr_trans_keys[1].value[2]);
//interpolate the translation keys
let result = new Cesium.Cartesian3();
Cesium.Cartesian3.lerp(start, end, t, result);
//account for delta / relative offset from original translation
result.x -= orig_trans[0];
result.y -= orig_trans[1];
result.z -= orig_trans[2];
//get the result expressed in local node space
Cesium.Matrix3.multiplyByVector(invMat, result, result);
this.entity.model.nodeTransformations[track_name].translation = result;
}
}
//--------------------------
// Rotation
//--------------------------
if(typeof curr_rot_keys !== 'undefined' && curr_rot_keys.length > 0) {
let orig_inv = node.inv_rotation;
let invMat = node.inv_rotation_matrix;
if(curr_rot_keys[0].time == curr_rot_keys[1].time) {
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]);
//isolate the axis
let resultAxis = new Cesium.Cartesian3(1,0,0);
let resultAngle = Cesium.Quaternion.computeAngle(result);
if(Math.abs(resultAngle) > Cesium.Math.EPSILON5)
Cesium.Quaternion.computeAxis(result, resultAxis);
//transform to local node space
Cesium.Matrix3.multiplyByVector(invMat, resultAxis, resultAxis);
//get the new quaternion expressed in local node space
Cesium.Quaternion.fromAxisAngle(resultAxis, resultAngle, result);
//calc the rotation delta/difference
Cesium.Quaternion.multiply(result, orig_inv, result);
this.entity.model.nodeTransformations[track_name].rotation = result;
} else {
let keyDelta = curr_rot_keys[1].time - curr_rot_keys[0].time;
let timeDelta = this.current_time - curr_rot_keys[0].time;
let t = timeDelta/keyDelta;
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]);
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]);
//slerp the rotation keys
let result = new Cesium.Quaternion();
Cesium.Quaternion.slerp(start, end, t, result);
//isolate the axis
let resultAxis = new Cesium.Cartesian3(1,0,0);
let resultAngle = Cesium.Quaternion.computeAngle(result);
if(Math.abs(resultAngle) > Cesium.Math.EPSILON5)
Cesium.Quaternion.computeAxis(result, resultAxis);
//transform to local node space
Cesium.Matrix3.multiplyByVector(invMat, resultAxis, resultAxis);
//get the new quaternion expressed in local node space
Cesium.Quaternion.fromAxisAngle(resultAxis, resultAngle, result);
//calc the rotation delta/difference
Cesium.Quaternion.multiply(result, orig_inv, result);
this.entity.model.nodeTransformations[track_name].rotation = result;
}
}
//--------------------------
// Scale
//--------------------------
if(typeof curr_scale_keys !== 'undefined' && curr_scale_keys.length > 0) {
let orig_scale = this.animation_set.nodes[track_name].scale;
if(curr_scale_keys[0].time == curr_scale_keys[1].time) {
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]);
this.entity.model.nodeTransformations[track_name].scale = result;
} else {
let keyDelta = curr_scale_keys[1].time - curr_scale_keys[0].time;
let timeDelta = this.current_time - curr_scale_keys[0].time;
let t = timeDelta/keyDelta;
let start = new Cesium.Cartesian3(curr_scale_keys[0].value[0], curr_scale_keys[0].value[1], curr_scale_keys[0].value[2]);
let end = new Cesium.Cartesian3(curr_scale_keys[1].value[0], curr_scale_keys[1].value[1], curr_scale_keys[1].value[2]);
let result = new Cesium.Cartesian3();
Cesium.Cartesian3.lerp(start, end, t, result);
//account for delta / relative offset from original scale
result.x /= orig_scale[0];
result.y /= orig_scale[1];
result.z /= orig_scale[2];
this.entity.model.nodeTransformations[track_name].scale = result;
}
}
}
}
getKeysAtTime(keys, time) {
let result = [];
if(keys.length == 0)
return result;
//we need to return some value even if the first key for this track isn't reached quite yet
if(keys[0].time > time) {
result.push(keys[0]);
result.push(keys[0]);
return result;
}
//just clamp the last key if we are at the end
if(time > keys[keys.length-1].time) {
result.push(keys[keys.length-1]);
result.push(keys[keys.length-1]);
return result;
}
for(var i = 0; i < keys.length-1; i++) {
if(keys[i].time <= time && keys[i+1].time >= time) {
result.push(keys[i]);
result.push(keys[i+1]);
return result;
}
}
}
stop() {
this.play_state = PLAY_STATE.STOP;
this.current_time = 0;
//reset the node transforms on the entity to the default pose
let cesium_nodes = {};
for(var node_name in this.animation_set.nodes) {
cesium_nodes[node_name] = {
translation: new Cesium.Cartesian3(0, 0, 0),
rotation: new Cesium.Cartesian4(0, 0, 0, 1),
scale: new Cesium.Cartesian3(1, 1, 1)
}
}
this.entity.model.nodeTransformations = cesium_nodes;
this._clearUpdateInterval();
}
pause() {
//no need to pause if we are not playing
if(this.play_state === PLAY_STATE.PLAY)
this.play_state = PLAY_STATE.PAUSE;
this._clearUpdateInterval();
}
}
export class AnimationParser {
static _readFileAsync(file) {
return new Promise((resolve, reject) => {
let reader = new FileReader();
reader.onload = () => {
resolve(reader.result);
};
reader.onerror = reject;
reader.readAsArrayBuffer(file);
});
}
static _getResourceAsync(uri) {
return new Promise((resolve, reject) => {
var req = new Request(uri);
fetch(req).then(function(response) {
if (!response.ok) {
reject(new Error(response.statusText));
}
return response;
}).then(function(response) {
resolve(response.arrayBuffer());
});
});
}
static parseAnimationNodesFromArrayBuffer(array_buffer) {
// get the length of the JSON data starting at 12 byte offset according to gltf standard
let dv = new DataView(array_buffer, 12, 4);
// don't forget to set little-endian = true when parsing from data view (gltf standard!)
let json_chunk_length = dv.getUint32(0, true);
console.log("gltf JSON length: " + json_chunk_length + " bytes");
// get the actual JSON data itself
let json_data_chunk = array_buffer.slice(20, 20+json_chunk_length);
let decoder = new TextDecoder('UTF-8');
let json_text = decoder.decode(json_data_chunk);
let gltf_json = JSON.parse(json_text);
console.log("gltf JSON loaded successfully:");
// store links to parent nodes
for(var i = 0; i < gltf_json.nodes.length; i++) {
if(typeof gltf_json.nodes[i].children != 'undefined') {
for(var k = 0; k < gltf_json.nodes[i].children.length; k++) {
gltf_json.nodes[gltf_json.nodes[i].children[k]].parent = gltf_json.nodes[i].name;
}
}
}
return gltf_json.nodes;
}
static parseAnimationsFromArrayBuffer(array_buffer) {
let animations = [];
// get the length of the JSON data starting at 12 byte offset according to gltf standard
let dv = new DataView(array_buffer, 12, 4);
// don't forget to set little-endian = true when parsing from data view (gltf tandard!)
let json_chunk_length = dv.getUint32(0, true);
console.log("gltf JSON length: " + json_chunk_length + " bytes");
// get the actual JSON data itself
let json_data_chunk = array_buffer.slice(20, 20+json_chunk_length);
let decoder = new TextDecoder('UTF-8');
let json_text = decoder.decode(json_data_chunk);
let gltf_json = JSON.parse(json_text);
console.log("gltf JSON loaded successfully:");
console.log(gltf_json);
// get the length of the gltf embedded binary data
let bin_offset = 20+json_chunk_length;
dv = new DataView(array_buffer, bin_offset, 4);
let bin_chunk_length = dv.getUint32(0, true);
console.log("gltf bin length: " + bin_chunk_length + " bytes");
// get the actual binary data, we add 8 to get past the "type" and "chunk length" headers
let bin_data_chunk = array_buffer.slice(bin_offset + 8, bin_offset + 8 + bin_chunk_length);
//--------------------------------------------------
// get and process all animations
//--------------------------------------------------
if(typeof gltf_json.animations === 'undefined')
return [];
for(var i = 0; i < gltf_json.animations.length; i++) {
let anim_name = gltf_json.animations[i].name;
if(typeof anim_name == 'undefined' || anim_name == "")
anim_name = "animation_" + i;
let curr_animation = new Animation(anim_name);
console.log("processing animation: " + anim_name);
for(var k = 0; k < gltf_json.animations[i].channels.length; k++) {
let channel = gltf_json.animations[i].channels[k];
// the following will be either "translation, rotation, or scale"
let dof_type = channel.target.path;
let node = gltf_json.nodes[channel.target.node];
if(typeof node == 'undefined') {
console.warn("node is undefined for channel " + k);
continue;
}
let node_name = node.name;
if(typeof node_name == 'undefined' || node.name == "") {
node_name = "node_" + channel.target.node;
}
// add a new track to this animation for the node if it does not exist already
if(typeof curr_animation.tracks[node_name] == 'undefined')
curr_animation.tracks[node_name] = new AnimationTrack();
let sampler = gltf_json.animations[i].samplers[channel.sampler];
//--------------------------------------------------
// get input accessor (keyframe times for this channel's sampler) and process the data for it
//--------------------------------------------------
let input = gltf_json.accessors[sampler.input];
//console.log("min: " + input.min + " max: " + input.max);
let input_accessor_byte_offset = (typeof input.byteOffset == 'undefined' ? 0 : input.byteOffset);
if(input.componentType != 5126)
console.warn("input component type is not a float!");
// each element (keyframe timestamp) is a 4 byte float
let input_element_size = 4;
//use the buffer view and accessor to offset into the binary buffer to retrieve our data
let input_bufferView = gltf_json.bufferViews[input.bufferView];
let input_accessor_data_offset = input_bufferView.byteOffset + input_accessor_byte_offset;
let input_bin = bin_data_chunk.slice(input_accessor_data_offset, input_accessor_data_offset + input_element_size * input.count);
let input_dv = new DataView(input_bin);
// parse and store each timestamp out of the buffer
let timestamps = [];
for(var j = 0; j < input.count; j++) {
let timestamp = input_dv.getFloat32(j*4, true);
if(timestamp > curr_animation.duration) {
curr_animation.duration = timestamp;
}
timestamps.push(timestamp);
}
//--------------------------------------------------
// get output accessor (keyframe values for this channel's sampler) and process the data for it
//--------------------------------------------------
let output = gltf_json.accessors[sampler.output];
let output_accessor_byte_offset = (typeof output.byteOffset == 'undefined' ? 0 : output.byteOffset);
// we only care about VEC3 and VEC4 since we are only dealing with rotation, scale, and translation,
// and we are going to assume they are floating point (componentType = 5126 according to gltf spec)
if(output.componentType != 5126)
console.warn("output component type is not a float!");
let output_component_count = (output.type == "VEC3" ? 3 : 4);
// 4 byte floats in according to gltf spec
let output_element_size = output_component_count * 4;
//use the buffer view and accessor to offset into the binary buffer to retrieve our value data
let output_bufferView = gltf_json.bufferViews[output.bufferView];
let output_accessor_data_offset = output_bufferView.byteOffset + output_accessor_byte_offset;
let output_bin = bin_data_chunk.slice(output_accessor_data_offset, output_accessor_data_offset + output_element_size * output.count);
let output_dv = new DataView(output_bin);
// parse and store each value
let values = [];
for(var j = 0; j < output.count * output_component_count; j += output_component_count) {
let value = [];
for(var l = 0; l < output_component_count; l++) {
value.push(output_dv.getFloat32(j*4 + l*4, true));
}
values.push(value);
}
if(dof_type == "translation") {
for(var j = 0; j < output.count; j++) {
curr_animation.tracks[node_name].translation_keys.push(new AnimationKey(timestamps[j], values[j]));
}
} else if(dof_type == "rotation") {
for(var j = 0; j < output.count; j++) {
curr_animation.tracks[node_name].rotation_keys.push(new AnimationKey(timestamps[j], values[j]));
}
} else if(dof_type == "scale") {
for(var j = 0; j < output.count; j++) {
curr_animation.tracks[node_name].scale_keys.push(new AnimationKey(timestamps[j], values[j]));
}
}
}
animations.push(curr_animation);
}
return animations;
}
static async parseAnimationSetFromUri(glb_uri) {
let array_buffer = await this._getResourceAsync(glb_uri);
return this._parseAnimationSetFromArrayBuffer(array_buffer);
}
static async parseAnimationSetFromFile(glb_file) {
let array_buffer = await this._readFileAsync(glb_file);
return this._parseAnimationSetFromArrayBuffer(array_buffer);
}
static _parseAnimationSetFromArrayBuffer(array_buffer) {
let animation_nodes = AnimationParser.parseAnimationNodesFromArrayBuffer(array_buffer);
// convert nodes to dictionary format
let nodes_dict = {};
for(var i = 0; i < animation_nodes.length; i++) {
nodes_dict[animation_nodes[i].name] = animation_nodes[i];
//if the node defines its TRS info as a matrix, we need to capture that (see glTF 2.0 spec)
if(typeof animation_nodes[i].matrix !== 'undefined') {
let mat = new Cesium.Matrix4();
Cesium.Matrix4.fromColumnMajorArray(animation_nodes[i].matrix, mat);
nodes_dict[animation_nodes[i].name].matrix = mat;
}
//set default values for translation rotation and scale if they do not exist
if(typeof nodes_dict[animation_nodes[i].name].translation === 'undefined')
nodes_dict[animation_nodes[i].name].translation = [0,0,0];
if(typeof nodes_dict[animation_nodes[i].name].rotation === 'undefined') {
nodes_dict[animation_nodes[i].name].rotation = [0,0,0,1];
nodes_dict[animation_nodes[i].name].inv_rotation_matrix = Cesium.Matrix3.IDENTITY;
nodes_dict[animation_nodes[i].name].inv_rotation = new Cesium.Quaternion(0,0,0,1);
}
else {
//compute and store the inverse rotation matrix and quaternion for future calculations
let orig_rot = nodes_dict[animation_nodes[i].name].rotation;
let orig_quat = new Cesium.Quaternion(orig_rot[0], orig_rot[1], orig_rot[2], orig_rot[3]);
let orig_quat_inv = new Cesium.Quaternion();
Cesium.Quaternion.inverse(orig_quat, orig_quat_inv);
let invMat = new Cesium.Matrix3();
Cesium.Matrix3.fromQuaternion(orig_quat_inv, invMat);
nodes_dict[animation_nodes[i].name].inv_rotation_matrix = invMat;
nodes_dict[animation_nodes[i].name].inv_rotation = orig_quat_inv;
}
if(typeof nodes_dict[animation_nodes[i].name].scale === 'undefined')
nodes_dict[animation_nodes[i].name].scale = [0,0,0];
}
let animations = AnimationParser.parseAnimationsFromArrayBuffer(array_buffer);
console.log(nodes_dict);
return new AnimationSet(animations, nodes_dict);
}
};