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(); private _normals = new Array(); private _stateNames = new Array(); // event series queue & reference vars for current seris & step within private _queue = new Array(); 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(); private _reusableNormalFinals = new Array(); 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, basisState : Array){ 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(); // 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) : 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){ ( next).execute(ActionEvent.CreateNew(this._automaton)); } else if (typeof next === "function"){ next.call(); } else{ this._nextDeformation( 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 = 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;} } }