diff --git a/jme3-core/src/main/resources/Common/ShaderLib/module/pbrlighting/PBRLightingUtils.glsllib b/jme3-core/src/main/resources/Common/ShaderLib/module/pbrlighting/PBRLightingUtils.glsllib index 9d58950f69..7593120476 100644 --- a/jme3-core/src/main/resources/Common/ShaderLib/module/pbrlighting/PBRLightingUtils.glsllib +++ b/jme3-core/src/main/resources/Common/ShaderLib/module/pbrlighting/PBRLightingUtils.glsllib @@ -229,49 +229,52 @@ #if defined(ENABLE_PBRLightingUtils_computeDirectLightContribution) || defined(ENABLE_PBRLightingUtils_computeLightInWorldSpace) - void PBRLightingUtils_computeLightInWorldSpace(vec3 worldPos,vec3 worldNormal, vec3 viewDir, inout Light light){ + void PBRLightingUtils_computeLightInWorldSpace(vec3 worldPos, vec3 worldNormal, vec3 viewDir, inout Light light){ if(light.ready) return; - // lightComputeDir - float posLight = step(0.5, light.type); - light.vector = light.position.xyz * sign(posLight - 0.5) - (worldPos * posLight); //tempVec lightVec + float posLight = step(0.5, light.type); + light.vector = light.position.xyz * sign(posLight - 0.5) - (worldPos * posLight); - vec3 L; // lightDir + vec3 L; float dist; - Math_lengthAndNormalize(light.vector,dist,L); + Math_lengthAndNormalize(light.vector, dist, L); - float invRange=light.invRadius; // position.w - const float light_threshold = 0.01; + if (posLight > 0.5) { + float clampedDist = max(dist, 0.00001); + float invSq = 1.0 / (clampedDist * clampedDist); - #ifdef SRGB - light.fallOff = (1.0 - invRange * dist) / (1.0 + invRange * dist * dist); // lightDir.w - light.fallOff = clamp(light.fallOff, 1.0 - posLight, 1.0); - #else - light.fallOff = clamp(1.0 - invRange * dist * posLight, 0.0, 1.0); - #endif + float radius = 1.0 / light.invRadius; + float rangeAtt = 1.0; + + if (radius > 0.0) { + float x = dist / radius; + rangeAtt = clamp(1.0 - pow(x, 4.0), 0.0, 1.0); + } + + light.fallOff = invSq * rangeAtt; + } else { + light.fallOff = 1.0; + } - // computeSpotFalloff - if(light.type>1.){ - vec3 spotdir = normalize(light.spotDirection); - float curAngleCos = dot(-L, spotdir); + if (light.type > 1.0) { float innerAngleCos = floor(light.spotAngleCos) * 0.001; float outerAngleCos = fract(light.spotAngleCos); - float innerMinusOuter = innerAngleCos - outerAngleCos; - float falloff = clamp((curAngleCos - outerAngleCos) / innerMinusOuter, 0.0, 1.0); - #ifdef SRGB - // Use quadratic falloff (notice the ^4) - falloff = pow(clamp((curAngleCos - outerAngleCos) / innerMinusOuter, 0.0, 1.0), 4.0); - #endif - light.fallOff*=falloff; - } + vec3 spotDir = normalize(light.spotDirection); + float cosA = dot(-L, spotDir); + float denom = max(innerAngleCos - outerAngleCos, 0.0001); + float spotAtten = clamp((cosA - outerAngleCos) / denom, 0.0, 1.0); + + light.fallOff *= spotAtten; + } - vec3 h=normalize(L+viewDir); - light.dir=L; + vec3 h = normalize(L + viewDir); + light.dir = L; light.NdotL = max(dot(worldNormal, L), 0.0); light.NdotH = max(dot(worldNormal, h), 0.0); light.LdotH = max(dot(L, h), 0.0); - light.HdotV = max(dot(viewDir,h), 0.); + light.HdotV = max(dot(viewDir, h), 0.0); + light.ready = true; } #endif diff --git a/jme3-effects/src/main/resources/Common/MatDefs/Post/KHRToneMap.frag b/jme3-effects/src/main/resources/Common/MatDefs/Post/KHRToneMap.frag index ba09268efa..a39c4b11c6 100644 --- a/jme3-effects/src/main/resources/Common/MatDefs/Post/KHRToneMap.frag +++ b/jme3-effects/src/main/resources/Common/MatDefs/Post/KHRToneMap.frag @@ -18,13 +18,13 @@ uniform sampler2DMS m_Texture; vec4 applyToneMap() { ivec2 iTexC = ivec2(texCoord * vec2(textureSize(m_Texture))); - vec4 color = vec4(0.0); + vec4 hdrColor = vec4(0.0); for (int i = 0; i < NUM_SAMPLES; i++) { - vec4 hdrColor = texelFetch(m_Texture, iTexC, i); - vec3 ldrColor = applyCurve(hdrColor.rgb); - color += vec4(ldrColor, hdrColor.a); + hdrColor += texelFetch(m_Texture, iTexC, i); } - return color / float(NUM_SAMPLES); + hdrColor /= float(NUM_SAMPLES); + vec3 ldrColor = vec4(applyCurve(hdrColor.rgb), hdrColor.a); + return ldrColor; } #else diff --git a/jme3-examples/src/main/java/jme3test/light/TestLightImportParity.java b/jme3-examples/src/main/java/jme3test/light/TestLightImportParity.java new file mode 100644 index 0000000000..27b6582968 --- /dev/null +++ b/jme3-examples/src/main/java/jme3test/light/TestLightImportParity.java @@ -0,0 +1,55 @@ +package jme3test.light; + +import com.jme3.app.SimpleApplication; +import com.jme3.environment.EnvironmentProbeControl; +import com.jme3.input.ChaseCamera; +import com.jme3.math.FastMath; +import com.jme3.math.Quaternion; +import com.jme3.math.Vector3f; +import com.jme3.post.FilterPostProcessor; +import com.jme3.post.filters.KHRToneMapFilter; +import com.jme3.scene.Node; +import com.jme3.scene.Spatial; +import com.jme3.util.SkyFactory; + +/** + * Test how lights render compared to the same scene in Blender. Open + * jme3-testdata/src/main/resources/BlenderParity/scene.blend in blender to compare. + */ +public class TestLightImportParity extends SimpleApplication { + public static void main(String[] args) { + TestLightImportParity app = new TestLightImportParity(); + app.start(); + } + + @Override + public void simpleInitApp() { + + flyCam.setDragToRotate(true); + flyCam.setMoveSpeed(100f); + + Spatial sky = SkyFactory.createSky(assetManager, "Textures/Sky/Alien.png", SkyFactory.EnvMapType.EquirectMap); + sky.rotate(new Quaternion().fromAngleAxis(FastMath.PI, Vector3f.UNIT_Y)); + rootNode.attachChild(sky); + + EnvironmentProbeControl probe = new EnvironmentProbeControl(assetManager, 512); + rootNode.addControl(probe); + probe.tag(sky); + + Node scene = (Node)assetManager.loadModel("BlenderParity/scene.glb"); + rootNode.attachChild(scene); + + KHRToneMapFilter toneMap = new KHRToneMapFilter(); + FilterPostProcessor fpp = new FilterPostProcessor(assetManager); + fpp.addFilter(toneMap); + viewPort.addProcessor(fpp); + + ChaseCamera chaseCam = new ChaseCamera(cam, scene, inputManager); + chaseCam.setDefaultDistance(100); + chaseCam.setMaxDistance(200); + + + + + } +} \ No newline at end of file diff --git a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfUtils.java b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfUtils.java index 677f15e6ea..bca9e159d1 100644 --- a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfUtils.java +++ b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfUtils.java @@ -795,7 +795,10 @@ public static ColorRGBA getAsColor(JsonObject parent, String name) { return null; } JsonArray color = el.getAsJsonArray(); - return new ColorRGBA(color.get(0).getAsFloat(), color.get(1).getAsFloat(), color.get(2).getAsFloat(), color.size() > 3 ? color.get(3).getAsFloat() : 1f); + // glTF colors are authored in linear space unless the spec says otherwise. + return new ColorRGBA().set( + color.get(0).getAsFloat(), color.get(1).getAsFloat(), color.get(2).getAsFloat(), color.size() > 3 ? color.get(3).getAsFloat() : 1f + ); } public static ColorRGBA getAsColor(JsonObject parent, String name, ColorRGBA defaultValue) { diff --git a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/LightsPunctualExtensionLoader.java b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/LightsPunctualExtensionLoader.java index f4611018e0..84f36c398f 100644 --- a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/LightsPunctualExtensionLoader.java +++ b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/LightsPunctualExtensionLoader.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009-2024 jMonkeyEngine + * Copyright (c) 2009-2026 jMonkeyEngine * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -53,9 +53,11 @@ * * Supports directional, point, and spot lights. * - * Created by Trevor Flynn - 3/23/2021 + * @author Trevor Flynn, Riccardo Balbo */ public class LightsPunctualExtensionLoader implements ExtensionLoader { + private static final boolean COMPUTE_LIGHT_RANGE = true; + private static final float GLTF_LIGHT_COMPAT_SCALE = 0.0009f; private final HashSet pendingNodes = new HashSet<>(); private final HashMap lightDefinitions = new HashMap<>(); @@ -126,13 +128,11 @@ private SpotLight buildSpotLight(JsonObject obj) { 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; + float innerConeAngle = (spot != null && spot.has("innerConeAngle")) ? spot.get("innerConeAngle").getAsFloat() : 0f; + float outerConeAngle = (spot != null && spot.has("outerConeAngle"))? spot.get("outerConeAngle").getAsFloat() : (FastMath.PI / 4f); /* Correct floating point error on half PI, GLTF spec says that the outerConeAngle @@ -143,9 +143,12 @@ private SpotLight buildSpotLight(JsonObject obj) { outerConeAngle = FastMath.HALF_PI - 0.000001f; } + float scaledIntensity = toCompatIntensity(intensity); + + float range = obj.has("range") ? obj.get("range").getAsFloat() : (COMPUTE_LIGHT_RANGE ? getCutoffDistance(color, scaledIntensity) : Float.POSITIVE_INFINITY); SpotLight spotLight = new SpotLight(true); spotLight.setName(name); - spotLight.setColor(color); + spotLight.setColor(applyScaledIntensity(color, scaledIntensity)); spotLight.setSpotRange(range); spotLight.setSpotInnerAngle(innerConeAngle); spotLight.setSpotOuterAngle(outerConeAngle); @@ -165,11 +168,11 @@ private DirectionalLight buildDirectionalLight(JsonObject obj) { 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 scaledIntensity = toCompatIntensity(intensity); DirectionalLight directionalLight = new DirectionalLight(true); directionalLight.setName(name); - directionalLight.setColor(color); + directionalLight.setColor(applyScaledIntensity(color, scaledIntensity)); directionalLight.setDirection(Vector3f.UNIT_Z.negate()); return directionalLight; @@ -185,13 +188,16 @@ private PointLight buildPointLight(JsonObject obj) { 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; + ColorRGBA color = obj.has("color") ? GltfUtils.getAsColor(obj, "color") + : new ColorRGBA(ColorRGBA.White); + + float scaledIntensity = toCompatIntensity(intensity); + + float range = obj.has("range") ? obj.get("range").getAsFloat() : (COMPUTE_LIGHT_RANGE ? getCutoffDistance(color, scaledIntensity) : Float.POSITIVE_INFINITY); PointLight pointLight = new PointLight(true); pointLight.setName(name); - pointLight.setColor(color); + pointLight.setColor(applyScaledIntensity(color, scaledIntensity)); pointLight.setRadius(range); return pointLight; @@ -206,7 +212,7 @@ private PointLight buildPointLight(JsonObject obj) { */ private void addLight(Node parent, Node node, int lightIndex) { if (lightDefinitions.containsKey(lightIndex)) { - Light light = lightDefinitions.get(lightIndex); + Light light = lightDefinitions.get(lightIndex).clone(); parent.addLight(light); LightControl control = new LightControl(light); control.setInvertAxisDirection(true); @@ -216,55 +222,17 @@ private void addLight(Node parent, Node node, int 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 lumens 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); + private float toCompatIntensity(float intensity) { + return intensity * GLTF_LIGHT_COMPAT_SCALE; } - /** - * Convert a floating point lumens value into a grayscale color that - * represents a brightness. - * - * @param lumens The lumens 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; + private ColorRGBA applyScaledIntensity(ColorRGBA color, float scaledIntensity) { + return color.mult(scaledIntensity); } - /** - * A bean to contain the relation between a node and a light index - */ private static class NodeNeedingLight { - private Node node; - private int lightIndex; + private final Node node; + private final int lightIndex; private NodeNeedingLight(Node node, int lightIndex) { this.node = node; @@ -275,16 +243,35 @@ private Node getNode() { return node; } - private void setNode(Node node) { - this.node = node; - } - private int getLightIndex() { return lightIndex; } + } + /** + * Computes the effective cutoff distance of a light based on its raw color and intensity. Uses + * inverse-square attenuation and a perceptual visibility threshold. + * + * @param color + * The base RGB color of the light (linear space) + * @param intensity + * The light's intensity in lumens (or equivalent) + * @return The cutoff distance where the light falls below a visible threshold + */ + private float getCutoffDistance(ColorRGBA color, float scaledIntensity) { + final float visibleThreshold = 0.001f; + final float maxRange = 10000f; - private void setLightIndex(int lightIndex) { - this.lightIndex = lightIndex; + // Compute the max channel (R/G/B) for luminance estimation + float maxComponent = Math.max(Math.max(color.r, color.g), color.b); + + if (maxComponent <= 0f || scaledIntensity <= 0f) { + return 0f; } + + // The actual light output (lux at 1 meter) per component + float effectiveIntensity = maxComponent * scaledIntensity; + // Inverse-square attenuation: intensity / d^2 = visibleThreshold + float range = (float) Math.sqrt(effectiveIntensity / visibleThreshold); + return Math.min(range, maxRange); } -} +} \ No newline at end of file diff --git a/jme3-testdata/src/main/resources/BlenderParity/env.png b/jme3-testdata/src/main/resources/BlenderParity/env.png new file mode 100644 index 0000000000..83f04ae248 Binary files /dev/null and b/jme3-testdata/src/main/resources/BlenderParity/env.png differ diff --git a/jme3-testdata/src/main/resources/BlenderParity/scene.blend b/jme3-testdata/src/main/resources/BlenderParity/scene.blend new file mode 100644 index 0000000000..31d0ec9a16 Binary files /dev/null and b/jme3-testdata/src/main/resources/BlenderParity/scene.blend differ diff --git a/jme3-testdata/src/main/resources/BlenderParity/scene.glb b/jme3-testdata/src/main/resources/BlenderParity/scene.glb new file mode 100644 index 0000000000..d478fe015a Binary files /dev/null and b/jme3-testdata/src/main/resources/BlenderParity/scene.glb differ diff --git a/jme3-testdata/src/main/resources/Textures/Sky/Alien.png b/jme3-testdata/src/main/resources/Textures/Sky/Alien.png new file mode 100644 index 0000000000..efb2deef28 Binary files /dev/null and b/jme3-testdata/src/main/resources/Textures/Sky/Alien.png differ