Browse Source

Added KHR_lights_punctual gltf extension (#1443)

* Added initial implementation for KHR_lights_punctual gltf extension

* Initial implementation of light intensity

* Add light to model root and add control to light node

* Fix adding lights when model only has one scene

* Minor optimization

* Fix for intensity calc

* Custom light control for GLTF -Z oriented lights

* Update dumpScene to give more detailed information about lights

* Updated to use built-in LightControl

* Update javadocs

* Make public members of private class private
Trevor Flynn 4 years ago
parent
commit
303e22ed98

+ 1 - 0
jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/CustomContentManager.java

@@ -55,6 +55,7 @@ public class CustomContentManager {
 
     static {
         defaultExtensionLoaders.put("KHR_materials_pbrSpecularGlossiness", new PBRSpecGlossExtensionLoader());
+        defaultExtensionLoaders.put("KHR_lights_punctual", new LightsPunctualExtensionLoader());
         defaultExtensionLoaders.put("KHR_materials_unlit", new UnlitExtensionLoader());
     }
 

+ 4 - 1
jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfLoader.java

@@ -167,7 +167,10 @@ public class GltfLoader implements AssetLoader {
 
             //only one scene let's not return the root.
             if (rootNode.getChildren().size() == 1) {
-                rootNode = (Node) rootNode.getChild(0);
+                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) {

+ 295 - 0
jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/LightsPunctualExtensionLoader.java

@@ -0,0 +1,295 @@
+/*
+ * Copyright (c) 2009-2021 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.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.jme3.asset.AssetKey;
+import com.jme3.asset.AssetLoadException;
+import com.jme3.light.DirectionalLight;
+import com.jme3.light.Light;
+import com.jme3.light.PointLight;
+import com.jme3.light.SpotLight;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.FastMath;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Node;
+import com.jme3.scene.control.LightControl;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.HashSet;
+
+import static com.jme3.scene.plugins.gltf.GltfUtils.getAsColor;
+import static com.jme3.scene.plugins.gltf.GltfUtils.getAsFloat;
+
+/**
+ * Extension loader for KHR_lights_punctual extension which allows
+ * for lights to be added to the node from the gltf model.
+ *
+ * Supports directional, point, and spot lights.
+ *
+ * Created by Trevor Flynn - 3/23/2021
+ */
+public class LightsPunctualExtensionLoader implements ExtensionLoader {
+
+    private final HashSet<NodeNeedingLight> pendingNodes = new HashSet<>();
+    private final HashMap<Integer, Light> lightDefinitions = new HashMap<>();
+
+    @Override
+    public Object handleExtension(GltfLoader loader, String parentName, JsonElement parent, JsonElement extension, Object input) {
+        if (input instanceof Node) { //We are processing a node
+            JsonObject jsonObject = extension.getAsJsonObject();
+            if (jsonObject.has("light")) { //These will get run first when loading the gltf file
+                //Add node to queue
+                JsonElement indexElement = jsonObject.get("light");
+                int index = indexElement.getAsInt();
+                if (!lightDefinitions.containsKey(index)) {
+                    pendingNodes.add(new NodeNeedingLight((Node) input, index));
+                } else {
+                    addLight((Node) input, (Node) input, index);
+                }
+            } else if (jsonObject.has("lights")) { //This will get run last
+                //Process the light definitions
+                JsonArray lights = jsonObject.getAsJsonArray("lights");
+
+                for (int i = 0; i < lights.size(); i++) {
+                    //Create light definition
+                    JsonObject light = lights.get(i).getAsJsonObject();
+                    String type = light.get("type").getAsString();
+
+                    Light lightNode;
+                    switch (type) {
+                        case "point":
+                            lightNode = buildPointLight(light);
+                            break;
+                        case "directional":
+                            lightNode = buildDirectionalLight(light);
+                            break;
+                        case "spot":
+                            lightNode = buildSpotLight(light);
+                            break;
+                        default:
+                            throw new AssetLoadException("KHR_lights_punctual unsupported light type: " + type);
+                    }
+
+                    lightDefinitions.put(i, lightNode);
+                }
+
+                //Build any lights that are pending now that we have definitions
+                for (NodeNeedingLight nodeInNeed : pendingNodes) {
+                    addLight((Node) input, nodeInNeed.getNode(), nodeInNeed.getLightIndex());
+                }
+                pendingNodes.clear();
+            } else {
+                throw new AssetLoadException("KHR_lights_punctual extension malformed json");
+            }
+
+            return input;
+        } else {
+            throw new AssetLoadException("KHR_lights_punctual extension added on unsupported element");
+        }
+    }
+
+    /**
+     * Build a spot light from gltf json.
+     * @param obj The gltf json object for the spot light
+     * @return A spot light representation of the gltf object
+     */
+    private SpotLight buildSpotLight(JsonObject obj) {
+        //Get properties
+        String name = obj.has("name") ? obj.get("name").getAsString() : "";
+
+        float intensity = obj.has("intensity") ? obj.get("intensity").getAsFloat() : 1.0f;
+        ColorRGBA color = obj.has("color") ? GltfUtils.getAsColor(obj, "color") : new ColorRGBA(ColorRGBA.White);
+        color = lumensToColor(color, intensity);
+        float range = obj.has("range") ? obj.get("range").getAsFloat() : Float.POSITIVE_INFINITY;
+
+        //Spot specific
+        JsonObject spot = obj.getAsJsonObject("spot");
+        float innerConeAngle = spot != null && spot.has("innerConeAngle") ? spot.get("innerConeAngle").getAsFloat() : 0f;
+        float outerConeAngle = spot != null && spot.has("outerConeAngle") ? spot.get("outerConeAngle").getAsFloat() : ((float) Math.PI) / 4f;
+
+        /*
+        Correct floating point error on half PI, GLTF spec says that the outerConeAngle
+        can be less or equal to PI/2, but JME requires less than PI/2.
+        We will bring the angle within PI/2 if it is equal or larger than PI/2
+         */
+        if (outerConeAngle >= FastMath.HALF_PI) {
+            outerConeAngle = FastMath.HALF_PI - 0.000001f;
+        }
+
+        SpotLight spotLight = new SpotLight();
+        spotLight.setName(name);
+        spotLight.setColor(color);
+        spotLight.setSpotRange(range);
+        spotLight.setSpotInnerAngle(innerConeAngle);
+        spotLight.setSpotOuterAngle(outerConeAngle);
+        spotLight.setDirection(Vector3f.UNIT_Z.negate());
+
+        return spotLight;
+    }
+
+    /**
+     * Build a directional light from gltf json.
+     * @param obj The gltf json object for the directional light
+     * @return A directional light representation of the gltf object
+     */
+    private DirectionalLight buildDirectionalLight(JsonObject obj) {
+        //Get properties
+        String name = obj.has("name") ? obj.get("name").getAsString() : "";
+
+        float intensity = obj.has("intensity") ? obj.get("intensity").getAsFloat() : 1.0f;
+        ColorRGBA color = obj.has("color") ? GltfUtils.getAsColor(obj, "color") : new ColorRGBA(ColorRGBA.White);
+        color = lumensToColor(color, intensity);
+
+        DirectionalLight directionalLight = new DirectionalLight();
+        directionalLight.setName(name);
+        directionalLight.setColor(color);
+        directionalLight.setDirection(Vector3f.UNIT_Z.negate());
+
+        return directionalLight;
+    }
+
+    /**
+     * Build a point light from gltf json.
+     * @param obj The gltf json object for the point light
+     * @return A point light representation of the gltf object
+     */
+    private PointLight buildPointLight(JsonObject obj) {
+        //Get properties
+        String name = obj.has("name") ? obj.get("name").getAsString() : "";
+
+        float intensity = obj.has("intensity") ? obj.get("intensity").getAsFloat() : 1.0f;
+        ColorRGBA color = obj.has("color") ? GltfUtils.getAsColor(obj, "color") : new ColorRGBA(ColorRGBA.White);
+        color = lumensToColor(color, intensity);
+        float range = obj.has("range") ? obj.get("range").getAsFloat() : Float.POSITIVE_INFINITY;
+
+        PointLight pointLight = new PointLight();
+        pointLight.setName(name);
+        pointLight.setColor(color);
+        pointLight.setRadius(range);
+
+        return pointLight;
+    }
+
+    /**
+     * Attach a light at the given index to the given parent node,
+     * and the control for the light to the given node.
+     * @param parent The node to attach the light to
+     * @param node The node to attach the light control to
+     * @param lightIndex The index of the light
+     */
+    private void addLight(Node parent, Node node, int lightIndex) {
+        if (lightDefinitions.containsKey(lightIndex)) {
+            Light light = lightDefinitions.get(lightIndex);
+            parent.addLight(light);
+            LightControl control = new LightControl(light);
+            node.addControl(control);
+        } else {
+            throw new AssetLoadException("KHR_lights_punctual extension accessed undefined light at index " + lightIndex);
+        }
+    }
+
+    /**
+     * Convert a floating point lumens value into a color that
+     * represents both color and brightness of the light.
+     *
+     * @param color The base color of the light
+     * @param lumens The lumnes value to convert to a color
+     * @return A color representing the intensity of the given lumens encoded into the given color
+     */
+    private ColorRGBA lumensToColor(ColorRGBA color, float lumens) {
+        ColorRGBA brightnessModifier = lumensToColor(lumens);
+        return color.mult(brightnessModifier);
+    }
+
+    /**
+     * Convert a floating point lumens value into a grayscale color that
+     * represents a brightness.
+     *
+     * @param lumens The lumnes value to convert to a color
+     * @return A color representing the intensity of the given lumens
+     */
+    private ColorRGBA lumensToColor(float lumens) {
+        /*
+        Taken from /Common/ShaderLib/Hdr.glsllib
+        vec4 HDR_EncodeLum(in float lum){
+        float Le = 2.0 * log2(lum + epsilon) + 127.0;
+        vec4 result = vec4(0.0);
+        result.a = fract(Le);
+        result.rgb = vec3((Le - (floor(result.a * 255.0)) / 255.0) / 255.0);
+        return result;
+         */
+        float epsilon = 0.0001f;
+
+        double Le = 2f * Math.log(lumens * epsilon) / Math.log(2) + 127.0;
+        ColorRGBA color = new ColorRGBA();
+        color.a = (float) (Le - Math.floor(Le)); //Get fractional part
+        float val = (float) ((Le - (Math.floor(color.a * 255.0)) / 255.0) / 255.0);
+        color.r = val;
+        color.g = val;
+        color.b = val;
+
+        return color;
+    }
+
+    /**
+     * A bean to contain the relation between a node and a light index
+     */
+    private static class NodeNeedingLight {
+        private Node node;
+        private int lightIndex;
+
+        private NodeNeedingLight(Node node, int lightIndex) {
+            this.node = node;
+            this.lightIndex = lightIndex;
+        }
+
+        private Node getNode() {
+            return node;
+        }
+
+        private void setNode(Node node) {
+            this.node = node;
+        }
+
+        private int getLightIndex() {
+            return lightIndex;
+        }
+
+        private void setLightIndex(int lightIndex) {
+            this.lightIndex = lightIndex;
+        }
+    }
+}

+ 34 - 2
jme3-plugins/src/test/java/com/jme3/scene/plugins/gltf/GltfLoaderTest.java

@@ -33,7 +33,12 @@ package com.jme3.scene.plugins.gltf;
 
 import com.jme3.asset.AssetLoadException;
 import com.jme3.asset.AssetManager;
+import com.jme3.light.DirectionalLight;
+import com.jme3.light.Light;
+import com.jme3.light.PointLight;
+import com.jme3.light.SpotLight;
 import com.jme3.material.plugin.TestMaterialWrite;
+import com.jme3.math.Vector3f;
 import com.jme3.scene.Geometry;
 import com.jme3.scene.Node;
 import com.jme3.scene.Spatial;
@@ -79,16 +84,43 @@ public class GltfLoaderTest {
         }
     }
 
+    @Test
+    public void testLightsPunctualExtension() {
+        try {
+            Spatial scene = assetManager.loadModel("gltf/lights/lights.gltf");
+            dumpScene(scene, 0);
+        } catch (AssetLoadException ex) {
+            ex.printStackTrace();
+            Assert.fail("Failed to import gltf model with lights punctual extension");
+        }
+    }
+
     private void dumpScene(Spatial s, int indent) {
         System.err.print(indentString.substring(0, indent) + s.getName() + " (" + s.getClass().getSimpleName() + ") / " +
                 s.getLocalTransform().getTranslation().toString() + ", " +
                 s.getLocalTransform().getRotation().toString() + ", " +
                 s.getLocalTransform().getScale().toString());
-        if (s instanceof Geometry)
-        {
+        if (s instanceof Geometry) {
             System.err.print(" / " + ((Geometry) s).getMaterial());
         }
         System.err.println();
+        for (Light light : s.getLocalLightList()) {
+            System.err.print(indentString.substring(0, indent + 1) + " (" + light.getClass().getSimpleName() + ")");
+            if (light instanceof SpotLight) {
+                Vector3f pos = ((SpotLight) light).getPosition();
+                Vector3f dir = ((SpotLight) light).getDirection();
+                System.err.println(" " + pos.toString() + ", " + dir.toString());
+            } else if (light instanceof PointLight) {
+                Vector3f pos = ((PointLight) light).getPosition();
+                System.err.println(" " + pos.toString());
+            } else if (light instanceof DirectionalLight) {
+                Vector3f dir = ((DirectionalLight) light).getDirection();
+                System.err.println(" " + dir.toString());
+            } else {
+                System.err.println();
+            }
+        }
+
         if (s instanceof Node) {
             Node n = (Node) s;
             for (Spatial spatial : n.getChildren()) {

BIN
jme3-plugins/src/test/resources/gltf/lights/MODEL_ROUNDED_CUBE_PART_1/indices.bin


File diff suppressed because it is too large
+ 108 - 0
jme3-plugins/src/test/resources/gltf/lights/MODEL_ROUNDED_CUBE_PART_1/normals.bin


BIN
jme3-plugins/src/test/resources/gltf/lights/MODEL_ROUNDED_CUBE_PART_1/positions.bin


+ 226 - 0
jme3-plugins/src/test/resources/gltf/lights/lights.gltf

@@ -0,0 +1,226 @@
+{
+  "extensionsUsed": [
+    "KHR_lights_punctual"
+  ],
+  "extensions": {
+    "KHR_lights_punctual": {
+      "lights": [
+        {
+          "color": [
+            1.0,
+            0.9,
+            0.7
+          ],
+          "name": "Directional",
+          "intensity": 3.0,
+          "type": "directional"
+        },
+        {
+          "color": [
+            1.0,
+            0.0,
+            0.0
+          ],
+          "name": "Point",
+          "intensity": 20.0,
+          "type": "point"
+        },
+        {
+          "color": [
+            0.3,
+            0.7,
+            1.0
+          ],
+          "name": "Spot",
+          "intensity": 40.0,
+          "type": "spot",
+          "spot": {
+            "innerConeAngle": 0.785398163397448,
+            "outerConeAngle": 1.57079632679
+          }
+        }
+      ]
+    }
+  },
+  "accessors": [
+    {
+      "bufferView": 0,
+      "byteOffset": 0,
+      "componentType": 5126,
+      "count": 3456,
+      "max": [
+        10,
+        19.949111938476562,
+        10
+      ],
+      "min": [
+        -10,
+        -0.050886999815702438,
+        -10
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 0,
+      "componentType": 5126,
+      "count": 3456,
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 0,
+      "componentType": 5125,
+      "count": 5172,
+      "max": [
+        3455
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    }
+  ],
+  "asset": {
+    "copyright": "2017 (c) Adobe Corp",
+    "minVersion": "2.0",
+    "version": "2.0"
+  },
+  "bufferViews": [
+    {
+      "buffer": 0,
+      "byteLength": 41472,
+      "byteOffset": 0
+    },
+    {
+      "buffer": 1,
+      "byteLength": 41472,
+      "byteOffset": 0
+    },
+    {
+      "buffer": 2,
+      "byteLength": 20688,
+      "byteOffset": 0,
+      "target": 34963
+    }
+  ],
+  "buffers": [
+    {
+      "byteLength": 41472,
+      "uri": "MODEL_ROUNDED_CUBE_PART_1/positions.bin"
+    },
+    {
+      "byteLength": 41472,
+      "uri": "MODEL_ROUNDED_CUBE_PART_1/normals.bin"
+    },
+    {
+      "byteLength": 20688,
+      "uri": "MODEL_ROUNDED_CUBE_PART_1/indices.bin"
+    }
+  ],
+  "cameras": [
+    {
+      "name": "render_camera",
+      "perspective": {
+        "aspectRatio": 1.3333333730697632,
+        "yfov": 0.58904862403869629,
+        "zfar": 100,
+        "znear": 9.9999997473787516e-05
+      },
+      "type": "perspective"
+    },
+    {
+      "name": "render_camera2",
+      "perspective": {
+        "aspectRatio": 1.3333333730697632,
+        "yfov": 0.58904862403869629,
+        "zfar": 100,
+        "znear": 9.9999997473787516e-05
+      },
+      "type": "perspective"
+    }
+  ],
+  "materials": [
+    {
+      "doubleSided": true,
+      "name": "Rounded Cube Material",
+      "pbrMetallicRoughness": {
+        "baseColorFactor": [
+          0.63075679540634155,
+          0.63075679540634155,
+          0.63075679540634155,
+          1
+        ],
+        "metallicFactor": 0,
+        "roughnessFactor": 0.50300002098083496
+      }
+    }
+  ],
+  "meshes": [
+    {
+      "name": "MODEL_ROUNDED_CUBE_PART_1",
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 1,
+            "POSITION": 0
+          },
+          "indices": 2,
+          "material": 0,
+          "mode": 4
+        }
+      ]
+    }
+  ],
+  "nodes": [
+    {
+      "extensions": {
+        "KHR_lights_punctual": {
+          "light": 1
+        }
+      },
+      "matrix": [
+        1,
+        0,
+        0,
+        0,
+        0,
+        1,
+        0,
+        0,
+        0,
+        0,
+        1,
+        0,
+        0,
+        22,
+        0,
+        1
+      ],
+      "name": "point_light"
+    },
+    {
+      "name": "directional_light",
+      "extensions": {
+        "KHR_lights_punctual": {
+          "light": 0
+        }
+      }
+    },
+    {
+      "mesh": 0,
+      "name": "MODEL_ROUNDED_CUBE_PART_1model_N3D"
+    }
+  ],
+  "scene": 0,
+  "scenes": [
+    {
+      "name": "scene",
+      "nodes": [
+        0,
+        1,
+        2
+      ]
+    }
+  ]
+}

Some files were not shown because too many files changed in this diff