import Base from './core/Base'; import Joint from './Joint'; import Texture2D from './Texture2D'; import Texture from './Texture'; import BoundingBox from './math/BoundingBox'; import Matrix4 from './math/Matrix4'; import mat4 from './glmatrix/mat4'; import vec3 from './glmatrix/vec3'; import quat from './glmatrix/quat'; var tmpBoundingBox = new BoundingBox(); var tmpMat4 = new Matrix4(); /** * @constructor clay.Skeleton */ var Skeleton = Base.extend(function () { return /** @lends clay.Skeleton# */{ /** * Relative root node that not affect transform of joint. * @type {clay.Node} */ relativeRootNode: null, /** * @type {string} */ name: '', /** * joints * @type {Array.} */ joints: [], /** * bounding box with bound geometry. * @type {clay.BoundingBox} */ boundingBox: null, _clips: [], // Matrix to joint space (relative to root joint) _invBindPoseMatricesArray: null, // Use subarray instead of copy back each time computing matrix // http://jsperf.com/subarray-vs-copy-for-array-transform/5 _jointMatricesSubArrays: [], // jointMatrix * currentPoseMatrix // worldTransform is relative to the root bone // still in model space not world space _skinMatricesArray: null, _skinMatricesSubArrays: [], _subSkinMatricesArray: {} }; }, /** @lends clay.Skeleton.prototype */ { /** * Add a skinning clip and create a map between clip and skeleton * @param {clay.animation.SkinningClip} clip * @param {Object} [mapRule] Map between joint name in skeleton and joint name in clip */ addClip: function (clip, mapRule) { // Clip have been exists in for (var i = 0; i < this._clips.length; i++) { if (this._clips[i].clip === clip) { return; } } // Map the joint index in skeleton to joint pose index in clip var maps = []; for (var i = 0; i < this.joints.length; i++) { maps[i] = -1; } // Create avatar for (var i = 0; i < clip.tracks.length; i++) { for (var j = 0; j < this.joints.length; j++) { var joint = this.joints[j]; var track = clip.tracks[i]; var jointName = joint.name; if (mapRule) { jointName = mapRule[jointName]; } if (track.name === jointName) { maps[j] = i; break; } } } this._clips.push({ maps: maps, clip: clip }); return this._clips.length - 1; }, /** * @param {clay.animation.SkinningClip} clip */ removeClip: function (clip) { var idx = -1; for (var i = 0; i < this._clips.length; i++) { if (this._clips[i].clip === clip) { idx = i; break; } } if (idx > 0) { this._clips.splice(idx, 1); } }, /** * Remove all clips */ removeClipsAll: function () { this._clips = []; }, /** * Get clip by index * @param {number} index */ getClip: function (index) { if (this._clips[index]) { return this._clips[index].clip; } }, /** * @return {number} */ getClipNumber: function () { return this._clips.length; }, /** * Calculate joint matrices from node transform * @function */ updateJointMatrices: (function () { var m4 = mat4.create(); return function () { this._invBindPoseMatricesArray = new Float32Array(this.joints.length * 16); this._skinMatricesArray = new Float32Array(this.joints.length * 16); for (var i = 0; i < this.joints.length; i++) { var joint = this.joints[i]; mat4.copy(m4, joint.node.worldTransform.array); mat4.invert(m4, m4); var offset = i * 16; for (var j = 0; j < 16; j++) { this._invBindPoseMatricesArray[offset + j] = m4[j]; } } this.updateMatricesSubArrays(); }; })(), /** * Update boundingBox of each joint bound to geometry. * ASSUME skeleton and geometry joints are matched. * @param {clay.Geometry} geometry */ updateJointsBoundingBoxes: function (geometry) { var attributes = geometry.attributes; var positionAttr = attributes.position; var jointAttr = attributes.joint; var weightAttr = attributes.weight; var jointsBoundingBoxes = []; for (var i = 0; i < this.joints.length; i++) { jointsBoundingBoxes[i] = new BoundingBox(); jointsBoundingBoxes[i].__updated = false; } var vtxJoint = []; var vtxPos = []; var vtxWeight = []; var maxJointIdx = 0; for (var i = 0; i < geometry.vertexCount; i++) { jointAttr.get(i, vtxJoint); positionAttr.get(i, vtxPos); weightAttr.get(i, vtxWeight); for (var k = 0; k < 4; k++) { if (vtxWeight[k] > 0.01) { var jointIdx = vtxJoint[k]; maxJointIdx = Math.max(maxJointIdx, jointIdx); var min = jointsBoundingBoxes[jointIdx].min.array; var max = jointsBoundingBoxes[jointIdx].max.array; jointsBoundingBoxes[jointIdx].__updated = true; min = vec3.min(min, min, vtxPos); max = vec3.max(max, max, vtxPos); } } } this._jointsBoundingBoxes = jointsBoundingBoxes; this.boundingBox = new BoundingBox(); if (maxJointIdx < this.joints.length - 1) { console.warn('Geometry joints and skeleton joints don\'t match'); } }, setJointMatricesArray: function (arr) { this._invBindPoseMatricesArray = arr; this._skinMatricesArray = new Float32Array(arr.length); this.updateMatricesSubArrays(); }, updateMatricesSubArrays: function () { for (var i = 0; i < this.joints.length; i++) { this._jointMatricesSubArrays[i] = this._invBindPoseMatricesArray.subarray(i * 16, (i+1) * 16); this._skinMatricesSubArrays[i] = this._skinMatricesArray.subarray(i * 16, (i+1) * 16); } }, /** * Update skinning matrices */ update: function () { this._setPose(); var jointsBoundingBoxes = this._jointsBoundingBoxes; for (var i = 0; i < this.joints.length; i++) { var joint = this.joints[i]; mat4.multiply( this._skinMatricesSubArrays[i], joint.node.worldTransform.array, this._jointMatricesSubArrays[i] ); } if (this.boundingBox) { this.boundingBox.min.set(Infinity, Infinity, Infinity); this.boundingBox.max.set(-Infinity, -Infinity, -Infinity); for (var i = 0; i < this.joints.length; i++) { var joint = this.joints[i]; var bbox = jointsBoundingBoxes[i]; if (bbox.__updated) { tmpBoundingBox.copy(bbox); tmpMat4.array = this._skinMatricesSubArrays[i]; tmpBoundingBox.applyTransform(tmpMat4); this.boundingBox.union(tmpBoundingBox); } } } }, getSubSkinMatrices: function (meshId, joints) { var subArray = this._subSkinMatricesArray[meshId]; if (!subArray) { subArray = this._subSkinMatricesArray[meshId] = new Float32Array(joints.length * 16); } var cursor = 0; for (var i = 0; i < joints.length; i++) { var idx = joints[i]; for (var j = 0; j < 16; j++) { subArray[cursor++] = this._skinMatricesArray[idx * 16 + j]; } } return subArray; }, getSubSkinMatricesTexture: function (meshId, joints) { var skinMatrices = this.getSubSkinMatrices(meshId, joints); var size; var numJoints = this.joints.length; if (numJoints > 256) { size = 64; } else if (numJoints > 64) { size = 32; } else if (numJoints > 16) { size = 16; } else { size = 8; } var texture = this._skinMatricesTexture = this._skinMatricesTexture || new Texture2D({ type: Texture.FLOAT, minFilter: Texture.NEAREST, magFilter: Texture.NEAREST, useMipmap: false, flipY: false }); texture.width = size; texture.height = size; if (!texture.pixels || texture.pixels.length !== size * size * 4) { texture.pixels = new Float32Array(size * size * 4); } texture.pixels.set(skinMatrices); texture.dirty(); return texture; }, getSkinMatricesTexture: function () { return this._skinMatricesTexture; }, _setPose: function () { if (this._clips[0]) { var clip = this._clips[0].clip; var maps = this._clips[0].maps; for (var i = 0; i < this.joints.length; i++) { var joint = this.joints[i]; if (maps[i] === -1) { continue; } var pose = clip.tracks[maps[i]]; // Not update if there is no data. // PENDING If sync pose.position, pose.rotation, pose.scale if (pose.channels.position) { vec3.copy(joint.node.position.array, pose.position); } if (pose.channels.rotation) { quat.copy(joint.node.rotation.array, pose.rotation); } if (pose.channels.scale) { vec3.copy(joint.node.scale.array, pose.scale); } joint.node.position._dirty = true; joint.node.rotation._dirty = true; joint.node.scale._dirty = true; } } }, clone: function (clonedNodesMap) { var skeleton = new Skeleton(); skeleton.name = this.name; for (var i = 0; i < this.joints.length; i++) { var newJoint = new Joint(); var joint = this.joints[i]; newJoint.name = joint.name; newJoint.index = joint.index; if (clonedNodesMap) { var newNode = clonedNodesMap[joint.node.__uid__]; if (!newNode) { // PENDING console.warn('Can\'t find node'); } newJoint.node = newNode || joint.node; } else { newJoint.node = joint.node; } skeleton.joints.push(newJoint); } if (this._invBindPoseMatricesArray) { var len = this._invBindPoseMatricesArray.length; skeleton._invBindPoseMatricesArray = new Float32Array(len); for (var i = 0; i < len; i++) { skeleton._invBindPoseMatricesArray[i] = this._invBindPoseMatricesArray[i]; } skeleton._skinMatricesArray = new Float32Array(len); skeleton.updateMatricesSubArrays(); } skeleton._jointsBoundingBoxe = (this._jointsBoundingBoxes || []).map(function (bbox) { return bbox.clone(); }); skeleton.update(); return skeleton; } }); export default Skeleton;