-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Expand file tree
/
Copy pathGltfLoader.java
More file actions
1466 lines (1283 loc) · 62.7 KB
/
GltfLoader.java
File metadata and controls
1466 lines (1283 loc) · 62.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
* Copyright (c) 2009-2023 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 com.google.gson.*;
import com.google.gson.stream.JsonReader;
import com.jme3.anim.*;
import com.jme3.asset.*;
import com.jme3.material.Material;
import com.jme3.material.RenderState;
import com.jme3.math.*;
import com.jme3.renderer.Camera;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.*;
import com.jme3.scene.control.CameraControl;
import com.jme3.scene.mesh.MorphTarget;
import static com.jme3.scene.plugins.gltf.GltfUtils.*;
import com.jme3.texture.Texture;
import com.jme3.texture.Texture2D;
import com.jme3.util.IntMap;
import com.jme3.util.mikktspace.MikktspaceTangentGenerator;
import java.io.*;
import java.net.URLDecoder;
import java.nio.Buffer;
import java.nio.FloatBuffer;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* GLTF 2.0 loader
* Created by Nehon on 07/08/2017.
*/
public class GltfLoader implements AssetLoader {
private static final Logger logger = Logger.getLogger(GltfLoader.class.getName());
// Data cache for already parsed JME objects
private final Map<String, Object[]> dataCache = new HashMap<>();
private JsonArray scenes;
private JsonArray nodes;
private JsonArray meshes;
private JsonArray accessors;
private JsonArray bufferViews;
private JsonArray buffers;
private JsonArray materials;
private JsonArray textures;
private JsonArray images;
private JsonArray samplers;
private JsonArray animations;
private JsonArray skins;
private JsonArray cameras;
private Material defaultMat;
private AssetInfo info;
private JsonObject docRoot;
private Node rootNode;
private final FloatArrayPopulator floatArrayPopulator = new FloatArrayPopulator();
private final Vector3fArrayPopulator vector3fArrayPopulator = new Vector3fArrayPopulator();
private final QuaternionArrayPopulator quaternionArrayPopulator = new QuaternionArrayPopulator();
private final Matrix4fArrayPopulator matrix4fArrayPopulator = new Matrix4fArrayPopulator();
private final Map<String, MaterialAdapter> defaultMaterialAdapters = new HashMap<>();
private final CustomContentManager customContentManager = new CustomContentManager();
private boolean useNormalsFlag = false;
Map<SkinData, List<Spatial>> skinnedSpatials = new HashMap<>();
IntMap<SkinBuffers> skinBuffers = new IntMap<>();
public GltfLoader() {
defaultMaterialAdapters.put("pbrMetallicRoughness", new PBRMetalRoughMaterialAdapter());
}
@Override
public Object load(AssetInfo assetInfo) throws IOException {
return loadFromStream(assetInfo, assetInfo.openStream());
}
protected Object loadFromStream(AssetInfo assetInfo, InputStream stream) throws IOException {
try {
dataCache.clear();
info = assetInfo;
skinnedSpatials.clear();
rootNode = new Node();
if (defaultMat == null) {
defaultMat = new Material(assetInfo.getManager(), "Common/MatDefs/Light/PBRLighting.j3md");
defaultMat.setColor("BaseColor", ColorRGBA.White);
defaultMat.setFloat("Metallic", 0f);
defaultMat.setFloat("Roughness", 1f);
}
docRoot = JsonParser.parseReader(new JsonReader(new InputStreamReader(stream))).getAsJsonObject();
JsonObject asset = docRoot.getAsJsonObject().get("asset").getAsJsonObject();
getAsString(asset, "generator");
String version = getAsString(asset, "version");
String minVersion = getAsString(asset, "minVersion");
if (!isSupported(version, minVersion)) {
logger.log(Level.SEVERE, "Gltf Loader doesn''t support this gltf version: {0}{1}", new Object[]{version, minVersion != null ? ("/" + minVersion) : ""});
}
scenes = docRoot.getAsJsonArray("scenes");
nodes = docRoot.getAsJsonArray("nodes");
meshes = docRoot.getAsJsonArray("meshes");
accessors = docRoot.getAsJsonArray("accessors");
bufferViews = docRoot.getAsJsonArray("bufferViews");
buffers = docRoot.getAsJsonArray("buffers");
materials = docRoot.getAsJsonArray("materials");
textures = docRoot.getAsJsonArray("textures");
images = docRoot.getAsJsonArray("images");
samplers = docRoot.getAsJsonArray("samplers");
animations = docRoot.getAsJsonArray("animations");
skins = docRoot.getAsJsonArray("skins");
cameras = docRoot.getAsJsonArray("cameras");
customContentManager.init(this);
readSkins();
readCameras();
JsonPrimitive defaultScene = docRoot.getAsJsonPrimitive("scene");
readScenes(defaultScene, rootNode);
rootNode = customContentManager.readExtensionAndExtras("root", docRoot, rootNode);
// Loading animations
if (animations != null) {
for (int i = 0; i < animations.size(); i++) {
readAnimation(i);
}
}
setupControls();
// only one scene let's not return the root.
if (rootNode.getChildren().size() == 1) {
Node child = (Node) rootNode.getChild(0);
// Migrate lights that were in the parent to the child.
rootNode.getLocalLightList().forEach(child::addLight);
rootNode = child;
}
// no name for the scene... let's set the file name.
if (rootNode.getName() == null) {
rootNode.setName(assetInfo.getKey().getName());
}
return rootNode;
} catch (Exception e) {
throw new AssetLoadException("An error occurred loading " + assetInfo.getKey().getName(), e);
} finally {
stream.close();
}
}
private void setDefaultParams(Material mat) {
mat.setColor("BaseColor", ColorRGBA.White);
mat.setFloat("Metallic", 0f);
mat.setFloat("Roughness", 1f);
}
private boolean isSupported(String version, String minVersion) {
return "2.0".equals(version);
}
public void readScenes(JsonPrimitive defaultScene, Node rootNode) throws IOException {
if (scenes == null) {
// no scene... lets handle this later...
throw new AssetLoadException("Gltf files with no scene is not yet supported");
}
for (JsonElement scene : scenes) {
Node sceneNode = new Node();
// Specs say that only the default scene should be rendered.
// If there are several scenes, they are attached to the rootScene, but they are culled.
sceneNode.setCullHint(Spatial.CullHint.Always);
sceneNode.setName(getAsString(scene.getAsJsonObject(), "name"));
// If the scene is empty, ignore it.
if (!scene.getAsJsonObject().has("nodes")) {
continue;
}
JsonArray sceneNodes = scene.getAsJsonObject().getAsJsonArray("nodes");
sceneNode = customContentManager.readExtensionAndExtras("scene", scene, sceneNode);
rootNode.attachChild(sceneNode);
for (JsonElement node : sceneNodes) {
readChild(sceneNode, node);
}
}
// Setting the default scene cul hint to inherit.
int activeChild = 0;
if (defaultScene != null) {
activeChild = defaultScene.getAsInt();
}
rootNode.getChild(activeChild).setCullHint(Spatial.CullHint.Inherit);
}
public Object readNode(int nodeIndex) throws IOException {
Object obj = fetchFromCache("nodes", nodeIndex, Object.class);
if (obj != null) {
if (obj instanceof JointWrapper) {
// the node can be a previously loaded bone let's return it
return obj;
} else {
// If a spatial is referenced several times, it may be attached to different parents,
// and it's not possible in JME, so we have to clone it.
return ((Spatial) obj).clone();
}
}
Spatial spatial;
JsonObject nodeData = nodes.get(nodeIndex).getAsJsonObject();
JsonArray children = nodeData.getAsJsonArray("children");
Integer meshIndex = getAsInteger(nodeData, "mesh");
if (meshIndex != null) {
assertNotNull(meshes, "Can't find any mesh data, yet a node references a mesh");
// there is a mesh in this node, however gltf
// can split meshes in primitives (some kind of sub meshes),
// We don't have this in JME, so we have to make one Mesh and one Geometry for each primitive.
Geometry[] primitives = readMeshPrimitives(meshIndex);
if (primitives.length == 1 && children == null) {
// only one geometry, let's not wrap it in another node unless the node has children.
spatial = primitives[0];
} else {
// several geometries, let's make a parent Node and attach them to it
Node node = new Node();
for (Geometry primitive : primitives) {
node.attachChild(primitive);
}
spatial = node;
}
spatial.setName(readMeshName(meshIndex));
} else {
// no mesh, we have a node. Can be a camera node or a regular node.
Integer camIndex = getAsInteger(nodeData, "camera");
if (camIndex != null) {
Camera cam = fetchFromCache("cameras", camIndex, Camera.class);
CameraNode node = new CameraNode(null, cam);
node.setControlDir(CameraControl.ControlDirection.SpatialToCamera);
spatial = node;
} else {
Node node = new Node();
spatial = node;
}
}
Integer skinIndex = getAsInteger(nodeData, "skin");
if (skinIndex != null) {
SkinData skinData = fetchFromCache("skins", skinIndex, SkinData.class);
if (skinData != null) {
List<Spatial> spatials = skinnedSpatials.get(skinData);
spatials.add(spatial);
skinData.used = true;
}
}
spatial.setLocalTransform(readTransforms(nodeData));
if (spatial.getName() == null) {
spatial.setName(getAsString(nodeData.getAsJsonObject(), "name"));
}
spatial = customContentManager.readExtensionAndExtras("node", nodeData, spatial);
addToCache("nodes", nodeIndex, spatial, nodes.size());
return spatial;
}
private void readChild(Spatial parent, JsonElement nodeIndex) throws IOException {
Object loaded = readNode(nodeIndex.getAsInt());
if (loaded instanceof Spatial) {
Spatial spatial = ((Spatial) loaded);
((Node) parent).attachChild(spatial);
JsonObject nodeElem = nodes.get(nodeIndex.getAsInt()).getAsJsonObject();
JsonArray children = nodeElem.getAsJsonArray("children");
if (children != null) {
for (JsonElement child : children) {
readChild(spatial, child);
}
}
} else if (loaded instanceof JointWrapper) {
// parent is the Armature Node, we have to apply its transforms to the root bone's animation data
JointWrapper bw = (JointWrapper) loaded;
bw.isRoot = true;
SkinData skinData = fetchFromCache("skins", bw.skinIndex, SkinData.class);
if (skinData == null) {
return;
}
skinData.parent = parent;
}
}
public Transform readTransforms(JsonObject nodeData) {
Transform transform = new Transform();
JsonArray matrix = nodeData.getAsJsonArray("matrix");
if (matrix != null) {
// transforms are given as a mat4
float[] tmpArray = new float[16];
for (int i = 0; i < tmpArray.length; i++) {
tmpArray[i] = matrix.get(i).getAsFloat();
}
// creates a row major matrix from color major data
Matrix4f mat = new Matrix4f(tmpArray);
transform.fromTransformMatrix(mat);
return transform;
}
// no matrix transforms: no transforms or transforms givens as translation/rotation/scale
JsonArray translation = nodeData.getAsJsonArray("translation");
if (translation != null) {
transform.setTranslation(
translation.get(0).getAsFloat(),
translation.get(1).getAsFloat(),
translation.get(2).getAsFloat());
}
JsonArray rotation = nodeData.getAsJsonArray("rotation");
if (rotation != null) {
transform.setRotation(new Quaternion(
rotation.get(0).getAsFloat(),
rotation.get(1).getAsFloat(),
rotation.get(2).getAsFloat(),
rotation.get(3).getAsFloat()));
}
JsonArray scale = nodeData.getAsJsonArray("scale");
if (scale != null) {
transform.setScale(
scale.get(0).getAsFloat(),
scale.get(1).getAsFloat(),
scale.get(2).getAsFloat());
}
return transform;
}
public Geometry[] readMeshPrimitives(int meshIndex) throws IOException {
Geometry[] geomArray = (Geometry[]) fetchFromCache("meshes", meshIndex, Object.class);
if (geomArray != null) {
// cloning the geoms.
Geometry[] geoms = new Geometry[geomArray.length];
for (int i = 0; i < geoms.length; i++) {
geoms[i] = geomArray[i].clone(false);
}
return geoms;
}
JsonObject meshData = meshes.get(meshIndex).getAsJsonObject();
JsonArray primitives = meshData.getAsJsonArray("primitives");
assertNotNull(primitives, "Can't find any primitives in mesh " + meshIndex);
String name = getAsString(meshData, "name");
geomArray = new Geometry[primitives.size()];
int index = 0;
for (JsonElement primitive : primitives) {
JsonObject meshObject = primitive.getAsJsonObject();
Mesh mesh = new Mesh();
addToCache("mesh", 0, mesh, 1);
Integer mode = getAsInteger(meshObject, "mode");
mesh.setMode(getMeshMode(mode));
Integer indices = getAsInteger(meshObject, "indices");
if (indices != null) {
mesh.setBuffer(readAccessorData(indices, new VertexBufferPopulator(VertexBuffer.Type.Index)));
}
JsonObject attributes = meshObject.getAsJsonObject("attributes");
assertNotNull(attributes, "No attributes defined for mesh " + mesh);
skinBuffers.clear();
for (Map.Entry<String, JsonElement> entry : attributes.entrySet()) {
// special case for joints and weights buffer.
// If there are more than 4 bones per vertex, there might be several of them
// we need to read them all and to keep only the 4 that have the most weight on the vertex.
String bufferType = entry.getKey();
if (bufferType.startsWith("JOINTS")) {
SkinBuffers buffs = getSkinBuffers(bufferType);
SkinBuffers buffer
= readAccessorData(entry.getValue().getAsInt(), new JointArrayPopulator());
buffs.joints = buffer.joints;
buffs.componentSize = buffer.componentSize;
} else if (bufferType.startsWith("WEIGHTS")) {
SkinBuffers buffs = getSkinBuffers(bufferType);
buffs.weights = readAccessorData(entry.getValue().getAsInt(), new FloatArrayPopulator());
} else {
VertexBuffer vb = readAccessorData(entry.getValue().getAsInt(),
new VertexBufferPopulator(getVertexBufferType(bufferType)));
if (vb != null) {
mesh.setBuffer(vb);
}
}
}
handleSkinningBuffers(mesh, skinBuffers);
if (mesh.getBuffer(VertexBuffer.Type.BoneIndex) != null) {
// the mesh has some skinning let's create needed buffers for HW skinning
// creating empty buffers for HW skinning
// the buffers will be setup if ever used.
VertexBuffer weightsHW = new VertexBuffer(VertexBuffer.Type.HWBoneWeight);
VertexBuffer indicesHW = new VertexBuffer(VertexBuffer.Type.HWBoneIndex);
// setting usage to cpuOnly so that the buffer is not sent empty to the GPU
indicesHW.setUsage(VertexBuffer.Usage.CpuOnly);
weightsHW.setUsage(VertexBuffer.Usage.CpuOnly);
mesh.setBuffer(weightsHW);
mesh.setBuffer(indicesHW);
mesh.generateBindPose();
}
// Read morph target names
LinkedList<String> targetNames = new LinkedList<>();
if (meshData.has("extras") && meshData.getAsJsonObject("extras").has("targetNames")) {
JsonArray targetNamesJson = meshData.getAsJsonObject("extras").getAsJsonArray("targetNames");
for (JsonElement target : targetNamesJson) {
targetNames.add(target.getAsString());
}
}
// Read morph targets
JsonArray targets = meshObject.getAsJsonArray("targets");
if (targets != null) {
for (JsonElement target : targets) {
MorphTarget morphTarget = new MorphTarget();
if (targetNames.size() > 0) {
morphTarget.setName(targetNames.pop());
}
for (Map.Entry<String, JsonElement> entry : target.getAsJsonObject().entrySet()) {
String bufferType = entry.getKey();
VertexBuffer.Type type = getVertexBufferType(bufferType);
VertexBuffer vb = readAccessorData(entry.getValue().getAsInt(),
new VertexBufferPopulator(type));
if (vb != null) {
morphTarget.setBuffer(type, (FloatBuffer) vb.getData());
}
}
mesh.addMorphTarget(morphTarget);
}
}
// Read mesh extras
mesh = customContentManager.readExtensionAndExtras("primitive", meshObject, mesh);
Geometry geom = new Geometry(null, mesh);
Integer materialIndex = getAsInteger(meshObject, "material");
if (materialIndex == null) {
geom.setMaterial(defaultMat);
} else {
useNormalsFlag = false;
geom.setMaterial(readMaterial(materialIndex));
if (geom.getMaterial().getAdditionalRenderState().getBlendMode()
== RenderState.BlendMode.Alpha) {
// Alpha blending is enabled for this material. Let's place the geom in the transparent bucket.
geom.setQueueBucket(RenderQueue.Bucket.Transparent);
}
if (useNormalsFlag && mesh.getBuffer(VertexBuffer.Type.Tangent) == null) {
// No tangent buffer, but there is a normal map, we have to generate them using MiiktSpace
MikktspaceTangentGenerator.generate(geom);
}
}
if (name != null) {
geom.setName(name + (primitives.size() > 1 ? ("_" + index) : ""));
}
geom.updateModelBound();
geomArray[index] = geom;
index++;
}
geomArray = customContentManager.readExtensionAndExtras("mesh", meshData, geomArray);
addToCache("meshes", meshIndex, geomArray, meshes.size());
return geomArray;
}
private SkinBuffers getSkinBuffers(String bufferType) {
int bufIndex = getIndex(bufferType);
SkinBuffers buffs = skinBuffers.get(bufIndex);
if (buffs == null) {
buffs = new SkinBuffers();
skinBuffers.put(bufIndex, buffs);
}
return buffs;
}
private <R> R readAccessorData(int accessorIndex, Populator<R> populator) throws IOException {
assertNotNull(accessors, "No accessor attribute in the gltf file");
JsonObject accessor = accessors.get(accessorIndex).getAsJsonObject();
Integer bufferViewIndex = getAsInteger(accessor, "bufferView");
int byteOffset = getAsInteger(accessor, "byteOffset", 0);
Integer componentType = getAsInteger(accessor, "componentType");
assertNotNull(componentType, "No component type defined for accessor " + accessorIndex);
Integer count = getAsInteger(accessor, "count");
assertNotNull(count, "No count attribute defined for accessor " + accessorIndex);
String type = getAsString(accessor, "type");
assertNotNull(type, "No type attribute defined for accessor " + accessorIndex);
boolean normalized = getAsBoolean(accessor, "normalized", false);
// TODO min / max...don't know what to do about them.
// TODO sparse
R data = populator.populate(bufferViewIndex, componentType, type, count, byteOffset, normalized);
data = customContentManager.readExtensionAndExtras("accessor", accessor, data);
return data;
}
public Object readBuffer(Integer bufferViewIndex, int byteOffset, int count, Object store,
int numComponents, VertexBuffer.Format format) 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);
// target defines ELEMENT_ARRAY_BUFFER or ARRAY_BUFFER,
// but we already know that since we know we load the index buffer or any other...
// 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);
data = customContentManager.readExtensionAndExtras("bufferView", bufferView, data);
if (store == null) {
store = new byte[byteLength];
}
if (count == -1) {
count = byteLength;
}
populateBuffer(store, data, count, byteOffset + bvByteOffset, byteStride, numComponents, format);
return store;
}
public byte[] 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);
if (data != null) {
return data;
}
data = getBytes(bufferIndex, uri, bufferLength);
data = customContentManager.readExtensionAndExtras("buffer", buffer, data);
addToCache("buffers", bufferIndex, data, buffers.size());
return data;
}
protected byte[] getBytes(int bufferIndex, String uri, Integer bufferLength) throws IOException {
byte[] data;
if (uri != null) {
if (uri.startsWith("data:")) {
// base 64 embed data
data = Base64.getDecoder().decode(uri.substring(uri.indexOf(",") + 1));
} else {
// external file let's load it
String decoded = decodeUri(uri);
if (!decoded.endsWith(".bin")) {
throw new AssetLoadException(
"Cannot load " + decoded + ", a .bin extension is required.");
}
BinDataKey key = new BinDataKey(info.getKey().getFolder() + decoded);
InputStream input = (InputStream) info.getManager().loadAsset(key);
data = new byte[bufferLength];
DataInputStream dataStream = new DataInputStream(input);
dataStream.readFully(data);
dataStream.close();
}
} else {
// no URI this should not happen in a gltf file, only in glb files.
throw new AssetLoadException("Buffer " + bufferIndex + " has no uri");
}
return data;
}
public Material readMaterial(int materialIndex) throws IOException {
assertNotNull(materials, "There is no material defined yet a mesh references one");
JsonObject matData = materials.get(materialIndex).getAsJsonObject();
JsonObject pbrMat = matData.getAsJsonObject("pbrMetallicRoughness");
MaterialAdapter adapter = null;
if (pbrMat != null) {
adapter = getAdapterForMaterial(info, "pbrMetallicRoughness");
if (adapter == null) {
adapter = defaultMaterialAdapters.get("pbrMetallicRoughness");
}
adapter.init(info.getManager());
}
adapter = customContentManager.readExtensionAndExtras("material", matData, adapter);
if (adapter == null) {
logger.log(Level.WARNING,
"Couldn't find any matching material definition for material " + materialIndex);
adapter = defaultMaterialAdapters.get("pbrMetallicRoughness");
adapter.init(info.getManager());
setDefaultParams(adapter.getMaterial());
}
Integer metallicRoughnessIndex = null;
if (pbrMat != null) {
adapter.setParam("baseColorFactor", getAsColor(pbrMat, "baseColorFactor", ColorRGBA.White));
adapter.setParam("metallicFactor", getAsFloat(pbrMat, "metallicFactor", 1f));
adapter.setParam("roughnessFactor", getAsFloat(pbrMat, "roughnessFactor", 1f));
adapter.setParam("baseColorTexture", readTexture(pbrMat.getAsJsonObject("baseColorTexture")));
adapter.setParam("metallicRoughnessTexture",
readTexture(pbrMat.getAsJsonObject("metallicRoughnessTexture")));
JsonObject metallicRoughnessJson = pbrMat.getAsJsonObject("metallicRoughnessTexture");
metallicRoughnessIndex = metallicRoughnessJson != null ? getAsInteger(metallicRoughnessJson, "index") : null;
}
adapter.getMaterial().setName(getAsString(matData, "name"));
adapter.setParam("emissiveFactor", getAsColor(matData, "emissiveFactor", ColorRGBA.Black));
String alphaMode = getAsString(matData, "alphaMode");
adapter.setParam("alphaMode", alphaMode);
if (alphaMode != null && alphaMode.equals("MASK")) {
adapter.setParam("alphaCutoff", getAsFloat(matData, "alphaCutoff"));
}
adapter.setParam("doubleSided", getAsBoolean(matData, "doubleSided"));
Texture2D normal = readTexture(matData.getAsJsonObject("normalTexture"));
adapter.setParam("normalTexture", normal);
if (normal != null) {
useNormalsFlag = true;
JsonObject normalTexture = matData.getAsJsonObject("normalTexture");
Float normalScale = getAsFloat(normalTexture, "scale");
if (normalScale != null) {
adapter.setParam("normalScale", normalScale);
}
}
JsonObject occlusionJson = matData.getAsJsonObject("occlusionTexture");
Integer occlusionIndex = occlusionJson != null ? getAsInteger(occlusionJson, "index") : null;
if (occlusionIndex != null && occlusionIndex.equals(metallicRoughnessIndex)) {
adapter.getMaterial().setBoolean("AoPackedInMRMap", true);
} else {
adapter.setParam("occlusionTexture", readTexture(matData.getAsJsonObject("occlusionTexture")));
}
adapter.setParam("emissiveTexture", readTexture(matData.getAsJsonObject("emissiveTexture")));
return adapter.getMaterial();
}
public void readCameras() throws IOException {
if (cameras == null) {
return;
}
for (int i = 0; i < cameras.size(); i++) {
// Can't access resolution here... actually it's a shame we can't access settings from anywhere.
// users will have to call resize on the camera.
Camera cam = new Camera(1, 1);
JsonObject camObj = cameras.get(i).getAsJsonObject();
String type = getAsString(camObj, "type");
assertNotNull(type, "No type defined for camera");
JsonObject camData = camObj.getAsJsonObject(type);
if (type.equals("perspective")) {
float aspectRatio = getAsFloat(camData, "aspectRation", 1f);
Float yfov = getAsFloat(camData, "yfov");
assertNotNull(yfov, "No yfov for perspective camera");
Float zNear = getAsFloat(camData, "znear");
assertNotNull(zNear, "No znear for perspective camera");
Float zFar = getAsFloat(camData, "zfar", zNear * 1000f);
cam.setFrustumPerspective(yfov * FastMath.RAD_TO_DEG, aspectRatio, zNear, zFar);
cam = customContentManager.readExtensionAndExtras("camera.perspective", camData, cam);
} else {
Float xmag = getAsFloat(camData, "xmag");
assertNotNull(xmag, "No xmag for orthographic camera");
Float ymag = getAsFloat(camData, "ymag");
assertNotNull(ymag, "No ymag for orthographic camera");
Float zNear = getAsFloat(camData, "znear");
assertNotNull(zNear, "No znear for orthographic camera");
Float zFar = getAsFloat(camData, "zfar", zNear * 1000f);
assertNotNull(zFar, "No zfar for orthographic camera");
cam.setParallelProjection(true);
cam.setFrustum(zNear, zFar, -xmag, xmag, ymag, -ymag);
cam = customContentManager.readExtensionAndExtras("camera.orthographic", camData, cam);
}
cam = customContentManager.readExtensionAndExtras("camera", camObj, cam);
addToCache("cameras", i, cam, cameras.size());
}
}
public Texture2D readTexture(JsonObject texture) throws IOException {
return readTexture(texture, false);
}
public Texture2D readTexture(JsonObject texture, boolean flip) throws IOException {
if (texture == null) {
return null;
}
Integer textureIndex = getAsInteger(texture, "index");
assertNotNull(textureIndex, "Texture has no index");
assertNotNull(textures, "There are no textures, yet one is referenced by a material");
JsonObject textureData = textures.get(textureIndex).getAsJsonObject();
Integer sourceIndex = getAsInteger(textureData, "source");
Integer samplerIndex = getAsInteger(textureData, "sampler");
Texture2D texture2d = readImage(sourceIndex, flip);
if (samplerIndex != null) {
texture2d = readSampler(samplerIndex, texture2d);
} else {
texture2d.setWrap(Texture.WrapMode.Repeat);
}
texture2d = customContentManager.readExtensionAndExtras("texture", texture, texture2d);
return texture2d;
}
public Texture2D readImage(int sourceIndex, boolean flip) throws IOException {
if (images == null) {
throw new AssetLoadException("No image defined");
}
JsonObject image = images.get(sourceIndex).getAsJsonObject();
String uri = getAsString(image, "uri");
Integer bufferView = getAsInteger(image, "bufferView");
String mimeType = getAsString(image, "mimeType");
Texture2D result;
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);
String extension = mimeType.split("/")[1];
TextureKey key = new TextureKey("image" + sourceIndex + "." + extension, flip);
result = (Texture2D) info.getManager().loadAssetFromStream(key, new ByteArrayInputStream(data));
} else if (uri.startsWith("data:")) {
// base64 encoded image
String[] uriInfo = uri.split(",");
byte[] data = 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));
} else {
// external file image
String decoded = decodeUri(uri);
TextureKey key = new TextureKey(info.getKey().getFolder() + decoded, flip);
Texture tex = info.getManager().loadTexture(key);
result = (Texture2D) tex;
}
return result;
}
public void readAnimation(int animationIndex) throws IOException {
JsonObject animation = animations.get(animationIndex).getAsJsonObject();
JsonArray channels = animation.getAsJsonArray("channels");
JsonArray samplers = animation.getAsJsonArray("samplers");
String name = getAsString(animation, "name");
assertNotNull(channels, "No channels for animation " + name);
assertNotNull(samplers, "No samplers for animation " + name);
// temp data storage of track data
TrackData[] tracks = new TrackData[nodes.size()];
boolean hasMorphTrack = false;
for (JsonElement channel : channels) {
JsonObject target = channel.getAsJsonObject().getAsJsonObject("target");
Integer targetNode = getAsInteger(target, "node");
String targetPath = getAsString(target, "path");
if (targetNode == null) {
// no target node for the channel, specs say to ignore the channel.
continue;
}
assertNotNull(targetPath, "No target path for channel");
//
// if (targetPath.equals("weights")) {
// // Morph animation, not implemented in JME, let's warn the user and skip the channel
// logger.log(Level.WARNING,
// "Morph animation is not supported by JME yet, skipping animation track");
// continue;
// }
TrackData trackData = tracks[targetNode];
if (trackData == null) {
trackData = new TrackData();
tracks[targetNode] = trackData;
}
Integer samplerIndex = getAsInteger(channel.getAsJsonObject(), "sampler");
assertNotNull(samplerIndex, "No animation sampler provided for channel");
JsonObject sampler = samplers.get(samplerIndex).getAsJsonObject();
Integer timeIndex = getAsInteger(sampler, "input");
assertNotNull(timeIndex, "No input accessor Provided for animation sampler");
Integer dataIndex = getAsInteger(sampler, "output");
assertNotNull(dataIndex, "No output accessor Provided for animation sampler");
String interpolation = getAsString(sampler, "interpolation");
if (interpolation == null || !interpolation.equals("LINEAR")) {
// JME anim system only supports Linear interpolation (will be possible with monkanim though)
// TODO rework this once monkanim is core,
// or allow a hook for animation loading to fit custom animation systems
logger.log(Level.WARNING, "JME only supports linear interpolation for animations");
}
trackData = customContentManager.readExtensionAndExtras("animation.sampler", sampler, trackData);
float[] times = fetchFromCache("accessors", timeIndex, float[].class);
if (times == null) {
times = readAccessorData(timeIndex, floatArrayPopulator);
addToCache("accessors", timeIndex, times, accessors.size());
}
if (targetPath.equals("translation")) {
trackData.timeArrays.add(new TrackData.TimeData(times, TrackData.Type.Translation));
Vector3f[] translations = readAccessorData(dataIndex, vector3fArrayPopulator);
trackData.translations = translations;
} else if (targetPath.equals("scale")) {
trackData.timeArrays.add(new TrackData.TimeData(times, TrackData.Type.Scale));
Vector3f[] scales = readAccessorData(dataIndex, vector3fArrayPopulator);
trackData.scales = scales;
} else if (targetPath.equals("rotation")) {
trackData.timeArrays.add(new TrackData.TimeData(times, TrackData.Type.Rotation));
Quaternion[] rotations = readAccessorData(dataIndex, quaternionArrayPopulator);
trackData.rotations = rotations;
} else {
trackData.timeArrays.add(new TrackData.TimeData(times, TrackData.Type.Morph));
float[] weights = readAccessorData(dataIndex, floatArrayPopulator);
trackData.weights = weights;
hasMorphTrack = true;
}
tracks[targetNode] = customContentManager.readExtensionAndExtras("channel", channel, trackData);
}
if (name == null) {
name = "anim_" + animationIndex;
}
List<Spatial> spatials = new ArrayList<>();
AnimClip anim = new AnimClip(name);
List<AnimTrack> aTracks = new ArrayList<>();
int skinIndex = -1;
List<Joint> usedJoints = new ArrayList<>();
for (int i = 0; i < tracks.length; i++) {
TrackData trackData = tracks[i];
if (trackData == null || trackData.timeArrays.isEmpty()) {
continue;
}
trackData.update();
Object node = fetchFromCache("nodes", i, Object.class);
if (node instanceof Spatial) {
Spatial s = (Spatial) node;
spatials.add(s);
if (trackData.rotations != null || trackData.translations != null
|| trackData.scales != null) {
TransformTrack track = new TransformTrack(s, trackData.times,
trackData.translations, trackData.rotations, trackData.scales);
aTracks.add(track);
}
if (trackData.weights != null && s instanceof Geometry) {
Geometry g = (Geometry) s;
int nbMorph = g.getMesh().getMorphTargets().length;
// for (int k = 0; k < trackData.weights.length; k++) {
// System.err.print(trackData.weights[k] + ",");
// if(k % nbMorph == 0 && k!=0){
// System.err.println(" ");
// }
// }
MorphTrack track = new MorphTrack(g, trackData.times, trackData.weights, nbMorph);
aTracks.add(track);
}
} else if (node instanceof JointWrapper) {
JointWrapper jw = (JointWrapper) node;
usedJoints.add(jw.joint);
if (skinIndex == -1) {
skinIndex = jw.skinIndex;
} else {
// Check if all joints affected by this animation are from the same skin,
// the track will be skipped.
if (skinIndex != jw.skinIndex) {
logger.log(Level.WARNING, "Animation " + animationIndex + " (" + name
+ ") applies to joints that are not from the same skin: skin "
+ skinIndex + ", joint " + jw.joint.getName()
+ " from skin " + jw.skinIndex);
continue;
}
}
TransformTrack track = new TransformTrack(jw.joint, trackData.times,
trackData.translations, trackData.rotations, trackData.scales);
aTracks.add(track);
}
}
// Check each bone to see if their local pose is different from their bind pose.
// If it is, we ensure that the bone has an animation track,
// else JME way of applying anim transforms will apply the bind pose to those bones,
// instead of the local pose that is supposed to be the default
if (skinIndex != -1) {
SkinData skin = fetchFromCache("skins", skinIndex, SkinData.class);
for (Joint joint : skin.joints) {
if (!usedJoints.contains(joint)) {
// create a track
float[] times = new float[]{0};
Vector3f[] translations = new Vector3f[]{joint.getLocalTranslation()};
Quaternion[] rotations = new Quaternion[]{joint.getLocalRotation()};
Vector3f[] scales = new Vector3f[]{joint.getLocalScale()};
TransformTrack track = new TransformTrack(joint, times, translations, rotations, scales);
aTracks.add(track);
}
}
}
anim.setTracks(aTracks.toArray(new AnimTrack[aTracks.size()]));
anim = customContentManager.readExtensionAndExtras("animations", animation, anim);
if (skinIndex != -1) {
// we have an armature animation.
SkinData skin = fetchFromCache("skins", skinIndex, SkinData.class);
skin.animComposer.addAnimClip(anim);
}
if (!spatials.isEmpty()) {
if (skinIndex != -1) {
// there are some spatial or morph tracks in this bone animation... or the other way around.
// Let's add the spatials in the skinnedSpatials.
SkinData skin = fetchFromCache("skins", skinIndex, SkinData.class);
List<Spatial> spat = skinnedSpatials.get(skin);
spat.addAll(spatials);
// the animControl will be added in the setupControls();
if (hasMorphTrack && skin.morphControl == null) {
skin.morphControl = new MorphControl();
}
} else {
// Spatial animation
Spatial spatial = null;
if (spatials.size() == 1) {
spatial = spatials.get(0);
} else {
spatial = findCommonAncestor(spatials);
}
AnimComposer composer = spatial.getControl(AnimComposer.class);
if (composer == null) {
composer = new AnimComposer();
spatial.addControl(composer);
}
composer.addAnimClip(anim);
if (hasMorphTrack && spatial.getControl(MorphControl.class) == null) {
spatial.addControl(new MorphControl());
}
}
}
}
public Texture2D readSampler(int samplerIndex, Texture2D texture) throws IOException {
if (samplers == null) {
throw new AssetLoadException("No samplers defined");
}
JsonObject sampler = samplers.get(samplerIndex).getAsJsonObject();
Texture.MagFilter magFilter = getMagFilter(getAsInteger(sampler, "magFilter"));