瀏覽代碼

Add userdata loader to gltf (#2106)

* Add userdata loader and make it the default

* turn unhandled extras log into a warning

* Make default extra loader static and configurable

* Make default extras loader configurable and reinstance with gltf loader

* code cleanup

* make defaultExtraLoaderClass volatile

* fix
Riccardo Balbo 1 年之前
父節點
當前提交
c8987bed1b

+ 39 - 5
jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/CustomContentManager.java

@@ -47,6 +47,9 @@ import java.util.logging.Logger;
  * Created by Nehon on 20/08/2017.
  */
 public class CustomContentManager {
+    static volatile Class<? extends ExtrasLoader> defaultExtraLoaderClass = UserDataLoader.class;
+    private ExtrasLoader defaultExtraLoaderInstance;
+
 
     private final static Logger logger = Logger.getLogger(CustomContentManager.class.getName());
 
@@ -68,6 +71,31 @@ public class CustomContentManager {
 
     public CustomContentManager() {
     }
+    
+    /**
+     * Returns the default extras loader.
+     * @return the default extras loader.
+     */
+    public ExtrasLoader getDefaultExtrasLoader() {
+        if (defaultExtraLoaderClass == null) { 
+            defaultExtraLoaderInstance = null; // do not hold reference
+            return null;
+        }
+
+        if (defaultExtraLoaderInstance != null
+                && defaultExtraLoaderInstance.getClass() != defaultExtraLoaderClass) {
+            defaultExtraLoaderInstance = null; // reset instance if class changed
+        }
+
+        try {
+            defaultExtraLoaderInstance = defaultExtraLoaderClass.getDeclaredConstructor().newInstance();
+        } catch (Exception e) {
+            logger.log(Level.WARNING, "Could not instantiate default extras loader", e);
+            defaultExtraLoaderInstance = null;
+        }
+
+        return defaultExtraLoaderInstance;
+    }
 
     void init(GltfLoader gltfLoader) {
         this.gltfLoader = gltfLoader;
@@ -156,14 +184,20 @@ public class CustomContentManager {
 
     @SuppressWarnings("unchecked")
     private <T> T readExtras(String name, JsonElement el, T input) throws AssetLoadException {
-        if (key == null) {
-            return input;
+        ExtrasLoader loader = null;
+
+        if (key != null) { // try to get the extras loader from the model key if available
+            loader = key.getExtrasLoader();
+        }
+ 
+        if (loader == null) { // if no loader was found, use the default extras loader
+            loader = getDefaultExtrasLoader();
         }
-        ExtrasLoader loader;
-        loader = key.getExtrasLoader();
-        if (loader == null) {
+
+        if (loader == null) { // if default loader is not set or failed to instantiate, skip extras
             return input;
         }
+           
         JsonElement extras = el.getAsJsonObject().getAsJsonObject("extras");
         if (extras == null) {
             return input;

+ 18 - 0
jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfLoader.java

@@ -1506,4 +1506,22 @@ public class GltfLoader implements AssetLoader {
     public static void unregisterExtension(String name) {
         CustomContentManager.defaultExtensionLoaders.remove(name);
     }
+    
+    /**
+     * Sets the default extras loader used when no loader is specified in the GltfModelKey.
+     * 
+     * @param loader
+     *            the default extras loader.
+     */
+    public static void registerDefaultExtrasLoader(Class<? extends ExtrasLoader> loader) {
+        CustomContentManager.defaultExtraLoaderClass = loader;
+    }
+
+
+    /**
+     * Unregisters the default extras loader.
+     */
+    public static void unregisterDefaultExtrasLoader() {
+        CustomContentManager.defaultExtraLoaderClass = UserDataLoader.class;
+    }
 }

+ 2 - 2
jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfModelKey.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2023 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -54,8 +54,8 @@ public class GltfModelKey extends ModelKey {
 
     private Map<String, MaterialAdapter> materialAdapters = new HashMap<>();
     private static Map<String, ExtensionLoader> extensionLoaders = new HashMap<>();
-    private ExtrasLoader extrasLoader;
     private boolean keepSkeletonPose = false;
+    private ExtrasLoader extrasLoader;
 
     public GltfModelKey(String name) {
         super(name);

+ 174 - 0
jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/UserDataLoader.java

@@ -0,0 +1,174 @@
+/*
+ * 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.jme3.plugins.json.JsonArray;
+import com.jme3.plugins.json.JsonElement;
+import com.jme3.plugins.json.JsonObject;
+import com.jme3.plugins.json.JsonPrimitive;
+import com.jme3.scene.Spatial;
+
+import java.lang.reflect.Array;
+import java.util.*;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Import user data from glTF extras.
+ * 
+ *      Derived from Simsilica JmeConvert
+ *      (https://github.com/Simsilica/JmeConvert/blob/master/src/main/java/com/simsilica/jmec/gltf/GltfExtrasLoader.java)
+ *      by Paul Speed (Copyright (c) 2019, Simsilica, LLC)
+ * 
+ */
+
+public class UserDataLoader implements ExtrasLoader {
+
+    private static final Logger log = Logger.getLogger(UserDataLoader.class.getName());
+
+    public UserDataLoader() {
+    }
+
+    @Override
+    public Object handleExtras(GltfLoader loader, String parentName, JsonElement parent, JsonElement extras,
+            Object input) {
+        log.fine("handleExtras(" + loader + ", " + parentName + ", " + parent + ", " + extras + ", " + input
+                + ")");
+        // Only interested in composite objects
+        if (!(extras instanceof JsonObject)) {
+            log.warning("Skipping extras:" + extras);
+            return input;
+        }
+        JsonObject jo = extras.getAsJsonObject();
+        apply(input, jo);
+        return input;
+    }
+
+    protected void apply(Object input, JsonObject extras) {
+        if (input == null) {
+            return;
+        }
+        if (input.getClass().isArray()) {
+            applyToArray(input, extras);
+        } else if (input instanceof Spatial) {
+            applyToSpatial((Spatial) input, extras);
+        } else {
+            log.warning("Unhandled input type:" + input.getClass());
+        }
+    }
+
+    protected void applyToArray(Object array, JsonObject extras) {
+        int size = Array.getLength(array);
+        for (int i = 0; i < size; i++) {
+            Object o = Array.get(array, i);
+            log.fine("processing array[" + i + "]:" + o);
+            apply(o, extras);
+        }
+    }
+
+    protected void applyToSpatial(Spatial spatial, JsonObject extras) {
+        for (Map.Entry<String, JsonElement> el : extras.entrySet()) {
+            log.fine(el.toString());
+            Object val = toAttribute(el.getValue(), false);
+
+            if (log.isLoggable(Level.FINE)) {
+                log.fine("setUserData(" + el.getKey() + ", " + val + ")");
+            }
+            spatial.setUserData(el.getKey(), val);
+        }
+    }
+
+    protected Object toAttribute(JsonElement el, boolean nested) {
+        if (el == null) {
+            return null;
+        }
+        if (el instanceof JsonObject) {
+            return toAttribute(el.getAsJsonObject(), nested);
+        } else if (el instanceof JsonArray) {
+            return toAttribute(el.getAsJsonArray(), nested);
+        } else if (el instanceof JsonPrimitive) {
+            return toAttribute(el.getAsJsonPrimitive(), nested);
+        }
+        log.warning("Unhandled extras element:" + el);
+        return null;
+    }
+
+    protected Object toAttribute(JsonObject jo, boolean nested) {
+        Map<String, Object> result = new HashMap<>();
+        for (Map.Entry<String, JsonElement> el : jo.entrySet()) {
+            result.put(el.getKey(), toAttribute(el.getValue(), true));
+        }
+        return result;
+    }
+
+    protected Object toAttribute(JsonArray ja, boolean nested) {
+        List<Object> result = new ArrayList<>();
+        for (JsonElement el : ja) {
+            result.add(toAttribute(el, true));
+        }
+        return result;
+    }
+
+    protected Object toAttribute(JsonPrimitive jp, boolean nested) {
+        if (jp.isBoolean()) {
+            return jp.getAsBoolean();
+        } else if (jp.isNumber()) {
+            // JME doesn't save Maps properly and treats them as two
+            // separate Lists... and it doesn't like saving Doubles
+            // in lists so we'll just return strings in the case where
+            // the value would end up in a map. If users someday really
+            // need properly typed map values and JME map storage hasn't
+            // been fixed then perhaps we give the users the option of
+            // flattening the nested properties into dot notation, ie:
+            // all directly on UserData with no Map children.
+            if (nested) {
+                return jp.getAsString();
+            }
+            Number num = jp.getAsNumber();
+            // JME doesn't like to save GSON's LazilyParsedNumber so we'll
+            // convert it into a real number. I don't think we can reliably
+            // guess what type of number the user intended. It would take
+            // some expirimentation to determine if things like 0.0 are
+            // preserved
+            // during export or just get exported as 0.
+            // Rather than randomly flip-flop between number types depending
+            // on the inclusion (or not) of a decimal point, we'll just always
+            // return Double.
+            return num.doubleValue();
+        } else if (jp.isString()) {
+            return jp.getAsString();
+        }
+        log.warning("Unhandled primitive:" + jp);
+        return null;
+    }
+}