diff --git a/jme3-core/src/main/java/com/jme3/scene/Mesh.java b/jme3-core/src/main/java/com/jme3/scene/Mesh.java index 2819c2838f..e4ab6393b3 100644 --- a/jme3-core/src/main/java/com/jme3/scene/Mesh.java +++ b/jme3-core/src/main/java/com/jme3/scene/Mesh.java @@ -366,6 +366,7 @@ public void generateBindPose() { 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 void generateBindPose() { norm.getNumComponents(), norm.getFormat(), BufferUtils.clone(norm.getData())); + clearBuffer(bindNorm.getBufferType()); setBuffer(bindNorm); norm.setUsage(Usage.Stream); } @@ -390,6 +392,7 @@ public void generateBindPose() { 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 diff --git a/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java b/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java index 4ece681a65..f1f2bdc875 100644 --- a/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java +++ b/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 assets = new ArrayList<>(); + private final Node autoRotate = new Node("autoRotate"); + private final List 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 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 void simpleInitApp() { 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 void simpleInitApp() { 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 void onAction(String name, boolean isPressed, float tpf) { dumpScene(rootNode, 0); - // stateManager.attach(new DetailedProfilerState()); + // stateManager.attach(new DetailedProfilerState()); } - private T findControl(Spatial s, Class 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 @@ private void loadModel(String path, Vector3f offset, Vector3f scale) { 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 anims = new LinkedList<>(); - private AnimComposer composer; - private void playFirstAnim(Spatial s) { AnimComposer control = s.getControl(AnimComposer.class); @@ -317,25 +312,25 @@ public void simpleUpdate(float tpf) { 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()) { diff --git a/jme3-plugins/build.gradle b/jme3-plugins/build.gradle index e84f234a68..05a30fb328 100644 --- a/jme3-plugins/build.gradle +++ b/jme3-plugins/build.gradle @@ -11,7 +11,7 @@ sourceSets { dependencies { api project(':jme3-core') - + implementation "com.openize:drako:1.4.4" implementation project(':jme3-plugins-json') implementation project(':jme3-plugins-json-gson') testRuntimeOnly project(':jme3-desktop') diff --git a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/BufferQuantization.java b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/BufferQuantization.java new file mode 100644 index 0000000000..ce9a309b60 --- /dev/null +++ b/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 + } + +} diff --git a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/CustomContentManager.java b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/CustomContentManager.java index 20f2c5e141..8606667eb5 100644 --- a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/CustomContentManager.java +++ b/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); } diff --git a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/DracoMeshCompressionExtensionLoader.java b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/DracoMeshCompressionExtensionLoader.java new file mode 100644 index 0000000000..4972bf8d05 --- /dev/null +++ b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/DracoMeshCompressionExtensionLoader.java @@ -0,0 +1,696 @@ +/* + * 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 KHR_draco_mesh_compression 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 KHR_draco_mesh_compression 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 + */ + private static final Level level = Level.INFO; + + /** + *
    + *
  • The parentName will be "primitive"
  • + *
  • The parent" will be the JSON element that represents the mesh primitive from the glTF + * JSON.
  • + *
  • The extension will be the JSON element that represents the + * KHR_draco_mesh_compression extension object.
  • + *
+ * + * {@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, convert them into a vertex buffer, + // and replace the index vertex buffer of the mesh with + // the newly created buffer. + 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 = createIndicesVertexBuffer(loader, indicesComponentType, indices); + mesh.clearBuffer(VertexBuffer.Type.Index); + mesh.setBuffer(indicesVertexBuffer); + + // 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 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 JOINTS_n attribute that was decoded from Draco. + * + * This will read the data from the attribute, and store it as the {@link SkinBuffers#joints} array for 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 GL_UNSIGNED_BYTE or + * GL_UNSIGNED_SHORT + */ + 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 WEIGHTS_n attribute that was decoded from Draco. + * + * This will read the data from the attribute, and store it as the {@link SkinBuffers#weights} array for + * 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 GL_UNSIGNED_BYTE or + * GL_UNSIGNED_SHORT or GL_FLOAT, or if it is + * GL_UNSIGNED_BYTE or GL_UNSIGNED_SHORT 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 openize-drako-java. + * + * @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 that should go into the mesh, based on the given Draco-decoded indices + * + * @param loader + * The glTF loader + * @param accessorIndex + * The accessor index of the vertices + * @param indices + * The Draco-decoded indices + * @return The indices vertex buffer + * @throws AssetLoadException + * If the given component type is not GL_UNSIGNED_BYTE, + * GL_UNSIGNED_SHORT, or GL_UNSIGNED_INT + */ + VertexBuffer createIndicesVertexBuffer(GltfLoader loader, int componentType, int indices[]) { + Buffer data = null; + if (componentType == GltfConstants.GL_UNSIGNED_BYTE) { + data = createByteBuffer(indices); + } else if (componentType == GltfConstants.GL_UNSIGNED_SHORT) { + data = createShortBuffer(indices); + } else if (componentType == GltfConstants.GL_UNSIGNED_INT) { + data = BufferUtils.createIntBuffer(indices); + } else { + 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); + } + VertexBuffer vb = new VertexBuffer(VertexBuffer.Type.Index); + VertexBuffer.Format format = getVertexBufferFormat(componentType); + int numComponents = 3; + vb.setupData(VertexBuffer.Usage.Dynamic, numComponents, format, data); + return vb; + } + + // TODO_DRACO Could go into GltfUtils + /** + * Determines the number of components per element for the given accessor, based on its type + * + * @param accessor + * The accessor + * @return The number of components + * @throws AssetLoadException + * If the accessor does not have a valid type 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 go into BufferUtils + /** + * Create a byte buffer containing the given values, cast to byte + * + * @param array + * The array + * @return The buffer + */ + private 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; + } + + // TODO_DRACO Could go into BufferUtils + /** + * Create a short buffer containing the given values, cast to short + * + * @param array + * The array + * @return The buffer + */ + private 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; + } + + // 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 "POSITION" (only used for error messages) + * @param id + * The unique ID of the attribute, i.e. the value that was stored as the + * "POSITION": id 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 byte 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 short 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 float 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 byte attribute data. + * + * If the accessor is normalized, then this will dequantize the given data into a + * Float 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 short attribute data + * + * If the accessor is normalized, then this will dequantize the given data into a + * Float 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 float 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; + } + +} diff --git a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfConstants.java b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfConstants.java new file mode 100644 index 0000000000..e9e4a5e853 --- /dev/null +++ b/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 + } +} diff --git a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfLoader.java b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfLoader.java index 0c8448e226..52c2074d6d 100644 --- a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfLoader.java +++ b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfLoader.java @@ -31,39 +31,87 @@ */ 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.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 +147,7 @@ public class GltfLoader implements AssetLoader { private boolean useNormalsFlag = false; Map> skinnedSpatials = new HashMap<>(); - IntMap skinBuffers = new IntMap<>(); + private final IntMap skinBuffers = new IntMap<>(); public GltfLoader() { defaultMaterialAdapters.put("pbrMetallicRoughness", new PBRMetalRoughMaterialAdapter()); @@ -130,7 +178,8 @@ protected Object loadFromStream(AssetInfo assetInfo, InputStream stream) throws 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 +329,9 @@ public Object readNode(int nodeIndex) throws IOException { } 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 +410,17 @@ public Transform readTransforms(JsonObject nodeData) { // 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 +429,7 @@ public Transform readTransforms(JsonObject nodeData) { 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 +445,8 @@ public Geometry[] readMeshPrimitives(int meshIndex) throws IOException { 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 +458,19 @@ public Geometry[] readMeshPrimitives(int meshIndex) throws IOException { for (Map.Entry 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 +483,13 @@ public Geometry[] readMeshPrimitives(int meshIndex) throws IOException { 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 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 +526,15 @@ public Geometry[] readMeshPrimitives(int meshIndex) throws IOException { } 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 +544,7 @@ public Geometry[] readMeshPrimitives(int meshIndex) throws IOException { } geom.setName(name + "_" + index); - + geom.updateModelBound(); geomArray[index] = geom; index++; @@ -528,7 +562,7 @@ public Geometry[] readMeshPrimitives(int meshIndex) throws IOException { 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 +572,36 @@ private SkinBuffers getSkinBuffers(String bufferType) { 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. + 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.clearBuffer(weightsHW.getBufferType()); + mesh.setBuffer(weightsHW); + mesh.clearBuffer(indicesHW.getBufferType()); + mesh.setBuffer(indicesHW); + mesh.generateBindPose(); + } + } + private R readAccessorData(int accessorIndex, Populator populator) throws IOException { assertNotNull(accessors, "No accessor attribute in the gltf file"); @@ -579,7 +643,7 @@ public Object readBuffer(Integer bufferViewIndex, int byteOffset, int count, Obj 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 +660,8 @@ public Object readBuffer(Integer bufferViewIndex, int byteOffset, int count, Obj 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 +673,74 @@ public Buffer viewBuffer(Integer bufferViewIndex, int byteOffset, int count, 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 +763,8 @@ protected ByteBuffer getBytes(int bufferIndex, String uri, Integer bufferLength) 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 +774,11 @@ protected ByteBuffer getBytes(int bufferIndex, String uri, Integer bufferLength) } 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 +822,8 @@ public Material readMaterial(int materialIndex) throws IOException { 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 +849,7 @@ public Material readMaterial(int materialIndex) throws IOException { 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 +923,7 @@ public Texture2D readTexture(JsonObject texture, boolean flip) throws IOExceptio if (texture2d != null) { return texture2d; } - + JsonObject textureData = textures.get(textureIndex).getAsJsonObject(); Integer sourceIndex = getAsInteger(textureData, "source"); Integer samplerIndex = getAsInteger(textureData, "sampler"); @@ -837,11 +956,12 @@ public Texture2D readImage(int sourceIndex, boolean flip) throws IOException { 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 +971,7 @@ public Texture2D readImage(int sourceIndex, boolean flip) throws IOException { 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 +991,7 @@ public void readAnimation(int animationIndex) throws IOException { 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 +1006,13 @@ public void readAnimation(int animationIndex) throws IOException { 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 +1087,8 @@ public void readAnimation(int animationIndex) throws IOException { 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 +1113,14 @@ public void readAnimation(int animationIndex) throws IOException { // 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 +1134,17 @@ public void readAnimation(int animationIndex) throws IOException { 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 +1301,7 @@ public Joint readNodeAsBone(int nodeIndex, int jointIndex, int skinIndex, Matrix 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 +1345,12 @@ private void setupControls() { 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 +1382,7 @@ private String readMeshName(int meshIndex) { 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 +1469,18 @@ private class SkinData { 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 { @@ -1380,7 +1499,9 @@ public VertexBufferPopulator(VertexBuffer.Type bufferType) { 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 +1523,8 @@ public VertexBuffer populate(Integer bufferViewIndex, int componentType, String // 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 +1556,29 @@ public float[] populate(Integer bufferViewIndex, int componentType, String type, return data; } } -// -// private class FloatGridPopulator implements Populator { -// -// @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 { + // + // @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 { @@ -1546,17 +1668,15 @@ public SkinBuffers populate(Integer bufferViewIndex, int componentType, String t return new SkinBuffers(data, format.getComponentSize()); } } - public static void registerExtension(String name, Class 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 +1687,6 @@ public static void registerDefaultExtrasLoader(Class loa CustomContentManager.defaultExtraLoaderClass = loader; } - /** * Unregisters the default extras loader. */ diff --git a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfUtils.java b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfUtils.java index 7c6ee853f1..677f15e6ea 100644 --- a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfUtils.java +++ b/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 static Mesh.Mode getMeshMode(Integer mode) { 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 static byte[] toByteArray(short[] shortArray) { } - public static void handleSkinningBuffers(Mesh mesh, IntMap skinBuffers) { + static void handleSkinningBuffers(Mesh mesh, IntMap skinBuffers) { if (skinBuffers.size() > 0) { int length = skinBuffers.get(0).joints.length; short[] jointsArray = new short[length]; @@ -724,6 +746,24 @@ public static Integer getAsInteger(JsonObject parent, String name) { return el == null ? null : el.getAsInt(); } + /** + * Returns the specified element from the given parent as an int, + * 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 int + * @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();