UNPKG

@awayjs/graphics

Version:
616 lines (528 loc) 21.8 kB
import { Quaternion, Vector3D } from '@awayjs/core'; import { ShaderBase, _Render_RenderableBase, ElementsEvent, IElements, TriangleElements } from '@awayjs/renderer'; import { _Render_Shape } from '../renderables/Shape'; import { AnimationStateEvent } from '../events/AnimationStateEvent'; import { JointPose } from './data/JointPose'; import { Skeleton } from './data/Skeleton'; import { SkeletonJoint } from './data/SkeletonJoint'; import { SkeletonPose } from './data/SkeletonPose'; import { ISkeletonAnimationState } from './states/ISkeletonAnimationState'; import { IAnimationTransition } from './transitions/IAnimationTransition'; import { SkeletonAnimationSet } from './SkeletonAnimationSet'; import { AnimatorBase } from './AnimatorBase'; /** * Provides an interface for assigning skeleton-based animation data sets to sprite-based entity objects * and controlling the various available states of animation through an interative playhead that can be * automatically updated or manually triggered. */ export class SkeletonAnimator extends AnimatorBase { private _globalMatrices: Float32Array; private _globalPose: SkeletonPose = new SkeletonPose(); private _globalPropertiesDirty: boolean; private _numJoints: number; private _morphedElements: Object = new Object(); private _morphedElementsDirty: Object = new Object(); private _condensedMatrices: Float32Array; private _skeletonAnimationSet: SkeletonAnimationSet; private _skeleton: Skeleton; private _forceCPU: boolean; private _useCondensedIndices: boolean; private _jointsPerVertex: number; private _activeSkeletonState: ISkeletonAnimationState; private _onTransitionCompleteDelegate: (event: AnimationStateEvent) => void; private _onIndicesUpdateDelegate: (event: ElementsEvent) => void; private _onVerticesUpdateDelegate: (event: ElementsEvent) => void; /** * returns the calculated global matrices of the current skeleton pose. * * @see #globalPose */ public get globalMatrices(): Float32Array { if (this._globalPropertiesDirty) this.updateGlobalProperties(); return this._globalMatrices; } /** * returns the current skeleton pose output from the animator. * * @see away.animators.data.SkeletonPose */ public get globalPose(): SkeletonPose { if (this._globalPropertiesDirty) this.updateGlobalProperties(); return this._globalPose; } /** * Returns the skeleton object in use by the animator - this defines the number and heirarchy of joints used by the * skinned geoemtry to which skeleon animator is applied. */ public get skeleton(): Skeleton { return this._skeleton; } /** * Indicates whether the skeleton animator is disabled by default for GPU rendering, something that allows the animator to perform calculation on the GPU. * Defaults to false. */ public get forceCPU(): boolean { return this._forceCPU; } /** * Offers the option of enabling GPU accelerated animation on skeletons larger than 32 joints * by condensing the number of joint index values required per sprite. Only applicable to * skeleton animations that utilise more than one sprite object. Defaults to false. */ public get useCondensedIndices(): boolean { return this._useCondensedIndices; } public set useCondensedIndices(value: boolean) { this._useCondensedIndices = value; } /** * Creates a new <code>SkeletonAnimator</code> object. * * @param skeletonAnimationSet The animation data set containing the skeleton animations used by the animator. * @param skeleton The skeleton object used for calculating the resulting global matrices for transforming skinned sprite data. * @param forceCPU Optional value that only allows the animator to perform calculation on the CPU. Defaults to false. */ constructor(animationSet: SkeletonAnimationSet, skeleton: Skeleton, forceCPU: boolean = false) { super(animationSet); this._skeletonAnimationSet = animationSet; this._skeleton = skeleton; this._forceCPU = forceCPU; this._jointsPerVertex = animationSet.jointsPerVertex; this._numJoints = this._skeleton.numJoints; this._globalMatrices = new Float32Array(this._numJoints * 12); let j: number = 0; for (let i: number = 0; i < this._numJoints; ++i) { this._globalMatrices[j++] = 1; this._globalMatrices[j++] = 0; this._globalMatrices[j++] = 0; this._globalMatrices[j++] = 0; this._globalMatrices[j++] = 0; this._globalMatrices[j++] = 1; this._globalMatrices[j++] = 0; this._globalMatrices[j++] = 0; this._globalMatrices[j++] = 0; this._globalMatrices[j++] = 0; this._globalMatrices[j++] = 1; this._globalMatrices[j++] = 0; } this._onTransitionCompleteDelegate = (event: AnimationStateEvent) => this.onTransitionComplete(event); this._onIndicesUpdateDelegate = (event: ElementsEvent) => this.onIndicesUpdate(event); this._onVerticesUpdateDelegate = (event: ElementsEvent) => this.onVerticesUpdate(event); } /** * @inheritDoc */ public clone(): AnimatorBase { return new SkeletonAnimator(this._skeletonAnimationSet, this._skeleton, this._forceCPU); } /** * Plays an animation state registered with the given name in the animation data set. * * @param name The data set name of the animation state to be played. * @param transition An optional transition object that determines how the animator will transition from the currently active animation state. * @param offset An option offset time (in milliseconds) that resets the state's internal clock to the absolute time of the animator plus the offset value. Required for non-looping animation states. */ public play(name: string, transition: IAnimationTransition = null, offset: number = NaN): void { if (this._pActiveAnimationName == name) return; this._pActiveAnimationName = name; if (!this._pAnimationSet.hasAnimation(name)) throw new Error('Animation root node ' + name + ' not found!'); if (transition && this._pActiveNode) { //setup the transition this._pActiveNode = transition.getAnimationNode(this, this._pActiveNode, this._pAnimationSet.getAnimation(name), this._pAbsoluteTime); this._pActiveNode.addEventListener(AnimationStateEvent.TRANSITION_COMPLETE, this._onTransitionCompleteDelegate); } else this._pActiveNode = this._pAnimationSet.getAnimation(name); this._pActiveState = this.getAnimationState(this._pActiveNode); if (this.updatePosition) { //update straight away to reset position deltas this._pActiveState.update(this._pAbsoluteTime); this._pActiveState.positionDelta; } this._activeSkeletonState = <ISkeletonAnimationState> this._pActiveState; this.start(); //apply a time offset if specified if (!isNaN(offset)) this.reset(name, offset); } /** * @inheritDoc */ public setRenderState(shader: ShaderBase, renderable: _Render_Shape): void { // do on request of globalProperties if (this._globalPropertiesDirty) this.updateGlobalProperties(); const elements: TriangleElements = <TriangleElements> renderable.shape.elements; elements.useCondensedIndices = this._useCondensedIndices; if (this._useCondensedIndices) { // using a condensed data set this.updateCondensedMatrices(elements.condensedIndexLookUp); shader.setVertexConstFromArray(this._skeletonAnimationSet.matricesIndex, this._condensedMatrices); } else { if (this._pAnimationSet.usesCPU) { if (this._morphedElementsDirty[elements.id]) this.morphElements(renderable, elements); return; } shader.setVertexConstFromArray(this._skeletonAnimationSet.matricesIndex, this._globalMatrices); } } /** * @inheritDoc */ public testGPUCompatibility(shader: ShaderBase): void { if (!this._useCondensedIndices && (this._forceCPU || this._jointsPerVertex > 4 || shader.numUsedVertexConstants + this._numJoints * 3 > 128)) this._pAnimationSet.cancelGPUCompatibility(); } /** * Applies the calculated time delta to the active animation state node or state transition object. */ public _pUpdateDeltaTime(dt: number): void { super._pUpdateDeltaTime(dt); //invalidate pose matrices this._globalPropertiesDirty = true; //trigger geometry invalidation if using CPU animation if (this._pAnimationSet.usesCPU) this.invalidateElements(); } private updateCondensedMatrices(condensedIndexLookUp: Array<number>): void { let j: number = 0, k: number = 0; const len: number = condensedIndexLookUp.length; let srcIndex: number; this._condensedMatrices = new Float32Array(len * 12); for (let i: number = 0; i < len; i++) { srcIndex = condensedIndexLookUp[i] * 12; //12 required for the three 4-component vectors that store the matrix k = 12; // copy into condensed while (k--) this._condensedMatrices[j++] = this._globalMatrices[srcIndex++]; } } private updateGlobalProperties(): void { this._globalPropertiesDirty = false; //get global pose this.localToGlobalPose(this._activeSkeletonState.getSkeletonPose(this._skeleton), this._globalPose, this._skeleton); // convert pose to matrix let mtxOffset: number = 0; const globalPoses: Array<JointPose> = this._globalPose.jointPoses; let raw: Float32Array; let ox: number, oy: number, oz: number, ow: number; let xy2: number, xz2: number, xw2: number; let yz2: number, yw2: number, zw2: number; let n11: number, n12: number, n13: number; let n21: number, n22: number, n23: number; let n31: number, n32: number, n33: number; let m11: number, m12: number, m13: number, m14: number; let m21: number, m22: number, m23: number, m24: number; let m31: number, m32: number, m33: number, m34: number; const joints: Array<SkeletonJoint> = this._skeleton.joints; let pose: JointPose; let quat: Quaternion; let vec: Vector3D; let t: number; for (let i: number = 0; i < this._numJoints; ++i) { pose = globalPoses[i]; quat = pose.orientation; vec = pose.translation; ox = quat.x; oy = quat.y; oz = quat.z; ow = quat.w; xy2 = (t = 2.0 * ox) * oy; xz2 = t * oz; xw2 = t * ow; yz2 = (t = 2.0 * oy) * oz; yw2 = t * ow; zw2 = 2.0 * oz * ow; yz2 = 2.0 * oy * oz; yw2 = 2.0 * oy * ow; zw2 = 2.0 * oz * ow; ox *= ox; oy *= oy; oz *= oz; ow *= ow; n11 = (t = ox - oy) - oz + ow; n12 = xy2 - zw2; n13 = xz2 + yw2; n21 = xy2 + zw2; n22 = -t - oz + ow; n23 = yz2 - xw2; n31 = xz2 - yw2; n32 = yz2 + xw2; n33 = -ox - oy + oz + ow; // prepend inverse bind pose raw = joints[i].inverseBindPose; m11 = raw[0]; m12 = raw[4]; m13 = raw[8]; m14 = raw[12]; m21 = raw[1]; m22 = raw[5]; m23 = raw[9]; m24 = raw[13]; m31 = raw[2]; m32 = raw[6]; m33 = raw[10]; m34 = raw[14]; this._globalMatrices[mtxOffset] = n11 * m11 + n12 * m21 + n13 * m31; this._globalMatrices[mtxOffset + 1] = n11 * m12 + n12 * m22 + n13 * m32; this._globalMatrices[mtxOffset + 2] = n11 * m13 + n12 * m23 + n13 * m33; this._globalMatrices[mtxOffset + 3] = n11 * m14 + n12 * m24 + n13 * m34 + vec.x; this._globalMatrices[mtxOffset + 4] = n21 * m11 + n22 * m21 + n23 * m31; this._globalMatrices[mtxOffset + 5] = n21 * m12 + n22 * m22 + n23 * m32; this._globalMatrices[mtxOffset + 6] = n21 * m13 + n22 * m23 + n23 * m33; this._globalMatrices[mtxOffset + 7] = n21 * m14 + n22 * m24 + n23 * m34 + vec.y; this._globalMatrices[mtxOffset + 8] = n31 * m11 + n32 * m21 + n33 * m31; this._globalMatrices[mtxOffset + 9] = n31 * m12 + n32 * m22 + n33 * m32; this._globalMatrices[mtxOffset + 10] = n31 * m13 + n32 * m23 + n33 * m33; this._globalMatrices[mtxOffset + 11] = n31 * m14 + n32 * m24 + n33 * m34 + vec.z; mtxOffset = mtxOffset + 12; } } public getRenderableElements(renderable: _Render_RenderableBase, sourceElements: TriangleElements): IElements { this._morphedElementsDirty[sourceElements.id] = true; //early out for GPU animations if (!this._pAnimationSet.usesCPU) return sourceElements; let targetElements: TriangleElements; if (!(targetElements = this._morphedElements[sourceElements.id])) { //not yet stored sourceElements.normals; sourceElements.tangents; targetElements = <TriangleElements> (this._morphedElements[sourceElements.id] = sourceElements.clone()); //turn off auto calculations on the morphed geometry targetElements.autoDeriveNormals = false; targetElements.autoDeriveTangents = false; //add event listeners for any changes in UV values on the source geometry sourceElements.addEventListener(ElementsEvent.INVALIDATE_INDICES, this._onIndicesUpdateDelegate); sourceElements.addEventListener(ElementsEvent.INVALIDATE_VERTICES, this._onVerticesUpdateDelegate); } return targetElements; } /** * If the animation can't be performed on GPU, transform vertices manually * @param subGeom The subgeometry containing the weights and joint index data per vertex. * @param pass The material pass for which we need to transform the vertices */ public morphElements(renderable: _Render_RenderableBase, sourceElements: TriangleElements): void { this._morphedElementsDirty[sourceElements.id] = false; const numVertices: number = sourceElements.numVertices; const sourcePositions: ArrayBufferView = sourceElements.positions.get(numVertices); const sourceNormals: Float32Array = sourceElements.normals.get(numVertices); const sourceTangents: Float32Array = sourceElements.tangents.get(numVertices); const posDim: number = sourceElements.positions.dimensions; const posStride: number = sourceElements.positions.stride; const normalStride: number = sourceElements.normals.stride; const tangentStride: number = sourceElements.tangents.stride; const jointIndices: Float32Array = <Float32Array> sourceElements.jointIndices.get(numVertices); const jointWeights: Float32Array = <Float32Array> sourceElements.jointWeights.get(numVertices); const jointStride: number = sourceElements.jointIndices.stride; const targetElements: TriangleElements = this._morphedElements[sourceElements.id]; const targetPositions: ArrayBufferView = targetElements.positions.get(numVertices); const targetNormals: Float32Array = targetElements.normals.get(numVertices); const targetTangents: Float32Array = targetElements.tangents.get(numVertices); targetElements.positions.attributesBuffer.invalidate(); targetElements.normals.attributesBuffer.invalidate(); targetElements.tangents.attributesBuffer.invalidate(); let index: number = 0; let i0: number = 0; let i1: number = 0; let i2: number = 0; let i3: number = 0; let k: number; let vx: number, vy: number, vz: number; let nx: number, ny: number, nz: number; let tx: number, ty: number, tz: number; let weight: number; let vertX: number, vertY: number, vertZ: number; let normX: number, normY: number, normZ: number; let tangX: number, tangY: number, tangZ: number; let m11: number, m12: number, m13: number, m14: number; let m21: number, m22: number, m23: number, m24: number; let m31: number, m32: number, m33: number, m34: number; while (index < numVertices) { i0 = index * posStride; vertX = sourcePositions[i0]; vertY = sourcePositions[i0 + 1]; vertZ = (posDim == 3) ? sourcePositions[i0 + 2] : 0; i1 = index * normalStride; normX = sourceNormals[i1]; normY = sourceNormals[i1 + 1]; normZ = sourceNormals[i1 + 2]; i2 = index * tangentStride; tangX = sourceTangents[i2]; tangY = sourceTangents[i2 + 1]; tangZ = sourceTangents[i2 + 2]; vx = 0; vy = 0; vz = 0; nx = 0; ny = 0; nz = 0; tx = 0; ty = 0; tz = 0; k = 0; i3 = index * jointStride; while (k < this._jointsPerVertex) { weight = jointWeights[i3 + k]; if (weight > 0) { // implicit /3*12 (/3 because indices are multiplied by 3 for gpu matrix access, *12 because it's the matrix size) const mtxOffset: number = jointIndices[i3 + k] << 2; m11 = this._globalMatrices[mtxOffset]; m12 = this._globalMatrices[mtxOffset + 1]; m13 = this._globalMatrices[mtxOffset + 2]; m14 = this._globalMatrices[mtxOffset + 3]; m21 = this._globalMatrices[mtxOffset + 4]; m22 = this._globalMatrices[mtxOffset + 5]; m23 = this._globalMatrices[mtxOffset + 6]; m24 = this._globalMatrices[mtxOffset + 7]; m31 = this._globalMatrices[mtxOffset + 8]; m32 = this._globalMatrices[mtxOffset + 9]; m33 = this._globalMatrices[mtxOffset + 10]; m34 = this._globalMatrices[mtxOffset + 11]; vx += weight * (m11 * vertX + m12 * vertY + m13 * vertZ + m14); vy += weight * (m21 * vertX + m22 * vertY + m23 * vertZ + m24); vz += weight * (m31 * vertX + m32 * vertY + m33 * vertZ + m34); nx += weight * (m11 * normX + m12 * normY + m13 * normZ); ny += weight * (m21 * normX + m22 * normY + m23 * normZ); nz += weight * (m31 * normX + m32 * normY + m33 * normZ); tx += weight * (m11 * tangX + m12 * tangY + m13 * tangZ); ty += weight * (m21 * tangX + m22 * tangY + m23 * tangZ); tz += weight * (m31 * tangX + m32 * tangY + m33 * tangZ); k++; } else { //if zero weight encountered, skip to the next vertex k = this._jointsPerVertex; } } targetPositions[i0] = vx; targetPositions[i0 + 1] = vy; if (posDim == 3) targetPositions[i0 + 2] = vz; targetNormals[i1] = nx; targetNormals[i1 + 1] = ny; targetNormals[i1 + 2] = nz; targetTangents[i2] = tx; targetTangents[i2 + 1] = ty; targetTangents[i2 + 2] = tz; index++; } } /** * Converts a local hierarchical skeleton pose to a global pose * @param targetPose The SkeletonPose object that will contain the global pose. * @param skeleton The skeleton containing the joints, and as such, the hierarchical data to transform to global poses. */ private localToGlobalPose(sourcePose: SkeletonPose, targetPose: SkeletonPose, skeleton: Skeleton): void { const globalPoses: Array<JointPose> = targetPose.jointPoses; let globalJointPose: JointPose; const joints: Array<SkeletonJoint> = skeleton.joints; const len: number = sourcePose.numJointPoses; const jointPoses: Array<JointPose> = sourcePose.jointPoses; let parentIndex: number; let joint: SkeletonJoint; let parentPose: JointPose; let pose: JointPose; let or: Quaternion; let tr: Vector3D; let t: Vector3D; let q: Quaternion; let x1: number, y1: number, z1: number, w1: number; let x2: number, y2: number, z2: number, w2: number; let x3: number, y3: number, z3: number; // :s if (globalPoses.length != len) globalPoses.length = len; for (let i: number = 0; i < len; ++i) { globalJointPose = globalPoses[i]; if (globalJointPose == null) globalJointPose = globalPoses[i] = new JointPose(); joint = joints[i]; parentIndex = joint.parentIndex; pose = jointPoses[i]; q = globalJointPose.orientation; t = globalJointPose.translation; if (parentIndex < 0) { tr = pose.translation; or = pose.orientation; q.x = or.x; q.y = or.y; q.z = or.z; q.w = or.w; t.x = tr.x; t.y = tr.y; t.z = tr.z; } else { // append parent pose parentPose = globalPoses[parentIndex]; // rotate point or = parentPose.orientation; tr = pose.translation; x2 = or.x; y2 = or.y; z2 = or.z; w2 = or.w; x3 = tr.x; y3 = tr.y; z3 = tr.z; w1 = -x2 * x3 - y2 * y3 - z2 * z3; x1 = w2 * x3 + y2 * z3 - z2 * y3; y1 = w2 * y3 - x2 * z3 + z2 * x3; z1 = w2 * z3 + x2 * y3 - y2 * x3; // append parent translation tr = parentPose.translation; t.x = -w1 * x2 + x1 * w2 - y1 * z2 + z1 * y2 + tr.x; t.y = -w1 * y2 + x1 * z2 + y1 * w2 - z1 * x2 + tr.y; t.z = -w1 * z2 - x1 * y2 + y1 * x2 + z1 * w2 + tr.z; // append parent orientation x1 = or.x; y1 = or.y; z1 = or.z; w1 = or.w; or = pose.orientation; x2 = or.x; y2 = or.y; z2 = or.z; w2 = or.w; q.w = w1 * w2 - x1 * x2 - y1 * y2 - z1 * z2; q.x = w1 * x2 + x1 * w2 + y1 * z2 - z1 * y2; q.y = w1 * y2 - x1 * z2 + y1 * w2 + z1 * x2; q.z = w1 * z2 + x1 * y2 - y1 * x2 + z1 * w2; } } } private onTransitionComplete(event: AnimationStateEvent): void { if (event.type == AnimationStateEvent.TRANSITION_COMPLETE) { event.animationNode.removeEventListener(AnimationStateEvent.TRANSITION_COMPLETE, this._onTransitionCompleteDelegate); //if this is the current active state transition, revert control to the active node if (this._pActiveState == event.animationState) { this._pActiveNode = this._pAnimationSet.getAnimation(this._pActiveAnimationName); this._pActiveState = this.getAnimationState(this._pActiveNode); this._activeSkeletonState = <ISkeletonAnimationState> this._pActiveState; } } } private onIndicesUpdate(event: ElementsEvent): void { const elements: TriangleElements = <TriangleElements> event.target; (<TriangleElements> this._morphedElements[elements.id]).setIndices(elements.indices); } private onVerticesUpdate(event: ElementsEvent): void { const elements: TriangleElements = <TriangleElements> event.target; //only update uvs if (event.attributesView != elements.uvs && event.attributesView != elements.getCustomAtributes('secondaryUVs')) return; const morphElements: TriangleElements = <TriangleElements> this._morphedElements[elements.id]; const morphUVs: Float32Array = <Float32Array> morphElements.uvs.get(elements.numVertices); morphElements.invalidateVertices(morphElements.uvs); const uvStride: number = morphElements.uvs.stride; const uvs: Float32Array = <Float32Array> event.attributesView.get(elements.numVertices); const len: number = elements.numVertices * uvStride; for (let i: number = 0; i < len; i += uvStride) { morphUVs[i] = uvs[i]; morphUVs[i + 1] = uvs[i + 1]; } } }