From 9eb6817ca68dfe557a03db0bfb6cafdd73cc811d Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Sat, 16 May 2026 23:05:27 +0200 Subject: [PATCH 1/2] singlepass sph baker and fast path --- .../environment/EnvironmentProbeControl.java | 57 +++++++- .../environment/baker/IBLGLEnvBakerLight.java | 125 ++++++++++++++---- .../resources/Common/IBLSphH/IBLSphH.frag | 75 +++++++---- .../resources/Common/IBLSphH/IBLSphH.j3md | 6 +- jme3-examples/build.gradle | 2 +- .../jme3test/light/pbr/TestPBRSimple.java | 21 ++- 6 files changed, 222 insertions(+), 64 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/environment/EnvironmentProbeControl.java b/jme3-core/src/main/java/com/jme3/environment/EnvironmentProbeControl.java index 82d45f32de..683244f303 100644 --- a/jme3-core/src/main/java/com/jme3/environment/EnvironmentProbeControl.java +++ b/jme3-core/src/main/java/com/jme3/environment/EnvironmentProbeControl.java @@ -36,7 +36,6 @@ import java.util.function.Predicate; import com.jme3.asset.AssetManager; import com.jme3.environment.baker.IBLGLEnvBakerLight; -import com.jme3.environment.baker.IBLHybridEnvBakerLight; import com.jme3.export.InputCapsule; import com.jme3.export.JmeExporter; import com.jme3.export.JmeImporter; @@ -49,7 +48,6 @@ import com.jme3.scene.Node; import com.jme3.scene.Spatial; import com.jme3.scene.control.Control; -import com.jme3.texture.Image.Format; /** * A control that automatically handles environment bake and rebake including @@ -87,6 +85,8 @@ public class EnvironmentProbeControl extends LightProbe implements Control { private float frustumNear = 0.001f, frustumFar = 1000f; private String uuid = "none"; private boolean enabled = true; + private IBLGLEnvBakerLight.SphericalHarmonicsMode sphericalHarmonicsMode = + IBLGLEnvBakerLight.SphericalHarmonicsMode.AUTO; private Predicate filter = (s) -> { return s.getUserData("tags.env") != null || s.getUserData("tags.env.env" + uuid) != null; @@ -187,7 +187,18 @@ public static void untagGlobal(Spatial s) { @Override public Control cloneForSpatial(Spatial spatial) { - throw new UnsupportedOperationException(); + EnvironmentProbeControl control = new EnvironmentProbeControl(); + control.setAssetManager(assetManager); + control.setFrustumFar(frustumFar); + control.setFrustumNear(frustumNear); + control.setRequiredSavableResults(requiredSavableResults); + control.setEnabled(enabled); + control.setSphericalHarmonicsMode(sphericalHarmonicsMode); + control.envMapSize = envMapSize; + control.uuid = uuid; + control.filter = filter; + control.spatial = spatial; + return control; } /** @@ -211,6 +222,38 @@ public boolean isRequiredSavableResults() { return requiredSavableResults; } + /** + * Sets how spherical harmonics coefficients are baked by this control. + * + * @param mode the spherical harmonics bake mode + */ + public void setSphericalHarmonicsMode(IBLGLEnvBakerLight.SphericalHarmonicsMode mode) { + if (mode == null) { + throw new IllegalArgumentException("mode cannot be null"); + } + sphericalHarmonicsMode = mode; + } + + /** + * Returns the spherical harmonics bake mode used by this control. + * + * @return the spherical harmonics bake mode + */ + public IBLGLEnvBakerLight.SphericalHarmonicsMode getSphericalHarmonicsMode() { + return sphericalHarmonicsMode; + } + + /** + * Enables or disables the spherical harmonics fast path explicitly. + * + * @param enabled true to use the fast path, false to use the quality path + */ + public void setSphericalHarmonicsFastPathEnabled(boolean enabled) { + setSphericalHarmonicsMode(enabled + ? IBLGLEnvBakerLight.SphericalHarmonicsMode.FAST + : IBLGLEnvBakerLight.SphericalHarmonicsMode.QUALITY); + } + @Override public void setSpatial(Spatial spatial) { if (this.spatial != null && spatial != null && spatial != this.spatial) { @@ -287,8 +330,9 @@ public void setAssetManager(AssetManager assetManager) { } void rebakeNow(RenderManager renderManager) { - IBLHybridEnvBakerLight baker = new IBLGLEnvBakerLight(renderManager, assetManager, null, + IBLGLEnvBakerLight baker = new IBLGLEnvBakerLight(renderManager, assetManager, null, null, envMapSize, envMapSize); + baker.setSphericalHarmonicsMode(sphericalHarmonicsMode); baker.setTexturePulling(isRequiredSavableResults()); baker.bakeEnvironment(spatial, getPosition(), frustumNear, frustumFar, filter); @@ -331,6 +375,8 @@ public void write(JmeExporter ex) throws IOException { oc.write(frustumFar, "frustumFar", 1000f); oc.write(frustumNear, "frustumNear", 0.001f); oc.write(uuid, "envProbeControlUUID", "none"); + oc.write(sphericalHarmonicsMode, "sphericalHarmonicsMode", + IBLGLEnvBakerLight.SphericalHarmonicsMode.AUTO); } @Override @@ -346,6 +392,9 @@ public void read(JmeImporter im) throws IOException { frustumFar = ic.readFloat("frustumFar", 1000f); frustumNear = ic.readFloat("frustumNear", 0.001f); uuid = ic.readString("envProbeControlUUID", "none"); + sphericalHarmonicsMode = ic.readEnum("sphericalHarmonicsMode", + IBLGLEnvBakerLight.SphericalHarmonicsMode.class, + IBLGLEnvBakerLight.SphericalHarmonicsMode.AUTO); } } diff --git a/jme3-core/src/main/java/com/jme3/environment/baker/IBLGLEnvBakerLight.java b/jme3-core/src/main/java/com/jme3/environment/baker/IBLGLEnvBakerLight.java index 54e0bac574..c613e73ea1 100644 --- a/jme3-core/src/main/java/com/jme3/environment/baker/IBLGLEnvBakerLight.java +++ b/jme3-core/src/main/java/com/jme3/environment/baker/IBLGLEnvBakerLight.java @@ -44,6 +44,8 @@ import com.jme3.renderer.RenderManager; import com.jme3.scene.Geometry; import com.jme3.scene.shape.Box; +import com.jme3.system.JmeSystem; +import com.jme3.system.Platform; import com.jme3.texture.FrameBuffer; import com.jme3.texture.Image; import com.jme3.texture.Texture2D; @@ -63,8 +65,31 @@ */ public class IBLGLEnvBakerLight extends IBLHybridEnvBakerLight { private static final int NUM_SH_COEFFICIENT = 9; + private static final int DEFAULT_FAST_SH_SAMPLE_COUNT = 8192; private static final Logger LOG = Logger.getLogger(IBLGLEnvBakerLight.class.getName()); + /** + * Selects how spherical harmonics coefficients are baked on the GPU. + */ + public enum SphericalHarmonicsMode { + /** + * Use full cubemap integration on desktop and the Hammersley fast path on + * Android and iOS. + */ + AUTO, + /** + * Use Hammersley sampling. + */ + FAST, + /** + * Integrate every cubemap texel in a single shader pass. + */ + QUALITY + } + + private SphericalHarmonicsMode sphericalHarmonicsMode = SphericalHarmonicsMode.AUTO; + private int sphericalHarmonicsFastPathSampleCount = DEFAULT_FAST_SH_SAMPLE_COUNT; + /** * Create a new IBL env baker * @@ -91,6 +116,49 @@ public boolean isTexturePulling() { return this.texturePulling; } + /** + * Sets how spherical harmonics coefficients are baked. + * + * @param mode the spherical harmonics bake mode + */ + public void setSphericalHarmonicsMode(SphericalHarmonicsMode mode) { + if (mode == null) { + throw new IllegalArgumentException("mode cannot be null"); + } + this.sphericalHarmonicsMode = mode; + } + + /** + * Returns the current spherical harmonics bake mode. + * + * @return the spherical harmonics bake mode + */ + public SphericalHarmonicsMode getSphericalHarmonicsMode() { + return sphericalHarmonicsMode; + } + + /** + * Sets the sample count used by the Hammersley spherical harmonics fast path. + * + * @param sampleCount the number of samples, must be positive + */ + public void setSphericalHarmonicsFastPathSampleCount(int sampleCount) { + if (sampleCount <= 0) { + throw new IllegalArgumentException("sampleCount must be greater than zero"); + } + this.sphericalHarmonicsFastPathSampleCount = sampleCount; + } + + /** + * Returns the sample count used by the Hammersley spherical harmonics fast path. + * + * @return the Hammersley sample count + */ + public int getSphericalHarmonicsFastPathSampleCount() { + return sphericalHarmonicsFastPathSampleCount; + } + + @Override public void bakeSphericalHarmonicsCoefficients() { Box boxm = new Box(1, 1, 1); @@ -99,8 +167,25 @@ public void bakeSphericalHarmonicsCoefficients() { Material mat = new Material(assetManager, "Common/IBLSphH/IBLSphH.j3md"); mat.setTexture("Texture", envMap); mat.setVector2("Resolution", new Vector2f(envMap.getImage().getWidth(), envMap.getImage().getHeight())); + mat.setInt("SampleCount", sphericalHarmonicsFastPathSampleCount); screen.setMaterial(mat); + switch (sphericalHarmonicsMode) { + case FAST: { + mat.setBoolean("UseFastSphericalHarmonics", true); + break; + } + case QUALITY: { + mat.setBoolean("UseFastSphericalHarmonics", false); + break; + } + case AUTO: { + Platform.Os os = JmeSystem.getPlatform().getOs(); + mat.setBoolean("UseFastSphericalHarmonics", os == Platform.Os.Android || os == Platform.Os.iOS); + break; + } + } + float remapMaxValue = 0; Format format = Format.RGBA32F; if (!renderManager.getRenderer().getCaps().contains(Caps.FloatColorBufferRGBA)) { @@ -117,38 +202,21 @@ public void bakeSphericalHarmonicsCoefficients() { mat.clearParam("RemapMaxValue"); } - Texture2D shCoefTx[] = { new Texture2D(NUM_SH_COEFFICIENT, 1, 1, format), new Texture2D(NUM_SH_COEFFICIENT, 1, 1, format) }; - - FrameBuffer shbaker[] = { new FrameBuffer(NUM_SH_COEFFICIENT, 1, 1), new FrameBuffer(NUM_SH_COEFFICIENT, 1, 1) }; - shbaker[0].setSrgb(false); - shbaker[0].addColorTarget(FrameBufferTarget.newTarget(shCoefTx[0])); - - shbaker[1].setSrgb(false); - shbaker[1].addColorTarget(FrameBufferTarget.newTarget(shCoefTx[1])); + Texture2D shCoefTx = new Texture2D(NUM_SH_COEFFICIENT, 1, 1, format); - int renderOnT = -1; + FrameBuffer shbaker = new FrameBuffer(NUM_SH_COEFFICIENT, 1, 1); + shbaker.setSrgb(false); + shbaker.addColorTarget(FrameBufferTarget.newTarget(shCoefTx)); - for (int faceId = 0; faceId < 6; faceId++) { - if (renderOnT != -1) { - int s = renderOnT; - renderOnT = renderOnT == 0 ? 1 : 0; - mat.setTexture("ShCoef", shCoefTx[s]); - } else { - renderOnT = 0; - } - - mat.setInt("FaceId", faceId); + screen.updateLogicalState(0); + screen.updateGeometricState(); - screen.updateLogicalState(0); - screen.updateGeometricState(); - - renderManager.setCamera(updateAndGetInternalCamera(0, shbaker[renderOnT].getWidth(), shbaker[renderOnT].getHeight(), Vector3f.ZERO, 1, 1000), false); - renderManager.getRenderer().setFrameBuffer(shbaker[renderOnT]); - renderManager.renderGeometry(screen); - } + renderManager.setCamera(updateAndGetInternalCamera(0, shbaker.getWidth(), shbaker.getHeight(), Vector3f.ZERO, 1, 1000), false); + renderManager.getRenderer().setFrameBuffer(shbaker); + renderManager.renderGeometry(screen); - ByteBuffer shCoefRaw = BufferUtils.createByteBuffer(NUM_SH_COEFFICIENT * 1 * (shbaker[renderOnT].getColorTarget().getFormat().getBitsPerPixel() / 8)); - renderManager.getRenderer().readFrameBufferWithFormat(shbaker[renderOnT], shCoefRaw, shbaker[renderOnT].getColorTarget().getFormat()); + ByteBuffer shCoefRaw = BufferUtils.createByteBuffer(NUM_SH_COEFFICIENT * 1 * (shbaker.getColorTarget().getFormat().getBitsPerPixel() / 8)); + renderManager.getRenderer().readFrameBufferWithFormat(shbaker, shCoefRaw, shbaker.getColorTarget().getFormat()); shCoefRaw.rewind(); Image img = new Image(format, NUM_SH_COEFFICIENT, 1, shCoefRaw, ColorSpace.Linear); @@ -176,6 +244,7 @@ else if (weightAccum != c.a) { } EnvMapUtils.prepareShCoefs(shCoef); img.dispose(); + shbaker.dispose(); } } diff --git a/jme3-core/src/main/resources/Common/IBLSphH/IBLSphH.frag b/jme3-core/src/main/resources/Common/IBLSphH/IBLSphH.frag index 220c6ad4ba..aa896e0536 100644 --- a/jme3-core/src/main/resources/Common/IBLSphH/IBLSphH.frag +++ b/jme3-core/src/main/resources/Common/IBLSphH/IBLSphH.frag @@ -15,11 +15,10 @@ in vec3 LocalPos; uniform samplerCube m_Texture; -#ifdef SH_COEF - uniform sampler2D m_ShCoef; +#ifdef FAST_SPHERICAL_HARMONICS + uniform int m_SampleCount; #endif uniform vec2 m_Resolution; -uniform int m_FaceId; const float sqrtPi = sqrt(PI); const float sqrt3Pi = sqrt(3. / PI); @@ -30,7 +29,6 @@ const float sqrt15Pi = sqrt(15. / PI); uniform float m_RemapMaxValue; #endif - vec3 getVectorFromCubemapFaceTexCoord(float x, float y, float mapSize, int face) { float u; float v; @@ -145,6 +143,37 @@ vec3 pixelFaceToV(int faceId, float pixelX, float pixelY, float cubeMapSize) { return normalize(direction); } +#ifdef FAST_SPHERICAL_HARMONICS +void sphHammersleyKernel(int coefficientIndex, out vec3 shCoef, out float weightAccum) { + vec3 texelVect = vec3(0.0); + float shDir = 0.0; + vec4 color = vec4(0.0); + + shCoef = vec3(0.0); + weightAccum = 0.0; + + for(int sampleIndex = 0; sampleIndex < m_SampleCount; sampleIndex++) { + vec4 xi = Hammersley(uint(sampleIndex), uint(m_SampleCount)); + float z = 1.0 - 2.0 * xi.x; + float r = sqrt(max(0.0, 1.0 - z * z)); + float phi = 2.0 * PI * xi.y; + texelVect = vec3(r * cos(phi), z, r * sin(phi)); + evalShBasis(texelVect, coefficientIndex, shDir); + color = texture(m_Texture, texelVect); + shCoef.x = (shCoef.x + color.r * shDir); + shCoef.y = (shCoef.y + color.g * shDir); + shCoef.z = (shCoef.z + color.b * shDir); + weightAccum += 1.0; + } + + #ifdef REMAP_MAX_VALUE + float sampleWeight = 4.0 * PI / float(m_SampleCount); + shCoef.xyz = shCoef.xyz * sampleWeight; + weightAccum = weightAccum * sampleWeight; + #endif +} +#endif + void sphKernel() { int width = int(m_Resolution.x); int height = int(m_Resolution.y); @@ -155,28 +184,26 @@ void sphKernel() { int i=int(gl_FragCoord.x); - #ifdef SH_COEF - vec4 r=texelFetch(m_ShCoef, ivec2(i, 0), 0); - vec3 shCoef=r.rgb; - float weightAccum = r.a; - #else - vec3 shCoef=vec3(0.0); - float weightAccum = 0.0; - #endif + vec3 shCoef=vec3(0.0); + float weightAccum = 0.0; - for(int y = 0; y < height; y++) { - for(int x = 0; x < width; x++) { - weight = getSolidAngleAndVector(float(x), float(y), float(width), m_FaceId, texelVect); - evalShBasis(texelVect, i, shDir); - color = texture(m_Texture, texelVect); - shCoef.x = (shCoef.x + color.r * shDir * weight); - shCoef.y = (shCoef.y + color.g * shDir * weight); - shCoef.z = (shCoef.z + color.b * shDir * weight); - weightAccum += weight; + #ifdef FAST_SPHERICAL_HARMONICS + sphHammersleyKernel(i, shCoef, weightAccum); + #else + for(int faceId = 0; faceId < 6; faceId++) { + for(int y = 0; y < height; y++) { + for(int x = 0; x < width; x++) { + weight = getSolidAngleAndVector(float(x), float(y), float(width), faceId, texelVect); + evalShBasis(texelVect, i, shDir); + color = texture(m_Texture, texelVect); + shCoef.x = (shCoef.x + color.r * shDir * weight); + shCoef.y = (shCoef.y + color.g * shDir * weight); + shCoef.z = (shCoef.z + color.b * shDir * weight); + weightAccum += weight; + } + } } - } - - + #endif #ifdef REMAP_MAX_VALUE shCoef.xyz=shCoef.xyz*m_RemapMaxValue; diff --git a/jme3-core/src/main/resources/Common/IBLSphH/IBLSphH.j3md b/jme3-core/src/main/resources/Common/IBLSphH/IBLSphH.j3md index eaafd2e108..a350cf01d8 100644 --- a/jme3-core/src/main/resources/Common/IBLSphH/IBLSphH.j3md +++ b/jme3-core/src/main/resources/Common/IBLSphH/IBLSphH.j3md @@ -3,9 +3,9 @@ MaterialDef IBLSphH { MaterialParameters { Int BoundDrawBuffer TextureCubeMap Texture -LINEAR - Int FaceId : 0 - Texture2D ShCoef -LINEAR Vector2 Resolution + Int SampleCount + Boolean UseFastSphericalHarmonics Float RemapMaxValue } @@ -26,8 +26,8 @@ MaterialDef IBLSphH { Defines { BOUND_DRAW_BUFFER: BoundDrawBuffer + FAST_SPHERICAL_HARMONICS: UseFastSphericalHarmonics REMAP_MAX_VALUE: RemapMaxValue - SH_COEF: ShCoef } } diff --git a/jme3-examples/build.gradle b/jme3-examples/build.gradle index 6130855c12..0ebd342143 100644 --- a/jme3-examples/build.gradle +++ b/jme3-examples/build.gradle @@ -368,7 +368,7 @@ dependencies { implementation project(':jme3-plugins-json-gson') implementation project(':jme3-terrain') implementation project(':jme3-awt-dialogs') - runtimeOnly project(':jme3-testdata') + implementation project(':jme3-testdata') runtimeOnly libs.nifty.examples // for the "all/intro.xml" example GUI } diff --git a/jme3-examples/src/main/java/jme3test/light/pbr/TestPBRSimple.java b/jme3-examples/src/main/java/jme3test/light/pbr/TestPBRSimple.java index 0f24697cae..33e5c87fe1 100644 --- a/jme3-examples/src/main/java/jme3test/light/pbr/TestPBRSimple.java +++ b/jme3-examples/src/main/java/jme3test/light/pbr/TestPBRSimple.java @@ -34,6 +34,7 @@ import com.jme3.app.SimpleApplication; import com.jme3.environment.EnvironmentProbeControl; +import com.jme3.environment.baker.IBLGLEnvBakerLight.SphericalHarmonicsMode; import com.jme3.input.KeyInput; import com.jme3.input.ChaseCamera; import com.jme3.input.controls.ActionListener; @@ -53,10 +54,13 @@ public class TestPBRSimple extends SimpleApplication { private boolean REALTIME_BAKING = false; private static final String INCREASE_METALLIC = "IncreaseMetallic"; private static final String DECREASE_METALLIC = "DecreaseMetallic"; + private static final String TOGGLE_SH_FAST_PATH = "ToggleShFastPath"; private Material pbrMat; + private EnvironmentProbeControl envProbe; private float metallic = 0.0f; private float roughness = 1.0f; + private boolean shFastPath = false; public static void main(String[] args) { new TestPBRSimple().start(); @@ -86,14 +90,16 @@ public void simpleInitApp() { rootNode.attachChild(sky); // Create baker control - EnvironmentProbeControl envProbe=new EnvironmentProbeControl(assetManager,256); + envProbe = new EnvironmentProbeControl(assetManager, 256); + envProbe.setSphericalHarmonicsMode(SphericalHarmonicsMode.QUALITY); rootNode.addControl(envProbe); // Tag the sky, only the tagged spatials will be rendered in the env map envProbe.tag(sky); inputManager.addMapping(INCREASE_METALLIC, new KeyTrigger(KeyInput.KEY_N)); inputManager.addMapping(DECREASE_METALLIC, new KeyTrigger(KeyInput.KEY_P)); - inputManager.addListener(metallicListener, INCREASE_METALLIC, DECREASE_METALLIC); + inputManager.addMapping(TOGGLE_SH_FAST_PATH, new KeyTrigger(KeyInput.KEY_F)); + inputManager.addListener(materialListener, INCREASE_METALLIC, DECREASE_METALLIC, TOGGLE_SH_FAST_PATH); updateMaterial(); @@ -113,7 +119,7 @@ public void simpleUpdate(float tpf) { } } - private final ActionListener metallicListener = (name, isPressed, tpf) -> { + private final ActionListener materialListener = (name, isPressed, tpf) -> { if (!isPressed) { return; } @@ -124,6 +130,12 @@ public void simpleUpdate(float tpf) { } else if (DECREASE_METALLIC.equals(name)) { metallic = FastMath.clamp(metallic - 0.1f, 0.0f, 1.0f); roughness = FastMath.clamp(roughness + 0.1f, 0.0f, 1.0f); + } else if (TOGGLE_SH_FAST_PATH.equals(name)) { + shFastPath = !shFastPath; + envProbe.setSphericalHarmonicsFastPathEnabled(shFastPath); + envProbe.rebake(); + System.out.println("Spherical harmonics mode -> " + + envProbe.getSphericalHarmonicsMode() + "; rebaking probe"); } updateMaterial(); @@ -133,6 +145,7 @@ private void updateMaterial() { pbrMat.setFloat("Metallic", metallic); pbrMat.setFloat("Roughness", roughness); System.out.println( - "Tank material -> metallic: " + metallic + ", roughness: " + roughness + " (N/P)"); + "Tank material -> metallic: " + metallic + ", roughness: " + roughness + + " (N/P, F toggles SH fast path)"); } } From 5f2828f770f4b9f910042b023bf8c19a2a666393 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Sun, 17 May 2026 01:22:10 +0200 Subject: [PATCH 2/2] fix cloning and improve baker performances --- .../environment/EnvironmentProbeControl.java | 60 +++++++++++++++---- .../environment/baker/GenericEnvBaker.java | 43 +++++++++---- .../environment/baker/IBLGLEnvBakerLight.java | 39 ++++++++++-- .../baker/IBLHybridEnvBakerLight.java | 49 ++++++++++++--- .../jme3test/light/pbr/TestPBRSimple.java | 7 ++- 5 files changed, 158 insertions(+), 40 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/environment/EnvironmentProbeControl.java b/jme3-core/src/main/java/com/jme3/environment/EnvironmentProbeControl.java index 683244f303..efbd468303 100644 --- a/jme3-core/src/main/java/com/jme3/environment/EnvironmentProbeControl.java +++ b/jme3-core/src/main/java/com/jme3/environment/EnvironmentProbeControl.java @@ -87,10 +87,10 @@ public class EnvironmentProbeControl extends LightProbe implements Control { private boolean enabled = true; private IBLGLEnvBakerLight.SphericalHarmonicsMode sphericalHarmonicsMode = IBLGLEnvBakerLight.SphericalHarmonicsMode.AUTO; - private Predicate filter = (s) -> { return s.getUserData("tags.env") != null || s.getUserData("tags.env.env" + uuid) != null; }; + private transient IBLGLEnvBakerLight baker; protected EnvironmentProbeControl() { super(); @@ -187,20 +187,38 @@ public static void untagGlobal(Spatial s) { @Override public Control cloneForSpatial(Spatial spatial) { - EnvironmentProbeControl control = new EnvironmentProbeControl(); - control.setAssetManager(assetManager); + EnvironmentProbeControl control = new EnvironmentProbeControl(assetManager, envMapSize); control.setFrustumFar(frustumFar); control.setFrustumNear(frustumNear); control.setRequiredSavableResults(requiredSavableResults); control.setEnabled(enabled); control.setSphericalHarmonicsMode(sphericalHarmonicsMode); - control.envMapSize = envMapSize; - control.uuid = uuid; - control.filter = filter; - control.spatial = spatial; + control.setColor(getColor()); + control.setName(getName()); + control.setFrustumCheckNeeded(isFrustumCheckNeeded()); + control.setAreaType(getAreaType()); + control.getArea().setRadius(getArea().getRadius()); + control.setPosition(getPosition()); + control.retagForClone(spatial, uuid); + control.setSpatial(spatial); return control; } + private void retagForClone(Spatial spatial, String sourceUuid) { + if (spatial instanceof Node) { + Node n = (Node) spatial; + for (Spatial sx : n.getChildren()) { + retagForClone(sx, sourceUuid); + } + } else if (spatial instanceof Geometry) { + String sourceTag = "tags.env.env" + sourceUuid; + if (spatial.getUserData(sourceTag) != null) { + spatial.setUserData("tags.env.env" + uuid, true); + spatial.setUserData(sourceTag, null); + } + } + } + /** * Requests savable results from the baking process. This will make the * baking process slower and more memory intensive but will allow to @@ -259,6 +277,10 @@ public void setSpatial(Spatial spatial) { if (this.spatial != null && spatial != null && spatial != this.spatial) { throw new IllegalStateException("This control has already been added to a Spatial"); } + if (spatial == null && baker != null) { + baker.clean(); + baker = null; + } this.spatial = spatial; if (spatial != null) spatial.addLight(this); } @@ -273,7 +295,12 @@ public void render(RenderManager rm, ViewPort vp) { if (!isEnabled()) return; if (bakeNeeded) { bakeNeeded = false; - rebakeNow(rm); + try { + rebakeNow(rm); + } finally { + rm.getRenderer().setFrameBuffer(vp.getOutputFrameBuffer()); + rm.setCamera(vp.getCamera(), false); + } } } @@ -327,11 +354,22 @@ public float getFrustumFar() { */ public void setAssetManager(AssetManager assetManager) { this.assetManager = assetManager; + if (baker != null) { + baker.clean(); + baker = null; + } + } + + private IBLGLEnvBakerLight getBaker(RenderManager renderManager) { + if (baker == null) { + baker = new IBLGLEnvBakerLight(renderManager, assetManager, null, + null, envMapSize, envMapSize); + } + return baker; } void rebakeNow(RenderManager renderManager) { - IBLGLEnvBakerLight baker = new IBLGLEnvBakerLight(renderManager, assetManager, null, - null, envMapSize, envMapSize); + IBLGLEnvBakerLight baker = getBaker(renderManager); baker.setSphericalHarmonicsMode(sphericalHarmonicsMode); baker.setTexturePulling(isRequiredSavableResults()); @@ -347,8 +385,6 @@ void rebakeNow(RenderManager renderManager) { setShCoeffs(baker.getSphericalHarmonicsCoefficients()); setPosition(Vector3f.ZERO); setReady(true); - - baker.clean(); } public void setEnabled(boolean enabled) { diff --git a/jme3-core/src/main/java/com/jme3/environment/baker/GenericEnvBaker.java b/jme3-core/src/main/java/com/jme3/environment/baker/GenericEnvBaker.java index d79a40c00f..9e80bab227 100644 --- a/jme3-core/src/main/java/com/jme3/environment/baker/GenericEnvBaker.java +++ b/jme3-core/src/main/java/com/jme3/environment/baker/GenericEnvBaker.java @@ -105,6 +105,7 @@ public abstract class GenericEnvBaker implements EnvBaker { protected final Camera cam; protected boolean texturePulling = false; protected List bos = new ArrayList<>(); + private FrameBuffer[] envBakers; protected GenericEnvBaker(RenderManager rm, AssetManager am, Format colorFormat, Format depthFormat, int env_size) { this.depthFormat = depthFormat; @@ -178,18 +179,35 @@ protected Camera updateAndGetInternalCamera(int faceId, int w, int h, Vector3f p @Override public void clean() { + if (envBakers != null) { + for (FrameBuffer envBaker : envBakers) { + if (envBaker != null) { + envBaker.dispose(); + } + } + envBakers = null; + } } - @Override - public void bakeEnvironment(Spatial scene, Vector3f position, float frustumNear, float frustumFar, Predicate filter) { - FrameBuffer envbakers[] = new FrameBuffer[6]; + private FrameBuffer[] getEnvBakers() { + if (envBakers != null) { + return envBakers; + } + + envBakers = new FrameBuffer[6]; for (int i = 0; i < 6; i++) { - envbakers[i] = new FrameBuffer(envMap.getImage().getWidth(), envMap.getImage().getHeight(), 1); - envbakers[i].setDepthTarget(FrameBufferTarget.newTarget(getDepthFormat())); - envbakers[i].setSrgb(false); - envbakers[i].addColorTarget(FrameBufferTarget.newTarget(envMap).face(TextureCubeMap.Face.values()[i])); + envBakers[i] = new FrameBuffer(envMap.getImage().getWidth(), envMap.getImage().getHeight(), 1); + envBakers[i].setDepthTarget(FrameBufferTarget.newTarget(getDepthFormat())); + envBakers[i].setSrgb(false); + envBakers[i].addColorTarget(FrameBufferTarget.newTarget(envMap).face(TextureCubeMap.Face.values()[i])); } + return envBakers; + } + + @Override + public void bakeEnvironment(Spatial scene, Vector3f position, float frustumNear, float frustumFar, Predicate filter) { + FrameBuffer envbakers[] = getEnvBakers(); if (isTexturePulling()) { startPulling(); @@ -212,8 +230,11 @@ public void bakeEnvironment(Spatial scene, Vector3f position, float frustumNear, Predicate ofilter = renderManager.getRenderFilter(); renderManager.setRenderFilter(filter); - renderManager.renderViewPort(viewPort, 0.16f); - renderManager.setRenderFilter(ofilter); + try { + renderManager.renderViewPort(viewPort, 0.16f); + } finally { + renderManager.setRenderFilter(ofilter); + } if (isTexturePulling()) { pull(envbaker, envMap, i); @@ -226,10 +247,6 @@ public void bakeEnvironment(Spatial scene, Vector3f position, float frustumNear, } envMap.getImage().clearUpdateNeeded(); - - for (int i = 0; i < 6; i++) { - envbakers[i].dispose(); - } } /** diff --git a/jme3-core/src/main/java/com/jme3/environment/baker/IBLGLEnvBakerLight.java b/jme3-core/src/main/java/com/jme3/environment/baker/IBLGLEnvBakerLight.java index c613e73ea1..35f032a32f 100644 --- a/jme3-core/src/main/java/com/jme3/environment/baker/IBLGLEnvBakerLight.java +++ b/jme3-core/src/main/java/com/jme3/environment/baker/IBLGLEnvBakerLight.java @@ -89,6 +89,8 @@ public enum SphericalHarmonicsMode { private SphericalHarmonicsMode sphericalHarmonicsMode = SphericalHarmonicsMode.AUTO; private int sphericalHarmonicsFastPathSampleCount = DEFAULT_FAST_SH_SAMPLE_COUNT; + private Texture2D shCoefTexture; + private FrameBuffer shBaker; /** * Create a new IBL env baker @@ -158,6 +160,35 @@ public int getSphericalHarmonicsFastPathSampleCount() { return sphericalHarmonicsFastPathSampleCount; } + @Override + public void clean() { + super.clean(); + if (shBaker != null) { + shBaker.dispose(); + shBaker = null; + } + shCoefTexture = null; + } + + private Texture2D getShCoefTexture(Format format) { + if (shCoefTexture == null || shCoefTexture.getImage().getFormat() != format) { + if (shBaker != null) { + shBaker.dispose(); + shBaker = null; + } + shCoefTexture = new Texture2D(NUM_SH_COEFFICIENT, 1, 1, format); + } + return shCoefTexture; + } + + private FrameBuffer getShBaker(Texture2D shCoefTexture) { + if (shBaker == null) { + shBaker = new FrameBuffer(NUM_SH_COEFFICIENT, 1, 1); + shBaker.setSrgb(false); + shBaker.addColorTarget(FrameBufferTarget.newTarget(shCoefTexture)); + } + return shBaker; + } @Override public void bakeSphericalHarmonicsCoefficients() { @@ -202,11 +233,8 @@ public void bakeSphericalHarmonicsCoefficients() { mat.clearParam("RemapMaxValue"); } - Texture2D shCoefTx = new Texture2D(NUM_SH_COEFFICIENT, 1, 1, format); - - FrameBuffer shbaker = new FrameBuffer(NUM_SH_COEFFICIENT, 1, 1); - shbaker.setSrgb(false); - shbaker.addColorTarget(FrameBufferTarget.newTarget(shCoefTx)); + Texture2D shCoefTx = getShCoefTexture(format); + FrameBuffer shbaker = getShBaker(shCoefTx); screen.updateLogicalState(0); screen.updateGeometricState(); @@ -244,7 +272,6 @@ else if (weightAccum != c.a) { } EnvMapUtils.prepareShCoefs(shCoef); img.dispose(); - shbaker.dispose(); } } diff --git a/jme3-core/src/main/java/com/jme3/environment/baker/IBLHybridEnvBakerLight.java b/jme3-core/src/main/java/com/jme3/environment/baker/IBLHybridEnvBakerLight.java index 606a894699..123d3ac440 100644 --- a/jme3-core/src/main/java/com/jme3/environment/baker/IBLHybridEnvBakerLight.java +++ b/jme3-core/src/main/java/com/jme3/environment/baker/IBLHybridEnvBakerLight.java @@ -64,6 +64,7 @@ public class IBLHybridEnvBakerLight extends GenericEnvBaker implements IBLEnvBak private static final Logger LOGGER = Logger.getLogger(IBLHybridEnvBakerLight.class.getName()); protected TextureCubeMap specular; protected Vector3f[] shCoef; + private FrameBuffer[][] specularBakers; /** * Create a new IBL env baker @@ -104,6 +105,24 @@ public IBLHybridEnvBakerLight(RenderManager rm, AssetManager am, Format format, } + @Override + public void clean() { + super.clean(); + if (specularBakers != null) { + for (FrameBuffer[] mipBakers : specularBakers) { + if (mipBakers == null) { + continue; + } + for (FrameBuffer specularBaker : mipBakers) { + if (specularBaker != null) { + specularBaker.dispose(); + } + } + } + specularBakers = null; + } + } + @Override public boolean isTexturePulling() { // always pull textures from gpu return true; @@ -123,19 +142,36 @@ private float roughnessFromMip(int mip) { return mipNorm * mipNorm; } - private void bakeSpecularIBL(int mip, float roughness, Material mat, Geometry screen) throws Exception { - mat.setFloat("Roughness", roughness); + private FrameBuffer[] getSpecularBakers(int mip, int mipWidth, int mipHeight) { + if (specularBakers == null) { + specularBakers = new FrameBuffer[specular.getImage().getMipMapSizes().length][]; + } - int mipWidth = (int) (specular.getImage().getWidth() * FastMath.pow(0.5f, mip)); - int mipHeight = (int) (specular.getImage().getHeight() * FastMath.pow(0.5f, mip)); + FrameBuffer[] specularbakers = specularBakers[mip]; + if (specularbakers != null + && specularbakers[0].getWidth() == mipWidth + && specularbakers[0].getHeight() == mipHeight) { + return specularbakers; + } - FrameBuffer specularbakers[] = new FrameBuffer[6]; + specularbakers = new FrameBuffer[6]; for (int i = 0; i < 6; i++) { specularbakers[i] = new FrameBuffer(mipWidth, mipHeight, 1); specularbakers[i].setSrgb(false); specularbakers[i].addColorTarget(FrameBufferTarget.newTarget(specular).level(mip).face(i)); specularbakers[i].setMipMapsGenerationHint(false); } + specularBakers[mip] = specularbakers; + return specularbakers; + } + + private void bakeSpecularIBL(int mip, float roughness, Material mat, Geometry screen) throws Exception { + mat.setFloat("Roughness", roughness); + + int mipWidth = (int) (specular.getImage().getWidth() * FastMath.pow(0.5f, mip)); + int mipHeight = (int) (specular.getImage().getHeight() * FastMath.pow(0.5f, mip)); + + FrameBuffer specularbakers[] = getSpecularBakers(mip, mipWidth, mipHeight); for (int i = 0; i < 6; i++) { FrameBuffer specularbaker = specularbakers[i]; @@ -153,9 +189,6 @@ private void bakeSpecularIBL(int mip, float roughness, Material mat, Geometry sc } } - for (int i = 0; i < 6; i++) { - specularbakers[i].dispose(); - } } @Override diff --git a/jme3-examples/src/main/java/jme3test/light/pbr/TestPBRSimple.java b/jme3-examples/src/main/java/jme3test/light/pbr/TestPBRSimple.java index 33e5c87fe1..2447800759 100644 --- a/jme3-examples/src/main/java/jme3test/light/pbr/TestPBRSimple.java +++ b/jme3-examples/src/main/java/jme3test/light/pbr/TestPBRSimple.java @@ -55,6 +55,7 @@ public class TestPBRSimple extends SimpleApplication { private static final String INCREASE_METALLIC = "IncreaseMetallic"; private static final String DECREASE_METALLIC = "DecreaseMetallic"; private static final String TOGGLE_SH_FAST_PATH = "ToggleShFastPath"; + private static final String TOGGLE_SH_REALTIME = "ToggleShRealtime"; private Material pbrMat; private EnvironmentProbeControl envProbe; @@ -99,7 +100,8 @@ public void simpleInitApp() { inputManager.addMapping(INCREASE_METALLIC, new KeyTrigger(KeyInput.KEY_N)); inputManager.addMapping(DECREASE_METALLIC, new KeyTrigger(KeyInput.KEY_P)); inputManager.addMapping(TOGGLE_SH_FAST_PATH, new KeyTrigger(KeyInput.KEY_F)); - inputManager.addListener(materialListener, INCREASE_METALLIC, DECREASE_METALLIC, TOGGLE_SH_FAST_PATH); + inputManager.addMapping(TOGGLE_SH_REALTIME, new KeyTrigger(KeyInput.KEY_R)); + inputManager.addListener(materialListener, TOGGLE_SH_REALTIME, INCREASE_METALLIC, DECREASE_METALLIC, TOGGLE_SH_FAST_PATH); updateMaterial(); @@ -136,6 +138,9 @@ public void simpleUpdate(float tpf) { envProbe.rebake(); System.out.println("Spherical harmonics mode -> " + envProbe.getSphericalHarmonicsMode() + "; rebaking probe"); + } else if (TOGGLE_SH_REALTIME.equals(name)) { + REALTIME_BAKING = !REALTIME_BAKING; + System.out.println("Real-time baking -> " + REALTIME_BAKING); } updateMaterial();