diff --git a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/CustomContentManager.java b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/CustomContentManager.java index 8606667eb5..e4e504e1ef 100644 --- a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/CustomContentManager.java +++ b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/CustomContentManager.java @@ -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. */ @@ -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> defaultExtensionLoaders = new ConcurrentHashMap<>(); static { defaultExtensionLoaders.put("KHR_materials_pbrSpecularGlossiness", PBRSpecGlossExtensionLoader.class); @@ -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 loadedExtensionLoaders = new HashMap<>(); public CustomContentManager() { @@ -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 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 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 getArrayAsStringList(JsonObject jsonObject, String property) { + JsonArray jsonArray = jsonObject.getAsJsonArray(property); + if (jsonArray == null) { + return Collections.emptyList(); + } + List list = new ArrayList(); + 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. KHR_texture_transform. + * + * This will return whether there is a default extension loader for the given extension registered in the + * {@link #defaultExtensionLoaders}, or whether the GltfModelKey that was obtained from the + * GltfLoader 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 readExtensionAndExtras(String name, JsonElement el, T input) throws AssetLoadException, IOException { T output = readExtension(name, el, input); output = readExtras(name, el, output); @@ -142,36 +207,11 @@ private T readExtension(String name, JsonElement el, T input) throws AssetLo } for (Map.Entry 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 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) { @@ -182,6 +222,45 @@ private T readExtension(String name, JsonElement el, T input) throws AssetLo return input; } + /** + * Returns the ExtensionLoader for the given glTF extension name. + * + * The extension name is a name like KHR_texture_transform. 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 null 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 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 readExtras(String name, JsonElement el, T input) throws AssetLoadException { ExtrasLoader loader = null; diff --git a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfModelKey.java b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfModelKey.java index 26da82bb3f..2490243a00 100644 --- a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfModelKey.java +++ b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfModelKey.java @@ -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 true, then the loader will cause an AssetLoadException when it + * encounters an asset that contains an extension in its extensionsRequired 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 true (the default), the loader will cause an AssetLoadException when it + * encounters an asset that contains an extension in its extensionsRequired declaration that + * is not supported. When false, 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 true (the default), the loader will cause an AssetLoadException when it + * encounters an asset that contains an extension in its extensionsRequired declaration that + * is not supported. When false, it will only log a SEVERE message. + * + * @return The flag + */ + public boolean isStrict() { + return strictExtensionCheck; + } /** * Registers a MaterialAdapter for the given materialName. diff --git a/jme3-plugins/src/test/java/com/jme3/scene/plugins/gltf/GltfLoaderTest.java b/jme3-plugins/src/test/java/com/jme3/scene/plugins/gltf/GltfLoaderTest.java index d9b873afa9..ebec4b4300 100644 --- a/jme3-plugins/src/test/java/com/jme3/scene/plugins/gltf/GltfLoaderTest.java +++ b/jme3-plugins/src/test/java/com/jme3/scene/plugins/gltf/GltfLoaderTest.java @@ -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; @@ -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 @@ -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() + ", " + diff --git a/jme3-plugins/src/test/resources/gltf/README.md b/jme3-plugins/src/test/resources/gltf/README.md new file mode 100644 index 0000000000..4072c90f66 --- /dev/null +++ b/jme3-plugins/src/test/resources/gltf/README.md @@ -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. + diff --git a/jme3-plugins/src/test/resources/gltf/TriangleUnsupportedExtensionRequired.gltf b/jme3-plugins/src/test/resources/gltf/TriangleUnsupportedExtensionRequired.gltf new file mode 100644 index 0000000000..c7216a38f9 --- /dev/null +++ b/jme3-plugins/src/test/resources/gltf/TriangleUnsupportedExtensionRequired.gltf @@ -0,0 +1,78 @@ +{ + "extensionsUsed": [ + "KHR_texture_transform", + "EXAMPLE_unsupported_extension" + ], + "extensionsRequired": [ + "KHR_texture_transform", + "EXAMPLE_unsupported_extension" + ], + "scene" : 0, + "scenes" : [ + { + "nodes" : [ 0 ] + } + ], + + "nodes" : [ + { + "mesh" : 0 + } + ], + + "meshes" : [ + { + "primitives" : [ { + "attributes" : { + "POSITION" : 1 + }, + "indices" : 0 + } ] + } + ], + + "buffers" : [ + { + "uri" : "data:application/octet-stream;base64,AAABAAIAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAA=", + "byteLength" : 44 + } + ], + "bufferViews" : [ + { + "buffer" : 0, + "byteOffset" : 0, + "byteLength" : 6, + "target" : 34963 + }, + { + "buffer" : 0, + "byteOffset" : 8, + "byteLength" : 36, + "target" : 34962 + } + ], + "accessors" : [ + { + "bufferView" : 0, + "byteOffset" : 0, + "componentType" : 5123, + "count" : 3, + "type" : "SCALAR", + "max" : [ 2 ], + "min" : [ 0 ] + }, + { + "bufferView" : 1, + "byteOffset" : 0, + "componentType" : 5126, + "count" : 3, + "type" : "VEC3", + "max" : [ 1.0, 1.0, 0.0 ], + "min" : [ 0.0, 0.0, 0.0 ] + } + ], + + "asset" : { + "version" : "2.0" + } +} diff --git a/jme3-plugins/src/test/resources/gltf/unitSquare11x11_unsignedShortTexCoords-draco.glb b/jme3-plugins/src/test/resources/gltf/unitSquare11x11_unsignedShortTexCoords-draco.glb new file mode 100644 index 0000000000..34421a09e1 Binary files /dev/null and b/jme3-plugins/src/test/resources/gltf/unitSquare11x11_unsignedShortTexCoords-draco.glb differ