Переглянути джерело

Merge pull request #2493 from capdevon/capdevon-ArmatureDebugger

Feat: Improve ArmatureDebugger
Ryan McDonough 2 місяців тому
батько
коміт
d73bac2d1e

+ 136 - 50
jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugger.java

@@ -1,7 +1,5 @@
-package com.jme3.scene.debug.custom;
-
 /*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -31,9 +29,11 @@ package com.jme3.scene.debug.custom;
  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
+package com.jme3.scene.debug.custom;
 
 import com.jme3.anim.Armature;
 import com.jme3.anim.Joint;
+import com.jme3.anim.SkinningControl;
 import com.jme3.asset.AssetManager;
 import com.jme3.collision.Collidable;
 import com.jme3.collision.CollisionResults;
@@ -55,104 +55,176 @@ import java.util.List;
 public class ArmatureDebugger extends Node {
 
     /**
-     * The lines of the bones or the wires between their heads.
+     * The node responsible for rendering the bones/wires and their outlines.
      */
     private ArmatureNode armatureNode;
-
+    /**
+     * The {@link Armature} instance being debugged.
+     */
     private Armature armature;
-
+    /**
+     * A node containing all {@link Geometry} objects representing the joint points.
+     */
     private Node joints;
+    /**
+     * A node containing all {@link Geometry} objects representing the bone outlines.
+     */
     private Node outlines;
+    /**
+     * A node containing all {@link Geometry} objects representing the bone wires/lines.
+     */
     private Node wires;
     /**
      * The dotted lines between a bone's tail and the had of its children. Not
      * available if the length data was not provided.
      */
     private ArmatureInterJointsWire interJointWires;
-
+    /**
+     * Default constructor for `ArmatureDebugger`.
+     * Use {@link #ArmatureDebugger(String, Armature, List)} for a functional instance.
+     */
     public ArmatureDebugger() {
     }
 
     /**
-     * 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.
+     * Convenience constructor that creates an {@code ArmatureDebugger} and immediately
+     * initializes its materials based on the provided {@code AssetManager}
+     * and {@code SkinningControl}.
+     *
+     * @param assetManager The {@link AssetManager} used to load textures and materials
+     *                     for the debug visualization.
+     * @param skControl    The {@link SkinningControl} from which to extract the
+     *                     {@link Armature} and its associated joints.
+     */
+    public ArmatureDebugger(AssetManager assetManager, SkinningControl skControl) {
+        this(null, skControl.getArmature(), skControl.getArmature().getJointList());
+        initialize(assetManager, null);
+    }
+
+    /**
+     * Creates an `ArmatureDebugger` instance without explicit bone length data.
+     * In this configuration, the visual representation will consist of wires
+     * connecting the bone heads, and points representing the bone heads.
+     * No dotted lines for inter-bone connections will be visible.
      *
-     * @param name     the name of the debugger's node
-     * @param armature the armature that will be shown
-     * @param deformingJoints a list of joints
+     * @param name            The name of this debugger's root node.
+     * @param armature        The {@link Armature} to be visualized.
+     * @param deformingJoints A {@link List} of {@link Joint} objects that are
+     *                        considered deforming joints.
      */
     public ArmatureDebugger(String name, Armature armature, List<Joint> deformingJoints) {
         super(name);
         this.armature = armature;
+        // Ensure the armature's world transforms are up-to-date before visualization.
         armature.update();
 
+        // Initialize the main container nodes for different visual elements.
         joints = new Node("joints");
         outlines = new Node("outlines");
         wires = new Node("bones");
         this.attachChild(joints);
         this.attachChild(outlines);
         this.attachChild(wires);
-        Node ndJoints = new Node("non deforming Joints");
-        Node ndOutlines = new Node("non deforming Joints outlines");
-        Node ndWires = new Node("non deforming Joints wires");
-        joints.attachChild(ndJoints);
-        outlines.attachChild(ndOutlines);
-        wires.attachChild(ndWires);
-        Node outlineDashed = new Node("Outlines Dashed");
-        Node wiresDashed = new Node("Wires Dashed");
-        wiresDashed.attachChild(new Node("dashed non defrom"));
-        outlineDashed.attachChild(new Node("dashed non defrom"));
+
+        // Create child nodes specifically for non-deforming joints' visualization
+        joints.attachChild(new Node("NonDeformingJoints"));
+        outlines.attachChild(new Node("NonDeformingOutlines"));
+        wires.attachChild(new Node("NonDeformingWires"));
+
+        Node outlineDashed = new Node("DashedOutlines");
+        outlineDashed.attachChild(new Node("DashedNonDeformingOutlines"));
         outlines.attachChild(outlineDashed);
+
+        Node wiresDashed = new Node("DashedWires");
+        wiresDashed.attachChild(new Node("DashedNonDeformingWires"));
         wires.attachChild(wiresDashed);
 
+        // Initialize the core ArmatureNode which handles the actual mesh generation.
         armatureNode = new ArmatureNode(armature, joints, wires, outlines, deformingJoints);
-
         this.attachChild(armatureNode);
 
+        // By default, non-deforming joints are hidden.
         displayNonDeformingJoint(false);
     }
 
+    /**
+     * Sets the visibility of non-deforming joints and their associated outlines and wires.
+     *
+     * @param display `true` to make non-deforming joints visible, `false` to hide them.
+     */
     public void displayNonDeformingJoint(boolean display) {
-        joints.getChild(0).setCullHint(display ? CullHint.Dynamic : CullHint.Always);
-        outlines.getChild(0).setCullHint(display ? CullHint.Dynamic : CullHint.Always);
-        wires.getChild(0).setCullHint(display ? CullHint.Dynamic : CullHint.Always);
-        ((Node) outlines.getChild(1)).getChild(0).setCullHint(display ? CullHint.Dynamic : CullHint.Always);
-        ((Node) wires.getChild(1)).getChild(0).setCullHint(display ? CullHint.Dynamic : CullHint.Always);
+        CullHint cullHint = display ? CullHint.Dynamic : CullHint.Always;
+
+        joints.getChild(0).setCullHint(cullHint);
+        outlines.getChild(0).setCullHint(cullHint);
+        wires.getChild(0).setCullHint(cullHint);
+
+        ((Node) outlines.getChild(1)).getChild(0).setCullHint(cullHint);
+        ((Node) wires.getChild(1)).getChild(0).setCullHint(cullHint);
     }
 
+    /**
+     * Initializes the materials and camera for the debugger's visual components.
+     * This method should be called after the `ArmatureDebugger` is added to a scene graph
+     * and an {@link AssetManager} and {@link Camera} are available.
+     *
+     * @param assetManager The {@link AssetManager} to load textures and materials.
+     * @param camera       The scene's primary {@link Camera}, used by the `ArmatureNode`
+     * for billboard rendering of joint points.
+     */
     public void initialize(AssetManager assetManager, Camera camera) {
 
         armatureNode.setCamera(camera);
 
-        Material matJoints = new Material(assetManager, "Common/MatDefs/Misc/Billboard.j3md");
-        Texture t = assetManager.loadTexture("Common/Textures/dot.png");
-        matJoints.setTexture("Texture", t);
-        matJoints.getAdditionalRenderState().setDepthTest(false);
-        matJoints.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
+        // Material for joint points (billboard dots).
+        Material matJoints = getJointMaterial(assetManager);
         joints.setQueueBucket(RenderQueue.Bucket.Translucent);
         joints.setMaterial(matJoints);
 
-        Material matWires = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
-        matWires.setBoolean("VertexColor", true);
-        matWires.getAdditionalRenderState().setLineWidth(1f);
+        // Material for bone wires/lines (unshaded, vertex colored).
+        Material matWires = getUnshadedMaterial(assetManager);
         wires.setMaterial(matWires);
 
-        Material matOutline = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
-        matOutline.setBoolean("VertexColor", true);
-        matOutline.getAdditionalRenderState().setLineWidth(1f);
+        // Material for dashed wires ("DashedLine.j3md" shader).
+        Material matWires2 = getDashedMaterial(assetManager);
+        wires.getChild(1).setMaterial(matWires2);
+
+        // Material for bone outlines (unshaded, vertex colored).
+        Material matOutline = getUnshadedMaterial(assetManager);
         outlines.setMaterial(matOutline);
 
-        Material matOutline2 = new Material(assetManager, "Common/MatDefs/Misc/DashedLine.j3md");
-        matOutline2.getAdditionalRenderState().setLineWidth(1);
+        // Material for dashed outlines ("DashedLine.j3md" shader).
+        Material matOutline2 = getDashedMaterial(assetManager);
         outlines.getChild(1).setMaterial(matOutline2);
+    }
 
-        Material matWires2 = new Material(assetManager, "Common/MatDefs/Misc/DashedLine.j3md");
-        matWires2.getAdditionalRenderState().setLineWidth(1);
-        wires.getChild(1).setMaterial(matWires2);
+    private Material getJointMaterial(AssetManager asm) {
+        Material mat = new Material(asm, "Common/MatDefs/Misc/Billboard.j3md");
+        Texture tex = asm.loadTexture("Common/Textures/dot.png");
+        mat.setTexture("Texture", tex);
+        mat.getAdditionalRenderState().setDepthTest(false);
+        mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
+        return mat;
+    }
+
+    private Material getUnshadedMaterial(AssetManager asm) {
+        Material mat = new Material(asm, "Common/MatDefs/Misc/Unshaded.j3md");
+        mat.setBoolean("VertexColor", true);
+        mat.getAdditionalRenderState().setDepthTest(false);
+        return mat;
+    }
 
+    private Material getDashedMaterial(AssetManager asm) {
+        Material mat = new Material(asm, "Common/MatDefs/Misc/DashedLine.j3md");
+        mat.getAdditionalRenderState().setDepthTest(false);
+        return mat;
     }
 
+    /**
+     * Returns the {@link Armature} instance associated with this debugger.
+     *
+     * @return The {@link Armature} being debugged.
+     */
     public Armature getArmature() {
         return armature;
     }
@@ -168,21 +240,35 @@ public class ArmatureDebugger extends Node {
         return armatureNode.collideWith(other, results);
     }
 
-    protected Joint select(Geometry g) {
-        return armatureNode.select(g);
+    /**
+     * Selects and returns the {@link Joint} associated with a given {@link Geometry}.
+     * This is an internal helper method, likely used for picking operations.
+     *
+     * @param geo The {@link Geometry} representing a part of a joint.
+     * @return The {@link Joint} corresponding to the geometry, or `null` if not found.
+     */
+    protected Joint select(Geometry geo) {
+        return armatureNode.select(geo);
     }
 
     /**
-     * @return the armature wires
+     * Returns the {@link ArmatureNode} which is responsible for generating and
+     * managing the visual mesh of the bones and wires.
+     *
+     * @return The {@link ArmatureNode} instance.
      */
     public ArmatureNode getBoneShapes() {
         return armatureNode;
     }
 
     /**
-     * @return the dotted line between bones (can be null)
+     * Returns the {@link ArmatureInterJointsWire} instance, which represents the
+     * dotted lines connecting a bone's tail to the head of its children.
+     * This will be `null` if the debugger was created without bone length data.
+     *
+     * @return The {@link ArmatureInterJointsWire} instance, or `null` if not present.
      */
     public ArmatureInterJointsWire getInterJointWires() {
         return interJointWires;
     }
-}
+}

+ 26 - 10
jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureInterJointsWire.java

@@ -1,7 +1,5 @@
-package com.jme3.scene.debug.custom;
-
 /*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -31,7 +29,7 @@ package com.jme3.scene.debug.custom;
  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
-
+package com.jme3.scene.debug.custom;
 
 import com.jme3.math.Vector3f;
 import com.jme3.scene.Mesh;
@@ -46,20 +44,38 @@ import java.nio.FloatBuffer;
  * @author Marcin Roguski (Kaelthas)
  */
 public class ArmatureInterJointsWire extends Mesh {
-    private final Vector3f tmp = new Vector3f();
 
+    /**
+     * A temporary {@link Vector3f} used for calculations to avoid object allocation.
+     */
+    private final Vector3f tempVec = new Vector3f();
+
+    /**
+     * For serialization only. Do not use.
+     */
+    protected ArmatureInterJointsWire() {
+    }
 
+    /**
+     * Creates a new {@code ArmatureInterJointsWire} mesh.
+     * The mesh will be set up to draw lines from the {@code start} vector to each of the {@code ends} vectors.
+     *
+     * @param start The starting point of the lines (e.g., the bone tail's position). Not null.
+     * @param ends An array of ending points for the lines (e.g., the children's head positions). Not null.
+     */
     public ArmatureInterJointsWire(Vector3f start, Vector3f[] ends) {
         setMode(Mode.Lines);
         updateGeometry(start, ends);
     }
 
     /**
-     * For serialization only. Do not use.
+     * Updates the geometry of this mesh based on the provided start and end points.
+     * This method re-generates the position, texture coordinate, normal, and index buffers
+     * for the mesh.
+     *
+     * @param start The new starting point for the lines. Not null.
+     * @param ends An array of new ending points for the lines. Not null.
      */
-    protected ArmatureInterJointsWire() {
-    }
-
     protected void updateGeometry(Vector3f start, Vector3f[] ends) {
         float[] pos = new float[ends.length * 3 + 3];
         pos[0] = start.x;
@@ -78,7 +94,7 @@ public class ArmatureInterJointsWire extends Mesh {
         texCoord[0] = 0;
         texCoord[1] = 0;
         for (int i = 0; i < ends.length * 2; i++) {
-            texCoord[i + 2] = tmp.set(start).subtractLocal(ends[i / 2]).length();
+            texCoord[i + 2] = tempVec.set(start).subtractLocal(ends[i / 2]).length();
         }
         setBuffer(Type.TexCoord, 2, texCoord);
 

+ 215 - 57
jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureNode.java

@@ -1,7 +1,5 @@
-package com.jme3.scene.debug.custom;
-
 /*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -31,43 +29,73 @@ package com.jme3.scene.debug.custom;
  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
+package com.jme3.scene.debug.custom;
 
 import com.jme3.anim.Armature;
 import com.jme3.anim.Joint;
-import com.jme3.collision.*;
-import com.jme3.math.*;
+import com.jme3.collision.Collidable;
+import com.jme3.collision.CollisionResult;
+import com.jme3.collision.CollisionResults;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.MathUtils;
+import com.jme3.math.Ray;
+import com.jme3.math.Vector2f;
+import com.jme3.math.Vector3f;
 import com.jme3.renderer.Camera;
 import com.jme3.renderer.queue.RenderQueue;
-import com.jme3.scene.*;
+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.Line;
 
 import java.nio.FloatBuffer;
-import java.util.*;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 
 /**
- * The class that displays either wires between the bones' heads if no length
- * data is supplied and full bones' shapes otherwise.
+ * Renders an {@link Armature} for debugging purposes. It can display either
+ * wires connecting the heads of bones (if no length data is available) or
+ * full bone shapes (from head to tail) when length data is supplied.
  */
 public class ArmatureNode extends Node {
 
+    /**
+     * The size of the picking box in pixels for joint selection.
+     */
     public static final float PIXEL_BOX = 10f;
     /**
      * The armature to be displayed.
      */
     private final Armature armature;
     /**
-     * The map between the bone index and its length.
+     * Maps a {@link Joint} to its corresponding {@link Geometry} array.
+     * The array typically contains [jointGeometry, boneWireGeometry, boneOutlineGeometry].
      */
     private final Map<Joint, Geometry[]> jointToGeoms = new HashMap<>();
+    /**
+     * Maps a {@link Geometry} to its associated {@link Joint}. Used for picking.
+     */
     private final Map<Geometry, Joint> geomToJoint = new HashMap<>();
+    /**
+     * The currently selected joint.
+     */
     private Joint selectedJoint = null;
-    private final Vector3f tmp = new Vector3f();
-    private final Vector2f tmpv2 = new Vector2f();
+
+    // Temporary vectors for calculations to avoid repeated allocations
+    private final Vector3f tempVec3f = new Vector3f();
+    private final Vector2f tempVec2f = new Vector2f();
+
+    // Color constants for rendering
     private static final ColorRGBA selectedColor = ColorRGBA.Orange;
-    private static final ColorRGBA selectedColorJ = ColorRGBA.Yellow;
+    private static final ColorRGBA selectedColorJoint = ColorRGBA.Yellow;
     private static final ColorRGBA outlineColor = ColorRGBA.LightGray;
     private static final ColorRGBA baseColor = new ColorRGBA(0.05f, 0.05f, 0.05f, 1f);
 
+    /**
+     * The camera used for 2D picking calculations.
+     */
     private Camera camera;
 
 
@@ -88,27 +116,36 @@ public class ArmatureNode extends Node {
         setColor(origin, ColorRGBA.Green);
         attach(joints, true, origin);
 
+        // Recursively create geometries for all joints and bones in the armature
         for (Joint joint : armature.getRoots()) {
             createSkeletonGeoms(joint, joints, wires, outlines, deformingJoints);
         }
         this.updateModelBound();
-
     }
 
+    /**
+     * Recursively creates the geometries for a given joint and its children.
+     *
+     * @param joint           The current joint for which to create geometries.
+     * @param joints          The node for joint geometries.
+     * @param wires           The node for bone wire geometries.
+     * @param outlines        The node for bone outline geometries.
+     * @param deformingJoints A list of deforming joints.
+     */
     protected final void createSkeletonGeoms(Joint joint, Node joints, Node wires, Node outlines, List<Joint> deformingJoints) {
         Vector3f start = joint.getModelTransform().getTranslation().clone();
 
         Vector3f[] ends = null;
         if (!joint.getChildren().isEmpty()) {
             ends = new Vector3f[joint.getChildren().size()];
-        }
-
-        for (int i = 0; i < joint.getChildren().size(); i++) {
-            ends[i] = joint.getChildren().get(i).getModelTransform().getTranslation().clone();
+            for (int i = 0; i < ends.length; i++) {
+                ends[i] = joint.getChildren().get(i).getModelTransform().getTranslation().clone();
+            }
         }
 
         boolean deforms = deformingJoints.contains(joint);
 
+        // Create geometry for the joint head
         Geometry jGeom = new Geometry(joint.getName() + "Joint", new JointShape());
         jGeom.setLocalTranslation(start);
         attach(joints, deforms, jGeom);
@@ -134,8 +171,8 @@ public class ArmatureNode extends Node {
             setColor(bGeom, outlinesAttach == null ? outlineColor : baseColor);
             geomToJoint.put(bGeom, joint);
             bGeom.setUserData("start", getWorldTransform().transformVector(start, start));
-            for (int i = 0; i < ends.length; i++) {
-                getWorldTransform().transformVector(ends[i], ends[i]);
+            for (Vector3f end : ends) {
+                getWorldTransform().transformVector(end, end);
             }
             bGeom.setUserData("end", ends);
             bGeom.setQueueBucket(RenderQueue.Bucket.Transparent);
@@ -148,11 +185,17 @@ public class ArmatureNode extends Node {
         }
         jointToGeoms.put(joint, new Geometry[]{jGeom, bGeom, bGeomO});
 
+        // Recursively call for children
         for (Joint child : joint.getChildren()) {
             createSkeletonGeoms(child, joints, wires, outlines, deformingJoints);
         }
     }
 
+    /**
+     * Sets the camera to be used for 2D picking calculations.
+     *
+     * @param camera The camera to set.
+     */
     public void setCamera(Camera camera) {
         this.camera = camera;
     }
@@ -165,53 +208,83 @@ public class ArmatureNode extends Node {
         }
     }
 
-    protected Joint select(Geometry g) {
-        if (g == null) {
+    /**
+     * Selects a joint based on its associated geometry.
+     * If the selected geometry is already the current selection, no change occurs.
+     * Resets the selection if {@code geometry} is null.
+     *
+     * @param geo The geometry representing the joint or bone to select.
+     * @return The newly selected {@link Joint}, or null if no joint was selected or the selection was reset.
+     */
+    protected Joint select(Geometry geo) {
+        if (geo == null) {
             resetSelection();
             return null;
         }
-        Joint j = geomToJoint.get(g);
-        if (j != null) {
-            if (selectedJoint == j) {
+        Joint jointToSelect = geomToJoint.get(geo);
+        if (jointToSelect != null) {
+            if (selectedJoint == jointToSelect) {
                 return null;
             }
             resetSelection();
-            selectedJoint = j;
+            selectedJoint = jointToSelect;
             Geometry[] geomArray = jointToGeoms.get(selectedJoint);
-            setColor(geomArray[0], selectedColorJ);
+            // Color the joint head
+            setColor(geomArray[0], selectedColorJoint);
 
+            // Color the bone wire
             if (geomArray[1] != null) {
                 setColor(geomArray[1], selectedColor);
             }
 
+            // Restore outline color if present (as it's often the base color when bone is selected)
             if (geomArray[2] != null) {
                 setColor(geomArray[2], baseColor);
             }
-            return j;
+            return jointToSelect;
         }
         return null;
     }
 
+    /**
+     * Resets the color of the currently selected joint and bone geometries to their default colors
+     * and clears the {@code selectedJoint}.
+     */
     private void resetSelection() {
         if (selectedJoint == null) {
             return;
         }
         Geometry[] geoms = jointToGeoms.get(selectedJoint);
+        // Reset joint head color
         setColor(geoms[0], ColorRGBA.White);
+
+        // Reset bone wire color (depends on whether it has an outline)
         if (geoms[1] != null) {
             setColor(geoms[1], geoms[2] == null ? outlineColor : baseColor);
         }
+
+        // Reset bone outline color
         if (geoms[2] != null) {
             setColor(geoms[2], outlineColor);
         }
         selectedJoint = null;
     }
 
+    /**
+     * Returns the currently selected joint.
+     *
+     * @return The {@link Joint} that is currently selected, or null if no joint is selected.
+     */
     protected Joint getSelectedJoint() {
         return selectedJoint;
     }
 
-
+    /**
+     * Updates the geometries associated with a given joint and its children to reflect their
+     * current model transforms. This method is called recursively.
+     *
+     * @param joint The joint to update.
+     */
     protected final void updateSkeletonGeoms(Joint joint) {
         Geometry[] geoms = jointToGeoms.get(joint);
         if (geoms != null) {
@@ -232,70 +305,142 @@ public class ArmatureNode extends Node {
                         updateBoneMesh(bGeomO, start, ends);
                     }
                     bGeom.setUserData("start", getWorldTransform().transformVector(start, start));
-                    for (int i = 0; i < ends.length; i++) {
-                        getWorldTransform().transformVector(ends[i], ends[i]);
+                    for (Vector3f end : ends) {
+                        getWorldTransform().transformVector(end, end);
                     }
                     bGeom.setUserData("end", ends);
-
                 }
             }
         }
 
+        // Recursively update children
         for (Joint child : joint.getChildren()) {
             updateSkeletonGeoms(child);
         }
     }
 
+    /**
+     * Sets the color of the head geometry for a specific joint.
+     *
+     * @param joint The joint whose head color is to be set.
+     * @param color The new color for the joint head.
+     */
+    public void setHeadColor(Joint joint, ColorRGBA color) {
+        Geometry[] geomArray = jointToGeoms.get(joint);
+        setColor(geomArray[0], color);
+    }
+
+    /**
+     * Sets the color of all joint head geometries.
+     *
+     * @param color The new color for all joint heads.
+     */
+    public void setHeadColor(ColorRGBA color) {
+        for (Geometry[] geomArray : jointToGeoms.values()) {
+            setColor(geomArray[0], color);
+        }
+    }
+
+    /**
+     * Sets the color of all bone line geometries.
+     *
+     * @param color The new color for all bone lines.
+     */
+    public void setLineColor(ColorRGBA color) {
+        for (Geometry[] geomArray : jointToGeoms.values()) {
+            if (geomArray[1] != null) {
+                setColor(geomArray[1], color);
+            }
+        }
+    }
+
+    /**
+     * Performs a 2D pick operation to find joints or bones near the given cursor position.
+     * This method primarily checks for joint heads within a {@link #PIXEL_BOX} box
+     * around the cursor, and then checks for bone wires.
+     *
+     * @param cursor  The 2D screen coordinates of the pick ray origin.
+     * @param results The {@link CollisionResults} to store the pick results.
+     * @return The number of collisions found.
+     */
     public int pick(Vector2f cursor, CollisionResults results) {
+        if (camera == null) {
+            return 0;
+        }
 
-        for (Geometry g : geomToJoint.keySet()) {
-            if (g.getMesh() instanceof JointShape) {
-                camera.getScreenCoordinates(g.getWorldTranslation(), tmp);
-                if (cursor.x <= tmp.x + PIXEL_BOX && cursor.x >= tmp.x - PIXEL_BOX
-                        && cursor.y <= tmp.y + PIXEL_BOX && cursor.y >= tmp.y - PIXEL_BOX) {
+        int collisions = 0;
+        for (Geometry geo : geomToJoint.keySet()) {
+            if (geo.getMesh() instanceof JointShape) {
+                camera.getScreenCoordinates(geo.getWorldTranslation(), tempVec3f);
+                if (cursor.x <= tempVec3f.x + PIXEL_BOX && cursor.x >= tempVec3f.x - PIXEL_BOX
+                        && cursor.y <= tempVec3f.y + PIXEL_BOX && cursor.y >= tempVec3f.y - PIXEL_BOX) {
                     CollisionResult res = new CollisionResult();
-                    res.setGeometry(g);
+                    res.setGeometry(geo);
                     results.addCollision(res);
+                    collisions++;
                 }
             }
         }
-        return 0;
+        return collisions;
     }
 
+    /**
+     * Collides this {@code ArmatureNode} with a {@link Collidable} object, typically a {@link Ray}.
+     * It prioritizes 2D picking for joint heads and then performs a distance-based check for bone wires.
+     *
+     * @param other   The {@link Collidable} object to collide with.
+     * @param results The {@link CollisionResults} to store the collision information.
+     * @return The number of collisions found.
+     */
     @Override
     public int collideWith(Collidable other, CollisionResults results) {
-        if (!(other instanceof Ray)) {
+        if (!(other instanceof Ray) || camera == null) {
             return 0;
         }
 
-        // first try a 2D pick;
-        camera.getScreenCoordinates(((Ray)other).getOrigin(),tmp);
-        tmpv2.x = tmp.x;
-        tmpv2.y = tmp.y;
-        int nbHit = pick(tmpv2, results);
-        if (nbHit > 0) {
-            return nbHit;
+        // First, try a 2D pick for joint heads
+        camera.getScreenCoordinates(((Ray) other).getOrigin(), tempVec3f);
+        tempVec2f.x = tempVec3f.x;
+        tempVec2f.y = tempVec3f.y;
+        int hitCount = pick(tempVec2f, results);
+
+        // If 2D pick found hits, return them. Otherwise, proceed with bone wire collision.
+        if (hitCount > 0) {
+            return hitCount;
         }
 
+        // Check for bone wire collisions
         for (Geometry g : geomToJoint.keySet()) {
             if (g.getMesh() instanceof JointShape) {
+                // Skip joint heads, already handled by 2D pick
                 continue;
             }
+
             Vector3f start = g.getUserData("start");
             Vector3f[] ends = g.getUserData("end");
-            for (int i = 0; i < ends.length; i++) {
-                float len = MathUtils.raySegmentShortestDistance((Ray) other, start, ends[i], camera);
-                if (len > 0 && len < PIXEL_BOX) {
+
+            for (Vector3f end : ends) {
+                // Calculate the shortest distance from ray to bone segment
+                float dist = MathUtils.raySegmentShortestDistance((Ray) other, start, end, camera);
+                if (dist > 0 && dist < PIXEL_BOX) {
                     CollisionResult res = new CollisionResult();
                     res.setGeometry(g);
                     results.addCollision(res);
-                    nbHit++;
+                    hitCount++;
                 }
             }
         }
-        return nbHit;
+        return hitCount;
     }
 
+    /**
+     * Updates the mesh of a bone geometry (either {@link ArmatureInterJointsWire} or {@link Line})
+     * with new start and end points.
+     *
+     * @param geom  The bone geometry whose mesh needs updating.
+     * @param start The new starting point of the bone.
+     * @param ends  The new ending points of the bone (can be multiple for {@link ArmatureInterJointsWire}).
+     */
     private void updateBoneMesh(Geometry geom, Vector3f start, Vector3f[] ends) {
         if (geom.getMesh() instanceof ArmatureInterJointsWire) {
             ((ArmatureInterJointsWire) geom.getMesh()).updatePoints(start, ends);
@@ -305,18 +450,31 @@ public class ArmatureNode extends Node {
         geom.updateModelBound();
     }
 
-    private void setColor(Geometry g, ColorRGBA color) {
-        float[] colors = new float[g.getMesh().getVertexCount() * 4];
-        for (int i = 0; i < g.getMesh().getVertexCount() * 4; i += 4) {
+    /**
+     * Sets the color of a given geometry's vertex buffer.
+     * This method creates a new color buffer or updates an existing one with the specified color.
+     *
+     * @param geo   The geometry whose color is to be set.
+     * @param color The {@link ColorRGBA} to apply.
+     */
+    private void setColor(Geometry geo, ColorRGBA color) {
+        Mesh mesh = geo.getMesh();
+        int vertexCount = mesh.getVertexCount();
+
+        float[] colors = new float[vertexCount * 4];
+        for (int i = 0; i < colors.length; i += 4) {
             colors[i] = color.r;
             colors[i + 1] = color.g;
             colors[i + 2] = color.b;
             colors[i + 3] = color.a;
         }
-        VertexBuffer colorBuff = g.getMesh().getBuffer(VertexBuffer.Type.Color);
+
+        VertexBuffer colorBuff = geo.getMesh().getBuffer(VertexBuffer.Type.Color);
         if (colorBuff == null) {
-            g.getMesh().setBuffer(VertexBuffer.Type.Color, 4, colors);
+            // If no color buffer exists, create a new one
+            geo.getMesh().setBuffer(VertexBuffer.Type.Color, 4, colors);
         } else {
+            // If a color buffer exists, update its data
             FloatBuffer cBuff = (FloatBuffer) colorBuff.getData();
             cBuff.rewind();
             cBuff.put(colors);