Skip to content

Commit 5e48cb0

Browse files
authored
Fix PBR sunlight exposure inputs (#3000)
* Honor PBR sunlight exposure inputs * Support standalone sunlight exposure and render it in integration tests * Remove shader source-text tests superseded by pixel coverage * Add cross-platform PBR sunlight screenshot coverage * Add reviewed CI baselines for PBR sunlight screenshots * Dispose PBR test framebuffer through its object manager
1 parent f972c7c commit 5e48cb0

13 files changed

Lines changed: 576 additions & 11 deletions

File tree

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

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#ifndef __PBR_LIGHT_UTILS_MODULE__
22
#define __PBR_LIGHT_UTILS_MODULE__
3+
4+
vec2 newTexCoord;
35

46
#import "Common/ShaderLib/Math.glsllib"
57
#import "Common/ShaderLib/PBR.glsllib"
@@ -33,7 +35,7 @@
3335
varying vec4 wTangent;
3436
#endif
3537

36-
#if defined(ENABLE_PBRLightingUtils_readPBRSurface)||defined(ENABLE_PBRLightingUtils_getTexCoord)
38+
#if defined(ENABLE_PBRLightingUtils_readPBRSurface)||defined(ENABLE_PBRLightingUtils_getTexCoord)||defined(EXPOSUREMAP)
3739
varying vec2 texCoord;
3840
#ifdef SEPARATE_TEXCOORD
3941
varying vec2 texCoord2;
@@ -138,23 +140,26 @@
138140
#if defined(EXPOSUREMAP)
139141
uniform sampler2D m_SunLightExposureMap;
140142
#endif
141-
#if defined(USE_VERTEX_COLORS_AS_SUN_EXPOSURE)
143+
#if defined(USE_VERTEX_COLORS_AS_SUN_EXPOSURE) || defined(USE_VERTEX_COLORS_AS_SUN_INTENSITY)
142144
varying vec4 vertColors;
143145
#endif
144-
#ifdef STATIC_SUN_EXPOSURE
146+
#if defined(STATIC_SUN_EXPOSURE) || defined(STATIC_SUN_INTENSITY)
145147
uniform float m_StaticSunIntensity;
146148
#endif
147149

148150
void PBRLightingUtils_readSunLightExposureParams(inout PBRSurface surface){
149151

150152
surface.exposure = 1.0; //default value
151-
#ifdef EXPOSUREMAP
153+
#ifdef EXPOSUREMAP
154+
#ifndef ENABLE_PBRLightingUtils_readPBRSurface
155+
newTexCoord = texCoord;
156+
#endif
152157
surface.exposure *= texture2D(m_SunLightExposureMap, newTexCoord).r;
153158
#endif
154-
#ifdef STATIC_SUN_EXPOSURE
159+
#if defined(STATIC_SUN_EXPOSURE) || defined(STATIC_SUN_INTENSITY)
155160
surface.exposure *= m_StaticSunIntensity; //single float value to indicate percentage of sunlight hitting the model (only suitable for small models or models with equal sunlight exposure accross the entire model
156161
#endif
157-
#ifdef USE_VERTEX_COLORS_AS_SUN_EXPOSURE
162+
#if defined(USE_VERTEX_COLORS_AS_SUN_EXPOSURE) || defined(USE_VERTEX_COLORS_AS_SUN_INTENSITY)
158163
surface.exposure *= vertColors.r; // use red channel of vertexColors for non-uniform sunlighting accross a single model
159164
#endif
160165
}
@@ -321,7 +326,6 @@
321326
}
322327

323328
#ifdef ENABLE_PBRLightingUtils_readPBRSurface
324-
vec2 newTexCoord;
325329

326330
void PBRLightingUtils_readPBRSurface(inout PBRSurface surface){
327331

@@ -346,6 +350,7 @@
346350
#endif
347351

348352

353+
newTexCoord = texCoord;
349354
#if (defined(PARALLAXMAP) || (defined(NORMALMAP_PARALLAX) && defined(NORMALMAP)))
350355
if (surface.hasTangents) {
351356
vec3 vViewDir = surface.viewDir * surface.tbnMat;
@@ -366,11 +371,7 @@
366371
newTexCoord = classicParallaxOffset(m_ParallaxMap, vViewDir, texCoord, m_ParallaxHeight);
367372
#endif
368373
#endif
369-
} else {
370-
newTexCoord = texCoord;
371374
}
372-
#else
373-
newTexCoord = texCoord;
374375
#endif
375376

376377
#ifdef BASECOLORMAP
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright (c) 2026 jMonkeyEngine
3+
* All rights reserved.
4+
*
5+
* Redistribution and use in source and binary forms, with or without
6+
* modification, are permitted provided that the following conditions are
7+
* met:
8+
*
9+
* * Redistributions of source code must retain the above copyright
10+
* notice, this list of conditions and the following disclaimer.
11+
*
12+
* * Redistributions in binary form must reproduce the above copyright
13+
* notice, this list of conditions and the following disclaimer in the
14+
* documentation and/or other materials provided with the distribution.
15+
*
16+
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
17+
* may be used to endorse or promote products derived from this software
18+
* without specific prior written permission.
19+
*
20+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
22+
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
23+
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
24+
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
25+
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
26+
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
27+
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
28+
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
29+
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
30+
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31+
*/
32+
package org.jmonkeyengine.screenshottests.android.light.pbr;
33+
34+
import androidx.test.ext.junit.runners.AndroidJUnit4;
35+
import org.jmonkeyengine.screenshottests.android.android.ScreenshotTestAndroidBase;
36+
import org.jmonkeyengine.screenshottests.scenarios.light.pbr.ScenarioPBRSunlight;
37+
import org.jmonkeyengine.screenshottests.testframework.AndroidRunner;
38+
import org.junit.Test;
39+
import org.junit.runner.RunWith;
40+
41+
/** Runs the same sunlight exposure charts on Android GLES. */
42+
@RunWith(AndroidJUnit4.class)
43+
public class TestPBRSunlight extends ScreenshotTestAndroidBase {
44+
45+
@Test
46+
public void testSunlightInputs() {
47+
ScenarioPBRSunlight.testSunlightInputs().run(new AndroidRunner());
48+
}
49+
50+
@Test
51+
public void testSunlightParallax() {
52+
ScenarioPBRSunlight.testSunlightParallax().run(new AndroidRunner());
53+
}
54+
}
Loading
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright (c) 2026 jMonkeyEngine
3+
* All rights reserved.
4+
*
5+
* Redistribution and use in source and binary forms, with or without
6+
* modification, are permitted provided that the following conditions are
7+
* met:
8+
*
9+
* * Redistributions of source code must retain the above copyright
10+
* notice, this list of conditions and the following disclaimer.
11+
*
12+
* * Redistributions in binary form must reproduce the above copyright
13+
* notice, this list of conditions and the following disclaimer in the
14+
* documentation and/or other materials provided with the distribution.
15+
*
16+
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
17+
* may be used to endorse or promote products derived from this software
18+
* without specific prior written permission.
19+
*
20+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
22+
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
23+
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
24+
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
25+
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
26+
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
27+
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
28+
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
29+
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
30+
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31+
*/
32+
package org.jmonkeyengine.screenshottests.light.pbr;
33+
34+
import org.jmonkeyengine.screenshottests.scenarios.light.pbr.ScenarioPBRSunlight;
35+
import org.jmonkeyengine.screenshottests.testframework.desktop.DesktopRunner;
36+
import org.jmonkeyengine.screenshottests.testframework.desktop.ScreenshotTestDesktopBase;
37+
import org.junit.jupiter.api.Test;
38+
39+
/** Sunlight exposure reference images for desktop OpenGL and ANGLE. */
40+
public class TestPBRSunlight extends ScreenshotTestDesktopBase {
41+
42+
@Test
43+
public void testSunlightInputs() {
44+
ScenarioPBRSunlight.testSunlightInputs().run(new DesktopRunner());
45+
}
46+
47+
@Test
48+
public void testSunlightParallax() {
49+
ScenarioPBRSunlight.testSunlightParallax().run(new DesktopRunner());
50+
}
51+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
/*
2+
* Copyright (c) 2009-2026 jMonkeyEngine
3+
* All rights reserved.
4+
*
5+
* Redistribution and use in source and binary forms, with or without
6+
* modification, are permitted provided that the following conditions are
7+
* met:
8+
*
9+
* * Redistributions of source code must retain the above copyright
10+
* notice, this list of conditions and the following disclaimer.
11+
*
12+
* * Redistributions in binary form must reproduce the above copyright
13+
* notice, this list of conditions and the following disclaimer in the
14+
* documentation and/or other materials provided with the distribution.
15+
*
16+
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
17+
* may be used to endorse or promote products derived from this software
18+
* without specific prior written permission.
19+
*
20+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
22+
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
23+
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
24+
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
25+
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
26+
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
27+
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
28+
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
29+
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
30+
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31+
*/
32+
package org.jmonkeyengine.screenshottests.light.pbr;
33+
34+
import static org.junit.jupiter.api.Assertions.assertEquals;
35+
36+
import com.jme3.asset.AssetManager;
37+
import com.jme3.material.Material;
38+
import com.jme3.material.RenderState;
39+
import com.jme3.math.ColorRGBA;
40+
import com.jme3.math.Vector3f;
41+
import com.jme3.renderer.Camera;
42+
import com.jme3.renderer.RenderManager;
43+
import com.jme3.renderer.Renderer;
44+
import com.jme3.scene.Geometry;
45+
import com.jme3.scene.VertexBuffer;
46+
import com.jme3.scene.shape.Quad;
47+
import com.jme3.system.AppSettings;
48+
import com.jme3.texture.FrameBuffer;
49+
import com.jme3.texture.Image;
50+
import com.jme3.texture.Texture;
51+
import com.jme3.texture.Texture2D;
52+
import com.jme3.texture.image.ColorSpace;
53+
import com.jme3.util.BufferUtils;
54+
import java.nio.ByteBuffer;
55+
import java.util.concurrent.CompletableFuture;
56+
import java.util.concurrent.CountDownLatch;
57+
import java.util.concurrent.TimeUnit;
58+
import org.jmonkeyengine.screenshottests.testframework.TestContainingApp;
59+
import org.jmonkeyengine.screenshottests.testframework.desktop.DesktopRunner;
60+
import org.junit.jupiter.api.Tag;
61+
import org.junit.jupiter.api.Test;
62+
63+
/** Compiles and renders exposure variants on both desktop and ANGLE CI jobs. */
64+
@Tag("integration")
65+
public class TestPbrSunExposure {
66+
67+
@Test
68+
public void sunlightInputsRenderExpectedExposure() throws Exception {
69+
CompletableFuture<Void> result = new CompletableFuture<>();
70+
CountDownLatch finished = new CountDownLatch(1);
71+
final TestContainingApp app = new TestContainingApp() {
72+
@Override
73+
public void simpleInitApp() {
74+
try {
75+
verifyExposure(renderManager, assetManager);
76+
result.complete(null);
77+
} catch (Throwable failure) {
78+
result.completeExceptionally(failure);
79+
} finally {
80+
finished.countDown();
81+
}
82+
}
83+
84+
@Override
85+
public void handleError(String message, Throwable failure) {
86+
result.completeExceptionally(failure == null ? new AssertionError(message) : failure);
87+
stop();
88+
finished.countDown();
89+
}
90+
};
91+
AppSettings settings = new AppSettings(true);
92+
settings.setResolution(16, 16);
93+
settings.setAudioRenderer(null);
94+
settings.setUseInput(false);
95+
settings.setRenderer(System.getProperty("jme.screenshot.renderer", AppSettings.LWJGL_OPENGL45));
96+
app.setSettings(settings);
97+
app.setShowSettings(false);
98+
new DesktopRunner().runApplicationUntilScenarioCompletes(app, finished);
99+
result.get(1, TimeUnit.SECONDS);
100+
}
101+
102+
static void verifyExposure(RenderManager manager, AssetManager assets) {
103+
Renderer renderer = manager.getRenderer();
104+
renderer.setMainFrameBufferSrgb(false);
105+
renderer.setLinearizeSrgbImages(false);
106+
Camera camera = new Camera(16, 16);
107+
camera.setParallelProjection(true);
108+
camera.setFrustum(0.1f, 10, -1, 1, 1, -1);
109+
camera.setLocation(new Vector3f(0, 0, 2));
110+
camera.lookAt(Vector3f.ZERO, Vector3f.UNIT_Y);
111+
manager.setCamera(camera, false);
112+
FrameBuffer target = new FrameBuffer(16, 16, 1);
113+
target.setColorBuffer(Image.Format.RGBA8);
114+
target.setDepthBuffer(Image.Format.Depth24);
115+
try {
116+
renderer.setFrameBuffer(target);
117+
for (String variant : new String[]{"static", "vertex", "map", "parallax", "no-tangents",
118+
"module-map", "module-control"}) {
119+
boolean module = variant.startsWith("module");
120+
Material material = new Material(assets, module
121+
? "TestMatDefs/PbrSunExposureModule.j3md" : "Common/MatDefs/Light/PBRLighting.j3md");
122+
if (!module) {
123+
material.setInt("DebugValuesMode", 6);
124+
material.setBoolean("UseSpecularAA", false);
125+
}
126+
material.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
127+
Geometry geometry = new Geometry("exposure", new Quad(2, 2));
128+
geometry.move(-1, -1, 0);
129+
geometry.setMaterial(material);
130+
if (variant.equals("static")) {
131+
material.setFloat("StaticSunIntensity", 0.25f);
132+
} else if (variant.equals("vertex")) {
133+
material.setBoolean("UseVertexColorsAsSunIntensity", true);
134+
geometry.getMesh().setBuffer(VertexBuffer.Type.Color, 4, new float[]{
135+
0.25f, 0, 0, 1, 0.25f, 0, 0, 1, 0.25f, 0, 0, 1, 0.25f, 0, 0, 1});
136+
} else if (!variant.equals("module-control")) {
137+
material.setTexture("SunLightExposureMap", exposureMap());
138+
}
139+
if (variant.equals("parallax") || variant.equals("no-tangents")) {
140+
material.setTexture("ParallaxMap", exposureMap());
141+
material.setFloat("ParallaxHeight", 0);
142+
if (variant.equals("parallax")) {
143+
geometry.getMesh().setBuffer(VertexBuffer.Type.Tangent, 4, new float[]{
144+
1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1});
145+
}
146+
}
147+
renderer.setBackgroundColor(ColorRGBA.Black);
148+
renderer.clearBuffers(true, true, true);
149+
geometry.updateGeometricState();
150+
manager.renderGeometry(geometry);
151+
ByteBuffer pixels = BufferUtils.createByteBuffer(16 * 16 * 4);
152+
renderer.readFrameBufferWithFormat(target, pixels, Image.Format.RGBA8);
153+
int left = variant.equals("module-control") ? 255 : 64;
154+
int right = variant.equals("static") || variant.equals("vertex") ? 64
155+
: variant.equals("module-control") ? 255 : 192;
156+
assertPixel(pixels, 4, left, variant);
157+
assertPixel(pixels, 12, right, variant);
158+
}
159+
} finally {
160+
renderer.setFrameBuffer(null);
161+
target.dispose();
162+
}
163+
}
164+
165+
private static Texture2D exposureMap() {
166+
ByteBuffer data = BufferUtils.createByteBuffer(8);
167+
data.put(new byte[]{64, 64, 64, (byte) 255, (byte) 192, (byte) 192, (byte) 192, (byte) 255}).flip();
168+
Texture2D texture = new Texture2D(new Image(Image.Format.RGBA8, 2, 1, data, ColorSpace.Linear));
169+
texture.setMinFilter(Texture.MinFilter.NearestNoMipMaps);
170+
texture.setMagFilter(Texture.MagFilter.Nearest);
171+
return texture;
172+
}
173+
174+
private static void assertPixel(ByteBuffer pixels, int x, int expected, String variant) {
175+
int offset = (8 * 16 + x) * 4;
176+
for (int channel = 0; channel < 3; channel++) {
177+
assertEquals(expected, pixels.get(offset + channel) & 255, 1, variant + " channel " + channel);
178+
}
179+
assertEquals(255, pixels.get(offset + 3) & 255, variant + " alpha");
180+
}
181+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#import "Common/ShaderLib/GLSLCompat.glsllib"
2+
#define ENABLE_PBRLightingUtils_getWorldPosition 1
3+
#define ENABLE_PBRLightingUtils_getWorldNormal 1
4+
#define ENABLE_PBRLightingUtils_computeDirectLight 1
5+
#import "Common/ShaderLib/module/pbrlighting/PBRLightingUtils.glsllib"
6+
7+
void main() {
8+
PBRSurface surface;
9+
PBRLightingUtils_readSunLightExposureParams(surface);
10+
gl_FragColor = vec4(vec3(surface.exposure), 1.0);
11+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
MaterialDef PbrSunExposureModule {
2+
MaterialParameters {
3+
Texture2D SunLightExposureMap
4+
}
5+
Technique {
6+
VertexShader GLSL310 GLSL300 GLSL150: Common/MatDefs/Light/PBRLighting.vert
7+
FragmentShader GLSL310 GLSL300 GLSL150: TestMatDefs/PbrSunExposureModule.frag
8+
WorldParameters {
9+
WorldViewProjectionMatrix
10+
WorldMatrix
11+
NormalMatrix
12+
}
13+
Defines {
14+
EXPOSUREMAP: SunLightExposureMap
15+
}
16+
}
17+
}
Loading
Loading

0 commit comments

Comments
 (0)