Skip to content

Commit 89fbfc0

Browse files
riccardoblyaRnMcDonutsrichardTingle
authored
Improve light render parity with blender (#2549)
* improve light render parity with blender * cleanup * fix merge issue * Update LightsPunctualExtensionLoader.java * Accept changed screenshot reference images for blender light * cleanup and parity test * tweaks * tweaks * screenshots --------- Co-authored-by: Ryan McDonough <peanut64646@gmail.com> Co-authored-by: Richard Tingle <6330028+richardTingle@users.noreply.github.com>
1 parent 2d753ca commit 89fbfc0

9 files changed

Lines changed: 148 additions & 100 deletions

File tree

jme3-core/src/main/resources/Common/ShaderLib/module/pbrlighting/PBRLightingUtils.glsllib

Lines changed: 32 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -229,49 +229,52 @@
229229

230230

231231
#if defined(ENABLE_PBRLightingUtils_computeDirectLightContribution) || defined(ENABLE_PBRLightingUtils_computeLightInWorldSpace)
232-
void PBRLightingUtils_computeLightInWorldSpace(vec3 worldPos,vec3 worldNormal, vec3 viewDir, inout Light light){
232+
void PBRLightingUtils_computeLightInWorldSpace(vec3 worldPos, vec3 worldNormal, vec3 viewDir, inout Light light){
233233
if(light.ready) return;
234234

235-
// lightComputeDir
236-
float posLight = step(0.5, light.type);
237-
light.vector = light.position.xyz * sign(posLight - 0.5) - (worldPos * posLight); //tempVec lightVec
235+
float posLight = step(0.5, light.type);
236+
light.vector = light.position.xyz * sign(posLight - 0.5) - (worldPos * posLight);
238237

239-
vec3 L; // lightDir
238+
vec3 L;
240239
float dist;
241-
Math_lengthAndNormalize(light.vector,dist,L);
240+
Math_lengthAndNormalize(light.vector, dist, L);
242241

243-
float invRange=light.invRadius; // position.w
244-
const float light_threshold = 0.01;
242+
if (posLight > 0.5) {
243+
float clampedDist = max(dist, 0.00001);
244+
float invSq = 1.0 / (clampedDist * clampedDist);
245245

246-
#ifdef SRGB
247-
light.fallOff = (1.0 - invRange * dist) / (1.0 + invRange * dist * dist); // lightDir.w
248-
light.fallOff = clamp(light.fallOff, 1.0 - posLight, 1.0);
249-
#else
250-
light.fallOff = clamp(1.0 - invRange * dist * posLight, 0.0, 1.0);
251-
#endif
246+
float radius = 1.0 / light.invRadius;
247+
float rangeAtt = 1.0;
248+
249+
if (radius > 0.0) {
250+
float x = dist / radius;
251+
rangeAtt = clamp(1.0 - pow(x, 4.0), 0.0, 1.0);
252+
}
253+
254+
light.fallOff = invSq * rangeAtt;
255+
} else {
256+
light.fallOff = 1.0;
257+
}
252258

253-
// computeSpotFalloff
254-
if(light.type>1.){
255-
vec3 spotdir = normalize(light.spotDirection);
256-
float curAngleCos = dot(-L, spotdir);
259+
if (light.type > 1.0) {
257260
float innerAngleCos = floor(light.spotAngleCos) * 0.001;
258261
float outerAngleCos = fract(light.spotAngleCos);
259-
float innerMinusOuter = innerAngleCos - outerAngleCos;
260-
float falloff = clamp((curAngleCos - outerAngleCos) / innerMinusOuter, 0.0, 1.0);
261-
#ifdef SRGB
262-
// Use quadratic falloff (notice the ^4)
263-
falloff = pow(clamp((curAngleCos - outerAngleCos) / innerMinusOuter, 0.0, 1.0), 4.0);
264-
#endif
265-
light.fallOff*=falloff;
266-
}
267262

263+
vec3 spotDir = normalize(light.spotDirection);
264+
float cosA = dot(-L, spotDir);
265+
float denom = max(innerAngleCos - outerAngleCos, 0.0001);
266+
float spotAtten = clamp((cosA - outerAngleCos) / denom, 0.0, 1.0);
267+
268+
light.fallOff *= spotAtten;
269+
}
268270

269-
vec3 h=normalize(L+viewDir);
270-
light.dir=L;
271+
vec3 h = normalize(L + viewDir);
272+
light.dir = L;
271273
light.NdotL = max(dot(worldNormal, L), 0.0);
272274
light.NdotH = max(dot(worldNormal, h), 0.0);
273275
light.LdotH = max(dot(L, h), 0.0);
274-
light.HdotV = max(dot(viewDir,h), 0.);
276+
light.HdotV = max(dot(viewDir, h), 0.0);
277+
light.ready = true;
275278
}
276279
#endif
277280

jme3-effects/src/main/resources/Common/MatDefs/Post/KHRToneMap.frag

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@ uniform sampler2DMS m_Texture;
1818

1919
vec4 applyToneMap() {
2020
ivec2 iTexC = ivec2(texCoord * vec2(textureSize(m_Texture)));
21-
vec4 color = vec4(0.0);
21+
vec4 hdrColor = vec4(0.0);
2222
for (int i = 0; i < NUM_SAMPLES; i++) {
23-
vec4 hdrColor = texelFetch(m_Texture, iTexC, i);
24-
vec3 ldrColor = applyCurve(hdrColor.rgb);
25-
color += vec4(ldrColor, hdrColor.a);
23+
hdrColor += texelFetch(m_Texture, iTexC, i);
2624
}
27-
return color / float(NUM_SAMPLES);
25+
hdrColor /= float(NUM_SAMPLES);
26+
vec3 ldrColor = vec4(applyCurve(hdrColor.rgb), hdrColor.a);
27+
return ldrColor;
2828
}
2929

3030
#else
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package jme3test.light;
2+
3+
import com.jme3.app.SimpleApplication;
4+
import com.jme3.environment.EnvironmentProbeControl;
5+
import com.jme3.input.ChaseCamera;
6+
import com.jme3.math.FastMath;
7+
import com.jme3.math.Quaternion;
8+
import com.jme3.math.Vector3f;
9+
import com.jme3.post.FilterPostProcessor;
10+
import com.jme3.post.filters.KHRToneMapFilter;
11+
import com.jme3.scene.Node;
12+
import com.jme3.scene.Spatial;
13+
import com.jme3.util.SkyFactory;
14+
15+
/**
16+
* Test how lights render compared to the same scene in Blender. Open
17+
* jme3-testdata/src/main/resources/BlenderParity/scene.blend in blender to compare.
18+
*/
19+
public class TestLightImportParity extends SimpleApplication {
20+
public static void main(String[] args) {
21+
TestLightImportParity app = new TestLightImportParity();
22+
app.start();
23+
}
24+
25+
@Override
26+
public void simpleInitApp() {
27+
28+
flyCam.setDragToRotate(true);
29+
flyCam.setMoveSpeed(100f);
30+
31+
Spatial sky = SkyFactory.createSky(assetManager, "Textures/Sky/Alien.png", SkyFactory.EnvMapType.EquirectMap);
32+
sky.rotate(new Quaternion().fromAngleAxis(FastMath.PI, Vector3f.UNIT_Y));
33+
rootNode.attachChild(sky);
34+
35+
EnvironmentProbeControl probe = new EnvironmentProbeControl(assetManager, 512);
36+
rootNode.addControl(probe);
37+
probe.tag(sky);
38+
39+
Node scene = (Node)assetManager.loadModel("BlenderParity/scene.glb");
40+
rootNode.attachChild(scene);
41+
42+
KHRToneMapFilter toneMap = new KHRToneMapFilter();
43+
FilterPostProcessor fpp = new FilterPostProcessor(assetManager);
44+
fpp.addFilter(toneMap);
45+
viewPort.addProcessor(fpp);
46+
47+
ChaseCamera chaseCam = new ChaseCamera(cam, scene, inputManager);
48+
chaseCam.setDefaultDistance(100);
49+
chaseCam.setMaxDistance(200);
50+
51+
52+
53+
54+
}
55+
}

jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfUtils.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -795,7 +795,10 @@ public static ColorRGBA getAsColor(JsonObject parent, String name) {
795795
return null;
796796
}
797797
JsonArray color = el.getAsJsonArray();
798-
return new ColorRGBA(color.get(0).getAsFloat(), color.get(1).getAsFloat(), color.get(2).getAsFloat(), color.size() > 3 ? color.get(3).getAsFloat() : 1f);
798+
// glTF colors are authored in linear space unless the spec says otherwise.
799+
return new ColorRGBA().set(
800+
color.get(0).getAsFloat(), color.get(1).getAsFloat(), color.get(2).getAsFloat(), color.size() > 3 ? color.get(3).getAsFloat() : 1f
801+
);
799802
}
800803

801804
public static ColorRGBA getAsColor(JsonObject parent, String name, ColorRGBA defaultValue) {

jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/LightsPunctualExtensionLoader.java

Lines changed: 52 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2009-2024 jMonkeyEngine
2+
* Copyright (c) 2009-2026 jMonkeyEngine
33
* All rights reserved.
44
*
55
* Redistribution and use in source and binary forms, with or without
@@ -53,9 +53,11 @@
5353
*
5454
* Supports directional, point, and spot lights.
5555
*
56-
* Created by Trevor Flynn - 3/23/2021
56+
* @author Trevor Flynn, Riccardo Balbo
5757
*/
5858
public class LightsPunctualExtensionLoader implements ExtensionLoader {
59+
private static final boolean COMPUTE_LIGHT_RANGE = true;
60+
private static final float GLTF_LIGHT_COMPAT_SCALE = 0.0009f;
5961

6062
private final HashSet<NodeNeedingLight> pendingNodes = new HashSet<>();
6163
private final HashMap<Integer, Light> lightDefinitions = new HashMap<>();
@@ -126,13 +128,11 @@ private SpotLight buildSpotLight(JsonObject obj) {
126128

127129
float intensity = obj.has("intensity") ? obj.get("intensity").getAsFloat() : 1.0f;
128130
ColorRGBA color = obj.has("color") ? GltfUtils.getAsColor(obj, "color") : new ColorRGBA(ColorRGBA.White);
129-
color = lumensToColor(color, intensity);
130-
float range = obj.has("range") ? obj.get("range").getAsFloat() : Float.POSITIVE_INFINITY;
131131

132132
//Spot specific
133133
JsonObject spot = obj.getAsJsonObject("spot");
134-
float innerConeAngle = spot != null && spot.has("innerConeAngle") ? spot.get("innerConeAngle").getAsFloat() : 0f;
135-
float outerConeAngle = spot != null && spot.has("outerConeAngle") ? spot.get("outerConeAngle").getAsFloat() : ((float) Math.PI) / 4f;
134+
float innerConeAngle = (spot != null && spot.has("innerConeAngle")) ? spot.get("innerConeAngle").getAsFloat() : 0f;
135+
float outerConeAngle = (spot != null && spot.has("outerConeAngle"))? spot.get("outerConeAngle").getAsFloat() : (FastMath.PI / 4f);
136136

137137
/*
138138
Correct floating point error on half PI, GLTF spec says that the outerConeAngle
@@ -143,9 +143,12 @@ private SpotLight buildSpotLight(JsonObject obj) {
143143
outerConeAngle = FastMath.HALF_PI - 0.000001f;
144144
}
145145

146+
float scaledIntensity = toCompatIntensity(intensity);
147+
148+
float range = obj.has("range") ? obj.get("range").getAsFloat() : (COMPUTE_LIGHT_RANGE ? getCutoffDistance(color, scaledIntensity) : Float.POSITIVE_INFINITY);
146149
SpotLight spotLight = new SpotLight(true);
147150
spotLight.setName(name);
148-
spotLight.setColor(color);
151+
spotLight.setColor(applyScaledIntensity(color, scaledIntensity));
149152
spotLight.setSpotRange(range);
150153
spotLight.setSpotInnerAngle(innerConeAngle);
151154
spotLight.setSpotOuterAngle(outerConeAngle);
@@ -165,11 +168,11 @@ private DirectionalLight buildDirectionalLight(JsonObject obj) {
165168

166169
float intensity = obj.has("intensity") ? obj.get("intensity").getAsFloat() : 1.0f;
167170
ColorRGBA color = obj.has("color") ? GltfUtils.getAsColor(obj, "color") : new ColorRGBA(ColorRGBA.White);
168-
color = lumensToColor(color, intensity);
171+
float scaledIntensity = toCompatIntensity(intensity);
169172

170173
DirectionalLight directionalLight = new DirectionalLight(true);
171174
directionalLight.setName(name);
172-
directionalLight.setColor(color);
175+
directionalLight.setColor(applyScaledIntensity(color, scaledIntensity));
173176
directionalLight.setDirection(Vector3f.UNIT_Z.negate());
174177

175178
return directionalLight;
@@ -185,13 +188,16 @@ private PointLight buildPointLight(JsonObject obj) {
185188
String name = obj.has("name") ? obj.get("name").getAsString() : "";
186189

187190
float intensity = obj.has("intensity") ? obj.get("intensity").getAsFloat() : 1.0f;
188-
ColorRGBA color = obj.has("color") ? GltfUtils.getAsColor(obj, "color") : new ColorRGBA(ColorRGBA.White);
189-
color = lumensToColor(color, intensity);
190-
float range = obj.has("range") ? obj.get("range").getAsFloat() : Float.POSITIVE_INFINITY;
191+
ColorRGBA color = obj.has("color") ? GltfUtils.getAsColor(obj, "color")
192+
: new ColorRGBA(ColorRGBA.White);
193+
194+
float scaledIntensity = toCompatIntensity(intensity);
195+
196+
float range = obj.has("range") ? obj.get("range").getAsFloat() : (COMPUTE_LIGHT_RANGE ? getCutoffDistance(color, scaledIntensity) : Float.POSITIVE_INFINITY);
191197

192198
PointLight pointLight = new PointLight(true);
193199
pointLight.setName(name);
194-
pointLight.setColor(color);
200+
pointLight.setColor(applyScaledIntensity(color, scaledIntensity));
195201
pointLight.setRadius(range);
196202

197203
return pointLight;
@@ -206,7 +212,7 @@ private PointLight buildPointLight(JsonObject obj) {
206212
*/
207213
private void addLight(Node parent, Node node, int lightIndex) {
208214
if (lightDefinitions.containsKey(lightIndex)) {
209-
Light light = lightDefinitions.get(lightIndex);
215+
Light light = lightDefinitions.get(lightIndex).clone();
210216
parent.addLight(light);
211217
LightControl control = new LightControl(light);
212218
control.setInvertAxisDirection(true);
@@ -216,55 +222,17 @@ private void addLight(Node parent, Node node, int lightIndex) {
216222
}
217223
}
218224

219-
/**
220-
* Convert a floating point lumens value into a color that
221-
* represents both color and brightness of the light.
222-
*
223-
* @param color The base color of the light
224-
* @param lumens The lumens value to convert to a color
225-
* @return A color representing the intensity of the given lumens encoded into the given color
226-
*/
227-
private ColorRGBA lumensToColor(ColorRGBA color, float lumens) {
228-
ColorRGBA brightnessModifier = lumensToColor(lumens);
229-
return color.mult(brightnessModifier);
225+
private float toCompatIntensity(float intensity) {
226+
return intensity * GLTF_LIGHT_COMPAT_SCALE;
230227
}
231228

232-
/**
233-
* Convert a floating point lumens value into a grayscale color that
234-
* represents a brightness.
235-
*
236-
* @param lumens The lumens value to convert to a color
237-
* @return A color representing the intensity of the given lumens
238-
*/
239-
private ColorRGBA lumensToColor(float lumens) {
240-
/*
241-
Taken from /Common/ShaderLib/Hdr.glsllib
242-
vec4 HDR_EncodeLum(in float lum){
243-
float Le = 2.0 * log2(lum + epsilon) + 127.0;
244-
vec4 result = vec4(0.0);
245-
result.a = fract(Le);
246-
result.rgb = vec3((Le - (floor(result.a * 255.0)) / 255.0) / 255.0);
247-
return result;
248-
*/
249-
float epsilon = 0.0001f;
250-
251-
double Le = 2f * Math.log(lumens * epsilon) / Math.log(2) + 127.0;
252-
ColorRGBA color = new ColorRGBA();
253-
color.a = (float) (Le - Math.floor(Le)); //Get fractional part
254-
float val = (float) ((Le - (Math.floor(color.a * 255.0)) / 255.0) / 255.0);
255-
color.r = val;
256-
color.g = val;
257-
color.b = val;
258-
259-
return color;
229+
private ColorRGBA applyScaledIntensity(ColorRGBA color, float scaledIntensity) {
230+
return color.mult(scaledIntensity);
260231
}
261232

262-
/**
263-
* A bean to contain the relation between a node and a light index
264-
*/
265233
private static class NodeNeedingLight {
266-
private Node node;
267-
private int lightIndex;
234+
private final Node node;
235+
private final int lightIndex;
268236

269237
private NodeNeedingLight(Node node, int lightIndex) {
270238
this.node = node;
@@ -275,16 +243,35 @@ private Node getNode() {
275243
return node;
276244
}
277245

278-
private void setNode(Node node) {
279-
this.node = node;
280-
}
281-
282246
private int getLightIndex() {
283247
return lightIndex;
284248
}
249+
}
250+
/**
251+
* Computes the effective cutoff distance of a light based on its raw color and intensity. Uses
252+
* inverse-square attenuation and a perceptual visibility threshold.
253+
*
254+
* @param color
255+
* The base RGB color of the light (linear space)
256+
* @param intensity
257+
* The light's intensity in lumens (or equivalent)
258+
* @return The cutoff distance where the light falls below a visible threshold
259+
*/
260+
private float getCutoffDistance(ColorRGBA color, float scaledIntensity) {
261+
final float visibleThreshold = 0.001f;
262+
final float maxRange = 10000f;
285263

286-
private void setLightIndex(int lightIndex) {
287-
this.lightIndex = lightIndex;
264+
// Compute the max channel (R/G/B) for luminance estimation
265+
float maxComponent = Math.max(Math.max(color.r, color.g), color.b);
266+
267+
if (maxComponent <= 0f || scaledIntensity <= 0f) {
268+
return 0f;
288269
}
270+
271+
// The actual light output (lux at 1 meter) per component
272+
float effectiveIntensity = maxComponent * scaledIntensity;
273+
// Inverse-square attenuation: intensity / d^2 = visibleThreshold
274+
float range = (float) Math.sqrt(effectiveIntensity / visibleThreshold);
275+
return Math.min(range, maxRange);
289276
}
290-
}
277+
}
41.9 MB
Loading
199 KB
Binary file not shown.
125 KB
Binary file not shown.
2.46 MB
Loading

0 commit comments

Comments
 (0)