|
|
|
@ -1,40 +1,44 @@ |
|
|
|
|
package com.jme3.scene.plugins.blender.constraints.definitions; |
|
|
|
|
|
|
|
|
|
import java.util.ArrayList; |
|
|
|
|
import java.util.Collections; |
|
|
|
|
import java.util.Collection; |
|
|
|
|
import java.util.HashSet; |
|
|
|
|
import java.util.List; |
|
|
|
|
|
|
|
|
|
import org.ejml.simple.SimpleMatrix; |
|
|
|
|
|
|
|
|
|
import com.jme3.animation.Bone; |
|
|
|
|
import com.jme3.math.Transform; |
|
|
|
|
import com.jme3.math.Vector3f; |
|
|
|
|
import com.jme3.scene.plugins.blender.BlenderContext; |
|
|
|
|
import com.jme3.scene.plugins.blender.animations.BoneContext; |
|
|
|
|
import com.jme3.scene.plugins.blender.constraints.ConstraintHelper; |
|
|
|
|
import com.jme3.scene.plugins.blender.constraints.ConstraintHelper.Space; |
|
|
|
|
import com.jme3.scene.plugins.blender.file.Structure; |
|
|
|
|
import com.jme3.scene.plugins.blender.math.DQuaternion; |
|
|
|
|
import com.jme3.scene.plugins.blender.math.DTransform; |
|
|
|
|
import com.jme3.scene.plugins.blender.math.Matrix; |
|
|
|
|
import com.jme3.scene.plugins.blender.math.Vector3d; |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* The Inverse Kinematics constraint. |
|
|
|
|
* A definiotion of a Inverse Kinematics constraint. This implementation uses Jacobian pseudoinverse algorithm. |
|
|
|
|
* |
|
|
|
|
* @author Wesley Shillingford (wezrule) |
|
|
|
|
* @author Marcin Roguski (Kaelthas) |
|
|
|
|
*/ |
|
|
|
|
public class ConstraintDefinitionIK extends ConstraintDefinition { |
|
|
|
|
private static final float MIN_DISTANCE = 0.0001f; |
|
|
|
|
private static final float MIN_DISTANCE = 0.001f; |
|
|
|
|
private static final float MIN_ANGLE_CHANGE = 0.001f; |
|
|
|
|
private static final int FLAG_USE_TAIL = 0x01; |
|
|
|
|
private static final int FLAG_POSITION = 0x20; |
|
|
|
|
|
|
|
|
|
private BonesChain bones; |
|
|
|
|
/** The number of affected bones. Zero means that all parent bones of the current bone should take part in baking. */ |
|
|
|
|
private int bonesAffected; |
|
|
|
|
/** The total length of the bone chain. Useful for optimisation of computations speed in some cases. */ |
|
|
|
|
private double chainLength; |
|
|
|
|
/** Indicates if the tail of the bone should be used or not. */ |
|
|
|
|
private boolean useTail; |
|
|
|
|
/** The amount of iterations of the algorithm. */ |
|
|
|
|
private int iterations; |
|
|
|
|
/** The count of bones' chain. */ |
|
|
|
|
private int bonesCount = -1; |
|
|
|
|
|
|
|
|
|
public ConstraintDefinitionIK(Structure constraintData, Long ownerOMA, BlenderContext blenderContext) { |
|
|
|
|
super(constraintData, ownerOMA, blenderContext); |
|
|
|
@ -51,122 +55,104 @@ public class ConstraintDefinitionIK extends ConstraintDefinition { |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Below are the variables that only need to be allocated once for IK constraint instance. |
|
|
|
|
*/ |
|
|
|
|
/** Temporal quaternion. */ |
|
|
|
|
private DQuaternion tempDQuaternion = new DQuaternion(); |
|
|
|
|
/** Temporal matrix column. */ |
|
|
|
|
private Vector3d col = new Vector3d(); |
|
|
|
|
/** Effector's position change. */ |
|
|
|
|
private Matrix deltaP = new Matrix(3, 1); |
|
|
|
|
/** The current target position. */ |
|
|
|
|
private Vector3d target = new Vector3d(); |
|
|
|
|
/** Rotation vectors for each joint (allocated when we know the size of a bones' chain. */ |
|
|
|
|
private Vector3d[] rotationVectors; |
|
|
|
|
/** The Jacobian matrix. Allocated when the bones' chain size is known. */ |
|
|
|
|
private Matrix J; |
|
|
|
|
|
|
|
|
|
@Override |
|
|
|
|
public void bake(Space ownerSpace, Space targetSpace, Transform targetTransform, float influence) { |
|
|
|
|
if (influence == 0 || !trackToBeChanged || targetTransform == null) { |
|
|
|
|
if (influence == 0 || !trackToBeChanged || targetTransform == null || bonesCount == 0) { |
|
|
|
|
return;// no need to do anything
|
|
|
|
|
} |
|
|
|
|
DQuaternion q = new DQuaternion(); |
|
|
|
|
Vector3d t = new Vector3d(targetTransform.getTranslation()); |
|
|
|
|
List<BoneContext> bones = this.loadBones(); |
|
|
|
|
|
|
|
|
|
if (bones == null) { |
|
|
|
|
bones = new BonesChain((Bone) this.getOwner(), useTail, bonesAffected, alteredOmas, blenderContext); |
|
|
|
|
} |
|
|
|
|
if (bones.size() == 0) { |
|
|
|
|
bonesCount = 0; |
|
|
|
|
return;// no need to do anything
|
|
|
|
|
} |
|
|
|
|
double distanceFromTarget = Double.MAX_VALUE; |
|
|
|
|
target.set(targetTransform.getTranslation().x, targetTransform.getTranslation().y, targetTransform.getTranslation().z); |
|
|
|
|
|
|
|
|
|
int iterations = this.iterations; |
|
|
|
|
if (bones.size() == 1) { |
|
|
|
|
iterations = 1;// if only one bone is in the chain then only one iteration that will properly rotate it will be needed
|
|
|
|
|
} else { |
|
|
|
|
// if the target cannot be rached by the bones' chain then the chain will become straight and point towards the target
|
|
|
|
|
// in this case only one iteration will be needed, computed from the root to top bone
|
|
|
|
|
BoneContext rootBone = bones.get(bones.size() - 1); |
|
|
|
|
Transform rootBoneTransform = constraintHelper.getTransform(rootBone.getArmatureObjectOMA(), rootBone.getBone().getName(), Space.CONSTRAINT_SPACE_WORLD); |
|
|
|
|
if (t.distance(new Vector3d(rootBoneTransform.getTranslation())) >= chainLength) { |
|
|
|
|
Collections.reverse(bones); |
|
|
|
|
|
|
|
|
|
for (BoneContext boneContext : bones) { |
|
|
|
|
Bone bone = boneContext.getBone(); |
|
|
|
|
DTransform boneTransform = new DTransform(constraintHelper.getTransform(boneContext.getArmatureObjectOMA(), bone.getName(), Space.CONSTRAINT_SPACE_WORLD)); |
|
|
|
|
|
|
|
|
|
Vector3d e = boneTransform.getTranslation().add(boneTransform.getRotation().mult(Vector3d.UNIT_Y).multLocal(boneContext.getLength()));// effector
|
|
|
|
|
Vector3d j = boneTransform.getTranslation(); // current join position
|
|
|
|
|
|
|
|
|
|
Vector3d currentDir = e.subtractLocal(j).normalizeLocal(); |
|
|
|
|
Vector3d target = t.subtract(j).normalizeLocal(); |
|
|
|
|
double angle = currentDir.angleBetween(target); |
|
|
|
|
if (angle != 0) { |
|
|
|
|
Vector3d cross = currentDir.crossLocal(target).normalizeLocal(); |
|
|
|
|
q.fromAngleAxis(angle, cross); |
|
|
|
|
|
|
|
|
|
if(bone.equals(this.getOwner())) { |
|
|
|
|
if (boneContext.isLockX()) { |
|
|
|
|
q.set(0, q.getY(), q.getZ(), q.getW()); |
|
|
|
|
} |
|
|
|
|
if (boneContext.isLockY()) { |
|
|
|
|
q.set(q.getX(), 0, q.getZ(), q.getW()); |
|
|
|
|
} |
|
|
|
|
if (boneContext.isLockZ()) { |
|
|
|
|
q.set(q.getX(), q.getY(), 0, q.getW()); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
boneTransform.getRotation().set(q.multLocal(boneTransform.getRotation())); |
|
|
|
|
constraintHelper.applyTransform(boneContext.getArmatureObjectOMA(), bone.getName(), Space.CONSTRAINT_SPACE_WORLD, boneTransform.toTransform()); |
|
|
|
|
if (bonesCount < 0) { |
|
|
|
|
bonesCount = bones.size(); |
|
|
|
|
rotationVectors = new Vector3d[bonesCount]; |
|
|
|
|
for (int i = 0; i < bonesCount; ++i) { |
|
|
|
|
rotationVectors[i] = new Vector3d(); |
|
|
|
|
} |
|
|
|
|
J = new Matrix(3, bonesCount); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
iterations = 0; |
|
|
|
|
} |
|
|
|
|
BoneContext topBone = bones.get(0); |
|
|
|
|
for (int i = 0; i < iterations; ++i) { |
|
|
|
|
DTransform topBoneTransform = bones.getWorldTransform(topBone); |
|
|
|
|
Vector3d e = topBoneTransform.getTranslation().add(topBoneTransform.getRotation().mult(Vector3d.UNIT_Y).multLocal(topBone.getLength()));// effector
|
|
|
|
|
distanceFromTarget = e.distance(target); |
|
|
|
|
if (distanceFromTarget <= MIN_DISTANCE) { |
|
|
|
|
break; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
List<Transform> bestSolution = new ArrayList<Transform>(bones.size()); |
|
|
|
|
double bestSolutionDistance = Double.MAX_VALUE; |
|
|
|
|
BoneContext topBone = bones.get(0); |
|
|
|
|
for (int i = 0; i < iterations && distanceFromTarget > MIN_DISTANCE; ++i) { |
|
|
|
|
deltaP.setColumn(0, 0, target.x - e.x, target.y - e.y, target.z - e.z); |
|
|
|
|
int column = 0; |
|
|
|
|
for (BoneContext boneContext : bones) { |
|
|
|
|
Bone bone = boneContext.getBone(); |
|
|
|
|
DTransform topBoneTransform = new DTransform(constraintHelper.getTransform(topBone.getArmatureObjectOMA(), topBone.getBone().getName(), Space.CONSTRAINT_SPACE_WORLD)); |
|
|
|
|
DTransform boneWorldTransform = new DTransform(constraintHelper.getTransform(boneContext.getArmatureObjectOMA(), bone.getName(), Space.CONSTRAINT_SPACE_WORLD)); |
|
|
|
|
|
|
|
|
|
Vector3d e = topBoneTransform.getTranslation().addLocal(topBoneTransform.getRotation().mult(Vector3d.UNIT_Y).multLocal(topBone.getLength()));// effector
|
|
|
|
|
DTransform boneWorldTransform = bones.getWorldTransform(boneContext); |
|
|
|
|
Vector3d j = boneWorldTransform.getTranslation(); // current join position
|
|
|
|
|
Vector3d vectorFromJointToEffector = e.subtract(j); |
|
|
|
|
vectorFromJointToEffector.cross(target.subtract(j), rotationVectors[column]).normalizeLocal(); |
|
|
|
|
rotationVectors[column].cross(vectorFromJointToEffector, col); |
|
|
|
|
J.setColumn(col, column++); |
|
|
|
|
} |
|
|
|
|
Matrix J_1 = J.pseudoinverse(); |
|
|
|
|
|
|
|
|
|
Vector3d currentDir = e.subtractLocal(j).normalizeLocal(); |
|
|
|
|
Vector3d target = t.subtract(j).normalizeLocal(); |
|
|
|
|
double angle = currentDir.angleBetween(target); |
|
|
|
|
if (angle != 0) { |
|
|
|
|
Vector3d cross = currentDir.crossLocal(target).normalizeLocal(); |
|
|
|
|
q.fromAngleAxis(angle, cross); |
|
|
|
|
SimpleMatrix deltaThetas = J_1.mult(deltaP); |
|
|
|
|
if (deltaThetas.elementMaxAbs() < MIN_ANGLE_CHANGE) { |
|
|
|
|
break; |
|
|
|
|
} |
|
|
|
|
for (int j = 0; j < deltaThetas.numRows(); ++j) { |
|
|
|
|
double angle = deltaThetas.get(j, 0); |
|
|
|
|
Vector3d rotationVector = rotationVectors[j]; |
|
|
|
|
|
|
|
|
|
tempDQuaternion.fromAngleAxis(angle, rotationVector); |
|
|
|
|
BoneContext boneContext = bones.get(j); |
|
|
|
|
Bone bone = boneContext.getBone(); |
|
|
|
|
if (bone.equals(this.getOwner())) { |
|
|
|
|
if (boneContext.isLockX()) { |
|
|
|
|
q.set(0, q.getY(), q.getZ(), q.getW()); |
|
|
|
|
tempDQuaternion.set(0, tempDQuaternion.getY(), tempDQuaternion.getZ(), tempDQuaternion.getW()); |
|
|
|
|
} |
|
|
|
|
if (boneContext.isLockY()) { |
|
|
|
|
q.set(q.getX(), 0, q.getZ(), q.getW()); |
|
|
|
|
tempDQuaternion.set(tempDQuaternion.getX(), 0, tempDQuaternion.getZ(), tempDQuaternion.getW()); |
|
|
|
|
} |
|
|
|
|
if (boneContext.isLockZ()) { |
|
|
|
|
q.set(q.getX(), q.getY(), 0, q.getW()); |
|
|
|
|
tempDQuaternion.set(tempDQuaternion.getX(), tempDQuaternion.getY(), 0, tempDQuaternion.getW()); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
boneWorldTransform.getRotation().set(q.multLocal(boneWorldTransform.getRotation())); |
|
|
|
|
constraintHelper.applyTransform(boneContext.getArmatureObjectOMA(), bone.getName(), Space.CONSTRAINT_SPACE_WORLD, boneWorldTransform.toTransform()); |
|
|
|
|
} else { |
|
|
|
|
iterations = 0; |
|
|
|
|
break; |
|
|
|
|
DTransform boneTransform = bones.getWorldTransform(boneContext); |
|
|
|
|
boneTransform.getRotation().set(tempDQuaternion.mult(boneTransform.getRotation())); |
|
|
|
|
bones.setWorldTransform(boneContext, boneTransform); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
DTransform topBoneTransform = new DTransform(constraintHelper.getTransform(topBone.getArmatureObjectOMA(), topBone.getBone().getName(), Space.CONSTRAINT_SPACE_WORLD)); |
|
|
|
|
Vector3d e = topBoneTransform.getTranslation().addLocal(topBoneTransform.getRotation().mult(Vector3d.UNIT_Y).multLocal(topBone.getLength()));// effector
|
|
|
|
|
distanceFromTarget = e.distance(t); |
|
|
|
|
|
|
|
|
|
if(distanceFromTarget < bestSolutionDistance) { |
|
|
|
|
bestSolutionDistance = distanceFromTarget; |
|
|
|
|
bestSolution.clear(); |
|
|
|
|
for(BoneContext boneContext : bones) { |
|
|
|
|
bestSolution.add(constraintHelper.getTransform(boneContext.getArmatureObjectOMA(), boneContext.getBone().getName(), Space.CONSTRAINT_SPACE_WORLD)); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// applying best solution
|
|
|
|
|
for(int i=0;i<bestSolution.size();++i) { |
|
|
|
|
// applying the results
|
|
|
|
|
for (int i = bonesCount - 1; i >= 0; --i) { |
|
|
|
|
BoneContext boneContext = bones.get(i); |
|
|
|
|
constraintHelper.applyTransform(boneContext.getArmatureObjectOMA(), boneContext.getBone().getName(), Space.CONSTRAINT_SPACE_WORLD, bestSolution.get(i)); |
|
|
|
|
DTransform transform = bones.getWorldTransform(boneContext); |
|
|
|
|
constraintHelper.applyTransform(boneContext.getArmatureObjectOMA(), boneContext.getBone().getName(), Space.CONSTRAINT_SPACE_WORLD, transform.toTransform()); |
|
|
|
|
} |
|
|
|
|
bones = null;// need to reload them again
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@Override |
|
|
|
@ -174,56 +160,68 @@ public class ConstraintDefinitionIK extends ConstraintDefinition { |
|
|
|
|
return "Inverse kinematics"; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@Override |
|
|
|
|
public boolean isTargetRequired() { |
|
|
|
|
return true; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* @return the bone contexts of all bones that will be used in this constraint computations |
|
|
|
|
* Loaded bones' chain. This class allows to operate on transform matrices that use double precision in computations. |
|
|
|
|
* Only the final result is being transformed to single precision numbers. |
|
|
|
|
* |
|
|
|
|
* @author Marcin Roguski (Kaelthas) |
|
|
|
|
*/ |
|
|
|
|
private List<BoneContext> loadBones() { |
|
|
|
|
List<BoneContext> bones = new ArrayList<BoneContext>(); |
|
|
|
|
Bone bone = (Bone) this.getOwner(); |
|
|
|
|
if (bone == null) { |
|
|
|
|
return bones; |
|
|
|
|
} |
|
|
|
|
private static class BonesChain extends ArrayList<BoneContext> { |
|
|
|
|
private static final long serialVersionUID = -1850524345643600718L; |
|
|
|
|
|
|
|
|
|
private List<Matrix> bonesMatrices = new ArrayList<Matrix>(); |
|
|
|
|
|
|
|
|
|
public BonesChain(Bone bone, boolean useTail, int bonesAffected, Collection<Long> alteredOmas, BlenderContext blenderContext) { |
|
|
|
|
if (bone != null) { |
|
|
|
|
ConstraintHelper constraintHelper = blenderContext.getHelper(ConstraintHelper.class); |
|
|
|
|
if (!useTail) { |
|
|
|
|
bone = bone.getParent(); |
|
|
|
|
} |
|
|
|
|
chainLength = 0; |
|
|
|
|
while (bone != null) { |
|
|
|
|
while (bone != null && (bonesAffected <= 0 || this.size() < bonesAffected)) { |
|
|
|
|
BoneContext boneContext = blenderContext.getBoneContext(bone); |
|
|
|
|
chainLength += boneContext.getLength(); |
|
|
|
|
bones.add(boneContext); |
|
|
|
|
this.add(boneContext); |
|
|
|
|
alteredOmas.add(boneContext.getBoneOma()); |
|
|
|
|
if (bonesAffected != 0 && bones.size() >= bonesAffected) { |
|
|
|
|
break; |
|
|
|
|
} |
|
|
|
|
// need to add spaces between bones to the chain length
|
|
|
|
|
Transform boneWorldTransform = constraintHelper.getTransform(boneContext.getArmatureObjectOMA(), boneContext.getBone().getName(), Space.CONSTRAINT_SPACE_WORLD); |
|
|
|
|
Vector3f boneWorldTranslation = boneWorldTransform.getTranslation(); |
|
|
|
|
|
|
|
|
|
bone = bone.getParent(); |
|
|
|
|
Space space = this.size() < bonesAffected ? Space.CONSTRAINT_SPACE_LOCAL : Space.CONSTRAINT_SPACE_WORLD; |
|
|
|
|
Transform transform = constraintHelper.getTransform(boneContext.getArmatureObjectOMA(), boneContext.getBone().getName(), space); |
|
|
|
|
bonesMatrices.add(new DTransform(transform).toMatrix()); |
|
|
|
|
|
|
|
|
|
if (bone != null) { |
|
|
|
|
boneContext = blenderContext.getBoneContext(bone); |
|
|
|
|
Transform parentWorldTransform = constraintHelper.getTransform(boneContext.getArmatureObjectOMA(), boneContext.getBone().getName(), Space.CONSTRAINT_SPACE_WORLD); |
|
|
|
|
Vector3f parentWorldTranslation = parentWorldTransform.getTranslation(); |
|
|
|
|
chainLength += boneWorldTranslation.distance(parentWorldTranslation); |
|
|
|
|
bone = bone.getParent(); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
return bones; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@Override |
|
|
|
|
public boolean isTrackToBeChanged() { |
|
|
|
|
if (trackToBeChanged) { |
|
|
|
|
// need to check the bone structure too (when constructor was called not all of the bones might have been loaded yet)
|
|
|
|
|
// that is why it is also checked here
|
|
|
|
|
List<BoneContext> bones = this.loadBones(); |
|
|
|
|
trackToBeChanged = bones.size() > 0; |
|
|
|
|
public DTransform getWorldTransform(BoneContext bone) { |
|
|
|
|
int index = this.indexOf(bone); |
|
|
|
|
return this.getWorldMatrix(index).toTransform(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public void setWorldTransform(BoneContext bone, DTransform transform) { |
|
|
|
|
int index = this.indexOf(bone); |
|
|
|
|
Matrix boneMatrix = transform.toMatrix(); |
|
|
|
|
|
|
|
|
|
if (index < this.size() - 1) { |
|
|
|
|
// computing the current bone local transform
|
|
|
|
|
Matrix parentWorldMatrix = this.getWorldMatrix(index + 1); |
|
|
|
|
SimpleMatrix m = parentWorldMatrix.invert().mult(boneMatrix); |
|
|
|
|
boneMatrix = new Matrix(m); |
|
|
|
|
} |
|
|
|
|
return trackToBeChanged; |
|
|
|
|
bonesMatrices.set(index, boneMatrix); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@Override |
|
|
|
|
public boolean isTargetRequired() { |
|
|
|
|
return true; |
|
|
|
|
public Matrix getWorldMatrix(int index) { |
|
|
|
|
if (index == this.size() - 1) { |
|
|
|
|
return new Matrix(bonesMatrices.get(this.size() - 1)); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
SimpleMatrix result = this.getWorldMatrix(index + 1); |
|
|
|
|
result = result.mult(bonesMatrices.get(index)); |
|
|
|
|
return new Matrix(result); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|