diff --git a/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java b/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java index 4ece681a65..0255f7e85a 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("BoomBox", "gltf"); - 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("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"); + + // FIXME: bad skinning? + // loadModelSample("BrainStem", "draco"); + + // loadModelSample("CesiumMilkTruck", "draco"); + + // FIXME: FAILS WITH INDEX OUT OF BOUND EXCEPTION + // loadModelSample("VirtualCity", "draco"); + + // loadModelSample("Corset", "draco"); + + // loadModelSample("Lantern", "draco"); + + // loadModelSample("MorphPrimitivesTest", "draco"); + + // FIXME: skinning? + // loadModelSample("RiggedFigure", "draco"); + + // FIXME: skinning? + // loadModelSample("RiggedSimple", "draco"); + + // FIXME: "dracoMesh" is null + // loadModelSample("SunglassesKhronos", "draco"); + // loadModelSample("WaterBottle", "draco"); + + probeNode.attachChild(assets.get(0)); inputManager.addMapping("autorotate", new KeyTrigger(KeyInput.KEY_SPACE)); inputManager.addListener(new ActionListener() { @@ -213,36 +198,63 @@ 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; + + 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(0.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 +262,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 +309,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..bb2fa9fa4a 100644 --- a/jme3-plugins/build.gradle +++ b/jme3-plugins/build.gradle @@ -9,9 +9,15 @@ sourceSets { } } +repositories{ + maven { + url "https://maven.rblb.it/jMonkeyEngine/openize-drako-java/" + } +} + dependencies { api project(':jme3-core') - + implementation "org.jmonkeyengine:drako:1.4.3" 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..2c978da2fe --- /dev/null +++ b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/DracoMeshCompressionExtensionLoader.java @@ -0,0 +1,637 @@ +/* + * 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.util.BufferUtils; + +import dev.fileformat.drako.Draco; +import dev.fileformat.drako.DracoMesh; +import dev.fileformat.drako.DrakoException; +import dev.fileformat.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); + VertexBuffer attributeVertexBuffer = createAttributeVertexBuffer(attributeName, accessor, + pointAttribute, indices); + mesh.clearBuffer(bufferType); + mesh.setBuffer(attributeVertexBuffer); + } + + logger.log(level, "Decoding draco data DONE"); + return mesh; + } + + /** + * 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 + * @param indices + * The indices, obtained from the draco mesh + * @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 indices[]) { + 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, indices, count, componentCount); + VertexBuffer attributeVertexBuffer = createByteAttributeVertexBuffer(accessor, bufferType, + attributeData); + return attributeVertexBuffer; + } + if (componentType == GltfConstants.GL_SHORT || componentType == GltfConstants.GL_UNSIGNED_SHORT) { + ShortBuffer attributeData = readShortDracoAttribute(pointAttribute, indices, count, + componentCount); + VertexBuffer attributeVertexBuffer = createShortAttributeVertexBuffer(accessor, bufferType, + attributeData); + return attributeVertexBuffer; + } + if (componentType == GltfConstants.GL_FLOAT) { + FloatBuffer attributeData = readFloatDracoAttribute(pointAttribute, indices, 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 indices + * The indices, obtained from the draco mesh + * @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 indices[], int count, + int componentCount) { + int numFaces = indices.length / 3; + byte p[] = new byte[componentCount]; + ByteBuffer attributeData = BufferUtils.createByteBuffer(count * componentCount); + for (int i = 0; i < numFaces; i++) { + int j0 = indices[i * 3 + 0]; + int j1 = indices[i * 3 + 1]; + int j2 = indices[i * 3 + 2]; + + int mj0 = pointAttribute.mappedIndex(j0); + int mj1 = pointAttribute.mappedIndex(j1); + int mj2 = pointAttribute.mappedIndex(j2); + + pointAttribute.getValue(mj0, p); + int offset0 = j0 * componentCount; + for (int c = 0; c < componentCount; c++) { + attributeData.put(offset0 + c, p[c]); + } + pointAttribute.getValue(mj1, p); + int offset1 = j1 * componentCount; + for (int c = 0; c < componentCount; c++) { + attributeData.put(offset1 + c, p[c]); + } + pointAttribute.getValue(mj2, p); + int offset2 = j2 * componentCount; + for (int c = 0; c < componentCount; c++) { + attributeData.put(offset2 + c, p[c]); + } + } + return attributeData; + } + + /** + * Read the data from the given point attribute, as short values + * + * @param pointAttribute + * The Draco-decoded point attribute + * @param indices + * The indices, obtained from the draco mesh + * @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 indices[], + int count, int componentCount) { + int numFaces = indices.length / 3; + short p[] = new short[componentCount]; + ShortBuffer attributeData = BufferUtils.createShortBuffer(count * componentCount); + for (int i = 0; i < numFaces; i++) { + int j0 = indices[i * 3 + 0]; + int j1 = indices[i * 3 + 1]; + int j2 = indices[i * 3 + 2]; + + int mj0 = pointAttribute.mappedIndex(j0); + int mj1 = pointAttribute.mappedIndex(j1); + int mj2 = pointAttribute.mappedIndex(j2); + + pointAttribute.getValue(mj0, p); + int offset0 = j0 * componentCount; + for (int c = 0; c < componentCount; c++) { + attributeData.put(offset0 + c, p[c]); + } + pointAttribute.getValue(mj1, p); + int offset1 = j1 * componentCount; + for (int c = 0; c < componentCount; c++) { + attributeData.put(offset1 + c, p[c]); + } + pointAttribute.getValue(mj2, p); + int offset2 = j2 * componentCount; + for (int c = 0; c < componentCount; c++) { + attributeData.put(offset2 + c, p[c]); + } + } + return attributeData; + } + + /** + * Read the data from the given point attribute, as float values + * + * @param pointAttribute + * The Draco-decoded point attribute + * @param indices + * The indices, obtained from the draco mesh + * @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 indices[], + int count, int componentCount) { + int numFaces = indices.length / 3; + float p[] = new float[componentCount]; + FloatBuffer attributeData = BufferUtils.createFloatBuffer(count * componentCount); + for (int i = 0; i < numFaces; i++) { + int j0 = indices[i * 3 + 0]; + int j1 = indices[i * 3 + 1]; + int j2 = indices[i * 3 + 2]; + + int mj0 = pointAttribute.mappedIndex(j0); + int mj1 = pointAttribute.mappedIndex(j1); + int mj2 = pointAttribute.mappedIndex(j2); + + pointAttribute.getValue(mj0, p); + int offset0 = j0 * componentCount; + for (int c = 0; c < componentCount; c++) { + attributeData.put(offset0 + c, p[c]); + } + pointAttribute.getValue(mj1, p); + int offset1 = j1 * componentCount; + for (int c = 0; c < componentCount; c++) { + attributeData.put(offset1 + c, p[c]); + } + pointAttribute.getValue(mj2, p); + int offset2 = j2 * componentCount; + for (int c = 0; c < componentCount; c++) { + attributeData.put(offset2 + 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..08638010d4 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 @@ -622,9 +622,59 @@ public Buffer viewBuffer(Integer bufferViewIndex, int byteOffset, int count, } + /** + * 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"); Integer bufferLength = getAsInteger(buffer, "byteLength"); 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..6b28840153 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 @@ -36,10 +36,10 @@ import com.jme3.plugins.json.JsonObject; import com.jme3.asset.AssetInfo; import com.jme3.asset.AssetLoadException; -import com.jme3.export.binary.ByteUtils; import com.jme3.math.*; import com.jme3.plugins.json.Json; import com.jme3.plugins.json.JsonParser; +import com.jme3.renderer.opengl.GL; import com.jme3.scene.*; import com.jme3.texture.Texture; import com.jme3.util.*; @@ -105,17 +105,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); @@ -724,6 +724,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();