From 8f29f486761e465faffb7c706b04710080ac3691 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Thu, 29 Jan 2026 16:42:03 +0100 Subject: [PATCH 01/11] Add extension loader for Draco --- .../plugins/gltf/CustomContentManager.java | 1 + .../DracoMeshCompressionExtensionLoader.java | 564 ++++++++++++++++++ .../jme3/scene/plugins/gltf/GltfLoader.java | 52 +- .../jme3/scene/plugins/gltf/GltfUtils.java | 32 +- 4 files changed, 641 insertions(+), 8 deletions(-) create mode 100644 jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/DracoMeshCompressionExtensionLoader.java 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..bfdaceeb7b --- /dev/null +++ b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/DracoMeshCompressionExtensionLoader.java @@ -0,0 +1,564 @@ +/* + * 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.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.renderer.opengl.GL; +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; + + /** + * + * + * {@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 == GL.GL_UNSIGNED_BYTE) { + data = createByteBuffer(indices); + } else if (componentType == GL.GL_UNSIGNED_SHORT) { + data = createShortBuffer(indices); + } else if (componentType == GL.GL_UNSIGNED_INT) { + data = BufferUtils.createIntBuffer(indices); + } else { + throw new AssetLoadException("The indices accessor must have a component type of " + GL.GL_UNSIGNED_BYTE + + ", " + GL.GL_UNSIGNED_SHORT + ", or " + GL.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 == GL.GL_BYTE || componentType == GL.GL_UNSIGNED_BYTE) { + ByteBuffer attributeData = readByteDracoAttribute(pointAttribute, indices, count, componentCount); + VertexBuffer attributeVertexBuffer = createByteAttributeVertexBuffer(accessor, bufferType, attributeData); + return attributeVertexBuffer; + } + if (componentType == GL.GL_SHORT || componentType == GL.GL_UNSIGNED_SHORT) { + ShortBuffer attributeData = readShortDracoAttribute(pointAttribute, indices, count, componentCount); + VertexBuffer attributeVertexBuffer = createShortAttributeVertexBuffer(accessor, bufferType, attributeData); + return attributeVertexBuffer; + } + if (componentType == GL.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 " + + GL.GL_BYTE + ", " + GL.GL_UNSIGNED_BYTE + ", " + GL.GL_SHORT + ", " + GL.GL_UNSIGNED_SHORT + ", " + + "or " + GL.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 + * + * @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); + VertexBuffer.Format format = getVertexBufferFormat(componentType); + int numComponents = getAccessorComponentCount(accessor); + vb.setupData(VertexBuffer.Usage.Dynamic, numComponents, format, attributeData); + return vb; + } + + /** + * Create the vertex buffer for the given short 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 createShortAttributeVertexBuffer(JsonObject accessor, VertexBuffer.Type bufferType, + ShortBuffer 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; + } + + /** + * 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/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..5ca33a33fa 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 GL.GL_BYTE: return VertexBuffer.Format.Byte; - case 5121: + case GL.GL_UNSIGNED_BYTE: return VertexBuffer.Format.UnsignedByte; - case 5122: + case GL.GL_SHORT: return VertexBuffer.Format.Short; - case 5123: + case GL.GL_UNSIGNED_SHORT: return VertexBuffer.Format.UnsignedShort; - case 5125: + case GL.GL_UNSIGNED_INT: return VertexBuffer.Format.UnsignedInt; - case 5126: + case GL.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(); From c63a548cc5a859bff569555f7e49baf8a5d0518f Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Thu, 29 Jan 2026 16:42:34 +0100 Subject: [PATCH 02/11] Add Draco glTF loading tests --- .../java/jme3test/model/TestGltfLoading.java | 62 ++++++++++++++++++- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java b/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java index 4ece681a65..e1a08119b0 100644 --- a/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java +++ b/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java @@ -45,6 +45,7 @@ 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.util.*; @@ -134,8 +135,65 @@ public void simpleInitApp() { //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/box/box.gltf", Vector3f.ZERO, 1); + //loadModel("Models/gltf/duck/Duck.gltf", new Vector3f(0, 1, 0), 1); + + + // =================================================================== + // TODO_DRACO Draco test start + // The following test assumes that the "Models" directory from + // https://github.com/KhronosGroup/glTF-Sample-Assets/tree/main/Models + // is copied into the + // jme3-testdata\src\main\resources\Models\gltf\ + // directory (or at least the following list of models) + + // Comment in/out the model to test + String dracoTestModel = null; + //dracoTestModel = "Avocado"; + //dracoTestModel = "BarramundiFish"; + dracoTestModel = "BoomBox"; + //dracoTestModel = "BrainStem"; + //dracoTestModel = "CesiumMan"; + //dracoTestModel = "CesiumMilkTruck"; + //dracoTestModel = "Corset"; + //dracoTestModel = "Lantern"; + //dracoTestModel = "MorphPrimitivesTest"; + //dracoTestModel = "RiggedFigure"; + //dracoTestModel = "RiggedSimple"; + //dracoTestModel = "SunglassesKhronos"; + //dracoTestModel = "VirtualCity"; + //dracoTestModel = "WaterBottle"; + + boolean testDraco = true; + // Uncomment this to not load the Draco-compressed + // version, but the glTF-Binary version of the model + //testDraco = false; + String dracoTestFlavor = null; + String dracoTestExtension = null; + if (testDraco) + { + dracoTestFlavor = "glTF-Draco"; + dracoTestExtension = "gltf"; + } + else + { + dracoTestFlavor = "glTF-Binary"; + dracoTestExtension = "glb"; + } + // Assemble a path like + // "Models/gltf/Models/BoomBox/glTF-Draco/BoomBox.gltf" + String dracoTestPath = "Models/gltf/Models/" + dracoTestModel + "/" + dracoTestFlavor + "/" + dracoTestModel + "." + dracoTestExtension; + + System.out.println("Running Draco test with "+dracoTestPath); + loadModel(dracoTestPath, new Vector3f(0, 0, 0), 20.0f); + + // TODO_DRACO Draco test end + // =================================================================== + + + //loadModel("Models/gltf/BoomBox/glTF-Draco/BoomBox.gltf", new Vector3f(0, 0, 0), 20); + //loadModel("Models/gltf/BoomBox/glTF-Binary/BoomBox.glb", new Vector3f(0, 0, 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); From 77d611f47a60b221d54678f2d5cddc250aa55de1 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Thu, 29 Jan 2026 17:27:11 +0100 Subject: [PATCH 03/11] add draco dependency --- jme3-plugins/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jme3-plugins/build.gradle b/jme3-plugins/build.gradle index e84f234a68..2d2ca68b9d 100644 --- a/jme3-plugins/build.gradle +++ b/jme3-plugins/build.gradle @@ -11,7 +11,7 @@ sourceSets { dependencies { api project(':jme3-core') - + implementation "dev.fileformat:drako:1.4.2" implementation project(':jme3-plugins-json') implementation project(':jme3-plugins-json-gson') testRuntimeOnly project(':jme3-desktop') From 37d1d82883f021474be84369bccb85ab3b4f2f2f Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Thu, 29 Jan 2026 18:34:37 +0100 Subject: [PATCH 04/11] format --- .../DracoMeshCompressionExtensionLoader.java | 278 ++++++++++-------- 1 file changed, 156 insertions(+), 122 deletions(-) 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 index bfdaceeb7b..64c7cc322c 100644 --- 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 @@ -63,28 +63,25 @@ import dev.fileformat.drako.PointAttribute; /** - * A class for handling the KHR_draco_mesh_compression extension - * when loading a glTF asset. + * 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 + * 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. + * 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()); + private final static Logger logger = Logger + .getLogger(DracoMeshCompressionExtensionLoader.class.getName()); /** * The default log level @@ -94,8 +91,8 @@ public class DracoMeshCompressionExtensionLoader implements ExtensionLoader { /** *
    *
  • The parentName will be "primitive"
  • - *
  • The parent" will be the JSON element that represents the - * mesh primitive from the glTF JSON.
  • + *
  • 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.
  • *
@@ -103,8 +100,8 @@ public class DracoMeshCompressionExtensionLoader implements ExtensionLoader { * {@inheritDoc} */ @Override - public Object handleExtension(GltfLoader loader, String parentName, JsonElement parent, JsonElement extension, - Object input) throws IOException { + public Object handleExtension(GltfLoader loader, String parentName, JsonElement parent, + JsonElement extension, Object input) throws IOException { logger.log(level, "Decoding draco data"); @@ -121,7 +118,8 @@ public Object handleExtension(GltfLoader loader, String parentName, JsonElement int indices[] = dracoMesh.getIndices().toArray(); int indicesAccessorIndex = getAsInt(meshPrimitiveObject, "mesh primitive", "indices"); JsonObject indicesAccessor = loader.getAccessor(indicesAccessorIndex); - int indicesComponentType = getAsInt(indicesAccessor, "accessor " + indicesAccessorIndex, "componentType"); + int indicesComponentType = getAsInt(indicesAccessor, "accessor " + indicesAccessorIndex, + "componentType"); VertexBuffer indicesVertexBuffer = createIndicesVertexBuffer(loader, indicesComponentType, indices); mesh.clearBuffer(VertexBuffer.Type.Index); mesh.setBuffer(indicesVertexBuffer); @@ -148,7 +146,8 @@ public Object handleExtension(GltfLoader loader, String parentName, JsonElement // The mesh primitive stores the accessor index for // each attribute - int attributeAccessorIndex = getAsInt(parentAttributes, attributeName + " attribute", attributeName); + int attributeAccessorIndex = getAsInt(parentAttributes, attributeName + " attribute", + attributeName); JsonObject accessor = loader.getAccessor(attributeAccessorIndex); logger.log(level, "attributeAccessorIndex " + attributeAccessorIndex); @@ -158,8 +157,8 @@ public Object handleExtension(GltfLoader loader, String parentName, JsonElement // created from the data that was fetched from the // decoded draco PointAttribute Type bufferType = getVertexBufferType(attributeName); - VertexBuffer attributeVertexBuffer = createAttributeVertexBuffer(attributeName, accessor, pointAttribute, - indices); + VertexBuffer attributeVertexBuffer = createAttributeVertexBuffer(attributeName, accessor, + pointAttribute, indices); mesh.clearBuffer(bufferType); mesh.setBuffer(attributeVertexBuffer); } @@ -169,15 +168,15 @@ public Object handleExtension(GltfLoader loader, String parentName, JsonElement } /** - * Read the draco data from the given extension, using - * openize-drako-java. + * 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 + * @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 + * @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"); @@ -193,7 +192,8 @@ private static DracoMesh readDracoMesh(GltfLoader loader, JsonElement extension) try { dracoMesh = (DracoMesh) Draco.decode(bufferViewDataArray); } catch (DrakoException e) { - throw new AssetLoadException("Could not decode Draco mesh from buffer view " + bufferViewIndex, e); + throw new AssetLoadException("Could not decode Draco mesh from buffer view " + bufferViewIndex, + e); } logger.log(level, "Decoding draco mesh DONE"); @@ -201,17 +201,18 @@ private static DracoMesh readDracoMesh(GltfLoader loader, JsonElement extension) } /** - * Create the indices vertex buffer that should go into the mesh, based on the - * given Draco-decoded indices + * 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 + * @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 + * @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; @@ -222,8 +223,9 @@ VertexBuffer createIndicesVertexBuffer(GltfLoader loader, int componentType, int } else if (componentType == GL.GL_UNSIGNED_INT) { data = BufferUtils.createIntBuffer(indices); } else { - throw new AssetLoadException("The indices accessor must have a component type of " + GL.GL_UNSIGNED_BYTE - + ", " + GL.GL_UNSIGNED_SHORT + ", or " + GL.GL_UNSIGNED_INT + ", but has " + componentType); + throw new AssetLoadException("The indices accessor must have a component type of " + + GL.GL_UNSIGNED_BYTE + ", " + GL.GL_UNSIGNED_SHORT + ", or " + GL.GL_UNSIGNED_INT + + ", but has " + componentType); } VertexBuffer vb = new VertexBuffer(VertexBuffer.Type.Index); VertexBuffer.Format format = getVertexBufferFormat(componentType); @@ -234,13 +236,13 @@ VertexBuffer createIndicesVertexBuffer(GltfLoader loader, int componentType, int // TODO_DRACO Could go into GltfUtils /** - * Determines the number of components per element for the given accessor, based - * on its type + * Determines the number of components per element for the given accessor, based on its type * - * @param accessor The accessor + * @param accessor + * The accessor * @return The number of components - * @throws AssetLoadException If the accessor does not have a valid - * type property + * @throws AssetLoadException + * If the accessor does not have a valid type property */ private static int getAccessorComponentCount(JsonObject accessor) { String type = getAsString(accessor, "type"); @@ -252,7 +254,8 @@ private static int getAccessorComponentCount(JsonObject accessor) { /** * Create a byte buffer containing the given values, cast to byte * - * @param array The array + * @param array + * The array * @return The buffer */ private static Buffer createByteBuffer(int[] array) { @@ -267,7 +270,8 @@ private static Buffer createByteBuffer(int[] array) { /** * Create a short buffer containing the given values, cast to short * - * @param array The array + * @param array + * The array * @return The buffer */ private static Buffer createShortBuffer(int[] array) { @@ -282,19 +286,22 @@ private static Buffer createShortBuffer(int[] array) { /** * 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. + * 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 + * @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 + * @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 { + 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); @@ -312,14 +319,16 @@ private static ByteBuffer obtainBufferViewData(GltfLoader loader, int bufferView /** * 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. + * @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 + * @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++) { @@ -328,25 +337,29 @@ private static PointAttribute getAttribute(DracoMesh dracoMesh, String gltfAttri return attribute; } } - throw new AssetLoadException( - "Could not obtain attribute " + gltfAttribute + " with unique ID " + id + " from decoded Draco mesh"); + 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 + * 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 + * @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 + * @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[]) { + PointAttribute pointAttribute, int indices[]) { int count = getAsInt(accessor, "accessor", "count"); int componentType = getAsInt(accessor, "accessor", "componentType"); int componentCount = getAccessorComponentCount(accessor); @@ -354,37 +367,45 @@ private static VertexBuffer createAttributeVertexBuffer(String attributeName, Js if (componentType == GL.GL_BYTE || componentType == GL.GL_UNSIGNED_BYTE) { ByteBuffer attributeData = readByteDracoAttribute(pointAttribute, indices, count, componentCount); - VertexBuffer attributeVertexBuffer = createByteAttributeVertexBuffer(accessor, bufferType, attributeData); + VertexBuffer attributeVertexBuffer = createByteAttributeVertexBuffer(accessor, bufferType, + attributeData); return attributeVertexBuffer; } if (componentType == GL.GL_SHORT || componentType == GL.GL_UNSIGNED_SHORT) { - ShortBuffer attributeData = readShortDracoAttribute(pointAttribute, indices, count, componentCount); - VertexBuffer attributeVertexBuffer = createShortAttributeVertexBuffer(accessor, bufferType, attributeData); + ShortBuffer attributeData = readShortDracoAttribute(pointAttribute, indices, count, + componentCount); + VertexBuffer attributeVertexBuffer = createShortAttributeVertexBuffer(accessor, bufferType, + attributeData); return attributeVertexBuffer; } if (componentType == GL.GL_FLOAT) { - FloatBuffer attributeData = readFloatDracoAttribute(pointAttribute, indices, count, componentCount); - VertexBuffer attributeVertexBuffer = createFloatAttributeVertexBuffer(accessor, bufferType, attributeData); + 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 " - + GL.GL_BYTE + ", " + GL.GL_UNSIGNED_BYTE + ", " + GL.GL_SHORT + ", " + GL.GL_UNSIGNED_SHORT + ", " - + "or " + GL.GL_FLOAT + ", but has " + componentType); + throw new AssetLoadException( + "The accessor for attribute " + attributeName + " must have a component type of " + GL.GL_BYTE + + ", " + GL.GL_UNSIGNED_BYTE + ", " + GL.GL_SHORT + ", " + GL.GL_UNSIGNED_SHORT + ", " + + "or " + GL.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 + * @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 componentCount) { int numFaces = indices.length / 3; byte p[] = new byte[componentCount]; ByteBuffer attributeData = BufferUtils.createByteBuffer(count * componentCount); @@ -419,16 +440,18 @@ private static ByteBuffer readByteDracoAttribute(PointAttribute pointAttribute, /** * 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 + * @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) { + 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); @@ -463,16 +486,18 @@ private static ShortBuffer readShortDracoAttribute(PointAttribute pointAttribute /** * 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 + * @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) { + 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); @@ -507,14 +532,17 @@ private static FloatBuffer readFloatDracoAttribute(PointAttribute pointAttribute /** * Create the vertex buffer for the given byte attribute data * - * @param accessor The accessor that describes the component type and type + * @param accessor + * The accessor that describes the component type and type * - * @param bufferType The buffer type - * @param attributeData The attribute data + * @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) { + private static VertexBuffer createByteAttributeVertexBuffer(JsonObject accessor, + VertexBuffer.Type bufferType, ByteBuffer attributeData) { int componentType = getAsInt(accessor, "accessor", "componentType"); VertexBuffer vb = new VertexBuffer(bufferType); VertexBuffer.Format format = getVertexBufferFormat(componentType); @@ -526,14 +554,17 @@ private static VertexBuffer createByteAttributeVertexBuffer(JsonObject accessor, /** * Create the vertex buffer for the given short attribute data * - * @param accessor The accessor that describes the component type and type + * @param accessor + * The accessor that describes the component type and type * - * @param bufferType The buffer type - * @param attributeData The attribute data + * @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) { + private static VertexBuffer createShortAttributeVertexBuffer(JsonObject accessor, + VertexBuffer.Type bufferType, ShortBuffer attributeData) { int componentType = getAsInt(accessor, "accessor", "componentType"); VertexBuffer vb = new VertexBuffer(bufferType); VertexBuffer.Format format = getVertexBufferFormat(componentType); @@ -545,14 +576,17 @@ private static VertexBuffer createShortAttributeVertexBuffer(JsonObject accessor /** * Create the vertex buffer for the given float attribute data * - * @param accessor The accessor that describes the component type and type + * @param accessor + * The accessor that describes the component type and type * - * @param bufferType The buffer type - * @param attributeData The attribute data + * @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) { + 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); From cf47e2154f75ebf386be561cbdb33b7337ccc92b Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Thu, 29 Jan 2026 18:34:48 +0100 Subject: [PATCH 05/11] example refactoring --- .../java/jme3test/model/TestGltfLoading.java | 331 +++++++----------- 1 file changed, 134 insertions(+), 197 deletions(-) diff --git a/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java b/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java index e1a08119b0..ff73a6e6c9 100644 --- a/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java +++ b/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java @@ -32,47 +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() { @@ -81,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); @@ -99,138 +104,63 @@ 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); - - - // =================================================================== - // TODO_DRACO Draco test start - // The following test assumes that the "Models" directory from - // https://github.com/KhronosGroup/glTF-Sample-Assets/tree/main/Models - // is copied into the - // jme3-testdata\src\main\resources\Models\gltf\ - // directory (or at least the following list of models) - - // Comment in/out the model to test - String dracoTestModel = null; - //dracoTestModel = "Avocado"; - //dracoTestModel = "BarramundiFish"; - dracoTestModel = "BoomBox"; - //dracoTestModel = "BrainStem"; - //dracoTestModel = "CesiumMan"; - //dracoTestModel = "CesiumMilkTruck"; - //dracoTestModel = "Corset"; - //dracoTestModel = "Lantern"; - //dracoTestModel = "MorphPrimitivesTest"; - //dracoTestModel = "RiggedFigure"; - //dracoTestModel = "RiggedSimple"; - //dracoTestModel = "SunglassesKhronos"; - //dracoTestModel = "VirtualCity"; - //dracoTestModel = "WaterBottle"; - - boolean testDraco = true; - // Uncomment this to not load the Draco-compressed - // version, but the glTF-Binary version of the model - //testDraco = false; - String dracoTestFlavor = null; - String dracoTestExtension = null; - if (testDraco) - { - dracoTestFlavor = "glTF-Draco"; - dracoTestExtension = "gltf"; - } - else - { - dracoTestFlavor = "glTF-Binary"; - dracoTestExtension = "glb"; - } - // Assemble a path like - // "Models/gltf/Models/BoomBox/glTF-Draco/BoomBox.gltf" - String dracoTestPath = "Models/gltf/Models/" + dracoTestModel + "/" + dracoTestFlavor + "/" + dracoTestModel + "." + dracoTestExtension; - - System.out.println("Running Draco test with "+dracoTestPath); - loadModel(dracoTestPath, new Vector3f(0, 0, 0), 20.0f); - - // TODO_DRACO Draco test end - // =================================================================== - - - //loadModel("Models/gltf/BoomBox/glTF-Draco/BoomBox.gltf", new Vector3f(0, 0, 0), 20); - //loadModel("Models/gltf/BoomBox/glTF-Binary/BoomBox.glb", new Vector3f(0, 0, 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"); + + // FIXME: wrong texture coords? + // loadModelSample("BarramundiFish", "draco"); + + // loadModelSample("BoomBox", "draco"); + + // FIXME: bad skinning? + // loadModelSample("BrainStem", "draco"); + + // FIXME: wrong offsets? + // loadModelSample("CesiumMilkTruck", "draco"); + + // FIXME: FAILS WITH INDEX OUT OF BOUND EXCEPTION + // loadModelSample("VirtualCity", "draco"); + + // loadModelSample("Corset", "draco"); + + // FIXME: unclear + // 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() { @@ -271,36 +201,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); @@ -308,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); @@ -375,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()) { From fd070cf5f5404ee396ac0b2766f0a14285fac321 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Thu, 29 Jan 2026 19:06:16 +0100 Subject: [PATCH 06/11] remove dependency from GL renderer --- .../DracoMeshCompressionExtensionLoader.java | 23 ++++++++++--------- .../scene/plugins/gltf/GltfConstants.java | 11 +++++++++ .../jme3/scene/plugins/gltf/GltfUtils.java | 12 +++++----- 3 files changed, 29 insertions(+), 17 deletions(-) create mode 100644 jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfConstants.java 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 index 64c7cc322c..97028441ad 100644 --- 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 @@ -51,7 +51,6 @@ import com.jme3.asset.AssetLoadException; import com.jme3.plugins.json.JsonElement; import com.jme3.plugins.json.JsonObject; -import com.jme3.renderer.opengl.GL; import com.jme3.scene.Mesh; import com.jme3.scene.VertexBuffer; import com.jme3.scene.VertexBuffer.Type; @@ -216,15 +215,16 @@ private static DracoMesh readDracoMesh(GltfLoader loader, JsonElement extension) */ VertexBuffer createIndicesVertexBuffer(GltfLoader loader, int componentType, int indices[]) { Buffer data = null; - if (componentType == GL.GL_UNSIGNED_BYTE) { + if (componentType == GltfConstants.GL_UNSIGNED_BYTE) { data = createByteBuffer(indices); - } else if (componentType == GL.GL_UNSIGNED_SHORT) { + } else if (componentType == GltfConstants.GL_UNSIGNED_SHORT) { data = createShortBuffer(indices); - } else if (componentType == GL.GL_UNSIGNED_INT) { + } else if (componentType == GltfConstants.GL_UNSIGNED_INT) { data = BufferUtils.createIntBuffer(indices); } else { throw new AssetLoadException("The indices accessor must have a component type of " - + GL.GL_UNSIGNED_BYTE + ", " + GL.GL_UNSIGNED_SHORT + ", or " + GL.GL_UNSIGNED_INT + + GltfConstants.GL_UNSIGNED_BYTE + ", " + GltfConstants.GL_UNSIGNED_SHORT + ", or " + + GltfConstants.GL_UNSIGNED_INT + ", but has " + componentType); } VertexBuffer vb = new VertexBuffer(VertexBuffer.Type.Index); @@ -365,20 +365,20 @@ private static VertexBuffer createAttributeVertexBuffer(String attributeName, Js int componentCount = getAccessorComponentCount(accessor); Type bufferType = getVertexBufferType(attributeName); - if (componentType == GL.GL_BYTE || componentType == GL.GL_UNSIGNED_BYTE) { + 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 == GL.GL_SHORT || componentType == GL.GL_UNSIGNED_SHORT) { + 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 == GL.GL_FLOAT) { + if (componentType == GltfConstants.GL_FLOAT) { FloatBuffer attributeData = readFloatDracoAttribute(pointAttribute, indices, count, componentCount); VertexBuffer attributeVertexBuffer = createFloatAttributeVertexBuffer(accessor, bufferType, @@ -386,9 +386,10 @@ private static VertexBuffer createAttributeVertexBuffer(String attributeName, Js return attributeVertexBuffer; } throw new AssetLoadException( - "The accessor for attribute " + attributeName + " must have a component type of " + GL.GL_BYTE - + ", " + GL.GL_UNSIGNED_BYTE + ", " + GL.GL_SHORT + ", " + GL.GL_UNSIGNED_SHORT + ", " - + "or " + GL.GL_FLOAT + ", but has " + componentType); + "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); } /** 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..9daf714cee --- /dev/null +++ b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfConstants.java @@ -0,0 +1,11 @@ +package com.jme3.scene.plugins.gltf; + +public class GltfConstants { + public static final int GL_BYTE = 0x1400; + public static final int GL_UNSIGNED_BYTE = 0x1401; + public static final int GL_SHORT = 0x1402; + public static final int GL_UNSIGNED_SHORT = 0x1403; + public static final int GL_UNSIGNED_INT = 0x1405; + public static final int GL_FLOAT = 0x1406; + +} 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 5ca33a33fa..755229d820 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 @@ -105,17 +105,17 @@ public static Mesh.Mode getMeshMode(Integer mode) { public static VertexBuffer.Format getVertexBufferFormat(int componentType) { switch (componentType) { - case GL.GL_BYTE: + case GltfConstants.GL_BYTE: return VertexBuffer.Format.Byte; - case GL.GL_UNSIGNED_BYTE: + case GltfConstants.GL_UNSIGNED_BYTE: return VertexBuffer.Format.UnsignedByte; - case GL.GL_SHORT: + case GltfConstants.GL_SHORT: return VertexBuffer.Format.Short; - case GL.GL_UNSIGNED_SHORT: + case GltfConstants.GL_UNSIGNED_SHORT: return VertexBuffer.Format.UnsignedShort; - case GL.GL_UNSIGNED_INT: + case GltfConstants.GL_UNSIGNED_INT: return VertexBuffer.Format.UnsignedInt; - case GL.GL_FLOAT: + case GltfConstants.GL_FLOAT: return VertexBuffer.Format.Float; default: throw new AssetLoadException("Illegal component type: " + componentType); From 618c9fa86c1197b80d6404699d33e8ae12fa9f83 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Thu, 29 Jan 2026 23:03:15 +0100 Subject: [PATCH 07/11] use openize drako fork --- .../src/main/java/jme3test/model/TestGltfLoading.java | 3 --- jme3-plugins/build.gradle | 8 +++++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java b/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java index ff73a6e6c9..0255f7e85a 100644 --- a/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java +++ b/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java @@ -129,7 +129,6 @@ public void simpleInitApp() { // loadModelSample("Avocado", "draco"); - // FIXME: wrong texture coords? // loadModelSample("BarramundiFish", "draco"); // loadModelSample("BoomBox", "draco"); @@ -137,7 +136,6 @@ public void simpleInitApp() { // FIXME: bad skinning? // loadModelSample("BrainStem", "draco"); - // FIXME: wrong offsets? // loadModelSample("CesiumMilkTruck", "draco"); // FIXME: FAILS WITH INDEX OUT OF BOUND EXCEPTION @@ -145,7 +143,6 @@ public void simpleInitApp() { // loadModelSample("Corset", "draco"); - // FIXME: unclear // loadModelSample("Lantern", "draco"); // loadModelSample("MorphPrimitivesTest", "draco"); diff --git a/jme3-plugins/build.gradle b/jme3-plugins/build.gradle index 2d2ca68b9d..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 "dev.fileformat:drako:1.4.2" + implementation "org.jmonkeyengine:drako:1.4.3" implementation project(':jme3-plugins-json') implementation project(':jme3-plugins-json-gson') testRuntimeOnly project(':jme3-desktop') From d810d191f4c8e5343eb0b49e6f317272d9125347 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Sat, 31 Jan 2026 15:51:39 +0100 Subject: [PATCH 08/11] Always minimize visibility. Add comments. --- .../scene/plugins/gltf/GltfConstants.java | 78 +++++++++++++++++-- 1 file changed, 71 insertions(+), 7 deletions(-) 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 index 9daf714cee..e9e4a5e853 100644 --- 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 @@ -1,11 +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; -public class GltfConstants { - public static final int GL_BYTE = 0x1400; - public static final int GL_UNSIGNED_BYTE = 0x1401; - public static final int GL_SHORT = 0x1402; - public static final int GL_UNSIGNED_SHORT = 0x1403; - public static final int GL_UNSIGNED_INT = 0x1405; - public static final int GL_FLOAT = 0x1406; +/** + * 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 + } } From d7dadf42a641aeea59ed35cb07dd6581d2320b10 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Sat, 31 Jan 2026 15:51:49 +0100 Subject: [PATCH 09/11] Minor formatting --- .../com/jme3/scene/plugins/gltf/GltfUtils.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) 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 755229d820..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 @@ -734,13 +734,13 @@ public static Integer getAsInteger(JsonObject parent, String name) { * @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 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); From 079eeace38f2f283a8d9c785100ffed63d72f297 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Sat, 31 Jan 2026 15:53:02 +0100 Subject: [PATCH 10/11] Restore proper formatting --- .../DracoMeshCompressionExtensionLoader.java | 1037 ++++++++--------- 1 file changed, 518 insertions(+), 519 deletions(-) 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 index 97028441ad..f2ceeafb01 100644 --- 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 @@ -76,524 +76,523 @@ */ 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 - * - * @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); - VertexBuffer.Format format = getVertexBufferFormat(componentType); - int numComponents = getAccessorComponentCount(accessor); - vb.setupData(VertexBuffer.Usage.Dynamic, numComponents, format, attributeData); - return vb; - } - - /** - * Create the vertex buffer for the given short 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 createShortAttributeVertexBuffer(JsonObject accessor, - VertexBuffer.Type bufferType, ShortBuffer 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; - } - - /** - * 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; - } + /** + * 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 + * + * @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); + VertexBuffer.Format format = getVertexBufferFormat(componentType); + int numComponents = getAccessorComponentCount(accessor); + vb.setupData(VertexBuffer.Usage.Dynamic, numComponents, format, attributeData); + return vb; + } + + /** + * Create the vertex buffer for the given short 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 createShortAttributeVertexBuffer(JsonObject accessor, + VertexBuffer.Type bufferType, ShortBuffer 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; + } + + /** + * 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; + } } From ecd6c9b041040919528782b395d97ab2c23e8516 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Sat, 31 Jan 2026 15:54:00 +0100 Subject: [PATCH 11/11] Handle quantized/normalized attributes from Draco --- .../plugins/gltf/BufferQuantization.java | 179 ++++++++++++++++++ .../DracoMeshCompressionExtensionLoader.java | 49 ++++- 2 files changed, 223 insertions(+), 5 deletions(-) create mode 100644 jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/BufferQuantization.java 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/DracoMeshCompressionExtensionLoader.java b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/DracoMeshCompressionExtensionLoader.java index f2ceeafb01..2c978da2fe 100644 --- 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 @@ -32,6 +32,7 @@ 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; @@ -530,7 +531,10 @@ private static FloatBuffer readFloatDracoAttribute(PointAttribute pointAttribute } /** - * Create the vertex buffer for the given byte attribute data + * 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 @@ -545,15 +549,34 @@ private static VertexBuffer createByteAttributeVertexBuffer(JsonObject accessor, VertexBuffer.Type bufferType, ByteBuffer 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); + + 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 * @@ -567,9 +590,25 @@ private static VertexBuffer createShortAttributeVertexBuffer(JsonObject accessor VertexBuffer.Type bufferType, ShortBuffer 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); + + 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; }