diff --git a/jme3-core/src/main/java/com/jme3/scene/debug/custom/BoneShape.java b/jme3-core/src/main/java/com/jme3/scene/debug/custom/BoneShape.java new file mode 100644 index 000000000..7a0a0300a --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/scene/debug/custom/BoneShape.java @@ -0,0 +1,417 @@ +/* + * Copyright (c) 2009-2012 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. + */ +// $Id: Cylinder.java 4131 2009-03-19 20:15:28Z blaine.dev $ +package com.jme3.scene.debug.custom; + +import com.jme3.export.InputCapsule; +import com.jme3.export.JmeExporter; +import com.jme3.export.JmeImporter; +import com.jme3.export.OutputCapsule; +import com.jme3.math.FastMath; +import com.jme3.math.Vector3f; +import com.jme3.scene.Mesh; +import com.jme3.scene.VertexBuffer.Type; +import com.jme3.scene.mesh.IndexBuffer; +import com.jme3.util.BufferUtils; + +import static com.jme3.util.BufferUtils.*; + +import java.io.IOException; +import java.nio.FloatBuffer; + +/** + * A simple cylinder, defined by it's height and radius. + * (Ported to jME3) + * + * @author Mark Powell + * @version $Revision: 4131 $, $Date: 2009-03-19 16:15:28 -0400 (Thu, 19 Mar 2009) $ + */ +public class BoneShape extends Mesh { + + private int axisSamples; + + private int radialSamples; + + private float radius; + private float radius2; + + private float height; + private boolean closed; + private boolean inverted; + + /** + * Default constructor for serialization only. Do not use. + */ + public BoneShape() { + } + + /** + * Creates a new Cylinder. By default its center is the origin. Usually, a + * higher sample number creates a better looking cylinder, but at the cost + * of more vertex information. + * + * @param axisSamples Number of triangle samples along the axis. + * @param radialSamples Number of triangle samples along the radial. + * @param radius The radius of the cylinder. + * @param height The cylinder's height. + */ + public BoneShape(int axisSamples, int radialSamples, + float radius, float height) { + this(axisSamples, radialSamples, radius, height, false); + } + + /** + * Creates a new Cylinder. By default its center is the origin. Usually, a + * higher sample number creates a better looking cylinder, but at the cost + * of more vertex information.
+ * If the cylinder is closed the texture is split into axisSamples parts: + * top most and bottom most part is used for top and bottom of the cylinder, + * rest of the texture for the cylinder wall. The middle of the top is + * mapped to texture coordinates (0.5, 1), bottom to (0.5, 0). Thus you need + * a suited distorted texture. + * + * @param axisSamples Number of triangle samples along the axis. + * @param radialSamples Number of triangle samples along the radial. + * @param radius The radius of the cylinder. + * @param height The cylinder's height. + * @param closed true to create a cylinder with top and bottom surface + */ + public BoneShape(int axisSamples, int radialSamples, + float radius, float height, boolean closed) { + this(axisSamples, radialSamples, radius, height, closed, false); + } + + /** + * Creates a new Cylinder. By default its center is the origin. Usually, a + * higher sample number creates a better looking cylinder, but at the cost + * of more vertex information.
+ * If the cylinder is closed the texture is split into axisSamples parts: + * top most and bottom most part is used for top and bottom of the cylinder, + * rest of the texture for the cylinder wall. The middle of the top is + * mapped to texture coordinates (0.5, 1), bottom to (0.5, 0). Thus you need + * a suited distorted texture. + * + * @param axisSamples Number of triangle samples along the axis. + * @param radialSamples Number of triangle samples along the radial. + * @param radius The radius of the cylinder. + * @param height The cylinder's height. + * @param closed true to create a cylinder with top and bottom surface + * @param inverted true to create a cylinder that is meant to be viewed from the + * interior. + */ + public BoneShape(int axisSamples, int radialSamples, + float radius, float height, boolean closed, boolean inverted) { + this(axisSamples, radialSamples, radius, radius, height, closed, inverted); + } + + public BoneShape(int axisSamples, int radialSamples, + float radius, float radius2, float height, boolean closed, boolean inverted) { + super(); + updateGeometry(axisSamples, radialSamples, radius, radius2, height, closed, inverted); + } + + /** + * @return the number of samples along the cylinder axis + */ + public int getAxisSamples() { + return axisSamples; + } + + /** + * @return Returns the height. + */ + public float getHeight() { + return height; + } + + /** + * @return number of samples around cylinder + */ + public int getRadialSamples() { + return radialSamples; + } + + /** + * @return Returns the radius. + */ + public float getRadius() { + return radius; + } + + public float getRadius2() { + return radius2; + } + + /** + * @return true if end caps are used. + */ + public boolean isClosed() { + return closed; + } + + /** + * @return true if normals and uvs are created for interior use + */ + public boolean isInverted() { + return inverted; + } + + /** + * Rebuilds the cylinder based on a new set of parameters. + * + * @param axisSamples the number of samples along the axis. + * @param radialSamples the number of samples around the radial. + * @param radius the radius of the bottom of the cylinder. + * @param radius2 the radius of the top of the cylinder. + * @param height the cylinder's height. + * @param closed should the cylinder have top and bottom surfaces. + * @param inverted is the cylinder is meant to be viewed from the inside. + */ + public void updateGeometry(int axisSamples, int radialSamples, + float radius, float radius2, float height, boolean closed, boolean inverted) { + this.axisSamples = axisSamples + (closed ? 2 : 0); + this.radialSamples = radialSamples; + this.radius = radius; + this.radius2 = radius2; + this.height = height; + this.closed = closed; + this.inverted = inverted; + +// VertexBuffer pvb = getBuffer(Type.Position); +// VertexBuffer nvb = getBuffer(Type.Normal); +// VertexBuffer tvb = getBuffer(Type.TexCoord); + + // Vertices + int vertCount = axisSamples * (radialSamples + 1) + (closed ? 2 : 0); + + setBuffer(Type.Position, 3, createVector3Buffer(getFloatBuffer(Type.Position), vertCount)); + + // Normals + setBuffer(Type.Normal, 3, createVector3Buffer(getFloatBuffer(Type.Normal), vertCount)); + + // Texture co-ordinates + setBuffer(Type.TexCoord, 2, createVector2Buffer(vertCount)); + + int triCount = ((closed ? 2 : 0) + 2 * (axisSamples - 1)) * radialSamples; + + setBuffer(Type.Index, 3, createShortBuffer(getShortBuffer(Type.Index), 3 * triCount)); + + //Color + setBuffer(Type.Color, 4, createFloatBuffer(vertCount * 4)); + + // generate geometry + float inverseRadial = 1.0f / radialSamples; + float inverseAxisLess = 1.0f / (closed ? axisSamples - 3 : axisSamples - 1); + float inverseAxisLessTexture = 1.0f / (axisSamples - 1); + float halfHeight = 0.5f * height; + + // Generate points on the unit circle to be used in computing the mesh + // points on a cylinder slice. + float[] sin = new float[radialSamples + 1]; + float[] cos = new float[radialSamples + 1]; + + for (int radialCount = 0; radialCount < radialSamples; radialCount++) { + float angle = FastMath.TWO_PI * inverseRadial * radialCount; + cos[radialCount] = FastMath.cos(angle); + sin[radialCount] = FastMath.sin(angle); + } + sin[radialSamples] = sin[0]; + cos[radialSamples] = cos[0]; + + // calculate normals + Vector3f[] vNormals = null; + Vector3f vNormal = Vector3f.UNIT_Z; + + if ((height != 0.0f) && (radius != radius2)) { + vNormals = new Vector3f[radialSamples]; + Vector3f vHeight = Vector3f.UNIT_Z.mult(height); + Vector3f vRadial = new Vector3f(); + + for (int radialCount = 0; radialCount < radialSamples; radialCount++) { + vRadial.set(cos[radialCount], sin[radialCount], 0.0f); + Vector3f vRadius = vRadial.mult(radius); + Vector3f vRadius2 = vRadial.mult(radius2); + Vector3f vMantle = vHeight.subtract(vRadius2.subtract(vRadius)); + Vector3f vTangent = vRadial.cross(Vector3f.UNIT_Z); + vNormals[radialCount] = vMantle.cross(vTangent).normalize(); + } + } + + FloatBuffer nb = getFloatBuffer(Type.Normal); + FloatBuffer pb = getFloatBuffer(Type.Position); + FloatBuffer tb = getFloatBuffer(Type.TexCoord); + FloatBuffer cb = getFloatBuffer(Type.Color); + + cb.rewind(); + for (int i = 0; i < vertCount; i++) { + cb.put(0.05f).put(0.05f).put(0.05f).put(1f); + } + + // generate the cylinder itself + Vector3f tempNormal = new Vector3f(); + for (int axisCount = 0, i = 0; axisCount < axisSamples; axisCount++, i++) { + float axisFraction; + float axisFractionTexture; + int topBottom = 0; + if (!closed) { + axisFraction = axisCount * inverseAxisLess; // in [0,1] + axisFractionTexture = axisFraction; + } else { + if (axisCount == 0) { + topBottom = -1; // bottom + axisFraction = 0; + axisFractionTexture = inverseAxisLessTexture; + } else if (axisCount == axisSamples - 1) { + topBottom = 1; // top + axisFraction = 1; + axisFractionTexture = 1 - inverseAxisLessTexture; + } else { + axisFraction = (axisCount - 1) * inverseAxisLess; + axisFractionTexture = axisCount * inverseAxisLessTexture; + } + } + + // compute center of slice + float z = height * axisFraction; + Vector3f sliceCenter = new Vector3f(0, 0, z); + + // compute slice vertices with duplication at end point + int save = i; + for (int radialCount = 0; radialCount < radialSamples; radialCount++, i++) { + float radialFraction = radialCount * inverseRadial; // in [0,1) + tempNormal.set(cos[radialCount], sin[radialCount], 0.0f); + + if (vNormals != null) { + vNormal = vNormals[radialCount]; + } else if (radius == radius2) { + vNormal = tempNormal; + } + + if (topBottom == 0) { + if (!inverted) + nb.put(vNormal.x).put(vNormal.y).put(vNormal.z); + else + nb.put(-vNormal.x).put(-vNormal.y).put(-vNormal.z); + } else { + nb.put(0).put(0).put(topBottom * (inverted ? -1 : 1)); + } + + tempNormal.multLocal((radius - radius2) * axisFraction + radius2) + .addLocal(sliceCenter); + pb.put(tempNormal.x).put(tempNormal.y).put(tempNormal.z); + + tb.put((inverted ? 1 - radialFraction : radialFraction)) + .put(axisFractionTexture); + } + + BufferUtils.copyInternalVector3(pb, save, i); + BufferUtils.copyInternalVector3(nb, save, i); + + tb.put((inverted ? 0.0f : 1.0f)) + .put(axisFractionTexture); + } + + if (closed) { + pb.put(0).put(0).put(-halfHeight); // bottom center + nb.put(0).put(0).put(-1 * (inverted ? -1 : 1)); + tb.put(0.5f).put(0); + pb.put(0).put(0).put(halfHeight); // top center + nb.put(0).put(0).put(1 * (inverted ? -1 : 1)); + tb.put(0.5f).put(1); + } + + IndexBuffer ib = getIndexBuffer(); + int index = 0; + // Connectivity + for (int axisCount = 0, axisStart = 0; axisCount < axisSamples - 1; axisCount++) { + int i0 = axisStart; + int i1 = i0 + 1; + axisStart += radialSamples + 1; + int i2 = axisStart; + int i3 = i2 + 1; + for (int i = 0; i < radialSamples; i++) { + if (closed && axisCount == 0) { + if (!inverted) { + ib.put(index++, i0++); + ib.put(index++, vertCount - 2); + ib.put(index++, i1++); + } else { + ib.put(index++, i0++); + ib.put(index++, i1++); + ib.put(index++, vertCount - 2); + } + } else if (closed && axisCount == axisSamples - 2) { + ib.put(index++, i2++); + ib.put(index++, inverted ? vertCount - 1 : i3++); + ib.put(index++, inverted ? i3++ : vertCount - 1); + } else { + ib.put(index++, i0++); + ib.put(index++, inverted ? i2 : i1); + ib.put(index++, inverted ? i1 : i2); + ib.put(index++, i1++); + ib.put(index++, inverted ? i2++ : i3++); + ib.put(index++, inverted ? i3++ : i2++); + } + } + } + + updateBound(); + } + + @Override + public void read(JmeImporter e) throws IOException { + super.read(e); + InputCapsule capsule = e.getCapsule(this); + axisSamples = capsule.readInt("axisSamples", 0); + radialSamples = capsule.readInt("radialSamples", 0); + radius = capsule.readFloat("radius", 0); + radius2 = capsule.readFloat("radius2", 0); + height = capsule.readFloat("height", 0); + closed = capsule.readBoolean("closed", false); + inverted = capsule.readBoolean("inverted", false); + } + + @Override + public void write(JmeExporter e) throws IOException { + super.write(e); + OutputCapsule capsule = e.getCapsule(this); + capsule.write(axisSamples, "axisSamples", 0); + capsule.write(radialSamples, "radialSamples", 0); + capsule.write(radius, "radius", 0); + capsule.write(radius2, "radius2", 0); + capsule.write(height, "height", 0); + capsule.write(closed, "closed", false); + capsule.write(inverted, "inverted", false); + } + + +} diff --git a/jme3-core/src/main/java/com/jme3/scene/debug/custom/SkeletonBone.java b/jme3-core/src/main/java/com/jme3/scene/debug/custom/SkeletonBone.java new file mode 100644 index 000000000..62edf2098 --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/scene/debug/custom/SkeletonBone.java @@ -0,0 +1,216 @@ +package com.jme3.scene.debug.custom; + +/* + * Copyright (c) 2009-2012 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. + */ + +import java.util.Map; + +import com.jme3.animation.Bone; +import com.jme3.animation.Skeleton; +import com.jme3.math.FastMath; +import com.jme3.math.Quaternion; +import com.jme3.math.Vector3f; +import com.jme3.scene.Geometry; +import com.jme3.scene.Mesh; +import com.jme3.scene.Node; +import com.jme3.scene.VertexBuffer; +import com.jme3.scene.shape.Sphere; + +import static com.jme3.util.BufferUtils.createFloatBuffer; + +import java.nio.FloatBuffer; +import java.util.HashMap; + +/** + * The class that displays either wires between the bones' heads if no length + * data is supplied and full bones' shapes otherwise. + */ +public class SkeletonBone extends Node { + + /** + * The skeleton to be displayed. + */ + private Skeleton skeleton; + /** + * The map between the bone index and its length. + */ + private Map boneNodes = new HashMap(); + private Map nodeBones = new HashMap(); + private Node selectedNode = null; + private boolean guessBonesOrientation = false; + + /** + * Creates a wire with bone lengths data. If the data is supplied then the + * wires will show each full bone (from head to tail). + * + * @param skeleton the skeleton that will be shown + * @param boneLengths a map between the bone's index and the bone's length + */ + public SkeletonBone(Skeleton skeleton, Map boneLengths, boolean guessBonesOrientation) { + this.skeleton = skeleton; + this.skeleton.reset(); + this.skeleton.updateWorldVectors(); + this.guessBonesOrientation = guessBonesOrientation; + + BoneShape boneShape = new BoneShape(5, 12, 0.02f, 0.07f, 1f, false, false); + Sphere jointShape = new Sphere(10, 10, 0.1f); + jointShape.setBuffer(VertexBuffer.Type.Color, 4, createFloatBuffer(jointShape.getVertexCount() * 4)); + FloatBuffer cb = jointShape.getFloatBuffer(VertexBuffer.Type.Color); + + cb.rewind(); + for (int i = 0; i < jointShape.getVertexCount(); i++) { + cb.put(0.05f).put(0.05f).put(0.05f).put(1f); + } + + for (Bone bone : skeleton.getRoots()) { + createSkeletonGeoms(bone, boneShape, jointShape, boneLengths, skeleton, this, guessBonesOrientation); + } + } + + protected final void createSkeletonGeoms(Bone bone, Mesh boneShape, Mesh jointShape, Map boneLengths, Skeleton skeleton, Node parent, boolean guessBonesOrientation) { + + if (guessBonesOrientation && bone.getName().equalsIgnoreCase("Site")) { + //BVH skeleton have a useless end point bone named Site + return; + } + Node n = new Node(bone.getName() + "Node"); + Geometry bGeom = new Geometry(bone.getName(), boneShape); + Geometry jGeom = new Geometry(bone.getName() + "Joint", jointShape); + n.setLocalTranslation(bone.getLocalPosition()); + n.setLocalRotation(bone.getLocalRotation()); + + float boneLength = boneLengths.get(skeleton.getBoneIndex(bone)); + n.setLocalScale(bone.getLocalScale()); + + bGeom.setLocalRotation(new Quaternion().fromAngleAxis(-FastMath.HALF_PI, Vector3f.UNIT_X).normalizeLocal()); + + if (guessBonesOrientation) { + //One child only, the bone direction is from the parent joint to the child joint. + if (bone.getChildren().size() == 1) { + Vector3f v = bone.getChildren().get(0).getLocalPosition(); + Quaternion q = new Quaternion(); + q.lookAt(v, Vector3f.UNIT_Z); + bGeom.setLocalRotation(q); + } + //no child, the bone has the same direction as the parent bone. + if (bone.getChildren().isEmpty()) { + if (parent.getChildren().size() > 0) { + bGeom.setLocalRotation(parent.getChild(0).getLocalRotation()); + } else { + //no parent, let's use the bind orientation of the bone + bGeom.setLocalRotation(bone.getBindRotation()); + } + } + } + bGeom.setLocalScale(boneLength); + jGeom.setLocalScale(boneLength); + + n.attachChild(bGeom); + n.attachChild(jGeom); + + //tip + if (bone.getChildren().size() != 1) { + Geometry gt = jGeom.clone(); + gt.scale(0.8f); + Vector3f v = new Vector3f(0, boneLength, 0); + if (guessBonesOrientation) { + if (bone.getChildren().isEmpty()) { + if (parent.getChildren().size() > 0) { + gt.setLocalTranslation(bGeom.getLocalRotation().mult(parent.getChild(0).getLocalRotation()).mult(v, v)); + } else { + gt.setLocalTranslation(bGeom.getLocalRotation().mult(bone.getBindRotation()).mult(v, v)); + } + } + } else { + gt.setLocalTranslation(v); + } + + n.attachChild(gt); + } + + + boneNodes.put(bone, n); + nodeBones.put(n, bone); + parent.attachChild(n); + for (Bone childBone : bone.getChildren()) { + createSkeletonGeoms(childBone, boneShape, jointShape, boneLengths, skeleton, n, guessBonesOrientation); + } + } + + protected Bone select(Geometry g) { + Node parentNode = g.getParent(); + + if (parent != null) { + Bone b = nodeBones.get(parentNode); + if (b != null) { + selectedNode = parentNode; + } + return b; + } + return null; + } + + protected Node getSelectedNode() { + return selectedNode; + } + + +// private Quaternion getRotationBetweenVect(Vector3f v1, Vector3f v2){ +// Vector3f a =v1.cross(v2); +// float w = FastMath.sqrt((v1.length() * v1.length()) * (v2.length() * v2.length())) + v1.dot(v2); +// return new Quaternion(a.x, a.y, a.z, w).normalizeLocal() ; +// } + + protected final void updateSkeletonGeoms(Bone bone) { + if (guessBonesOrientation && bone.getName().equalsIgnoreCase("Site")) { + return; + } + Node n = boneNodes.get(bone); + n.setLocalTranslation(bone.getLocalPosition()); + n.setLocalRotation(bone.getLocalRotation()); + n.setLocalScale(bone.getLocalScale()); + + for (Bone childBone : bone.getChildren()) { + updateSkeletonGeoms(childBone); + } + } + + /** + * The method updates the geometry according to the poitions of the bones. + */ + public void updateGeometry() { + + for (Bone bone : skeleton.getRoots()) { + updateSkeletonGeoms(bone); + } + } +} diff --git a/jme3-core/src/main/java/com/jme3/scene/debug/custom/SkeletonDebugAppState.java b/jme3-core/src/main/java/com/jme3/scene/debug/custom/SkeletonDebugAppState.java new file mode 100644 index 000000000..4aa8f1bd1 --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/scene/debug/custom/SkeletonDebugAppState.java @@ -0,0 +1,150 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.jme3.scene.debug.custom; + +import com.jme3.animation.Bone; +import com.jme3.animation.Skeleton; +import com.jme3.animation.SkeletonControl; +import com.jme3.app.Application; +import com.jme3.app.state.AbstractAppState; +import com.jme3.app.state.AppStateManager; +import com.jme3.collision.CollisionResults; +import com.jme3.input.MouseInput; +import com.jme3.input.controls.ActionListener; +import com.jme3.input.controls.MouseButtonTrigger; +import com.jme3.math.Ray; +import com.jme3.math.Vector2f; +import com.jme3.math.Vector3f; +import com.jme3.renderer.ViewPort; +import com.jme3.scene.Geometry; +import com.jme3.scene.Node; +import com.jme3.scene.Spatial; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author Nehon + */ +public class SkeletonDebugAppState extends AbstractAppState { + + private Node debugNode = new Node("debugNode"); + private Map skeletons = new HashMap(); + private Map selectedBones = new HashMap(); + private Application app; + + @Override + public void initialize(AppStateManager stateManager, Application app) { + ViewPort vp = app.getRenderManager().createMainView("debug", app.getCamera()); + vp.attachScene(debugNode); + vp.setClearDepth(true); + this.app = app; + for (SkeletonDebugger skeletonDebugger : skeletons.values()) { + skeletonDebugger.initialize(app.getAssetManager()); + } + app.getInputManager().addListener(actionListener, "shoot"); + app.getInputManager().addMapping("shoot", new MouseButtonTrigger(MouseInput.BUTTON_LEFT), new MouseButtonTrigger(MouseInput.BUTTON_RIGHT)); + super.initialize(stateManager, app); + } + + @Override + public void update(float tpf) { + debugNode.updateLogicalState(tpf); + debugNode.updateGeometricState(); + } + + public SkeletonDebugger addSkeleton(SkeletonControl skeletonControl, boolean guessBonesOrientation) { + Skeleton skeleton = skeletonControl.getSkeleton(); + Spatial forSpatial = skeletonControl.getSpatial(); + SkeletonDebugger sd = new SkeletonDebugger(forSpatial.getName() + "_Skeleton", skeleton, guessBonesOrientation); + sd.setLocalTransform(forSpatial.getWorldTransform()); + if (forSpatial instanceof Node) { + List geoms = new ArrayList<>(); + findGeoms((Node) forSpatial, geoms); + if (geoms.size() == 1) { + sd.setLocalTransform(geoms.get(0).getWorldTransform()); + } + } + skeletons.put(skeleton, sd); + debugNode.attachChild(sd); + if (isInitialized()) { + sd.initialize(app.getAssetManager()); + } + return sd; + } + + private void findGeoms(Node node, List geoms) { + for (Spatial spatial : node.getChildren()) { + if (spatial instanceof Geometry) { + geoms.add((Geometry) spatial); + } else if (spatial instanceof Node) { + findGeoms((Node) spatial, geoms); + } + } + } + + /** + * Pick a Target Using the Mouse Pointer.
  1. Map "pick target" action + * to a MouseButtonTrigger.
  2. flyCam.setEnabled(false); + *
  3. inputManager.setCursorVisible(true);
  4. Implement action in + * AnalogListener (TODO).
+ */ + private ActionListener actionListener = new ActionListener() { + public void onAction(String name, boolean isPressed, float tpf) { + if (name.equals("shoot") && isPressed) { + CollisionResults results = new CollisionResults(); + Vector2f click2d = app.getInputManager().getCursorPosition(); + Vector3f click3d = app.getCamera().getWorldCoordinates(new Vector2f(click2d.x, click2d.y), 0f).clone(); + Vector3f dir = app.getCamera().getWorldCoordinates(new Vector2f(click2d.x, click2d.y), 1f).subtractLocal(click3d); + Ray ray = new Ray(click3d, dir); + + debugNode.collideWith(ray, results); + + if (results.size() > 0) { + // The closest result is the target that the player picked: + Geometry target = results.getClosestCollision().getGeometry(); + for (SkeletonDebugger skeleton : skeletons.values()) { + Bone selectedBone = skeleton.select(target); + if (selectedBone != null) { + selectedBones.put(skeleton.getSkeleton(), selectedBone); + System.err.println("-----------------------"); + System.err.println("Selected Bone : " + selectedBone.getName() + " in skeleton " + skeleton.getName()); + System.err.println("-----------------------"); + System.err.println("Bind translation: " + selectedBone.getBindPosition()); + System.err.println("Bind rotation: " + selectedBone.getBindRotation()); + System.err.println("Bind scale: " + selectedBone.getBindScale()); + System.err.println("---"); + System.err.println("Local translation: " + selectedBone.getLocalPosition()); + System.err.println("Local rotation: " + selectedBone.getLocalRotation()); + System.err.println("Local scale: " + selectedBone.getLocalScale()); + System.err.println("---"); + System.err.println("Model translation: " + selectedBone.getModelSpacePosition()); + System.err.println("Model rotation: " + selectedBone.getModelSpaceRotation()); + System.err.println("Model scale: " + selectedBone.getModelSpaceScale()); + System.err.println("---"); + System.err.println("Bind inverse Transform: "); + System.err.println(selectedBone.getBindInverseTransform()); + return; + } + } + } + } + } + }; + + public Map getSelectedBones() { + return selectedBones; + } + + public Node getDebugNode() { + return debugNode; + } + + public void setDebugNode(Node debugNode) { + this.debugNode = debugNode; + } +} diff --git a/jme3-core/src/main/java/com/jme3/scene/debug/custom/SkeletonDebugger.java b/jme3-core/src/main/java/com/jme3/scene/debug/custom/SkeletonDebugger.java new file mode 100644 index 000000000..1336b9d1b --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/scene/debug/custom/SkeletonDebugger.java @@ -0,0 +1,218 @@ +package com.jme3.scene.debug.custom; + +/* + * Copyright (c) 2009-2012 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. + */ + +import com.jme3.animation.Bone; + +import java.util.Map; + +import com.jme3.animation.Skeleton; +import com.jme3.asset.AssetManager; +import com.jme3.material.Material; +import com.jme3.math.ColorRGBA; +import com.jme3.scene.BatchNode; +import com.jme3.scene.Geometry; +import com.jme3.scene.Node; +import com.jme3.scene.Spatial; +import com.jme3.scene.VertexBuffer; + +import java.nio.FloatBuffer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +/** + * The class that creates a mesh to display how bones behave. If it is supplied + * with the bones' lengths it will show exactly how the bones look like on the + * scene. If not then only connections between each bone heads will be shown. + */ +public class SkeletonDebugger extends BatchNode { + + /** + * The lines of the bones or the wires between their heads. + */ + private SkeletonBone bones; + + private Skeleton skeleton; + /** + * The dotted lines between a bone's tail and the had of its children. Not + * available if the length data was not provided. + */ + private SkeletonInterBoneWire interBoneWires; + private List selectedBones = new ArrayList(); + + public SkeletonDebugger() { + } + + /** + * Creates a debugger with no length data. The wires will be a connection + * between the bones' heads only. The points will show the bones' heads only + * and no dotted line of inter bones connection will be visible. + * + * @param name the name of the debugger's node + * @param skeleton the skeleton that will be shown + */ + public SkeletonDebugger(String name, Skeleton skeleton, boolean guessBonesOrientation) { + super(name); + this.skeleton = skeleton; + skeleton.reset(); + skeleton.updateWorldVectors(); + Map boneLengths = new HashMap(); + + for (Bone bone : skeleton.getRoots()) { + computeLength(bone, boneLengths, skeleton); + } + + bones = new SkeletonBone(skeleton, boneLengths, guessBonesOrientation); + + this.attachChild(bones); + + interBoneWires = new SkeletonInterBoneWire(skeleton, boneLengths, guessBonesOrientation); + Geometry g = new Geometry(name + "_interwires", interBoneWires); + g.setBatchHint(BatchHint.Never); + this.attachChild(g); + } + + protected void initialize(AssetManager assetManager) { + Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); + mat.setColor("Color", new ColorRGBA(0.05f, 0.05f, 0.05f, 1.0f));//new ColorRGBA(0.1f, 0.1f, 0.1f, 1.0f) + setMaterial(mat); + Material mat2 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); + mat2.setBoolean("VertexColor", true); + bones.setMaterial(mat2); + batch(); + + } + + @Override + public final void setMaterial(Material material) { + if (batches.isEmpty()) { + for (int i = 0; i < children.size(); i++) { + children.get(i).setMaterial(material); + } + } else { + super.setMaterial(material); + } + } + + public Skeleton getSkeleton() { + return skeleton; + } + + + private void computeLength(Bone b, Map boneLengths, Skeleton skeleton) { + if (b.getChildren().isEmpty()) { + if (b.getParent() != null) { + boneLengths.put(skeleton.getBoneIndex(b), boneLengths.get(skeleton.getBoneIndex(b.getParent())) * 0.75f); + } else { + boneLengths.put(skeleton.getBoneIndex(b), 0.1f); + } + } else { + float length = Float.MAX_VALUE; + for (Bone bone : b.getChildren()) { + float len = b.getModelSpacePosition().subtract(bone.getModelSpacePosition()).length(); + if (len < length) { + length = len; + } + } + boneLengths.put(skeleton.getBoneIndex(b), length); + for (Bone bone : b.getChildren()) { + computeLength(bone, boneLengths, skeleton); + } + } + } + + @Override + public void updateLogicalState(float tpf) { + super.updateLogicalState(tpf); + bones.updateGeometry(); + if (interBoneWires != null) { + interBoneWires.updateGeometry(); + } + } + + ColorRGBA selectedColor = ColorRGBA.Orange; + ColorRGBA baseColor = new ColorRGBA(0.05f, 0.05f, 0.05f, 1f); + + protected Bone select(Geometry g) { + Node oldNode = bones.getSelectedNode(); + Bone b = bones.select(g); + if (b == null) { + return null; + } + if (oldNode != null) { + markSelected(oldNode, false); + } + markSelected(bones.getSelectedNode(), true); + return b; + } + + /** + * @return the skeleton wires + */ + public SkeletonBone getBoneShapes() { + return bones; + } + + /** + * @return the dotted line between bones (can be null) + */ + public SkeletonInterBoneWire getInterBoneWires() { + return interBoneWires; + } + + protected void markSelected(Node n, boolean selected) { + ColorRGBA c = baseColor; + if (selected) { + c = selectedColor; + } + for (Spatial spatial : n.getChildren()) { + if (spatial instanceof Geometry) { + Geometry geom = (Geometry) spatial; + + Geometry batch = (Geometry) getChild(getName() + "-batch0"); + VertexBuffer vb = batch.getMesh().getBuffer(VertexBuffer.Type.Color); + FloatBuffer color = (FloatBuffer) vb.getData(); + // System.err.println(getName() + "." + geom.getName() + " index " + getGeometryStartIndex(geom) * 4 + "/" + color.limit()); + + color.position(getGeometryStartIndex(geom) * 4); + + for (int i = 0; i < geom.getVertexCount(); i++) { + color.put(c.r).put(c.g).put(c.b).put(c.a); + } + color.rewind(); + vb.updateData(color); + } + } + } +} \ No newline at end of file diff --git a/jme3-core/src/main/java/com/jme3/scene/debug/custom/SkeletonInterBoneWire.java b/jme3-core/src/main/java/com/jme3/scene/debug/custom/SkeletonInterBoneWire.java new file mode 100644 index 000000000..504c81fe2 --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/scene/debug/custom/SkeletonInterBoneWire.java @@ -0,0 +1,141 @@ +package com.jme3.scene.debug.custom; + +/* + * Copyright (c) 2009-2012 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. + */ + + +import java.nio.FloatBuffer; +import java.util.Map; + +import com.jme3.animation.Bone; +import com.jme3.animation.Skeleton; +import com.jme3.math.Vector3f; +import com.jme3.scene.Mesh; +import com.jme3.scene.VertexBuffer; +import com.jme3.scene.VertexBuffer.Format; +import com.jme3.scene.VertexBuffer.Type; +import com.jme3.scene.VertexBuffer.Usage; +import com.jme3.util.BufferUtils; + +/** + * A class that displays a dotted line between a bone tail and its childrens' heads. + * + * @author Marcin Roguski (Kaelthas) + */ +public class SkeletonInterBoneWire extends Mesh { + private static final int POINT_AMOUNT = 10; + /** + * The amount of connections between bones. + */ + private int connectionsAmount; + /** + * The skeleton that will be showed. + */ + private Skeleton skeleton; + /** + * The map between the bone index and its length. + */ + private Map boneLengths; + + private boolean guessBonesOrientation = false; + + /** + * Creates buffers for points. Each line has POINT_AMOUNT of points. + * + * @param skeleton the skeleton that will be showed + * @param boneLengths the lengths of the bones + */ + public SkeletonInterBoneWire(Skeleton skeleton, Map boneLengths, boolean guessBonesOrientation) { + this.skeleton = skeleton; + + for (Bone bone : skeleton.getRoots()) { + this.countConnections(bone); + } + + this.setMode(Mode.Points); + this.setPointSize(2); + this.boneLengths = boneLengths; + + VertexBuffer pb = new VertexBuffer(Type.Position); + FloatBuffer fpb = BufferUtils.createFloatBuffer(POINT_AMOUNT * connectionsAmount * 3); + pb.setupData(Usage.Stream, 3, Format.Float, fpb); + this.setBuffer(pb); + + this.guessBonesOrientation = guessBonesOrientation; + this.updateCounts(); + } + + /** + * The method updates the geometry according to the poitions of the bones. + */ + public void updateGeometry() { + VertexBuffer vb = this.getBuffer(Type.Position); + FloatBuffer posBuf = this.getFloatBuffer(Type.Position); + posBuf.clear(); + for (int i = 0; i < skeleton.getBoneCount(); ++i) { + Bone bone = skeleton.getBone(i); + Vector3f parentTail = bone.getModelSpacePosition().add(bone.getModelSpaceRotation().mult(Vector3f.UNIT_Y.mult(boneLengths.get(i)))); + + if (guessBonesOrientation) { + parentTail = bone.getModelSpacePosition(); + } + + for (Bone child : bone.getChildren()) { + Vector3f childHead = child.getModelSpacePosition(); + Vector3f v = childHead.subtract(parentTail); + float pointDelta = v.length() / POINT_AMOUNT; + v.normalizeLocal().multLocal(pointDelta); + Vector3f pointPosition = parentTail.clone(); + for (int j = 0; j < POINT_AMOUNT; ++j) { + posBuf.put(pointPosition.getX()).put(pointPosition.getY()).put(pointPosition.getZ()); + pointPosition.addLocal(v); + } + } + } + posBuf.flip(); + vb.updateData(posBuf); + + this.updateBound(); + } + + /** + * Th method couns the connections between bones. + * + * @param bone the bone where counting starts + */ + private void countConnections(Bone bone) { + for (Bone child : bone.getChildren()) { + ++connectionsAmount; + this.countConnections(child); + } + } +}