Browse Source

Merge pull request #286 from Palmer-JC/Automaton

Automatons Objects; test code to follow
deltakosh 11 years ago
parent
commit
1023565da1

+ 368 - 0
Babylon/Mesh/Automaton/babylon.automaton.ts

@@ -0,0 +1,368 @@
+module BABYLON {
+    export class Automaton extends Mesh {
+        public  debug = false;
+        private _engine: Engine;
+        private _positionsVBO : WebGLBuffer;
+        private _normalsVBO   : WebGLBuffer;
+        private _positions32F : Float32Array;
+        private _normals32F   : Float32Array;
+        private _shapeKeyGroups = new Array<ShapeKeyGroup>();
+        
+        // for normal processing
+        private _vertexMemberOfFaces = new Array<Array<number>>(); // outer array each vertex, inner array faces vertex is a member of
+        
+        // for passive detection of game pause
+        private _lastResumeTime = 0;
+        private _instancePaused = false;
+
+        // tracking system members
+        private _clockStart = -1;
+        private _renderCPU = 0;
+        private _totalDeformations = 0;
+        private _totalFrames = 0;
+        
+        // pov orientation
+        private _definedFacingForward : boolean = true;
+
+        constructor(name: string, scene: Scene) {
+            super(name, scene);
+            this._engine = scene.getEngine();    
+            
+            // tricky registering a prototype as a callback in constructor; cannot say 'this.beforeRender()' & must be wrappered
+            var ref = this;
+            this.registerBeforeRender(function(){ref.beforeRender();});
+        }
+        // ============================ beforeRender callback & tracking =============================
+        public beforeRender() : void {
+            if (this._positions32F === null || this._normals32F === null || Automaton._systemPaused || this._instancePaused) return;
+            var startTime = Automaton.now();            
+
+            // system resume test 
+            if (this._lastResumeTime < Automaton._systemResumeTime){
+                for (var g = this._shapeKeyGroups.length - 1; g >= 0; g--){
+                    this._shapeKeyGroups[g].resumePlay();
+                }
+                this._lastResumeTime = Automaton._systemResumeTime;
+            }
+
+            var changesMade = false;
+            for (var g = this._shapeKeyGroups.length - 1; g >= 0; g--){               
+                // do NOT combine these 2 lines or only 1 group will run!
+                var changed = this._shapeKeyGroups[g].incrementallyDeform(this._positions32F, this._normals32F);
+                changesMade = changesMade || changed;
+            }
+            
+            if (changesMade){            
+                if (this._clockStart < 0) this._resetTracking(startTime); // delay tracking until the first change is made
+
+                //recompute parts of normals where positions updated & resend
+                this._engine.updateDynamicVertexBuffer(this._normalsVBO, this._normals32F);
+                
+                // resend positions
+                this._engine.updateDynamicVertexBuffer(this._positionsVBO, this._positions32F);
+            
+                this._renderCPU += Automaton.now() - startTime;
+                this._totalDeformations++;  
+            }
+                
+            this._totalFrames ++;
+        }
+        
+        public resetTracking() : void{
+            this._resetTracking(Automaton.now());
+        }
+        private _resetTracking(startTime : number) : void{
+            this._clockStart = startTime;
+            this._renderCPU = 0;
+            this._totalDeformations = 0; 
+            this._totalFrames = 0;           
+        }
+        
+        public getTrackingReport(reset : boolean = false) : string{
+            var totalWallClock = Automaton.now() - this._clockStart;
+            var report =
+                    "\nNum Deformations: " + this._totalDeformations +
+                    "\nRender CPU milli: " + this._renderCPU.toFixed(2) +
+                    "\nRender CPU milli / Deformations: " + (this._renderCPU / this._totalDeformations).toFixed(2) +
+                    "\nWallclock milli / Deformations: " + (totalWallClock / this._totalDeformations).toFixed(2) +
+                    "\nMemo, Deformations / Sec: " + (this._totalDeformations / (totalWallClock / 1000)).toFixed(2) +
+                    "\nMemo, Frames with no deformation: " + (this._totalFrames - this._totalDeformations) +
+                    "\nMemo, Total vertices: " + this.getTotalVertices() +
+                    "\nShape keys:";
+            for (var i = 0; i < this._shapeKeyGroups.length; i++)
+                report += "\n" + this._shapeKeyGroups[i].toString();
+            
+            if (reset) this.resetTracking();
+            return report;    
+        }
+        // ======================================== Overrides ========================================
+        public clone(name: string, newParent: Node, doNotCloneChildren?: boolean): Mesh {
+             alert("Shared vertex instances not supported for Automatons");
+            return null;
+        }
+        public createInstance(name: string): InstancedMesh {
+             alert("Shared vertex instances not supported for Automatons");
+             return null;
+        }
+        public convertToFlatShadedMesh() : void {
+            alert("Flat shading not supported for Automatons");
+        }
+         
+        /* wrappered is so positions & normals vertex buffer & initial data can be captured */
+        public setVerticesData(kind: any, data: any, updatable?: boolean) : void {
+            super.setVerticesData(kind, data, updatable || kind === VertexBuffer.PositionKind || kind === VertexBuffer.NormalKind);
+            
+            var babylonVertexBuffer : VertexBuffer;
+            if (kind === VertexBuffer.PositionKind){
+                babylonVertexBuffer = this.getVertexBuffer(VertexBuffer.PositionKind);
+            
+                this._positionsVBO = babylonVertexBuffer.getBuffer();
+                this._positions32F = new Float32Array(babylonVertexBuffer.getData());
+            }
+            else if (kind === VertexBuffer.NormalKind){
+                babylonVertexBuffer = this.getVertexBuffer(VertexBuffer.NormalKind);
+            
+                this._normalsVBO = babylonVertexBuffer.getBuffer();
+                this._normals32F = new Float32Array(babylonVertexBuffer.getData());
+            }
+        }
+        
+        /** wrappered so this._vertexMemberOfFaces can be built after super.setIndices() called */
+        public setIndices(indices: number[]): void {
+            super.setIndices(indices);
+            
+            // now determine _vertexMemberOfFaces, to improve normals performance
+            var nFaces = indices.length / 3;
+            var faceOffset : number;
+            
+            // _vertexMemberOfFaces:  outer array each vertex, inner array faces vertex is a member of
+            var nVertices = super.getTotalVertices();
+
+            // possibly remove or comment out
+            var nZeroAreaFaces = this.findZeroAreaFaces();
+            if (nZeroAreaFaces > 0) console.warn("Automaton: Zero area faces found:  " + nZeroAreaFaces + ", nFaces: " + nFaces + ", nVert " + nVertices);
+            
+            for (var v = 0; v < nVertices; v++){
+                var memberOf = new Array<number>();
+                
+                for (var f = 0; f < nFaces; f++){
+                    faceOffset = f * 3;
+                    if (indices[faceOffset] === v || indices[faceOffset + 1] === v ||indices[faceOffset + 2] === v){
+                        memberOf.push(f);
+                    }
+                }
+                this._vertexMemberOfFaces.push(memberOf);
+            }       
+        }
+        /** bad things happen to normals when a face has no area.  Double check & put out warning in setIndices() if any found */
+        private findZeroAreaFaces() : number {
+            var indices = super.getIndices();
+            var nFaces = indices.length / 3;
+            var positions = super.getVerticesData(VertexBuffer.PositionKind);
+            var nZeroAreaFaces = 0;
+            var faceOffset : number;
+            var p1 = Vector3.Zero();
+            var p2 = Vector3.Zero();
+            var p3 = Vector3.Zero();
+    
+            for (var f = 0; f < nFaces; f++){
+                faceOffset = f * 3;
+                Vector3.FromArrayToRef(positions, 3 * indices[faceOffset    ], p1);
+                Vector3.FromArrayToRef(positions, 3 * indices[faceOffset + 1], p2);
+                Vector3.FromArrayToRef(positions, 3 * indices[faceOffset + 2], p3);
+                
+                if (p1.equals(p2) || p1.equals(p3) || p2.equals(p3)) nZeroAreaFaces++
+            }
+            return nZeroAreaFaces;
+        }
+        // ==================================== Normals processing ===================================
+        /**
+         * based on http://stackoverflow.com/questions/18519586/calculate-normal-per-vertex-opengl 
+         * @param {Uint16Array} vertices - the vertices which need the normals calculated, so do not have to do the entire mesh
+         * @param {Float32Array} normals - the array to place the results, size:  vertices.length * 3
+         * @param {Float32Array} futurePos - value of positions on which to base normals, passing since so does not have to be set to in mesh yet
+         */
+        public normalsforVerticesInPlace(vertices : Uint16Array, normals : Float32Array, futurePos : Float32Array) : void {
+            var indices = super.getIndices();
+            var nVertices = vertices.length;
+            
+            // Define all the reusable objects outside the for loop, so ..ToRef() & ..InPlace() versions can be used, 
+            // avoiding many single use objects to garbage collect.
+            var memberOfFaces : Array<number>;
+            var nFaces : number;
+            var faceOffset : number;
+            var vertexID : number;
+            var p1 = Vector3.Zero();
+            var p2 = Vector3.Zero();
+            var p3 = Vector3.Zero();
+            var p1p2 = Vector3.Zero();
+            var p3p2 = Vector3.Zero();
+
+            var cross = Vector3.Zero();
+            var normal = Vector3.Zero();
+            var sinAlpha :number;
+            var weightedAvgSum = Vector3.Zero();
+            
+            for (var v = 0; v < nVertices; v++){
+                memberOfFaces = this._vertexMemberOfFaces[vertices[v]];
+                nFaces = memberOfFaces.length;
+                Vector3.FromFloatsToRef(0, 0, 0, weightedAvgSum); // initialize reused vector to all zeros
+                
+                for (var f = 0; f < nFaces; f++){
+                    faceOffset = memberOfFaces[f] * 3;
+                    vertexID = this.indexOfVertInFace(indices[faceOffset], indices[faceOffset + 1], indices[faceOffset + 2], vertices[v]);
+                    if (vertexID === -1) throw "Automaton: vertex not part of face";  // should not happen, but better to check
+                    
+                    // triangleNormalFromVertex() as from example noted above
+                    Vector3.FromFloatArrayToRef(futurePos, 3 * indices[faceOffset +   vertexID], p1);
+                    Vector3.FromFloatArrayToRef(futurePos, 3 * indices[faceOffset + ((vertexID + 1) % 3)], p2);
+                    Vector3.FromFloatArrayToRef(futurePos, 3 * indices[faceOffset + ((vertexID + 2) % 3)], p3);
+                        
+                    p1.subtractToRef(p2, p1p2);
+                    p3.subtractToRef(p2, p3p2);
+                    
+                    Vector3.CrossToRef(p1p2, p3p2, cross);
+                    Vector3.NormalizeToRef(cross, normal);
+                    
+                    sinAlpha = cross.length() / (p1p2.length() * p3p2.length());
+                    
+                    // due floating point, might not be -1 ≤ sinAlpha ≤ 1, e.g. 1.0000000000000002; fix to avoid Math.asin() from returning NaN
+                    if (sinAlpha < -1) sinAlpha = -1;
+                    else if (sinAlpha > 1) sinAlpha = 1;
+                    
+                    normal.scaleInPlace(Math.asin(sinAlpha));                    
+                    weightedAvgSum.addInPlace(normal);
+                }
+                weightedAvgSum.normalize();
+                normals[ v * 3     ] = weightedAvgSum.x;
+                normals[(v * 3) + 1] = weightedAvgSum.y;
+                normals[(v * 3) + 2] = weightedAvgSum.z;
+            }
+        }
+
+        private indexOfVertInFace(idx0 : number, idx1 : number, idx2 : number, vertIdx : number) : number{
+            if (vertIdx === idx0) return 0;
+            if (vertIdx === idx1) return 1;
+            if (vertIdx === idx2) return 2;
+            return -1;
+        }
+        // ================================== ShapeKeyGroup related ==================================
+        public addShapeKeyGroup(shapeKeyGroup : ShapeKeyGroup) : void {
+            this._shapeKeyGroups.push(shapeKeyGroup);
+        }
+            
+        public queueEventSeries(eSeries : AutomatonEventSeries) : void {
+            var groupFound = false;  
+            for (var g = this._shapeKeyGroups.length - 1; g >= 0; g--){
+                if (eSeries.isShapeKeyGroupParticipating(this._shapeKeyGroups[g].getName())){
+                    this._shapeKeyGroups[g].queueEventSeries(eSeries);
+                    groupFound = true;
+                }
+            }
+            if (this.debug && !groupFound) console.warn("no shape keys groups participating in event series");
+        }
+        
+        public getShapeKeyGroup(groupName : string) : ShapeKeyGroup {
+            for (var g = this._shapeKeyGroups.length - 1; g >= 0; g--){
+                if (this._shapeKeyGroups[g].getName() === groupName){
+                    return this._shapeKeyGroups[g];
+                }
+            }
+            return null;
+        }
+        // ================================== Point of View Movement =================================
+        /**
+         * When the mesh is defined facing forward, multipliers must be set so that movePOV() is 
+         * from the point of view of behind the front of the mesh.
+         * @param {boolean} definedFacingForward - True is the default
+         */
+        public setDefinedFacingForward(definedFacingForward : boolean) : void {
+            this._definedFacingForward = definedFacingForward;
+        }
+        
+        /**
+         * Perform relative position change from the point of view of behind the front of the mesh.
+         * This is performed taking into account the meshes current rotation, so you do not have to care.
+         * Supports definition of mesh facing forward or backward.
+         * @param {number} amountRight
+         * @param {number} amountUp
+         * @param {number} amountForward
+         */
+        public movePOV(amountRight : number, amountUp : number, amountForward : number) : void {
+            this.position.addInPlace(this.calcMovePOV(amountRight, amountUp, amountForward));
+        }
+        
+        /**
+         * Calculate relative position change from the point of view of behind the front of the mesh.
+         * This is performed taking into account the meshes current rotation, so you do not have to care.
+         * Supports definition of mesh facing forward or backward.
+         * @param {number} amountRight
+         * @param {number} amountUp
+         * @param {number} amountForward
+         */
+        public calcMovePOV(amountRight : number, amountUp : number, amountForward : number) : Vector3 {
+            var rotMatrix = new Matrix();
+            var rotQuaternion = (this.rotationQuaternion) ? this.rotationQuaternion : Quaternion.RotationYawPitchRoll(this.rotation.y, this.rotation.x, this.rotation.z);
+            rotQuaternion.toRotationMatrix(rotMatrix);
+            
+            var translationDelta = Vector3.Zero();
+            var defForwardMult = this._definedFacingForward ? -1 : 1;
+            Vector3.TransformCoordinatesFromFloatsToRef(amountRight * defForwardMult, amountUp, amountForward * defForwardMult, rotMatrix, translationDelta);
+            return translationDelta;
+        }
+        // ================================== Point of View Rotation =================================
+        /**
+         * Perform relative rotation change from the point of view of behind the front of the mesh.
+         * Supports definition of mesh facing forward or backward.
+         * @param {number} flipBack
+         * @param {number} twirlClockwise
+         * @param {number} tiltRight
+         */
+        public rotatePOV(flipBack : number, twirlClockwise : number, tiltRight : number) : void {
+            this.rotation.addInPlace(this.calcRotatePOV(flipBack, twirlClockwise, tiltRight));
+        }
+        
+        /**
+         * Calculate relative rotation change from the point of view of behind the front of the mesh.
+         * Supports definition of mesh facing forward or backward.
+         * @param {number} flipBack
+         * @param {number} twirlClockwise
+         * @param {number} tiltRight
+         */
+        public calcRotatePOV(flipBack : number, twirlClockwise : number, tiltRight : number) : Vector3 {
+            var defForwardMult = this._definedFacingForward ? 1 : -1;
+            return new Vector3(flipBack * defForwardMult, twirlClockwise, tiltRight * defForwardMult);
+        }
+        // =================================== play - pause system ===================================
+        // pause & resume statics
+        private static _systemPaused = false;
+        private static _systemResumeTime = 0;
+        
+        /** system could be paused at a higher up without notification; just by stop calling beforeRender() */
+        public static pauseSystem(){ Automaton._systemPaused = true; }        
+        public static isSystemPaused() : boolean { return Automaton._systemPaused; }
+        
+        public static resumeSystem(){
+            Automaton._systemPaused = false;
+            Automaton._systemResumeTime = Automaton.now();
+        }
+        
+        // instance level methods
+        public pausePlay(){ this._instancePaused = true; }       
+        public isPaused() : boolean { return this._instancePaused; }
+        
+        public resumePlay(){
+            this._instancePaused = false;
+            this._lastResumeTime = Automaton.now();
+            
+            for (var g = this._shapeKeyGroups.length - 1; g >= 0; g--){
+                this._shapeKeyGroups[g].resumePlay();
+            }
+        }
+        // ========================================= Statics =========================================
+        /** wrapper for window.performance.now, incase not implemented, e.g. Safari */
+        public static now() : number{
+            return (typeof window.performance === "undefined") ? Date.now() : window.performance.now();
+        }
+    }
+}

+ 185 - 0
Babylon/Mesh/Automaton/babylon.automatonEventSeries.ts

@@ -0,0 +1,185 @@
+module BABYLON {
+    /** Internal helper class used by AutomatonEventSeries to support a multi-shape group EventSeries */
+    class ParticipatingGroup{
+        _indexInRun  = -99; // ensure isReady() initially returns false
+        _highestIndexInRun = -1;
+        
+        constructor (public groupName : string) {}
+        public isReady    () : boolean { return this._indexInRun === -1; }
+        public runComplete() : boolean { return this._indexInRun > this._highestIndexInRun; }
+        public activate() : void{
+            this._indexInRun = -1;
+        }
+    }
+    
+    /** Provide an action for an AutomatonEventSeries, for integration into action manager */
+    export class AutomatonEventSeriesAction extends Action{
+        constructor(triggerOptions: any, private _target: Automaton, private _eSeries : AutomatonEventSeries, condition?: Condition) {
+            super(triggerOptions, condition);
+        }
+        public execute(evt: ActionEvent): void {
+            this._target.queueEventSeries(this._eSeries);
+        }
+    }
+
+    /** main class of file */
+    export class AutomatonEventSeries {
+        private _nEvents : number; // events always loop in ascending order; reduces .length calls        
+        private _groups = new Array<ParticipatingGroup>();
+        public  nGroups : number;  // public for ShapeKeyGroup, so it can determine if it the sole sole participating, === 1
+        private _everyBodyReady : boolean;
+        private _repeatCounter : number;
+        private _proRatingThisRepeat;
+        
+        /**
+         * Validate each of the events passed and build unique shapekey groups particpating.
+         * @param {Array} _eventSeries - Elements must either be a ReferenceDeformation, Action, or function.  Min # of Deformations: 1
+         * @param {number} _nRepeats - Number of times to run through series elements.  There is sync across runs. (Default 1)
+         * @param {number} _initialWallclockProrating - The factor to multiply the duration of a Deformation before passing to a
+         *                 ShapeKeyGroup.  Amount is decreased or increased across repeats, so that it is 1 for the final repeat.
+         *                 Facilitates acceleration when > 1, & deceleration when < 1. (Default 1)
+         * @param {string} _debug - Write progress messages to console when true (Default false)
+         */
+        constructor(private _eventSeries : Array<any>, private _nRepeats = 1, private _initialWallclockProrating = 1.0, private _debug = false) {
+            this._nEvents = _eventSeries.length;
+
+            // go through each event in series, building up the unique set shape key groups participating, this._groups
+            for (var i = 0; i < this._nEvents; i++){
+                if (this._eventSeries[i] instanceof ReferenceDeformation || this._eventSeries[i] instanceof Action || typeof this._eventSeries[i] === "function"){
+                    
+                    if (this._eventSeries[i] instanceof ReferenceDeformation){
+                        var groupName = (<ReferenceDeformation> this._eventSeries[i]).shapeKeyGroupName;
+                        var pGroup : ParticipatingGroup = null;
+                    
+                        for (var g = this._groups.length - 1; g >= 0; g--){
+                            if (this._groups[g].groupName === groupName){
+                                pGroup = this._groups[g];               
+                                break;               
+                            }
+                        }
+                        if (pGroup === null){
+                            pGroup = new ParticipatingGroup(groupName);
+                            this._groups.push(pGroup);
+                        } 
+                        pGroup._highestIndexInRun = i;
+                    }
+                    else{
+                        // Actions & function()s all run from group 0 (may not have been assigned yet)
+                        if (this._groups.length > 0) this._groups[0]._highestIndexInRun = i;
+                        if (this._eventSeries[i] instanceof Action) (<Action> this._eventSeries[i])._prepare();
+                    }
+                    
+                }else{
+                     throw "AutomatonEventSeries:  eventSeries elements must either be a Deformation, Action, or function";
+                }
+            }
+            // make sure at least 1 Deformation passed, not just Actions or functions, since there will be no group to assign them to
+            this.nGroups = this._groups.length;
+            if (this.nGroups === 0) throw "AutomatonEventSeries: Must have at least 1 Deformation in series.";
+            
+            if (this._debug && this._nRepeats === 1 && this._initialWallclockProrating !== 1)
+                console.log("AutomatonEventSeries: clock prorating ignored when # of repeats is 1");
+        }
+        
+        /** 
+         * called by Automaton, to figure out which shape key group(s) this should be queued on.
+         * @param {string} groupName - This is the group name to see if it has things to do in event series.
+         */
+        public isShapeKeyGroupParticipating(groupName : string) : boolean{
+            for (var g = 0; g < this.nGroups; g++){
+                if (this._groups[g].groupName === groupName) return true;
+            }
+            return false;
+        }
+        
+        /**
+         * Signals that a ParticipatingGroup is ready to start processing.  Also evaluates if everyBodyReady.
+         * @param {string} groupName - This is the group name saying it is ready.
+         */
+        public activate(groupName : string) : void{
+            this._everyBodyReady = true;
+            for (var g = 0; g < this.nGroups; g++){
+                if (this._groups[g].groupName === groupName) 
+                    this._groups[g].activate();
+                else this._everyBodyReady = this._everyBodyReady && this._groups[g].isReady();
+            }
+            if (this._debug) console.log("series activated by " + groupName + ", _everyBodyReady: " + this._everyBodyReady);
+            this._repeatCounter = 0;
+            this._proRatingThisRepeat = (this._nRepeats > 1) ? this._initialWallclockProrating : 1.0;
+        }
+        
+        /**
+         * Called by a shape key group to know if series is complete.  nextEvent() may still
+         * return null if other groups not yet completed their events in a run, or this group has
+         * no more to do, but is being blocked from starting its next series till all are done here.
+         */
+        public hasMoreEvents(){
+            return this._repeatCounter < this._nRepeats;
+        }
+        
+        /**
+         * Called by a shape key group to get its next event of the series.  Returns null if
+         * blocked, while waiting for other groups.
+         * @param {string} groupName - Name of the group calling for its next event
+         * 
+         */
+        public nextEvent(groupName : string) : any {
+            // return nothing till all groups signal they are ready to start
+            if (!this._everyBodyReady) return null;
+            
+            var pGroup : ParticipatingGroup;
+            var isGroupForActions    = false; // actions are processed on group 0
+            var allGroupsRunComplete = true; 
+            
+            // look up the appropriate ParticipatingGroup for below & set allGroupsRunComplete
+            for (var g = 0; g < this.nGroups; g++){
+                allGroupsRunComplete = allGroupsRunComplete && this._groups[g].runComplete();   
+                         
+                // no break statement inside block, so allGroupsRunComplete is valid
+                if (this._groups[g].groupName === groupName){
+                    pGroup = this._groups[g]; 
+                    isGroupForActions = g === 0;
+                }
+            }
+            
+            if (allGroupsRunComplete){
+                // increment repeat counter, reset for next run unless no more repeats
+                if (++this._repeatCounter < this._nRepeats){
+                    for (var g = 0; g < this.nGroups; g++){
+                        this._groups[g].activate();
+                    }
+                    if (this._initialWallclockProrating !== 1){
+                        this._proRatingThisRepeat = this._initialWallclockProrating + ((1 - this._initialWallclockProrating) * ((this._repeatCounter + 1) / this._nRepeats) );
+                    }
+                    if (this._debug) console.log("set for repeat # " + this._repeatCounter);
+                }else{
+                 if (this._debug) console.log("Series complete");
+                 this._everyBodyReady = false; // ensure that nothing happens until all groups call activate() again
+                }
+            }
+            
+            if (!pGroup.runComplete()){
+                // test if should declare complete
+                if (pGroup._indexInRun === pGroup._highestIndexInRun){
+                    pGroup._indexInRun++;
+                    return null;
+                }
+                for (var i = pGroup._indexInRun + 1; i < this._nEvents; i++){
+                    if (this._eventSeries[i] instanceof ReferenceDeformation){
+                        var name = (<ReferenceDeformation> this._eventSeries[i]).shapeKeyGroupName;
+                        if (pGroup.groupName === name){
+                            pGroup._indexInRun = i;
+                            (<ReferenceDeformation>this._eventSeries[i]).setProratedWallClocks(this._proRatingThisRepeat);
+                            if (this._debug) 
+                                console.log(i + " in series returned: " + name + ", allGroupsRunComplete " + allGroupsRunComplete + ", everyBodyReady " + this._everyBodyReady);
+                            return this._eventSeries[i];
+                        }
+                    }else if (isGroupForActions){
+                        pGroup._indexInRun = i;
+                        return this._eventSeries[i];
+                    }
+                }
+            }else return null; 
+        }
+    }
+}

+ 22 - 0
Babylon/Mesh/Automaton/babylon.deformation.ts

@@ -0,0 +1,22 @@
+module BABYLON {
+    /**
+     * sub-class of ReferenceDeformation, where the referenceStateName is Fixed to "BASIS"
+     */ 
+    export class Deformation extends ReferenceDeformation{
+        /**
+         * @param {string} shapeKeyGroupName -  Used by Automaton to place in the correct ShapeKeyGroup queue(s).
+         * @param {string} endStateName - Name of state key to deform to
+         * @param {number} milliDuration - The number of milli seconds the deformation is to be completed in
+         * @param {number} millisBefore - Fixed wait period, once a syncPartner (if any) is also ready (default 0)
+         * @param {number} endStateRatio - ratio of the end state to be obtained from reference state: -1 (mirror) to 1 (default 1)
+         * @param {Vector3} movePOV - Mesh movement relative to its current position/rotation to be performed at the same time  (default null)
+         *                  right-up-forward
+         * @param {Vector3} rotatePOV - Incremental Mesh rotation to be performed at the same time  (default null)
+         *                  flipBack-twirlClockwise-tiltRight
+         * @param {Pace} pace - Any Object with the function: getCompletionMilestone(currentDurationRatio) (default Pace.LINEAR)
+         */
+        constructor(shapeKeyGroupName : string, endStateName : string, milliDuration : number, millisBefore : number, endStateRatio : number, movePOV : Vector3, rotatePOV : Vector3, pace : Pace){
+            super(shapeKeyGroupName, "BASIS", endStateName, milliDuration, millisBefore, endStateRatio, movePOV, rotatePOV, pace);
+        }   
+    }
+}

+ 72 - 0
Babylon/Mesh/Automaton/babylon.pace.ts

@@ -0,0 +1,72 @@
+module BABYLON {
+    /** 
+     *  Class used to coorelate duration ratio to completion ratio.  Enables Deformations to have
+     *  characteristics like accelation, deceleration, & linear.
+     */    
+    export class Pace {
+        // Constants
+        public static LINEAR = new Pace([1.0], [1.0]);
+
+        // Members
+        public steps : number; 
+        public incremetalCompletionBetweenSteps : Array<number>;
+        public incremetalDurationBetweenSteps   : Array<number>;
+
+        /**
+         * @immutable, reusable
+         * @param {Array} completionRatios - values from (> 0 to 1.0), not required to increase from left to right, for 'hicup' effects
+         * @param {Array} durationRatios - values from (> 0 to 1.0), MUST increase from left to right
+         */
+        constructor(public completionRatios : Array<number>, public durationRatios : Array<number>) {
+            // argument validations for JavaScript
+            if (!(completionRatios instanceof Array) || !(durationRatios instanceof Array)) throw "Pace: ratios not arrays";
+            if (completionRatios.length !== durationRatios.length) throw "Pace: ratio arrays not of equal length";
+
+            if (completionRatios.length === 0) throw "Pace: ratio arrays cannot be empty";
+        
+            var cRatio : number, dRatio : number, prevD : number = -1;
+            for (var i = 0; i < completionRatios.length; i++){
+                cRatio = completionRatios[i];
+                dRatio = durationRatios  [i];
+                if (cRatio <= 0 || dRatio <= 0) throw "Pace: ratios must be > 0";
+                if (cRatio >  1 || dRatio >  1) throw "Pace: ratios must be <= 1";
+                if (prevD >= dRatio) throw "Pace: durationRatios must be in increasing order";
+                prevD = dRatio;
+            }
+            if (cRatio !== 1 || dRatio !== 1) throw "Pace: final ratios must be 1";
+        
+            // public member assignment for all, since immutable
+            this.steps = completionRatios.length;        
+        
+            this.incremetalCompletionBetweenSteps = [completionRatios[0]]; // elements can be negative for 'hicups'
+            this.incremetalDurationBetweenSteps   = [durationRatios  [0]];
+            for (var i = 1; i < this.steps; i++){
+                this.incremetalCompletionBetweenSteps.push(completionRatios[i] - completionRatios[i - 1]);
+                this.incremetalDurationBetweenSteps  .push(durationRatios  [i] - durationRatios  [i - 1]);
+            }       
+            Object.freeze(this);  // make immutable
+        }
+    
+        /**
+         * Determine based on time since beginning,  return what should be ration of completion
+         * @param{number} currentDurationRatio - How much time has elapse / how long it is supposed to take
+         */
+        public getCompletionMilestone(currentDurationRatio : number) : number{
+            // breakout start & running late cases, no need to take into account later
+            if (currentDurationRatio <= 0) return 0;
+            else if (currentDurationRatio >= 1) return 1;
+        
+            var upperIdx = 0;  // ends up being an index into durationRatios, 1 greater than highest obtained
+            for (; upperIdx < this.steps; upperIdx++){
+                if (currentDurationRatio < this.durationRatios[upperIdx]) 
+                    break;
+            }
+
+            var baseCompletion = (upperIdx > 0) ? this.completionRatios[upperIdx - 1] : 0;        
+            var baseDuration   = (upperIdx > 0) ? this.durationRatios  [upperIdx - 1] : 0; 
+            var interStepRatio = (currentDurationRatio - baseDuration) / this.incremetalDurationBetweenSteps[upperIdx];
+        
+            return baseCompletion + (interStepRatio * this.incremetalCompletionBetweenSteps[upperIdx]);
+        }  
+    }    
+}

+ 160 - 0
Babylon/Mesh/Automaton/babylon.referenceDeformation.ts

@@ -0,0 +1,160 @@
+module BABYLON{
+   /**
+    * Class to store Deformation info & evaluate how complete it should be.
+    */
+    export class ReferenceDeformation {
+        private _syncPartner : ReferenceDeformation; // not part of constructor, since cannot be in both partners constructors, use setSyncPartner()
+
+        // time and state management members
+        private _startTime = -1;
+        private _currentDurationRatio = ReferenceDeformation._COMPLETE;
+        
+        // wallclock prorating members, used for acceleration / deceleration across AutomaonEventSeries runs
+        private _proratedMilliDuration : number;
+        private _proratedMillisBefore : number;
+
+        /**
+         * @param {string} shapeKeyGroupName -  Used by Automaton to place in the correct ShapeKeyGroup queue(s).
+         * @param {string} referenceStateName - Name of state key to be used as a reference, so that a endStateRatio can be used
+         * @param {string} endStateName - Name of state key to deform to
+         * @param {number} milliDuration - The number of milli seconds the deformation is to be completed in
+         * @param {number} millisBefore - Fixed wait period, once a syncPartner (if any) is also ready (default 0)
+         * @param {number} endStateRatio - ratio of the end state to be obtained from reference state: -1 (mirror) to 1 (default 1)
+         * @param {Vector3} movePOV - Mesh movement relative to its current position/rotation to be performed at the same time (default null)
+         *                  right-up-forward
+         * @param {Vector3} rotatePOV - Incremental Mesh rotation to be performed at the same time (default null)
+         *                  flipBack-twirlClockwise-tiltRight
+         * @param {Pace} pace - Any Object with the function: getCompletionMilestone(currentDurationRatio) (default Pace.LINEAR)
+         */
+        constructor(
+            public  shapeKeyGroupName   : string, 
+            private _referenceStateName : string, 
+            private _endStateName       : string, 
+            private _milliDuration      : number, 
+            private _millisBefore       : number = 0, 
+            private _endStateRatio      : number = 1, 
+            public  movePOV             : Vector3 = null, 
+            public  rotatePOV           : Vector3 = null,  
+            private _pace               : Pace = Pace.LINEAR)
+        {
+            // argument validations
+            if (this._referenceStateName === this._endStateName) throw "Deformation: reference state cannot be the same as the end state";
+            if (this._milliDuration <= 0) throw "Deformation: milliDuration must > 0";
+            if (this._millisBefore < 0) throw "Deformation: millisBefore cannot be negative";
+            if (this._endStateRatio < -1 || this._endStateRatio > 1) throw "Deformation: endStateRatio range  > -1 and < 1";
+
+            // mixed case group & state names not supported
+            this.shapeKeyGroupName   = this.shapeKeyGroupName  .toUpperCase(); 
+            this._referenceStateName = this._referenceStateName.toUpperCase();
+            this._endStateName       = this._endStateName      .toUpperCase();
+            
+            this.setProratedWallClocks(1); // ensure values actually used for timings are initialized
+        }
+        // =================================== run time processing ===================================    
+        /**
+         * Indicate readiness by caller to start processing event.  
+         * @param {number} lateStartMilli - indication of how far behind already 
+         */
+        public activate(lateStartMilli = 0) : void {
+            this._startTime = Automaton.now();
+            if (lateStartMilli > 0){
+                // apply 20% of the late start or 10% of duration which ever is less
+                lateStartMilli /= 5;
+                this._startTime -= (lateStartMilli < this._milliDuration / 10) ? lateStartMilli : this._milliDuration / 10;
+            }
+            this._currentDurationRatio = (this._syncPartner) ? ReferenceDeformation._BLOCKED : 
+                                         ((this._proratedMillisBefore > 0) ? ReferenceDeformation._WAITING : ReferenceDeformation._READY);
+        }
+    
+        /** called by ShapeKeyGroup.incrementallyDeform() to determine how much of the deformation should be performed right now */
+        public getCompletionMilestone() : number {
+            if (this._currentDurationRatio === ReferenceDeformation._COMPLETE){
+                return ReferenceDeformation._COMPLETE;
+            }
+
+            // BLOCK only occurs when there is a sync partner
+            if (this._currentDurationRatio === ReferenceDeformation._BLOCKED){                
+                // change both to WAITING & start clock, once both are BLOCKED
+                if (this._syncPartner.isBlocked() ){
+                    this._startTime = Automaton.now(); // reset the start clock
+                    this._currentDurationRatio = ReferenceDeformation._WAITING;
+                    this._syncPartner.syncReady(this._startTime);
+                }
+                else return ReferenceDeformation._BLOCKED;
+            }
+        
+            var millisSoFar = Automaton.now() - this._startTime;
+        
+            if (this._currentDurationRatio === ReferenceDeformation._WAITING){
+                millisSoFar -= this._proratedMillisBefore;
+                if (millisSoFar >= 0){
+                    this._startTime = Automaton.now() - millisSoFar;  // prorate start for time served   
+                }
+                else return ReferenceDeformation._WAITING;
+            }
+        
+            this._currentDurationRatio = millisSoFar / this._proratedMilliDuration;
+            if (this._currentDurationRatio > ReferenceDeformation._COMPLETE)
+                this._currentDurationRatio = ReferenceDeformation._COMPLETE;
+        
+            return this._pace.getCompletionMilestone(this._currentDurationRatio);
+        }
+       
+        /** support game pausing / resuming.  There is no need to actively pause a Deformation. */
+        public resumePlay() : void {
+            if (this._currentDurationRatio === ReferenceDeformation._COMPLETE ||
+                this._currentDurationRatio === ReferenceDeformation._BLOCKED  ||
+                this._currentDurationRatio === ReferenceDeformation._COMPLETE) return;
+            
+            // back into a start time which reflects the currentDurationRatio
+            this._startTime = Automaton.now() - (this._proratedMilliDuration * this._currentDurationRatio);            
+        }
+        // =================================== sync partner methods ===================================    
+        /**
+         * @param {Deformation} syncPartner - Deformation which should start at the same time as this one.  MUST be in a different shape key group!
+         */
+        public setSyncPartner(syncPartner : ReferenceDeformation) : void{
+            this._syncPartner = syncPartner;            
+        }
+        /** 
+         *  Called by the first of the syncPartners to detect that both are waiting for each other.
+         *  Only intended to be called from getCompletionMilestone() of the partner.
+         *  @param {number} startTime - passed from partner, so both are in sync as close as possible.
+         */
+        public syncReady(startTime : number) : void{
+            this._startTime = startTime;
+            this._currentDurationRatio = ReferenceDeformation._WAITING;
+        }
+        // ==================================== Getters & setters ====================================    
+        public isBlocked () : boolean { return this._currentDurationRatio === ReferenceDeformation._BLOCKED ; }
+        public isComplete() : boolean { return this._currentDurationRatio === ReferenceDeformation._COMPLETE; }
+       
+        public getReferenceStateName() : string { return this._referenceStateName; }     
+        public getEndStateName() : string { return this._endStateName; }     
+        public getMilliDuration() : number { return this._milliDuration; }      
+        public getMillisBefore() : number { return this._millisBefore; }     
+        public getEndStateRatio() :number {return this._endStateRatio; }
+        public getPace() : Pace {return this._pace; }
+        public getSyncPartner() : ReferenceDeformation{return this._syncPartner; }
+       
+        /**
+         * Called by the Automaton Event Series, before Deformation is passed to the ShapeKeyGroup.  This
+         * is to support acceleration / deceleration across event series repeats.
+         * @param {number} factor - amount to multiply the constructor supplied duration & time before by.
+         */
+        public setProratedWallClocks(factor : number) : void {
+            this._proratedMilliDuration = this._milliDuration * factor;
+            this._proratedMillisBefore = this._millisBefore * factor;
+        }
+        // ========================================== Enums  =========================================    
+        private static _BLOCKED  = -20;
+        private static _WAITING  = -10;
+        private static _READY    =   0;
+        private static _COMPLETE =   1;
+
+        public static get BLOCKED (): number { return ReferenceDeformation._BLOCKED ; }
+        public static get WAITING (): number { return ReferenceDeformation._WAITING ; }
+        public static get READY   (): number { return ReferenceDeformation._READY   ; }
+        public static get COMPLETE(): number { return ReferenceDeformation._COMPLETE; }
+    }
+}

+ 375 - 0
Babylon/Mesh/Automaton/babylon.shapeKeyGroup.ts

@@ -0,0 +1,375 @@
+module BABYLON {
+    export class ShapeKeyGroup {
+        // position elements converted to typed array
+        private _affectedPositionElements : Uint16Array;
+        private _nPosElements : number;
+        
+        // arrays for the storage of each state
+        private _states  = new Array<Float32Array>();
+        private _normals = new Array<Float32Array>();
+        private _stateNames = new Array<string>();      
+
+        // event series queue & reference vars for current seris & step within
+        private _queue = new Array<AutomatonEventSeries>();
+        private _currentSeries : AutomatonEventSeries = null;
+        private _currentStepInSeries : ReferenceDeformation = null;
+        private _endOfLastFrameTs = -1;
+
+        // affected vertices are used for normals, since all the entire vertex is involved, even if only the x of a position is affected
+        private _affectedVertices : Uint16Array;
+        private _nVertices;
+            
+        // reference vars for the current & prior deformation; assigned either an item of (_states / _normals) or one of the reusables
+        private _currFinalPositionVals  : Float32Array; 
+        private _priorFinalPositionVals : Float32Array;
+        private _currFinalNormalVals    : Float32Array;
+        private _priorFinalNormalVals   : Float32Array;
+        
+        // typed arrays are more expense to create, pre-allocate pairs for reuse
+        private _reusablePositionFinals = new Array<Float32Array>();  
+        private _reusableNormalFinals   = new Array<Float32Array>();  
+        private _lastReusablePosUsed  = 0;
+        private _lastReusableNormUsed = 0;
+        
+        // rotation control members
+        private _doingRotation = false;
+        private _rotationStartVec : Vector3;
+        private _rotationEndVec   : Vector3;
+        
+        // position control members
+        private _doingMovePOV = false;
+        private _positionStartVec : Vector3;  // for lerp(ing) when NOT also rotating too
+        private _positionEndVec   : Vector3;  // for lerp(ing) when NOT also rotating too
+        private _fullAmtRight     : number;   // for when also rotating
+        private _fullAmtUp        : number;   // for when also rotating
+        private _fullAmtForward   : number;   // for when also rotating
+        private _amtRightSoFar    : number;   // for when also rotating
+        private _amtUpSoFar       : number;   // for when also rotating
+        private _amtForwardSoFar  : number;   // for when also rotating
+        
+        // misc
+        private _activeLockedCamera : any = null; // any, or would require casting to FreeCamera & no point in JavaScript
+        private _mirrorAxis = -1; // when in use x = 1, y = 2, z = 3
+ 
+        /**
+         * @param {Automaton} _automaton - reference of Automaton this ShapeKeyGroup is a part of
+         * @param {String} _name - Name of the Key Group, upper case only
+         * @param {Array} affectedPositionElements - index of either an x, y, or z of positions.  Not all 3 of a vertex need be present.  Ascending order.
+         * @param {Array} basisState - original state of the affectedPositionElements of positions
+         */
+        constructor(private _automaton : Automaton, private _name : string, affectedPositionElements : Array<number>, basisState : Array<number>){
+            if (!(affectedPositionElements instanceof Array) || affectedPositionElements.length === 0                ) throw "ShapeKeyGroup: invalid affectedPositionElements arg";
+            if (!(basisState               instanceof Array) || basisState.length !== affectedPositionElements.length) throw "ShapeKeyGroup: invalid basisState arg";
+
+            // validation that position elements are in ascending order; normals relies on this being true
+            this._affectedPositionElements = new Uint16Array(affectedPositionElements);        
+            this._nPosElements = affectedPositionElements.length;
+            for (var i = 0; i + 1 < this._nPosElements; i++)
+                if (!(this._affectedPositionElements[i] < this._affectedPositionElements[i + 1])) throw "ShapeKeyGroup: affectedPositionElements must be in ascending order";
+            
+            // initialize 2 position reusables, the size needed
+            this._reusablePositionFinals.push(new Float32Array(this._nPosElements));
+            this._reusablePositionFinals.push(new Float32Array(this._nPosElements));
+            
+            // determine affectedVertices for updating cooresponding normals
+            var affectedVert = new Array<number>(); // final size unknown, so use a push-able array & convert to Uint16Array at end
+            var vertIdx  = -1;
+            var nextVertIdx : number;
+            
+            // go through each position element 
+            for (var i = 0; i < this._nPosElements; i++){
+                // the vertex index is 1/3 the position element index
+                nextVertIdx = Math.floor(this._affectedPositionElements[i] / 3);
+                
+                // since position element indexes in ascending order, check if vertex not already added by the x, or y elements
+                if (vertIdx !== nextVertIdx){
+                    vertIdx = nextVertIdx;
+                    affectedVert.push(vertIdx);
+                }
+            }
+            this._affectedVertices = new Uint16Array(affectedVert);
+            this._nVertices = this._affectedVertices.length;
+            
+            // initialize 2 normal reusables, the size needed
+            this._reusableNormalFinals.push(new Float32Array(this._nVertices * 3));
+            this._reusableNormalFinals.push(new Float32Array(this._nVertices * 3));               
+        
+            // push 'BASIS' to _states & _stateNames, then initialize _currFinalVals to 'BASIS' state
+            this.addShapeKey("BASIS", basisState);
+            this._currFinalPositionVals  = this._states [0];       
+            this._currFinalNormalVals    = this._normals[0];   
+        }
+        // =============================== Shape-Key adding & deriving ===============================
+        private getDerivedName(referenceIdx : number, endStateIdx : number, endStateRatio : number) : string{
+            return referenceIdx + "-" + endStateIdx + "@" + endStateRatio;
+        }
+        /**
+         * add a derived key from the data contained in a deformation; wrapper for addDerivedKey()
+         * @param {ReferenceDeformation} deformation - mined for its reference & end state names, and end state ratio
+         */
+        public addDerivedKeyFromDeformation(deformation : ReferenceDeformation) : void{
+            this.addDerivedKey(deformation.getReferenceStateName(), deformation.getEndStateName(), deformation.getEndStateRatio());
+        }
+        
+        /**
+         * add a derived key from the arguments
+         * @param {string} referenceStateName - Name of the reference state to be based on
+         * @param {string} endStateName - Name of the end state to be based on
+         * @param {number} endStateRatio - Unvalidated, but if -1 < or > 1, then can never be called, since Deformation validates
+         */
+        public addDerivedKey(referenceStateName : string, endStateName : string, endStateRatio : number) : void{
+            var referenceIdx = this.getIdxForState(referenceStateName.toUpperCase());
+            var endStateIdx  = this.getIdxForState(endStateName      .toUpperCase());
+            if (referenceIdx === -1 || endStateIdx === -1) throw "ShapeKeyGroup: invalid source state name(s)";
+            if (endStateRatio === 1) throw "ShapeKeyGroup: deriving a shape key where the endStateRatio is 1 is pointless";
+            
+            var stateName = this.getDerivedName(referenceIdx, endStateIdx, endStateRatio);
+            var stateKey  = new Float32Array(this._nPosElements);
+            this.buildPosEndPoint(stateKey, referenceIdx, endStateIdx, endStateRatio);
+            this.addShapeKeyInternal(stateName, stateKey);
+        }
+        
+        /** called in construction code from TOB, but outside the constructor, except for 'BASIS'.  Unlikely to be called by application code. */
+        public addShapeKey(stateName : string, stateKey : Array<number>) : void {
+            if (!(stateKey instanceof Array) || stateKey.length !== this._nPosElements) throw "ShapeKeyGroup: invalid stateKey arg";
+            this.addShapeKeyInternal(stateName, new Float32Array(stateKey) );
+        }
+
+        /** worker method for both the addShapeKey() & addDerivedKey() methods */
+        private addShapeKeyInternal(stateName : string, stateKey : Float32Array) : void {
+            if (typeof stateName !== 'string' || stateName.length === 0) throw "ShapeKeyGroup: invalid stateName arg";
+            if (this.getIdxForState(stateName) !== -1) throw "ShapeKeyGroup: stateName " + stateName + " is a duplicate";
+
+            this._states.push(stateKey);
+            this._stateNames.push(stateName);
+                                
+            var coorespondingNormals = new Float32Array(this._nVertices * 3);
+            this.buildNormEndPoint(coorespondingNormals, stateKey);
+            this._normals.push(coorespondingNormals);
+
+            if (this._automaton.debug) console.log("Shape key: " + stateName + " added to group: " + this._name + " on Automaton: " + this._automaton.name);
+        }
+        // =================================== inside before render ==================================
+        /**
+         * Called by the beforeRender() registered by this._automaton
+         * @param {Float32Array} positions - Array of the positions for the entire mesh, portion updated based on _affectedIndices
+         * @param {Float32Array } normals  - Array of the normals for the entire mesh, if not null, portion updated based on _affectedVertices
+         */
+        public incrementallyDeform(positions : Float32Array, normals :Float32Array) : boolean {
+            // series level of processing; get another series from the queue when none or last is done
+            if (this._currentSeries === null || !this._currentSeries.hasMoreEvents() ){
+                if (! this._nextEventSeries()) return false;
+            }
+            
+            // ok, have an active event series, now get the next deformation in series if required
+            while (this._currentStepInSeries === null || this._currentStepInSeries.isComplete() ){
+                var next : any = this._currentSeries.nextEvent(this._name);
+                
+                if (next === null) return false; // being blocked, this must be a multi-group series, not ready for us
+                if (next instanceof Action){
+                    (<Action> next).execute(ActionEvent.CreateNew(this._automaton));    
+                }
+                else if (typeof next === "function"){
+                    next.call();
+                }
+                else{
+                   this._nextDeformation(<ReferenceDeformation> next);  // must be a new deformation. _currentStepInSeries assigned if valid
+                }  
+            }
+            
+            // have a deformation to process
+            var ratioComplete = this._currentStepInSeries.getCompletionMilestone();
+            if (ratioComplete < 0) return false; // Deformation.BLOCKED or Deformation.WAITING
+        
+            // update the positions
+            for (var i = 0; i < this._nPosElements; i++){
+                positions[this._affectedPositionElements[i]] = this._priorFinalPositionVals[i] + ((this._currFinalPositionVals[i] - this._priorFinalPositionVals[i]) * ratioComplete);
+            }
+            
+            // update the normals
+            var mIdx : number, kIdx : number;
+            for (var i = 0; i < this._nVertices; i++){
+                mIdx = 3 * this._affectedVertices[i] // offset for this vertex in the entire mesh
+                kIdx = 3 * i;                        // offset for this vertex in the shape key group
+                normals[mIdx    ] = this._priorFinalNormalVals[kIdx    ] + ((this._currFinalNormalVals[kIdx    ] - this._priorFinalNormalVals[kIdx    ]) * ratioComplete);
+                normals[mIdx + 1] = this._priorFinalNormalVals[kIdx + 1] + ((this._currFinalNormalVals[kIdx + 1] - this._priorFinalNormalVals[kIdx + 1]) * ratioComplete);
+                normals[mIdx + 2] = this._priorFinalNormalVals[kIdx + 2] + ((this._currFinalNormalVals[kIdx + 2] - this._priorFinalNormalVals[kIdx + 2]) * ratioComplete);
+            }
+            
+            if (this._doingRotation){
+                this._automaton.rotation = BABYLON.Vector3.Lerp(this._rotationStartVec, this._rotationEndVec, ratioComplete);
+            }
+            
+            if (this._doingMovePOV === true){
+                if (this._doingRotation){
+                    // some of these amounts, could be negative, if has a Pace with a hiccup
+                    var amtRight   = (this._fullAmtRight   * ratioComplete) - this._amtRightSoFar;
+                    var amtUp      = (this._fullAmtUp      * ratioComplete) - this._amtUpSoFar;
+                    var amtForward = (this._fullAmtForward * ratioComplete) - this._amtForwardSoFar;
+                    
+                    this._automaton.movePOV(amtRight, amtUp, amtForward);
+                    
+                    this._amtRightSoFar   += amtRight;
+                    this._amtUpSoFar      += amtUp;
+                    this._amtForwardSoFar += amtForward;
+                }else{
+                    this._automaton.position = BABYLON.Vector3.Lerp(this._positionStartVec, this._positionEndVec, ratioComplete);
+                }
+                
+                if (this._activeLockedCamera !== null) this._activeLockedCamera._getViewMatrix();
+            }
+            this._endOfLastFrameTs = Automaton.now();       
+            return true;
+        }
+       
+        public resumePlay() : void {
+            if (this._currentStepInSeries !== null) this._currentStepInSeries.resumePlay();
+        }
+        // ============================ Event Series Queueing & retrieval ============================
+        public queueEventSeries(eSeries : AutomatonEventSeries) :void {
+            this._queue.push(eSeries);
+        }
+    
+        private _nextEventSeries() : boolean {
+            var ret = this._queue.length > 0;
+            if (ret){
+                this._currentSeries = this._queue.shift();
+                this._currentSeries.activate(this._name);
+            }
+            return ret;
+        }
+        // ===================================== deformation prep ====================================    
+        private _nextDeformation(deformation : ReferenceDeformation) : void {
+            // do this as soon as possible to get the clock started, retroactively, when sole group in the series, and within 50 millis of last deform
+            var lateStart = Automaton.now() - this._endOfLastFrameTs;
+            deformation.activate((this._currentSeries.nGroups === 1 && lateStart - this._endOfLastFrameTs < 50) ? lateStart : 0);
+            
+            this._currentStepInSeries = deformation;
+            this._priorFinalPositionVals = this._currFinalPositionVals;
+            this._priorFinalNormalVals   = this._currFinalNormalVals  ;
+            
+            var referenceIdx = this.getIdxForState(deformation.getReferenceStateName() );
+            var endStateIdx  = this.getIdxForState(deformation.getEndStateName      () );
+            if (referenceIdx === -1 || endStateIdx === -1) throw "ShapeKeyGroup " + this._name + ": invalid deformation, source state name(s) not found";
+
+            var endStateRatio = deformation.getEndStateRatio();
+            if (endStateRatio < 0 && this._mirrorAxis === -1) throw "ShapeKeyGroup " + this._name + ": invalid deformation, negative end state ratios when not mirroring";
+           
+            // when endStateRatio is 1 or 0, just assign _currFinalVals directly from _states
+            if (endStateRatio === 1 || endStateRatio === 0){
+                 if (endStateRatio === 0) endStateIdx = referenceIdx; // really just the reference when 0
+                 this._currFinalPositionVals = this._states [endStateIdx];
+                 this._currFinalNormalVals   = this._normals[endStateIdx];
+            }else{
+                // check there was not a pre-built derived key to assign
+                var derivedIdx = this.getIdxForState(this.getDerivedName(referenceIdx, endStateIdx, endStateRatio) );
+                if (derivedIdx !== -1){
+                    this._currFinalPositionVals = this._states [derivedIdx];
+                    this._currFinalNormalVals   = this._normals[derivedIdx];
+                } else{
+                    // need to build _currFinalVals, toggling the _lastReusableUsed
+                    this._lastReusablePosUsed = (this._lastReusablePosUsed === 1) ? 0 : 1;
+                    this.buildPosEndPoint(this._reusablePositionFinals[this._lastReusablePosUsed], referenceIdx, endStateIdx, endStateRatio, this._automaton.debug);
+                    this._currFinalPositionVals = this._reusablePositionFinals[this._lastReusablePosUsed];
+                    
+                    // need to build _currFinalNormalVals, toggling the _lastReusableUsed
+                    this._lastReusableNormUsed = (this._lastReusableNormUsed === 1) ? 0 : 1;
+                    this.buildNormEndPoint(this._reusableNormalFinals[this._lastReusableNormUsed], this._currFinalPositionVals);
+                    this._currFinalNormalVals = this._reusableNormalFinals[this._lastReusableNormUsed];
+                }
+            }
+
+            // prepare for rotation, if deformation calls for
+            this._doingRotation = deformation.rotatePOV !== null;
+            if (this._doingRotation){
+                this._rotationStartVec = this._automaton.rotation; // no clone required, since Lerp() returns a new Vec3 written over .rotation
+                this._rotationEndVec   = this._rotationStartVec.add(this._automaton.calcRotatePOV(deformation.rotatePOV.x, deformation.rotatePOV.y, deformation.rotatePOV.z));
+            }
+            
+            // prepare for POV move, if deformation calls for
+            this._doingMovePOV = deformation.movePOV !== null;
+            if (this._doingMovePOV){
+                this._fullAmtRight   = deformation.movePOV.x; this._amtRightSoFar   = 0;
+                this._fullAmtUp      = deformation.movePOV.y; this._amtUpSoFar      = 0;
+                this._fullAmtForward = deformation.movePOV.z; this._amtForwardSoFar = 0;
+                
+                // less resources to calcMovePOV() once then Lerp(), but calcMovePOV() uses rotation, so can only go fast when not rotating too
+                if (!this._doingRotation){
+                    this._positionStartVec = this._automaton.position; // no clone required, since Lerp() returns a new Vec3 written over .position
+                    this._positionEndVec   = this._positionStartVec.add(this._automaton.calcMovePOV(this._fullAmtRight, this._fullAmtUp, this._fullAmtForward));
+                }
+            }
+            
+            // determine if camera needs to be woke up for tracking
+            this._activeLockedCamera = null; // assigned for failure
+            
+            if (this._doingRotation || this._doingMovePOV){
+                var activeCamera = <any> this._automaton.getScene().activeCamera;
+                if(activeCamera.lockedTarget && activeCamera.lockedTarget === this._automaton)
+                     this._activeLockedCamera = activeCamera;
+            }
+        }
+        /**
+         * Called by addShapeKeyInternal() & _nextDeformation() to build the positions for an end point
+         * @param {Float32Array} targetArray - location of output. One of the _reusablePositionFinals for _nextDeformation().  Bound for: _states[], if addShapeKeyInternal().
+         * @param {number} referenceIdx - the index into _states[] to use as a reference
+         * @param {number} endStateIdx - the index into _states[] to use as a target
+         * @param {number} endStateRatio - the ratio of the target state to achive, relative to the reference state
+         * @param {boolean} log - write console message of action, when true (Default false)
+         * 
+         */
+        private buildPosEndPoint(targetArray : Float32Array, referenceIdx: number, endStateIdx : number, endStateRatio : number, log = false) : void {            
+            var refEndState = this._states[referenceIdx];
+            var newEndState = this._states[endStateIdx];
+            
+            // compute each of the new final values of positions
+            var deltaToRefState : number;
+            for (var i = 0; i < this._nPosElements; i++){
+                deltaToRefState = (newEndState[i] - refEndState[i]) * endStateRatio;
+                
+                // reverse sign on appropriate elements of referenceDelta when ratio neg & mirroring
+                if (endStateRatio < 0 && this._mirrorAxis !== (i + 1) % 3){
+                    deltaToRefState *= -1;
+                }            
+                targetArray[i] = refEndState[i] + deltaToRefState;            
+            }
+            if (log) console.log(this._name + " end Point built for referenceIdx: " + referenceIdx + ",  endStateIdx: " + endStateIdx + ", endStateRatio: " + endStateRatio);
+        }
+        
+        /**
+         * Called by addShapeKeyInternal() & _nextDeformation() to build the normals for an end point
+         * @param {Float32Array} targetArray - location of output. One of the _reusableNormalFinals for _nextDeformation().  Bound for: _normals[], if addShapeKeyInternal().
+         * @param {Float32Array} endStatePos - postion data to build the normals for.  Output from buildPosEndPoint, or data passed in from addShapeKey()
+         */
+        private buildNormEndPoint(targetArray : Float32Array, endStatePos : Float32Array) : void {
+            // build a full, mesh sized, set of positions & populate with the left-over initial data 
+            var futurePos = new Float32Array(this._automaton.getVerticesData(VertexBuffer.PositionKind));
+            
+            // populate the changes that this state has
+            for (var i = 0; i < this._nPosElements; i++){
+                futurePos[this._affectedPositionElements[i]] = endStatePos[i];
+            }
+            
+            // compute using method in _automaton
+            this._automaton.normalsforVerticesInPlace(this._affectedVertices, targetArray, futurePos);
+        }
+        // ==================================== Getters & setters ====================================    
+        private getIdxForState(stateName : string) : number{
+            for (var i = this._stateNames.length - 1; i >= 0; i--){
+                if (this._stateNames[i] === stateName){
+                    return i;
+                }
+            }
+            return -1;
+        }
+
+        public getName() : string { return this._name; }
+        public getNPosElements() : number { return this._nPosElements; }
+        public getNStates() : number { return this._stateNames.length; }
+        public toString() : string { return 'ShapeKeyGroup: ' + this._name + ', n position elements: ' + this._nPosElements + ',\nStates: ' + this._stateNames; }
+       
+        public mirrorAxisOnX() : void {this._mirrorAxis = 1;}
+        public mirrorAxisOnY() : void {this._mirrorAxis = 2;}
+        public mirrorAxisOnZ() : void {this._mirrorAxis = 3;}
+    }
+}

+ 6 - 0
Tools/Gulp/gulpfile.js

@@ -82,6 +82,11 @@ gulp.task('scripts', ['shaders'] ,function() {
       '../../Babylon/Mesh/babylon.InstancedMesh.js',
       '../../Babylon/Mesh/babylon.mesh.js',
       '../../Babylon/Mesh/babylon.subMesh.js',
+      '../../Babylon/Mesh/Automaton/babylon.automaton.js',
+      '../../Babylon/Mesh/Automaton/babylon.referenceDeformation.js',
+      '../../Babylon/Mesh/Automaton/babylon.deformation.js',
+      '../../Babylon/Mesh/Automaton/babylon.pace.js',
+      '../../Babylon/Mesh/Automaton/babylon.shapeKeyGroup.js',
       '../../Babylon/Materials/textures/babylon.baseTexture.js',
       '../../Babylon/Materials/textures/babylon.texture.js',
       '../../Babylon/Materials/textures/babylon.cubeTexture.js',
@@ -145,6 +150,7 @@ gulp.task('scripts', ['shaders'] ,function() {
       '../../Babylon/Actions/babylon.actionManager.js',
       '../../Babylon/Actions/babylon.interpolateValueAction.js',
       '../../Babylon/Actions/babylon.directActions.js',
+      '../../Babylon/Mesh/Automaton/babylon.automatonEventSeries.js',
       '../../Babylon/Mesh/babylon.geometry.js',
       '../../Babylon/Mesh/babylon.groundMesh.js',
       '../../Babylon/Mesh/babylon.instancedMesh.js',