Hi everyone,
I’d like to share some ideas with you. I thought about the discussion about the ‘ArmatureMask’ class based on feedback from @sgold and @pspeed.
Here are two proposals:
- Version A (implements AnimationMask)
AnimMaskBuilder.java
- Method signature is consistent.
- You can chain all methods.
- The ‘
Armature
’ is passed only once in the constructor. - ‘
Joints
’ are passed to methods as strings instead of objects. - Redundant reference of Armature inside of AnimationMask.
import java.util.ArrayList;
import java.util.BitSet;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.jme3.anim.AnimationMask;
import com.jme3.anim.Armature;
import com.jme3.anim.Joint;
/**
* An AnimationMask to select joints from a single Armature.
* @author capdevon
*/
public class AnimMaskBuilder implements AnimationMask {
private static final Logger logger = Logger.getLogger(AnimMaskBuilder.class.getName());
private final BitSet affectedJoints;
private final Armature armature;
/**
* Instantiate a mask that affects no joints.
*
* @param armature
*/
public AnimMaskBuilder(Armature armature) {
this.armature = armature;
this.affectedJoints = new BitSet(armature.getJointCount());
logger.log(Level.INFO, "Joint count: {0}", armature.getJointCount());
}
/**
* Add all the bones of the model's armature to be influenced by this
* animation mask.
*
* @return AnimMaskBuilder
*/
public AnimMaskBuilder addAllJoints() {
int numJoints = armature.getJointCount();
affectedJoints.set(0, numJoints);
return this;
}
/**
* Add joints to be influenced by this animation mask.
*
* @param jointNames
* @return AnimMaskBuilder
*/
public AnimMaskBuilder addJoints(String...jointNames) {
for (String jointName: jointNames) {
Joint joint = findJoint(jointName);
affectedJoints.set(joint.getId());
}
return this;
}
private Joint findJoint(String jointName) {
Joint joint = armature.getJoint(jointName);
if (joint == null) {
throw new IllegalArgumentException("Cannot find joint " + jointName);
}
return joint;
}
/**
* Add a joint and all its sub armature joints to be influenced by this
* animation mask.
*
* @param jointName the starting point (may be null, unaffected)
* @return AnimMaskBuilder
*/
public AnimMaskBuilder addFromJoint(String jointName) {
Joint joint = findJoint(jointName);
addFromJoint(joint);
return this;
}
private void addFromJoint(Joint joint) {
affectedJoints.set(joint.getId());
for (Joint j: joint.getChildren()) {
addFromJoint(j);
}
}
/**
* Remove a joint and all its sub armature joints to be influenced by this
* animation mask.
*
* @param jointName the starting point (may be null, unaffected)
* @return AnimMaskBuilder
*/
public AnimMaskBuilder removeFromJoint(String jointName) {
Joint joint = findJoint(jointName);
removeFromJoint(joint);
return this;
}
private void removeFromJoint(Joint joint) {
affectedJoints.clear(joint.getId());
for (Joint j: joint.getChildren()) {
removeFromJoint(j);
}
}
/**
* Add the specified Joint and all its ancestors.
*
* @param jointName the starting point (may be null, unaffected)
* @return AnimMaskBuilder
*/
public AnimMaskBuilder addAncestors(String jointName) {
Joint joint = findJoint(jointName);
addAncestors(joint);
return this;
}
private void addAncestors(Joint start) {
for (Joint joint = start; joint != null; joint = joint.getParent()) {
affectedJoints.set(joint.getId());
}
}
/**
* Remove the specified Joint and all its ancestors.
*
* @param jointName the starting point (may be null, unaffected)
* @return AnimMaskBuilder
*/
public AnimMaskBuilder removeAncestors(String jointName) {
Joint joint = findJoint(jointName);
removeAncestors(joint);
return this;
}
private void removeAncestors(Joint start) {
for (Joint joint = start; joint != null; joint = joint.getParent()) {
affectedJoints.clear(joint.getId());
}
}
/**
* Remove the named joints.
*
* @param jointNames the names of the joints to be removed
* @return AnimMaskBuilder
*/
public AnimMaskBuilder removeJoints(String...jointNames) {
for (String jointName: jointNames) {
Joint joint = findJoint(jointName);
affectedJoints.clear(joint.getId());
}
return this;
}
/**
* Get the list of joints affected by this animation mask.
*
* @return
*/
public List<Joint> getAffectedJoints() {
List<Joint> lst = new ArrayList<>();
for (Joint joint : armature.getJointList()) {
if (contains(joint)) {
lst.add(joint);
}
}
return lst;
}
@Override
public boolean contains(Object target) {
Joint joint = (Joint) target;
return affectedJoints.get(joint.getId());
}
}
Use case:
Armature armature = skinningControl.getArmature();
// upperBody
AnimationMask upperBody = new AnimMaskBuilder(armature)
.addFromJoint("Spine");
// lowerBody
AnimationMask lowerBody = new AnimMaskBuilder(armature)
.addJoints("Hips")
.addFromJoint("RightUpLeg")
.addFromJoint("LeftUpLeg");
// allBody
AnimationMask allBody = new AnimMaskBuilder(armature)
.addAllJoints();
// noHeadBody
AnimationMask noHeadBody = new AnimMaskBuilder(armature)
.addAllJoints()
.removeJoints("Head", "HeadTop_End", "RightEye", "LeftEye");
// noHeadBody2
AnimationMask noHeadBody2 = new AnimMaskBuilder(armature)
.addAllJoints()
.removeFromJoint("Neck");
- Version B (use Builder Pattern):
AnimMaskBuilder.java:
- Responsible for the construction of the mask.
- Method signature is consistent.
- You can chain all methods.
- The ‘
Armature
’ is passed only once in the constructor. - ‘
Joints
’ are passed to methods as strings instead of objects.
import java.util.ArrayList;
import java.util.BitSet;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.jme3.anim.AnimationMask;
import com.jme3.anim.Armature;
import com.jme3.anim.Joint;
/**
* An AnimationMask to select joints from a single Armature.
* @author capdevon
*/
public class AnimMaskBuilder {
private static final Logger logger = Logger.getLogger(AnimMaskBuilder.class.getName());
private final BitSet affectedJoints;
private final Armature armature;
/**
* Instantiate a mask that affects no joints.
*
* @param armature
*/
public AnimMaskBuilder(Armature armature) {
this.armature = armature;
this.affectedJoints = new BitSet(armature.getJointCount());
logger.log(Level.INFO, "Joint count: {0}", armature.getJointCount());
}
/**
* Add all the bones of the model's armature to be influenced by this
* animation mask.
*
* @return AnimMaskBuilder
*/
public AnimMaskBuilder addAllJoints() {
int numJoints = armature.getJointCount();
affectedJoints.set(0, numJoints);
return this;
}
/**
* Add joints to be influenced by this animation mask.
*
* @param jointNames
* @return AnimMaskBuilder
*/
public AnimMaskBuilder addJoints(String...jointNames) {
for (String jointName: jointNames) {
Joint joint = findJoint(jointName);
affectedJoints.set(joint.getId());
}
return this;
}
private Joint findJoint(String jointName) {
Joint joint = armature.getJoint(jointName);
if (joint == null) {
throw new IllegalArgumentException("Cannot find joint " + jointName);
}
return joint;
}
/**
* Add a joint and all its sub armature joints to be influenced by this
* animation mask.
*
* @param jointName the starting point (may be null, unaffected)
* @return AnimMaskBuilder
*/
public AnimMaskBuilder addFromJoint(String jointName) {
Joint joint = findJoint(jointName);
addFromJoint(joint);
return this;
}
private void addFromJoint(Joint joint) {
affectedJoints.set(joint.getId());
for (Joint j: joint.getChildren()) {
addFromJoint(j);
}
}
/**
* Remove a joint and all its sub armature joints to be influenced by this
* animation mask.
*
* @param jointName the starting point (may be null, unaffected)
* @return AnimMaskBuilder
*/
public AnimMaskBuilder removeFromJoint(String jointName) {
Joint joint = findJoint(jointName);
removeFromJoint(joint);
return this;
}
private void removeFromJoint(Joint joint) {
affectedJoints.clear(joint.getId());
for (Joint j: joint.getChildren()) {
removeFromJoint(j);
}
}
/**
* Add the specified Joint and all its ancestors.
*
* @param jointName the starting point (may be null, unaffected)
* @return AnimMaskBuilder
*/
public AnimMaskBuilder addAncestors(String jointName) {
Joint joint = findJoint(jointName);
addAncestors(joint);
return this;
}
private void addAncestors(Joint start) {
for (Joint joint = start; joint != null; joint = joint.getParent()) {
affectedJoints.set(joint.getId());
}
}
/**
* Remove the specified Joint and all its ancestors.
*
* @param jointName the starting point (may be null, unaffected)
* @return AnimMaskBuilder
*/
public AnimMaskBuilder removeAncestors(String jointName) {
Joint joint = findJoint(jointName);
removeAncestors(joint);
return this;
}
private void removeAncestors(Joint start) {
for (Joint joint = start; joint != null; joint = joint.getParent()) {
affectedJoints.clear(joint.getId());
}
}
/**
* Remove the named joints.
*
* @param jointNames the names of the joints to be removed
* @return AnimMaskBuilder
*/
public AnimMaskBuilder removeJoints(String...jointNames) {
for (String jointName: jointNames) {
Joint joint = findJoint(jointName);
affectedJoints.clear(joint.getId());
}
return this;
}
/**
* Get the list of joints affected by this animation mask.
*
* @return
*/
public List<Joint> getAffectedJoints() {
List<Joint> lst = new ArrayList<>();
for (Joint joint : armature.getJointList()) {
if (affectedJoints.get(joint.getId())) {
lst.add(joint);
}
}
return lst;
}
/**
* (NEW)
* Build AnimationMask
*
* @return
*/
public AnimationMask build() {
AvatarMask mask = new AvatarMask();
for (int i = 0; i < affectedJoints.length(); i++) {
if (affectedJoints.get(i) == true) {
Joint joint = armature.getJoint(i);
mask.addJoint(joint.getId());
}
}
return mask;
}
}
AvatarMask.java:
- Simple implementation of the mask containing only the necessary data and a minimal set of functions (add/remove).
- AnimationMask instance can be used for many different Armature instances. (ie: shared masks across multiple instances of the same model).
import java.util.BitSet;
import com.jme3.anim.AnimationMask;
import com.jme3.anim.Joint;
/**
*
* @author capdevon
*/
public class AvatarMask implements AnimationMask {
private final BitSet affectedJoints = new BitSet();
/**
* Instantiate a mask that affects no joints.
*/
public AvatarMask() {
// do nothing
}
public void addJoint(int jointId) {
affectedJoints.set(jointId);
}
public void removeJoint(int jointId) {
affectedJoints.clear(jointId);
}
@Override
public boolean contains(Object target) {
Joint joint = (Joint) target;
return affectedJoints.get(joint.getId());
}
}
Use case:
Armature armature = skinningControl.getArmature();
// upperBody
AnimationMask upperBody = new AnimMaskBuilder(skinningControl.getArmature())
.addFromJoint("Spine")
.build(); /* build mask */
// lowerBody
AnimationMask lowerBody = new AnimMaskBuilder(skinningControl.getArmature())
.addJoints("Hips")
.addFromJoint("RightUpLeg")
.addFromJoint("LeftUpLeg")
.build(); /* build mask */
// allBody
AnimationMask allBody = new AnimMaskBuilder(skinningControl.getArmature())
.addAllJoints()
.build(); /* build mask */
// noHeadBody
AnimationMask noHeadBody = new AnimMaskBuilder(skinningControl.getArmature())
.addAllJoints()
.removeJoints("Head", "HeadTop_End", "RightEye", "LeftEye")
.build(); /* build mask */
// noHeadBody 2
AnimationMask noHeadBody2 = new AnimMaskBuilder(skinningControl.getArmature())
.addAllJoints()
.removeFromJoint("Neck")
.build(); /* build mask */
Do you think it might be useful to integrate one of the two proposed solutions into the core library by deprecating the current implementation of the ArmatureMask class? Otherwise no problem, it remains as an example for posterity. The thing I like most is hearing your opinion.