com.jme3.anim.SkinningControl Maven / Gradle / Ivy
Show all versions of jme3-core Show documentation
/*
* Copyright (c) 2009-2023 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.anim;
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 Skinning control deforms a model according to an armature, It handles the
* computation of the deformation matrices and performs the transformations on
* the mesh
*
* It can perform software skinning or Hardware skinning
*
* @author Rémy Bouquet Based on SkeletonControl by Kirill Vainer
*/
public class SkinningControl extends AbstractControl implements Cloneable, JmeCloneable {
private static final Logger logger = Logger.getLogger(SkinningControl.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.
*/
protected SkinningControl() {
}
/**
* Creates an armature control. The list of targets will be acquired
* automatically when the control is attached to a node.
*
* @param armature the armature
*/
public SkinningControl(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 it's not preferred, or not
* supported by GPU, then it shall be disabled.
*
* @param preferred true to prefer hardware skinning, false to prefer
* software skinning (default=true)
* @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(SkinningControl.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;
armature.update();
}
//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);
FloatBuffer pb = (FloatBuffer) pos.getData();
FloatBuffer bpb = (FloatBuffer) bindPos.getData();
pb.clear();
bpb.clear();
// reset bind normals if there is a BindPoseNormal buffer
if (bindNorm != null) {
VertexBuffer norm = mesh.getBuffer(Type.Normal);
FloatBuffer nb = (FloatBuffer) norm.getData();
FloatBuffer bnb = (FloatBuffer) bindNorm.getData();
nb.clear();
bnb.clear();
nb.put(bnb).clear();
}
//resetting 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();
}
}
}
@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 the pre-existing instance
*/
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 matrices 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 mesh the mesh
* @param offsetMatrices the offsetMatrices 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 = (nb == null) ? null : (FloatBuffer) nb.getData();
if (fnb != null) {
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);
if (fnb != null) {
fnb.get(normBuf, 0, bufLength);
}
ftb.get(tanBuf, 0, tanLength);
int verts = bufLength / 3;
int idxPositions = 0;
// Tangents have their own index because they have 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);
if (fnb != null) {
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);
if (nb != null) {
nb.updateData(fnb);
}
tb.updateData(ftb);
}
/**
* Serialize this Control to the specified exporter, for example when saving
* to a J3O file.
*
* @param ex the exporter to write to (not null)
* @throws IOException from the exporter
*/
@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);
}
/**
* De-serialize this Control from the specified importer, for example when
* loading from a J3O file.
*
* @param im the importer to read from (not null)
* @throws IOException from the importer
*/
@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);
}
}
}