From 697a02e7c530c936f62e6e75f0eb35a5113cf792 Mon Sep 17 00:00:00 2001 From: "Kae..pl" Date: Tue, 21 Jun 2011 19:00:26 +0000 Subject: [PATCH] Support for object animation constraints loading. git-svn-id: https://jmonkeyengine.googlecode.com/svn/trunk@7684 75d07b2b-3a1a-0410-a2c5-0572b91ccdca --- .../helpers/v249/ConstraintHelper.java | 1346 +++++++++-------- .../blender/helpers/v249/ModifierHelper.java | 10 +- .../blender/helpers/v249/ObjectHelper.java | 2 +- .../structures/AbstractInfluenceFunction.java | 2 +- .../test-data/Blender/2.4x/constraints.blend | Bin 136360 -> 145376 bytes 5 files changed, 685 insertions(+), 675 deletions(-) diff --git a/engine/src/blender/com/jme3/scene/plugins/blender/helpers/v249/ConstraintHelper.java b/engine/src/blender/com/jme3/scene/plugins/blender/helpers/v249/ConstraintHelper.java index edd362227..b0c9755dc 100644 --- a/engine/src/blender/com/jme3/scene/plugins/blender/helpers/v249/ConstraintHelper.java +++ b/engine/src/blender/com/jme3/scene/plugins/blender/helpers/v249/ConstraintHelper.java @@ -32,121 +32,121 @@ import java.util.logging.Level; */ public class ConstraintHelper extends AbstractBlenderHelper { - /** - * A table containing implementations of influence functions for constraints. It should contain functions for - * blender at least 249 and higher. - */ - protected static AbstractInfluenceFunction[] influenceFunctions; - /** - * Constraints stored for object with the given old memory address. - */ - protected Map constraints = new HashMap(); - - /** - * Helper constructor. It's main task is to generate the affection functions. These functions are common to all - * ConstraintHelper instances. Unfortunately this constructor might grow large. If it becomes too large - I shall - * consider refactoring. The constructor parses the given blender version and stores the result. Some - * functionalities may differ in different blender versions. - * @param blenderVersion - * the version read from the blend file - */ - public ConstraintHelper(String blenderVersion, DataRepository dataRepository) { - super(blenderVersion); - this.initializeConstraintFunctions(dataRepository); - } + /** + * A table containing implementations of influence functions for constraints. It should contain functions for + * blender at least 249 and higher. + */ + protected static AbstractInfluenceFunction[] influenceFunctions; + /** + * Constraints stored for object with the given old memory address. + */ + protected Map constraints = new HashMap(); + + /** + * Helper constructor. It's main task is to generate the affection functions. These functions are common to all + * ConstraintHelper instances. Unfortunately this constructor might grow large. If it becomes too large - I shall + * consider refactoring. The constructor parses the given blender version and stores the result. Some + * functionalities may differ in different blender versions. + * @param blenderVersion + * the version read from the blend file + */ + public ConstraintHelper(String blenderVersion, DataRepository dataRepository) { + super(blenderVersion); + this.initializeConstraintFunctions(dataRepository); + } /** * This method initializes constraint functions for Blender 2.49. * @param dataRepository * the data repository */ - private synchronized void initializeConstraintFunctions(DataRepository dataRepository) { - if (influenceFunctions == null) { - influenceFunctions = new AbstractInfluenceFunction[ConstraintType.getLastDefinedTypeValue() + 1]; - //ACTION constraint (TODO: to implement) - influenceFunctions[ConstraintType.CONSTRAINT_TYPE_ACTION.getConstraintId()] = new AbstractInfluenceFunction(ConstraintType.CONSTRAINT_TYPE_ACTION, dataRepository) { - }; - - //CHILDOF constraint (TODO: to implement) - influenceFunctions[ConstraintType.CONSTRAINT_TYPE_CHILDOF.getConstraintId()] = new AbstractInfluenceFunction(ConstraintType.CONSTRAINT_TYPE_CHILDOF, dataRepository) { - }; - - //CLAMPTO constraint - influenceFunctions[ConstraintType.CONSTRAINT_TYPE_CLAMPTO.getConstraintId()] = new AbstractInfluenceFunction(ConstraintType.CONSTRAINT_TYPE_CLAMPTO, dataRepository) { - - @Override - public void affectAnimation(Skeleton skeleton, BoneAnimation boneAnimation, Constraint constraint) { - this.validateConstraintType(constraint.getData()); - LOGGER.log(Level.INFO, "{0} not active! Curves not yet implemented!", constraint.getName());//TODO: implement when curves are implemented - } - }; - - //DISTLIMIT constraint - influenceFunctions[ConstraintType.CONSTRAINT_TYPE_DISTLIMIT.getConstraintId()] = new AbstractInfluenceFunction(ConstraintType.CONSTRAINT_TYPE_DISTLIMIT, dataRepository) { - - @Override - public void affectAnimation(Skeleton skeleton, BoneAnimation boneAnimation, Constraint constraint) { - Structure constraintStructure = constraint.getData(); - this.validateConstraintType(constraintStructure); - Vector3f targetLocation = this.getTargetLocation(constraint); - BoneTrack boneTrack = this.getBoneTrack(skeleton, boneAnimation, constraint); - if (boneTrack != null) { - //TODO: target vertex group !!! - float dist = ((Number) constraintStructure.getFieldValue("dist")).floatValue(); - int mode = ((Number) constraintStructure.getFieldValue("mode")).intValue(); - - int maxFrames = boneTrack.getTimes().length; - Vector3f[] translations = boneTrack.getTranslations(); - for (int frame = 0; frame < maxFrames; ++frame) { - Vector3f v = translations[frame].subtract(targetLocation); - float currentDistance = v.length(); - float influence = constraint.getIpo().calculateValue(frame); - float modifier = 0.0f; - switch (mode) { - case LIMITDIST_INSIDE: - if (currentDistance >= dist) { - modifier = (dist - currentDistance) / currentDistance; - } - break; - case LIMITDIST_ONSURFACE: - modifier = (dist - currentDistance) / currentDistance; - break; - case LIMITDIST_OUTSIDE: - if (currentDistance <= dist) { - modifier = (dist - currentDistance) / currentDistance; - } - break; - default: - throw new IllegalStateException("Unknown distance limit constraint mode: " + mode); - } - translations[frame].addLocal(v.multLocal(modifier * influence)); - } - boneTrack.setKeyframes(boneTrack.getTimes(), translations, boneTrack.getRotations(), boneTrack.getScales()); - } - } - }; - - //FOLLOWPATH constraint - influenceFunctions[ConstraintType.CONSTRAINT_TYPE_FOLLOWPATH.getConstraintId()] = new AbstractInfluenceFunction(ConstraintType.CONSTRAINT_TYPE_FOLLOWPATH, dataRepository) { - - @Override - public void affectAnimation(Skeleton skeleton, BoneAnimation boneAnimation, Constraint constraint) { - this.validateConstraintType(constraint.getData()); - LOGGER.log(Level.INFO, "{0} not active! Curves not yet implemented!", constraint.getName());//TODO: implement when curves are implemented - } - }; - - //KINEMATIC constraint (TODO: to implement) - influenceFunctions[ConstraintType.CONSTRAINT_TYPE_KINEMATIC.getConstraintId()] = new AbstractInfluenceFunction(ConstraintType.CONSTRAINT_TYPE_KINEMATIC, dataRepository) { - - @Override - public void affectAnimation(Skeleton skeleton, BoneAnimation boneAnimation, Constraint constraint) { - Structure constraintStructure = constraint.getData(); - this.validateConstraintType(constraintStructure); - /*Long boneOMA = constraint.getBoneOMA(); + private synchronized void initializeConstraintFunctions(DataRepository dataRepository) { + if (influenceFunctions == null) { + influenceFunctions = new AbstractInfluenceFunction[ConstraintType.getLastDefinedTypeValue() + 1]; + //ACTION constraint (TODO: to implement) + influenceFunctions[ConstraintType.CONSTRAINT_TYPE_ACTION.getConstraintId()] = new AbstractInfluenceFunction(ConstraintType.CONSTRAINT_TYPE_ACTION, dataRepository) { + }; + + //CHILDOF constraint (TODO: to implement) + influenceFunctions[ConstraintType.CONSTRAINT_TYPE_CHILDOF.getConstraintId()] = new AbstractInfluenceFunction(ConstraintType.CONSTRAINT_TYPE_CHILDOF, dataRepository) { + }; + + //CLAMPTO constraint + influenceFunctions[ConstraintType.CONSTRAINT_TYPE_CLAMPTO.getConstraintId()] = new AbstractInfluenceFunction(ConstraintType.CONSTRAINT_TYPE_CLAMPTO, dataRepository) { + + @Override + public void affectAnimation(Skeleton skeleton, BoneAnimation boneAnimation, Constraint constraint) { + this.validateConstraintType(constraint.getData()); + LOGGER.log(Level.INFO, "{0} not active! Curves not yet implemented!", constraint.getName());//TODO: implement when curves are implemented + } + }; + + //DISTLIMIT constraint + influenceFunctions[ConstraintType.CONSTRAINT_TYPE_DISTLIMIT.getConstraintId()] = new AbstractInfluenceFunction(ConstraintType.CONSTRAINT_TYPE_DISTLIMIT, dataRepository) { + + @Override + public void affectAnimation(Skeleton skeleton, BoneAnimation boneAnimation, Constraint constraint) { + Structure constraintStructure = constraint.getData(); + this.validateConstraintType(constraintStructure); + Vector3f targetLocation = this.getTargetLocation(constraint); + BoneTrack boneTrack = this.getBoneTrack(skeleton, boneAnimation, constraint); + if (boneTrack != null) { + //TODO: target vertex group !!! + float dist = ((Number) constraintStructure.getFieldValue("dist")).floatValue(); + int mode = ((Number) constraintStructure.getFieldValue("mode")).intValue(); + + int maxFrames = boneTrack.getTimes().length; + Vector3f[] translations = boneTrack.getTranslations(); + for (int frame = 0; frame < maxFrames; ++frame) { + Vector3f v = translations[frame].subtract(targetLocation); + float currentDistance = v.length(); + float influence = constraint.getIpo().calculateValue(frame); + float modifier = 0.0f; + switch (mode) { + case LIMITDIST_INSIDE: + if (currentDistance >= dist) { + modifier = (dist - currentDistance) / currentDistance; + } + break; + case LIMITDIST_ONSURFACE: + modifier = (dist - currentDistance) / currentDistance; + break; + case LIMITDIST_OUTSIDE: + if (currentDistance <= dist) { + modifier = (dist - currentDistance) / currentDistance; + } + break; + default: + throw new IllegalStateException("Unknown distance limit constraint mode: " + mode); + } + translations[frame].addLocal(v.multLocal(modifier * influence)); + } + boneTrack.setKeyframes(boneTrack.getTimes(), translations, boneTrack.getRotations(), boneTrack.getScales()); + } + } + }; + + //FOLLOWPATH constraint + influenceFunctions[ConstraintType.CONSTRAINT_TYPE_FOLLOWPATH.getConstraintId()] = new AbstractInfluenceFunction(ConstraintType.CONSTRAINT_TYPE_FOLLOWPATH, dataRepository) { + + @Override + public void affectAnimation(Skeleton skeleton, BoneAnimation boneAnimation, Constraint constraint) { + this.validateConstraintType(constraint.getData()); + LOGGER.log(Level.INFO, "{0} not active! Curves not yet implemented!", constraint.getName());//TODO: implement when curves are implemented + } + }; + + //KINEMATIC constraint (TODO: to implement) + influenceFunctions[ConstraintType.CONSTRAINT_TYPE_KINEMATIC.getConstraintId()] = new AbstractInfluenceFunction(ConstraintType.CONSTRAINT_TYPE_KINEMATIC, dataRepository) { + + @Override + public void affectAnimation(Skeleton skeleton, BoneAnimation boneAnimation, Constraint constraint) { + Structure constraintStructure = constraint.getData(); + this.validateConstraintType(constraintStructure); + /*Long boneOMA = constraint.getBoneOMA(); //IK solver is only attached to bones Bone ownerBone = (Bone)dataRepository.getLoadedFeature(boneOMA, LoadedFeatureDataType.LOADED_FEATURE); - + //get the target point Object targetObject = this.getTarget(constraint, LoadedFeatureDataType.LOADED_FEATURE); Vector3f pt = null;//Point Target @@ -172,7 +172,7 @@ public class ConstraintHelper extends AbstractBlenderHelper { } Quaternion rotation = new Quaternion(); int maxFrames = bones[0].track.getTimes().length;//all tracks should have the same amount of frames - + for(int frame = 0; frame < maxFrames; ++frame) { float error = IK_SOLVER_ERROR; int iteration = 0; @@ -181,10 +181,10 @@ public class ConstraintHelper extends AbstractBlenderHelper { for(int i = 0; i < bones.length - 1; ++i) { Vector3f pe = bones[i].getEndPoint(); Vector3f pc = bones[i + 1].getWorldTranslation().clone(); - + Vector3f peSUBpc = pe.subtract(pc).normalizeLocal(); Vector3f ptSUBpc = pt.subtract(pc).normalizeLocal(); - + float theta = FastMath.acos(peSUBpc.dot(ptSUBpc)); Vector3f direction = peSUBpc.cross(ptSUBpc).normalizeLocal(); bones[i].rotate(rotation.fromAngleAxis(theta, direction), frame); @@ -194,570 +194,582 @@ public class ConstraintHelper extends AbstractBlenderHelper { } System.out.println("error = " + error + " iterations = " + iteration); } - + for(CalculationBone bone : bones) { bone.applyCalculatedTracks(); } - + System.out.println("&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&"); for(int i=0;i bonesList = new ArrayList(); - Bone currentBone = bone; - do { - int boneIndex = skeleton.getBoneIndex(currentBone); - for (int i = 0; i < boneAnimation.getTracks().length; ++i) { - if (boneAnimation.getTracks()[i].getTargetBoneIndex() == boneIndex) { - bonesList.add(new CalculationBone(currentBone, boneAnimation.getTracks()[i])); - break; - } - } - currentBone = currentBone.getParent(); - } while (currentBone != null); - //attaching children - CalculationBone[] result = bonesList.toArray(new CalculationBone[bonesList.size()]); - for (int i = result.length - 1; i > 0; --i) { - result[i].attachChild(result[i - 1]); - } - return result; - } - }; - - //LOCKTRACK constraint (TODO: to implement) - influenceFunctions[ConstraintType.CONSTRAINT_TYPE_LOCKTRACK.getConstraintId()] = new AbstractInfluenceFunction(ConstraintType.CONSTRAINT_TYPE_LOCKTRACK, dataRepository) { - }; - - //LOCLIKE constraint - influenceFunctions[ConstraintType.CONSTRAINT_TYPE_LOCLIKE.getConstraintId()] = new AbstractInfluenceFunction(ConstraintType.CONSTRAINT_TYPE_LOCLIKE, dataRepository) { - - @Override - public void affectAnimation(Skeleton skeleton, BoneAnimation boneAnimation, Constraint constraint) { - Structure constraintData = constraint.getData(); - this.validateConstraintType(constraintData); - BoneTrack boneTrack = this.getBoneTrack(skeleton, boneAnimation, constraint); - if (boneTrack != null) { - Vector3f targetLocation = this.getTargetLocation(constraint); - int flag = ((Number) constraintData.getFieldValue("flag")).intValue(); - Vector3f[] translations = boneTrack.getTranslations(); - int maxFrames = translations.length; - for (int frame = 0; frame < maxFrames; ++frame) { - Vector3f offset = Vector3f.ZERO; - if ((flag & LOCLIKE_OFFSET) != 0) {//we add the original location to the copied location - offset = translations[frame].clone(); - } - - if ((flag & LOCLIKE_X) != 0) { - translations[frame].x = targetLocation.x; - if ((flag & LOCLIKE_X_INVERT) != 0) { - translations[frame].x = -translations[frame].x; - } - } else if ((flag & LOCLIKE_Y) != 0) { - translations[frame].y = targetLocation.y; - if ((flag & LOCLIKE_Y_INVERT) != 0) { - translations[frame].y = -translations[frame].y; - } - } else if ((flag & LOCLIKE_Z) != 0) { - translations[frame].z = targetLocation.z; - if ((flag & LOCLIKE_Z_INVERT) != 0) { - translations[frame].z = -translations[frame].z; - } - } - translations[frame].addLocal(offset);//TODO: ipo influence - } - boneTrack.setKeyframes(boneTrack.getTimes(), translations, boneTrack.getRotations(), boneTrack.getScales()); - } - } - }; - - //LOCLIMIT constraint - influenceFunctions[ConstraintType.CONSTRAINT_TYPE_LOCLIMIT.getConstraintId()] = new AbstractInfluenceFunction(ConstraintType.CONSTRAINT_TYPE_LOCLIMIT, dataRepository) { - - @Override - public void affectAnimation(Skeleton skeleton, BoneAnimation boneAnimation, Constraint constraint) { - Structure constraintStructure = constraint.getData(); - this.validateConstraintType(constraintStructure); - BoneTrack boneTrack = this.getBoneTrack(skeleton, boneAnimation, constraint); - if (boneTrack != null) { - int flag = ((Number) constraintStructure.getFieldValue("flag")).intValue(); - Vector3f[] translations = boneTrack.getTranslations(); - int maxFrames = translations.length; - for (int frame = 0; frame < maxFrames; ++frame) { - float influence = constraint.getIpo().calculateValue(frame); - if ((flag & LIMIT_XMIN) != 0) { - float xmin = ((Number) constraintStructure.getFieldValue("xmin")).floatValue(); - if (translations[frame].x < xmin) { - translations[frame].x -= (translations[frame].x - xmin) * influence; - } - } - if ((flag & LIMIT_XMAX) != 0) { - float xmax = ((Number) constraintStructure.getFieldValue("xmax")).floatValue(); - if (translations[frame].x > xmax) { - translations[frame].x -= (translations[frame].x - xmax) * influence; - } - } - if ((flag & LIMIT_YMIN) != 0) { - float ymin = ((Number) constraintStructure.getFieldValue("ymin")).floatValue(); - if (translations[frame].y < ymin) { - translations[frame].y -= (translations[frame].y - ymin) * influence; - } - } - if ((flag & LIMIT_YMAX) != 0) { - float ymax = ((Number) constraintStructure.getFieldValue("ymax")).floatValue(); - if (translations[frame].y > ymax) { - translations[frame].y -= (translations[frame].y - ymax) * influence; - } - } - if ((flag & LIMIT_ZMIN) != 0) { - float zmin = ((Number) constraintStructure.getFieldValue("zmin")).floatValue(); - if (translations[frame].z < zmin) { - translations[frame].z -= (translations[frame].z - zmin) * influence; - } - } - if ((flag & LIMIT_ZMAX) != 0) { - float zmax = ((Number) constraintStructure.getFieldValue("zmax")).floatValue(); - if (translations[frame].z > zmax) { - translations[frame].z -= (translations[frame].z - zmax) * influence; - } - }//TODO: consider constraint space !!! - } - boneTrack.setKeyframes(boneTrack.getTimes(), translations, boneTrack.getRotations(), boneTrack.getScales()); - } - } - }; - - //MINMAX constraint (TODO: to implement) - influenceFunctions[ConstraintType.CONSTRAINT_TYPE_MINMAX.getConstraintId()] = new AbstractInfluenceFunction(ConstraintType.CONSTRAINT_TYPE_MINMAX, dataRepository) { - }; - - //NULL constraint - does nothing - influenceFunctions[ConstraintType.CONSTRAINT_TYPE_NULL.getConstraintId()] = new AbstractInfluenceFunction(ConstraintType.CONSTRAINT_TYPE_NULL, dataRepository) { - }; - - //PYTHON constraint (TODO: to implement) - influenceFunctions[ConstraintType.CONSTRAINT_TYPE_PYTHON.getConstraintId()] = new AbstractInfluenceFunction(ConstraintType.CONSTRAINT_TYPE_PYTHON, dataRepository) { - }; - - //RIGIDBODYJOINT constraint (TODO: to implement) - influenceFunctions[ConstraintType.CONSTRAINT_TYPE_RIGIDBODYJOINT.getConstraintId()] = new AbstractInfluenceFunction(ConstraintType.CONSTRAINT_TYPE_RIGIDBODYJOINT, dataRepository) { - }; - - //ROTLIKE constraint - influenceFunctions[ConstraintType.CONSTRAINT_TYPE_ROTLIKE.getConstraintId()] = new AbstractInfluenceFunction(ConstraintType.CONSTRAINT_TYPE_ROTLIKE, dataRepository) { - - @Override - public void affectAnimation(Skeleton skeleton, BoneAnimation boneAnimation, Constraint constraint) { - Structure constraintData = constraint.getData(); - this.validateConstraintType(constraintData); - BoneTrack boneTrack = this.getBoneTrack(skeleton, boneAnimation, constraint); - if (boneTrack != null) { - Quaternion targetRotation = this.getTargetRotation(constraint); - int flag = ((Number) constraintData.getFieldValue("flag")).intValue(); - float[] targetAngles = targetRotation.toAngles(null); - Quaternion[] rotations = boneTrack.getRotations(); - int maxFrames = rotations.length; - for (int frame = 0; frame < maxFrames; ++frame) { - float[] angles = rotations[frame].toAngles(null); - - Quaternion offset = Quaternion.IDENTITY; - if ((flag & ROTLIKE_OFFSET) != 0) {//we add the original rotation to the copied rotation - offset = rotations[frame].clone(); - } - - if ((flag & ROTLIKE_X) != 0) { - angles[0] = targetAngles[0]; - if ((flag & ROTLIKE_X_INVERT) != 0) { - angles[0] = -angles[0]; - } - } else if ((flag & ROTLIKE_Y) != 0) { - angles[1] = targetAngles[1]; - if ((flag & ROTLIKE_Y_INVERT) != 0) { - angles[1] = -angles[1]; - } - } else if ((flag & ROTLIKE_Z) != 0) { - angles[2] = targetAngles[2]; - if ((flag & ROTLIKE_Z_INVERT) != 0) { - angles[2] = -angles[2]; - } - } - rotations[frame].fromAngles(angles).multLocal(offset);//TODO: ipo influence - } - boneTrack.setKeyframes(boneTrack.getTimes(), boneTrack.getTranslations(), rotations, boneTrack.getScales()); - } - } - }; - - //ROTLIMIT constraint - influenceFunctions[ConstraintType.CONSTRAINT_TYPE_ROTLIMIT.getConstraintId()] = new AbstractInfluenceFunction(ConstraintType.CONSTRAINT_TYPE_ROTLIMIT, dataRepository) { - - @Override - public void affectAnimation(Skeleton skeleton, BoneAnimation boneAnimation, Constraint constraint) { - Structure constraintStructure = constraint.getData(); - this.validateConstraintType(constraintStructure); - BoneTrack boneTrack = this.getBoneTrack(skeleton, boneAnimation, constraint); - if (boneTrack != null) { - int flag = ((Number) constraintStructure.getFieldValue("flag")).intValue(); - Quaternion[] rotations = boneTrack.getRotations(); - int maxFrames = rotations.length; - for (int frame = 0; frame < maxFrames; ++frame) { - float[] angles = rotations[frame].toAngles(null); - float influence = constraint.getIpo().calculateValue(frame); - if ((flag & LIMIT_XROT) != 0) { - float xmin = ((Number) constraintStructure.getFieldValue("xmin")).floatValue() * FastMath.DEG_TO_RAD; - float xmax = ((Number) constraintStructure.getFieldValue("xmax")).floatValue() * FastMath.DEG_TO_RAD; - float difference = 0.0f; - if (angles[0] < xmin) { - difference = (angles[0] - xmin) * influence; - } else if (angles[0] > xmax) { - difference = (angles[0] - xmax) * influence; - } - angles[0] -= difference; - } - if ((flag & LIMIT_YROT) != 0) { - float ymin = ((Number) constraintStructure.getFieldValue("ymin")).floatValue() * FastMath.DEG_TO_RAD; - float ymax = ((Number) constraintStructure.getFieldValue("ymax")).floatValue() * FastMath.DEG_TO_RAD; - float difference = 0.0f; - if (angles[1] < ymin) { - difference = (angles[1] - ymin) * influence; - } else if (angles[1] > ymax) { - difference = (angles[1] - ymax) * influence; - } - angles[1] -= difference; - } - if ((flag & LIMIT_ZROT) != 0) { - float zmin = ((Number) constraintStructure.getFieldValue("zmin")).floatValue() * FastMath.DEG_TO_RAD; - float zmax = ((Number) constraintStructure.getFieldValue("zmax")).floatValue() * FastMath.DEG_TO_RAD; - float difference = 0.0f; - if (angles[2] < zmin) { - difference = (angles[2] - zmin) * influence; - } else if (angles[2] > zmax) { - difference = (angles[2] - zmax) * influence; - } - angles[2] -= difference; - } - rotations[frame].fromAngles(angles);//TODO: consider constraint space !!! - } - boneTrack.setKeyframes(boneTrack.getTimes(), boneTrack.getTranslations(), rotations, boneTrack.getScales()); - } - } - }; - - //SHRINKWRAP constraint (TODO: to implement) - influenceFunctions[ConstraintType.CONSTRAINT_TYPE_SHRINKWRAP.getConstraintId()] = new AbstractInfluenceFunction(ConstraintType.CONSTRAINT_TYPE_SHRINKWRAP, dataRepository) { - }; - - //SIZELIKE constraint - influenceFunctions[ConstraintType.CONSTRAINT_TYPE_SIZELIKE.getConstraintId()] = new AbstractInfluenceFunction(ConstraintType.CONSTRAINT_TYPE_SIZELIKE, dataRepository) { - - @Override - public void affectAnimation(Skeleton skeleton, BoneAnimation boneAnimation, Constraint constraint) { - Structure constraintData = constraint.getData(); - this.validateConstraintType(constraintData); - Vector3f targetScale = this.getTargetLocation(constraint); - BoneTrack boneTrack = this.getBoneTrack(skeleton, boneAnimation, constraint); - if (boneTrack != null) { - int flag = ((Number) constraintData.getFieldValue("flag")).intValue(); - Vector3f[] scales = boneTrack.getScales(); - int maxFrames = scales.length; - for (int frame = 0; frame < maxFrames; ++frame) { - Vector3f offset = Vector3f.ZERO; - if ((flag & LOCLIKE_OFFSET) != 0) {//we add the original scale to the copied scale - offset = scales[frame].clone(); - } - - if ((flag & SIZELIKE_X) != 0) { - scales[frame].x = targetScale.x; - } else if ((flag & SIZELIKE_Y) != 0) { - scales[frame].y = targetScale.y; - } else if ((flag & SIZELIKE_Z) != 0) { - scales[frame].z = targetScale.z; - } - scales[frame].addLocal(offset);//TODO: ipo influence - //TODO: add or multiply??? - } - boneTrack.setKeyframes(boneTrack.getTimes(), boneTrack.getTranslations(), boneTrack.getRotations(), scales); - } - } - }; - - //SIZELIMIT constraint - influenceFunctions[ConstraintType.CONSTRAINT_TYPE_SIZELIMIT.getConstraintId()] = new AbstractInfluenceFunction(ConstraintType.CONSTRAINT_TYPE_SIZELIMIT, dataRepository) { - - @Override - public void affectAnimation(Skeleton skeleton, BoneAnimation boneAnimation, Constraint constraint) { - Structure constraintStructure = constraint.getData(); - this.validateConstraintType(constraintStructure); - BoneTrack boneTrack = this.getBoneTrack(skeleton, boneAnimation, constraint); - if (boneTrack != null) { - int flag = ((Number) constraintStructure.getFieldValue("flag")).intValue(); - Vector3f[] scales = boneTrack.getScales(); - int maxFrames = scales.length; - for (int frame = 0; frame < maxFrames; ++frame) { - float influence = constraint.getIpo().calculateValue(frame); - if ((flag & LIMIT_XMIN) != 0) { - float xmin = ((Number) constraintStructure.getFieldValue("xmin")).floatValue(); - if (scales[frame].x < xmin) { - scales[frame].x -= (scales[frame].x - xmin) * influence; - } - } - if ((flag & LIMIT_XMAX) != 0) { - float xmax = ((Number) constraintStructure.getFieldValue("xmax")).floatValue(); - if (scales[frame].x > xmax) { - scales[frame].x -= (scales[frame].x - xmax) * influence; - } - } - if ((flag & LIMIT_YMIN) != 0) { - float ymin = ((Number) constraintStructure.getFieldValue("ymin")).floatValue(); - if (scales[frame].y < ymin) { - scales[frame].y -= (scales[frame].y - ymin) * influence; - } - } - if ((flag & LIMIT_YMAX) != 0) { - float ymax = ((Number) constraintStructure.getFieldValue("ymax")).floatValue(); - if (scales[frame].y > ymax) { - scales[frame].y -= (scales[frame].y - ymax) * influence; - } - } - if ((flag & LIMIT_ZMIN) != 0) { - float zmin = ((Number) constraintStructure.getFieldValue("zmin")).floatValue(); - if (scales[frame].z < zmin) { - scales[frame].z -= (scales[frame].z - zmin) * influence; - } - } - if ((flag & LIMIT_ZMAX) != 0) { - float zmax = ((Number) constraintStructure.getFieldValue("zmax")).floatValue(); - if (scales[frame].z > zmax) { - scales[frame].z -= (scales[frame].z - zmax) * influence; - } - }//TODO: consider constraint space !!! - } - boneTrack.setKeyframes(boneTrack.getTimes(), boneTrack.getTranslations(), boneTrack.getRotations(), scales); - } - } - }; - - //STRETCHTO constraint (TODO: to implement) - influenceFunctions[ConstraintType.CONSTRAINT_TYPE_STRETCHTO.getConstraintId()] = new AbstractInfluenceFunction(ConstraintType.CONSTRAINT_TYPE_STRETCHTO, dataRepository) { - }; - - //TRANSFORM constraint (TODO: to implement) - influenceFunctions[ConstraintType.CONSTRAINT_TYPE_TRANSFORM.getConstraintId()] = new AbstractInfluenceFunction(ConstraintType.CONSTRAINT_TYPE_TRANSFORM, dataRepository) { - }; - } - } - - /** - * This method reads constraints for for the given structure. The constraints are loaded only once for object/bone. - * @param ownerOMA - * the owner's old memory address - * @param objectStructure - * the structure we read constraint's for - * @param dataRepository - * the data repository - * @throws BlenderFileException - */ - public void loadConstraints(Structure objectStructure, DataRepository dataRepository) throws BlenderFileException { - // reading influence ipos for the constraints - IpoHelper ipoHelper = dataRepository.getHelper(IpoHelper.class); - Map> constraintsIpos = new HashMap>(); - Pointer pActions = (Pointer) objectStructure.getFieldValue("action"); - if (pActions.isNotNull()) { - List actions = pActions.fetchData(dataRepository.getInputStream()); - for (Structure action : actions) { - Structure chanbase = (Structure) action.getFieldValue("chanbase"); - List actionChannels = chanbase.evaluateListBase(dataRepository); - for (Structure actionChannel : actionChannels) { - Map ipos = new HashMap(); - Structure constChannels = (Structure) actionChannel.getFieldValue("constraintChannels"); - List constraintChannels = constChannels.evaluateListBase(dataRepository); - for (Structure constraintChannel : constraintChannels) { - Pointer pIpo = (Pointer) constraintChannel.getFieldValue("ipo"); - if (pIpo.isNotNull()) { - String constraintName = constraintChannel.getFieldValue("name").toString(); - Ipo ipo = ipoHelper.createIpo(pIpo.fetchData(dataRepository.getInputStream()).get(0), dataRepository); - ipos.put(constraintName, ipo); - } - } - String actionName = actionChannel.getFieldValue("name").toString(); - constraintsIpos.put(actionName, ipos); - } - } - } - - //loading constraints connected with the object's bones - List constraintsList = new ArrayList(); - Pointer pPose = (Pointer) objectStructure.getFieldValue("pose");//TODO: what if the object has two armatures ???? - if (pPose.isNotNull()) { - //getting pose channels - List poseChannels = ((Structure) pPose.fetchData(dataRepository.getInputStream()).get(0).getFieldValue("chanbase")).evaluateListBase(dataRepository); - for (Structure poseChannel : poseChannels) { - Long boneOMA = Long.valueOf(((Pointer) poseChannel.getFieldValue("bone")).getOldMemoryAddress()); - //the name is read directly from structure because bone might not yet be loaded - String name = dataRepository.getFileBlock(boneOMA).getStructure(dataRepository).getFieldValue("name").toString(); - List constraints = ((Structure) poseChannel.getFieldValue("constraints")).evaluateListBase(dataRepository); - for (Structure constraint : constraints) { - int type = ((Number) constraint.getFieldValue("type")).intValue(); - String constraintName = constraint.getFieldValue("name").toString(); - Ipo ipo = constraintsIpos.get(name).get(constraintName); - if (ipo == null) { - float enforce = ((Number) constraint.getFieldValue("enforce")).floatValue(); - ipo = ipoHelper.createIpo(enforce); - } - Space ownerSpace = Space.valueOf(((Number) constraint.getFieldValue("ownspace")).byteValue()); - Space targetSpace = Space.valueOf(((Number) constraint.getFieldValue("tarspace")).byteValue()); - Constraint c = new Constraint(constraint, influenceFunctions[type], boneOMA, ownerSpace, targetSpace, ipo, dataRepository); - constraintsList.add(c); - } - } - } - /* TODO: reading constraints for objects (implement when object's animation will be available) - List constraintChannels = ((Structure)objectStructure.getFieldValue("constraintChannels")).evaluateListBase(dataRepository); - for(Structure constraintChannel : constraintChannels) { - System.out.println(constraintChannel); - } - - //loading constraints connected with the object itself (TODO: test this) - if(!this.constraints.containsKey(objectStructure.getOldMemoryAddress())) { - List constraints = ((Structure)objectStructure.getFieldValue("constraints")).evaluateListBase(dataRepository); - Constraint[] result = new Constraint[constraints.size()]; - int i = 0; - for(Structure constraint : constraints) { - int type = ((Number)constraint.getFieldValue("type")).intValue(); - String name = constraint.getFieldValue("name").toString(); - result[i++] = new Constraint(constraint, influenceFunctions[type], null, dataRepository);//TODO: influence ipos for object animation - } - this.constraints.put(objectStructure.getOldMemoryAddress(), result); - } - */ - if (constraintsList.size() > 0) { - this.constraints.put(objectStructure.getOldMemoryAddress(), constraintsList.toArray(new Constraint[constraintsList.size()])); - } - } - - /** - * This method returns a list of constraints of the feature's constraints. The order of constraints is important. - * @param ownerOMA - * the owner's old memory address - * @return a table of constraints for the feature specified by old memory address - */ - public Constraint[] getConstraints(Long ownerOMA) { - return constraints.get(ownerOMA); - } - - @Override - public void clearState() { - constraints.clear(); - } - - /** - * The purpose of this class is to imitate bone's movement when calculating inverse kinematics. - * @author Marcin Roguski - */ - private static class CalculationBone extends Node { - - /** The name of the bone. Only to be used in toString method. */ - private String boneName; - /** The bone's tracks. Will be altered at the end of calculation process. */ - private BoneTrack track; - /** The starting position of the bone. */ - private Vector3f startTranslation; - /** The starting rotation of the bone. */ - private Quaternion startRotation; - /** The starting scale of the bone. */ - private Vector3f startScale; - private Vector3f[] translations; - private Quaternion[] rotations; - private Vector3f[] scales; - - /** - * Constructor. Stores the track, starting transformation and sets the transformation to the starting positions. - * @param bone - * the bone this class will imitate - * @param track - * the bone's tracks - */ - public CalculationBone(Bone bone, BoneTrack track) { - this.boneName = bone.getName(); - this.track = track; - this.startRotation = bone.getModelSpaceRotation().clone(); - this.startTranslation = bone.getModelSpacePosition().clone(); - this.startScale = bone.getModelSpaceScale().clone(); - this.translations = track.getTranslations(); - this.rotations = track.getRotations(); - this.scales = track.getScales(); - this.reset(); - } - - /** - * This method returns the end point of the bone. If the bone has parent it is calculated from the start point - * of parent to the start point of this bone. If the bone doesn't have a parent the end location is considered - * to be 1 point up along Y axis (scale is applied if set to != 1.0); - * @return the end point of this bone - */ - //TODO: set to Z axis if user defined it this way - public Vector3f getEndPoint() { - if (this.getParent() == null) { - return new Vector3f(0, this.getLocalScale().y, 0); - } else { - Node parent = this.getParent(); - return parent.getWorldTranslation().subtract(this.getWorldTranslation()).multLocal(this.getWorldScale()); - } - } - - /** - * This method resets the calculation bone to the starting position. - */ - public void reset() { - this.setLocalTranslation(startTranslation); - this.setLocalRotation(startRotation); - this.setLocalScale(startScale); - } - - @Override - public int attachChild(Spatial child) { - if (this.getChildren() != null && this.getChildren().size() > 1) { - throw new IllegalStateException(this.getClass().getName() + " class instance can only have one child!"); - } - return super.attachChild(child); - } - - public Spatial rotate(Quaternion rot, int frame) { - Spatial spatial = super.rotate(rot); - this.updateWorldTransforms(); - if (this.getChildren() != null && this.getChildren().size() > 0) { - CalculationBone child = (CalculationBone) this.getChild(0); - child.updateWorldTransforms(); - } - rotations[frame].set(this.getLocalRotation()); - translations[frame].set(this.getLocalTranslation()); - if (scales != null) { - scales[frame].set(this.getLocalScale()); - } - return spatial; - } - - public void applyCalculatedTracks() { - track.setKeyframes(track.getTimes(), translations, rotations);//TODO:scales - } - - @Override - public String toString() { - return boneName + ": " + this.getLocalRotation() + " " + this.getLocalTranslation(); - } - } + } + + /** + * This method returns bones used for rotation calculations. + * @param bone + * the bone to which the constraint is applied + * @param skeleton + * the skeleton owning the bone and its ancestors + * @param boneAnimation + * the bone animation data that stores the traces for the skeleton's bones + * @return a list of bones to imitate the bone's movement during IK solving + */ + private CalculationBone[] getBonesToCalculate(Bone bone, Skeleton skeleton, BoneAnimation boneAnimation) { + List bonesList = new ArrayList(); + Bone currentBone = bone; + do { + int boneIndex = skeleton.getBoneIndex(currentBone); + for (int i = 0; i < boneAnimation.getTracks().length; ++i) { + if (boneAnimation.getTracks()[i].getTargetBoneIndex() == boneIndex) { + bonesList.add(new CalculationBone(currentBone, boneAnimation.getTracks()[i])); + break; + } + } + currentBone = currentBone.getParent(); + } while (currentBone != null); + //attaching children + CalculationBone[] result = bonesList.toArray(new CalculationBone[bonesList.size()]); + for (int i = result.length - 1; i > 0; --i) { + result[i].attachChild(result[i - 1]); + } + return result; + } + }; + + //LOCKTRACK constraint (TODO: to implement) + influenceFunctions[ConstraintType.CONSTRAINT_TYPE_LOCKTRACK.getConstraintId()] = new AbstractInfluenceFunction(ConstraintType.CONSTRAINT_TYPE_LOCKTRACK, dataRepository) { + }; + + //LOCLIKE constraint + influenceFunctions[ConstraintType.CONSTRAINT_TYPE_LOCLIKE.getConstraintId()] = new AbstractInfluenceFunction(ConstraintType.CONSTRAINT_TYPE_LOCLIKE, dataRepository) { + + @Override + public void affectAnimation(Skeleton skeleton, BoneAnimation boneAnimation, Constraint constraint) { + Structure constraintData = constraint.getData(); + this.validateConstraintType(constraintData); + BoneTrack boneTrack = this.getBoneTrack(skeleton, boneAnimation, constraint); + if (boneTrack != null) { + Vector3f targetLocation = this.getTargetLocation(constraint); + int flag = ((Number) constraintData.getFieldValue("flag")).intValue(); + Vector3f[] translations = boneTrack.getTranslations(); + int maxFrames = translations.length; + for (int frame = 0; frame < maxFrames; ++frame) { + Vector3f offset = Vector3f.ZERO; + if ((flag & LOCLIKE_OFFSET) != 0) {//we add the original location to the copied location + offset = translations[frame].clone(); + } + + if ((flag & LOCLIKE_X) != 0) { + translations[frame].x = targetLocation.x; + if ((flag & LOCLIKE_X_INVERT) != 0) { + translations[frame].x = -translations[frame].x; + } + } else if ((flag & LOCLIKE_Y) != 0) { + translations[frame].y = targetLocation.y; + if ((flag & LOCLIKE_Y_INVERT) != 0) { + translations[frame].y = -translations[frame].y; + } + } else if ((flag & LOCLIKE_Z) != 0) { + translations[frame].z = targetLocation.z; + if ((flag & LOCLIKE_Z_INVERT) != 0) { + translations[frame].z = -translations[frame].z; + } + } + translations[frame].addLocal(offset);//TODO: ipo influence + } + boneTrack.setKeyframes(boneTrack.getTimes(), translations, boneTrack.getRotations(), boneTrack.getScales()); + } + } + }; + + //LOCLIMIT constraint + influenceFunctions[ConstraintType.CONSTRAINT_TYPE_LOCLIMIT.getConstraintId()] = new AbstractInfluenceFunction(ConstraintType.CONSTRAINT_TYPE_LOCLIMIT, dataRepository) { + + @Override + public void affectAnimation(Skeleton skeleton, BoneAnimation boneAnimation, Constraint constraint) { + Structure constraintStructure = constraint.getData(); + this.validateConstraintType(constraintStructure); + BoneTrack boneTrack = this.getBoneTrack(skeleton, boneAnimation, constraint); + if (boneTrack != null) { + int flag = ((Number) constraintStructure.getFieldValue("flag")).intValue(); + Vector3f[] translations = boneTrack.getTranslations(); + int maxFrames = translations.length; + for (int frame = 0; frame < maxFrames; ++frame) { + float influence = constraint.getIpo().calculateValue(frame); + if ((flag & LIMIT_XMIN) != 0) { + float xmin = ((Number) constraintStructure.getFieldValue("xmin")).floatValue(); + if (translations[frame].x < xmin) { + translations[frame].x -= (translations[frame].x - xmin) * influence; + } + } + if ((flag & LIMIT_XMAX) != 0) { + float xmax = ((Number) constraintStructure.getFieldValue("xmax")).floatValue(); + if (translations[frame].x > xmax) { + translations[frame].x -= (translations[frame].x - xmax) * influence; + } + } + if ((flag & LIMIT_YMIN) != 0) { + float ymin = ((Number) constraintStructure.getFieldValue("ymin")).floatValue(); + if (translations[frame].y < ymin) { + translations[frame].y -= (translations[frame].y - ymin) * influence; + } + } + if ((flag & LIMIT_YMAX) != 0) { + float ymax = ((Number) constraintStructure.getFieldValue("ymax")).floatValue(); + if (translations[frame].y > ymax) { + translations[frame].y -= (translations[frame].y - ymax) * influence; + } + } + if ((flag & LIMIT_ZMIN) != 0) { + float zmin = ((Number) constraintStructure.getFieldValue("zmin")).floatValue(); + if (translations[frame].z < zmin) { + translations[frame].z -= (translations[frame].z - zmin) * influence; + } + } + if ((flag & LIMIT_ZMAX) != 0) { + float zmax = ((Number) constraintStructure.getFieldValue("zmax")).floatValue(); + if (translations[frame].z > zmax) { + translations[frame].z -= (translations[frame].z - zmax) * influence; + } + }//TODO: consider constraint space !!! + } + boneTrack.setKeyframes(boneTrack.getTimes(), translations, boneTrack.getRotations(), boneTrack.getScales()); + } + } + }; + + //MINMAX constraint (TODO: to implement) + influenceFunctions[ConstraintType.CONSTRAINT_TYPE_MINMAX.getConstraintId()] = new AbstractInfluenceFunction(ConstraintType.CONSTRAINT_TYPE_MINMAX, dataRepository) { + }; + + //NULL constraint - does nothing + influenceFunctions[ConstraintType.CONSTRAINT_TYPE_NULL.getConstraintId()] = new AbstractInfluenceFunction(ConstraintType.CONSTRAINT_TYPE_NULL, dataRepository) { + }; + + //PYTHON constraint (TODO: to implement) + influenceFunctions[ConstraintType.CONSTRAINT_TYPE_PYTHON.getConstraintId()] = new AbstractInfluenceFunction(ConstraintType.CONSTRAINT_TYPE_PYTHON, dataRepository) { + }; + + //RIGIDBODYJOINT constraint (TODO: to implement) + influenceFunctions[ConstraintType.CONSTRAINT_TYPE_RIGIDBODYJOINT.getConstraintId()] = new AbstractInfluenceFunction(ConstraintType.CONSTRAINT_TYPE_RIGIDBODYJOINT, dataRepository) { + }; + + //ROTLIKE constraint + influenceFunctions[ConstraintType.CONSTRAINT_TYPE_ROTLIKE.getConstraintId()] = new AbstractInfluenceFunction(ConstraintType.CONSTRAINT_TYPE_ROTLIKE, dataRepository) { + + @Override + public void affectAnimation(Skeleton skeleton, BoneAnimation boneAnimation, Constraint constraint) { + Structure constraintData = constraint.getData(); + this.validateConstraintType(constraintData); + BoneTrack boneTrack = this.getBoneTrack(skeleton, boneAnimation, constraint); + if (boneTrack != null) { + Quaternion targetRotation = this.getTargetRotation(constraint); + int flag = ((Number) constraintData.getFieldValue("flag")).intValue(); + float[] targetAngles = targetRotation.toAngles(null); + Quaternion[] rotations = boneTrack.getRotations(); + int maxFrames = rotations.length; + for (int frame = 0; frame < maxFrames; ++frame) { + float[] angles = rotations[frame].toAngles(null); + + Quaternion offset = Quaternion.IDENTITY; + if ((flag & ROTLIKE_OFFSET) != 0) {//we add the original rotation to the copied rotation + offset = rotations[frame].clone(); + } + + if ((flag & ROTLIKE_X) != 0) { + angles[0] = targetAngles[0]; + if ((flag & ROTLIKE_X_INVERT) != 0) { + angles[0] = -angles[0]; + } + } else if ((flag & ROTLIKE_Y) != 0) { + angles[1] = targetAngles[1]; + if ((flag & ROTLIKE_Y_INVERT) != 0) { + angles[1] = -angles[1]; + } + } else if ((flag & ROTLIKE_Z) != 0) { + angles[2] = targetAngles[2]; + if ((flag & ROTLIKE_Z_INVERT) != 0) { + angles[2] = -angles[2]; + } + } + rotations[frame].fromAngles(angles).multLocal(offset);//TODO: ipo influence + } + boneTrack.setKeyframes(boneTrack.getTimes(), boneTrack.getTranslations(), rotations, boneTrack.getScales()); + } + } + }; + + //ROTLIMIT constraint + influenceFunctions[ConstraintType.CONSTRAINT_TYPE_ROTLIMIT.getConstraintId()] = new AbstractInfluenceFunction(ConstraintType.CONSTRAINT_TYPE_ROTLIMIT, dataRepository) { + + @Override + public void affectAnimation(Skeleton skeleton, BoneAnimation boneAnimation, Constraint constraint) { + Structure constraintStructure = constraint.getData(); + this.validateConstraintType(constraintStructure); + BoneTrack boneTrack = this.getBoneTrack(skeleton, boneAnimation, constraint); + if (boneTrack != null) { + int flag = ((Number) constraintStructure.getFieldValue("flag")).intValue(); + Quaternion[] rotations = boneTrack.getRotations(); + int maxFrames = rotations.length; + for (int frame = 0; frame < maxFrames; ++frame) { + float[] angles = rotations[frame].toAngles(null); + float influence = constraint.getIpo().calculateValue(frame); + if ((flag & LIMIT_XROT) != 0) { + float xmin = ((Number) constraintStructure.getFieldValue("xmin")).floatValue() * FastMath.DEG_TO_RAD; + float xmax = ((Number) constraintStructure.getFieldValue("xmax")).floatValue() * FastMath.DEG_TO_RAD; + float difference = 0.0f; + if (angles[0] < xmin) { + difference = (angles[0] - xmin) * influence; + } else if (angles[0] > xmax) { + difference = (angles[0] - xmax) * influence; + } + angles[0] -= difference; + } + if ((flag & LIMIT_YROT) != 0) { + float ymin = ((Number) constraintStructure.getFieldValue("ymin")).floatValue() * FastMath.DEG_TO_RAD; + float ymax = ((Number) constraintStructure.getFieldValue("ymax")).floatValue() * FastMath.DEG_TO_RAD; + float difference = 0.0f; + if (angles[1] < ymin) { + difference = (angles[1] - ymin) * influence; + } else if (angles[1] > ymax) { + difference = (angles[1] - ymax) * influence; + } + angles[1] -= difference; + } + if ((flag & LIMIT_ZROT) != 0) { + float zmin = ((Number) constraintStructure.getFieldValue("zmin")).floatValue() * FastMath.DEG_TO_RAD; + float zmax = ((Number) constraintStructure.getFieldValue("zmax")).floatValue() * FastMath.DEG_TO_RAD; + float difference = 0.0f; + if (angles[2] < zmin) { + difference = (angles[2] - zmin) * influence; + } else if (angles[2] > zmax) { + difference = (angles[2] - zmax) * influence; + } + angles[2] -= difference; + } + rotations[frame].fromAngles(angles);//TODO: consider constraint space !!! + } + boneTrack.setKeyframes(boneTrack.getTimes(), boneTrack.getTranslations(), rotations, boneTrack.getScales()); + } + } + }; + + //SHRINKWRAP constraint (TODO: to implement) + influenceFunctions[ConstraintType.CONSTRAINT_TYPE_SHRINKWRAP.getConstraintId()] = new AbstractInfluenceFunction(ConstraintType.CONSTRAINT_TYPE_SHRINKWRAP, dataRepository) { + }; + + //SIZELIKE constraint + influenceFunctions[ConstraintType.CONSTRAINT_TYPE_SIZELIKE.getConstraintId()] = new AbstractInfluenceFunction(ConstraintType.CONSTRAINT_TYPE_SIZELIKE, dataRepository) { + + @Override + public void affectAnimation(Skeleton skeleton, BoneAnimation boneAnimation, Constraint constraint) { + Structure constraintData = constraint.getData(); + this.validateConstraintType(constraintData); + Vector3f targetScale = this.getTargetLocation(constraint); + BoneTrack boneTrack = this.getBoneTrack(skeleton, boneAnimation, constraint); + if (boneTrack != null) { + int flag = ((Number) constraintData.getFieldValue("flag")).intValue(); + Vector3f[] scales = boneTrack.getScales(); + int maxFrames = scales.length; + for (int frame = 0; frame < maxFrames; ++frame) { + Vector3f offset = Vector3f.ZERO; + if ((flag & LOCLIKE_OFFSET) != 0) {//we add the original scale to the copied scale + offset = scales[frame].clone(); + } + + if ((flag & SIZELIKE_X) != 0) { + scales[frame].x = targetScale.x; + } else if ((flag & SIZELIKE_Y) != 0) { + scales[frame].y = targetScale.y; + } else if ((flag & SIZELIKE_Z) != 0) { + scales[frame].z = targetScale.z; + } + scales[frame].addLocal(offset);//TODO: ipo influence + //TODO: add or multiply??? + } + boneTrack.setKeyframes(boneTrack.getTimes(), boneTrack.getTranslations(), boneTrack.getRotations(), scales); + } + } + }; + + //SIZELIMIT constraint + influenceFunctions[ConstraintType.CONSTRAINT_TYPE_SIZELIMIT.getConstraintId()] = new AbstractInfluenceFunction(ConstraintType.CONSTRAINT_TYPE_SIZELIMIT, dataRepository) { + + @Override + public void affectAnimation(Skeleton skeleton, BoneAnimation boneAnimation, Constraint constraint) { + Structure constraintStructure = constraint.getData(); + this.validateConstraintType(constraintStructure); + BoneTrack boneTrack = this.getBoneTrack(skeleton, boneAnimation, constraint); + if (boneTrack != null) { + int flag = ((Number) constraintStructure.getFieldValue("flag")).intValue(); + Vector3f[] scales = boneTrack.getScales(); + int maxFrames = scales.length; + for (int frame = 0; frame < maxFrames; ++frame) { + float influence = constraint.getIpo().calculateValue(frame); + if ((flag & LIMIT_XMIN) != 0) { + float xmin = ((Number) constraintStructure.getFieldValue("xmin")).floatValue(); + if (scales[frame].x < xmin) { + scales[frame].x -= (scales[frame].x - xmin) * influence; + } + } + if ((flag & LIMIT_XMAX) != 0) { + float xmax = ((Number) constraintStructure.getFieldValue("xmax")).floatValue(); + if (scales[frame].x > xmax) { + scales[frame].x -= (scales[frame].x - xmax) * influence; + } + } + if ((flag & LIMIT_YMIN) != 0) { + float ymin = ((Number) constraintStructure.getFieldValue("ymin")).floatValue(); + if (scales[frame].y < ymin) { + scales[frame].y -= (scales[frame].y - ymin) * influence; + } + } + if ((flag & LIMIT_YMAX) != 0) { + float ymax = ((Number) constraintStructure.getFieldValue("ymax")).floatValue(); + if (scales[frame].y > ymax) { + scales[frame].y -= (scales[frame].y - ymax) * influence; + } + } + if ((flag & LIMIT_ZMIN) != 0) { + float zmin = ((Number) constraintStructure.getFieldValue("zmin")).floatValue(); + if (scales[frame].z < zmin) { + scales[frame].z -= (scales[frame].z - zmin) * influence; + } + } + if ((flag & LIMIT_ZMAX) != 0) { + float zmax = ((Number) constraintStructure.getFieldValue("zmax")).floatValue(); + if (scales[frame].z > zmax) { + scales[frame].z -= (scales[frame].z - zmax) * influence; + } + }//TODO: consider constraint space !!! + } + boneTrack.setKeyframes(boneTrack.getTimes(), boneTrack.getTranslations(), boneTrack.getRotations(), scales); + } + } + }; + + //STRETCHTO constraint (TODO: to implement) + influenceFunctions[ConstraintType.CONSTRAINT_TYPE_STRETCHTO.getConstraintId()] = new AbstractInfluenceFunction(ConstraintType.CONSTRAINT_TYPE_STRETCHTO, dataRepository) { + }; + + //TRANSFORM constraint (TODO: to implement) + influenceFunctions[ConstraintType.CONSTRAINT_TYPE_TRANSFORM.getConstraintId()] = new AbstractInfluenceFunction(ConstraintType.CONSTRAINT_TYPE_TRANSFORM, dataRepository) { + }; + } + } + + /** + * This method reads constraints for for the given structure. The constraints are loaded only once for object/bone. + * @param ownerOMA + * the owner's old memory address + * @param objectStructure + * the structure we read constraint's for + * @param dataRepository + * the data repository + * @throws BlenderFileException + */ + public void loadConstraints(Structure objectStructure, DataRepository dataRepository) throws BlenderFileException { + // reading influence ipos for the constraints + IpoHelper ipoHelper = dataRepository.getHelper(IpoHelper.class); + Map> constraintsIpos = new HashMap>(); + Pointer pActions = (Pointer) objectStructure.getFieldValue("action"); + if (pActions.isNotNull()) { + List actions = pActions.fetchData(dataRepository.getInputStream()); + for (Structure action : actions) { + Structure chanbase = (Structure) action.getFieldValue("chanbase"); + List actionChannels = chanbase.evaluateListBase(dataRepository); + for (Structure actionChannel : actionChannels) { + Map ipos = new HashMap(); + Structure constChannels = (Structure) actionChannel.getFieldValue("constraintChannels"); + List constraintChannels = constChannels.evaluateListBase(dataRepository); + for (Structure constraintChannel : constraintChannels) { + Pointer pIpo = (Pointer) constraintChannel.getFieldValue("ipo"); + if (pIpo.isNotNull()) { + String constraintName = constraintChannel.getFieldValue("name").toString(); + Ipo ipo = ipoHelper.createIpo(pIpo.fetchData(dataRepository.getInputStream()).get(0), dataRepository); + ipos.put(constraintName, ipo); + } + } + String actionName = actionChannel.getFieldValue("name").toString(); + constraintsIpos.put(actionName, ipos); + } + } + } + + //loading constraints connected with the object's bones + List constraintsList = new ArrayList(); + Pointer pPose = (Pointer) objectStructure.getFieldValue("pose");//TODO: what if the object has two armatures ???? + if (pPose.isNotNull()) { + //getting pose channels + List poseChannels = ((Structure) pPose.fetchData(dataRepository.getInputStream()).get(0).getFieldValue("chanbase")).evaluateListBase(dataRepository); + for (Structure poseChannel : poseChannels) { + Long boneOMA = Long.valueOf(((Pointer) poseChannel.getFieldValue("bone")).getOldMemoryAddress()); + //the name is read directly from structure because bone might not yet be loaded + String name = dataRepository.getFileBlock(boneOMA).getStructure(dataRepository).getFieldValue("name").toString(); + List constraints = ((Structure) poseChannel.getFieldValue("constraints")).evaluateListBase(dataRepository); + for (Structure constraint : constraints) { + int type = ((Number) constraint.getFieldValue("type")).intValue(); + String constraintName = constraint.getFieldValue("name").toString(); + Ipo ipo = constraintsIpos.get(name).get(constraintName); + if (ipo == null) { + float enforce = ((Number) constraint.getFieldValue("enforce")).floatValue(); + ipo = ipoHelper.createIpo(enforce); + } + Space ownerSpace = Space.valueOf(((Number) constraint.getFieldValue("ownspace")).byteValue()); + Space targetSpace = Space.valueOf(((Number) constraint.getFieldValue("tarspace")).byteValue()); + Constraint c = new Constraint(constraint, influenceFunctions[type], boneOMA, ownerSpace, targetSpace, ipo, dataRepository); + constraintsList.add(c); + } + } + } + // TODO: reading constraints for objects (implement when object's animation will be available) + List constraintChannels = ((Structure)objectStructure.getFieldValue("constraintChannels")).evaluateListBase(dataRepository); + for(Structure constraintChannel : constraintChannels) { + System.out.println(constraintChannel); + } + + //loading constraints connected with the object itself (TODO: test this) + if(!this.constraints.containsKey(objectStructure.getOldMemoryAddress())) { + List constraints = ((Structure)objectStructure.getFieldValue("constraints")).evaluateListBase(dataRepository); + Constraint[] result = new Constraint[constraints.size()]; + int i = 0; + + for(Structure constraint : constraints) { + int type = ((Number)constraint.getFieldValue("type")).intValue(); + String constraintName = constraint.getFieldValue("name").toString(); + String objectName = objectStructure.getName(); + + Map objectConstraintsIpos = constraintsIpos.get(objectName); + Ipo ipo = objectConstraintsIpos!=null ? objectConstraintsIpos.get(constraintName) : null; + if (ipo == null) { + float enforce = ((Number) constraint.getFieldValue("enforce")).floatValue(); + ipo = ipoHelper.createIpo(enforce); + } + Space ownerSpace = Space.valueOf(((Number) constraint.getFieldValue("ownspace")).byteValue()); + Space targetSpace = Space.valueOf(((Number) constraint.getFieldValue("tarspace")).byteValue()); + result[i++] = new Constraint(constraint, influenceFunctions[type], null, + ownerSpace, targetSpace, ipo, dataRepository);//TODO: influence ipos for object animation + } + this.constraints.put(objectStructure.getOldMemoryAddress(), result); + } + + if (constraintsList.size() > 0) { + this.constraints.put(objectStructure.getOldMemoryAddress(), constraintsList.toArray(new Constraint[constraintsList.size()])); + } + } + + /** + * This method returns a list of constraints of the feature's constraints. The order of constraints is important. + * @param ownerOMA + * the owner's old memory address + * @return a table of constraints for the feature specified by old memory address + */ + public Constraint[] getConstraints(Long ownerOMA) { + return constraints.get(ownerOMA); + } + + @Override + public void clearState() { + constraints.clear(); + } + + /** + * The purpose of this class is to imitate bone's movement when calculating inverse kinematics. + * @author Marcin Roguski + */ + private static class CalculationBone extends Node { + + /** The name of the bone. Only to be used in toString method. */ + private String boneName; + /** The bone's tracks. Will be altered at the end of calculation process. */ + private BoneTrack track; + /** The starting position of the bone. */ + private Vector3f startTranslation; + /** The starting rotation of the bone. */ + private Quaternion startRotation; + /** The starting scale of the bone. */ + private Vector3f startScale; + private Vector3f[] translations; + private Quaternion[] rotations; + private Vector3f[] scales; + + /** + * Constructor. Stores the track, starting transformation and sets the transformation to the starting positions. + * @param bone + * the bone this class will imitate + * @param track + * the bone's tracks + */ + public CalculationBone(Bone bone, BoneTrack track) { + this.boneName = bone.getName(); + this.track = track; + this.startRotation = bone.getModelSpaceRotation().clone(); + this.startTranslation = bone.getModelSpacePosition().clone(); + this.startScale = bone.getModelSpaceScale().clone(); + this.translations = track.getTranslations(); + this.rotations = track.getRotations(); + this.scales = track.getScales(); + this.reset(); + } + + /** + * This method returns the end point of the bone. If the bone has parent it is calculated from the start point + * of parent to the start point of this bone. If the bone doesn't have a parent the end location is considered + * to be 1 point up along Y axis (scale is applied if set to != 1.0); + * @return the end point of this bone + */ + //TODO: set to Z axis if user defined it this way + public Vector3f getEndPoint() { + if (this.getParent() == null) { + return new Vector3f(0, this.getLocalScale().y, 0); + } else { + Node parent = this.getParent(); + return parent.getWorldTranslation().subtract(this.getWorldTranslation()).multLocal(this.getWorldScale()); + } + } + + /** + * This method resets the calculation bone to the starting position. + */ + public void reset() { + this.setLocalTranslation(startTranslation); + this.setLocalRotation(startRotation); + this.setLocalScale(startScale); + } + + @Override + public int attachChild(Spatial child) { + if (this.getChildren() != null && this.getChildren().size() > 1) { + throw new IllegalStateException(this.getClass().getName() + " class instance can only have one child!"); + } + return super.attachChild(child); + } + + public Spatial rotate(Quaternion rot, int frame) { + Spatial spatial = super.rotate(rot); + this.updateWorldTransforms(); + if (this.getChildren() != null && this.getChildren().size() > 0) { + CalculationBone child = (CalculationBone) this.getChild(0); + child.updateWorldTransforms(); + } + rotations[frame].set(this.getLocalRotation()); + translations[frame].set(this.getLocalTranslation()); + if (scales != null) { + scales[frame].set(this.getLocalScale()); + } + return spatial; + } + + public void applyCalculatedTracks() { + track.setKeyframes(track.getTimes(), translations, rotations);//TODO:scales + } + + @Override + public String toString() { + return boneName + ": " + this.getLocalRotation() + " " + this.getLocalTranslation(); + } + } } diff --git a/engine/src/blender/com/jme3/scene/plugins/blender/helpers/v249/ModifierHelper.java b/engine/src/blender/com/jme3/scene/plugins/blender/helpers/v249/ModifierHelper.java index 7ac6c1e29..0793e2327 100644 --- a/engine/src/blender/com/jme3/scene/plugins/blender/helpers/v249/ModifierHelper.java +++ b/engine/src/blender/com/jme3/scene/plugins/blender/helpers/v249/ModifierHelper.java @@ -663,16 +663,14 @@ public class ModifierHelper extends AbstractBlenderHelper { Vector3f[] translations = new Vector3f[tablesLength]; Quaternion[] rotations = new Quaternion[tablesLength]; - Vector3f[] scales = sourceScales == null ? null : new Vector3f[tablesLength]; + Vector3f[] scales = new Vector3f[tablesLength]; for (int j = 0; j < tablesLength; ++j) { translations[j] = sourceTranslations[j].clone(); rotations[j] = sourceRotations[j].clone(); - if (sourceScales != null) {// only scales may not be applied - scales[j] = sourceScales[j].clone(); - } + scales[j] = sourceScales != null ? sourceScales[j].clone() : new Vector3f(1.0f, 1.0f, 1.0f); } - boneTracks[i] = new BoneTrack(sourceTracks[i].getTargetBoneIndex(), sourceTracks[i].getTimes(),// times do not change, no need - // to clone them, + // times do not change, no need to clone them + boneTracks[i] = new BoneTrack(sourceTracks[i].getTargetBoneIndex(), sourceTracks[i].getTimes(), translations, rotations, scales); } result.setTracks(boneTracks); diff --git a/engine/src/blender/com/jme3/scene/plugins/blender/helpers/v249/ObjectHelper.java b/engine/src/blender/com/jme3/scene/plugins/blender/helpers/v249/ObjectHelper.java index 9f99fb95c..3c3ca85b0 100644 --- a/engine/src/blender/com/jme3/scene/plugins/blender/helpers/v249/ObjectHelper.java +++ b/engine/src/blender/com/jme3/scene/plugins/blender/helpers/v249/ObjectHelper.java @@ -403,7 +403,7 @@ public class ObjectHelper extends AbstractBlenderHelper { Bone bone = new Bone(null); bone.setBindTransforms(t.getTranslation(), t.getRotation(), t.getScale()); - return new Modifier(Modifier.ARMATURE_MODIFIER_DATA, new AnimData(new Skeleton(new Bone[] {bone}), animations), null); + return new Modifier(Modifier.ARMATURE_MODIFIER_DATA, new AnimData(new Skeleton(new Bone[] {bone}), animations), objectStructure.getOldMemoryAddress()); } return null; } diff --git a/engine/src/blender/com/jme3/scene/plugins/blender/structures/AbstractInfluenceFunction.java b/engine/src/blender/com/jme3/scene/plugins/blender/structures/AbstractInfluenceFunction.java index 4fbe0feb2..ef54fb649 100644 --- a/engine/src/blender/com/jme3/scene/plugins/blender/structures/AbstractInfluenceFunction.java +++ b/engine/src/blender/com/jme3/scene/plugins/blender/structures/AbstractInfluenceFunction.java @@ -117,7 +117,7 @@ public abstract class AbstractInfluenceFunction { protected BoneTrack getBoneTrack(Skeleton skeleton, BoneAnimation boneAnimation, Constraint constraint) { Long boneOMA = constraint.getBoneOMA(); Bone bone = (Bone) dataRepository.getLoadedFeature(boneOMA, LoadedFeatureDataType.LOADED_FEATURE); - int boneIndex = skeleton.getBoneIndex(bone); + int boneIndex = bone==null ? 0 : skeleton.getBoneIndex(bone);//bone==null may mean the object animation if (boneIndex != -1) { //searching for track for this bone for (BoneTrack boneTrack : boneAnimation.getTracks()) { diff --git a/engine/src/test-data/Blender/2.4x/constraints.blend b/engine/src/test-data/Blender/2.4x/constraints.blend index 36df267400fc2c376b346bd17375767bcbf0d2ce..7b54c29874f5e464bd6c19639bde1ca94c042996 100644 GIT binary patch delta 21663 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 a>^G@1n1mV&m<(-!$YHG4En8IVz<&Z6WZsGZ delta 18838 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 e-!iMp-k!ikoJG6xHq5TBW8XIidTkNz*2nw2g9