Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,22 @@
*/
package com.jme3.scene.plugins.gltf;

import com.jme3.plugins.json.JsonArray;
import com.jme3.plugins.json.JsonElement;
import com.jme3.asset.AssetLoadException;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.jme3.asset.AssetLoadException;
import com.jme3.plugins.json.JsonArray;
import com.jme3.plugins.json.JsonElement;
import com.jme3.plugins.json.JsonObject;

/**
* Created by Nehon on 20/08/2017.
*/
Expand All @@ -56,6 +61,10 @@ public class CustomContentManager {
private GltfLoader gltfLoader;


/**
* The mapping from glTF extension names to the classes that
* represent the respective laoders.
*/
static final Map<String, Class<? extends ExtensionLoader>> defaultExtensionLoaders = new ConcurrentHashMap<>();
static {
defaultExtensionLoaders.put("KHR_materials_pbrSpecularGlossiness", PBRSpecGlossExtensionLoader.class);
Expand All @@ -64,9 +73,13 @@ public class CustomContentManager {
defaultExtensionLoaders.put("KHR_texture_transform", TextureTransformExtensionLoader.class);
defaultExtensionLoaders.put("KHR_materials_emissive_strength", PBREmissiveStrengthExtensionLoader.class);
defaultExtensionLoaders.put("KHR_draco_mesh_compression", DracoMeshCompressionExtensionLoader.class);

}

/**
* The mapping from glTF extension names to the actual loader instances
* that have been lazily created from the defaultExtensionLoaders,
* in {@link #findExtensionLoader(String)}
*/
private final Map<String, ExtensionLoader> loadedExtensionLoaders = new HashMap<>();

public CustomContentManager() {
Expand Down Expand Up @@ -104,30 +117,82 @@ void init(GltfLoader gltfLoader) {
this.key = (GltfModelKey) gltfLoader.getInfo().getKey();
}

JsonArray extensionUsed = gltfLoader.getDocRoot().getAsJsonArray("extensionsUsed");
if (extensionUsed != null) {
for (JsonElement extElem : extensionUsed) {
String ext = extElem.getAsString();
if (ext != null) {
if (defaultExtensionLoaders.get(ext) == null && (this.key != null && this.key.getExtensionLoader(ext) == null)) {
logger.log(Level.WARNING, "Extension " + ext + " is not supported, please provide your own implementation in the GltfModelKey");
}
}
// For extensions that are USED but not supported, print a warning
List<String> extensionsUsed = getArrayAsStringList(gltfLoader.getDocRoot(), "extensionsUsed");
for (String extensionName : extensionsUsed) {
if (!isExtensionSupported(extensionName)) {
logger.log(Level.WARNING, "Extension " + extensionName
+ " is not supported, please provide your own implementation in the GltfModelKey");
}
}
JsonArray extensionRequired = gltfLoader.getDocRoot().getAsJsonArray("extensionsRequired");
if (extensionRequired != null) {
for (JsonElement extElem : extensionRequired) {
String ext = extElem.getAsString();
if (ext != null) {
if (defaultExtensionLoaders.get(ext) == null && (this.key != null && this.key.getExtensionLoader(ext) == null)) {
logger.log(Level.SEVERE, "Extension " + ext + " is mandatory for this file, the loaded scene result will be unexpected.");
}

// For extensions that are REQUIRED but not supported,
// throw an AssetLoadException by default
// If the GltfModelKey#isStrict returns false, then
// still print an error message, at least
List<String> extensionsRequired = getArrayAsStringList(gltfLoader.getDocRoot(), "extensionsRequired");
for (String extensionName : extensionsRequired) {
if (!isExtensionSupported(extensionName)) {
if (this.key != null && !this.key.isStrict()) {
logger.log(Level.SEVERE, "Extension " + extensionName
+ " is required for this file. The behavior of the loader is unspecified.");
} else {
throw new AssetLoadException(
"Extension " + extensionName + " is required for this file.");
}
}
}
}

/**
* Returns a (possibly unmodifiable) list of the string representations of the elements in the specified
* array, or an empty list if the specified array does not exist.
*
* @param jsonObject
* The JSON object
* @param property
* The property name of the array property
* @return The list
*/
private static List<String> getArrayAsStringList(JsonObject jsonObject, String property) {
JsonArray jsonArray = jsonObject.getAsJsonArray(property);
if (jsonArray == null) {
return Collections.emptyList();
}
List<String> list = new ArrayList<String>();
for (JsonElement jsonElement : jsonArray) {
String string = jsonElement.getAsString();
if (string != null) {
list.add(string);
}
}
return list;
}

/**
* Returns whether the specified glTF extension is supported.
*
* The given string is the name of the extension, e.g. <code>KHR_texture_transform</code>.
*
* This will return whether there is a default extension loader for the given extension registered in the
* {@link #defaultExtensionLoaders}, or whether the <code>GltfModelKey</code> that was obtained from the
* <code>GltfLoader</code> contains a custom extension loader that was registered via
* {@link GltfModelKey#registerExtensionLoader(String, ExtensionLoader)}.
*
* @param ext
* The glTF extension name
* @return Whether the given extension is supported
*/
private boolean isExtensionSupported(String ext) {
if (defaultExtensionLoaders.containsKey(ext)) {
return true;
}
if (this.key != null && this.key.getExtensionLoader(ext) != null) {
return true;
}
return false;
}

public <T> T readExtensionAndExtras(String name, JsonElement el, T input) throws AssetLoadException, IOException {
T output = readExtension(name, el, input);
output = readExtras(name, el, output);
Expand All @@ -142,36 +207,11 @@ private <T> T readExtension(String name, JsonElement el, T input) throws AssetLo
}

for (Map.Entry<String, JsonElement> ext : extensions.getAsJsonObject().entrySet()) {
ExtensionLoader loader = null;

if (key != null) {
loader = key.getExtensionLoader(ext.getKey());
}

if (loader == null) {
loader = loadedExtensionLoaders.get(ext.getKey());
if (loader == null) {
try {
Class<? extends ExtensionLoader> clz = defaultExtensionLoaders.get(ext.getKey());
if (clz != null) {
loader = clz.getDeclaredConstructor().newInstance();
}
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException e) {
logger.log(Level.WARNING, "Could not instantiate loader", e);
}

if (loader != null) {
loadedExtensionLoaders.put(ext.getKey(), loader);
}
}
}


ExtensionLoader loader = findExtensionLoader(ext.getKey());
if (loader == null) {
logger.log(Level.WARNING, "Could not find loader for extension " + ext.getKey());
continue;
}

try {
return (T) loader.handleExtension(gltfLoader, name, el, ext.getValue(), input);
} catch (ClassCastException e) {
Expand All @@ -182,6 +222,45 @@ private <T> T readExtension(String name, JsonElement el, T input) throws AssetLo
return input;
}

/**
* Returns the <code>ExtensionLoader</code> for the given glTF extension name.
*
* The extension name is a name like <code>KHR_texture_transform</code>. This method will first try to
* return the custom extension loader that was registered in the GltfModelKey.
*
* If it does not exist, it will return an instance of the default extension loader that was registered
* for the given extension, lazily creating the instance based on the registered defaultExtensionLoaders.
*
* @param extensionName
* The extension name
* @return The loader, or <code>null</code> if no loader could be found or instantiated
*/
private ExtensionLoader findExtensionLoader(String extensionName) {
if (key != null) {
ExtensionLoader loader = key.getExtensionLoader(extensionName);
if (loader != null) {
return loader;
}
}

ExtensionLoader loader = loadedExtensionLoaders.get(extensionName);
if (loader != null) {
return loader;
}
try {
Class<? extends ExtensionLoader> clz = defaultExtensionLoaders.get(extensionName);
if (clz != null) {
loader = clz.getDeclaredConstructor().newInstance();
loadedExtensionLoaders.put(extensionName, loader);
}
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException
| InvocationTargetException | NoSuchMethodException | SecurityException e) {
logger.log(Level.WARNING, "Could not instantiate loader", e);
}
return loader;

}

@SuppressWarnings("unchecked")
private <T> T readExtras(String name, JsonElement el, T input) throws AssetLoadException {
ExtrasLoader loader = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,49 @@ public class GltfModelKey extends ModelKey {
private boolean keepSkeletonPose = false;
private ExtrasLoader extrasLoader;

/**
* The flag indicating whether the loader should perform stricter consistency checks of the supported glTF
* extensions.
*
* When this is <code>true</code>, then the loader will cause an <code>AssetLoadException</code> when it
* encounters an asset that contains an extension in its <code>extensionsRequired</code> declaration that
* is not supported.
*/
private boolean strictExtensionCheck = true;

public GltfModelKey(String name) {
super(name);
}

public GltfModelKey() {
}

/**
* Set whether the loader should perform stricter consistency checks of the supported glTF extensions.
*
* When this is <code>true</code> (the default), the loader will cause an <code>AssetLoadException</code> when it
* encounters an asset that contains an extension in its <code>extensionsRequired</code> declaration that
* is not supported. When <code>false</code>, it will only log a SEVERE message.
*
* @param strict
* The flag
*/
public void setStrict(boolean strict) {
this.strictExtensionCheck = strict;
}

/**
* Returns whether the loader should perform stricter consistency checks of the supported glTF extensions.
*
* When this is <code>true</code> (the default), the loader will cause an <code>AssetLoadException</code> when it
* encounters an asset that contains an extension in its <code>extensionsRequired</code> declaration that
* is not supported. When <code>false</code>, it will only log a SEVERE message.
*
* @return The flag
*/
public boolean isStrict() {
return strictExtensionCheck;
}

/**
* Registers a MaterialAdapter for the given materialName.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,10 @@
import com.jme3.material.plugin.TestMaterialWrite;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.VertexBuffer;
import com.jme3.system.JmeSystem;

import org.junit.Assert;
Expand Down Expand Up @@ -69,8 +71,8 @@ public void init() {
public void testLoad() {
Spatial scene = assetManager.loadModel("gltf/box/box.gltf");
dumpScene(scene, 0);
// scene = assetManager.loadModel("gltf/hornet/scene.gltf");
// dumpScene(scene, 0);
// scene = assetManager.loadModel("gltf/hornet/scene.gltf");
// dumpScene(scene, 0);
}

@Test
Expand All @@ -95,6 +97,72 @@ public void testLightsPunctualExtension() {
}
}


@Test
public void testRequiredExtensionHandling() {

// By default, the unsupported extension that is listed in
// the 'extensionsRequired' will cause an AssetLoadException
Assert.assertThrows(AssetLoadException.class, () -> {
GltfModelKey gltfModelKey = new GltfModelKey("gltf/TriangleUnsupportedExtensionRequired.gltf");
Spatial scene = assetManager.loadModel(gltfModelKey);
dumpScene(scene, 0);
});

// When setting the 'strict' flag to 'false', then the
// asset will be loaded despite the unsupported extension
try {
GltfModelKey gltfModelKey = new GltfModelKey("gltf/TriangleUnsupportedExtensionRequired.gltf");
gltfModelKey.setStrict(false);
Spatial scene = assetManager.loadModel(gltfModelKey);
dumpScene(scene, 0);
} catch (AssetLoadException ex) {
ex.printStackTrace();
Assert.fail("Failed to load TriangleUnsupportedExtensionRequired");
}

}

@Test
public void testDracoExtension() {
try {
Spatial scene = assetManager.loadModel("gltf/unitSquare11x11_unsignedShortTexCoords-draco.glb");

Node node0 = (Node) scene;
Node node1 = (Node) node0.getChild(0);
Node node2 = (Node) node1.getChild(0);
Geometry geometry = (Geometry) node2.getChild(0);
Mesh mesh = geometry.getMesh();

// The geometry has 11x11 vertices arranged in a square,
// so there are 10 x 10 * 2 triangles
VertexBuffer indices = mesh.getBuffer(VertexBuffer.Type.Index);
Assert.assertEquals(10 * 10 * 2, indices.getNumElements());
Assert.assertEquals(VertexBuffer.Format.UnsignedShort, indices.getFormat());

// All attributes of the 11 x 11 vertices are stored as Float
// attributes (even the texture coordinates, which originally
// had been normalized(!) unsigned shorts!)
VertexBuffer positions = mesh.getBuffer(VertexBuffer.Type.Position);
Assert.assertEquals(11 * 11, positions.getNumElements());
Assert.assertEquals(VertexBuffer.Format.Float, positions.getFormat());

VertexBuffer normal = mesh.getBuffer(VertexBuffer.Type.Normal);
Assert.assertEquals(11 * 11, normal.getNumElements());
Assert.assertEquals(VertexBuffer.Format.Float, normal.getFormat());

VertexBuffer texCoord = mesh.getBuffer(VertexBuffer.Type.TexCoord);
Assert.assertEquals(11 * 11, texCoord.getNumElements());
Assert.assertEquals(VertexBuffer.Format.Float, texCoord.getFormat());

dumpScene(scene, 0);

} catch (AssetLoadException ex) {
ex.printStackTrace();
Assert.fail("Failed to import unitSquare11x11_unsignedShortTexCoords");
}
}

private void dumpScene(Spatial s, int indent) {
System.err.print(indentString.substring(0, indent) + s.getName() + " (" + s.getClass().getSimpleName() + ") / " +
s.getLocalTransform().getTranslation().toString() + ", " +
Expand Down
13 changes: 13 additions & 0 deletions jme3-plugins/src/test/resources/gltf/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Models for the glTF loader unit tests

Used in `com.jme3.scene.plugins.gltf.GltfLoaderTest`

- `TriangleUnsupportedExtensionRequired.gltf` is the embedded representation of
the `Triangle` sample asset, with additional declarations in `extensionsUsed`
and `extensionsRequired`, to test the behavior of the loader when encountering
unknown extensions
- `unitSquare11x11_unsignedShortTexCoords-draco.glb` is a simple unit square with
11x11 vertices, and texture coordinates that are stored as (normalized) unsigned
short values. The asset is draco-compressed, to check the behavior of the Draco
extension handler.

Loading