diff --git a/jme3-core/src/main/java/com/jme3/animation/Armature.java b/jme3-core/src/main/java/com/jme3/animation/Armature.java new file mode 100644 index 000000000..9c09e63a0 --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/animation/Armature.java @@ -0,0 +1,251 @@ +package com.jme3.animation; + +import com.jme3.export.*; +import com.jme3.math.Matrix4f; +import com.jme3.util.TempVars; +import com.jme3.util.clone.Cloner; +import com.jme3.util.clone.JmeCloneable; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Nehon on 15/12/2017. + */ +public class Armature implements JmeCloneable, Savable { + + private Joint[] rootJoints; + private Joint[] jointList; + + /** + * Contains the skinning matrices, multiplying it by a vertex effected by a bone + * will cause it to go to the animated position. + */ + private transient Matrix4f[] skinningMatrixes; + + + /** + * Serialization only + */ + public Armature() { + } + + /** + * Creates an armature from a joint list. + * The root joints are found automatically. + *

+ * Note that using this constructor will cause the joints in the list + * to have their bind pose recomputed based on their local transforms. + * + * @param jointList The list of joints to manage by this Armature + */ + public Armature(Joint[] jointList) { + this.jointList = jointList; + + List rootJointList = new ArrayList<>(); + for (int i = jointList.length - 1; i >= 0; i--) { + Joint b = jointList[i]; + if (b.getParent() == null) { + rootJointList.add(b); + } + } + rootJoints = rootJointList.toArray(new Joint[rootJointList.size()]); + + createSkinningMatrices(); + + for (int i = rootJoints.length - 1; i >= 0; i--) { + Joint rootJoint = rootJoints[i]; + rootJoint.update(); + } + } + +// +// /** +// * Special-purpose copy constructor. +// *

+// * Shallow copies bind pose data from the source skeleton, does not +// * copy any other data. +// * +// * @param source The source Skeleton to copy from +// */ +// public Armature(Armature source) { +// Joint[] sourceList = source.jointList; +// jointList = new Joint[sourceList.length]; +// for (int i = 0; i < sourceList.length; i++) { +// jointList[i] = new Joint(sourceList[i]); +// } +// +// rootJoints = new Bone[source.rootJoints.length]; +// for (int i = 0; i < rootJoints.length; i++) { +// rootJoints[i] = recreateBoneStructure(source.rootJoints[i]); +// } +// createSkinningMatrices(); +// +// for (int i = rootJoints.length - 1; i >= 0; i--) { +// rootJoints[i].update(); +// } +// } + + /** + * Update all joints sin this Amature. + */ + public void update() { + for (Joint rootJoint : rootJoints) { + rootJoint.update(); + } + } + + private void createSkinningMatrices() { + skinningMatrixes = new Matrix4f[jointList.length]; + for (int i = 0; i < skinningMatrixes.length; i++) { + skinningMatrixes[i] = new Matrix4f(); + } + } + + /** + * returns the array of all root joints of this Armatire + * + * @return + */ + public Joint[] getRoots() { + return rootJoints; + } + + /** + * return a joint for the given index + * + * @param index + * @return + */ + public Joint getJoint(int index) { + return jointList[index]; + } + + /** + * returns the joint with the given name + * + * @param name + * @return + */ + public Joint getJoint(String name) { + for (int i = 0; i < jointList.length; i++) { + if (jointList[i].getName().equals(name)) { + return jointList[i]; + } + } + return null; + } + + /** + * returns the bone index of the given bone + * + * @param joint + * @return + */ + public int getJointIndex(Joint joint) { + for (int i = 0; i < jointList.length; i++) { + if (jointList[i] == joint) { + return i; + } + } + + return -1; + } + + /** + * returns the joint index of the joint that has the given name + * + * @param name + * @return + */ + public int getJointIndex(String name) { + for (int i = 0; i < jointList.length; i++) { + if (jointList[i].getName().equals(name)) { + return i; + } + } + + return -1; + } + + /** + * Saves the current Armature state as it's bind pose. + */ + public void setBindPose() { + //make sure all bones are updated + update(); + //Save the current pose as bind pose + for (Joint rootJoint : rootJoints) { + rootJoint.setBindPose(); + } + } + + /** + * Compute the skining matrices for each bone of the armature that would be used to transform vertices of associated meshes + * + * @return + */ + public Matrix4f[] computeSkinningMatrices() { + TempVars vars = TempVars.get(); + for (int i = 0; i < jointList.length; i++) { + jointList[i].getOffsetTransform(skinningMatrixes[i], vars.quat1, vars.vect1, vars.vect2, vars.tempMat3); + } + vars.release(); + return skinningMatrixes; + } + + /** + * returns the number of joints of this armature + * + * @return + */ + public int getJointCount() { + return jointList.length; + } + + @Override + public Object jmeClone() { + try { + Armature clone = (Armature) super.clone(); + return clone; + } catch (CloneNotSupportedException ex) { + throw new AssertionError(); + } + } + + @Override + public void cloneFields(Cloner cloner, Object original) { + this.rootJoints = cloner.clone(rootJoints); + this.jointList = cloner.clone(jointList); + this.skinningMatrixes = cloner.clone(skinningMatrixes); + } + + + @Override + public void read(JmeImporter im) throws IOException { + InputCapsule input = im.getCapsule(this); + + Savable[] jointRootsAsSavable = input.readSavableArray("rootJoints", null); + rootJoints = new Joint[jointRootsAsSavable.length]; + System.arraycopy(jointRootsAsSavable, 0, rootJoints, 0, jointRootsAsSavable.length); + + Savable[] jointListAsSavable = input.readSavableArray("jointList", null); + jointList = new Joint[jointListAsSavable.length]; + System.arraycopy(jointListAsSavable, 0, jointList, 0, jointListAsSavable.length); + + createSkinningMatrices(); + + for (Joint rootJoint : rootJoints) { + rootJoint.update(); + } + } + + @Override + public void write(JmeExporter ex) throws IOException { + OutputCapsule output = ex.getCapsule(this); + output.write(rootJoints, "rootJoints", null); + output.write(jointList, "jointList", null); + } + +} diff --git a/jme3-core/src/main/java/com/jme3/animation/ArmatureControl.java b/jme3-core/src/main/java/com/jme3/animation/ArmatureControl.java new file mode 100644 index 000000000..00f017f32 --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/animation/ArmatureControl.java @@ -0,0 +1,742 @@ +/* + * Copyright (c) 2009-2017 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.jme3.animation; + +import com.jme3.export.*; +import com.jme3.material.MatParamOverride; +import com.jme3.math.FastMath; +import com.jme3.math.Matrix4f; +import com.jme3.renderer.*; +import com.jme3.scene.*; +import com.jme3.scene.VertexBuffer.Type; +import com.jme3.scene.control.AbstractControl; +import com.jme3.scene.mesh.IndexBuffer; +import com.jme3.shader.VarType; +import com.jme3.util.SafeArrayList; +import com.jme3.util.TempVars; +import com.jme3.util.clone.Cloner; +import com.jme3.util.clone.JmeCloneable; + +import java.io.IOException; +import java.nio.Buffer; +import java.nio.FloatBuffer; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * The Skeleton control deforms a model according to a armature, 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 ArmatureControl extends AbstractControl implements Cloneable, JmeCloneable { + + private static final Logger logger = Logger.getLogger(ArmatureControl.class.getName()); + + /** + * The armature of the model. + */ + private Armature armature; + + /** + * List of geometries affected by this control. + */ + private SafeArrayList targets = new SafeArrayList(Geometry.class); + + /** + * 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; + + /** + * User wishes to use hardware skinning if available. + */ + private transient boolean hwSkinningDesired = true; + + /** + * Hardware skinning is currently being used. + */ + private transient boolean hwSkinningEnabled = false; + + /** + * Hardware skinning was tested on this GPU, results + * are stored in {@link #hwSkinningSupported} variable. + */ + private transient boolean hwSkinningTested = false; + + /** + * If hardware skinning was {@link #hwSkinningTested tested}, then + * this variable will be set to true if supported, and false if otherwise. + */ + private transient boolean hwSkinningSupported = false; + + /** + * Bone offset matrices, recreated each frame + */ + private transient Matrix4f[] offsetMatrices; + + + private MatParamOverride numberOfJointsParam; + private MatParamOverride jointMatricesParam; + + /** + * Serialization only. Do not use. + */ + public ArmatureControl() { + } + + /** + * Creates a armature control. The list of targets will be acquired + * automatically when the control is attached to a node. + * + * @param skeleton the armature + */ + public ArmatureControl(Armature armature) { + if (armature == null) { + throw new IllegalArgumentException("armature cannot be null"); + } + this.armature = armature; + this.numberOfJointsParam = new MatParamOverride(VarType.Int, "NumberOfBones", null); + this.jointMatricesParam = new MatParamOverride(VarType.Matrix4Array, "BoneMatrices", null); + } + + + private void switchToHardware() { + numberOfJointsParam.setEnabled(true); + jointMatricesParam.setEnabled(true); + + // Next full 10 bones (e.g. 30 on 24 bones) + int numBones = ((armature.getJointCount() / 10) + 1) * 10; + numberOfJointsParam.setValue(numBones); + + for (Geometry geometry : targets) { + Mesh mesh = geometry.getMesh(); + if (mesh != null && mesh.isAnimated()) { + mesh.prepareForAnim(false); + } + } + } + + private void switchToSoftware() { + numberOfJointsParam.setEnabled(false); + jointMatricesParam.setEnabled(false); + + for (Geometry geometry : targets) { + Mesh mesh = geometry.getMesh(); + if (mesh != null && mesh.isAnimated()) { + mesh.prepareForAnim(true); + } + } + } + + private boolean testHardwareSupported(RenderManager rm) { + + //Only 255 bones max supported with hardware skinning + if (armature.getJointCount() > 255) { + return false; + } + + switchToHardware(); + + try { + rm.preloadScene(spatial); + return true; + } catch (RendererException e) { + logger.log(Level.WARNING, "Could not enable HW skinning due to shader compile error:", e); + return false; + } + } + + /** + * Specifies if hardware skinning is preferred. If it is preferred and + * supported by GPU, it shall be enabled, if its not preferred, or not + * supported by GPU, then it shall be disabled. + * + * @param preferred + * @see #isHardwareSkinningUsed() + */ + public void setHardwareSkinningPreferred(boolean preferred) { + hwSkinningDesired = preferred; + } + + /** + * @return True if hardware skinning is preferable to software skinning. + * Set to false by default. + * @see #setHardwareSkinningPreferred(boolean) + */ + public boolean isHardwareSkinningPreferred() { + return hwSkinningDesired; + } + + /** + * @return True is hardware skinning is activated and is currently used, false otherwise. + */ + public boolean isHardwareSkinningUsed() { + return hwSkinningEnabled; + } + + + /** + * If specified the geometry has an animated mesh, add its mesh and material + * to the lists of animation targets. + */ + private void findTargets(Geometry geometry) { + Mesh mesh = geometry.getMesh(); + if (mesh != null && mesh.isAnimated()) { + targets.add(geometry); + } + + } + + private void findTargets(Node node) { + for (Spatial child : node.getChildren()) { + if (child instanceof Geometry) { + findTargets((Geometry) child); + } else if (child instanceof Node) { + findTargets((Node) child); + } + } + } + + @Override + public void setSpatial(Spatial spatial) { + Spatial oldSpatial = this.spatial; + super.setSpatial(spatial); + updateTargetsAndMaterials(spatial); + + if (oldSpatial != null) { + oldSpatial.removeMatParamOverride(numberOfJointsParam); + oldSpatial.removeMatParamOverride(jointMatricesParam); + } + + if (spatial != null) { + spatial.removeMatParamOverride(numberOfJointsParam); + spatial.removeMatParamOverride(jointMatricesParam); + spatial.addMatParamOverride(numberOfJointsParam); + spatial.addMatParamOverride(jointMatricesParam); + } + } + + private void controlRenderSoftware() { + resetToBind(); // reset morph meshes to bind pose + + offsetMatrices = armature.computeSkinningMatrices(); + + for (Geometry geometry : targets) { + Mesh mesh = geometry.getMesh(); + // NOTE: This assumes code higher up has + // already ensured this mesh is animated. + // Otherwise a crash will happen in skin update. + softwareSkinUpdate(mesh, offsetMatrices); + } + } + + private void controlRenderHardware() { + offsetMatrices = armature.computeSkinningMatrices(); + jointMatricesParam.setValue(offsetMatrices); + } + + @Override + protected void controlRender(RenderManager rm, ViewPort vp) { + if (!wasMeshUpdated) { + updateTargetsAndMaterials(spatial); + + // Prevent illegal cases. These should never happen. + assert hwSkinningTested || (!hwSkinningTested && !hwSkinningSupported && !hwSkinningEnabled); + assert !hwSkinningEnabled || (hwSkinningEnabled && hwSkinningTested && hwSkinningSupported); + + if (hwSkinningDesired && !hwSkinningTested) { + hwSkinningTested = true; + hwSkinningSupported = testHardwareSupported(rm); + + if (hwSkinningSupported) { + hwSkinningEnabled = true; + + Logger.getLogger(ArmatureControl.class.getName()).log(Level.INFO, "Hardware skinning engaged for {0}", spatial); + } else { + switchToSoftware(); + } + } else if (hwSkinningDesired && hwSkinningSupported && !hwSkinningEnabled) { + switchToHardware(); + hwSkinningEnabled = true; + } else if (!hwSkinningDesired && hwSkinningEnabled) { + switchToSoftware(); + hwSkinningEnabled = false; + } + + if (hwSkinningEnabled) { + controlRenderHardware(); + } else { + controlRenderSoftware(); + } + + wasMeshUpdated = true; + } + } + + @Override + protected void controlUpdate(float tpf) { + wasMeshUpdated = false; + } + + //only do this for software updates + void resetToBind() { + for (Geometry geometry : targets) { + Mesh mesh = geometry.getMesh(); + if (mesh != null && mesh.isAnimated()) { + Buffer bwBuff = mesh.getBuffer(Type.BoneWeight).getData(); + Buffer biBuff = mesh.getBuffer(Type.BoneIndex).getData(); + if (!biBuff.hasArray() || !bwBuff.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(); + } + } + } + + @Override + public Object jmeClone() { + return super.jmeClone(); + } + + @Override + public void cloneFields(Cloner cloner, Object original) { + super.cloneFields(cloner, original); + + this.armature = cloner.clone(armature); + + // If the targets were cloned then this will clone them. If the targets + // were shared then this will share them. + this.targets = cloner.clone(targets); + + this.numberOfJointsParam = cloner.clone(numberOfJointsParam); + this.jointMatricesParam = cloner.clone(jointMatricesParam); + } + + /** + * Access the attachments node of the named bone. If the bone doesn't + * already have an attachments node, create one and attach it to the scene + * graph. Models and effects attached to the attachments node will follow + * the bone's motions. + * + * @param jointName the name of the joint + * @return the attachments node of the joint + */ + public Node getAttachmentsNode(String jointName) { + Joint b = armature.getJoint(jointName); + if (b == null) { + throw new IllegalArgumentException("Given bone name does not exist " + + "in the armature."); + } + + updateTargetsAndMaterials(spatial); + int boneIndex = armature.getJointIndex(b); + Node n = b.getAttachmentsNode(boneIndex, targets); + /* + * Select a node to parent the attachments node. + */ + Node parent; + if (spatial instanceof Node) { + parent = (Node) spatial; // the usual case + } else { + parent = spatial.getParent(); + } + parent.attachChild(n); + + return n; + } + + /** + * returns the armature of this control + * + * @return + */ + public Armature getArmature() { + return armature; + } + + /** + * Enumerate the target meshes of this control. + * + * @return a new array + */ + public Mesh[] getTargets() { + Mesh[] result = new Mesh[targets.size()]; + int i = 0; + for (Geometry geometry : targets) { + Mesh mesh = geometry.getMesh(); + result[i] = mesh; + i++; + } + + return result; + } + + /** + * 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 + IndexBuffer ib = IndexBuffer.wrapIndexBuffer(mesh.getBuffer(Type.BoneIndex).getData()); + FloatBuffer wb = (FloatBuffer) mesh.getBuffer(Type.BoneWeight).getData(); + + wb.rewind(); + + float[] weights = wb.array(); + int idxWeights = 0; + + TempVars vars = TempVars.get(); + + float[] posBuf = vars.skinPositions; + float[] normBuf = vars.skinNormals; + + int iterations = (int) FastMath.ceil(fvb.limit() / ((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--) { + // Skip this vertex if the first weight is zero. + if (weights[idxWeights] == 0) { + idxPositions += 3; + idxWeights += 4; + continue; + } + + 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[ib.get(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 + IndexBuffer ib = IndexBuffer.wrapIndexBuffer(mesh.getBuffer(Type.BoneIndex).getData()); + FloatBuffer wb = (FloatBuffer) mesh.getBuffer(Type.BoneWeight).getData(); + + wb.rewind(); + + float[] weights = wb.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.limit() / ((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--) { + // Skip this vertex if the first weight is zero. + if (weights[idxWeights] == 0) { + idxTangents += 4; + idxPositions += 3; + idxWeights += 4; + continue; + } + + 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[ib.get(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(armature, "armature", null); + + oc.write(numberOfJointsParam, "numberOfBonesParam", null); + oc.write(jointMatricesParam, "boneMatricesParam", null); + } + + @Override + public void read(JmeImporter im) throws IOException { + super.read(im); + InputCapsule in = im.getCapsule(this); + armature = (Armature) in.readSavable("armature", null); + + numberOfJointsParam = (MatParamOverride) in.readSavable("numberOfBonesParam", null); + jointMatricesParam = (MatParamOverride) in.readSavable("boneMatricesParam", null); + + if (numberOfJointsParam == null) { + numberOfJointsParam = new MatParamOverride(VarType.Int, "NumberOfBones", null); + jointMatricesParam = new MatParamOverride(VarType.Matrix4Array, "BoneMatrices", null); + getSpatial().addMatParamOverride(numberOfJointsParam); + getSpatial().addMatParamOverride(jointMatricesParam); + } + } + + /** + * Update the lists of animation targets. + * + * @param spatial the controlled spatial + */ + private void updateTargetsAndMaterials(Spatial spatial) { + targets.clear(); + + if (spatial instanceof Node) { + findTargets((Node) spatial); + } else if (spatial instanceof Geometry) { + findTargets((Geometry) spatial); + } + } +} diff --git a/jme3-core/src/main/java/com/jme3/animation/Joint.java b/jme3-core/src/main/java/com/jme3/animation/Joint.java new file mode 100644 index 000000000..a5a35af7f --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/animation/Joint.java @@ -0,0 +1,292 @@ +package com.jme3.animation; + +import com.jme3.export.*; +import com.jme3.material.MatParamOverride; +import com.jme3.math.*; +import com.jme3.scene.*; +import com.jme3.shader.VarType; +import com.jme3.util.SafeArrayList; +import com.jme3.util.clone.Cloner; +import com.jme3.util.clone.JmeCloneable; + +import java.io.IOException; +import java.util.ArrayList; + +/** + * A Joint is the basic component of an armature designed to perform skeletal animation + * Created by Nehon on 15/12/2017. + */ +public class Joint implements Savable, JmeCloneable { + + private String name; + private Joint parent; + private ArrayList children = new ArrayList<>(); + private Geometry targetGeometry; + + public Joint() { + } + + public Joint(String name) { + this.name = name; + } + + /** + * The attachment node. + */ + private Node attachedNode; + + /** + * The transform of the joint in local space. Relative to its parent. + * Or relative to the model's origin for the root joint. + */ + private Transform localTransform = new Transform(); + + /** + * The base transform of the joint in local space. + * Those transform are the bone's initial value. + */ + private Transform baseLocalTransform = new Transform(); + + /** + * The transform of the bone in model space. Relative to the origin of the model. + */ + private Transform modelTransform = new Transform(); + + /** + * The matrix used to transform affected vertices position into the bone model space. + * Used for skinning. + */ + private Matrix4f inverseModelBindMatrix = new Matrix4f(); + + /** + * Updates world transforms for this bone and it's children. + */ + public final void update() { + this.updateModelTransforms(); + + for (int i = children.size() - 1; i >= 0; i--) { + children.get(i).update(); + } + } + + /** + * Updates the model transforms for this bone, and, possibly the attach node + * if not null. + *

+ * The model transform of this bone is computed by combining the parent's + * model transform with this bones' local transform. + */ + public final void updateModelTransforms() { + modelTransform.set(localTransform); + if (parent != null) { + modelTransform.combineWithParent(parent.getModelTransform()); + } + + updateAttachNode(); + } + + /** + * Update the local transform of the attachments node. + */ + private void updateAttachNode() { + if (attachedNode == null) { + return; + } + Node attachParent = attachedNode.getParent(); + if (attachParent == null || targetGeometry == null + || targetGeometry.getParent() == attachParent + && targetGeometry.getLocalTransform().isIdentity()) { + /* + * The animated meshes are in the same coordinate system as the + * attachments node: no further transforms are needed. + */ + attachedNode.setLocalTransform(modelTransform); + + } else { + Spatial loopSpatial = targetGeometry; + Transform combined = modelTransform.clone(); + /* + * Climb the scene graph applying local transforms until the + * attachments node's parent is reached. + */ + while (loopSpatial != attachParent && loopSpatial != null) { + Transform localTransform = loopSpatial.getLocalTransform(); + combined.combineWithParent(localTransform); + loopSpatial = loopSpatial.getParent(); + } + attachedNode.setLocalTransform(combined); + } + } + + /** + * Stores the skinning transform in the specified Matrix4f. + * The skinning transform applies the animation of the bone to a vertex. + *

+ * This assumes that the world transforms for the entire bone hierarchy + * have already been computed, otherwise this method will return undefined + * results. + * + * @param outTransform + */ + void getOffsetTransform(Matrix4f outTransform, Quaternion tmp1, Vector3f tmp2, Vector3f tmp3, Matrix3f tmp4) { + modelTransform.toTransformMatrix(outTransform).mult(inverseModelBindMatrix, outTransform); + } + + protected void setBindPose() { + //Note that the whole Armature must be updated before calling this method. + modelTransform.toTransformMatrix(inverseModelBindMatrix); + inverseModelBindMatrix.invertLocal(); + } + + public Vector3f getLocalTranslation() { + return localTransform.getTranslation(); + } + + public Quaternion getLocalRotation() { + return localTransform.getRotation(); + } + + public Vector3f getLocalScale() { + return localTransform.getScale(); + } + + public void setLocalTranslation(Vector3f translation) { + localTransform.setTranslation(translation); + } + + public void setLocalRotation(Quaternion rotation) { + localTransform.setRotation(rotation); + } + + public void setLocalScale(Vector3f scale) { + localTransform.setScale(scale); + } + + public void addChild(Joint child) { + children.add(child); + child.parent = this; + } + + public void setName(String name) { + this.name = name; + } + + public void setLocalTransform(Transform localTransform) { + this.localTransform.set(localTransform); + } + + public void setInverseModelBindMatrix(Matrix4f inverseModelBindMatrix) { + this.inverseModelBindMatrix = inverseModelBindMatrix; + } + + public String getName() { + return name; + } + + public Joint getParent() { + return parent; + } + + public ArrayList getChildren() { + return children; + } + + /** + * Access the attachments node of this joint. If this joint doesn't already + * have an attachments node, create one. Models and effects attached to the + * attachments node will follow this bone's motions. + * + * @param jointIndex this bone's index in its armature (≥0) + * @param targets a list of geometries animated by this bone's skeleton (not + * null, unaffected) + */ + Node getAttachmentsNode(int jointIndex, SafeArrayList targets) { + targetGeometry = null; + /* + * Search for a geometry animated by this particular bone. + */ + for (Geometry geometry : targets) { + Mesh mesh = geometry.getMesh(); + if (mesh != null && mesh.isAnimatedByJoint(jointIndex)) { + targetGeometry = geometry; + break; + } + } + + if (attachedNode == null) { + attachedNode = new Node(name + "_attachnode"); + attachedNode.setUserData("AttachedBone", this); + //We don't want the node to have a numBone set by a parent node so we force it to null + attachedNode.addMatParamOverride(new MatParamOverride(VarType.Int, "NumberOfBones", null)); + } + + return attachedNode; + } + + + public Transform getLocalTransform() { + return localTransform; + } + + public Transform getModelTransform() { + return modelTransform; + } + + public Matrix4f getInverseModelBindMatrix() { + return inverseModelBindMatrix; + } + + @Override + public Object jmeClone() { + try { + Joint clone = (Joint) super.clone(); + return clone; + } catch (CloneNotSupportedException ex) { + throw new AssertionError(); + } + } + + @Override + public void cloneFields(Cloner cloner, Object original) { + this.children = cloner.clone(children); + this.attachedNode = cloner.clone(attachedNode); + this.targetGeometry = cloner.clone(targetGeometry); + + this.baseLocalTransform = cloner.clone(baseLocalTransform); + this.localTransform = cloner.clone(baseLocalTransform); + this.modelTransform = cloner.clone(baseLocalTransform); + this.inverseModelBindMatrix = cloner.clone(inverseModelBindMatrix); + } + + + @Override + @SuppressWarnings("unchecked") + public void read(JmeImporter im) throws IOException { + InputCapsule input = im.getCapsule(this); + + name = input.readString("name", null); + attachedNode = (Node) input.readSavable("attachedNode", null); + targetGeometry = (Geometry) input.readSavable("targetGeometry", null); + baseLocalTransform = (Transform) input.readSavable("baseLocalTransforms", baseLocalTransform); + localTransform.set(baseLocalTransform); + inverseModelBindMatrix = (Matrix4f) input.readSavable("inverseModelBindMatrix", inverseModelBindMatrix); + + ArrayList childList = input.readSavableArrayList("children", null); + for (int i = childList.size() - 1; i >= 0; i--) { + this.addChild(childList.get(i)); + } + } + + @Override + public void write(JmeExporter ex) throws IOException { + OutputCapsule output = ex.getCapsule(this); + + output.write(name, "name", null); + output.write(attachedNode, "attachedNode", null); + output.write(targetGeometry, "targetGeometry", null); + output.write(baseLocalTransform, "baseLocalTransform", new Transform()); + output.write(inverseModelBindMatrix, "inverseModelBindMatrix", new Matrix4f()); + output.writeSavableArrayList(children, "children", null); + } + +} diff --git a/jme3-core/src/main/java/com/jme3/math/Transform.java b/jme3-core/src/main/java/com/jme3/math/Transform.java index 6fb29c99b..f0a18bf91 100644 --- a/jme3-core/src/main/java/com/jme3/math/Transform.java +++ b/jme3-core/src/main/java/com/jme3/math/Transform.java @@ -32,6 +32,7 @@ package com.jme3.math; import com.jme3.export.*; + import java.io.IOException; /** @@ -257,11 +258,17 @@ public final class Transform implements Savable, Cloneable, java.io.Serializable } public Matrix4f toTransformMatrix() { - Matrix4f trans = new Matrix4f(); - trans.setTranslation(translation); - trans.setRotationQuaternion(rot); - trans.setScale(scale); - return trans; + return toTransformMatrix(null); + } + + public Matrix4f toTransformMatrix(Matrix4f store) { + if (store == null) { + store = new Matrix4f(); + } + store.setTranslation(translation); + store.setRotationQuaternion(rot); + store.setScale(scale); + return store; } public void fromTransformMatrix(Matrix4f mat) { diff --git a/jme3-core/src/main/java/com/jme3/scene/Mesh.java b/jme3-core/src/main/java/com/jme3/scene/Mesh.java index e07c2cfa2..e9fccb73e 100644 --- a/jme3-core/src/main/java/com/jme3/scene/Mesh.java +++ b/jme3-core/src/main/java/com/jme3/scene/Mesh.java @@ -39,18 +39,11 @@ import com.jme3.collision.bih.BIHTree; import com.jme3.export.*; import com.jme3.material.Material; import com.jme3.material.RenderState; -import com.jme3.math.Matrix4f; -import com.jme3.math.Triangle; -import com.jme3.math.Vector2f; -import com.jme3.math.Vector3f; -import com.jme3.scene.VertexBuffer.Format; -import com.jme3.scene.VertexBuffer.Type; -import com.jme3.scene.VertexBuffer.Usage; +import com.jme3.math.*; +import com.jme3.scene.VertexBuffer.*; import com.jme3.scene.mesh.*; -import com.jme3.util.BufferUtils; -import com.jme3.util.IntMap; +import com.jme3.util.*; import com.jme3.util.IntMap.Entry; -import com.jme3.util.SafeArrayList; import com.jme3.util.clone.Cloner; import com.jme3.util.clone.JmeCloneable; @@ -1446,13 +1439,23 @@ public class Mesh implements Savable, Cloneable, JmeCloneable { getBuffer(Type.HWBoneIndex) != null; } + /** + * @deprecated use isAnimatedByJoint + * @param boneIndex + * @return + */ + @Deprecated + public boolean isAnimatedByBone(int boneIndex) { + return isAnimatedByJoint(boneIndex); + } + /** * Test whether the specified bone animates this mesh. * - * @param boneIndex the bone's index in its skeleton + * @param jointIndex the bone's index in its skeleton * @return true if the specified bone animates this mesh, otherwise false */ - public boolean isAnimatedByBone(int boneIndex) { + public boolean isAnimatedByJoint(int jointIndex) { VertexBuffer biBuf = getBuffer(VertexBuffer.Type.BoneIndex); VertexBuffer wBuf = getBuffer(VertexBuffer.Type.BoneWeight); if (biBuf == null || wBuf == null) { @@ -1472,7 +1475,7 @@ public class Mesh implements Savable, Cloneable, JmeCloneable { /* * Test each vertex to determine whether the bone affects it. */ - int biByte = boneIndex; + int biByte = jointIndex; for (int vIndex = 0; vIndex < numVertices; vIndex++) { for (int wIndex = 0; wIndex < 4; wIndex++) { int bIndex = boneIndexBuffer.get();