diff --git a/jme3-core/src/main/java/com/jme3/util/BufferInputStream.java b/jme3-core/src/main/java/com/jme3/util/BufferInputStream.java new file mode 100644 index 0000000000..4a425df3cb --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/util/BufferInputStream.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2009-2025 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.util; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; + +public class BufferInputStream extends InputStream { + + ByteBuffer input; + + public BufferInputStream(ByteBuffer input) { + this.input = input; + } + + @Override + public int read() throws IOException { + if (input.remaining() == 0) return -1; else return input.get() & 0xff; + } + + @Override + public int read(byte[] b) { + return read(b, 0, b.length); + } + + @Override + public int read(byte[] b, int off, int len) { + if (b == null) throw new NullPointerException("b == null"); + if (off < 0 || len < 0 || len > b.length - off) throw new IndexOutOfBoundsException(); + if (len == 0) return 0; + if (!input.hasRemaining()) return -1; + + int toRead = Math.min(len, input.remaining()); + input.get(b, off, toRead); + return toRead; + } + + @Override + public int available() { + return input.remaining(); + } +} diff --git a/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java b/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java index b4ff39d248..4ece681a65 100644 --- a/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java +++ b/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java @@ -31,11 +31,11 @@ */ package jme3test.model; -import com.jme3.anim.AnimClip; 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.input.KeyInput; import com.jme3.input.controls.ActionListener; import com.jme3.input.controls.KeyTrigger; @@ -82,6 +82,7 @@ public void simpleInitApp() { 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); // cam.setLocation(new Vector3f(4.0339394f, 2.645184f, 6.4627485f)); // cam.setRotation(new Quaternion(-0.013950467f, 0.98604023f, -0.119502485f, -0.11510504f)); @@ -152,7 +153,14 @@ public void simpleInitApp() { // 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); + probeNode.attachChild(assets.get(0)); ChaseCameraAppState chaseCam = new ChaseCameraAppState(); @@ -231,7 +239,10 @@ private void loadModel(String path, Vector3f offset, float scale) { private void loadModel(String path, Vector3f offset, Vector3f scale) { GltfModelKey k = new GltfModelKey(path); //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); diff --git a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GlbLoader.java b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GlbLoader.java index 33f5e34680..04456b0361 100644 --- a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GlbLoader.java +++ b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GlbLoader.java @@ -32,9 +32,11 @@ package com.jme3.scene.plugins.gltf; import com.jme3.asset.AssetInfo; +import com.jme3.util.BufferUtils; import com.jme3.util.LittleEndien; import java.io.*; +import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.logging.Level; import java.util.logging.Logger; @@ -50,12 +52,12 @@ public class GlbLoader extends GltfLoader { */ private static final Logger logger = Logger.getLogger(GlbLoader.class.getName()); - private ArrayList data = new ArrayList<>(); + private ArrayList data = new ArrayList<>(); @Override public Object load(AssetInfo assetInfo) throws IOException { data.clear(); - LittleEndien stream = new LittleEndien(new DataInputStream(assetInfo.openStream())); + LittleEndien stream = new LittleEndien(new BufferedInputStream(assetInfo.openStream())); /* magic */ stream.readInt(); int version = stream.readInt(); @@ -76,11 +78,11 @@ public Object load(AssetInfo assetInfo) throws IOException { int chunkType = stream.readInt(); if (chunkType == JSON_TYPE) { json = new byte[chunkLength]; - stream.read(json); + GltfUtils.readToByteArray(stream, json, chunkLength); } else { - byte[] bin = new byte[chunkLength]; - stream.read(bin); - data.add(bin); + ByteBuffer buff = BufferUtils.createByteBuffer(chunkLength); + GltfUtils.readToByteBuffer(stream, buff, chunkLength); + data.add(buff); } //8 is the byte size of the 2 ints chunkLength and chunkType. length -= chunkLength + 8; @@ -93,7 +95,7 @@ public Object load(AssetInfo assetInfo) throws IOException { } @Override - protected byte[] getBytes(int bufferIndex, String uri, Integer bufferLength) throws IOException { + protected ByteBuffer getBytes(int bufferIndex, String uri, Integer bufferLength) throws IOException { return data.get(bufferIndex); } 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 3002515633..0c8448e226 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 @@ -48,11 +48,14 @@ import static com.jme3.scene.plugins.gltf.GltfUtils.*; import com.jme3.texture.Texture; import com.jme3.texture.Texture2D; +import com.jme3.util.BufferInputStream; +import com.jme3.util.BufferUtils; import com.jme3.util.IntMap; import com.jme3.util.mikktspace.MikktspaceTangentGenerator; import java.io.*; import java.net.URLDecoder; import java.nio.Buffer; +import java.nio.ByteBuffer; import java.nio.FloatBuffer; import java.util.*; import java.util.logging.Level; @@ -109,7 +112,6 @@ public Object load(AssetInfo assetInfo) throws IOException { protected Object loadFromStream(AssetInfo assetInfo, InputStream stream) throws IOException { try { - dataCache.clear(); info = assetInfo; skinnedSpatials.clear(); rootNode = new Node(); @@ -181,6 +183,27 @@ protected Object loadFromStream(AssetInfo assetInfo, InputStream stream) throws throw new AssetLoadException("An error occurred loading " + assetInfo.getKey().getName(), e); } finally { stream.close(); + dataCache.clear(); + skinBuffers.clear(); + skinnedSpatials.clear(); + info = null; + docRoot = null; + rootNode = null; + defaultMat = null; + accessors = null; + bufferViews = null; + buffers = null; + scenes = null; + nodes = null; + meshes = null; + materials = null; + textures = null; + images = null; + samplers = null; + animations = null; + skins = null; + cameras = null; + useNormalsFlag = false; } } @@ -553,11 +576,15 @@ public Object readBuffer(Integer bufferViewIndex, int byteOffset, int count, Obj // Not sure it's useful for us, but I guess it's useful when you map data directly to the GPU. // int target = getAsInteger(bufferView, "target", 0); - byte[] data = readData(bufferIndex); + ByteBuffer data = readData(bufferIndex); data = customContentManager.readExtensionAndExtras("bufferView", bufferView, data); + if(!(data instanceof ByteBuffer)){ + throw new IOException("Buffer data is not a NIO Buffer"); + } + if (store == null) { - store = new byte[byteLength]; + store = BufferUtils.createByteBuffer(byteLength); } if (count == -1) { @@ -569,14 +596,40 @@ public Object readBuffer(Integer bufferViewIndex, int byteOffset, int count, Obj return store; } - public byte[] readData(int bufferIndex) throws IOException { + public Buffer viewBuffer(Integer bufferViewIndex, int byteOffset, int count, + int numComponents, VertexBuffer.Format originalFormat, VertexBuffer.Format targetFormat) throws IOException { + JsonObject bufferView = bufferViews.get(bufferViewIndex).getAsJsonObject(); + Integer bufferIndex = getAsInteger(bufferView, "buffer"); + assertNotNull(bufferIndex, "No buffer defined for bufferView " + bufferViewIndex); + int bvByteOffset = getAsInteger(bufferView, "byteOffset", 0); + Integer byteLength = getAsInteger(bufferView, "byteLength"); + assertNotNull(byteLength, "No byte length defined for bufferView " + bufferViewIndex); + int byteStride = getAsInteger(bufferView, "byteStride", 0); + + ByteBuffer data = readData(bufferIndex); + data = customContentManager.readExtensionAndExtras("bufferView", bufferView, data); + + if(!(data instanceof ByteBuffer)){ + throw new IOException("Buffer data is not a NIO Buffer"); + } + + + if (count == -1) { + count = byteLength; + } + + return GltfUtils.getBufferView(data, byteOffset + bvByteOffset, count, byteStride, numComponents, originalFormat, targetFormat ); + + } + + public ByteBuffer readData(int bufferIndex) throws IOException { assertNotNull(buffers, "No buffer defined"); JsonObject buffer = buffers.get(bufferIndex).getAsJsonObject(); String uri = getAsString(buffer, "uri"); Integer bufferLength = getAsInteger(buffer, "byteLength"); assertNotNull(bufferLength, "No byteLength defined for buffer " + bufferIndex); - byte[] data = (byte[]) fetchFromCache("buffers", bufferIndex, Object.class); + ByteBuffer data = (ByteBuffer) fetchFromCache("buffers", bufferIndex, Object.class); if (data != null) { return data; } @@ -588,12 +641,12 @@ public byte[] readData(int bufferIndex) throws IOException { return data; } - protected byte[] getBytes(int bufferIndex, String uri, Integer bufferLength) throws IOException { - byte[] data; + protected ByteBuffer getBytes(int bufferIndex, String uri, Integer bufferLength) throws IOException { + ByteBuffer data; if (uri != null) { if (uri.startsWith("data:")) { // base 64 embed data - data = Base64.getDecoder().decode(uri.substring(uri.indexOf(",") + 1)); + data = BufferUtils.createByteBuffer(Base64.getDecoder().decode(uri.substring(uri.indexOf(",") + 1))); } else { // external file let's load it String decoded = decodeUri(uri); @@ -603,11 +656,11 @@ protected byte[] getBytes(int bufferIndex, String uri, Integer bufferLength) thr } BinDataKey key = new BinDataKey(info.getKey().getFolder() + decoded); - InputStream input = (InputStream) info.getManager().loadAsset(key); - data = new byte[bufferLength]; - try (DataInputStream dataStream = new DataInputStream(input)) { - dataStream.readFully(data); + try(InputStream input = (InputStream) info.getManager().loadAsset(key)){ + data = BufferUtils.createByteBuffer(bufferLength); + GltfUtils.readToByteBuffer(input, data, bufferLength); } + } } else { // no URI, this should not happen in a gltf file, only in glb files. @@ -784,19 +837,23 @@ public Texture2D readImage(int sourceIndex, boolean flip) throws IOException { if (uri == null) { assertNotNull(bufferView, "Image " + sourceIndex + " should either have an uri or a bufferView"); assertNotNull(mimeType, "Image " + sourceIndex + " should have a mimeType"); - byte[] data = (byte[]) readBuffer(bufferView, 0, -1, null, 1, VertexBuffer.Format.Byte); + ByteBuffer data = (ByteBuffer) viewBuffer(bufferView, 0, -1, 1, VertexBuffer.Format.Byte, VertexBuffer.Format.Byte); + String extension = mimeType.split("/")[1]; TextureKey key = new TextureKey("image" + sourceIndex + "." + extension, flip); - result = (Texture2D) info.getManager().loadAssetFromStream(key, new ByteArrayInputStream(data)); - + try(BufferedInputStream bis = new BufferedInputStream(new BufferInputStream(data))){ + result = (Texture2D) info.getManager().loadAssetFromStream(key, bis); + } } else if (uri.startsWith("data:")) { // base64 encoded image String[] uriInfo = uri.split(","); - byte[] data = Base64.getDecoder().decode(uriInfo[1]); + ByteBuffer data = BufferUtils.createByteBuffer(Base64.getDecoder().decode(uriInfo[1])); String headerInfo = uriInfo[0].split(";")[0]; String extension = headerInfo.split("/")[1]; TextureKey key = new TextureKey("image" + sourceIndex + "." + extension, flip); - result = (Texture2D) info.getManager().loadAssetFromStream(key, new ByteArrayInputStream(data)); + try(BufferedInputStream bis = new BufferedInputStream(new BufferInputStream(data))){ + result = (Texture2D) info.getManager().loadAssetFromStream(key, bis); + } } else { // external file image String decoded = decodeUri(uri); @@ -1338,13 +1395,14 @@ public VertexBuffer populate(Integer bufferViewIndex, int componentType, String } int numComponents = getNumberOfComponents(type); - Buffer buff = VertexBuffer.createBuffer(format, numComponents, count); int bufferSize = numComponents * count; + Buffer buff; if (bufferViewIndex == null) { + buff = VertexBuffer.createBuffer(format, numComponents, count); // no referenced buffer, specs says to pad the buffer with zeros. padBuffer(buff, bufferSize); } else { - readBuffer(bufferViewIndex, byteOffset, count, buff, numComponents, originalFormat); + buff = (Buffer) viewBuffer(bufferViewIndex, byteOffset, count, numComponents, originalFormat, format); } if (bufferType == VertexBuffer.Type.Index) { 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 8f015f28db..c1ce5f5e6e 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 @@ -44,6 +44,8 @@ import com.jme3.util.*; import java.io.*; import java.nio.*; +import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; @@ -60,7 +62,11 @@ public class GltfUtils { */ private GltfUtils() { } - + + public static ByteBuffer asReadableByteBuffer(ByteBuffer bbf){ + return bbf.slice().order(ByteOrder.LITTLE_ENDIAN); + } + /** * Parse a json input stream and returns a {@link JsonObject} * @param stream the stream to parse @@ -227,62 +233,68 @@ public static Texture.WrapMode getWrapMode(Integer value) { } } - public static void padBuffer(Object store, int bufferSize) { - if (store instanceof Buffer) { - Buffer buffer = (Buffer) store; - buffer.clear(); - if (buffer instanceof IntBuffer) { - IntBuffer ib = (IntBuffer) buffer; - for (int i = 0; i < bufferSize; i++) { - ib.put(0); - } - } else if (buffer instanceof FloatBuffer) { - FloatBuffer fb = (FloatBuffer) buffer; - for (int i = 0; i < bufferSize; i++) { - fb.put(0); - } - } else if (buffer instanceof ShortBuffer) { - ShortBuffer sb = (ShortBuffer) buffer; - for (int i = 0; i < bufferSize; i++) { - sb.put((short) 0); - } - } else if (buffer instanceof ByteBuffer) { - ByteBuffer bb = (ByteBuffer) buffer; - for (int i = 0; i < bufferSize; i++) { - bb.put((byte) 0); - } - } - buffer.rewind(); - } - if (store instanceof short[]) { - short[] array = (short[]) store; - for (int i = 0; i < array.length; i++) { - array[i] = 0; - } - } else if (store instanceof float[]) { - float[] array = (float[]) store; - for (int i = 0; i < array.length; i++) { - array[i] = 0; + public static void padBuffer(Buffer buffer, int bufferSize) { + buffer.clear(); + if (buffer instanceof IntBuffer) { + IntBuffer ib = (IntBuffer) buffer; + for (int i = 0; i < bufferSize; i++) { + ib.put(0); } - } else if (store instanceof Vector3f[]) { - Vector3f[] array = (Vector3f[]) store; - for (int i = 0; i < array.length; i++) { - array[i] = new Vector3f(); + } else if (buffer instanceof FloatBuffer) { + FloatBuffer fb = (FloatBuffer) buffer; + for (int i = 0; i < bufferSize; i++) { + fb.put(0); } - } else if (store instanceof Quaternion[]) { - Quaternion[] array = (Quaternion[]) store; - for (int i = 0; i < array.length; i++) { - array[i] = new Quaternion(); + } else if (buffer instanceof ShortBuffer) { + ShortBuffer sb = (ShortBuffer) buffer; + for (int i = 0; i < bufferSize; i++) { + sb.put((short) 0); } - } else if (store instanceof Matrix4f[]) { - Matrix4f[] array = (Matrix4f[]) store; - for (int i = 0; i < array.length; i++) { - array[i] = new Matrix4f(); + } else if (buffer instanceof ByteBuffer) { + ByteBuffer bb = (ByteBuffer) buffer; + for (int i = 0; i < bufferSize; i++) { + bb.put((byte) 0); } } + buffer.rewind(); + } + + public static void padBuffer(float[] array, int bufferSize) { + for (int i = 0; i < bufferSize; i++) { + array[i] = 0; + } } - public static void populateBuffer(Object store, byte[] source, int count, int byteOffset, int byteStride, int numComponents, VertexBuffer.Format format) throws IOException { + public static void padBuffer(short[] array, int bufferSize) { + for (int i = 0; i < bufferSize; i++) { + array[i] = 0; + } + } + + public static void padBuffer(Vector3f[] array, int bufferSize) { + for (int i = 0; i < bufferSize; i++) { + array[i] = new Vector3f(); + } + } + + public static void padBuffer(Quaternion[] array, int bufferSize) { + for (int i = 0; i < bufferSize; i++) { + array[i] = new Quaternion(); + } + } + + public static void padBuffer(Matrix4f[] array, int bufferSize) { + for (int i = 0; i < bufferSize; i++) { + array[i] = new Matrix4f(); + } + } + + + + + + public static void populateBuffer(Object store, ByteBuffer source, int count, int byteOffset, int byteStride, int numComponents, VertexBuffer.Format format) throws IOException { + source = asReadableByteBuffer(source); if (store instanceof Buffer) { Buffer buffer = (Buffer) store; @@ -291,34 +303,37 @@ public static void populateBuffer(Object store, byte[] source, int count, int by populateByteBuffer((ByteBuffer) buffer, source, count, byteOffset, byteStride, numComponents, format); return; } - LittleEndien stream = getStream(source); if (buffer instanceof ShortBuffer) { - populateShortBuffer((ShortBuffer) buffer, stream, count, byteOffset, byteStride, numComponents, format); + populateShortBuffer((ShortBuffer) buffer, source, count, byteOffset, byteStride, numComponents, format); } else if (buffer instanceof IntBuffer) { - populateIntBuffer((IntBuffer) buffer, stream, count, byteOffset, byteStride, numComponents, format); + populateIntBuffer((IntBuffer) buffer, source, count, byteOffset, byteStride, numComponents, format); } else if (buffer instanceof FloatBuffer) { - populateFloatBuffer((FloatBuffer) buffer, stream, count, byteOffset, byteStride, numComponents, format); + populateFloatBuffer((FloatBuffer) buffer, source, count, byteOffset, byteStride, numComponents, format); } buffer.rewind(); return; } - LittleEndien stream = getStream(source); + if (store instanceof byte[]) { - populateByteArray((byte[]) store, stream, count, byteOffset, byteStride, numComponents, format); + populateByteArray((byte[]) store, source, count, byteOffset, byteStride, numComponents, format); } else if (store instanceof short[]) { - populateShortArray((short[]) store, stream, count, byteOffset, byteStride, numComponents, format); + populateShortArray((short[]) store, source, count, byteOffset, byteStride, numComponents, format); } else if (store instanceof float[]) { - populateFloatArray((float[]) store, stream, count, byteOffset, byteStride, numComponents, format); + populateFloatArray((float[]) store, source, count, byteOffset, byteStride, numComponents, format); } else if (store instanceof Vector3f[]) { - populateVector3fArray((Vector3f[]) store, stream, count, byteOffset, byteStride, numComponents, format); + populateVector3fArray((Vector3f[]) store, source, count, byteOffset, byteStride, numComponents, format); } else if (store instanceof Quaternion[]) { - populateQuaternionArray((Quaternion[]) store, stream, count, byteOffset, byteStride, numComponents, format); + populateQuaternionArray((Quaternion[]) store, source, count, byteOffset, byteStride, numComponents, format); } else if (store instanceof Matrix4f[]) { - populateMatrix4fArray((Matrix4f[]) store, stream, count, byteOffset, byteStride, numComponents, format); + populateMatrix4fArray((Matrix4f[]) store, source, count, byteOffset, byteStride, numComponents, format); } } - private static void populateByteBuffer(ByteBuffer buffer, byte[] source, int count, int byteOffset, int byteStride, int numComponents, VertexBuffer.Format format) { + private static void skip(ByteBuffer buff, int n) { + buff.position(Math.min(buff.position() + n, buff.limit())); + } + + private static void populateByteBuffer(ByteBuffer buffer, ByteBuffer source, int count, int byteOffset, int byteStride, int numComponents, VertexBuffer.Format format) { int componentSize = format.getComponentSize(); int index = byteOffset; int dataLength = componentSize * numComponents; @@ -326,69 +341,69 @@ private static void populateByteBuffer(ByteBuffer buffer, byte[] source, int cou int end = count * stride + byteOffset; while (index < end) { for (int i = 0; i < numComponents; i++) { - buffer.put(source[index + i]); + buffer.put(source.get(index + i)); } index += stride; } } - private static void populateShortBuffer(ShortBuffer buffer, LittleEndien stream, int count, int byteOffset, int byteStride, int numComponents, VertexBuffer.Format format) throws IOException { + private static void populateShortBuffer(ShortBuffer buffer, ByteBuffer source, int count, int byteOffset, int byteStride, int numComponents, VertexBuffer.Format format) throws IOException { int componentSize = format.getComponentSize(); int index = byteOffset; int dataLength = componentSize * numComponents; int stride = Math.max(dataLength, byteStride); - int end = count * stride + byteOffset; - stream.skipBytes(byteOffset); + int end = count * stride + byteOffset; + source.position(source.position() + byteOffset); while (index < end) { for (int i = 0; i < numComponents; i++) { - buffer.put(stream.readShort()); + buffer.put(source.getShort()); } if (dataLength < stride) { - stream.skipBytes(stride - dataLength); + skip(source, stride - dataLength); } index += stride; } } - private static void populateIntBuffer(IntBuffer buffer, LittleEndien stream, int count, int byteOffset, int byteStride, int numComponents, VertexBuffer.Format format) throws IOException { + private static void populateIntBuffer(IntBuffer buffer, ByteBuffer source, int count, int byteOffset, int byteStride, int numComponents, VertexBuffer.Format format) throws IOException { int componentSize = format.getComponentSize(); int index = byteOffset; int dataLength = componentSize * numComponents; int stride = Math.max(dataLength, byteStride); int end = count * stride + byteOffset; - stream.skipBytes(byteOffset); + source.position(source.position() + byteOffset); while (index < end) { for (int i = 0; i < numComponents; i++) { - buffer.put(stream.readInt()); + buffer.put(source.getInt()); } if (dataLength < stride) { - stream.skipBytes(stride - dataLength); + skip(source, stride - dataLength); } index += stride; } } - private static void populateFloatBuffer(FloatBuffer buffer, LittleEndien stream, int count, int byteOffset, int byteStride, int numComponents, VertexBuffer.Format format) throws IOException { + private static void populateFloatBuffer(FloatBuffer buffer, ByteBuffer source, int count, int byteOffset, int byteStride, int numComponents, VertexBuffer.Format format) throws IOException { int componentSize = format.getComponentSize(); int index = byteOffset; int dataLength = componentSize * numComponents; int stride = Math.max(dataLength, byteStride); int end = count * stride + byteOffset; - stream.skipBytes(byteOffset); + source.position(source.position() + byteOffset); while (index < end) { for (int i = 0; i < numComponents; i++) { - buffer.put(readAsFloat(stream, format)); + buffer.put(readAsFloat(source, format)); } if (dataLength < stride) { - stream.skipBytes(stride - dataLength); + skip(source, stride - dataLength); } index += stride; } } - public static float readAsFloat(LittleEndien stream, VertexBuffer.Format format) throws IOException { + public static float readAsFloat(ByteBuffer source, VertexBuffer.Format format) throws IOException { //We may have packed data so depending on the format, we need to read data differently and unpack it // Implementations must use following equations to get corresponding floating-point value f from a normalized integer c and vise-versa: // accessor.componentType int-to-float float-to-int @@ -399,34 +414,34 @@ public static float readAsFloat(LittleEndien stream, VertexBuffer.Format format) int c; switch (format) { case Byte: - c = stream.readByte(); + c = source.get(); return Math.max(c / 127f, -1f); case UnsignedByte: - c = stream.readUnsignedByte(); + c = source.get() & 0xFF; return c / 255f; case Short: - c = stream.readShort(); + c = source.getShort(); return Math.max(c / 32767f, -1f); - case UnsignedShort: - c = stream.readUnsignedShort(); + case UnsignedShort: + c = source.get() & 0xff | (source.get() & 0xff) << 8; return c / 65535f; default: //we have a regular float - return stream.readFloat(); + return source.getFloat(); } } - private static void populateByteArray(byte[] array, LittleEndien stream, int count, int byteOffset, int byteStride, int numComponents, VertexBuffer.Format format) throws IOException { + private static void populateByteArray(byte[] array, ByteBuffer source, int count, int byteOffset, int byteStride, int numComponents, VertexBuffer.Format format) throws IOException { int componentSize = format.getComponentSize(); int index = byteOffset; int dataLength = componentSize * numComponents; int stride = Math.max(dataLength, byteStride); int end = count * stride + byteOffset; - stream.skipBytes(byteOffset); + source.position(source.position() + byteOffset); if (dataLength == stride) { - read(stream, array, end - index); + read(source, array, end - index); return; } @@ -434,20 +449,21 @@ private static void populateByteArray(byte[] array, LittleEndien stream, int cou int arrayIndex = 0; byte[] buffer = new byte[numComponents]; while (index < end) { - read(stream, buffer, numComponents); + read(source, buffer, numComponents); System.arraycopy(buffer, 0, array, arrayIndex, numComponents); arrayIndex += numComponents; if (dataLength < stride) { - stream.skipBytes(stride - dataLength); + skip(source, stride - dataLength); } index += stride; } } - private static void read(LittleEndien stream, byte[] buffer, int length) throws IOException { + private static void read(ByteBuffer source, byte[] buffer, int length) throws IOException { int n = 0; while (n < length) { - int cnt = stream.read(buffer, n, length - n); + int cnt = Math.min(source.remaining(), length - n); + source.get(buffer, n, cnt); if (cnt < 0) { throw new AssetLoadException("Data ended prematurely"); } @@ -455,25 +471,25 @@ private static void read(LittleEndien stream, byte[] buffer, int length) throws } } - private static void populateShortArray(short[] array, LittleEndien stream, int count, int byteOffset, int byteStride, int numComponents, VertexBuffer.Format format) throws IOException { + private static void populateShortArray(short[] array, ByteBuffer source, int count, int byteOffset, int byteStride, int numComponents, VertexBuffer.Format format) throws IOException { int componentSize = format.getComponentSize(); int index = byteOffset; int dataLength = componentSize * numComponents; int stride = Math.max(dataLength, byteStride); int end = count * stride + byteOffset; - stream.skipBytes(byteOffset); + source.position(source.position() + byteOffset); int arrayIndex = 0; while (index < end) { for (int i = 0; i < numComponents; i++) { if (componentSize == 2) { - array[arrayIndex] = stream.readShort(); + array[arrayIndex] = source.getShort(); } else { - array[arrayIndex] = stream.readByte(); + array[arrayIndex] = source.get(); } arrayIndex++; } if (dataLength < stride) { - stream.skipBytes(stride - dataLength); + skip(source, stride - dataLength); } index += stride; } @@ -557,107 +573,106 @@ public static void setSkinBuffers(Mesh mesh, short[] jointsArray, float[] weight mesh.getBuffer(VertexBuffer.Type.BoneWeight).setUsage(VertexBuffer.Usage.CpuOnly); } - private static void populateFloatArray(float[] array, LittleEndien stream, int count, int byteOffset, int byteStride, int numComponents, VertexBuffer.Format format) throws IOException { + private static void populateFloatArray(float[] array, ByteBuffer source, int count, int byteOffset, int byteStride, int numComponents, VertexBuffer.Format format) throws IOException { int componentSize = format.getComponentSize(); int index = byteOffset; int dataLength = componentSize * numComponents; int stride = Math.max(dataLength, byteStride); int end = count * stride + byteOffset; - stream.skipBytes(byteOffset); + source.position(source.position() + byteOffset); int arrayIndex = 0; while (index < end) { for (int i = 0; i < numComponents; i++) { - array[arrayIndex] = readAsFloat(stream, format); + array[arrayIndex] = readAsFloat(source, format); arrayIndex++; } if (dataLength < stride) { - stream.skipBytes(stride - dataLength); + skip(source, stride - dataLength); } index += stride; } } - private static void populateVector3fArray(Vector3f[] array, LittleEndien stream, int count, int byteOffset, int byteStride, int numComponents, VertexBuffer.Format format) throws IOException { + private static void populateVector3fArray(Vector3f[] array, ByteBuffer source, int count, int byteOffset, int byteStride, int numComponents, VertexBuffer.Format format) throws IOException { int componentSize = format.getComponentSize(); int index = byteOffset; int dataLength = componentSize * numComponents; int stride = Math.max(dataLength, byteStride); int end = count * stride + byteOffset; - stream.skipBytes(byteOffset); + source.position(source.position() + byteOffset); int arrayIndex = 0; while (index < end) { array[arrayIndex] = new Vector3f( - readAsFloat(stream, format), - readAsFloat(stream, format), - readAsFloat(stream, format) + readAsFloat(source, format), + readAsFloat(source, format), + readAsFloat(source, format) ); arrayIndex++; if (dataLength < stride) { - stream.skipBytes(stride - dataLength); + skip(source, stride - dataLength); } - index += stride; } } - private static void populateQuaternionArray(Quaternion[] array, LittleEndien stream, int count, int byteOffset, int byteStride, int numComponents, VertexBuffer.Format format) throws IOException { + private static void populateQuaternionArray(Quaternion[] array, ByteBuffer source, int count, int byteOffset, int byteStride, int numComponents, VertexBuffer.Format format) throws IOException { int componentSize = format.getComponentSize(); int index = byteOffset; int dataLength = componentSize * numComponents; int stride = Math.max(dataLength, byteStride); int end = count * stride + byteOffset; - stream.skipBytes(byteOffset); + source.position(source.position() + byteOffset); int arrayIndex = 0; while (index < end) { array[arrayIndex] = new Quaternion( - readAsFloat(stream, format), - readAsFloat(stream, format), - readAsFloat(stream, format), - readAsFloat(stream, format) + readAsFloat(source, format), + readAsFloat(source, format), + readAsFloat(source, format), + readAsFloat(source, format) ); arrayIndex++; if (dataLength < stride) { - stream.skipBytes(stride - dataLength); + skip(source, stride - dataLength); } index += stride; } } - private static void populateMatrix4fArray(Matrix4f[] array, LittleEndien stream, int count, int byteOffset, int byteStride, int numComponents, VertexBuffer.Format format) throws IOException { + private static void populateMatrix4fArray(Matrix4f[] array, ByteBuffer source, int count, int byteOffset, int byteStride, int numComponents, VertexBuffer.Format format) throws IOException { int componentSize = format.getComponentSize(); int index = byteOffset; int dataLength = componentSize * numComponents; int stride = Math.max(dataLength, byteStride); int end = count * stride + byteOffset; - stream.skipBytes(byteOffset); + source.position(source.position() + byteOffset); int arrayIndex = 0; while (index < end) { array[arrayIndex] = toRowMajor( - readAsFloat(stream, format), - readAsFloat(stream, format), - readAsFloat(stream, format), - readAsFloat(stream, format), - readAsFloat(stream, format), - readAsFloat(stream, format), - readAsFloat(stream, format), - readAsFloat(stream, format), - readAsFloat(stream, format), - readAsFloat(stream, format), - readAsFloat(stream, format), - readAsFloat(stream, format), - readAsFloat(stream, format), - readAsFloat(stream, format), - readAsFloat(stream, format), - readAsFloat(stream, format) + readAsFloat(source, format), + readAsFloat(source, format), + readAsFloat(source, format), + readAsFloat(source, format), + readAsFloat(source, format), + readAsFloat(source, format), + readAsFloat(source, format), + readAsFloat(source, format), + readAsFloat(source, format), + readAsFloat(source, format), + readAsFloat(source, format), + readAsFloat(source, format), + readAsFloat(source, format), + readAsFloat(source, format), + readAsFloat(source, format), + readAsFloat(source, format) ); //gltf matrix are column major, JME ones are row major. arrayIndex++; if (dataLength < stride) { - stream.skipBytes(stride - dataLength); + skip(source, stride - dataLength); } index += stride; @@ -895,4 +910,165 @@ public static void dumpMesh(Mesh m) { System.err.println("\n---------------------------"); } } + + public static void readToByteBuffer(InputStream input, ByteBuffer dst, int bytesToRead) throws IOException { + if (bytesToRead <= 0) throw new IOException("bytesToRead must be > 0"); + + int startPos = dst.position(); + int remaining = dst.limit() - startPos; + if (remaining < bytesToRead) { + throw new IOException("Destination ByteBuffer too small: remaining=" + remaining + " < bytesToRead=" + bytesToRead); + } + + ReadableByteChannel ch = Channels.newChannel(input); + int total = 0; + while (total < bytesToRead) { + int n = ch.read(dst); + if (n == -1) break; + total += n; + } + + if (total < bytesToRead) { + throw new IOException("Data ended prematurely " + total + " < " + bytesToRead); + } + + dst.flip(); + } + + public static void readToByteArray(InputStream input, byte[] dst, int bytesToRead) throws IOException { + if (bytesToRead < 0) throw new IllegalArgumentException("bytesToRead < 0"); + if (bytesToRead > dst.length) { + throw new IOException("Destination array too small: length=" + dst.length + " < bytesToRead=" + bytesToRead); + } + + int totalRead = 0; + while (totalRead < bytesToRead) { + int n = input.read(dst, totalRead, bytesToRead - totalRead); + if (n == -1) break; + totalRead += n; + } + + if (totalRead < bytesToRead) { + throw new IOException("Data ended prematurely " + totalRead + " < " + bytesToRead); + } + } + + + /** + * Try to expose a glTF buffer region as a typed NIO view without copying. + * Falls back to allocating a destination buffer and populating it when + * interleaving, normalization, or format mismatch prevents a pure view. + * + * @param source the original ByteBuffer (direct or heap) + * @param count number of elements + * @param byteOffset start offset within source (relative to beginning) + * @param byteStride stride in bytes (0 means tightly packed = element size) + * @param numComponents components per element (e.g. 3 for VEC3) + * @param originalFormat the source component type + * @param targetFormat the desired buffer view type to return + */ + public static Buffer getBufferView(ByteBuffer source, int byteOffset, int count, int byteStride, + int numComponents, VertexBuffer.Format originalFormat, + VertexBuffer.Format targetFormat) throws IOException { + // Work in little-endian as per glTF spec + source = asReadableByteBuffer(source); + + // Layout from source format + int srcCompSize = originalFormat.getComponentSize(); + int elemSize = srcCompSize * numComponents; + int stride = Math.max(elemSize, byteStride); + int start = byteOffset; + int bytes = stride * count; + + + boolean tightlyPacked = (stride == elemSize); + + if (tightlyPacked) { + ByteBuffer view = source.duplicate(); + view.position(start).limit(start + bytes); + view = view.slice().order(ByteOrder.LITTLE_ENDIAN); + + // Zero-copy returns only when source/target formats are compatible and aligned + switch (targetFormat) { + case Byte: + case UnsignedByte: + if (srcCompSize == 1 && + (originalFormat == VertexBuffer.Format.Byte || + originalFormat == VertexBuffer.Format.UnsignedByte)) { + return view; + } + break; + + case Short: + case UnsignedShort: + if (srcCompSize == 2 && + (originalFormat == VertexBuffer.Format.Short || + originalFormat == VertexBuffer.Format.UnsignedShort) && + (start & 1) == 0) { + return view.asShortBuffer(); + } + break; + + case Int: + case UnsignedInt: + if (srcCompSize == 4 && + (originalFormat == VertexBuffer.Format.Int || + originalFormat == VertexBuffer.Format.UnsignedInt) && + (start & 3) == 0) { + return view.asIntBuffer(); + } + break; + + case Float: + if (srcCompSize == 4 && + originalFormat == VertexBuffer.Format.Float && + (start & 3) == 0) { + return view.asFloatBuffer(); + } + break; + + case Double: + if (srcCompSize == 8 && + originalFormat == VertexBuffer.Format.Double && + (start & 7) == 0) { + return view.asDoubleBuffer(); + } + break; + } + } + + // Fallback: allocate destination buffer by desired targetFormat and populate from source + int elements = count * numComponents; + switch (targetFormat) { + case Byte: + case UnsignedByte: { + ByteBuffer out = BufferUtils.createByteBuffer(elements); + populateBuffer(out, source, count, byteOffset, byteStride, numComponents, originalFormat); + return out; + } + case Short: + case UnsignedShort: { + ShortBuffer out = BufferUtils.createShortBuffer(elements); + populateBuffer(out, source, count, byteOffset, byteStride, numComponents, originalFormat); + return out; + } + case Int: + case UnsignedInt: { + IntBuffer out = BufferUtils.createIntBuffer(elements); + populateBuffer(out, source, count, byteOffset, byteStride, numComponents, originalFormat); + return out; + } + case Float: { + FloatBuffer out = BufferUtils.createFloatBuffer(elements); + populateBuffer(out, source, count, byteOffset, byteStride, numComponents, originalFormat); + return out; + } + case Double: + throw new IllegalArgumentException("Double conversion fallback not supported"); + default: + throw new IllegalArgumentException("Unsupported format " + targetFormat); + } + } + + }