/* * To change this template, choose Tools | Templates * and open the template in the editor. */ package com.jme3.animation; import com.jme3.export.*; import com.jme3.math.FastMath; import com.jme3.math.Matrix4f; import com.jme3.renderer.RenderManager; import com.jme3.renderer.ViewPort; import com.jme3.scene.*; import com.jme3.scene.VertexBuffer.Type; import com.jme3.scene.control.AbstractControl; import com.jme3.scene.control.Control; import com.jme3.util.TempVars; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.FloatBuffer; import java.util.ArrayList; /** * The Skeleton control deforms a model according to a skeleton, * It handles the computation of the deformation matrices and performs * the transformations on the mesh * * @author Rémy Bouquet Based on AnimControl by Kirill Vainer */ public class SkeletonControl extends AbstractControl implements Cloneable { /** * The skeleton of the model */ private Skeleton skeleton; /** * List of targets which this controller effects. */ private Mesh[] targets; /** * Used to track when a mesh was updated. Meshes are only updated * if they are visible in at least one camera. */ private boolean wasMeshUpdated = false; /** * Serialization only. Do not use. */ public SkeletonControl() { } /** * Creates a skeleton control. * The list of targets will be acquired automatically when * the control is attached to a node. * * @param skeleton the skeleton */ public SkeletonControl(Skeleton skeleton) { this.skeleton = skeleton; } /** * Creates a skeleton control. * * @param targets the meshes controlled by the skeleton * @param skeleton the skeleton */ @Deprecated SkeletonControl(Mesh[] targets, Skeleton skeleton) { this.skeleton = skeleton; this.targets = targets; } private boolean isMeshAnimated(Mesh mesh) { return mesh.getBuffer(Type.BindPosePosition) != null; } private Mesh[] findTargets(Node node) { Mesh sharedMesh = null; ArrayList animatedMeshes = new ArrayList(); for (Spatial child : node.getChildren()) { if (!(child instanceof Geometry)) { continue; // could be an attachment node, ignore. } Geometry geom = (Geometry) child; // is this geometry using a shared mesh? Mesh childSharedMesh = geom.getUserData(UserData.JME_SHAREDMESH); if (childSharedMesh != null) { // Don't bother with non-animated shared meshes if (isMeshAnimated(childSharedMesh)) { // child is using shared mesh, // so animate the shared mesh but ignore child if (sharedMesh == null) { sharedMesh = childSharedMesh; } else if (sharedMesh != childSharedMesh) { throw new IllegalStateException("Two conflicting shared meshes for " + node); } } } else { Mesh mesh = geom.getMesh(); if (isMeshAnimated(mesh)) { animatedMeshes.add(mesh); } } } if (sharedMesh != null) { animatedMeshes.add(sharedMesh); } return animatedMeshes.toArray(new Mesh[animatedMeshes.size()]); } @Override public void setSpatial(Spatial spatial) { super.setSpatial(spatial); if (spatial != null) { Node node = (Node) spatial; targets = findTargets(node); } else { targets = null; } } @Override protected void controlRender(RenderManager rm, ViewPort vp) { if (!wasMeshUpdated) { resetToBind(); // reset morph meshes to bind pose Matrix4f[] offsetMatrices = skeleton.computeSkinningMatrices(); // if hardware skinning is supported, the matrices and weight buffer // will be sent by the SkinningShaderLogic object assigned to the shader for (int i = 0; i < targets.length; i++) { // NOTE: This assumes that code higher up // Already ensured those targets are animated // otherwise a crash will happen in skin update //if (isMeshAnimated(targets[i])) { softwareSkinUpdate(targets[i], offsetMatrices); //} } wasMeshUpdated = true; } } @Override protected void controlUpdate(float tpf) { wasMeshUpdated = false; } void resetToBind() { for (Mesh mesh : targets) { if (isMeshAnimated(mesh)) { VertexBuffer bi = mesh.getBuffer(Type.BoneIndex); ByteBuffer bib = (ByteBuffer) bi.getData(); if (!bib.hasArray()) { mesh.prepareForAnim(true); // prepare for software animation } VertexBuffer bindPos = mesh.getBuffer(Type.BindPosePosition); VertexBuffer bindNorm = mesh.getBuffer(Type.BindPoseNormal); VertexBuffer pos = mesh.getBuffer(Type.Position); VertexBuffer norm = mesh.getBuffer(Type.Normal); FloatBuffer pb = (FloatBuffer) pos.getData(); FloatBuffer nb = (FloatBuffer) norm.getData(); FloatBuffer bpb = (FloatBuffer) bindPos.getData(); FloatBuffer bnb = (FloatBuffer) bindNorm.getData(); pb.clear(); nb.clear(); bpb.clear(); bnb.clear(); //reseting bind tangents if there is a bind tangent buffer VertexBuffer bindTangents = mesh.getBuffer(Type.BindPoseTangent); if (bindTangents != null) { VertexBuffer tangents = mesh.getBuffer(Type.Tangent); FloatBuffer tb = (FloatBuffer) tangents.getData(); FloatBuffer btb = (FloatBuffer) bindTangents.getData(); tb.clear(); btb.clear(); tb.put(btb).clear(); } pb.put(bpb).clear(); nb.put(bnb).clear(); } } } public Control cloneForSpatial(Spatial spatial) { Node clonedNode = (Node) spatial; AnimControl ctrl = spatial.getControl(AnimControl.class); SkeletonControl clone = new SkeletonControl(); clone.setSpatial(clonedNode); clone.skeleton = ctrl.getSkeleton(); // Fix animated targets for the cloned node clone.targets = findTargets(clonedNode); // Fix attachments for the cloned node for (int i = 0; i < clonedNode.getQuantity(); i++) { // go through attachment nodes, apply them to correct bone Spatial child = clonedNode.getChild(i); if (child instanceof Node) { Node clonedAttachNode = (Node) child; Bone originalBone = (Bone) clonedAttachNode.getUserData("AttachedBone"); if (originalBone != null) { Bone clonedBone = clone.skeleton.getBone(originalBone.getName()); clonedAttachNode.setUserData("AttachedBone", clonedBone); clonedBone.setAttachmentsNode(clonedAttachNode); } } } return clone; } /** * * @param boneName the name of the bone * @return the node attached to this bone */ public Node getAttachmentsNode(String boneName) { Bone b = skeleton.getBone(boneName); if (b == null) { throw new IllegalArgumentException("Given bone name does not exist " + "in the skeleton."); } Node n = b.getAttachmentsNode(); Node model = (Node) spatial; model.attachChild(n); return n; } /** * returns the skeleton of this control * @return */ public Skeleton getSkeleton() { return skeleton; } /** * sets the skeleton for this control * @param skeleton */ // public void setSkeleton(Skeleton skeleton) { // this.skeleton = skeleton; // } /** * returns the targets meshes of this control * @return */ public Mesh[] getTargets() { return targets; } /** * sets the target meshes of this control * @param targets */ // public void setTargets(Mesh[] targets) { // this.targets = targets; // } /** * Update the mesh according to the given transformation matrices * @param mesh then mesh * @param offsetMatrices the transformation matrices to apply */ private void softwareSkinUpdate(Mesh mesh, Matrix4f[] offsetMatrices) { VertexBuffer tb = mesh.getBuffer(Type.Tangent); if (tb == null) { //if there are no tangents use the classic skinning applySkinning(mesh, offsetMatrices); } else { //if there are tangents use the skinning with tangents applySkinningTangents(mesh, offsetMatrices, tb); } } /** * Method to apply skinning transforms to a mesh's buffers * @param mesh the mesh * @param offsetMatrices the offset matices to apply */ private void applySkinning(Mesh mesh, Matrix4f[] offsetMatrices) { int maxWeightsPerVert = mesh.getMaxNumWeights(); if (maxWeightsPerVert <= 0) { throw new IllegalStateException("Max weights per vert is incorrectly set!"); } int fourMinusMaxWeights = 4 - maxWeightsPerVert; // NOTE: This code assumes the vertex buffer is in bind pose // resetToBind() has been called this frame VertexBuffer vb = mesh.getBuffer(Type.Position); FloatBuffer fvb = (FloatBuffer) vb.getData(); fvb.rewind(); VertexBuffer nb = mesh.getBuffer(Type.Normal); FloatBuffer fnb = (FloatBuffer) nb.getData(); fnb.rewind(); // get boneIndexes and weights for mesh ByteBuffer ib = (ByteBuffer) mesh.getBuffer(Type.BoneIndex).getData(); FloatBuffer wb = (FloatBuffer) mesh.getBuffer(Type.BoneWeight).getData(); ib.rewind(); wb.rewind(); float[] weights = wb.array(); byte[] indices = ib.array(); int idxWeights = 0; TempVars vars = TempVars.get(); float[] posBuf = vars.skinPositions; float[] normBuf = vars.skinNormals; int iterations = (int) FastMath.ceil(fvb.capacity() / ((float) posBuf.length)); int bufLength = posBuf.length; for (int i = iterations - 1; i >= 0; i--) { // read next set of positions and normals from native buffer bufLength = Math.min(posBuf.length, fvb.remaining()); fvb.get(posBuf, 0, bufLength); fnb.get(normBuf, 0, bufLength); int verts = bufLength / 3; int idxPositions = 0; // iterate vertices and apply skinning transform for each effecting bone for (int vert = verts - 1; vert >= 0; vert--) { float nmx = normBuf[idxPositions]; float vtx = posBuf[idxPositions++]; float nmy = normBuf[idxPositions]; float vty = posBuf[idxPositions++]; float nmz = normBuf[idxPositions]; float vtz = posBuf[idxPositions++]; float rx = 0, ry = 0, rz = 0, rnx = 0, rny = 0, rnz = 0; for (int w = maxWeightsPerVert - 1; w >= 0; w--) { float weight = weights[idxWeights]; Matrix4f mat = offsetMatrices[indices[idxWeights++]]; rx += (mat.m00 * vtx + mat.m01 * vty + mat.m02 * vtz + mat.m03) * weight; ry += (mat.m10 * vtx + mat.m11 * vty + mat.m12 * vtz + mat.m13) * weight; rz += (mat.m20 * vtx + mat.m21 * vty + mat.m22 * vtz + mat.m23) * weight; rnx += (nmx * mat.m00 + nmy * mat.m01 + nmz * mat.m02) * weight; rny += (nmx * mat.m10 + nmy * mat.m11 + nmz * mat.m12) * weight; rnz += (nmx * mat.m20 + nmy * mat.m21 + nmz * mat.m22) * weight; } idxWeights += fourMinusMaxWeights; idxPositions -= 3; normBuf[idxPositions] = rnx; posBuf[idxPositions++] = rx; normBuf[idxPositions] = rny; posBuf[idxPositions++] = ry; normBuf[idxPositions] = rnz; posBuf[idxPositions++] = rz; } fvb.position(fvb.position() - bufLength); fvb.put(posBuf, 0, bufLength); fnb.position(fnb.position() - bufLength); fnb.put(normBuf, 0, bufLength); } vars.release(); vb.updateData(fvb); nb.updateData(fnb); } /** * Specific method for skinning with tangents to avoid cluttering the classic skinning calculation with * null checks that would slow down the process even if tangents don't have to be computed. * Also the iteration has additional indexes since tangent has 4 components instead of 3 for pos and norm * @param maxWeightsPerVert maximum number of weights per vertex * @param mesh the mesh * @param offsetMatrices the offsetMaytrices to apply * @param tb the tangent vertexBuffer */ private void applySkinningTangents(Mesh mesh, Matrix4f[] offsetMatrices, VertexBuffer tb) { int maxWeightsPerVert = mesh.getMaxNumWeights(); if (maxWeightsPerVert <= 0) { throw new IllegalStateException("Max weights per vert is incorrectly set!"); } int fourMinusMaxWeights = 4 - maxWeightsPerVert; // NOTE: This code assumes the vertex buffer is in bind pose // resetToBind() has been called this frame VertexBuffer vb = mesh.getBuffer(Type.Position); FloatBuffer fvb = (FloatBuffer) vb.getData(); fvb.rewind(); VertexBuffer nb = mesh.getBuffer(Type.Normal); FloatBuffer fnb = (FloatBuffer) nb.getData(); fnb.rewind(); FloatBuffer ftb = (FloatBuffer) tb.getData(); ftb.rewind(); // get boneIndexes and weights for mesh ByteBuffer ib = (ByteBuffer) mesh.getBuffer(Type.BoneIndex).getData(); FloatBuffer wb = (FloatBuffer) mesh.getBuffer(Type.BoneWeight).getData(); ib.rewind(); wb.rewind(); float[] weights = wb.array(); byte[] indices = ib.array(); int idxWeights = 0; TempVars vars = TempVars.get(); float[] posBuf = vars.skinPositions; float[] normBuf = vars.skinNormals; float[] tanBuf = vars.skinTangents; int iterations = (int) FastMath.ceil(fvb.capacity() / ((float) posBuf.length)); int bufLength = 0; int tanLength = 0; for (int i = iterations - 1; i >= 0; i--) { // read next set of positions and normals from native buffer bufLength = Math.min(posBuf.length, fvb.remaining()); tanLength = Math.min(tanBuf.length, ftb.remaining()); fvb.get(posBuf, 0, bufLength); fnb.get(normBuf, 0, bufLength); ftb.get(tanBuf, 0, tanLength); int verts = bufLength / 3; int idxPositions = 0; //tangents has their own index because of the 4 components int idxTangents = 0; // iterate vertices and apply skinning transform for each effecting bone for (int vert = verts - 1; vert >= 0; vert--) { float nmx = normBuf[idxPositions]; float vtx = posBuf[idxPositions++]; float nmy = normBuf[idxPositions]; float vty = posBuf[idxPositions++]; float nmz = normBuf[idxPositions]; float vtz = posBuf[idxPositions++]; float tnx = tanBuf[idxTangents++]; float tny = tanBuf[idxTangents++]; float tnz = tanBuf[idxTangents++]; //skipping the 4th component of the tangent since it doesn't have to be transformed idxTangents++; float rx = 0, ry = 0, rz = 0, rnx = 0, rny = 0, rnz = 0, rtx = 0, rty = 0, rtz = 0; for (int w = maxWeightsPerVert - 1; w >= 0; w--) { float weight = weights[idxWeights]; Matrix4f mat = offsetMatrices[indices[idxWeights++]]; rx += (mat.m00 * vtx + mat.m01 * vty + mat.m02 * vtz + mat.m03) * weight; ry += (mat.m10 * vtx + mat.m11 * vty + mat.m12 * vtz + mat.m13) * weight; rz += (mat.m20 * vtx + mat.m21 * vty + mat.m22 * vtz + mat.m23) * weight; rnx += (nmx * mat.m00 + nmy * mat.m01 + nmz * mat.m02) * weight; rny += (nmx * mat.m10 + nmy * mat.m11 + nmz * mat.m12) * weight; rnz += (nmx * mat.m20 + nmy * mat.m21 + nmz * mat.m22) * weight; rtx += (tnx * mat.m00 + tny * mat.m01 + tnz * mat.m02) * weight; rty += (tnx * mat.m10 + tny * mat.m11 + tnz * mat.m12) * weight; rtz += (tnx * mat.m20 + tny * mat.m21 + tnz * mat.m22) * weight; } idxWeights += fourMinusMaxWeights; idxPositions -= 3; normBuf[idxPositions] = rnx; posBuf[idxPositions++] = rx; normBuf[idxPositions] = rny; posBuf[idxPositions++] = ry; normBuf[idxPositions] = rnz; posBuf[idxPositions++] = rz; idxTangents -= 4; tanBuf[idxTangents++] = rtx; tanBuf[idxTangents++] = rty; tanBuf[idxTangents++] = rtz; //once again skipping the 4th component of the tangent idxTangents++; } fvb.position(fvb.position() - bufLength); fvb.put(posBuf, 0, bufLength); fnb.position(fnb.position() - bufLength); fnb.put(normBuf, 0, bufLength); ftb.position(ftb.position() - tanLength); ftb.put(tanBuf, 0, tanLength); } vars.release(); vb.updateData(fvb); nb.updateData(fnb); tb.updateData(ftb); } @Override public void write(JmeExporter ex) throws IOException { super.write(ex); OutputCapsule oc = ex.getCapsule(this); oc.write(targets, "targets", null); oc.write(skeleton, "skeleton", null); } @Override public void read(JmeImporter im) throws IOException { super.read(im); InputCapsule in = im.getCapsule(this); Savable[] sav = in.readSavableArray("targets", null); if (sav != null) { targets = new Mesh[sav.length]; System.arraycopy(sav, 0, targets, 0, sav.length); } skeleton = (Skeleton) in.readSavable("skeleton", null); } }