Skip to content

Commit 024bd34

Browse files
committed
improve memory efficiency of gltf importer
1 parent 1f8b5ed commit 024bd34

File tree

5 files changed

+516
-161
lines changed

5 files changed

+516
-161
lines changed
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/**
2+
* Copyright (c) 2025, Nostr Game Engine
3+
*
4+
* Redistribution and use in source and binary forms, with or without
5+
* modification, are permitted provided that the following conditions are met:
6+
*
7+
* 1. Redistributions of source code must retain the above copyright notice, this
8+
* list of conditions and the following disclaimer.
9+
*
10+
* 2. Redistributions in binary form must reproduce the above copyright notice,
11+
* this list of conditions and the following disclaimer in the documentation
12+
* and/or other materials provided with the distribution.
13+
*
14+
* 3. Neither the name of the copyright holder nor the names of its
15+
* contributors may be used to endorse or promote products derived from
16+
* this software without specific prior written permission.
17+
*
18+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21+
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22+
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23+
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24+
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25+
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26+
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28+
*
29+
* Nostr Game Engine is a fork of the jMonkeyEngine, which is licensed under
30+
* the BSD 3-Clause License. The original jMonkeyEngine license is as follows:
31+
*/
32+
package com.jme3.util;
33+
34+
import java.io.IOException;
35+
import java.io.InputStream;
36+
import java.nio.ByteBuffer;
37+
38+
public class BufferInputStream extends InputStream {
39+
40+
ByteBuffer input;
41+
42+
public BufferInputStream(ByteBuffer input) {
43+
this.input = input;
44+
}
45+
46+
@Override
47+
public int read() throws IOException {
48+
if (input.remaining() == 0) return -1; else return input.get() & 0xff;
49+
}
50+
51+
@Override
52+
public int read(byte[] b) {
53+
return read(b, 0, b.length);
54+
}
55+
56+
@Override
57+
public int read(byte[] b, int off, int len) {
58+
if (b == null) throw new NullPointerException("b == null");
59+
if (off < 0 || len < 0 || len > b.length - off) throw new IndexOutOfBoundsException();
60+
if (len == 0) return 0;
61+
if (!input.hasRemaining()) return -1;
62+
63+
int toRead = Math.min(len, input.remaining());
64+
input.get(b, off, toRead);
65+
return toRead;
66+
}
67+
68+
@Override
69+
public int available() {
70+
return input.remaining();
71+
}
72+
}

jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,11 @@
3131
*/
3232
package jme3test.model;
3333

34-
import com.jme3.anim.AnimClip;
3534
import com.jme3.anim.AnimComposer;
3635
import com.jme3.anim.SkinningControl;
3736
import com.jme3.app.*;
3837
import com.jme3.asset.plugins.FileLocator;
38+
import com.jme3.asset.plugins.UrlLocator;
3939
import com.jme3.input.KeyInput;
4040
import com.jme3.input.controls.ActionListener;
4141
import com.jme3.input.controls.KeyTrigger;
@@ -82,6 +82,7 @@ public void simpleInitApp() {
8282

8383
String folder = System.getProperty("user.home");
8484
assetManager.registerLocator(folder, FileLocator.class);
85+
assetManager.registerLocator("https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Assets/refs/heads/main/", UrlLocator.class);
8586

8687
// cam.setLocation(new Vector3f(4.0339394f, 2.645184f, 6.4627485f));
8788
// cam.setRotation(new Quaternion(-0.013950467f, 0.98604023f, -0.119502485f, -0.11510504f));
@@ -152,7 +153,14 @@ public void simpleInitApp() {
152153

153154
// loadModel("Models/gltf/Corset/glTF/Corset.gltf", new Vector3f(0, -1, 0), 20f);
154155
// loadModel("Models/gltf/BoxInterleaved/glTF/BoxInterleaved.gltf", new Vector3f(0, 0, 0), 1f);
155-
156+
157+
// From url locator
158+
159+
// loadModel("Models/AnimatedColorsCube/glTF/AnimatedColorsCube.gltf", new Vector3f(0, 0f, 0), 0.1f);
160+
// loadModel("Models/AntiqueCamera/glTF/AntiqueCamera.gltf", new Vector3f(0, 0, 0), 0.1f);
161+
// loadModel("Models/AnimatedMorphCube/glTF/AnimatedMorphCube.gltf", new Vector3f(0, 0, 0), 0.1f);
162+
// loadModel("Models/AnimatedMorphCube/glTF-Binary/AnimatedMorphCube.glb", new Vector3f(0, 0, 0), 0.1f);
163+
156164
probeNode.attachChild(assets.get(0));
157165

158166
ChaseCameraAppState chaseCam = new ChaseCameraAppState();
@@ -231,7 +239,10 @@ private void loadModel(String path, Vector3f offset, float scale) {
231239
private void loadModel(String path, Vector3f offset, Vector3f scale) {
232240
GltfModelKey k = new GltfModelKey(path);
233241
//k.setKeepSkeletonPose(true);
242+
long t = System.currentTimeMillis();
234243
Spatial s = assetManager.loadModel(k);
244+
System.out.println("Load time : " + (System.currentTimeMillis() - t) + " ms");
245+
235246
s.scale(scale.x, scale.y, scale.z);
236247
s.move(offset);
237248
assets.add(s);

jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GlbLoader.java

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,11 @@
3232
package com.jme3.scene.plugins.gltf;
3333

3434
import com.jme3.asset.AssetInfo;
35+
import com.jme3.util.BufferUtils;
3536
import com.jme3.util.LittleEndien;
3637

3738
import java.io.*;
39+
import java.nio.ByteBuffer;
3840
import java.util.ArrayList;
3941
import java.util.logging.Level;
4042
import java.util.logging.Logger;
@@ -50,12 +52,12 @@ public class GlbLoader extends GltfLoader {
5052
*/
5153
private static final Logger logger = Logger.getLogger(GlbLoader.class.getName());
5254

53-
private ArrayList<byte[]> data = new ArrayList<>();
55+
private ArrayList<ByteBuffer> data = new ArrayList<>();
5456

5557
@Override
5658
public Object load(AssetInfo assetInfo) throws IOException {
5759
data.clear();
58-
LittleEndien stream = new LittleEndien(new DataInputStream(assetInfo.openStream()));
60+
LittleEndien stream = new LittleEndien(new BufferedInputStream(assetInfo.openStream()));
5961
/* magic */ stream.readInt();
6062

6163
int version = stream.readInt();
@@ -76,11 +78,11 @@ public Object load(AssetInfo assetInfo) throws IOException {
7678
int chunkType = stream.readInt();
7779
if (chunkType == JSON_TYPE) {
7880
json = new byte[chunkLength];
79-
stream.read(json);
81+
GltfUtils.readToByteArray(stream, json, chunkLength, -1);
8082
} else {
81-
byte[] bin = new byte[chunkLength];
82-
stream.read(bin);
83-
data.add(bin);
83+
ByteBuffer buff = BufferUtils.createByteBuffer(chunkLength);
84+
GltfUtils.readToByteBuffer(stream, buff, chunkLength, -1);
85+
data.add(buff);
8486
}
8587
//8 is the byte size of the 2 ints chunkLength and chunkType.
8688
length -= chunkLength + 8;
@@ -93,7 +95,7 @@ public Object load(AssetInfo assetInfo) throws IOException {
9395
}
9496

9597
@Override
96-
protected byte[] getBytes(int bufferIndex, String uri, Integer bufferLength) throws IOException {
98+
protected ByteBuffer getBytes(int bufferIndex, String uri, Integer bufferLength) throws IOException {
9799
return data.get(bufferIndex);
98100
}
99101

jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfLoader.java

Lines changed: 79 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,14 @@
4848
import static com.jme3.scene.plugins.gltf.GltfUtils.*;
4949
import com.jme3.texture.Texture;
5050
import com.jme3.texture.Texture2D;
51+
import com.jme3.util.BufferInputStream;
52+
import com.jme3.util.BufferUtils;
5153
import com.jme3.util.IntMap;
5254
import com.jme3.util.mikktspace.MikktspaceTangentGenerator;
5355
import java.io.*;
5456
import java.net.URLDecoder;
5557
import java.nio.Buffer;
58+
import java.nio.ByteBuffer;
5659
import java.nio.FloatBuffer;
5760
import java.util.*;
5861
import java.util.logging.Level;
@@ -109,7 +112,6 @@ public Object load(AssetInfo assetInfo) throws IOException {
109112

110113
protected Object loadFromStream(AssetInfo assetInfo, InputStream stream) throws IOException {
111114
try {
112-
dataCache.clear();
113115
info = assetInfo;
114116
skinnedSpatials.clear();
115117
rootNode = new Node();
@@ -181,6 +183,27 @@ protected Object loadFromStream(AssetInfo assetInfo, InputStream stream) throws
181183
throw new AssetLoadException("An error occurred loading " + assetInfo.getKey().getName(), e);
182184
} finally {
183185
stream.close();
186+
dataCache.clear();
187+
skinBuffers.clear();
188+
skinnedSpatials.clear();
189+
info = null;
190+
docRoot = null;
191+
rootNode = null;
192+
defaultMat = null;
193+
accessors = null;
194+
bufferViews = null;
195+
buffers = null;
196+
scenes = null;
197+
nodes = null;
198+
meshes = null;
199+
materials = null;
200+
textures = null;
201+
images = null;
202+
samplers = null;
203+
animations = null;
204+
skins = null;
205+
cameras = null;
206+
useNormalsFlag = false;
184207
}
185208
}
186209

@@ -553,11 +576,15 @@ public Object readBuffer(Integer bufferViewIndex, int byteOffset, int count, Obj
553576
// Not sure it's useful for us, but I guess it's useful when you map data directly to the GPU.
554577
// int target = getAsInteger(bufferView, "target", 0);
555578

556-
byte[] data = readData(bufferIndex);
579+
ByteBuffer data = readData(bufferIndex);
557580
data = customContentManager.readExtensionAndExtras("bufferView", bufferView, data);
558581

582+
if(!(data instanceof ByteBuffer)){
583+
throw new IOException("Buffer data is not a NIO Buffer");
584+
}
585+
559586
if (store == null) {
560-
store = new byte[byteLength];
587+
store = BufferUtils.createByteBuffer(byteLength);
561588
}
562589

563590
if (count == -1) {
@@ -569,14 +596,40 @@ public Object readBuffer(Integer bufferViewIndex, int byteOffset, int count, Obj
569596
return store;
570597
}
571598

572-
public byte[] readData(int bufferIndex) throws IOException {
599+
public Buffer viewBuffer(Integer bufferViewIndex, int byteOffset, int count,
600+
int numComponents, VertexBuffer.Format originalFormat, VertexBuffer.Format targetFormat) throws IOException {
601+
JsonObject bufferView = bufferViews.get(bufferViewIndex).getAsJsonObject();
602+
Integer bufferIndex = getAsInteger(bufferView, "buffer");
603+
assertNotNull(bufferIndex, "No buffer defined for bufferView " + bufferViewIndex);
604+
int bvByteOffset = getAsInteger(bufferView, "byteOffset", 0);
605+
Integer byteLength = getAsInteger(bufferView, "byteLength");
606+
assertNotNull(byteLength, "No byte length defined for bufferView " + bufferViewIndex);
607+
int byteStride = getAsInteger(bufferView, "byteStride", 0);
608+
609+
ByteBuffer data = readData(bufferIndex);
610+
data = customContentManager.readExtensionAndExtras("bufferView", bufferView, data);
611+
612+
if(!(data instanceof ByteBuffer)){
613+
throw new IOException("Buffer data is not a NIO Buffer");
614+
}
615+
616+
617+
if (count == -1) {
618+
count = byteLength;
619+
}
620+
621+
return GltfUtils.getBufferView(data, byteOffset + bvByteOffset, count, byteStride, numComponents, originalFormat, targetFormat );
622+
623+
}
624+
625+
public ByteBuffer readData(int bufferIndex) throws IOException {
573626
assertNotNull(buffers, "No buffer defined");
574627

575628
JsonObject buffer = buffers.get(bufferIndex).getAsJsonObject();
576629
String uri = getAsString(buffer, "uri");
577630
Integer bufferLength = getAsInteger(buffer, "byteLength");
578631
assertNotNull(bufferLength, "No byteLength defined for buffer " + bufferIndex);
579-
byte[] data = (byte[]) fetchFromCache("buffers", bufferIndex, Object.class);
632+
ByteBuffer data = (ByteBuffer) fetchFromCache("buffers", bufferIndex, Object.class);
580633
if (data != null) {
581634
return data;
582635
}
@@ -588,12 +641,12 @@ public byte[] readData(int bufferIndex) throws IOException {
588641
return data;
589642
}
590643

591-
protected byte[] getBytes(int bufferIndex, String uri, Integer bufferLength) throws IOException {
592-
byte[] data;
644+
protected ByteBuffer getBytes(int bufferIndex, String uri, Integer bufferLength) throws IOException {
645+
ByteBuffer data;
593646
if (uri != null) {
594647
if (uri.startsWith("data:")) {
595648
// base 64 embed data
596-
data = Base64.getDecoder().decode(uri.substring(uri.indexOf(",") + 1));
649+
data = BufferUtils.createByteBuffer(Base64.getDecoder().decode(uri.substring(uri.indexOf(",") + 1)));
597650
} else {
598651
// external file let's load it
599652
String decoded = decodeUri(uri);
@@ -603,11 +656,11 @@ protected byte[] getBytes(int bufferIndex, String uri, Integer bufferLength) thr
603656
}
604657

605658
BinDataKey key = new BinDataKey(info.getKey().getFolder() + decoded);
606-
InputStream input = (InputStream) info.getManager().loadAsset(key);
607-
data = new byte[bufferLength];
608-
try (DataInputStream dataStream = new DataInputStream(input)) {
609-
dataStream.readFully(data);
659+
try(InputStream input = (InputStream) info.getManager().loadAsset(key)){
660+
data = BufferUtils.createByteBuffer(bufferLength);
661+
GltfUtils.readToByteBuffer(input, data, bufferLength, -1);
610662
}
663+
611664
}
612665
} else {
613666
// 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 {
784837
if (uri == null) {
785838
assertNotNull(bufferView, "Image " + sourceIndex + " should either have an uri or a bufferView");
786839
assertNotNull(mimeType, "Image " + sourceIndex + " should have a mimeType");
787-
byte[] data = (byte[]) readBuffer(bufferView, 0, -1, null, 1, VertexBuffer.Format.Byte);
840+
ByteBuffer data = (ByteBuffer) viewBuffer(bufferView, 0, -1, 1, VertexBuffer.Format.Byte, VertexBuffer.Format.Byte);
841+
788842
String extension = mimeType.split("/")[1];
789843
TextureKey key = new TextureKey("image" + sourceIndex + "." + extension, flip);
790-
result = (Texture2D) info.getManager().loadAssetFromStream(key, new ByteArrayInputStream(data));
791-
844+
try(BufferedInputStream bis = new BufferedInputStream(new BufferInputStream(data))){
845+
result = (Texture2D) info.getManager().loadAssetFromStream(key, bis);
846+
}
792847
} else if (uri.startsWith("data:")) {
793848
// base64 encoded image
794849
String[] uriInfo = uri.split(",");
795-
byte[] data = Base64.getDecoder().decode(uriInfo[1]);
850+
ByteBuffer data = BufferUtils.createByteBuffer(Base64.getDecoder().decode(uriInfo[1]));
796851
String headerInfo = uriInfo[0].split(";")[0];
797852
String extension = headerInfo.split("/")[1];
798853
TextureKey key = new TextureKey("image" + sourceIndex + "." + extension, flip);
799-
result = (Texture2D) info.getManager().loadAssetFromStream(key, new ByteArrayInputStream(data));
854+
try(BufferedInputStream bis = new BufferedInputStream(new BufferInputStream(data))){
855+
result = (Texture2D) info.getManager().loadAssetFromStream(key, bis);
856+
}
800857
} else {
801858
// external file image
802859
String decoded = decodeUri(uri);
@@ -1338,13 +1395,16 @@ public VertexBuffer populate(Integer bufferViewIndex, int componentType, String
13381395
}
13391396
int numComponents = getNumberOfComponents(type);
13401397

1341-
Buffer buff = VertexBuffer.createBuffer(format, numComponents, count);
13421398
int bufferSize = numComponents * count;
1399+
Buffer buff;
13431400
if (bufferViewIndex == null) {
1401+
buff = VertexBuffer.createBuffer(format, numComponents, count);
13441402
// no referenced buffer, specs says to pad the buffer with zeros.
13451403
padBuffer(buff, bufferSize);
13461404
} else {
1347-
readBuffer(bufferViewIndex, byteOffset, count, buff, numComponents, originalFormat);
1405+
// buff = VertexBuffer.createBuffer(format, numComponents, count);
1406+
// buff = (Buffer) readBuffer(bufferViewIndex, byteOffset, count, buff, numComponents, originalFormat);
1407+
buff = (Buffer) viewBuffer(bufferViewIndex, byteOffset, count, numComponents, originalFormat, format);
13481408
}
13491409

13501410
if (bufferType == VertexBuffer.Type.Index) {

0 commit comments

Comments
 (0)