Browse Source

Add glTF draco support (#2591)

* Add extension loader for Draco

* Add Draco glTF loading tests

* add draco dependency

* format

* example refactoring

* remove dependency from GL renderer

* use openize drako fork

* Always minimize visibility. Add comments.

* Minor formatting

* Restore proper formatting

* Handle quantized/normalized attributes from Draco

* Drako library update and skinning cleanups

* Removed probably unused repository entry

* Minor update for glTF loader test

* Move functions into BufferUtils

* Adjust log level for Draco handling

* Fill existing index buffer instead of creating new one

* Set up skinning buffers only once

* use artifact shipped by jme

* default to non-draco model

---------

Co-authored-by: Riccardo Balbo <[email protected]>
Marco Hutter 1 week ago
parent
commit
7a2d8ea134

+ 3 - 0
jme3-core/src/main/java/com/jme3/scene/Mesh.java

@@ -366,6 +366,7 @@ public class Mesh implements Savable, Cloneable, JmeCloneable {
                 pos.getNumComponents(),
                 pos.getFormat(),
                 BufferUtils.clone(pos.getData()));
+        clearBuffer(bindPos.getBufferType());
         setBuffer(bindPos);
 
         // XXX: note that this method also sets stream mode
@@ -379,6 +380,7 @@ public class Mesh implements Savable, Cloneable, JmeCloneable {
                     norm.getNumComponents(),
                     norm.getFormat(),
                     BufferUtils.clone(norm.getData()));
+            clearBuffer(bindNorm.getBufferType());
             setBuffer(bindNorm);
             norm.setUsage(Usage.Stream);
         }
@@ -390,6 +392,7 @@ public class Mesh implements Savable, Cloneable, JmeCloneable {
                     tangents.getNumComponents(),
                     tangents.getFormat(),
                     BufferUtils.clone(tangents.getData()));
+            clearBuffer(bindTangents.getBufferType());
             setBuffer(bindTangents);
             tangents.setUsage(Usage.Stream);
         }// else hardware setup does nothing, mesh already in bind pose

+ 31 - 0
jme3-core/src/main/java/com/jme3/util/BufferUtils.java

@@ -1139,6 +1139,37 @@ public final class BufferUtils {
         return copy;
     }
 
+
+    /**
+     * Create a byte buffer containing the given values, cast to <code>byte</code>
+     *
+     * @param array
+     *            The array
+     * @return The buffer
+     */
+    public static Buffer createByteBuffer(int[] array) {
+        ByteBuffer buffer = BufferUtils.createByteBuffer(array.length);
+        for (int i = 0; i < array.length; i++) {
+            buffer.put(i, (byte) array[i]);
+        }
+        return buffer;
+    }
+
+    /**
+     * Create a short buffer containing the given values, cast to <code>short</code>
+     *
+     * @param array
+     *            The array
+     * @return The buffer
+     */
+    public static Buffer createShortBuffer(int[] array) {
+        ShortBuffer buffer = BufferUtils.createShortBuffer(array.length);
+        for (int i = 0; i < array.length; i++) {
+            buffer.put(i, (short) array[i]);
+        }
+        return buffer;
+    }
+
     /**
      * Ensures there is at least the <code>required</code> number of entries
      * left after the current position of the buffer. If the buffer is too small

+ 134 - 139
jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java

@@ -32,46 +32,50 @@
 package jme3test.model;
 
 import com.jme3.anim.AnimComposer;
-import com.jme3.anim.SkinningControl;
 import com.jme3.app.*;
 import com.jme3.asset.plugins.FileLocator;
 import com.jme3.asset.plugins.UrlLocator;
+import com.jme3.bounding.BoundingBox;
 import com.jme3.input.KeyInput;
 import com.jme3.input.controls.ActionListener;
 import com.jme3.input.controls.KeyTrigger;
 import com.jme3.math.*;
 import com.jme3.renderer.Limits;
 import com.jme3.scene.*;
-import com.jme3.scene.control.Control;
 import com.jme3.scene.debug.custom.ArmatureDebugAppState;
 import com.jme3.scene.plugins.gltf.GltfModelKey;
 import jme3test.model.anim.EraseTimer;
 
+import java.io.File;
 import java.util.*;
 
 public class TestGltfLoading extends SimpleApplication {
 
-    final private Node autoRotate = new Node("autoRotate");
-    final private List<Spatial> assets = new ArrayList<>();
+    private final Node autoRotate = new Node("autoRotate");
+    private final List<Spatial> assets = new ArrayList<>();
     private Node probeNode;
     private float time = 0;
     private int assetIndex = 0;
     private boolean useAutoRotate = false;
     private final static String indentString = "\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t";
-    final private int duration = 1;
+    private final int duration = 1;
     private boolean playAnim = true;
+    private ChaseCameraAppState chaseCam;
+
+    private final Queue<String> anims = new LinkedList<>();
+    private AnimComposer composer;
 
     public static void main(String[] args) {
         TestGltfLoading app = new TestGltfLoading();
         app.start();
     }
 
-    /*
-    WARNING this test case can't work without the assets, and considering their size, they are not pushed into the repo
-    you can find them here :
-    https://github.com/KhronosGroup/glTF-Sample-Models/tree/master/2.0
-    https://sketchfab.com/features/gltf
-    You have to copy them in Model/gltf folder in the jme3-testdata project.
+    /**
+     * WARNING This test case will try to load models from $HOME/glTF-Sample-Models, if the models is not
+     * found there, it will automatically try to load it from the repository
+     * https://github.com/KhronosGroup/glTF-Sample-Models .
+     * 
+     * Depending on the your connection speed and github rate limiting, this can be quite slow.
      */
     @Override
     public void simpleInitApp() {
@@ -80,12 +84,14 @@ public class TestGltfLoading extends SimpleApplication {
         getStateManager().attach(armatureDebugappState);
         setTimer(new EraseTimer());
 
-        String folder = System.getProperty("user.home");
-        assetManager.registerLocator(folder, FileLocator.class);
-        assetManager.registerLocator("https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Assets/refs/heads/main/", UrlLocator.class);
+        String folder = System.getProperty("user.home") + "/glTF-Sample-Models";
+        if (new File(folder).exists()) {
+            assetManager.registerLocator(folder, FileLocator.class);
+        }
+        assetManager.registerLocator(
+                "https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Assets/refs/heads/main/",
+                UrlLocator.class);
 
-        // cam.setLocation(new Vector3f(4.0339394f, 2.645184f, 6.4627485f));
-        // cam.setRotation(new Quaternion(-0.013950467f, 0.98604023f, -0.119502485f, -0.11510504f));
         cam.setFrustumPerspective(45f, (float) cam.getWidth() / cam.getHeight(), 0.1f, 100f);
         renderer.setDefaultAnisotropicFilter(Math.min(renderer.getLimits().get(Limits.TextureAnisotropy), 8));
         setPauseOnLostFocus(false);
@@ -98,81 +104,60 @@ public class TestGltfLoading extends SimpleApplication {
         probeNode = (Node) assetManager.loadModel("Scenes/defaultProbe.j3o");
         autoRotate.attachChild(probeNode);
 
-//        DirectionalLight dl = new DirectionalLight();
-//        dl.setDirection(new Vector3f(-1f, -1.0f, -1f).normalizeLocal());
-//        dl.setColor(new ColorRGBA(1f, 1f, 1f, 1.0f));
-//        rootNode.addLight(dl);
-
-//        DirectionalLight dl2 = new DirectionalLight();
-//        dl2.setDirection(new Vector3f(1f, 1.0f, 1f).normalizeLocal());
-//        dl2.setColor(new ColorRGBA(0.5f, 0.5f, 0.5f, 1.0f));
-//        rootNode.addLight(dl2);
-
-//        PointLight pl = new PointLight(new Vector3f(5.0f, 5.0f, 5.0f), ColorRGBA.White, 30);
-//        rootNode.addLight(pl);
-//        PointLight pl1 = new PointLight(new Vector3f(-5.0f, -5.0f, -5.0f), ColorRGBA.White.mult(0.5f), 50);
-//        rootNode.addLight(pl1);
-
-        //loadModel("Models/gltf/polly/project_polly.gltf", new Vector3f(0, 0, 0), 0.5f);
-        //loadModel("Models/gltf/zophrac/scene.gltf", new Vector3f(0, 0, 0), 0.01f);
-    //    loadModel("Models/gltf/scifigirl/scene.gltf", new Vector3f(0, -1, 0), 0.1f);
-        //loadModel("Models/gltf/man/scene.gltf", new Vector3f(0, -1, 0), 0.1f);
-       //loadModel("Models/gltf/torus/scene.gltf", new Vector3f(0, -1, 0), 0.1f);
-        //loadModel("Models/gltf/morph/scene.gltf", new Vector3f(0, 0, 0), 0.2f);
-//        loadModel("Models/gltf/AnimatedMorphCube/glTF/AnimatedMorphCube.gltf", new Vector3f(0, 0, 0), 1f);
-//        loadModel("Models/gltf/SimpleMorph/glTF/SimpleMorph.gltf", new Vector3f(0, 0, 0), 0.1f);
-        //loadModel("Models/gltf/nier/scene.gltf", new Vector3f(0, -1.5f, 0), 0.01f);
-        //loadModel("Models/gltf/izzy/scene.gltf", new Vector3f(0, -1, 0), 0.01f);
-        //loadModel("Models/gltf/darth/scene.gltf", new Vector3f(0, -1, 0), 0.01f);
-        //loadModel("Models/gltf/mech/scene.gltf", new Vector3f(0, -1, 0), 0.01f);
-        //loadModel("Models/gltf/elephant/scene.gltf", new Vector3f(0, -1, 0), 0.01f);
-        //loadModel("Models/gltf/buffalo/scene.gltf", new Vector3f(0, -1, 0), 0.1f);
-        //loadModel("Models/gltf/war/scene.gltf", new Vector3f(0, -1, 0), 0.1f);
-        //loadModel("Models/gltf/ganjaarl/scene.gltf", new Vector3f(0, -1, 0), 0.01f);
-        //loadModel("Models/gltf/hero/scene.gltf", new Vector3f(0, -1, 0), 0.1f);
-        //loadModel("Models/gltf/mercy/scene.gltf", new Vector3f(0, -1, 0), 0.01f);
-        //loadModel("Models/gltf/crab/scene.gltf", Vector3f.ZERO, 1);
-        //loadModel("Models/gltf/manta/scene.gltf", Vector3f.ZERO, 0.2f);
-        //loadModel("Models/gltf/bone/scene.gltf", Vector3f.ZERO, 0.1f);
-//        loadModel("Models/gltf/box/box.gltf", Vector3f.ZERO, 1);
-        loadModel("Models/gltf/duck/Duck.gltf", new Vector3f(0, 1, 0), 1);
-//        loadModel("Models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf", Vector3f.ZERO, 1);
-//        loadModel("Models/gltf/hornet/scene.gltf", new Vector3f(0, -0.5f, 0), 0.4f);
-////        loadModel("Models/gltf/adamHead/adamHead.gltf", Vector3f.ZERO, 0.6f);
-        //loadModel("Models/gltf/busterDrone/busterDrone.gltf", new Vector3f(0, 0f, 0), 0.8f);
-//        loadModel("Models/gltf/AnimatedCube/glTF/AnimatedCube.gltf", Vector3f.ZERO, 0.5f);
-//        loadModel("Models/gltf/BoxAnimated/glTF/BoxAnimated.gltf", new Vector3f(0, 0f, 0), 0.8f);
-//        loadModel("Models/gltf/RiggedSimple/glTF/RiggedSimple.gltf", new Vector3f(0, -0.3f, 0), 0.2f);
-//        loadModel("Models/gltf/RiggedFigure/glTF/RiggedFigure.gltf", new Vector3f(0, -1f, 0), 1f);
-//        loadModel("Models/gltf/CesiumMan/glTF/CesiumMan.gltf", new Vector3f(0, -1, 0), 1f);
-//        loadModel("Models/gltf/BrainStem/glTF/BrainStem.gltf", new Vector3f(0, -1, 0), 1f);
-        //loadModel("Models/gltf/Jaime/Jaime.gltf", new Vector3f(0, -1, 0), 1f);
-        // loadModel("Models/gltf/GiantWorm/GiantWorm.gltf", new Vector3f(0, -1, 0), 1f);
-        //loadModel("Models/gltf/RiggedFigure/WalkingLady.gltf", new Vector3f(0, -0.f, 0), 1f);
-        //loadModel("Models/gltf/Monster/Monster.gltf", Vector3f.ZERO, 0.03f);
-
-//        loadModel("Models/gltf/Corset/glTF/Corset.gltf", new Vector3f(0, -1, 0), 20f);
-//        loadModel("Models/gltf/BoxInterleaved/glTF/BoxInterleaved.gltf", new Vector3f(0, 0, 0), 1f);
-
-        // From url locator
-
-        // loadModel("Models/AnimatedColorsCube/glTF/AnimatedColorsCube.gltf", new Vector3f(0, 0f, 0), 0.1f);
-        // loadModel("Models/AntiqueCamera/glTF/AntiqueCamera.gltf", new Vector3f(0, 0, 0), 0.1f);
-        // loadModel("Models/AnimatedMorphCube/glTF/AnimatedMorphCube.gltf", new Vector3f(0, 0, 0), 0.1f);
-        // loadModel("Models/AnimatedMorphCube/glTF-Binary/AnimatedMorphCube.glb", new Vector3f(0, 0, 0), 0.1f);
+        chaseCam = new ChaseCameraAppState();
+        getStateManager().attach(chaseCam);
 
-        probeNode.attachChild(assets.get(0));
+        // loadModelSample("Duck", "gltf");
+        // loadModelSample("Duck", "glb");
+        // loadModelSample("ABeautifulGame", "gltf");
+        // loadModelSample("Avocado", "glb");
+        // loadModelSample("Avocado", "gltf");
+        // loadModelSample("CesiumMilkTruck", "glb");
+        // loadModelSample("VirtualCity", "glb");
+        // loadModelSample("BrainStem", "glb");
+        // loadModelSample("Lantern", "glb");
+        // loadModelSample("RiggedFigure", "glb");
+        // loadModelSample("SciFiHelmet", "gltf");
+        loadModelSample("DamagedHelmet", "gltf");
+        // loadModelSample("AnimatedCube", "gltf");
+        // loadModelSample("AntiqueCamera", "glb");
+        // loadModelSample("AnimatedMorphCube", "glb");
+
+        // DRACO SAMPLES
+
+        // loadModelSample("Avocado", "draco");
+        // loadModelSample("BarramundiFish", "draco");
+        // loadModelSample("BoomBox", "draco");
+        // loadModelSample("CesiumMilkTruck", "draco");
+        // loadModelSample("Corset", "draco");
+        // loadModelSample("Lantern", "draco");
+        // loadModelSample("MorphPrimitivesTest", "draco");
+        // loadModelSample("WaterBottle", "draco");
+        
+        // Draco skinning samples
+        //loadModelSample("BrainStem", "draco");
+        //loadModelSample("BrainStem", "glb");
 
-        ChaseCameraAppState chaseCam = new ChaseCameraAppState();
-        chaseCam.setTarget(probeNode);
-        getStateManager().attach(chaseCam);
-        chaseCam.setInvertHorizontalAxis(true);
-        chaseCam.setInvertVerticalAxis(true);
-        chaseCam.setZoomSpeed(0.5f);
-        chaseCam.setMinVerticalRotation(-FastMath.HALF_PI);
-        chaseCam.setRotationSpeed(3);
-        chaseCam.setDefaultDistance(3);
-        chaseCam.setDefaultVerticalRotation(0.3f);
+        //loadModelSample("CesiumMan", "draco");
+        //loadModelSample("CesiumMan", "glb");
+
+        // loadModelSample("RiggedFigure", "draco");
+        //loadModelSample("RiggedFigure", "glb");
+
+        //loadModelSample("RiggedSimple", "draco");
+        //loadModelSample("RiggedSimple", "glb");
+
+        // Test for normalized texture coordinates in draco
+        //loadModelFromPath("Models/gltf/unitSquare11x11_unsignedShortTexCoords-draco.glb");
+
+        // Uses EXT_texture_webp - not supported yet 
+        //loadModelSample("SunglassesKhronos", "draco");
+        
+        // Probably invalid model
+        // See https://github.com/KhronosGroup/glTF-Sample-Assets/issues/264
+        // loadModelSample("VirtualCity", "draco");
+        
+        probeNode.attachChild(assets.get(0));
 
         inputManager.addMapping("autorotate", new KeyTrigger(KeyInput.KEY_SPACE));
         inputManager.addListener(new ActionListener() {
@@ -213,36 +198,66 @@ public class TestGltfLoading extends SimpleApplication {
 
         dumpScene(rootNode, 0);
 
-      //  stateManager.attach(new DetailedProfilerState());
+        // stateManager.attach(new DetailedProfilerState());
     }
 
-    private <T extends Control> T findControl(Spatial s, Class<T> controlClass) {
-        T ctrl = s.getControl(controlClass);
-        if (ctrl != null) {
-            return ctrl;
+    private void loadModelSample(String name, String type) {
+        String path = "Models/" + name;
+        String ext = "gltf";
+        switch (type) {
+            case "draco":
+                path += "/glTF-Draco/";
+                ext = "gltf";
+                break;
+            case "glb":
+                path += "/glTF-Binary/";
+                ext = "glb";
+                break;
+            default:
+                path += "/glTF/";
+                ext = "gltf";
+                break;
         }
-        if (s instanceof Node) {
-            Node n = (Node) s;
-            for (Spatial spatial : n.getChildren()) {
-                ctrl = findControl(spatial, controlClass);
-                if (ctrl != null) {
-                    return ctrl;
-                }
-            }
+        path += name + "." + ext;
+        loadModelFromPath(path);
+    }
+    
+    private void loadModelFromPath(String path) {
+
+        Spatial s = loadModel(path, new Vector3f(0, 0, 0), 1f);
+
+        BoundingBox bbox = (BoundingBox) s.getWorldBound();
+        float maxExtent = Math.max(bbox.getXExtent(), Math.max(bbox.getYExtent(), bbox.getZExtent()));
+        if (maxExtent < 10f) {
+            s.scale(10f / maxExtent);
+            maxExtent = 10f;
         }
-        return null;
+        float distance = 50f;
+
+        chaseCam.setTarget(s);
+        chaseCam.setInvertHorizontalAxis(true);
+        chaseCam.setInvertVerticalAxis(true);
+        chaseCam.setZoomSpeed(1.5f);
+        chaseCam.setMinVerticalRotation(-FastMath.HALF_PI);
+        chaseCam.setRotationSpeed(3);
+        chaseCam.setDefaultDistance(distance);
+        chaseCam.setMaxDistance(distance * 10);
+        chaseCam.setDefaultVerticalRotation(0.3f);
+
     }
 
-    private void loadModel(String path, Vector3f offset, float scale) {
-        loadModel(path, offset, new Vector3f(scale, scale, scale));
+    private Spatial loadModel(String path, Vector3f offset, float scale) {
+        return loadModel(path, offset, new Vector3f(scale, scale, scale));
     }
-    private void loadModel(String path, Vector3f offset, Vector3f scale) {
+
+    private Spatial loadModel(String path, Vector3f offset, Vector3f scale) {
+        System.out.println("Loading model: " + path);
         GltfModelKey k = new GltfModelKey(path);
-        //k.setKeepSkeletonPose(true);
-        long t  = System.currentTimeMillis();        
+        // k.setKeepSkeletonPose(true);
+        long t = System.currentTimeMillis();
         Spatial s = assetManager.loadModel(k);
         System.out.println("Load time : " + (System.currentTimeMillis() - t) + " ms");
-        
+
         s.scale(scale.x, scale.y, scale.z);
         s.move(offset);
         assets.add(s);
@@ -250,29 +265,9 @@ public class TestGltfLoading extends SimpleApplication {
             playFirstAnim(s);
         }
 
-        SkinningControl ctrl = findControl(s, SkinningControl.class);
-
-        //  ctrl.getSpatial().removeControl(ctrl);
-        if (ctrl == null) {
-            return;
-        }
-        //System.err.println(ctrl.getArmature().toString());
-        //ctrl.setHardwareSkinningPreferred(false);
-        //     getStateManager().getState(ArmatureDebugAppState.class).addArmatureFrom(ctrl);
-//        AnimControl aCtrl = findControl(s, AnimControl.class);
-//        //ctrl.getSpatial().removeControl(ctrl);
-//        if (aCtrl == null) {
-//            return;
-//        }
-//        if (aCtrl.getArmature() != null) {
-//            getStateManager().getState(SkeletonDebugAppState.class).addSkeleton(aCtrl.getArmature(), aCtrl.getSpatial(), true);
-//        }
-
+        return s;
     }
 
-    final private Queue<String> anims = new LinkedList<>();
-    private AnimComposer composer;
-
     private void playFirstAnim(Spatial s) {
 
         AnimComposer control = s.getControl(AnimComposer.class);
@@ -317,25 +312,25 @@ public class TestGltfLoading extends SimpleApplication {
             return;
         }
         time += tpf;
-      //  autoRotate.rotate(0, tpf * 0.5f, 0);
+        // autoRotate.rotate(0, tpf * 0.5f, 0);
         if (time > duration) {
             // morphIndex++;
-            //  setMorphTarget(morphIndex);
+            // setMorphTarget(morphIndex);
             assets.get(assetIndex).removeFromParent();
             assetIndex = (assetIndex + 1) % assets.size();
-//            if (assetIndex == 0) {
-//                duration = 10;
-//            }
+            // if (assetIndex == 0) {
+            // duration = 10;
+            // }
             probeNode.attachChild(assets.get(assetIndex));
             time = 0;
         }
     }
 
     private void dumpScene(Spatial s, int indent) {
-        System.err.println(indentString.substring(0, indent) + s.getName() + " (" + s.getClass().getSimpleName() + ") / " +
-                s.getLocalTransform().getTranslation().toString() + ", " +
-                s.getLocalTransform().getRotation().toString() + ", " +
-                s.getLocalTransform().getScale().toString());
+        System.err.println(indentString.substring(0, indent) + s.getName() + " ("
+                + s.getClass().getSimpleName() + ") / " + s.getLocalTransform().getTranslation().toString()
+                + ", " + s.getLocalTransform().getRotation().toString() + ", "
+                + s.getLocalTransform().getScale().toString());
         if (s instanceof Node) {
             Node n = (Node) s;
             for (Spatial spatial : n.getChildren()) {

+ 1 - 1
jme3-plugins/build.gradle

@@ -11,7 +11,7 @@ sourceSets {
 
 dependencies {
     api project(':jme3-core')
-    
+    implementation "org.jmonkeyengine:drako:1.4.5-jme"
     implementation project(':jme3-plugins-json')
     implementation project(':jme3-plugins-json-gson')
     testRuntimeOnly project(':jme3-desktop')

+ 179 - 0
jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/BufferQuantization.java

@@ -0,0 +1,179 @@
+/*
+ * Copyright (c) 2009-2026 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.
+ */
+package com.jme3.scene.plugins.gltf;
+
+import java.nio.ByteBuffer;
+import java.nio.FloatBuffer;
+import java.nio.ShortBuffer;
+
+import com.jme3.util.BufferUtils;
+
+/**
+ * A package-private class to perform dequantization of buffers.
+ * 
+ * This handled buffers that contain (unsigned) byte or short values and that are "normalized", i.e. supposed
+ * to be interpreted as float values.
+ * 
+ * (NOTE: Some of these methods are taken from a non-published state of JglTF, but published by the original
+ * author, as part of JMonkeyEngine)
+ */
+class BufferQuantization {
+
+    /**
+     * Dequantize the given buffer into a float buffer, treating each element of the input as a signed byte.
+     * 
+     * @param byteBuffer
+     *            The input buffer
+     * @return The result
+     */
+    static FloatBuffer dequantizeByteBuffer(ByteBuffer byteBuffer) {
+        FloatBuffer floatBuffer = BufferUtils.createFloatBuffer(byteBuffer.capacity());
+        for (int i = 0; i < byteBuffer.capacity(); i++) {
+            byte c = byteBuffer.get(i);
+            float f = dequantizeByte(c);
+            floatBuffer.put(i, f);
+        }
+        return floatBuffer;
+    }
+
+    /**
+     * Dequantize the given buffer into a float buffer, treating each element of the input as an unsigned
+     * byte.
+     * 
+     * @param byteBuffer
+     *            The input buffer
+     * @return The result
+     */
+    static FloatBuffer dequantizeUnsignedByteBuffer(ByteBuffer byteBuffer) {
+        FloatBuffer floatBuffer = BufferUtils.createFloatBuffer(byteBuffer.capacity());
+        for (int i = 0; i < byteBuffer.capacity(); i++) {
+            byte c = byteBuffer.get(i);
+            float f = dequantizeUnsignedByte(c);
+            floatBuffer.put(i, f);
+        }
+        return floatBuffer;
+    }
+
+    /**
+     * Dequantize the given buffer into a float buffer, treating each element of the input as a signed short.
+     * 
+     * @param shortBuffer
+     *            The input buffer
+     * @return The result
+     */
+    static FloatBuffer dequantizeShortBuffer(ShortBuffer shortBuffer) {
+        FloatBuffer floatBuffer = BufferUtils.createFloatBuffer(shortBuffer.capacity());
+        for (int i = 0; i < shortBuffer.capacity(); i++) {
+            short c = shortBuffer.get(i);
+            float f = dequantizeShort(c);
+            floatBuffer.put(i, f);
+        }
+        return floatBuffer;
+    }
+
+    /**
+     * Dequantize the given buffer into a float buffer, treating each element of the input as an unsigned
+     * short.
+     * 
+     * @param shortBuffer
+     *            The input buffer
+     * @return The result
+     */
+    static FloatBuffer dequantizeUnsignedShortBuffer(ShortBuffer shortBuffer) {
+        FloatBuffer floatBuffer = BufferUtils.createFloatBuffer(shortBuffer.capacity());
+        for (int i = 0; i < shortBuffer.capacity(); i++) {
+            short c = shortBuffer.get(i);
+            float f = dequantizeUnsignedShort(c);
+            floatBuffer.put(i, f);
+        }
+        return floatBuffer;
+    }
+
+    /**
+     * Dequantize the given signed byte into a floating point value
+     * 
+     * @param c
+     *            The input
+     * @return The result
+     */
+    private static float dequantizeByte(byte c) {
+        float f = Math.max(c / 127.0f, -1.0f);
+        return f;
+    }
+
+    /**
+     * Dequantize the given unsigned byte into a floating point value
+     * 
+     * @param c
+     *            The input
+     * @return The result
+     */
+    private static float dequantizeUnsignedByte(byte c) {
+        int i = Byte.toUnsignedInt(c);
+        float f = i / 255.0f;
+        return f;
+    }
+
+    /**
+     * Dequantize the given signed short into a floating point value
+     * 
+     * @param c
+     *            The input
+     * @return The result
+     */
+    private static float dequantizeShort(short c) {
+        float f = Math.max(c / 32767.0f, -1.0f);
+        return f;
+    }
+
+    /**
+     * 
+     * Dequantize the given unsigned byte into a floating point value
+     * 
+     * @param c
+     *            The input
+     * @return The result
+     */
+    private static float dequantizeUnsignedShort(short c) {
+        int i = Short.toUnsignedInt(c);
+        float f = i / 65535.0f;
+        return f;
+    }
+
+    /**
+     * Private constructor to prevent instantiation
+     */
+    private BufferQuantization() {
+        // Private constructor to prevent instantiation
+    }
+
+}

+ 1 - 0
jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/CustomContentManager.java

@@ -63,6 +63,7 @@ public class CustomContentManager {
         defaultExtensionLoaders.put("KHR_materials_unlit", UnlitExtensionLoader.class);
         defaultExtensionLoaders.put("KHR_texture_transform", TextureTransformExtensionLoader.class);
         defaultExtensionLoaders.put("KHR_materials_emissive_strength", PBREmissiveStrengthExtensionLoader.class);
+        defaultExtensionLoaders.put("KHR_draco_mesh_compression", DracoMeshCompressionExtensionLoader.class);
 
     }
     

+ 658 - 0
jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/DracoMeshCompressionExtensionLoader.java

@@ -0,0 +1,658 @@
+/*
+ * Copyright (c) 2009-2026 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.
+ */
+package com.jme3.scene.plugins.gltf;
+
+import static com.jme3.scene.plugins.gltf.GltfUtils.assertNotNull;
+import static com.jme3.scene.plugins.gltf.GltfUtils.getAsBoolean;
+import static com.jme3.scene.plugins.gltf.GltfUtils.getAsInt;
+import static com.jme3.scene.plugins.gltf.GltfUtils.getAsInteger;
+import static com.jme3.scene.plugins.gltf.GltfUtils.getAsString;
+import static com.jme3.scene.plugins.gltf.GltfUtils.getNumberOfComponents;
+import static com.jme3.scene.plugins.gltf.GltfUtils.getVertexBufferFormat;
+import static com.jme3.scene.plugins.gltf.GltfUtils.getVertexBufferType;
+
+import java.io.IOException;
+import java.nio.Buffer;
+import java.nio.ByteBuffer;
+import java.nio.FloatBuffer;
+import java.nio.ShortBuffer;
+import java.util.Map.Entry;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.jme3.asset.AssetLoadException;
+import com.jme3.plugins.json.JsonElement;
+import com.jme3.plugins.json.JsonObject;
+import com.jme3.scene.Mesh;
+import com.jme3.scene.VertexBuffer;
+import com.jme3.scene.VertexBuffer.Type;
+import com.jme3.scene.plugins.gltf.GltfLoader.SkinBuffers;
+import com.jme3.util.BufferUtils;
+import com.openize.drako.Draco;
+import com.openize.drako.DracoMesh;
+import com.openize.drako.DrakoException;
+import com.openize.drako.PointAttribute;
+
+/**
+ * A class for handling the <code>KHR_draco_mesh_compression</code> extension when loading a glTF asset.
+ * 
+ * It is registered as the handler for this extension in the glTF {@link CustomContentManager}. In the
+ * {@link GltfLoader#readMeshPrimitives(int)} method, the custom content handler will be called for each mesh
+ * primitive, and handle the <code>KHR_draco_mesh_compression</code> of the primitive by calling the
+ * {@link #handleExtension} method of this class.
+ * 
+ * TODO_DRACO Strictly speaking, the loader should ignore any attribute definitions when the draco extension
+ * is present. Right now, this is called after the mesh was already filled with the vertex buffers that have
+ * been created by the default loading process. See the check for "bufferViewIndex == null" in
+ * VertexBufferPopulator.
+ */
+public class DracoMeshCompressionExtensionLoader implements ExtensionLoader {
+
+    /**
+     * The logger used in this class
+     */
+    private final static Logger logger = Logger
+            .getLogger(DracoMeshCompressionExtensionLoader.class.getName());
+
+    /**
+     * The default log level for Draco extension handling
+     */
+    private static final Level level = Level.FINER;
+
+    /**
+     * <ul>
+     * <li>The <code>parentName</code> will be <code>"primitive"</code></li>
+     * <li>The <code>parent</code>" will be the JSON element that represents the mesh primitive from the glTF
+     * JSON.</li>
+     * <li>The <code>extension</code> will be the JSON element that represents the
+     * <code>KHR_draco_mesh_compression</code> extension object.</li>
+     * </ul>
+     * 
+     * {@inheritDoc}
+     */
+    @Override
+    public Object handleExtension(GltfLoader loader, String parentName, JsonElement parent,
+            JsonElement extension, Object input) throws IOException {
+
+        logger.log(level, "Decoding draco data");
+
+        JsonObject meshPrimitiveObject = parent.getAsJsonObject();
+        JsonObject extensionObject = extension.getAsJsonObject();
+        Mesh mesh = (Mesh) input;
+
+        DracoMesh dracoMesh = readDracoMesh(loader, extension);
+
+        // Fetch the indices and fill the index vertex buffer of
+        // the mesh with the data from draco
+        logger.log(level, "Decoding draco indices");
+        int indices[] = dracoMesh.getIndices().toArray();
+        int indicesAccessorIndex = getAsInt(meshPrimitiveObject, "mesh primitive", "indices");
+        JsonObject indicesAccessor = loader.getAccessor(indicesAccessorIndex);
+        int indicesComponentType = getAsInt(indicesAccessor, "accessor " + indicesAccessorIndex,
+                "componentType");
+
+        VertexBuffer indicesVertexBuffer = mesh.getBuffer(VertexBuffer.Type.Index);
+        Buffer indicesVertexBufferData = createIndicesVertexBufferData(indicesComponentType, indices);
+        indicesVertexBuffer.setupData(VertexBuffer.Usage.Dynamic, 3,
+                getVertexBufferFormat(indicesComponentType), indicesVertexBufferData);
+
+        // Iterate over all attributes that are found in the
+        // "attributes" dictionary of the extension object.
+        // According to the specification, these must be
+        // a subset of the attributes of the mesh primitive.
+        JsonObject attributes = extensionObject.get("attributes").getAsJsonObject();
+        JsonObject parentAttributes = meshPrimitiveObject.get("attributes").getAsJsonObject();
+        for (Entry<String, JsonElement> entry : attributes.entrySet()) {
+            String attributeName = entry.getKey();
+            logger.log(level, "Decoding draco attribute " + attributeName);
+
+            // The extension object stores the attribute ID, which
+            // is an identifier for the attribute in the decoded
+            // draco data. It is NOT an accessor index!
+            int attributeId = entry.getValue().getAsInt();
+            PointAttribute pointAttribute = getAttribute(dracoMesh, attributeName, attributeId);
+
+            logger.log(level, "attribute " + attributeName);
+            logger.log(level, "attributeId " + attributeId);
+            logger.log(level, "pointAttribute " + pointAttribute);
+
+            // The mesh primitive stores the accessor index for
+            // each attribute
+            int attributeAccessorIndex = getAsInt(parentAttributes, attributeName + " attribute",
+                    attributeName);
+            JsonObject accessor = loader.getAccessor(attributeAccessorIndex);
+
+            logger.log(level, "attributeAccessorIndex " + attributeAccessorIndex);
+            logger.log(level, "accessor " + accessor);
+
+            // Replace the buffer in the mesh with a buffer that was
+            // created from the data that was fetched from the
+            // decoded draco PointAttribute
+            Type bufferType = getVertexBufferType(attributeName);
+
+            if (attributeName.startsWith("JOINTS")) {
+                readJoints(loader, attributeName, accessor, pointAttribute);
+            } else if (attributeName.startsWith("WEIGHTS")) {
+                readWeights(loader, attributeName, accessor, pointAttribute);
+            } else {
+                VertexBuffer attributeVertexBuffer = createAttributeVertexBuffer(attributeName, accessor,
+                        pointAttribute);
+                mesh.clearBuffer(bufferType);
+                mesh.setBuffer(attributeVertexBuffer);
+            }
+        }
+        loader.postProcessSkinning(mesh);
+
+        logger.log(level, "Decoding draco data DONE");
+        return mesh;
+    }
+
+    /**
+     * Read the data from a <code>JOINTS_n</code> attribute that was decoded from Draco.
+     * 
+     * This will read the data from the attribute, and store it as the {@link SkinBuffers#joints} array in the
+     * skin buffers information that is obtained via {@link GltfLoader#getSkinBuffers(String)} for the given
+     * attribute name.
+     * 
+     * @param loader
+     *            The {@link GltfLoader}
+     * @param attributeName
+     *            The attribute name
+     * @param accessor
+     *            The accessor for the attribute
+     * @param pointAttribute
+     *            The actual Draco-decoded attribute data
+     * @throws AssetLoadException
+     *             If the component type of the given accessor is not <code>GL_UNSIGNED_BYTE</code> or
+     *             <code>GL_UNSIGNED_SHORT</code>
+     */
+    private static void readJoints(GltfLoader loader, String attributeName, JsonObject accessor,
+            PointAttribute pointAttribute) {
+        int count = getAsInt(accessor, "accessor", "count");
+        int componentType = getAsInt(accessor, "accessor", "componentType");
+        int componentCount = getAccessorComponentCount(accessor);
+
+        if (componentType == GltfConstants.GL_UNSIGNED_BYTE) {
+            ByteBuffer attributeData = readByteDracoAttribute(pointAttribute, count, componentCount);
+            short array[] = new short[attributeData.capacity()];
+            for (int i = 0; i < array.length; i++) {
+                array[i] = attributeData.get(i);
+            }
+            SkinBuffers buffs = loader.getSkinBuffers(attributeName);
+            buffs.componentSize = 2;
+            buffs.joints = array;
+        } else if (componentType == GltfConstants.GL_UNSIGNED_SHORT) {
+            ShortBuffer attributeData = readShortDracoAttribute(pointAttribute, count, componentCount);
+            short array[] = new short[attributeData.capacity()];
+            attributeData.slice().get(array);
+            SkinBuffers buffs = loader.getSkinBuffers(attributeName);
+            buffs.componentSize = 2;
+            buffs.joints = array;
+        } else {
+            throw new AssetLoadException("The accessor for attribute " + attributeName
+                    + " must have a component type of " + GltfConstants.GL_UNSIGNED_BYTE + " or "
+                    + GltfConstants.GL_UNSIGNED_SHORT + ", but has " + componentType);
+        }
+    }
+
+    /**
+     * Read the data from a <code>WEIGHTS_n</code> attribute that was decoded from Draco.
+     * 
+     * This will read the data from the attribute, and store it as the {@link SkinBuffers#weights} array in
+     * the skin buffers information that is obtained via {@link GltfLoader#getSkinBuffers(String)} for the
+     * given attribute name.
+     * 
+     * @param loader
+     *            The {@link GltfLoader}
+     * @param attributeName
+     *            The attribute name
+     * @param accessor
+     *            The accessor for the attribute
+     * @param pointAttribute
+     *            The actual Draco-decoded attribute data
+     * @throws AssetLoadException
+     *             If the component type of the given accessor is not <code>GL_UNSIGNED_BYTE</code> or
+     *             <code>GL_UNSIGNED_SHORT</code> or <code>GL_FLOAT</code>, or if it is
+     *             <code>GL_UNSIGNED_BYTE</code> or <code>GL_UNSIGNED_SHORT</code> and the accessor is not
+     *             normalized.
+     */
+    private static void readWeights(GltfLoader loader, String attributeName, JsonObject accessor,
+            PointAttribute pointAttribute) {
+        int count = getAsInt(accessor, "accessor", "count");
+        int componentType = getAsInt(accessor, "accessor", "componentType");
+        int componentCount = getAccessorComponentCount(accessor);
+
+        if (componentType == GltfConstants.GL_UNSIGNED_BYTE) {
+            boolean normalized = Boolean.TRUE.equals(getAsBoolean(accessor, "normalized"));
+            if (!normalized) {
+                throw new AssetLoadException("The accessor for attribute " + attributeName
+                        + " has a component type of " + componentType + " but is not normalized");
+            }
+            ByteBuffer attributeData = readByteDracoAttribute(pointAttribute, count, componentCount);
+            FloatBuffer resultAttributeData = BufferQuantization.dequantizeByteBuffer(attributeData);
+            float array[] = new float[attributeData.capacity()];
+            resultAttributeData.slice().get(array);
+            SkinBuffers buffs = loader.getSkinBuffers(attributeName);
+            buffs.weights = array;
+        } else if (componentType == GltfConstants.GL_UNSIGNED_SHORT) {
+            boolean normalized = Boolean.TRUE.equals(getAsBoolean(accessor, "normalized"));
+            if (!normalized) {
+                throw new AssetLoadException("The accessor for attribute " + attributeName
+                        + " has a component type of " + componentType + " but is not normalized");
+            }
+            ShortBuffer attributeData = readShortDracoAttribute(pointAttribute, count, componentCount);
+            FloatBuffer resultAttributeData = BufferQuantization.dequantizeShortBuffer(attributeData);
+            float array[] = new float[attributeData.capacity()];
+            resultAttributeData.slice().get(array);
+            SkinBuffers buffs = loader.getSkinBuffers(attributeName);
+            buffs.weights = array;
+        } else if (componentType == GltfConstants.GL_FLOAT) {
+            FloatBuffer attributeData = readFloatDracoAttribute(pointAttribute, count, componentCount);
+            float array[] = new float[attributeData.capacity()];
+            attributeData.slice().get(array);
+            SkinBuffers buffs = loader.getSkinBuffers(attributeName);
+            buffs.weights = array;
+        } else {
+            throw new AssetLoadException(
+                    "The accessor for attribute " + attributeName + " must have a component type of "
+                            + GltfConstants.GL_UNSIGNED_BYTE + ", " + GltfConstants.GL_UNSIGNED_SHORT
+                            + ", or " + GltfConstants.GL_FLOAT + ", but has " + componentType);
+        }
+    }
+
+    /**
+     * Read the draco data from the given extension, using <code>openize-drako-java</code>.
+     * 
+     * @param loader
+     *            The glTF loader
+     * @param extension
+     *            The draco extension object that was found in a mesh primitive
+     * @return The Draco mesh
+     * @throws IOException
+     *             If attempting to load the underlying buffer causes an IO error
+     */
+    private static DracoMesh readDracoMesh(GltfLoader loader, JsonElement extension) throws IOException {
+        logger.log(level, "Decoding draco mesh");
+
+        JsonObject jsonObject = extension.getAsJsonObject();
+        int bufferViewIndex = getAsInt(jsonObject, "Draco extension object", "bufferView");
+
+        ByteBuffer bufferViewData = obtainBufferViewData(loader, bufferViewIndex);
+
+        byte bufferViewDataArray[] = new byte[bufferViewData.remaining()];
+        bufferViewData.slice().get(bufferViewDataArray);
+        DracoMesh dracoMesh = null;
+        try {
+            dracoMesh = (DracoMesh) Draco.decode(bufferViewDataArray);
+        } catch (DrakoException e) {
+            throw new AssetLoadException("Could not decode Draco mesh from buffer view " + bufferViewIndex,
+                    e);
+        }
+
+        logger.log(level, "Decoding draco mesh DONE");
+        return dracoMesh;
+    }
+
+    /**
+     * Create the indices vertex buffer data, based on the given Draco-decoded indices
+     * 
+     * @param accessorIndex
+     *            The accessor index of the vertices
+     * @param indices
+     *            The Draco-decoded indices
+     * @return The indices vertex buffer data
+     * @throws AssetLoadException
+     *             If the given component type is not <code>GL_UNSIGNED_BYTE</code>,
+     *             <code>GL_UNSIGNED_SHORT</code>, or <code>GL_UNSIGNED_INT</code>
+     */
+    private Buffer createIndicesVertexBufferData(int componentType, int indices[]) {
+        if (componentType == GltfConstants.GL_UNSIGNED_BYTE) {
+            return BufferUtils.createByteBuffer(indices);
+        }
+        if (componentType == GltfConstants.GL_UNSIGNED_SHORT) {
+            return BufferUtils.createShortBuffer(indices);
+        }
+        if (componentType == GltfConstants.GL_UNSIGNED_INT) {
+            return BufferUtils.createIntBuffer(indices);
+        }
+        throw new AssetLoadException("The indices accessor must have a component type of "
+                + GltfConstants.GL_UNSIGNED_BYTE + ", " + GltfConstants.GL_UNSIGNED_SHORT + ", or "
+                + GltfConstants.GL_UNSIGNED_INT + ", but has " + componentType);
+    }
+
+    // TODO_DRACO Could go into GltfUtils
+    /**
+     * Determines the number of components per element for the given accessor, based on its <code>type</code>
+     * 
+     * @param accessor
+     *            The accessor
+     * @return The number of components
+     * @throws AssetLoadException
+     *             If the accessor does not have a valid <code>type</code> property
+     */
+    private static int getAccessorComponentCount(JsonObject accessor) {
+        String type = getAsString(accessor, "type");
+        assertNotNull(type, "No type attribute defined for accessor");
+        return getNumberOfComponents(type);
+    }
+
+    // TODO_DRACO Could fit into GltfLoader
+    /**
+     * Obtain the data for the specified buffer view of the given loader.
+     * 
+     * This will return a slice of the data of the underlying buffer. Callers may not modify the returned
+     * data.
+     * 
+     * @param loader
+     *            The loader
+     * @param bufferViewIndex
+     *            The buffer view index
+     * @return The buffer view data
+     * @throws IOException
+     *             If attempting to load the underlying buffer causes an IO error
+     * @throws AssetLoadException
+     *             If the specified index is not valid, or the buffer view did not define a valid buffer index
+     *             or byte length
+     */
+    private static ByteBuffer obtainBufferViewData(GltfLoader loader, int bufferViewIndex)
+            throws IOException {
+        JsonObject bufferView = loader.getBufferView(bufferViewIndex);
+        int bufferIndex = getAsInt(bufferView, "bufferView", "buffer");
+        assertNotNull(bufferIndex, "No buffer defined for bufferView " + bufferViewIndex);
+
+        int byteOffset = getAsInteger(bufferView, "byteOffset", 0);
+        int byteLength = getAsInt(bufferView, "bufferView " + bufferViewIndex, "byteLength");
+
+        ByteBuffer bufferData = loader.readData(bufferIndex);
+        ByteBuffer bufferViewData = bufferData.slice();
+        bufferViewData.limit(byteOffset + byteLength);
+        bufferViewData.position(byteOffset);
+        return bufferViewData;
+    }
+
+    /**
+     * Obtains the point attribute with the given ID from the given draco mesh.
+     * 
+     * @param dracoMesh
+     *            The draco mesh
+     * @param gltfAttribute
+     *            The glTF attribute name, like <code>"POSITION"</code> (only used for error messages)
+     * @param id
+     *            The unique ID of the attribute, i.e. the value that was stored as the
+     *            <code>"POSITION": id</code> in the draco extension JSON object.
+     * @return The point attribute
+     * @throws AssetLoadException
+     *             If the attribute with the given ID cannot be found
+     */
+    private static PointAttribute getAttribute(DracoMesh dracoMesh, String gltfAttribute, int id) {
+        for (int i = 0; i < dracoMesh.getNumAttributes(); i++) {
+            PointAttribute attribute = dracoMesh.attribute(i);
+            if (attribute.getUniqueId() == id) {
+                return attribute;
+            }
+        }
+        throw new AssetLoadException("Could not obtain attribute " + gltfAttribute + " with unique ID " + id
+                + " from decoded Draco mesh");
+    }
+
+    /**
+     * Creates a vertex buffer for the specified attribute, according to the structure that is described by
+     * the given accessor JSON object, using the data that is obtained from the given Draco-decoded point
+     * attribute
+     * 
+     * @param attributeName
+     *            The attribute name
+     * @param accessor
+     *            The accessor JSON object
+     * @param pointAttribute
+     *            The Draco-decoded point attribute
+     * @return The vertex buffer
+     * @throws AssetLoadException
+     *             If the given accessor does not have a component type that is valid for a vertex attribute
+     */
+    private static VertexBuffer createAttributeVertexBuffer(String attributeName, JsonObject accessor,
+            PointAttribute pointAttribute) {
+        int count = getAsInt(accessor, "accessor", "count");
+        int componentType = getAsInt(accessor, "accessor", "componentType");
+        int componentCount = getAccessorComponentCount(accessor);
+        Type bufferType = getVertexBufferType(attributeName);
+
+        if (componentType == GltfConstants.GL_BYTE || componentType == GltfConstants.GL_UNSIGNED_BYTE) {
+            ByteBuffer attributeData = readByteDracoAttribute(pointAttribute, count, componentCount);
+            VertexBuffer attributeVertexBuffer = createByteAttributeVertexBuffer(accessor, bufferType,
+                    attributeData);
+            return attributeVertexBuffer;
+        }
+        if (componentType == GltfConstants.GL_SHORT || componentType == GltfConstants.GL_UNSIGNED_SHORT) {
+            ShortBuffer attributeData = readShortDracoAttribute(pointAttribute, count, componentCount);
+            VertexBuffer attributeVertexBuffer = createShortAttributeVertexBuffer(accessor, bufferType,
+                    attributeData);
+            return attributeVertexBuffer;
+        }
+        if (componentType == GltfConstants.GL_FLOAT) {
+            FloatBuffer attributeData = readFloatDracoAttribute(pointAttribute, count, componentCount);
+            VertexBuffer attributeVertexBuffer = createFloatAttributeVertexBuffer(accessor, bufferType,
+                    attributeData);
+            return attributeVertexBuffer;
+        }
+        throw new AssetLoadException(
+                "The accessor for attribute " + attributeName + " must have a component type of "
+                        + GltfConstants.GL_BYTE + ", " + GltfConstants.GL_UNSIGNED_BYTE + ", "
+                        + GltfConstants.GL_SHORT + ", " + GltfConstants.GL_UNSIGNED_SHORT + ", " + "or "
+                        + GltfConstants.GL_FLOAT + ", but has " + componentType);
+    }
+
+    /**
+     * Read the data from the given point attribute, as <code>byte</code> values
+     * 
+     * @param pointAttribute
+     *            The Draco-decoded point attribute
+     * @param count
+     *            The count, obtained from the accessor for this attribute
+     * @param componentCount
+     *            The component count (number of components per element), obtained from the accessor type
+     * @return The resulting data, as a byte buffer
+     */
+    private static ByteBuffer readByteDracoAttribute(PointAttribute pointAttribute, int count,
+            int componentCount) {
+
+        byte p[] = new byte[componentCount];
+        ByteBuffer attributeData = BufferUtils.createByteBuffer(count * componentCount);
+
+        for (int i = 0; i < count; i++) {
+            int j = pointAttribute.mappedIndex(i);
+            pointAttribute.getValue(j, p);
+            for (int c = 0; c < componentCount; c++) {
+                attributeData.put(i * componentCount + c, p[c]);
+            }
+        }
+        return attributeData;
+    }
+
+    /**
+     * Read the data from the given point attribute, as <code>short</code> values
+     * 
+     * @param pointAttribute
+     *            The Draco-decoded point attribute
+     * @param count
+     *            The count, obtained from the accessor for this attribute
+     * @param componentCount
+     *            The component count (number of components per element), obtained from the accessor type
+     * @return The resulting data, as a short buffer
+     */
+    private static ShortBuffer readShortDracoAttribute(PointAttribute pointAttribute, int count,
+            int componentCount) {
+
+        short p[] = new short[componentCount];
+        ShortBuffer attributeData = BufferUtils.createShortBuffer(count * componentCount);
+
+        for (int i = 0; i < count; i++) {
+            int j = pointAttribute.mappedIndex(i);
+            pointAttribute.getValue(j, p);
+            for (int c = 0; c < componentCount; c++) {
+                attributeData.put(i * componentCount + c, p[c]);
+            }
+        }
+        return attributeData;
+    }
+
+    /**
+     * Read the data from the given point attribute, as <code>float</code> values
+     * 
+     * @param pointAttribute
+     *            The Draco-decoded point attribute
+     * @param count
+     *            The count, obtained from the accessor for this attribute
+     * @param componentCount
+     *            The component count (number of components per element), obtained from the accessor type
+     * @return The resulting data, as a float buffer
+     */
+    private static FloatBuffer readFloatDracoAttribute(PointAttribute pointAttribute, int count,
+            int componentCount) {
+        float p[] = new float[componentCount];
+        FloatBuffer attributeData = BufferUtils.createFloatBuffer(count * componentCount);
+        for (int i = 0; i < count; i++) {
+            int j = pointAttribute.mappedIndex(i);
+            pointAttribute.getValue(j, p);
+            int offset0 = i * componentCount;
+            for (int c = 0; c < componentCount; c++) {
+                attributeData.put(offset0 + c, p[c]);
+            }
+        }
+        return attributeData;
+    }
+
+    /**
+     * Create the vertex buffer for the given <code>byte</code> attribute data.
+     * 
+     * If the accessor is <code>normalized</code>, then this will dequantize the given data into a
+     * <code>Float</code> vertex buffer.
+     * 
+     * @param accessor
+     *            The accessor that describes the component type and type
+     * 
+     * @param bufferType
+     *            The buffer type
+     * @param attributeData
+     *            The attribute data
+     * @return The vertex buffer
+     */
+    private static VertexBuffer createByteAttributeVertexBuffer(JsonObject accessor,
+            VertexBuffer.Type bufferType, ByteBuffer attributeData) {
+        int componentType = getAsInt(accessor, "accessor", "componentType");
+        VertexBuffer vb = new VertexBuffer(bufferType);
+        int numComponents = getAccessorComponentCount(accessor);
+
+        VertexBuffer.Format originalFormat = getVertexBufferFormat(componentType);
+        VertexBuffer.Format resultFormat = originalFormat;
+        Buffer resultAttributeData = attributeData;
+
+        boolean normalized = Boolean.TRUE.equals(getAsBoolean(accessor, "normalized"));
+        if (normalized) {
+            logger.log(level,
+                    "Draco-decoded data is " + originalFormat + ", but normalized - dequantizing to Float");
+            resultFormat = VertexBuffer.Format.Float;
+            if (originalFormat == VertexBuffer.Format.Byte) {
+                resultAttributeData = BufferQuantization.dequantizeByteBuffer(attributeData);
+            } else {
+                resultAttributeData = BufferQuantization.dequantizeUnsignedByteBuffer(attributeData);
+            }
+        }
+
+        vb.setupData(VertexBuffer.Usage.Dynamic, numComponents, resultFormat, resultAttributeData);
+        return vb;
+    }
+
+    /**
+     * Create the vertex buffer for the given <code>short</code> attribute data
+     * 
+     * If the accessor is <code>normalized</code>, then this will dequantize the given data into a
+     * <code>Float</code> vertex buffer.
+     * 
+     * @param accessor
+     *            The accessor that describes the component type and type
+     * 
+     * @param bufferType
+     *            The buffer type
+     * @param attributeData
+     *            The attribute data
+     * @return The vertex buffer
+     */
+    private static VertexBuffer createShortAttributeVertexBuffer(JsonObject accessor,
+            VertexBuffer.Type bufferType, ShortBuffer attributeData) {
+        int componentType = getAsInt(accessor, "accessor", "componentType");
+        VertexBuffer vb = new VertexBuffer(bufferType);
+        int numComponents = getAccessorComponentCount(accessor);
+
+        VertexBuffer.Format originalFormat = getVertexBufferFormat(componentType);
+        VertexBuffer.Format resultFormat = originalFormat;
+        Buffer resultAttributeData = attributeData;
+
+        boolean normalized = Boolean.TRUE.equals(getAsBoolean(accessor, "normalized"));
+        if (normalized) {
+            logger.log(level,
+                    "Draco-decoded data is " + originalFormat + ", but normalized - dequantizing to Float");
+            resultFormat = VertexBuffer.Format.Float;
+            if (originalFormat == VertexBuffer.Format.Short) {
+                resultAttributeData = BufferQuantization.dequantizeShortBuffer(attributeData);
+            } else {
+                resultAttributeData = BufferQuantization.dequantizeUnsignedShortBuffer(attributeData);
+            }
+        }
+
+        vb.setupData(VertexBuffer.Usage.Dynamic, numComponents, resultFormat, resultAttributeData);
+        return vb;
+    }
+
+    /**
+     * Create the vertex buffer for the given <code>float</code> attribute data
+     * 
+     * @param accessor
+     *            The accessor that describes the component type and type
+     * 
+     * @param bufferType
+     *            The buffer type
+     * @param attributeData
+     *            The attribute data
+     * @return The vertex buffer
+     */
+    private static VertexBuffer createFloatAttributeVertexBuffer(JsonObject accessor,
+            VertexBuffer.Type bufferType, FloatBuffer attributeData) {
+        int componentType = getAsInt(accessor, "accessor", "componentType");
+        VertexBuffer vb = new VertexBuffer(bufferType);
+        VertexBuffer.Format format = getVertexBufferFormat(componentType);
+        int numComponents = getAccessorComponentCount(accessor);
+        vb.setupData(VertexBuffer.Usage.Dynamic, numComponents, format, attributeData);
+        return vb;
+    }
+
+}

+ 75 - 0
jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfConstants.java

@@ -0,0 +1,75 @@
+/*
+ * Copyright (c) 2009-2026 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.
+ */
+package com.jme3.scene.plugins.gltf;
+
+/**
+ * A package-private class summarizing GL constants that are used in the context of glTF loading.
+ */
+class GltfConstants {
+
+    /**
+     * GL_BYTE, 5120, 0x1400
+     */
+    static final int GL_BYTE = 0x1400;
+
+    /**
+     * GL_UNSIGNED_BYTE, 5121, 0x1401
+     */
+    static final int GL_UNSIGNED_BYTE = 0x1401;
+
+    /**
+     * GL_SHORT, 5122, 0x1402
+     */
+    static final int GL_SHORT = 0x1402;
+
+    /**
+     * GL_UNSIGNED_SHORT, 5123, 0x1403
+     */
+    static final int GL_UNSIGNED_SHORT = 0x1403;
+
+    /**
+     * GL_UNSIGNED_INT, 5125, 0x1405
+     */
+    static final int GL_UNSIGNED_INT = 0x1405;
+
+    /**
+     * GL_FLOAT, 5126, 0x1406
+     */
+    static final int GL_FLOAT = 0x1406;
+
+    /**
+     * Private constructor to prevent instantiation
+     */
+    private GltfConstants() {
+        // Private constructor to prevent instantiation
+    }
+}

+ 272 - 138
jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfLoader.java

@@ -31,39 +31,88 @@
  */
 package com.jme3.scene.plugins.gltf;
 
+import static com.jme3.scene.plugins.gltf.GltfUtils.assertNotNull;
+import static com.jme3.scene.plugins.gltf.GltfUtils.findCommonAncestor;
+import static com.jme3.scene.plugins.gltf.GltfUtils.getAdapterForMaterial;
+import static com.jme3.scene.plugins.gltf.GltfUtils.getAsBoolean;
+import static com.jme3.scene.plugins.gltf.GltfUtils.getAsColor;
+import static com.jme3.scene.plugins.gltf.GltfUtils.getAsFloat;
+import static com.jme3.scene.plugins.gltf.GltfUtils.getAsInteger;
+import static com.jme3.scene.plugins.gltf.GltfUtils.getAsString;
+import static com.jme3.scene.plugins.gltf.GltfUtils.getIndex;
+import static com.jme3.scene.plugins.gltf.GltfUtils.getMagFilter;
+import static com.jme3.scene.plugins.gltf.GltfUtils.getMeshMode;
+import static com.jme3.scene.plugins.gltf.GltfUtils.getMinFilter;
+import static com.jme3.scene.plugins.gltf.GltfUtils.getNumberOfComponents;
+import static com.jme3.scene.plugins.gltf.GltfUtils.getVertexBufferFormat;
+import static com.jme3.scene.plugins.gltf.GltfUtils.getVertexBufferType;
+import static com.jme3.scene.plugins.gltf.GltfUtils.getWrapMode;
+import static com.jme3.scene.plugins.gltf.GltfUtils.padBuffer;
+import static com.jme3.scene.plugins.gltf.GltfUtils.parse;
+import static com.jme3.scene.plugins.gltf.GltfUtils.populateBuffer;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.nio.Buffer;
+import java.nio.ByteBuffer;
+import java.nio.FloatBuffer;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.jme3.anim.AnimClip;
+import com.jme3.anim.AnimComposer;
+import com.jme3.anim.AnimTrack;
+import com.jme3.anim.Armature;
+import com.jme3.anim.Joint;
+import com.jme3.anim.MorphControl;
+import com.jme3.anim.MorphTrack;
+import com.jme3.anim.SkinningControl;
+import com.jme3.anim.TransformTrack;
+import com.jme3.asset.AssetInfo;
+import com.jme3.asset.AssetLoadException;
+import com.jme3.asset.AssetLoader;
+import com.jme3.asset.TextureKey;
+import com.jme3.material.Material;
+import com.jme3.material.RenderState;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.FastMath;
+import com.jme3.math.Matrix4f;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Transform;
+import com.jme3.math.Vector3f;
 import com.jme3.plugins.json.JsonArray;
+import com.jme3.plugins.json.JsonElement;
 import com.jme3.plugins.json.JsonObject;
 import com.jme3.plugins.json.JsonPrimitive;
-import com.jme3.plugins.json.JsonElement;
-import com.jme3.anim.*;
-import com.jme3.asset.*;
-import com.jme3.material.Material;
-import com.jme3.material.RenderState;
-import com.jme3.math.*;
 import com.jme3.renderer.Camera;
 import com.jme3.renderer.queue.RenderQueue;
-import com.jme3.scene.*;
+import com.jme3.scene.CameraNode;
+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.Usage;
 import com.jme3.scene.control.CameraControl;
 import com.jme3.scene.mesh.MorphTarget;
-import static com.jme3.scene.plugins.gltf.GltfUtils.*;
 import com.jme3.texture.Texture;
 import com.jme3.texture.Texture2D;
 import com.jme3.util.BufferInputStream;
 import com.jme3.util.BufferUtils;
 import com.jme3.util.IntMap;
 import com.jme3.util.mikktspace.MikktspaceTangentGenerator;
-import java.io.*;
-import java.net.URLDecoder;
-import java.nio.Buffer;
-import java.nio.ByteBuffer;
-import java.nio.FloatBuffer;
-import java.util.*;
-import java.util.logging.Level;
-import java.util.logging.Logger;
 
 /**
- * GLTF 2.0 loader
- * Created by Nehon on 07/08/2017.
+ * GLTF 2.0 loader Created by Nehon on 07/08/2017.
  */
 public class GltfLoader implements AssetLoader {
 
@@ -99,7 +148,7 @@ public class GltfLoader implements AssetLoader {
     private boolean useNormalsFlag = false;
 
     Map<SkinData, List<Spatial>> skinnedSpatials = new HashMap<>();
-    IntMap<SkinBuffers> skinBuffers = new IntMap<>();
+    private final IntMap<SkinBuffers> skinBuffers = new IntMap<>();
 
     public GltfLoader() {
         defaultMaterialAdapters.put("pbrMetallicRoughness", new PBRMetalRoughMaterialAdapter());
@@ -130,7 +179,8 @@ public class GltfLoader implements AssetLoader {
             String version = getAsString(asset, "version");
             String minVersion = getAsString(asset, "minVersion");
             if (!isSupported(version, minVersion)) {
-                logger.log(Level.SEVERE, "Gltf Loader doesn''t support this gltf version: {0}{1}", new Object[]{version, minVersion != null ? ("/" + minVersion) : ""});
+                logger.log(Level.SEVERE, "Gltf Loader doesn''t support this gltf version: {0}{1}",
+                        new Object[] { version, minVersion != null ? ("/" + minVersion) : "" });
             }
 
             scenes = docRoot.getAsJsonArray("scenes");
@@ -280,9 +330,9 @@ public class GltfLoader implements AssetLoader {
             }
 
             node.setName(readMeshName(meshIndex));
-            
+
             spatial = new Node();
-            ((Node)spatial).attachChild(node);
+            ((Node) spatial).attachChild(node);
 
         } else {
             // no mesh, we have a node. Can be a camera node or a regular node.
@@ -361,24 +411,17 @@ public class GltfLoader implements AssetLoader {
         // no matrix transforms: no transforms or transforms given as translation/rotation/scale
         JsonArray translation = nodeData.getAsJsonArray("translation");
         if (translation != null) {
-            transform.setTranslation(
-                    translation.get(0).getAsFloat(),
-                    translation.get(1).getAsFloat(),
+            transform.setTranslation(translation.get(0).getAsFloat(), translation.get(1).getAsFloat(),
                     translation.get(2).getAsFloat());
         }
         JsonArray rotation = nodeData.getAsJsonArray("rotation");
         if (rotation != null) {
-            transform.setRotation(new Quaternion(
-                    rotation.get(0).getAsFloat(),
-                    rotation.get(1).getAsFloat(),
-                    rotation.get(2).getAsFloat(),
-                    rotation.get(3).getAsFloat()));
+            transform.setRotation(new Quaternion(rotation.get(0).getAsFloat(), rotation.get(1).getAsFloat(),
+                    rotation.get(2).getAsFloat(), rotation.get(3).getAsFloat()));
         }
         JsonArray scale = nodeData.getAsJsonArray("scale");
         if (scale != null) {
-            transform.setScale(
-                    scale.get(0).getAsFloat(),
-                    scale.get(1).getAsFloat(),
+            transform.setScale(scale.get(0).getAsFloat(), scale.get(1).getAsFloat(),
                     scale.get(2).getAsFloat());
         }
 
@@ -387,7 +430,7 @@ public class GltfLoader implements AssetLoader {
 
     public Geometry[] readMeshPrimitives(int meshIndex) throws IOException {
         Geometry[] geomArray = (Geometry[]) fetchFromCache("meshes", meshIndex, Object.class);
-        if (geomArray == null) {                
+        if (geomArray == null) {
             JsonObject meshData = meshes.get(meshIndex).getAsJsonObject();
             JsonArray primitives = meshData.getAsJsonArray("primitives");
             assertNotNull(primitives, "Can't find any primitives in mesh " + meshIndex);
@@ -403,7 +446,8 @@ public class GltfLoader implements AssetLoader {
                 mesh.setMode(getMeshMode(mode));
                 Integer indices = getAsInteger(meshObject, "indices");
                 if (indices != null) {
-                    mesh.setBuffer(readAccessorData(indices, new VertexBufferPopulator(VertexBuffer.Type.Index)));
+                    mesh.setBuffer(
+                            readAccessorData(indices, new VertexBufferPopulator(VertexBuffer.Type.Index)));
                 }
                 JsonObject attributes = meshObject.getAsJsonObject("attributes");
                 assertNotNull(attributes, "No attributes defined for mesh " + mesh);
@@ -415,17 +459,19 @@ public class GltfLoader implements AssetLoader {
                 for (Map.Entry<String, JsonElement> entry : attributes.entrySet()) {
                     // special case for joints and weights buffer.
                     // If there are more than 4 bones per vertex, there might be several of them
-                    // we need to read them all and to keep only the 4 that have the most weight on the vertex.
+                    // we need to read them all and to keep only the 4 that have the most weight on the
+                    // vertex.
                     String bufferType = entry.getKey();
                     if (bufferType.startsWith("JOINTS")) {
                         SkinBuffers buffs = getSkinBuffers(bufferType);
-                        SkinBuffers buffer
-                                = readAccessorData(entry.getValue().getAsInt(), new JointArrayPopulator());
+                        SkinBuffers buffer = readAccessorData(entry.getValue().getAsInt(),
+                                new JointArrayPopulator());
                         buffs.joints = buffer.joints;
                         buffs.componentSize = buffer.componentSize;
                     } else if (bufferType.startsWith("WEIGHTS")) {
                         SkinBuffers buffs = getSkinBuffers(bufferType);
-                        buffs.weights = readAccessorData(entry.getValue().getAsInt(), new FloatArrayPopulator());
+                        buffs.weights = readAccessorData(entry.getValue().getAsInt(),
+                                new FloatArrayPopulator());
                     } else {
                         VertexBuffer vb = readAccessorData(entry.getValue().getAsInt(),
                                 new VertexBufferPopulator(getVertexBufferType(bufferType)));
@@ -438,26 +484,13 @@ public class GltfLoader implements AssetLoader {
                         useVertexColors = true;
                     }
                 }
-                handleSkinningBuffers(mesh, skinBuffers);
-
-                if (mesh.getBuffer(VertexBuffer.Type.BoneIndex) != null) {
-                    // the mesh has some skinning, let's create needed buffers for HW skinning
-                    // creating empty buffers for HW skinning
-                    // the buffers will be set up if ever used.
-                    VertexBuffer weightsHW = new VertexBuffer(VertexBuffer.Type.HWBoneWeight);
-                    VertexBuffer indicesHW = new VertexBuffer(VertexBuffer.Type.HWBoneIndex);
-                    // setting usage to cpuOnly so that the buffer is not sent empty to the GPU
-                    indicesHW.setUsage(VertexBuffer.Usage.CpuOnly);
-                    weightsHW.setUsage(VertexBuffer.Usage.CpuOnly);
-                    mesh.setBuffer(weightsHW);
-                    mesh.setBuffer(indicesHW);
-                    mesh.generateBindPose();
-                }
+                postProcessSkinning(mesh);
 
                 // Read morph target names
                 LinkedList<String> targetNames = new LinkedList<>();
                 if (meshData.has("extras") && meshData.getAsJsonObject("extras").has("targetNames")) {
-                    JsonArray targetNamesJson = meshData.getAsJsonObject("extras").getAsJsonArray("targetNames");
+                    JsonArray targetNamesJson = meshData.getAsJsonObject("extras")
+                            .getAsJsonArray("targetNames");
                     for (JsonElement target : targetNamesJson) {
                         targetNames.add(target.getAsString());
                     }
@@ -494,13 +527,15 @@ public class GltfLoader implements AssetLoader {
                 } else {
                     useNormalsFlag = false;
                     geom.setMaterial(readMaterial(materialIndex));
-                    if (geom.getMaterial().getAdditionalRenderState().getBlendMode()
-                            == RenderState.BlendMode.Alpha) {
-                        // Alpha blending is enabled for this material. Let's place the geom in the transparent bucket.
+                    if (geom.getMaterial().getAdditionalRenderState()
+                            .getBlendMode() == RenderState.BlendMode.Alpha) {
+                        // Alpha blending is enabled for this material. Let's place the geom in the
+                        // transparent bucket.
                         geom.setQueueBucket(RenderQueue.Bucket.Transparent);
                     }
                     if (useNormalsFlag && mesh.getBuffer(VertexBuffer.Type.Tangent) == null) {
-                        // No tangent buffer, but there is a normal map, we have to generate them using MikktSpace
+                        // No tangent buffer, but there is a normal map, we have to generate them using
+                        // MikktSpace
                         MikktspaceTangentGenerator.generate(geom);
                     }
                 }
@@ -510,7 +545,7 @@ public class GltfLoader implements AssetLoader {
                 }
 
                 geom.setName(name + "_" + index);
-                
+
                 geom.updateModelBound();
                 geomArray[index] = geom;
                 index++;
@@ -528,7 +563,7 @@ public class GltfLoader implements AssetLoader {
         return geoms;
     }
 
-    private SkinBuffers getSkinBuffers(String bufferType) {
+    SkinBuffers getSkinBuffers(String bufferType) {
         int bufIndex = getIndex(bufferType);
         SkinBuffers buffs = skinBuffers.get(bufIndex);
         if (buffs == null) {
@@ -538,6 +573,50 @@ public class GltfLoader implements AssetLoader {
         return buffs;
     }
 
+    /**
+     * Perform the post-processing on the given mesh that is required for the skinning to work properly, after
+     * the mesh information was read from the glTF input.
+     * 
+     * Many details are unspecified here. But this is what had originally been done after reading the skinning
+     * information for a mesh primitive. Now it is also called after any Draco-encoded data was decoded (which
+     * may have updated the skinning data with the Draco-decoded data).
+     * 
+     * @param mesh
+     *            The mesh
+     */
+    void postProcessSkinning(Mesh mesh) {
+        GltfUtils.handleSkinningBuffers(mesh, skinBuffers);
+        if (mesh.getBuffer(VertexBuffer.Type.BoneIndex) != null) {
+            // the mesh has some skinning, let's create needed buffers for HW skinning
+            // creating empty buffers for HW skinning
+            // the buffers will be set up if ever used.
+            // setting usage to cpuOnly so that the buffer is not sent empty to the GPU
+            ensureBuffer(mesh, VertexBuffer.Type.HWBoneIndex, VertexBuffer.Usage.CpuOnly);
+            ensureBuffer(mesh, VertexBuffer.Type.HWBoneWeight, VertexBuffer.Usage.CpuOnly);
+            mesh.generateBindPose();
+        }
+    }
+
+    /**
+     * Ensure that the given mesh has a buffer with the given type.
+     * 
+     * If it does not yet have a buffer with the given type, then
+     * a new buffer with the given type and usage is created and
+     * assigned to the mesh.
+     * 
+     * @param mesh The mesh
+     * @param type The type
+     * @param usage The usage
+     */
+    private static void ensureBuffer(Mesh mesh, VertexBuffer.Type type, Usage usage) {
+        if (mesh.getBuffer(type) != null) {
+            return;
+        }
+        VertexBuffer vb = new VertexBuffer(type);
+        vb.setUsage(usage);
+        mesh.setBuffer(vb);
+    }
+
     private <R> R readAccessorData(int accessorIndex, Populator<R> populator) throws IOException {
         assertNotNull(accessors, "No accessor attribute in the gltf file");
 
@@ -579,7 +658,7 @@ public class GltfLoader implements AssetLoader {
         ByteBuffer data = readData(bufferIndex);
         data = customContentManager.readExtensionAndExtras("bufferView", bufferView, data);
 
-        if(!(data instanceof ByteBuffer)){
+        if (!(data instanceof ByteBuffer)) {
             throw new IOException("Buffer data is not a NIO Buffer");
         }
 
@@ -596,8 +675,8 @@ public class GltfLoader implements AssetLoader {
         return store;
     }
 
-    public Buffer viewBuffer(Integer bufferViewIndex, int byteOffset, int count,  
-            int numComponents, VertexBuffer.Format originalFormat,  VertexBuffer.Format targetFormat) throws IOException {
+    public Buffer viewBuffer(Integer bufferViewIndex, int byteOffset, int count, int numComponents,
+            VertexBuffer.Format originalFormat, VertexBuffer.Format targetFormat) throws IOException {
         JsonObject bufferView = bufferViews.get(bufferViewIndex).getAsJsonObject();
         Integer bufferIndex = getAsInteger(bufferView, "buffer");
         assertNotNull(bufferIndex, "No buffer defined for bufferView " + bufferViewIndex);
@@ -609,21 +688,74 @@ public class GltfLoader implements AssetLoader {
         ByteBuffer data = readData(bufferIndex);
         data = customContentManager.readExtensionAndExtras("bufferView", bufferView, data);
 
-        if(!(data instanceof ByteBuffer)){
+        if (!(data instanceof ByteBuffer)) {
             throw new IOException("Buffer data is not a NIO Buffer");
         }
- 
 
         if (count == -1) {
             count = byteLength;
         }
 
-        return GltfUtils.getBufferView(data, byteOffset + bvByteOffset, count, byteStride, numComponents, originalFormat, targetFormat );
+        return GltfUtils.getBufferView(data, byteOffset + bvByteOffset, count, byteStride, numComponents,
+                originalFormat, targetFormat);
+
+    }
+
+    /**
+     * Returns the JSON object that represents the buffer view with the specified index in the glTF JSON.
+     * 
+     * @param index
+     *            The buffer view index
+     * @return The buffer view as a JSON object
+     * @throws AssetLoadException
+     *             If the index is negative or not smaller than the number of buffer views in the glTF JSON
+     */
+    JsonObject getBufferView(int index) {
+        assertNotNull(bufferViews, "No buffer views when trying to access buffer view with index " + index);
+        validateIndex("bufferView", index, bufferViews.size());
+        JsonObject bufferView = bufferViews.get(index).getAsJsonObject();
+        return bufferView;
+    }
+
+    /**
+     * Returns the JSON object that represents the accessor with the specified index in the glTF JSON.
+     * 
+     * @param index
+     *            The accessor index
+     * @return The accessor as a JSON object
+     * @throws AssetLoadException
+     *             If the index is negative or not smaller than the number of accessors in the glTF JSON
+     */
+    JsonObject getAccessor(int index) {
+        assertNotNull(accessors, "No accessors when trying to access accessor with index " + index);
+        validateIndex("accessor", index, accessors.size());
+        JsonObject accessor = accessors.get(index).getAsJsonObject();
+        return accessor;
+    }
 
+    /**
+     * Ensure that the given index is valid for the specified size, and throw an exception of this is not the
+     * case.
+     * 
+     * @param name
+     *            The name of the index
+     * @param index
+     *            The index
+     * @param size
+     *            The size
+     * @throws AssetLoadException
+     *             If the index is negative or not smaller than the size
+     */
+    private static void validateIndex(String name, int index, int size) {
+        if (index < 0 || index >= size) {
+            throw new AssetLoadException(
+                    "The " + name + " index must be positive and smaller than " + size + ", but is " + index);
+        }
     }
 
     public ByteBuffer readData(int bufferIndex) throws IOException {
         assertNotNull(buffers, "No buffer defined");
+        validateIndex("buffer", bufferIndex, buffers.size());
 
         JsonObject buffer = buffers.get(bufferIndex).getAsJsonObject();
         String uri = getAsString(buffer, "uri");
@@ -646,7 +778,8 @@ public class GltfLoader implements AssetLoader {
         if (uri != null) {
             if (uri.startsWith("data:")) {
                 // base 64 embed data
-                data = BufferUtils.createByteBuffer(Base64.getDecoder().decode(uri.substring(uri.indexOf(",") + 1)));
+                data = BufferUtils
+                        .createByteBuffer(Base64.getDecoder().decode(uri.substring(uri.indexOf(",") + 1)));
             } else {
                 // external file let's load it
                 String decoded = decodeUri(uri);
@@ -656,11 +789,11 @@ public class GltfLoader implements AssetLoader {
                 }
 
                 BinDataKey key = new BinDataKey(info.getKey().getFolder() + decoded);
-                try(InputStream input = (InputStream) info.getManager().loadAsset(key)){
+                try (InputStream input = (InputStream) info.getManager().loadAsset(key)) {
                     data = BufferUtils.createByteBuffer(bufferLength);
                     GltfUtils.readToByteBuffer(input, data, bufferLength);
                 }
-               
+
             }
         } else {
             // no URI, this should not happen in a gltf file, only in glb files.
@@ -704,7 +837,8 @@ public class GltfLoader implements AssetLoader {
             adapter.setParam("metallicRoughnessTexture",
                     readTexture(pbrMat.getAsJsonObject("metallicRoughnessTexture")));
             JsonObject metallicRoughnessJson = pbrMat.getAsJsonObject("metallicRoughnessTexture");
-            metallicRoughnessIndex = metallicRoughnessJson != null ? getAsInteger(metallicRoughnessJson, "index") : null;            
+            metallicRoughnessIndex = metallicRoughnessJson != null ? getAsInteger(metallicRoughnessJson,
+                    "index") : null;
         }
 
         adapter.getMaterial().setName(getAsString(matData, "name"));
@@ -730,7 +864,7 @@ public class GltfLoader implements AssetLoader {
         Integer occlusionIndex = occlusionJson != null ? getAsInteger(occlusionJson, "index") : null;
         if (occlusionIndex != null && occlusionIndex.equals(metallicRoughnessIndex)) {
             adapter.getMaterial().setBoolean("AoPackedInMRMap", true);
-        } else {        
+        } else {
             adapter.setParam("occlusionTexture", readTexture(matData.getAsJsonObject("occlusionTexture")));
         }
 
@@ -804,7 +938,7 @@ public class GltfLoader implements AssetLoader {
         if (texture2d != null) {
             return texture2d;
         }
-        
+
         JsonObject textureData = textures.get(textureIndex).getAsJsonObject();
         Integer sourceIndex = getAsInteger(textureData, "source");
         Integer samplerIndex = getAsInteger(textureData, "sampler");
@@ -837,11 +971,12 @@ public class GltfLoader implements AssetLoader {
         if (uri == null) {
             assertNotNull(bufferView, "Image " + sourceIndex + " should either have an uri or a bufferView");
             assertNotNull(mimeType, "Image " + sourceIndex + " should have a mimeType");
-            ByteBuffer data = (ByteBuffer) viewBuffer(bufferView, 0, -1, 1, VertexBuffer.Format.Byte, VertexBuffer.Format.Byte);
+            ByteBuffer data = (ByteBuffer) viewBuffer(bufferView, 0, -1, 1, VertexBuffer.Format.Byte,
+                    VertexBuffer.Format.Byte);
 
             String extension = mimeType.split("/")[1];
             TextureKey key = new TextureKey("image" + sourceIndex + "." + extension, flip);
-            try(BufferedInputStream bis = new BufferedInputStream(new BufferInputStream(data))){
+            try (BufferedInputStream bis = new BufferedInputStream(new BufferInputStream(data))) {
                 result = (Texture2D) info.getManager().loadAssetFromStream(key, bis);
             }
         } else if (uri.startsWith("data:")) {
@@ -851,7 +986,7 @@ public class GltfLoader implements AssetLoader {
             String headerInfo = uriInfo[0].split(";")[0];
             String extension = headerInfo.split("/")[1];
             TextureKey key = new TextureKey("image" + sourceIndex + "." + extension, flip);
-            try(BufferedInputStream bis = new BufferedInputStream(new BufferInputStream(data))){
+            try (BufferedInputStream bis = new BufferedInputStream(new BufferInputStream(data))) {
                 result = (Texture2D) info.getManager().loadAssetFromStream(key, bis);
             }
         } else {
@@ -871,7 +1006,7 @@ public class GltfLoader implements AssetLoader {
         String name = getAsString(animation, "name");
         assertNotNull(channels, "No channels for animation " + name);
         assertNotNull(samplers, "No samplers for animation " + name);
-        
+
         // temp data storage of track data
         TrackData[] tracks = new TrackData[nodes.size()];
         boolean hasMorphTrack = false;
@@ -886,13 +1021,13 @@ public class GltfLoader implements AssetLoader {
                 continue;
             }
             assertNotNull(targetPath, "No target path for channel");
-//
-//            if (targetPath.equals("weights")) {
-//                // Morph animation, not implemented in JME, let's warn the user and skip the channel
-//                logger.log(Level.WARNING,
-//                    "Morph animation is not supported by JME yet, skipping animation track");
-//                continue;
-//            }
+            //
+            // if (targetPath.equals("weights")) {
+            // // Morph animation, not implemented in JME, let's warn the user and skip the channel
+            // logger.log(Level.WARNING,
+            // "Morph animation is not supported by JME yet, skipping animation track");
+            // continue;
+            // }
 
             TrackData trackData = tracks[targetNode];
             if (trackData == null) {
@@ -967,8 +1102,8 @@ public class GltfLoader implements AssetLoader {
                 spatials.add(s);
                 if (trackData.rotations != null || trackData.translations != null
                         || trackData.scales != null) {
-                    TransformTrack track = new TransformTrack(s, trackData.times,
-                            trackData.translations, trackData.rotations, trackData.scales);
+                    TransformTrack track = new TransformTrack(s, trackData.times, trackData.translations,
+                            trackData.rotations, trackData.scales);
                     aTracks.add(track);
                 }
                 if (trackData.weights != null) {
@@ -993,15 +1128,14 @@ public class GltfLoader implements AssetLoader {
                     // the track will be skipped.
                     if (skinIndex != jw.skinIndex) {
                         logger.log(Level.WARNING, "Animation " + animationIndex + " (" + name
-                                + ") applies to joints that are not from the same skin: skin "
-                                + skinIndex + ", joint " + jw.joint.getName()
-                                + " from skin " + jw.skinIndex);
+                                + ") applies to joints that are not from the same skin: skin " + skinIndex
+                                + ", joint " + jw.joint.getName() + " from skin " + jw.skinIndex);
                         continue;
                     }
                 }
 
-                TransformTrack track = new TransformTrack(jw.joint, trackData.times,
-                        trackData.translations, trackData.rotations, trackData.scales);
+                TransformTrack track = new TransformTrack(jw.joint, trackData.times, trackData.translations,
+                        trackData.rotations, trackData.scales);
                 aTracks.add(track);
             }
         }
@@ -1015,17 +1149,17 @@ public class GltfLoader implements AssetLoader {
             for (Joint joint : skin.joints) {
                 if (!usedJoints.contains(joint)) {
                     // create a track
-                    float[] times = new float[]{0};
+                    float[] times = new float[] { 0 };
 
-                    Vector3f[] translations = new Vector3f[]{joint.getLocalTranslation()};
-                    Quaternion[] rotations = new Quaternion[]{joint.getLocalRotation()};
-                    Vector3f[] scales = new Vector3f[]{joint.getLocalScale()};
+                    Vector3f[] translations = new Vector3f[] { joint.getLocalTranslation() };
+                    Quaternion[] rotations = new Quaternion[] { joint.getLocalRotation() };
+                    Vector3f[] scales = new Vector3f[] { joint.getLocalScale() };
                     TransformTrack track = new TransformTrack(joint, times, translations, rotations, scales);
                     aTracks.add(track);
                 }
             }
         }
-        
+
         anim.setTracks(aTracks.toArray(new AnimTrack[aTracks.size()]));
         anim = customContentManager.readExtensionAndExtras("animations", animation, anim);
 
@@ -1182,8 +1316,7 @@ public class GltfLoader implements AssetLoader {
     private void findChildren(int nodeIndex) throws IOException {
         JointWrapper jw = fetchFromCache("nodes", nodeIndex, JointWrapper.class);
         if (jw == null) {
-            logger.log(Level.WARNING,
-                    "No JointWrapper found for nodeIndex={0}.", nodeIndex);
+            logger.log(Level.WARNING, "No JointWrapper found for nodeIndex={0}.", nodeIndex);
             return;
         }
 
@@ -1227,12 +1360,12 @@ public class GltfLoader implements AssetLoader {
             if (spatials.size() >= 1) {
                 spatial = findCommonAncestor(spatials);
             }
-//            if (spatial != skinData.parent) {
-//                skinData.rootBoneTransformOffset = spatial.getWorldTransform().invert();
-//                if (skinData.parent != null) {
-//                    skinData.rootBoneTransformOffset.combineWithParent(skinData.parent.getWorldTransform());
-//                }
-//            }
+            // if (spatial != skinData.parent) {
+            // skinData.rootBoneTransformOffset = spatial.getWorldTransform().invert();
+            // if (skinData.parent != null) {
+            // skinData.rootBoneTransformOffset.combineWithParent(skinData.parent.getWorldTransform());
+            // }
+            // }
             if (skinData.animComposer != null && skinData.animComposer.getSpatial() == null) {
                 spatial.addControl(skinData.animComposer);
             }
@@ -1264,7 +1397,7 @@ public class GltfLoader implements AssetLoader {
         JsonObject meshData = meshes.get(meshIndex).getAsJsonObject();
         return getAsString(meshData, "name");
     }
-    
+
     private MorphTrack toMorphTrack(TrackData data, Spatial spatial) {
         Geometry g = (Geometry) spatial;
         int nbMorph = g.getMesh().getMorphTargets().length;
@@ -1351,17 +1484,18 @@ public class GltfLoader implements AssetLoader {
         boolean used = false;
     }
 
-    public static class SkinBuffers {
+    static class SkinBuffers {
         short[] joints;
         float[] weights;
         int componentSize;
 
-        public SkinBuffers(short[] joints, int componentSize) {
+        SkinBuffers(short[] joints, int componentSize) {
             this.joints = joints;
             this.componentSize = componentSize;
         }
 
-        public SkinBuffers() {}
+        SkinBuffers() {
+        }
     }
 
     private interface Populator<T> {
@@ -1380,7 +1514,9 @@ public class GltfLoader implements AssetLoader {
         public VertexBuffer populate(Integer bufferViewIndex, int componentType, String type, int count,
                 int byteOffset, boolean normalized) throws IOException {
             if (bufferType == null) {
-                logger.log(Level.WARNING, "could not assign data to any VertexBuffer type for buffer view {0}", bufferViewIndex);
+                logger.log(Level.WARNING,
+                        "could not assign data to any VertexBuffer type for buffer view {0}",
+                        bufferViewIndex);
                 return null;
             }
 
@@ -1402,7 +1538,8 @@ public class GltfLoader implements AssetLoader {
                 // no referenced buffer, specs says to pad the buffer with zeros.
                 padBuffer(buff, bufferSize);
             } else {
-                buff = (Buffer) viewBuffer(bufferViewIndex, byteOffset, count, numComponents, originalFormat, format);
+                buff = (Buffer) viewBuffer(bufferViewIndex, byteOffset, count, numComponents, originalFormat,
+                        format);
             }
 
             if (bufferType == VertexBuffer.Type.Index) {
@@ -1434,29 +1571,29 @@ public class GltfLoader implements AssetLoader {
             return data;
         }
     }
-//
-//    private class FloatGridPopulator implements Populator<float[]> {
-//
-//        @Override
-//        public float[][] populate(Integer bufferViewIndex, int componentType, String type, int count,
-//                int byteOffset, boolean normalized) throws IOException {
-//
-//            int numComponents = getNumberOfComponents(type);
-//            int dataSize = numComponents * count;
-//            float[] data = new float[dataSize];
-//
-//            if (bufferViewIndex == null) {
-//                // no referenced buffer, specs say to pad the data with zeros.
-//                padBuffer(data, dataSize);
-//            } else {
-//                readBuffer(bufferViewIndex, byteOffset, count, data, numComponents,
-//                        getVertexBufferFormat(componentType));
-//            }
-//
-//            return data;
-//        }
-//
-//    }
+    //
+    // private class FloatGridPopulator implements Populator<float[]> {
+    //
+    // @Override
+    // public float[][] populate(Integer bufferViewIndex, int componentType, String type, int count,
+    // int byteOffset, boolean normalized) throws IOException {
+    //
+    // int numComponents = getNumberOfComponents(type);
+    // int dataSize = numComponents * count;
+    // float[] data = new float[dataSize];
+    //
+    // if (bufferViewIndex == null) {
+    // // no referenced buffer, specs say to pad the data with zeros.
+    // padBuffer(data, dataSize);
+    // } else {
+    // readBuffer(bufferViewIndex, byteOffset, count, data, numComponents,
+    // getVertexBufferFormat(componentType));
+    // }
+    //
+    // return data;
+    // }
+    //
+    // }
 
     private class Vector3fArrayPopulator implements Populator<Vector3f[]> {
 
@@ -1546,17 +1683,15 @@ public class GltfLoader implements AssetLoader {
             return new SkinBuffers(data, format.getComponentSize());
         }
     }
-    
 
     public static void registerExtension(String name, Class<? extends ExtensionLoader> ext) {
-        CustomContentManager.defaultExtensionLoaders.put(name, ext);        
+        CustomContentManager.defaultExtensionLoaders.put(name, ext);
     }
-    
 
     public static void unregisterExtension(String name) {
         CustomContentManager.defaultExtensionLoaders.remove(name);
     }
-    
+
     /**
      * Sets the default extras loader used when no loader is specified in the GltfModelKey.
      * 
@@ -1567,7 +1702,6 @@ public class GltfLoader implements AssetLoader {
         CustomContentManager.defaultExtraLoaderClass = loader;
     }
 
-
     /**
      * Unregisters the default extras loader.
      */

+ 61 - 21
jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfUtils.java

@@ -31,25 +31,47 @@
  */
 package com.jme3.scene.plugins.gltf;
 
-import com.jme3.plugins.json.JsonArray;
-import com.jme3.plugins.json.JsonElement;
-import com.jme3.plugins.json.JsonObject;
+import java.io.ByteArrayInputStream;
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.Buffer;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.FloatBuffer;
+import java.nio.IntBuffer;
+import java.nio.ShortBuffer;
+import java.nio.channels.Channels;
+import java.nio.channels.ReadableByteChannel;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
 import com.jme3.asset.AssetInfo;
 import com.jme3.asset.AssetLoadException;
-import com.jme3.export.binary.ByteUtils;
-import com.jme3.math.*;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.FastMath;
+import com.jme3.math.Matrix4f;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Vector3f;
 import com.jme3.plugins.json.Json;
+import com.jme3.plugins.json.JsonArray;
+import com.jme3.plugins.json.JsonElement;
+import com.jme3.plugins.json.JsonObject;
 import com.jme3.plugins.json.JsonParser;
-import com.jme3.scene.*;
+import com.jme3.scene.Mesh;
+import com.jme3.scene.Spatial;
+import com.jme3.scene.VertexBuffer;
+import com.jme3.scene.plugins.gltf.GltfLoader.SkinBuffers;
 import com.jme3.texture.Texture;
-import com.jme3.util.*;
-import java.io.*;
-import java.nio.*;
-import java.nio.channels.Channels;
-import java.nio.channels.ReadableByteChannel;
-import java.util.*;
-import java.util.logging.Level;
-import java.util.logging.Logger;
+import com.jme3.util.BufferUtils;
+import com.jme3.util.IntMap;
+import com.jme3.util.LittleEndien;
 
 /**
  * Created by Nehon on 07/08/2017.
@@ -105,17 +127,17 @@ public class GltfUtils {
 
     public static VertexBuffer.Format getVertexBufferFormat(int componentType) {
         switch (componentType) {
-            case 5120:
+            case GltfConstants.GL_BYTE:
                 return VertexBuffer.Format.Byte;
-            case 5121:
+            case GltfConstants.GL_UNSIGNED_BYTE:
                 return VertexBuffer.Format.UnsignedByte;
-            case 5122:
+            case GltfConstants.GL_SHORT:
                 return VertexBuffer.Format.Short;
-            case 5123:
+            case GltfConstants.GL_UNSIGNED_SHORT:
                 return VertexBuffer.Format.UnsignedShort;
-            case 5125:
+            case GltfConstants.GL_UNSIGNED_INT:
                 return VertexBuffer.Format.UnsignedInt;
-            case 5126:
+            case GltfConstants.GL_FLOAT:
                 return VertexBuffer.Format.Float;
             default:
                 throw new AssetLoadException("Illegal component type: " + componentType);
@@ -505,7 +527,7 @@ public class GltfUtils {
     }
 
 
-    public static void handleSkinningBuffers(Mesh mesh, IntMap<GltfLoader.SkinBuffers> skinBuffers) {
+    static void handleSkinningBuffers(Mesh mesh, IntMap<GltfLoader.SkinBuffers> skinBuffers) {
         if (skinBuffers.size() > 0) {
             int length = skinBuffers.get(0).joints.length;
             short[] jointsArray = new short[length];
@@ -724,6 +746,24 @@ public class GltfUtils {
         return el == null ? null : el.getAsInt();
     }
 
+    /**
+     * Returns the specified element from the given parent as an <code>int</code>,
+     * throwing an exception if it is not present.
+     * 
+     * @param parent The parent element
+     * @param parentName The parent name
+     * @param name The name of the element
+     * @return The value, as an <code>int</code>
+     * @throws AssetLoadException If the element is not present
+     */
+    public static int getAsInt(JsonObject parent, String parentName, String name) {
+        JsonElement el = parent.get(name);
+        if (el == null) {
+            throw new AssetLoadException("No " + name + " defined for " + parentName);
+        }
+        return el.getAsInt();
+    }
+    
     public static Integer getAsInteger(JsonObject parent, String name, int defaultValue) {
         JsonElement el = parent.get(name);
         return el == null ? defaultValue : el.getAsInt();