Преглед изворни кода

Merge pull request #2504 from capdevon/capdevon-SkinningControl

Refactor: SkinningControl optimization + javadoc
Ryan McDonough пре 2 месеци
родитељ
комит
a42bb5beef

+ 181 - 150
jme3-core/src/main/java/com/jme3/anim/SkinningControl.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2023 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -31,12 +31,21 @@
  */
 package com.jme3.anim;
 
-import com.jme3.export.*;
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
 import com.jme3.material.MatParamOverride;
 import com.jme3.math.FastMath;
 import com.jme3.math.Matrix4f;
-import com.jme3.renderer.*;
-import com.jme3.scene.*;
+import com.jme3.renderer.RenderManager;
+import com.jme3.renderer.RendererException;
+import com.jme3.renderer.ViewPort;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Mesh;
+import com.jme3.scene.Node;
+import com.jme3.scene.Spatial;
+import com.jme3.scene.VertexBuffer;
 import com.jme3.scene.VertexBuffer.Type;
 import com.jme3.scene.control.AbstractControl;
 import com.jme3.scene.mesh.IndexBuffer;
@@ -53,64 +62,77 @@ import java.util.logging.Level;
 import java.util.logging.Logger;
 
 /**
- * The Skinning control deforms a model according to an armature, It handles the
- * computation of the deformation matrices and performs the transformations on
- * the mesh
+ * The `SkinningControl` deforms a 3D model according to an {@link Armature}. It manages the
+ * computation of deformation matrices and applies these transformations to the mesh,
+ * supporting both software and hardware-accelerated skinning.
+ *
+ * <p>
+ * **Software Skinning:** Performed on the CPU, offering broader compatibility but
+ * potentially lower performance for complex models.
  * <p>
- * It can perform software skinning or Hardware skinning
+ * **Hardware Skinning:** Utilizes the GPU for deformation, providing significantly
+ * better performance but requiring shader support and having a limit on the number
+ * of bones (typically 255 in common shaders).
  *
- * @author Rémy Bouquet Based on SkeletonControl by Kirill Vainer
+ * @author Nehon
  */
-public class SkinningControl extends AbstractControl implements Cloneable, JmeCloneable {
+public class SkinningControl extends AbstractControl implements JmeCloneable {
 
     private static final Logger logger = Logger.getLogger(SkinningControl.class.getName());
 
+    /**
+     * The maximum number of bones supported for hardware skinning in common shaders.
+     */
+    private static final int MAX_BONES_HW_SKINNING_SUPPORT = 255;
+
     /**
      * The armature of the model.
      */
     private Armature armature;
 
     /**
-     * List of geometries affected by this control.
+     * A list of geometries that this control will deform.
      */
     private SafeArrayList<Geometry> targets = new SafeArrayList<>(Geometry.class);
 
     /**
-     * Used to track when a mesh was updated. Meshes are only updated if they
+     * Used to track when a mesh needs to be updated. Meshes are only updated if they
      * are visible in at least one camera.
      */
-    private boolean wasMeshUpdated = false;
+    private boolean meshUpdateRequired = true;
 
     /**
-     * User wishes to use hardware skinning if available.
+     * Indicates whether hardware skinning is preferred. If `true` and the GPU
+     * supports it, hardware skinning will be enabled.
      */
-    private transient boolean hwSkinningDesired = true;
+    private transient boolean hwSkinningPreferred = true;
 
     /**
-     * Hardware skinning is currently being used.
+     * Indicates if hardware skinning is currently active and being used.
      */
     private transient boolean hwSkinningEnabled = false;
 
     /**
-     * Hardware skinning was tested on this GPU, results
-     * are stored in {@link #hwSkinningSupported} variable.
+     * Flag indicating whether hardware skinning compatibility has been tested
+     * on the current GPU. Results are stored in {@link #hwSkinningSupported}.
      */
     private transient boolean hwSkinningTested = false;
 
     /**
-     * If hardware skinning was {@link #hwSkinningTested tested}, then
-     * this variable will be set to true if supported, and false if otherwise.
+     * Stores the result of the hardware skinning compatibility test. `true` if
+     * supported, `false` otherwise. This is only valid after
+     * {@link #hwSkinningTested} is `true`.
      */
     private transient boolean hwSkinningSupported = false;
 
     /**
-     * Bone offset matrices, recreated each frame
+     * Bone offset matrices, computed each frame to deform the mesh based on
+     * the armature's current pose.
      */
-    private transient Matrix4f[] offsetMatrices;
-
+    private transient Matrix4f[] boneOffsetMatrices;
 
-    private MatParamOverride numberOfJointsParam;
-    private MatParamOverride jointMatricesParam;
+    private MatParamOverride numberOfJointsParam = new MatParamOverride(VarType.Int, "NumberOfBones", null);
+    private MatParamOverride jointMatricesParam = new MatParamOverride(VarType.Matrix4Array, "BoneMatrices", null);
 
     /**
      * Serialization only. Do not use.
@@ -119,26 +141,26 @@ public class SkinningControl extends AbstractControl implements Cloneable, JmeCl
     }
 
     /**
-     * Creates an armature control. The list of targets will be acquired
-     * automatically when the control is attached to a node.
+     * Creates a new `SkinningControl` for the given armature.
      *
-     * @param armature the armature
+     * @param armature The armature that drives the deformation (not null).
      */
     public SkinningControl(Armature armature) {
         if (armature == null) {
-            throw new IllegalArgumentException("armature cannot be null");
+            throw new IllegalArgumentException("armature cannot be null.");
         }
         this.armature = armature;
-        this.numberOfJointsParam = new MatParamOverride(VarType.Int, "NumberOfBones", null);
-        this.jointMatricesParam = new MatParamOverride(VarType.Matrix4Array, "BoneMatrices", null);
     }
 
-
-    private void switchToHardware() {
+    /**
+     * Configures the material parameters and meshes for hardware skinning.
+     */
+    private void enableHardwareSkinning() {
         numberOfJointsParam.setEnabled(true);
         jointMatricesParam.setEnabled(true);
 
-        // Next full 10 bones (e.g. 30 on 24 bones)
+        // Calculate the number of bones rounded up to the nearest multiple of 10.
+        // This is often required by shaders for array uniform declarations.
         int numBones = ((armature.getJointCount() / 10) + 1) * 10;
         numberOfJointsParam.setValue(numBones);
 
@@ -150,7 +172,10 @@ public class SkinningControl extends AbstractControl implements Cloneable, JmeCl
         }
     }
 
-    private void switchToSoftware() {
+    /**
+     * Configures the material parameters and meshes for software skinning.
+     */
+    private void enableSoftwareSkinning() {
         numberOfJointsParam.setEnabled(false);
         jointMatricesParam.setEnabled(false);
 
@@ -162,22 +187,34 @@ public class SkinningControl extends AbstractControl implements Cloneable, JmeCl
         }
     }
 
-    private boolean testHardwareSupported(RenderManager rm) {
-
-        //Only 255 bones max supported with hardware skinning
-        if (armature.getJointCount() > 255) {
+    /**
+     * Tests if hardware skinning is supported by the GPU for the current spatial.
+     *
+     * @param renderManager the RenderManager instance
+     * @return true if hardware skinning is supported, false otherwise
+     */
+    private boolean testHardwareSupported(RenderManager renderManager) {
+        // Only 255 bones max supported with hardware skinning in common shaders.
+        if (armature.getJointCount() > MAX_BONES_HW_SKINNING_SUPPORT) {
+            logger.log(Level.INFO, "Hardware skinning not supported for {0}: Too many bones ({1} > 255).",
+                    new Object[]{spatial, armature.getJointCount()});
             return false;
         }
 
-        switchToHardware();
+        // Temporarily enable hardware skinning to test shader compilation.
+        enableHardwareSkinning();
+        boolean hwSkinningEngaged = false;
 
         try {
-            rm.preloadScene(spatial);
-            return true;
-        } catch (RendererException e) {
-            logger.log(Level.WARNING, "Could not enable HW skinning due to shader compile error:", e);
-            return false;
+            renderManager.preloadScene(spatial);
+            logger.log(Level.INFO, "Hardware skinning engaged for {0}", spatial);
+            hwSkinningEngaged = true;
+
+        } catch (RendererException ex) {
+            logger.log(Level.WARNING, "Could not enable HW skinning due to shader compile error: ", ex);
         }
+
+        return hwSkinningEngaged;
     }
 
     /**
@@ -190,7 +227,7 @@ public class SkinningControl extends AbstractControl implements Cloneable, JmeCl
      * @see #isHardwareSkinningUsed()
      */
     public void setHardwareSkinningPreferred(boolean preferred) {
-        hwSkinningDesired = preferred;
+        hwSkinningPreferred = preferred;
     }
 
     /**
@@ -199,7 +236,7 @@ public class SkinningControl extends AbstractControl implements Cloneable, JmeCl
      * @see #setHardwareSkinningPreferred(boolean)
      */
     public boolean isHardwareSkinningPreferred() {
-        return hwSkinningDesired;
+        return hwSkinningPreferred;
     }
 
     /**
@@ -209,25 +246,21 @@ public class SkinningControl extends AbstractControl implements Cloneable, JmeCl
         return hwSkinningEnabled;
     }
 
-
     /**
-     * If specified the geometry has an animated mesh, add its mesh and material
-     * to the lists of animation targets.
+     * Recursively finds and adds animated geometries to the targets list.
+     *
+     * @param sp The spatial to search within.
      */
-    private void findTargets(Geometry geometry) {
-        Mesh mesh = geometry.getMesh();
-        if (mesh != null && mesh.isAnimated()) {
-            targets.add(geometry);
-        }
-
-    }
-
-    private void findTargets(Node node) {
-        for (Spatial child : node.getChildren()) {
-            if (child instanceof Geometry) {
-                findTargets((Geometry) child);
-            } else if (child instanceof Node) {
-                findTargets((Node) child);
+    private void collectAnimatedGeometries(Spatial sp) {
+        if (sp instanceof Geometry) {
+            Geometry geo = (Geometry) sp;
+            Mesh mesh = geo.getMesh();
+            if (mesh != null && mesh.isAnimated()) {
+                targets.add(geo);
+            }
+        } else if (sp instanceof Node) {
+            for (Spatial child : ((Node) sp).getChildren()) {
+                collectAnimatedGeometries(child);
             }
         }
     }
@@ -236,65 +269,72 @@ public class SkinningControl extends AbstractControl implements Cloneable, JmeCl
     public void setSpatial(Spatial spatial) {
         Spatial oldSpatial = this.spatial;
         super.setSpatial(spatial);
-        updateTargetsAndMaterials(spatial);
+        updateAnimationTargets(spatial);
 
         if (oldSpatial != null) {
+            // Ensure parameters are removed from the old spatial to prevent memory leaks
             oldSpatial.removeMatParamOverride(numberOfJointsParam);
             oldSpatial.removeMatParamOverride(jointMatricesParam);
         }
 
         if (spatial != null) {
-            spatial.removeMatParamOverride(numberOfJointsParam);
-            spatial.removeMatParamOverride(jointMatricesParam);
+            // Add parameters to the new spatial. No need to remove first if they are not already present.
             spatial.addMatParamOverride(numberOfJointsParam);
             spatial.addMatParamOverride(jointMatricesParam);
         }
     }
 
+    /**
+     * Performs software skinning updates.
+     */
     private void controlRenderSoftware() {
         resetToBind(); // reset morph meshes to bind pose
 
-        offsetMatrices = armature.computeSkinningMatrices();
+        boneOffsetMatrices = armature.computeSkinningMatrices();
 
         for (Geometry geometry : targets) {
             Mesh mesh = geometry.getMesh();
-            // NOTE: This assumes code higher up has
-            // already ensured this mesh is animated.
-            // Otherwise a crash will happen in skin update.
-            softwareSkinUpdate(mesh, offsetMatrices);
+            // NOTE: This assumes code higher up has already ensured this mesh is animated.
+            // Otherwise, a crash will happen in skin update.
+            applySoftwareSkinning(mesh, boneOffsetMatrices);
         }
     }
 
+    /**
+     * Prepares parameters for hardware skinning.
+     */
     private void controlRenderHardware() {
-        offsetMatrices = armature.computeSkinningMatrices();
-        jointMatricesParam.setValue(offsetMatrices);
+        boneOffsetMatrices = armature.computeSkinningMatrices();
+        jointMatricesParam.setValue(boneOffsetMatrices);
     }
 
     @Override
     protected void controlRender(RenderManager rm, ViewPort vp) {
-        if (!wasMeshUpdated) {
-            updateTargetsAndMaterials(spatial);
+        if (meshUpdateRequired) {
+            updateAnimationTargets(spatial);
 
             // Prevent illegal cases. These should never happen.
-            assert hwSkinningTested || (!hwSkinningTested && !hwSkinningSupported && !hwSkinningEnabled);
-            assert !hwSkinningEnabled || (hwSkinningEnabled && hwSkinningTested && hwSkinningSupported);
+            assert hwSkinningTested || (!hwSkinningSupported && !hwSkinningEnabled);
+            assert !hwSkinningEnabled || (hwSkinningTested && hwSkinningSupported);
 
-            if (hwSkinningDesired && !hwSkinningTested) {
+            if (hwSkinningPreferred && !hwSkinningTested) {
+                // If hardware skinning is preferred and hasn't been tested yet, test it.
                 hwSkinningTested = true;
                 hwSkinningSupported = testHardwareSupported(rm);
 
                 if (hwSkinningSupported) {
                     hwSkinningEnabled = true;
-
-                    Logger.getLogger(SkinningControl.class.getName()).log(Level.INFO, "Hardware skinning engaged for {0}", spatial);
                 } else {
-                    switchToSoftware();
+                    enableSoftwareSkinning();
                 }
-            } else if (hwSkinningDesired && hwSkinningSupported && !hwSkinningEnabled) {
-                switchToHardware();
+            } else if (hwSkinningPreferred && hwSkinningSupported && !hwSkinningEnabled) {
+                // If hardware skinning is preferred, supported, but not yet enabled, enable it.
+                enableHardwareSkinning();
                 hwSkinningEnabled = true;
-            } else if (!hwSkinningDesired && hwSkinningEnabled) {
-                switchToSoftware();
+
+            } else if (!hwSkinningPreferred && hwSkinningEnabled) {
+                // If hardware skinning is no longer preferred but is enabled, switch to software.
+                enableSoftwareSkinning();
                 hwSkinningEnabled = false;
             }
 
@@ -304,17 +344,22 @@ public class SkinningControl extends AbstractControl implements Cloneable, JmeCl
                 controlRenderSoftware();
             }
 
-            wasMeshUpdated = true;
+            meshUpdateRequired = false; // Reset flag after update
         }
     }
 
     @Override
     protected void controlUpdate(float tpf) {
-        wasMeshUpdated = false;
+        meshUpdateRequired = true; // Mark for mesh update on next render pass
         armature.update();
     }
 
-    //only do this for software updates
+    /**
+     * Resets the vertex, normal, and tangent buffers of animated meshes to their
+     * original bind pose. This is crucial for software skinning to ensure
+     * transformations are applied from a consistent base.
+     * This method is only applied when performing software updates.
+     */
     void resetToBind() {
         for (Geometry geometry : targets) {
             Mesh mesh = geometry.getMesh();
@@ -378,51 +423,51 @@ public class SkinningControl extends AbstractControl implements Cloneable, JmeCl
     }
 
     /**
-     * Access the attachments node of the named bone. If the bone doesn't
-     * already have an attachments node, create one and attach it to the scene
-     * graph. Models and effects attached to the attachments node will follow
-     * the bone's motions.
+     * Provides access to the attachment node for a specific joint in the armature.
+     * If an attachment node does not already exist for the named joint, one will be
+     * created and attached to the scene graph. Models or effects attached to this
+     * node will follow the motion of the corresponding bone.
      *
      * @param jointName the name of the joint
      * @return the attachments node of the joint
      */
     public Node getAttachmentsNode(String jointName) {
-        Joint b = armature.getJoint(jointName);
-        if (b == null) {
-            throw new IllegalArgumentException("Given bone name does not exist "
-                    + "in the armature.");
+        Joint joint = armature.getJoint(jointName);
+        if (joint == null) {
+            throw new IllegalArgumentException(
+                    "Given joint name '" + jointName + "' does not exist in the armature.");
         }
 
-        updateTargetsAndMaterials(spatial);
-        int boneIndex = armature.getJointIndex(b);
-        Node n = b.getAttachmentsNode(boneIndex, targets);
-        /*
-         * Select a node to parent the attachments node.
-         */
+        updateAnimationTargets(spatial);
+        int jointIndex = armature.getJointIndex(joint);
+        Node attachNode = joint.getAttachmentsNode(jointIndex, targets);
+
+        // Determine the appropriate parent for the attachment node.
         Node parent;
         if (spatial instanceof Node) {
             parent = (Node) spatial; // the usual case
         } else {
             parent = spatial.getParent();
         }
-        parent.attachChild(n);
+        parent.attachChild(attachNode);
 
-        return n;
+        return attachNode;
     }
 
     /**
-     * returns the armature of this control
+     * Returns the armature associated with this skinning control.
      *
-     * @return the pre-existing instance
+     * @return The pre-existing `Armature` instance.
      */
     public Armature getArmature() {
         return armature;
     }
 
     /**
-     * Enumerate the target meshes of this control.
+     * Returns an array containing all the target meshes that this control
+     * is currently affecting.
      *
-     * @return a new array
+     * @return A new array of `Mesh` objects.
      */
     public Mesh[] getTargets() {
         Mesh[] result = new Mesh[targets.size()];
@@ -437,30 +482,31 @@ public class SkinningControl extends AbstractControl implements Cloneable, JmeCl
     }
 
     /**
-     * Update the mesh according to the given transformation matrices
+     * Applies software skinning transformations to the given mesh using the
+     * provided bone offset matrices.
      *
-     * @param mesh           then mesh
-     * @param offsetMatrices the transformation matrices to apply
+     * @param mesh           The mesh to deform.
+     * @param offsetMatrices The array of transformation matrices for each bone.
      */
-    private void softwareSkinUpdate(Mesh mesh, Matrix4f[] offsetMatrices) {
+    private void applySoftwareSkinning(Mesh mesh, Matrix4f[] offsetMatrices) {
 
         VertexBuffer tb = mesh.getBuffer(Type.Tangent);
         if (tb == null) {
-            //if there are no tangents use the classic skinning
+            // if there are no tangents use the classic skinning
             applySkinning(mesh, offsetMatrices);
         } else {
-            //if there are tangents use the skinning with tangents
+            // if there are tangents use the skinning with tangents
             applySkinningTangents(mesh, offsetMatrices, tb);
         }
-
-
     }
 
     /**
-     * Method to apply skinning transforms to a mesh's buffers
+     * Applies skinning transformations to a mesh's position and normal buffers.
+     * This method iterates through each vertex, applies the weighted sum of
+     * bone transformations, and updates the vertex buffers.
      *
-     * @param mesh           the mesh
-     * @param offsetMatrices the offset matrices to apply
+     * @param mesh           The mesh to apply skinning to.
+     * @param offsetMatrices The bone offset matrices to use for transformation.
      */
     private void applySkinning(Mesh mesh, Matrix4f[] offsetMatrices) {
         int maxWeightsPerVert = mesh.getMaxNumWeights();
@@ -555,19 +601,16 @@ public class SkinningControl extends AbstractControl implements Cloneable, JmeCl
 
         vb.updateData(fvb);
         nb.updateData(fnb);
-
     }
 
     /**
-     * Specific method for skinning with tangents to avoid cluttering the
-     * classic skinning calculation with null checks that would slow down the
-     * process even if tangents don't have to be computed. Also the iteration
-     * has additional indexes since tangent has 4 components instead of 3 for
-     * pos and norm
+     * Applies skinning transformations to a mesh's position, normal, and tangent buffers.
+     * This method is specifically designed for meshes that include tangent data,
+     * ensuring proper deformation of tangents alongside positions and normals.
      *
-     * @param mesh           the mesh
-     * @param offsetMatrices the offsetMatrices to apply
-     * @param tb             the tangent vertexBuffer
+     * @param mesh           The mesh to apply skinning to.
+     * @param offsetMatrices The bone offset matrices to use for transformation.
+     * @param tb             The tangent `VertexBuffer`.
      */
     private void applySkinningTangents(Mesh mesh, Matrix4f[] offsetMatrices, VertexBuffer tb) {
         int maxWeightsPerVert = mesh.getMaxNumWeights();
@@ -594,7 +637,6 @@ public class SkinningControl extends AbstractControl implements Cloneable, JmeCl
         FloatBuffer ftb = (FloatBuffer) tb.getData();
         ftb.rewind();
 
-
         // get boneIndexes and weights for mesh
         IndexBuffer ib = IndexBuffer.wrapIndexBuffer(mesh.getBuffer(Type.BoneIndex).getData());
         FloatBuffer wb = (FloatBuffer) mesh.getBuffer(Type.BoneWeight).getData();
@@ -605,8 +647,6 @@ public class SkinningControl extends AbstractControl implements Cloneable, JmeCl
         int idxWeights = 0;
 
         TempVars vars = TempVars.get();
-
-
         float[] posBuf = vars.skinPositions;
         float[] normBuf = vars.skinNormals;
         float[] tanBuf = vars.skinTangents;
@@ -723,9 +763,6 @@ public class SkinningControl extends AbstractControl implements Cloneable, JmeCl
         super.write(ex);
         OutputCapsule oc = ex.getCapsule(this);
         oc.write(armature, "armature", null);
-
-        oc.write(numberOfJointsParam, "numberOfBonesParam", null);
-        oc.write(jointMatricesParam, "boneMatricesParam", null);
     }
 
     /**
@@ -741,15 +778,13 @@ public class SkinningControl extends AbstractControl implements Cloneable, JmeCl
         InputCapsule in = im.getCapsule(this);
         armature = (Armature) in.readSavable("armature", null);
 
-        numberOfJointsParam = (MatParamOverride) in.readSavable("numberOfBonesParam", null);
-        jointMatricesParam = (MatParamOverride) in.readSavable("boneMatricesParam", null);
-
-        if (numberOfJointsParam == null) {
-            numberOfJointsParam = new MatParamOverride(VarType.Int, "NumberOfBones", null);
-            jointMatricesParam = new MatParamOverride(VarType.Matrix4Array, "BoneMatrices", null);
-            getSpatial().addMatParamOverride(numberOfJointsParam);
-            getSpatial().addMatParamOverride(jointMatricesParam);
+        for (MatParamOverride mpo : spatial.getLocalMatParamOverrides().getArray()) {
+            if (mpo.getName().equals("NumberOfBones") || mpo.getName().equals("BoneMatrices")) {
+                spatial.removeMatParamOverride(mpo);
+            }
         }
+        spatial.addMatParamOverride(numberOfJointsParam);
+        spatial.addMatParamOverride(jointMatricesParam);
     }
 
     /**
@@ -757,13 +792,9 @@ public class SkinningControl extends AbstractControl implements Cloneable, JmeCl
      *
      * @param spatial the controlled spatial
      */
-    private void updateTargetsAndMaterials(Spatial spatial) {
+    private void updateAnimationTargets(Spatial spatial) {
         targets.clear();
-
-        if (spatial instanceof Node) {
-            findTargets((Node) spatial);
-        } else if (spatial instanceof Geometry) {
-            findTargets((Geometry) spatial);
-        }
+        collectAnimatedGeometries(spatial);
     }
+
 }

+ 79 - 23
jme3-examples/src/main/java/jme3test/export/TestOgreConvert.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2012 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -29,53 +29,109 @@
  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
-
 package jme3test.export;
 
 import com.jme3.anim.AnimComposer;
+import com.jme3.anim.SkinningControl;
 import com.jme3.app.SimpleApplication;
 import com.jme3.export.binary.BinaryExporter;
-import com.jme3.export.binary.BinaryImporter;
+import com.jme3.font.BitmapText;
+import com.jme3.light.AmbientLight;
 import com.jme3.light.DirectionalLight;
+import com.jme3.material.MatParamOverride;
 import com.jme3.math.ColorRGBA;
 import com.jme3.math.Vector3f;
-import com.jme3.scene.Node;
 import com.jme3.scene.Spatial;
 
-import java.io.*;
-
+/**
+ * This class is a jMonkeyEngine 3 (jME3) test application designed to verify
+ * the import, export, and runtime behavior of 3D models, particularly those
+ * in or compatible with the Ogre3D format (.mesh.xml).
+ * It loads an Ogre model, saves and reloads it using jME3's binary exporter,
+ * plays an animation, and displays debugging information about its skinning
+ * and material parameters.
+ *
+ * @author capdevon
+ */
 public class TestOgreConvert extends SimpleApplication {
 
-    public static void main(String[] args){
+    public static void main(String[] args) {
         TestOgreConvert app = new TestOgreConvert();
+        app.setPauseOnLostFocus(false);
         app.start();
     }
 
+    private final StringBuilder sb = new StringBuilder();
+    private int frameCount = 0;
+    private BitmapText bmp;
+    private Spatial spCopy;
+    private SkinningControl skinningControl;
+
     @Override
     public void simpleInitApp() {
-        Spatial ogreModel = assetManager.loadModel("Models/Oto/Oto.mesh.xml");
+        configureCamera();
+        setupLights();
+
+        bmp = createLabelText(10, 20, "<placeholder>");
+
+        // Load the Ogre model (Oto.mesh.xml) from the assets
+        Spatial model = assetManager.loadModel("Models/Oto/Oto.mesh.xml");
+        // Save the loaded model to jME3's binary format and then reload it.
+        // This tests the binary serialization/deserialization process.
+        spCopy = BinaryExporter.saveAndLoad(assetManager, model);
+        spCopy.setName("Oto-Copy");
+        rootNode.attachChild(spCopy);
+
+        AnimComposer animComposer = spCopy.getControl(AnimComposer.class);
+        animComposer.setCurrentAction("Walk");
+
+        // Get the SkinningControl from the model to inspect skinning properties
+        skinningControl = spCopy.getControl(SkinningControl.class);
+    }
+
+    private void setupLights() {
+        AmbientLight al = new AmbientLight();
+        rootNode.addLight(al);
 
         DirectionalLight dl = new DirectionalLight();
-        dl.setColor(ColorRGBA.White);
-        dl.setDirection(new Vector3f(0,-1,-1).normalizeLocal());
+        dl.setDirection(new Vector3f(0, -1, -1).normalizeLocal());
         rootNode.addLight(dl);
+    }
 
-        try {
-            ByteArrayOutputStream baos = new ByteArrayOutputStream();
-            BinaryExporter exp = new BinaryExporter();
-            exp.save(ogreModel, baos);
+    private void configureCamera() {
+        flyCam.setDragToRotate(true);
+        flyCam.setMoveSpeed(15f);
 
-            ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
-            BinaryImporter imp = new BinaryImporter();
-            imp.setAssetManager(assetManager);
-            Node ogreModelReloaded = (Node) imp.load(bais, null, null);
+        cam.setLocation(new Vector3f(0, 0, 20));
+    }
 
-            AnimComposer composer = ogreModelReloaded.getControl(AnimComposer.class);
-            composer.setCurrentAction("Walk");
+    @Override
+    public void simpleUpdate(float tpf) {
+        frameCount++;
+        if (frameCount == 10) {
+            frameCount = 0;
 
-            rootNode.attachChild(ogreModelReloaded);
-        } catch (IOException ex){
-            ex.printStackTrace();
+            sb.append("HW Skinning Preferred: ").append(skinningControl.isHardwareSkinningPreferred()).append("\n");
+            sb.append("HW Skinning Enabled: ").append(skinningControl.isHardwareSkinningUsed()).append("\n");
+            sb.append("Mesh Targets: ").append(skinningControl.getTargets().length).append("\n");
+
+            for (MatParamOverride mpo : spCopy.getLocalMatParamOverrides()) {
+                sb.append(mpo.getVarType()).append(" ");
+                sb.append(mpo.getName()).append(": ");
+                sb.append(mpo.getValue()).append("\n");
+            }
+
+            bmp.setText(sb.toString());
+            sb.setLength(0);
         }
     }
+
+    private BitmapText createLabelText(int x, int y, String text) {
+        BitmapText bmp = new BitmapText(guiFont);
+        bmp.setText(text);
+        bmp.setLocalTranslation(x, settings.getHeight() - y, 0);
+        bmp.setColor(ColorRGBA.Red);
+        guiNode.attachChild(bmp);
+        return bmp;
+    }
 }