/* <copyright> This file contains proprietary software owned by Motorola Mobility, Inc.<br/> No rights, expressed or implied, whatsoever to this software are provided by Motorola Mobility, Inc. hereunder.<br/> (c) Copyright 2011 Motorola Mobility, Inc. All Rights Reserved. </copyright> */ var g_numChannels = new stat( "animation", "numChannels", 0, null, false ); var g_numTracks = new stat( "animation", "numTracks", 0, null, false ); /** * channelController * The channel controller is really the workhorse of the RDGE animation system. It handles timing, * interpolation, and sampling of attributes over the lifetime of an animation. Each channel controller * is responsible for animating a single attribute. The current implementation supports animating vector, * boolean, or quaternion attributes. This class is used internally by the animation system. * * @param _animation - the animation resource * @param _channel - the channel id * */ channelController = function(_animation, _channel) { /** * this.interpolate - Enable/Disable interpolation between animation frames. * Typically this should be enabled for smoother looking animation. However, * there may be applications where interpolation is undesireable. */ this.interpolate = false; /** * this.animation - the animation resource. * This is where the keyframes for the channel are stored. */ this.animation = _animation; /** * this.channel - the channel id. This is used to look up the keyframe data for this channel. */ this.channel = _channel; /** * this.localTime - the current time, relative to the start time. */ this.localTime = 0.0; /** * this.startTime - the start time of the animation clip window. */ this.startTime = this.animation.clipStart / this.animation.framesPerSec; /** * this.endTime - the end time of the animation clip window. */ this.endTime = this.animation.clipEnd / this.animation.framesPerSec; /** * this.cachedFrame - cached frame index, this optimizes best case scenario computeFrame calls. */ this.cachedFrame = -1; /** * oneFrameInSecs - stores the interval of a single frame in seconds. This is used for internal calculations. */ oneFrameInSecs = 1.0 / _animation.framesPerSec; /** * this.channel.timeline - stores the animation timeline. * Currently this is calculated based on the framePerSec settings of the animation. * Eventually the timeline should be exported with the animation. Individual channels * may have different timelines depending on which frames are keyed. */ this.channel.timeline = new Array(this.channel.numKeys + 1); for (i = 0; i <= this.channel.numKeys; ++i) { this.channel.timeline[i] = i / this.animation.framesPerSec; } /** this.computeFrame * Calculates the current frame index of the animation at the current time. * In the worst case, this function will perform a binary search for the frame * whose time is closest to and less than the current time. In the best case, * the current frame is near the most recently cached frame, or it remains unchanged. */ this.computeFrame = function() { var absTime = this.localTime + this.startTime; var start = this.animation.clipStart; var end = this.animation.clipEnd; if (this.cachedFrame != -1) { // if the current time is reasonably close to the last frame processed try searching // forward or backward. best case it is the next frame. if (Math.abs(absTime - this.channel.timeline[this.cachedFrame]) < 5 * oneFrameInSecs) { if (this.channel.timeline[this.cachedFrame] < absTime) { while (this.channel.timeline[this.cachedFrame + 1] <= absTime) { this.cachedFrame++; if (this.animation.looping) { if (this.cachedFrame > this.numFrames - 1) { this.cachedFrame -= this.numFrames - 1; } } else { if (this.cachedFrame > this.numFrames - 1) { this.cachedFrame = this.numFrames - 1; } } } return this.cachedFrame; } else { while (this.channel.timeline[this.cachedFrame] > absTime) { this.cachedFrame--; if (this.animation.looping) { if (this.cachedFrame < 0) { this.cachedFrame += this.numFrames - 1; } } else { if (this.cachedFrame > this.numFrames - 1) { this.cachedFrame = this.numFrames - 1; } } } return this.cachedFrame; } } } // binary search... while (start + 1 < end) { var mid = Math.floor((start + end) / 2); if (absTime > this.channel.timeline[mid]) { start = mid + 1; } else { end = mid - 1; } } this.cachedFrame = start; return start; } /* this.sampleBool - Sample a boolean at the current frame, booleans are not interpolated. * This function is used internally. */ this.sampleBool = function() { // no interpolation on flags... var index = this.computeFrame(); return this.channel.keys[index]; } /* this.sampleQuat - Sample a quaternion at the current frame. * if this.interpolate == true, quaternions are interpolated inbetween frames using spherical linear interpolation (SLERP). */ this.sampleQuat = function() { var frame0 = this.computeFrame(); var frame1 = frame0 + 1; var k0 = this.channel.timeline[frame0]; var k1 = this.channel.timeline[frame1]; var index0 = frame0 * 4; var index1 = frame1 * 4; if (this.interpolate) { var absTime = this.localTime + this.startTime; var t = (absTime - k0) / (k1 - k0); var a = [this.channel.keys[index0 + 0], this.channel.keys[index0 + 1], this.channel.keys[index0 + 2], this.channel.keys[index0 + 3]]; var b = [this.channel.keys[index1 + 0], this.channel.keys[index1 + 1], this.channel.keys[index1 + 2], this.channel.keys[index1 + 3]]; return quat.slerp(a, b, t); } return [this.channel.keys[index0 + 0], this.channel.keys[index0 + 1], this.channel.keys[index0 + 2], this.channel.keys[index0 + 3]]; } /* this.sampleVec3 - Sample a vector3 at the current frame. * if this.interpolate == true, vectors are interpolated inbetween frames using linear interpolation (LERP). */ this.sampleVec3 = function() { var frame0 = this.computeFrame(); var frame1 = frame0 + 1; var k0 = this.channel.timeline[frame0]; var k1 = this.channel.timeline[frame1]; var index0 = frame0 * 3; var index1 = frame1 * 3; if (this.interpolate) { var absTime = this.localTime + this.startTime; var t = (absTime - k0) / (k1 - k0); var a = [this.channel.keys[index0 + 0], this.channel.keys[index0 + 1], this.channel.keys[index0 + 2]]; var b = [this.channel.keys[index1 + 0], this.channel.keys[index1 + 1], this.channel.keys[index1 + 2]]; return vec3.lerp(a, b, t); } return [this.channel.keys[index0 + 0], this.channel.keys[index0 + 1], this.channel.keys[index0 + 2]]; } /* this.setTime - set the current time. */ this.setTime = function(t) { this.localTime = t; if (this.localTime < 0.0) { this.localTime = 0.0; } if (this.localTime > this.animation.duration - oneFrameInSecs) { this.localTime = this.animation.duration - oneFrameInSecs; } } /* this.setProgress - set the current time as a percentage of the duration. */ this.setProgress = function(f) { this.setTime(f * this.animation.duration); } /* this.setFrame - set the current time by frame number. */ this.setFrame = function(f) { this.setTime(f / this.animation.framesPerSec); } /* this.step - advance time by the given timestep and wrap if looping. */ this.step = function(_dt) { this.localTime += _dt; if (this.animation.looping) { while (this.localTime < 0.0) { this.localTime += this.animation.duration - oneFrameInSecs; } while (this.localTime >= this.animation.duration - oneFrameInSecs) { this.localTime -= this.animation.duration - oneFrameInSecs; } } else { if (this.localTime < 0.0) { this.localTime = 0.0; } if (this.localTime > this.animation.duration) { this.localTime = this.animation.duration; } } } } /** * track * Each track advances and samples from a list of channel controllers, and is assigned to a scene graph node. * * @param _animation - the animation resource * @param _track - the track id * @param _node - the scene node * */ track = function(_animation, _track, _node) { this.track = _track; this.node = _node; this.channelControllers = new Array(); for (ch in _track) { this.channelControllers[ch] = new channelController(_animation, _track[ch]); g_numChannels.value++; } this.step = function(_dt) { for (cc in this.channelControllers) { this.channelControllers[cc].step(_dt); } var rotate = this.channelControllers["rotate"].sampleQuat(); var scale = this.channelControllers["scale"].sampleVec3(); var translate = this.channelControllers["translate"].sampleVec3(); if (this.channelControllers["vis"] != null) { var vis = this.channelControllers["vis"].sampleBool(); this.node.hide = !vis; } var m = mat4.identity(); m = mat4.scale(m, scale); m = mat4.mul(m, quat.toMatrix(rotate)); m = mat4.translate(m, translate); this.node.local = m; } } animation = function(_scene, _clipStart, _clipEnd, _loop) { this.animation = _scene.scene.animation; this.clipStart = _clipStart; // this is a little hacky, but it works for now. if (_clipEnd == -1) { for (tr in this.animation) { for (ch in this.animation[tr]) { _clipEnd = this.animation[tr][ch].numKeys - 1; break; } break; } } this.clipEnd = _clipEnd; this.numFrames = _clipEnd - _clipStart; this.framesPerSec = 30.0; this.duration = this.numFrames / this.framesPerSec; this.rate = 1.0; this.looping = _loop; this.tracks = new Array(); // creating a mapping here to make binding tracks a little more // straightforward. mapping = new Array(); mapping.process = function(trNode, parent) { mapping[trNode.name] = trNode; } g_Engine.getContext().getScene().Traverse(mapping); //g_sg.Traverse(mapping); for (tr in this.animation) { if (mapping[tr] !== undefined) { this.tracks.push(new track(this, this.animation[tr], mapping[tr])); g_numTracks.value++; } } this.step = function(_dt) { for (tr in this.tracks) { this.tracks[tr].step(g_animationRate * this.rate * _dt); } } }