From e452b169ae2c760989a963a139f198010b22d9f4 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Tue, 23 Jan 2024 19:28:14 +0100 Subject: [PATCH] Accelerated env baking on the GPU (#1165) Adds 3 accelerated bakers that can be used in place of the current CPU baker. The enhancements brought by these bakers is very remarkable, as they allow the environment baking process to be completed in <1s, effectively rendering the need for prebaked environments obsolete. --- .../environment/EnvironmentProbeControl.java | 351 ++++++++++++++++++ .../environment/FastLightProbeFactory.java | 122 ++++++ .../com/jme3/environment/baker/EnvBaker.java | 89 +++++ .../environment/baker/GenericEnvBaker.java | 293 +++++++++++++++ .../jme3/environment/baker/IBLEnvBaker.java | 74 ++++ .../environment/baker/IBLEnvBakerLight.java | 52 +++ .../jme3/environment/baker/IBLGLEnvBaker.java | 294 +++++++++++++++ .../environment/baker/IBLGLEnvBakerLight.java | 176 +++++++++ .../baker/IBLHybridEnvBakerLight.java | 209 +++++++++++ .../java/com/jme3/renderer/RenderManager.java | 23 ++ .../main/resources/Common/IBL/IBLKernels.frag | 109 ++++++ .../main/resources/Common/IBL/IBLKernels.j3md | 39 ++ .../main/resources/Common/IBL/IBLKernels.vert | 31 ++ .../src/main/resources/Common/IBL/Math.glsl | 95 +++++ .../resources/Common/IBLSphH/IBLSphH.frag | 191 ++++++++++ .../resources/Common/IBLSphH/IBLSphH.j3md | 34 ++ .../resources/Common/IBLSphH/IBLSphH.vert | 18 + .../jme3test/light/pbr/TestPBRLighting.java | 27 +- .../jme3test/light/pbr/TestPBRSimple.java | 101 +++++ 19 files changed, 2318 insertions(+), 10 deletions(-) create mode 100644 jme3-core/src/main/java/com/jme3/environment/EnvironmentProbeControl.java create mode 100644 jme3-core/src/main/java/com/jme3/environment/FastLightProbeFactory.java create mode 100644 jme3-core/src/main/java/com/jme3/environment/baker/EnvBaker.java create mode 100644 jme3-core/src/main/java/com/jme3/environment/baker/GenericEnvBaker.java create mode 100644 jme3-core/src/main/java/com/jme3/environment/baker/IBLEnvBaker.java create mode 100644 jme3-core/src/main/java/com/jme3/environment/baker/IBLEnvBakerLight.java create mode 100644 jme3-core/src/main/java/com/jme3/environment/baker/IBLGLEnvBaker.java create mode 100644 jme3-core/src/main/java/com/jme3/environment/baker/IBLGLEnvBakerLight.java create mode 100644 jme3-core/src/main/java/com/jme3/environment/baker/IBLHybridEnvBakerLight.java create mode 100644 jme3-core/src/main/resources/Common/IBL/IBLKernels.frag create mode 100644 jme3-core/src/main/resources/Common/IBL/IBLKernels.j3md create mode 100644 jme3-core/src/main/resources/Common/IBL/IBLKernels.vert create mode 100644 jme3-core/src/main/resources/Common/IBL/Math.glsl create mode 100644 jme3-core/src/main/resources/Common/IBLSphH/IBLSphH.frag create mode 100644 jme3-core/src/main/resources/Common/IBLSphH/IBLSphH.j3md create mode 100644 jme3-core/src/main/resources/Common/IBLSphH/IBLSphH.vert create mode 100644 jme3-examples/src/main/java/jme3test/light/pbr/TestPBRSimple.java diff --git a/jme3-core/src/main/java/com/jme3/environment/EnvironmentProbeControl.java b/jme3-core/src/main/java/com/jme3/environment/EnvironmentProbeControl.java new file mode 100644 index 0000000000..d07a039995 --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/environment/EnvironmentProbeControl.java @@ -0,0 +1,351 @@ +/* + * Copyright (c) 2009-2023 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.environment; + +import java.io.IOException; +import java.util.concurrent.atomic.AtomicInteger; +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; +import com.jme3.export.OutputCapsule; +import com.jme3.light.LightProbe; +import com.jme3.math.Vector3f; +import com.jme3.renderer.RenderManager; +import com.jme3.renderer.ViewPort; +import com.jme3.scene.Geometry; +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 + * only tagged spatials. + * + * Simple usage example: + * 1. Load a scene + * Node scene=(Node)assetManager.loadModel("Scenes/MyScene.j3o"); + * 2. Add one or more EnvironmentProbeControl to the root of the scene + * EnvironmentProbeControl ec1=new EnvironmentProbeControl(assetManager, 512); + * // EnvironmentProbeControl ec2=new EnvironmentProbeControl(assetManager, 512); + * 2b. (optional) Set the position of the probes + * ec1.setPosition(new Vector3f(0,0,0)); + * // ec2.setPosition(new Vector3f(0,0,10)); + * 3. Tag the spatials that are part of the environment + * scene.deepFirstTraversal(s->{ + * if(s.getUserData("isEnvNode")!=null){ + * EnvironmentProbeControl.tagGlobal(s); + * // or ec1.tag(s); + * // ec2.tag(s); + * } + * }); + * + * + * @author Riccardo Balbo + */ +public class EnvironmentProbeControl extends LightProbe implements Control { + private static AtomicInteger instanceCounter = new AtomicInteger(0); + + private AssetManager assetManager; + private boolean bakeNeeded = true; + private int envMapSize = 256; + private Spatial spatial; + private boolean requiredSavableResults = false; + private float frustumNear = 0.001f, frustumFar = 1000f; + private String uuid = "none"; + private boolean enabled = true; + + private Predicate filter = (s) -> { + return s.getUserData("tags.env") != null || s.getUserData("tags.env.env" + uuid) != null; + }; + + protected EnvironmentProbeControl() { + super(); + uuid = System.currentTimeMillis() + "_" + instanceCounter.getAndIncrement(); + this.setAreaType(AreaType.Spherical); + this.getArea().setRadius(Float.MAX_VALUE); + } + + /** + * Creates a new environment probe control. + * + * @param assetManager + * the asset manager used to load the shaders needed for the + * baking + * @param size + * the size of side of the resulting cube map (eg. 1024) + */ + public EnvironmentProbeControl(AssetManager assetManager, int size) { + this(); + this.envMapSize = size; + this.assetManager = assetManager; + } + + /** + * Tags the specified spatial as part of the environment for this EnvironmentProbeControl. + * Only tagged spatials will be rendered in the environment map. + * + * @param s + * the spatial + */ + public void tag(Spatial s) { + if (s instanceof Node) { + Node n = (Node) s; + for (Spatial sx : n.getChildren()) { + tag(sx); + } + } else if (s instanceof Geometry) { + s.setUserData("tags.env.env" + uuid, true); + } + } + + /** + * Untags the specified spatial as part of the environment for this + * EnvironmentProbeControl. + * + * @param s + * the spatial + */ + public void untag(Spatial s) { + if (s instanceof Node) { + Node n = (Node) s; + for (Spatial sx : n.getChildren()) { + untag(sx); + } + } else if (s instanceof Geometry) { + s.setUserData("tags.env.env" + uuid, null); + } + } + + /** + * Tags the specified spatial as part of the environment for every EnvironmentProbeControl. + * Only tagged spatials will be rendered in the environment map. + * + * @param s + * the spatial + */ + public static void tagGlobal(Spatial s) { + if (s instanceof Node) { + Node n = (Node) s; + for (Spatial sx : n.getChildren()) { + tagGlobal(sx); + } + } else if (s instanceof Geometry) { + s.setUserData("tags.env", true); + } + } + + /** + * Untags the specified spatial as part of the environment for every + * EnvironmentProbeControl. + * + * @param s the spatial + */ + public static void untagGlobal(Spatial s) { + if (s instanceof Node) { + Node n = (Node) s; + for (Spatial sx : n.getChildren()) { + untagGlobal(sx); + } + } else if (s instanceof Geometry) { + s.setUserData("tags.env", null); + } + } + + @Override + public Control cloneForSpatial(Spatial spatial) { + throw new UnsupportedOperationException(); + } + + /** + * Requests savable results from the baking process. This will make the + * baking process slower and more memory intensive but will allow to + * serialize the results with the control. + * + * @param v + * true to enable (default: false) + */ + public void setRequiredSavableResults(boolean v) { + requiredSavableResults = v; + } + + /** + * Returns true if savable results are required by this control. + * + * @return true if savable results are required. + */ + public boolean isRequiredSavableResults() { + return requiredSavableResults; + } + + @Override + 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"); + } + this.spatial = spatial; + if (spatial != null) spatial.addLight(this); + } + + @Override + public void update(float tpf) { + + } + + @Override + public void render(RenderManager rm, ViewPort vp) { + if (!isEnabled()) return; + if (bakeNeeded) { + bakeNeeded = false; + rebakeNow(rm); + } + } + + /** + * Schedules a rebake of the environment map. + */ + public void rebake() { + bakeNeeded = true; + } + + /** + * Sets the minimum distance to render. + * + * @param frustumNear the minimum distance to render + */ + public void setFrustumNear(float frustumNear) { + this.frustumNear = frustumNear; + } + + /** + * Sets the maximum distance to render. + * + * @param frustumFar the maximum distance to render + */ + public void setFrustumFar(float frustumFar) { + this.frustumFar = frustumFar; + } + + /** + * Gets the minimum distance to render. + * + * @return frustum near + */ + public float getFrustumNear() { + return frustumNear; + } + + /** + * Gets the maximum distance to render. + * + * @return frustum far + */ + public float getFrustumFar() { + return frustumFar; + } + + /** + * Sets the asset manager used to load the shaders needed for the baking. + * + * @param assetManager the asset manager + */ + public void setAssetManager(AssetManager assetManager) { + this.assetManager = assetManager; + } + + void rebakeNow(RenderManager renderManager) { + IBLHybridEnvBakerLight baker = new IBLGLEnvBakerLight(renderManager, assetManager, Format.RGB16F, Format.Depth, + envMapSize, envMapSize); + + baker.setTexturePulling(isRequiredSavableResults()); + baker.bakeEnvironment(spatial, getPosition(), frustumNear, frustumFar, filter); + baker.bakeSpecularIBL(); + baker.bakeSphericalHarmonicsCoefficients(); + + setPrefilteredMap(baker.getSpecularIBL()); + + int[] mipSizes = getPrefilteredEnvMap().getImage().getMipMapSizes(); + setNbMipMaps(mipSizes != null ? mipSizes.length : 1); + + setShCoeffs(baker.getSphericalHarmonicsCoefficients()); + setPosition(Vector3f.ZERO); + setReady(true); + + baker.clean(); + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public boolean isEnabled() { + return enabled; + } + + public Spatial getSpatial() { + return spatial; + } + + @Override + public void write(JmeExporter ex) throws IOException { + super.write(ex); + OutputCapsule oc = ex.getCapsule(this); + oc.write(enabled, "enabled", true); + oc.write(spatial, "spatial", null); + oc.write(envMapSize, "size", 256); + oc.write(requiredSavableResults, "requiredSavableResults", false); + oc.write(bakeNeeded, "bakeNeeded", true); + oc.write(frustumFar, "frustumFar", 1000f); + oc.write(frustumNear, "frustumNear", 0.001f); + oc.write(uuid, "envProbeControlUUID", "none"); + } + + @Override + public void read(JmeImporter im) throws IOException { + super.read(im); + InputCapsule ic = im.getCapsule(this); + enabled = ic.readBoolean("enabled", true); + spatial = (Spatial) ic.readSavable("spatial", null); + envMapSize = ic.readInt("size", 256); + requiredSavableResults = ic.readBoolean("requiredSavableResults", false); + bakeNeeded = ic.readBoolean("bakeNeeded", true); + assetManager = im.getAssetManager(); + frustumFar = ic.readFloat("frustumFar", 1000f); + frustumNear = ic.readFloat("frustumNear", 0.001f); + uuid = ic.readString("envProbeControlUUID", "none"); + } + +} diff --git a/jme3-core/src/main/java/com/jme3/environment/FastLightProbeFactory.java b/jme3-core/src/main/java/com/jme3/environment/FastLightProbeFactory.java new file mode 100644 index 0000000000..d78edc561e --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/environment/FastLightProbeFactory.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2009-2023 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.environment; + +import com.jme3.asset.AssetManager; +import com.jme3.environment.baker.IBLGLEnvBakerLight; +import com.jme3.environment.util.EnvMapUtils; +import com.jme3.light.LightProbe; +import com.jme3.math.Vector3f; +import com.jme3.renderer.RenderManager; +import com.jme3.scene.Node; +import com.jme3.scene.Spatial; +import com.jme3.texture.Image.Format; + +/** + * A faster LightProbeFactory that uses GPU accelerated algorithms. + * This is the GPU version of @{link LightProbeFactory} and should be generally preferred. + * + * For common use cases where the probe is baking the scene or part of the scene around it, it + * is advised to use the @{link EnvironmentProbeControl} instead since it does automatically most of the + * boilerplate work. + * + * + * @author Riccardo Balbo + */ +public class FastLightProbeFactory { + + /** + * Creates a LightProbe with the given EnvironmentCamera in the given scene. + * + * @param rm + * The RenderManager + * @param am + * The AssetManager + * @param size + * The size of the probe + * @param pos + * The position of the probe + * @param frustumNear + * The near frustum of the probe + * @param frustumFar + * The far frustum of the probe + * @param scene + * The scene to bake + * @return The baked LightProbe + */ + public static LightProbe makeProbe(RenderManager rm, AssetManager am, int size, Vector3f pos, float frustumNear, float frustumFar, Spatial scene) { + IBLGLEnvBakerLight baker = new IBLGLEnvBakerLight(rm, am, Format.RGB16F, Format.Depth, size, size); + + baker.setTexturePulling(true); + baker.bakeEnvironment(scene, pos, frustumNear, frustumFar, null); + baker.bakeSpecularIBL(); + baker.bakeSphericalHarmonicsCoefficients(); + + LightProbe probe = new LightProbe(); + + probe.setPosition(pos); + probe.setPrefilteredMap(baker.getSpecularIBL()); + + int[] mipSizes = probe.getPrefilteredEnvMap().getImage().getMipMapSizes(); + probe.setNbMipMaps(mipSizes != null ? mipSizes.length : 1); + + probe.setShCoeffs(baker.getSphericalHarmonicsCoefficients()); + probe.setReady(true); + + baker.clean(); + + return probe; + + } + + /** + * For debuging purposes only Will return a Node meant to be added to a GUI + * presenting the 2 cube maps in a cross pattern with all the mip maps. + * + * @param manager + * the asset manager + * @return a debug node + */ + public static Node getDebugGui(AssetManager manager, LightProbe probe) { + if (!probe.isReady()) { + throw new UnsupportedOperationException("This EnvProbe is not ready yet, try to test isReady()"); + } + + Node debugNode = new Node("debug gui probe"); + Node debugPfemCm = EnvMapUtils.getCubeMapCrossDebugViewWithMipMaps(probe.getPrefilteredEnvMap(), manager); + debugNode.attachChild(debugPfemCm); + debugPfemCm.setLocalTranslation(520, 0, 0); + + return debugNode; + } + +} diff --git a/jme3-core/src/main/java/com/jme3/environment/baker/EnvBaker.java b/jme3-core/src/main/java/com/jme3/environment/baker/EnvBaker.java new file mode 100644 index 0000000000..65ee9805f1 --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/environment/baker/EnvBaker.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2009-2023 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.environment.baker; + +import java.util.function.Predicate; +import com.jme3.math.Vector3f; +import com.jme3.scene.Geometry; +import com.jme3.scene.Spatial; +import com.jme3.texture.TextureCubeMap; + +/** + * An environment baker to bake a 3d environment into a cubemap + * + * @author Riccardo Balbo + */ +public interface EnvBaker { + /** + * Bakes the environment. + * + * @param scene + * The scene to bake + * @param position + * The position of the camera + * @param frustumNear + * The near frustum + * @param frustumFar + * The far frustum + * @param filter + * A filter to select which geometries to bake + */ + public void bakeEnvironment(Spatial scene, Vector3f position, float frustumNear, float frustumFar, Predicate filter); + + /** + * Gets the environment map. + * + * @return The environment map + */ + public TextureCubeMap getEnvMap(); + + /** + * Cleans the environment baker This method should be called when the baker + * is no longer needed It will clean up all the resources. + */ + public void clean(); + + /** + * Specifies whether textures should be pulled from the GPU. + * + * @param v + */ + public void setTexturePulling(boolean v); + + /** + * Gets if textures should be pulled from the GPU. + * + * @return + */ + public boolean isTexturePulling(); +} 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 new file mode 100644 index 0000000000..6831914945 --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/environment/baker/GenericEnvBaker.java @@ -0,0 +1,293 @@ +/* + * Copyright (c) 2009-2023 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.environment.baker; + +import java.io.ByteArrayOutputStream; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; +import java.util.logging.Level; +import java.util.logging.Logger; +import com.jme3.asset.AssetManager; +import com.jme3.math.ColorRGBA; +import com.jme3.math.Quaternion; +import com.jme3.math.Vector3f; +import com.jme3.renderer.Camera; +import com.jme3.renderer.RenderManager; +import com.jme3.renderer.ViewPort; +import com.jme3.scene.Geometry; +import com.jme3.scene.Spatial; +import com.jme3.texture.FrameBuffer; +import com.jme3.texture.Texture; +import com.jme3.texture.FrameBuffer.FrameBufferTarget; +import com.jme3.texture.Image.Format; +import com.jme3.texture.Texture.MagFilter; +import com.jme3.texture.Texture.MinFilter; +import com.jme3.texture.Texture.WrapMode; +import com.jme3.texture.TextureCubeMap; +import com.jme3.texture.image.ColorSpace; +import com.jme3.util.BufferUtils; + +/** + * Render the environment into a cubemap + * + * @author Riccardo Balbo + */ +public abstract class GenericEnvBaker implements EnvBaker { + private static final Logger LOG = Logger.getLogger(GenericEnvBaker.class.getName()); + + protected static Vector3f[] axisX = new Vector3f[6]; + protected static Vector3f[] axisY = new Vector3f[6]; + protected static Vector3f[] axisZ = new Vector3f[6]; + static { + // PositiveX axis(left, up, direction) + axisX[0] = Vector3f.UNIT_Z.mult(1.0F); + axisY[0] = Vector3f.UNIT_Y.mult(-1.0F); + axisZ[0] = Vector3f.UNIT_X.mult(1.0F); + // NegativeX + axisX[1] = Vector3f.UNIT_Z.mult(-1.0F); + axisY[1] = Vector3f.UNIT_Y.mult(-1.0F); + axisZ[1] = Vector3f.UNIT_X.mult(-1.0F); + // PositiveY + axisX[2] = Vector3f.UNIT_X.mult(-1.0F); + axisY[2] = Vector3f.UNIT_Z.mult(1.0F); + axisZ[2] = Vector3f.UNIT_Y.mult(1.0F); + // NegativeY + axisX[3] = Vector3f.UNIT_X.mult(-1.0F); + axisY[3] = Vector3f.UNIT_Z.mult(-1.0F); + axisZ[3] = Vector3f.UNIT_Y.mult(-1.0F); + // PositiveZ + axisX[4] = Vector3f.UNIT_X.mult(-1.0F); + axisY[4] = Vector3f.UNIT_Y.mult(-1.0F); + axisZ[4] = Vector3f.UNIT_Z; + // NegativeZ + axisX[5] = Vector3f.UNIT_X.mult(1.0F); + axisY[5] = Vector3f.UNIT_Y.mult(-1.0F); + axisZ[5] = Vector3f.UNIT_Z.mult(-1.0F); + } + + protected TextureCubeMap envMap; + protected Format depthFormat; + + protected final RenderManager renderManager; + protected final AssetManager assetManager; + protected final Camera cam; + protected boolean texturePulling = false; + protected List bos = new ArrayList<>(); + + protected GenericEnvBaker(RenderManager rm, AssetManager am, Format colorFormat, Format depthFormat, int env_size) { + this.depthFormat = depthFormat; + + renderManager = rm; + assetManager = am; + + cam = new Camera(128, 128); + + envMap = new TextureCubeMap(env_size, env_size, colorFormat); + envMap.setMagFilter(MagFilter.Bilinear); + envMap.setMinFilter(MinFilter.BilinearNoMipMaps); + envMap.setWrap(WrapMode.EdgeClamp); + envMap.getImage().setColorSpace(ColorSpace.Linear); + } + + @Override + public void setTexturePulling(boolean v) { + texturePulling = v; + } + + @Override + public boolean isTexturePulling() { + return texturePulling; + } + + public TextureCubeMap getEnvMap() { + return envMap; + } + + /** + * Updates the internal camera to face the given cubemap face + * and return it. + * + * @param faceId + * the id of the face (0-5) + * @param w + * width of the camera + * @param h + * height of the camera + * @param position + * position of the camera + * @param frustumNear + * near frustum + * @param frustumFar + * far frustum + * @return The updated camera + */ + protected Camera updateAndGetInternalCamera(int faceId, int w, int h, Vector3f position, float frustumNear, float frustumFar) { + cam.resize(w, h, false); + cam.setLocation(position); + cam.setFrustumPerspective(90.0F, 1F, frustumNear, frustumFar); + cam.setRotation(new Quaternion().fromAxes(axisX[faceId], axisY[faceId], axisZ[faceId])); + return cam; + } + + @Override + public void clean() { + + } + + @Override + public void bakeEnvironment(Spatial scene, Vector3f position, float frustumNear, float frustumFar, Predicate filter) { + FrameBuffer 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(depthFormat)); + envbakers[i].setSrgb(false); + envbakers[i].addColorTarget(FrameBufferTarget.newTarget(envMap).face(TextureCubeMap.Face.values()[i])); + } + + if (isTexturePulling()) { + startPulling(); + } + + for (int i = 0; i < 6; i++) { + FrameBuffer envbaker = envbakers[i]; + + ViewPort viewPort = new ViewPort("EnvBaker", updateAndGetInternalCamera(i, envbaker.getWidth(), envbaker.getHeight(), position, frustumNear, frustumFar)); + viewPort.setClearFlags(true, true, true); + viewPort.setBackgroundColor(ColorRGBA.Pink); + + viewPort.setOutputFrameBuffer(envbaker); + viewPort.clearScenes(); + viewPort.attachScene(scene); + + scene.updateLogicalState(0); + scene.updateGeometricState(); + + Predicate ofilter = renderManager.getRenderFilter(); + + renderManager.setRenderFilter(filter); + renderManager.renderViewPort(viewPort, 0.16f); + renderManager.setRenderFilter(ofilter); + + if (isTexturePulling()) { + pull(envbaker, envMap, i); + } + + } + + if (isTexturePulling()) { + endPulling(envMap); + } + + envMap.getImage().clearUpdateNeeded(); + + for (int i = 0; i < 6; i++) { + envbakers[i].dispose(); + } + } + + /** + * Starts pulling the data from the framebuffer into the texture. + */ + protected void startPulling() { + bos.clear(); + } + + /** + * Pulls the data from the framebuffer into the texture Nb. mipmaps must be + * pulled sequentially on the same faceId. + * + * @param fb + * the framebuffer to pull from + * @param env + * the texture to pull into + * @param faceId + * id of face if cubemap or 0 otherwise + * @return the ByteBuffer containing the pulled data + */ + protected ByteBuffer pull(FrameBuffer fb, Texture env, int faceId) { + + if (fb.getColorTarget().getFormat() != env.getImage().getFormat()) + throw new IllegalArgumentException("Format mismatch: " + fb.getColorTarget().getFormat() + "!=" + env.getImage().getFormat()); + + ByteBuffer face = BufferUtils.createByteBuffer(fb.getWidth() * fb.getHeight() * (fb.getColorTarget().getFormat().getBitsPerPixel() / 8)); + renderManager.getRenderer().readFrameBufferWithFormat(fb, face, fb.getColorTarget().getFormat()); + face.rewind(); + + while (bos.size() <= faceId) { + bos.add(null); + } + + ByteArrayOutputStream bo = bos.get(faceId); + if (bo == null) { + bos.set(faceId, bo = new ByteArrayOutputStream()); + } + try { + byte array[] = new byte[face.limit()]; + face.get(array); + bo.write(array); + } catch (Exception ex) { + LOG.log(Level.SEVERE, null, ex); + } + return face; + } + + /** + * Ends pulling the data into the texture + * + * @param tx + * the texture to pull into + */ + protected void endPulling(Texture tx) { + for (int i = 0; i < bos.size(); i++) { + ByteArrayOutputStream bo = bos.get(i); + if (bo != null) { + ByteBuffer faceMip = ByteBuffer.wrap(bo.toByteArray()); + tx.getImage().setData(i, faceMip); + } else { + LOG.log(Level.SEVERE, "Missing face {0}. Pulling incomplete!", i); + } + } + bos.clear(); + tx.getImage().clearUpdateNeeded(); + } + + protected int limitMips(int nbMipMaps, int baseW, int baseH, RenderManager rm) { + if (nbMipMaps > 6) { + nbMipMaps = 6; + } + return nbMipMaps; + } + +} diff --git a/jme3-core/src/main/java/com/jme3/environment/baker/IBLEnvBaker.java b/jme3-core/src/main/java/com/jme3/environment/baker/IBLEnvBaker.java new file mode 100644 index 0000000000..982ccc79df --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/environment/baker/IBLEnvBaker.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2009-2023 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.environment.baker; + +import com.jme3.texture.Texture2D; +import com.jme3.texture.TextureCubeMap; + +/** + * An environment baker, but this one is for Imaged Base Lighting. + * + * @author Riccardo Balbo + */ +public interface IBLEnvBaker extends EnvBaker { + /** + * Generates the BRDF texture. + * + * @return The BRDF texture + */ + public Texture2D genBRTF(); + + /** + * Bakes the irradiance map. + */ + public void bakeIrradiance(); + + /** + * Bakes the specular IBL map. + */ + public void bakeSpecularIBL(); + + /** + * Gets the specular IBL map. + * + * @return The specular IBL map + */ + public TextureCubeMap getSpecularIBL(); + + /** + * Gets the irradiance map. + * + * @return The irradiance map + */ + public TextureCubeMap getIrradiance(); +} diff --git a/jme3-core/src/main/java/com/jme3/environment/baker/IBLEnvBakerLight.java b/jme3-core/src/main/java/com/jme3/environment/baker/IBLEnvBakerLight.java new file mode 100644 index 0000000000..19275d514e --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/environment/baker/IBLEnvBakerLight.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2009-2023 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.environment.baker; + +import com.jme3.math.Vector3f; +import com.jme3.texture.TextureCubeMap; + +/** + * An environment baker for IBL, that uses spherical harmonics for irradiance. + * + * @author Riccardo Balbo + */ +public interface IBLEnvBakerLight extends EnvBaker { + + public void bakeSpecularIBL(); + + public void bakeSphericalHarmonicsCoefficients(); + + public TextureCubeMap getSpecularIBL(); + + public Vector3f[] getSphericalHarmonicsCoefficients(); +} \ No newline at end of file diff --git a/jme3-core/src/main/java/com/jme3/environment/baker/IBLGLEnvBaker.java b/jme3-core/src/main/java/com/jme3/environment/baker/IBLGLEnvBaker.java new file mode 100644 index 0000000000..0a28664e0d --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/environment/baker/IBLGLEnvBaker.java @@ -0,0 +1,294 @@ +/* + * Copyright (c) 2009-2023 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.environment.baker; + +import java.util.Arrays; +import java.util.logging.Level; +import java.util.logging.Logger; +import com.jme3.asset.AssetManager; +import com.jme3.material.Material; +import com.jme3.math.FastMath; +import com.jme3.math.Vector3f; +import com.jme3.renderer.Camera; +import com.jme3.renderer.RenderManager; +import com.jme3.scene.Geometry; +import com.jme3.scene.shape.Box; +import com.jme3.texture.FrameBuffer; +import com.jme3.texture.Image.Format; +import com.jme3.texture.Texture.MagFilter; +import com.jme3.texture.Texture.MinFilter; +import com.jme3.texture.Texture.WrapMode; +import com.jme3.texture.Texture2D; +import com.jme3.texture.TextureCubeMap; +import com.jme3.texture.FrameBuffer.FrameBufferTarget; +import com.jme3.texture.image.ColorSpace; +import com.jme3.ui.Picture; + +/** + * Fully accelerated env baker for IBL that runs entirely on the GPU + * + * @author Riccardo Balbo + */ +public class IBLGLEnvBaker extends GenericEnvBaker implements IBLEnvBaker { + private static final Logger LOGGER = Logger.getLogger(IBLHybridEnvBakerLight.class.getName()); + + protected Texture2D brtf; + protected TextureCubeMap irradiance; + protected TextureCubeMap specular; + + /** + * Create a new IBL env baker + * @param rm The render manager used to render the env scene + * @param am The asset manager used to load the baking shaders + * @param format The format of the color buffers + * @param depthFormat The format of the depth buffers + * @param env_size The size in pixels of the output environment cube map (eg. 1024) + * @param specular_size The size in pixels of the output specular cube map (eg. 1024) + * @param irradiance_size The size in pixels of the output irradiance cube map (eg. 512) + * @param brtf_size The size in pixels of the output brtf map (eg. 512) + */ + public IBLGLEnvBaker(RenderManager rm, AssetManager am, Format format, Format depthFormat, int env_size, int specular_size, int irradiance_size, int brtf_size) { + super(rm, am, format, depthFormat, env_size); + + irradiance = new TextureCubeMap(irradiance_size, irradiance_size, format); + irradiance.setMagFilter(MagFilter.Bilinear); + irradiance.setMinFilter(MinFilter.BilinearNoMipMaps); + irradiance.setWrap(WrapMode.EdgeClamp); + irradiance.getImage().setColorSpace(ColorSpace.Linear); + + specular = new TextureCubeMap(specular_size, specular_size, format); + specular.setMagFilter(MagFilter.Bilinear); + specular.setMinFilter(MinFilter.Trilinear); + specular.setWrap(WrapMode.EdgeClamp); + specular.getImage().setColorSpace(ColorSpace.Linear); + + int nbMipMaps = (int) (Math.log(specular_size) / Math.log(2) + 1); + nbMipMaps = limitMips(nbMipMaps, specular.getImage().getWidth(), specular.getImage().getHeight(), rm); + + int[] sizes = new int[nbMipMaps]; + for (int i = 0; i < nbMipMaps; i++) { + int size = (int) FastMath.pow(2, nbMipMaps - 1 - i); + sizes[i] = size * size * (specular.getImage().getFormat().getBitsPerPixel() / 8); + } + specular.getImage().setMipMapSizes(sizes); + + brtf = new Texture2D(brtf_size, brtf_size, format); + brtf.setMagFilter(MagFilter.Bilinear); + brtf.setMinFilter(MinFilter.BilinearNoMipMaps); + brtf.setWrap(WrapMode.EdgeClamp); + brtf.getImage().setColorSpace(ColorSpace.Linear); + } + + public TextureCubeMap getSpecularIBL() { + return specular; + } + + public TextureCubeMap getIrradiance() { + return irradiance; + } + + 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[] = 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); + } + + for (int i = 0; i < 6; i++) { + FrameBuffer specularbaker = specularbakers[i]; + mat.setInt("FaceId", i); + + screen.updateLogicalState(0); + screen.updateGeometricState(); + + renderManager.setCamera(updateAndGetInternalCamera(i, specularbaker.getWidth(), specularbaker.getHeight(), Vector3f.ZERO, 1, 1000), false); + renderManager.getRenderer().setFrameBuffer(specularbaker); + renderManager.renderGeometry(screen); + + if (isTexturePulling()) { + pull(specularbaker, specular, i); + } + + } + for (int i = 0; i < 6; i++) { + specularbakers[i].dispose(); + } + } + + @Override + public void bakeSpecularIBL() { + Box boxm = new Box(1, 1, 1); + Geometry screen = new Geometry("BakeBox", boxm); + + Material mat = new Material(assetManager, "Common/IBL/IBLKernels.j3md"); + mat.setBoolean("UseSpecularIBL", true); + mat.setTexture("EnvMap", envMap); + screen.setMaterial(mat); + + if (isTexturePulling()) { + startPulling(); + } + + int mip = 0; + for (; mip < specular.getImage().getMipMapSizes().length; mip++) { + try { + float roughness = (float) mip / (float) (specular.getImage().getMipMapSizes().length - 1); + bakeSpecularIBL(mip, roughness, mat, screen); + } catch (Exception e) { + LOGGER.log(Level.WARNING, "Error while computing mip level " + mip, e); + break; + } + } + + if (mip < specular.getImage().getMipMapSizes().length) { + + int[] sizes = specular.getImage().getMipMapSizes(); + sizes = Arrays.copyOf(sizes, mip); + specular.getImage().setMipMapSizes(sizes); + specular.getImage().setMipmapsGenerated(true); + if (sizes.length <= 1) { + try { + LOGGER.log(Level.WARNING, "Workaround driver BUG: only one mip level available, regenerate it with higher roughness (shiny fix)"); + bakeSpecularIBL(0, 1f, mat, screen); + } catch (Exception e) { + LOGGER.log(Level.FINE, "Error while recomputing mip level 0", e); + } + } + } + + if (isTexturePulling()) { + endPulling(specular); + } + specular.getImage().clearUpdateNeeded(); + + } + + @Override + public Texture2D genBRTF() { + + Picture screen = new Picture("BakeScreen", true); + screen.setWidth(1); + screen.setHeight(1); + + FrameBuffer brtfbaker = new FrameBuffer(brtf.getImage().getWidth(), brtf.getImage().getHeight(), 1); + brtfbaker.setSrgb(false); + brtfbaker.addColorTarget(FrameBufferTarget.newTarget(brtf)); + + if (isTexturePulling()) { + startPulling(); + } + + Camera envcam = updateAndGetInternalCamera(0, brtf.getImage().getWidth(), brtf.getImage().getHeight(), Vector3f.ZERO, 1, 1000); + + Material mat = new Material(assetManager, "Common/IBL/IBLKernels.j3md"); + mat.setBoolean("UseBRDF", true); + screen.setMaterial(mat); + + renderManager.getRenderer().setFrameBuffer(brtfbaker); + renderManager.setCamera(envcam, false); + + screen.updateLogicalState(0); + screen.updateGeometricState(); + renderManager.renderGeometry(screen); + + if (isTexturePulling()) { + pull(brtfbaker, brtf, 0); + } + + brtfbaker.dispose(); + + if (isTexturePulling()) { + endPulling(brtf); + } + brtf.getImage().clearUpdateNeeded(); + + return brtf; + } + + @Override + public void bakeIrradiance() { + + Box boxm = new Box(1, 1, 1); + Geometry screen = new Geometry("BakeBox", boxm); + + FrameBuffer irradiancebaker = new FrameBuffer(irradiance.getImage().getWidth(), irradiance.getImage().getHeight(), 1); + irradiancebaker.setSrgb(false); + + if (isTexturePulling()) { + startPulling(); + } + + for (int i = 0; i < 6; i++) { + irradiancebaker.addColorTarget( + FrameBufferTarget.newTarget(irradiance).face(TextureCubeMap.Face.values()[i])); + } + + Material mat = new Material(assetManager, "Common/IBL/IBLKernels.j3md"); + mat.setBoolean("UseIrradiance", true); + mat.setTexture("EnvMap", envMap); + screen.setMaterial(mat); + + for (int i = 0; i < 6; i++) { + irradiancebaker.setTargetIndex(i); + + mat.setInt("FaceId", i); + + screen.updateLogicalState(0); + screen.updateGeometricState(); + + renderManager.setCamera(updateAndGetInternalCamera(i, irradiancebaker.getWidth(), irradiancebaker.getHeight(), Vector3f.ZERO, 1, 1000), false); + renderManager.getRenderer().setFrameBuffer(irradiancebaker); + renderManager.renderGeometry(screen); + + if (isTexturePulling()) { + pull(irradiancebaker, irradiance, i); + } + } + + irradiancebaker.dispose(); + + if (isTexturePulling()) { + endPulling(irradiance); + } + irradiance.getImage().clearUpdateNeeded(); + + } + +} \ No newline at end of file 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 new file mode 100644 index 0000000000..8daa62ef40 --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/environment/baker/IBLGLEnvBakerLight.java @@ -0,0 +1,176 @@ +/* + * Copyright (c) 2009-2023 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.environment.baker; + +import java.nio.ByteBuffer; +import java.util.logging.Logger; +import com.jme3.asset.AssetManager; +import com.jme3.material.Material; +import com.jme3.math.ColorRGBA; +import com.jme3.math.FastMath; +import com.jme3.math.Vector2f; +import com.jme3.math.Vector3f; +import com.jme3.renderer.Caps; +import com.jme3.renderer.RenderManager; +import com.jme3.scene.Geometry; +import com.jme3.scene.shape.Box; +import com.jme3.texture.FrameBuffer; +import com.jme3.texture.Image; +import com.jme3.texture.Texture2D; +import com.jme3.texture.FrameBuffer.FrameBufferTarget; +import com.jme3.texture.Image.Format; +import com.jme3.texture.image.ColorSpace; +import com.jme3.texture.image.ImageRaster; +import com.jme3.util.BufferUtils; + +/** + * Fully accelerated env baker for IBL that bakes the specular map and spherical + * harmonics on the GPU. + * + * This is lighter on VRAM but it is not as parallelized as IBLGLEnvBaker + * + * @author Riccardo Balbo + */ +public class IBLGLEnvBakerLight extends IBLHybridEnvBakerLight { + private static final int NUM_SH_COEFFICIENT = 9; + private static final Logger LOG = Logger.getLogger(IBLGLEnvBakerLight.class.getName()); + + /** + * Create a new IBL env baker + * + * @param rm + * The render manager used to render the env scene + * @param am + * The asset manager used to load the baking shaders + * @param format + * The format of the color buffers + * @param depthFormat + * The format of the depth buffers + * @param env_size + * The size in pixels of the output environment cube map (eg. + * 1024) + * @param specular_size + * The size in pixels of the output specular cube map (eg. 1024) + */ + public IBLGLEnvBakerLight(RenderManager rm, AssetManager am, Format format, Format depthFormat, int env_size, int specular_size) { + super(rm, am, format, depthFormat, env_size, specular_size); + } + + @Override + public boolean isTexturePulling() { + return this.texturePulling; + } + + @Override + public void bakeSphericalHarmonicsCoefficients() { + Box boxm = new Box(1, 1, 1); + Geometry screen = new Geometry("BakeBox", boxm); + + Material mat = new Material(assetManager, "Common/IBLSphH/IBLSphH.j3md"); + mat.setTexture("Texture", envMap); + mat.setVector2("Resolution", new Vector2f(envMap.getImage().getWidth(), envMap.getImage().getHeight())); + screen.setMaterial(mat); + + float remapMaxValue = 0; + Format format = Format.RGBA32F; + if (!renderManager.getRenderer().getCaps().contains(Caps.FloatColorBufferRGBA)) { + LOG.warning("Float textures not supported, using RGB8 instead. This may cause accuracy issues."); + format = Format.RGBA8; + remapMaxValue = 0.05f; + } + + if (remapMaxValue > 0) { + mat.setFloat("RemapMaxValue", remapMaxValue); + } else { + 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])); + + int renderOnT = -1; + + for (int faceId = 0; faceId < 6; faceId++) { + if (renderOnT != -1) { + int s = renderOnT; + renderOnT = renderOnT == 0 ? 1 : 0; + mat.setTexture("ShCoef", shCoefTx[s]); + mat.setInt("FaceId", faceId); + } else { + renderOnT = 0; + } + + 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); + } + + ByteBuffer shCoefRaw = BufferUtils.createByteBuffer(NUM_SH_COEFFICIENT * 1 * (shbaker[renderOnT].getColorTarget().getFormat().getBitsPerPixel() / 8)); + renderManager.getRenderer().readFrameBufferWithFormat(shbaker[renderOnT], shCoefRaw, shbaker[renderOnT].getColorTarget().getFormat()); + shCoefRaw.rewind(); + + Image img = new Image(format, NUM_SH_COEFFICIENT, 1, shCoefRaw, ColorSpace.Linear); + ImageRaster imgr = ImageRaster.create(img); + + shCoef = new Vector3f[NUM_SH_COEFFICIENT]; + float weightAccum = 0.0f; + + for (int i = 0; i < shCoef.length; i++) { + ColorRGBA c = imgr.getPixel(i, 0); + shCoef[i] = new Vector3f(c.r, c.g, c.b); + if (weightAccum == 0) weightAccum = c.a; + else if (weightAccum != c.a) { + LOG.warning("SH weight is not uniform, this may cause issues."); + } + + } + + if (remapMaxValue > 0) weightAccum /= remapMaxValue; + + for (int i = 0; i < NUM_SH_COEFFICIENT; ++i) { + if (remapMaxValue > 0) shCoef[i].divideLocal(remapMaxValue); + shCoef[i].multLocal(4.0f * FastMath.PI / weightAccum); + } + + img.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 new file mode 100644 index 0000000000..d72e0dc30f --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/environment/baker/IBLHybridEnvBakerLight.java @@ -0,0 +1,209 @@ +/* + * Copyright (c) 2009-2023 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.environment.baker; + +import java.util.Arrays; +import java.util.logging.Level; +import java.util.logging.Logger; +import com.jme3.asset.AssetManager; +import com.jme3.environment.util.EnvMapUtils; +import com.jme3.material.Material; +import com.jme3.math.FastMath; +import com.jme3.math.Vector3f; +import com.jme3.renderer.RenderManager; +import com.jme3.scene.Geometry; +import com.jme3.scene.shape.Box; +import com.jme3.texture.FrameBuffer; +import com.jme3.texture.TextureCubeMap; +import com.jme3.texture.FrameBuffer.FrameBufferTarget; +import com.jme3.texture.Image.Format; +import com.jme3.texture.Texture.MagFilter; +import com.jme3.texture.Texture.MinFilter; +import com.jme3.texture.Texture.WrapMode; +import com.jme3.texture.image.ColorSpace; + +/** + * An env baker for IBL that bakes the specular map on the GPU and uses + * spherical harmonics generated on the CPU for the irradiance map. + * + * This is lighter on VRAM but uses the CPU to compute the irradiance map. + * + * @author Riccardo Balbo + */ +public class IBLHybridEnvBakerLight extends GenericEnvBaker implements IBLEnvBakerLight { + private static final Logger LOGGER = Logger.getLogger(IBLHybridEnvBakerLight.class.getName()); + protected TextureCubeMap specular; + protected Vector3f[] shCoef; + + /** + * Create a new IBL env baker + * + * @param rm + * The render manager used to render the env scene + * @param am + * The asset manager used to load the baking shaders + * @param format + * The format of the color buffers + * @param depthFormat + * The format of the depth buffers + * @param env_size + * The size in pixels of the output environment cube map (eg. + * 1024) + * @param specular_size + * The size in pixels of the output specular cube map (eg. 1024) + */ + public IBLHybridEnvBakerLight(RenderManager rm, AssetManager am, Format format, Format depthFormat, int env_size, int specular_size) { + super(rm, am, format, depthFormat, env_size); + + specular = new TextureCubeMap(specular_size, specular_size, format); + specular.setWrap(WrapMode.EdgeClamp); + specular.setMagFilter(MagFilter.Bilinear); + specular.setMinFilter(MinFilter.Trilinear); + specular.getImage().setColorSpace(ColorSpace.Linear); + + int nbMipMaps = (int) (Math.log(specular_size) / Math.log(2) + 1); + nbMipMaps = limitMips(nbMipMaps, specular.getImage().getWidth(), specular.getImage().getHeight(), rm); + + int[] sizes = new int[nbMipMaps]; + for (int i = 0; i < nbMipMaps; i++) { + int size = (int) FastMath.pow(2, nbMipMaps - 1 - i); + sizes[i] = size * size * (specular.getImage().getFormat().getBitsPerPixel() / 8); + } + specular.getImage().setMipMapSizes(sizes); + specular.getImage().setMipmapsGenerated(true); + + } + + @Override + public boolean isTexturePulling() { // always pull textures from gpu + return true; + } + + 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[] = 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); + } + + for (int i = 0; i < 6; i++) { + FrameBuffer specularbaker = specularbakers[i]; + mat.setInt("FaceId", i); + + screen.updateLogicalState(0); + screen.updateGeometricState(); + + renderManager.setCamera(updateAndGetInternalCamera(i, specularbaker.getWidth(), specularbaker.getHeight(), Vector3f.ZERO, 1, 1000), false); + renderManager.getRenderer().setFrameBuffer(specularbaker); + renderManager.renderGeometry(screen); + + if (isTexturePulling()) { + pull(specularbaker, specular, i); + } + + } + for (int i = 0; i < 6; i++) { + specularbakers[i].dispose(); + } + } + + @Override + public void bakeSpecularIBL() { + Box boxm = new Box(1, 1, 1); + Geometry screen = new Geometry("BakeBox", boxm); + + Material mat = new Material(assetManager, "Common/IBL/IBLKernels.j3md"); + mat.setBoolean("UseSpecularIBL", true); + mat.setTexture("EnvMap", envMap); + screen.setMaterial(mat); + + if (isTexturePulling()) { + startPulling(); + } + + int mip = 0; + for (; mip < specular.getImage().getMipMapSizes().length; mip++) { + try { + float roughness = (float) mip / (float) (specular.getImage().getMipMapSizes().length - 1); + bakeSpecularIBL(mip, roughness, mat, screen); + } catch (Exception e) { + LOGGER.log(Level.WARNING, "Error while computing mip level " + mip, e); + break; + } + } + + if (mip < specular.getImage().getMipMapSizes().length) { + + int[] sizes = specular.getImage().getMipMapSizes(); + sizes = Arrays.copyOf(sizes, mip); + specular.getImage().setMipMapSizes(sizes); + specular.getImage().setMipmapsGenerated(true); + if (sizes.length <= 1) { + try { + LOGGER.log(Level.WARNING, "Workaround driver BUG: only one mip level available, regenerate it with higher roughness (shiny fix)"); + bakeSpecularIBL(0, 1f, mat, screen); + } catch (Exception e) { + LOGGER.log(Level.FINE, "Error while recomputing mip level 0", e); + } + } + } + + if (isTexturePulling()) { + endPulling(specular); + } + specular.getImage().clearUpdateNeeded(); + + } + + @Override + public TextureCubeMap getSpecularIBL() { + return specular; + } + + @Override + public void bakeSphericalHarmonicsCoefficients() { + shCoef = EnvMapUtils.getSphericalHarmonicsCoefficents(getEnvMap()); + } + + @Override + public Vector3f[] getSphericalHarmonicsCoefficients() { + return shCoef; + } +} \ No newline at end of file diff --git a/jme3-core/src/main/java/com/jme3/renderer/RenderManager.java b/jme3-core/src/main/java/com/jme3/renderer/RenderManager.java index bdaf1d3c92..c36c6c2955 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/RenderManager.java +++ b/jme3-core/src/main/java/com/jme3/renderer/RenderManager.java @@ -66,6 +66,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.function.Predicate; import java.util.logging.Logger; /** @@ -104,6 +105,7 @@ public class RenderManager { private TechniqueDef.LightMode preferredLightMode = TechniqueDef.LightMode.MultiPass; private int singlePassLightBatchSize = 1; private MatParamOverride boundDrawBufferId=new MatParamOverride(VarType.Int,"BoundDrawBuffer",0); + private Predicate renderFilter; /** @@ -626,6 +628,7 @@ public void updateUniformBindings(Shader shader) { * @see com.jme3.material.Material#render(com.jme3.scene.Geometry, com.jme3.renderer.RenderManager) */ public void renderGeometry(Geometry geom) { + if (renderFilter != null && !renderFilter.test(geom)) return; this.renderer.pushDebugGroup(geom.getName()); if (geom.isIgnoreTransform()) { setWorldMatrix(Matrix4f.IDENTITY); @@ -1328,4 +1331,24 @@ public void setPassDrawBufferTargetIdToShaders(boolean v) { this.forcedOverrides.remove(boundDrawBufferId); } } + /** + * Set a render filter. Every geometry will be tested against this filter + * before rendering and will only be rendered if the filter returns true. + * + * @param filter + */ + public void setRenderFilter(Predicate filter) { + renderFilter = filter; + } + + /** + * Returns the render filter that the RenderManager is currently using + * + * @param filter + * the render filter + */ + public Predicate getRenderFilter() { + return renderFilter; + } + } diff --git a/jme3-core/src/main/resources/Common/IBL/IBLKernels.frag b/jme3-core/src/main/resources/Common/IBL/IBLKernels.frag new file mode 100644 index 0000000000..7f32c0ae01 --- /dev/null +++ b/jme3-core/src/main/resources/Common/IBL/IBLKernels.frag @@ -0,0 +1,109 @@ +/** +* This code is based on the following articles: +* https://learnopengl.com/PBR/IBL/Diffuse-irradiance +* https://learnopengl.com/PBR/IBL/Specular-IBL +* - Riccardo Balbo +*/ +#import "Common/ShaderLib/GLSLCompat.glsllib" +#import "Common/IBL/Math.glsl" + +in vec2 TexCoords; +in vec3 LocalPos; + +uniform samplerCube m_EnvMap; +uniform float m_Roughness; +uniform int m_FaceId; + +void brdfKernel(){ + float NdotV=TexCoords.x; + float m_Roughness=TexCoords.y; + + vec3 V; + V.x = sqrt(1.0 - NdotV*NdotV); + V.y = 0.0; + V.z = NdotV; + float A = 0.0; + float B = 0.0; + vec3 N = vec3(0.0, 0.0, 1.0); + const uint SAMPLE_COUNT = 1024u; + for(uint i = 0u; i < SAMPLE_COUNT; i++){ + vec4 Xi = Hammersley(i, SAMPLE_COUNT); + vec3 H = ImportanceSampleGGX(Xi, m_Roughness, N); + vec3 L = normalize(2.0 * dot(V, H) * H - V); + float NdotL = max(L.z, 0.0); + float NdotH = max(H.z, 0.0); + float VdotH = max(dot(V, H), 0.0); + if(NdotL > 0.0){ + float G = GeometrySmith(N, V, L, m_Roughness); + float G_Vis = (G * VdotH) / (NdotH * NdotV); + float Fc = pow(1.0 - VdotH, 5.0); + A += (1.0 - Fc) * G_Vis; + B += Fc * G_Vis; + } + } + A /= float(SAMPLE_COUNT); + B /= float(SAMPLE_COUNT); + outFragColor.rg=vec2(A, B); + outFragColor.ba=vec2(0); +} + +void irradianceKernel(){ + // the sample direction equals the hemisphere's orientation + vec3 N = normalize(LocalPos); + vec3 irradiance = vec3(0.0); + vec3 up = vec3(0.0, 1.0, 0.0); + vec3 right = cross(up, N); + up = cross(N, right); + float sampleDelta = 0.025; + float nrSamples = 0.0; + for(float phi = 0.0; phi < 2.0 * PI; phi += sampleDelta){ + for(float theta = 0.0; theta < 0.5 * PI; theta += sampleDelta){ + // spherical to cartesian (in tangent space) + vec3 tangentSample = vec3(sin(theta) * cos(phi), sin(theta) * sin(phi), cos(theta)); + // tangent space to world + vec3 sampleVec = tangentSample.x * right + tangentSample.y * up + tangentSample.z * N; + irradiance += texture(m_EnvMap, sampleVec).rgb * cos(theta) * sin(theta); + nrSamples++; + } + } + irradiance = PI * irradiance * (1.0 / float(nrSamples)); + outFragColor = vec4(irradiance, 1.0); +} + +void prefilteredEnvKernel(){ + vec3 N = normalize(LocalPos); + vec3 R = N; + vec3 V = R; + + // float a2 = m_Roughness; + float a2 = m_Roughness * m_Roughness; // jme impl, why? + a2 *= a2; + + const uint SAMPLE_COUNT = 1024u; + float totalWeight = 0.0; + vec3 prefilteredColor = vec3(0.0); + for(uint i = 0u; i < SAMPLE_COUNT; ++i) { + vec4 Xi = Hammersley(i, SAMPLE_COUNT); + vec3 H = ImportanceSampleGGX(Xi, a2, N); + float VoH = dot(V,H); + vec3 L = normalize(2.0 * VoH * H - V); + float NdotL = max(dot(N, L), 0.0); + if(NdotL > 0.0) { + // TODO: use mipmap + prefilteredColor += texture(m_EnvMap, L).rgb * NdotL; + totalWeight += NdotL; + } + } + prefilteredColor = prefilteredColor / totalWeight; + outFragColor = vec4(prefilteredColor, 1.0); +} + +void main(){ + #if defined(SIBL) + prefilteredEnvKernel(); + #elif defined(IRRADIANCE) + irradianceKernel(); + #else + brdfKernel(); + #endif +} \ No newline at end of file diff --git a/jme3-core/src/main/resources/Common/IBL/IBLKernels.j3md b/jme3-core/src/main/resources/Common/IBL/IBLKernels.j3md new file mode 100644 index 0000000000..147c1bb8a4 --- /dev/null +++ b/jme3-core/src/main/resources/Common/IBL/IBLKernels.j3md @@ -0,0 +1,39 @@ +MaterialDef IBLKernels { + + MaterialParameters { + Int BoundDrawBuffer + TextureCubeMap EnvMap -LINEAR + Float Roughness + Int FaceId : 0 + Boolean UseBRDF + Boolean UseIrradiance + Boolean UseSpecularIBL + } + + Technique { + + VertexShader GLSL300 GLSL150 : Common/IBL/IBLKernels.vert + FragmentShader GLSL300 GLSL150 : Common/IBL/IBLKernels.frag + + WorldParameters { + WorldMatrix + ViewMatrix + ProjectionMatrix + } + + RenderState { + DepthWrite Off + DepthTest Off + DepthFunc Equal + FaceCull Off + } + + Defines { + BOUND_DRAW_BUFFER: BoundDrawBuffer + BRDF:UseBRDF + IRRADIANCE: UseIrradiance + SIBL: UseSpecularIBL + } + + } +} \ No newline at end of file diff --git a/jme3-core/src/main/resources/Common/IBL/IBLKernels.vert b/jme3-core/src/main/resources/Common/IBL/IBLKernels.vert new file mode 100644 index 0000000000..aff3d7eae6 --- /dev/null +++ b/jme3-core/src/main/resources/Common/IBL/IBLKernels.vert @@ -0,0 +1,31 @@ +#import "Common/ShaderLib/GLSLCompat.glsllib" + +/** +* This code is based on the following articles: +* https://learnopengl.com/PBR/IBL/Diffuse-irradiance +* https://learnopengl.com/PBR/IBL/Specular-IBL +* - Riccardo Balbo +*/ +in vec3 inPosition; +in vec2 inTexCoord; +in vec3 inNormal; + +out vec2 TexCoords; +out vec3 LocalPos; + +uniform mat4 g_ViewMatrix; +uniform mat4 g_WorldMatrix; +uniform mat4 g_ProjectionMatrix; + +void main() { + LocalPos = inPosition.xyz; + TexCoords = inTexCoord.xy; + #ifdef BRDF + vec2 pos = inPosition.xy * 2.0 - 1.0; + gl_Position = vec4(pos, 0.0, 1.0); + #else + mat4 rotView = mat4(mat3(g_ViewMatrix)); // remove translation from the view matrix + vec4 clipPos = g_ProjectionMatrix * rotView * vec4(LocalPos, 1.0); + gl_Position = clipPos.xyww; + #endif +} \ No newline at end of file diff --git a/jme3-core/src/main/resources/Common/IBL/Math.glsl b/jme3-core/src/main/resources/Common/IBL/Math.glsl new file mode 100644 index 0000000000..e7e57240bd --- /dev/null +++ b/jme3-core/src/main/resources/Common/IBL/Math.glsl @@ -0,0 +1,95 @@ +/** +* This code is based on the following articles: +* https://learnopengl.com/PBR/IBL/Diffuse-irradiance +* https://learnopengl.com/PBR/IBL/Specular-IBL +* - Riccardo Balbo +*/ +const float PI = 3.14159265359; + +float RadicalInverse_VdC(uint bits) { + bits = (bits << 16u) | (bits >> 16u); + bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u); + bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u); + bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u); + bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u); + return float(bits) * 2.3283064365386963e-10; // / 0x100000000 +} + +vec4 Hammersley(uint i, uint N){ + vec4 store=vec4(0); + store.x = float(i) / float(N); + store.y = RadicalInverse_VdC(i); + + float phi = 2.0 * PI *store.x; + store.z = cos(phi); + store.w = sin(phi); + + return store; +} + +// float VanDerCorput(uint n, uint base){ +// float invBase = 1.0 / float(base); +// float denom = 1.0; +// float result = 0.0; + +// for(uint i = 0u; i < 32u; ++i) +// { +// if(n > 0u) +// { +// denom = mod(float(n), 2.0); +// result += denom * invBase; +// invBase = invBase / 2.0; +// n = uint(float(n) / 2.0); +// } +// } + +// return result; +// } + +// vec2 Hammersley(uint i, uint N){ +// return vec2(float(i)/float(N), VanDerCorput(i, 2u)); +// } + + +vec3 ImportanceSampleGGX(vec4 Xi, float a2, vec3 N){ + + float cosTheta = sqrt((1.0 - Xi.y) / (1.0 + (a2 - 1.0) * Xi.y)); + float sinTheta = sqrt(1.0 - cosTheta*cosTheta); + + // from spherical coordinates to cartesian coordinates + vec3 H; + H.x = Xi.z * sinTheta; + H.y = Xi.w * sinTheta; + H.z = cosTheta; + + // from tangent-space vector to world-space sample vector + vec3 up = abs(N.z) < 0.999 ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0); + vec3 tangent = normalize(cross(up, N)); + vec3 bitangent = cross(N, tangent); + + vec3 sampleVec = tangent * H.x + bitangent * H.y + N * H.z; + return normalize(sampleVec); +} + + + + +float GeometrySchlickGGX(float NdotV, float roughness){ + float a = roughness; + float k = (a * a) / 2.0; + + float nom = NdotV; + float denom = NdotV * (1.0 - k) + k; + + return nom / denom; +} +// ---------------------------------------------------------------------------- +float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness){ + float NdotV = max(dot(N, V), 0.0); + float NdotL = max(dot(N, L), 0.0); + float ggx2 = GeometrySchlickGGX(NdotV, roughness); + float ggx1 = GeometrySchlickGGX(NdotL, roughness); + + return ggx1 * ggx2; +} + \ No newline at end of file diff --git a/jme3-core/src/main/resources/Common/IBLSphH/IBLSphH.frag b/jme3-core/src/main/resources/Common/IBLSphH/IBLSphH.frag new file mode 100644 index 0000000000..6e83dcfde8 --- /dev/null +++ b/jme3-core/src/main/resources/Common/IBLSphH/IBLSphH.frag @@ -0,0 +1,191 @@ +/** + +* - Riccardo Balbo +*/ +#import "Common/ShaderLib/GLSLCompat.glsllib" +#import "Common/IBL/Math.glsl" + +// #define NUM_SH_COEFFICIENT 9 +#ifndef PI + #define PI 3.1415926535897932384626433832795 +#endif + +in vec2 TexCoords; +in vec3 LocalPos; + + +uniform samplerCube m_Texture; +#ifdef SH_COEF + uniform sampler2D m_ShCoef; +#endif +uniform vec2 m_Resolution; +uniform int m_FaceId; + +const float sqrtPi = sqrt(PI); +const float sqrt3Pi = sqrt(3. / PI); +const float sqrt5Pi = sqrt(5. / PI); +const float sqrt15Pi = sqrt(15. / PI); + +#ifdef REMAP_MAX_VALUE + uniform float m_RemapMaxValue; +#endif + + +vec3 getVectorFromCubemapFaceTexCoord(float x, float y, float mapSize, int face) { + float u; + float v; + + /* transform from [0..res - 1] to [- (1 - 1 / res) .. (1 - 1 / res)] + * (+ 0.5f is for texel center addressing) */ + u = (2.0 * (x + 0.5) / mapSize) - 1.0; + v = (2.0 * (y + 0.5) / mapSize) - 1.0; + + + // Warp texel centers in the proximity of the edges. + float a = pow(mapSize, 2.0) / pow(mapSize - 1., 3.0); + + u = a * pow(u, 3.) + u; + v = a * pow(v, 3.) + v; + //compute vector depending on the face + // Code from Nvtt : https://github.com/castano/nvidia-texture-tools/blob/master/src/nvtt/CubeSurface.cpp#L101 + vec3 o =vec3(0); + switch(face) { + case 0: + o= normalize(vec3(1., -v, -u)); + break; + case 1: + o= normalize(vec3(-1., -v, u)); + break; + case 2: + o= normalize(vec3(u, 1., v)); + break; + case 3: + o= normalize(vec3(u, -1., -v)); + break; + case 4: + o= normalize(vec3(u, -v, 1.)); + break; + case 5: + o= normalize(vec3(-u, -v, -1.)); + break; + } + + return o; +} + +float atan2(in float y, in float x) { + bool s = (abs(x) > abs(y)); + return mix(PI / 2.0 - atan(x, y), atan(y, x), s); +} + +float areaElement(float x, float y) { + return atan2(x * y, sqrt(x * x + y * y + 1.)); +} + +float getSolidAngleAndVector(float x, float y, float mapSize, int face, out vec3 store) { + /* transform from [0..res - 1] to [- (1 - 1 / res) .. (1 - 1 / res)] + (+ 0.5f is for texel center addressing) */ + float u = (2.0 * (x + 0.5) / mapSize) - 1.0; + float v = (2.0 * (y + 0.5) / mapSize) - 1.0; + + store = getVectorFromCubemapFaceTexCoord(x, y, mapSize, face); + + /* Solid angle weight approximation : + * U and V are the -1..1 texture coordinate on the current face. + * Get projected area for this texel */ + float x0, y0, x1, y1; + float invRes = 1.0 / mapSize; + x0 = u - invRes; + y0 = v - invRes; + x1 = u + invRes; + y1 = v + invRes; + + return areaElement(x0, y0) - areaElement(x0, y1) - areaElement(x1, y0) + areaElement(x1, y1); +} + +void evalShBasis(vec3 texelVect, int i, out float shDir) { + float xV = texelVect.x; + float yV = texelVect.y; + float zV = texelVect.z; + + float x2 = xV * xV; + float y2 = yV * yV; + float z2 = zV * zV; + + if(i==0) shDir = (1. / (2. * sqrtPi)); + else if(i==1) shDir = -(sqrt3Pi * yV) / 2.; + else if(i == 2) shDir = (sqrt3Pi * zV) / 2.; + else if(i == 3) shDir = -(sqrt3Pi * xV) / 2.; + else if(i == 4) shDir = (sqrt15Pi * xV * yV) / 2.; + else if(i == 5) shDir = -(sqrt15Pi * yV * zV) / 2.; + else if(i == 6) shDir = (sqrt5Pi * (-1. + 3. * z2)) / 4.; + else if(i == 7) shDir = -(sqrt15Pi * xV * zV) / 2.; + else shDir = sqrt15Pi * (x2 - y2) / 4.; +} + +vec3 pixelFaceToV(int faceId, float pixelX, float pixelY, float cubeMapSize) { + vec2 normalizedCoords = vec2((2.0 * pixelX + 1.0) / cubeMapSize, (2.0 * pixelY + 1.0) / cubeMapSize); + + vec3 direction; + if(faceId == 0) { + direction = vec3(1.0, -normalizedCoords.y, -normalizedCoords.x); + } else if(faceId == 1) { + direction = vec3(-1.0, -normalizedCoords.y, normalizedCoords.x); + } else if(faceId == 2) { + direction = vec3(normalizedCoords.x, 1.0, normalizedCoords.y); + } else if(faceId == 3) { + direction = vec3(normalizedCoords.x, -1.0, -normalizedCoords.y); + } else if(faceId == 4) { + direction = vec3(normalizedCoords.x, -normalizedCoords.y, 1.0); + } else if(faceId == 5) { + direction = vec3(-normalizedCoords.x, -normalizedCoords.y, -1.0); + } + + return normalize(direction); +} + +void sphKernel() { + int width = int(m_Resolution.x); + int height = int(m_Resolution.y); + vec3 texelVect=vec3(0); + float shDir=0.; + float weight=0.; + vec4 color=vec4(0); + + 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 + + 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 REMAP_MAX_VALUE + shCoef.xyz=shCoef.xyz*m_RemapMaxValue; + weightAccum=weightAccum*m_RemapMaxValue; + #endif + + outFragColor = vec4(shCoef.xyz,weightAccum); + +} + +void main() { + sphKernel(); +} \ No newline at end of file diff --git a/jme3-core/src/main/resources/Common/IBLSphH/IBLSphH.j3md b/jme3-core/src/main/resources/Common/IBLSphH/IBLSphH.j3md new file mode 100644 index 0000000000..eaafd2e108 --- /dev/null +++ b/jme3-core/src/main/resources/Common/IBLSphH/IBLSphH.j3md @@ -0,0 +1,34 @@ +MaterialDef IBLSphH { + + MaterialParameters { + Int BoundDrawBuffer + TextureCubeMap Texture -LINEAR + Int FaceId : 0 + Texture2D ShCoef -LINEAR + Vector2 Resolution + Float RemapMaxValue + } + + Technique { + + VertexShader GLSL300 GLSL150 : Common/IBLSphH/IBLSphH.vert + FragmentShader GLSL300 GLSL150 : Common/IBLSphH/IBLSphH.frag + + WorldParameters { + } + + RenderState { + DepthWrite Off + DepthTest Off + DepthFunc Equal + FaceCull Off + } + + Defines { + BOUND_DRAW_BUFFER: BoundDrawBuffer + REMAP_MAX_VALUE: RemapMaxValue + SH_COEF: ShCoef + } + + } +} \ No newline at end of file diff --git a/jme3-core/src/main/resources/Common/IBLSphH/IBLSphH.vert b/jme3-core/src/main/resources/Common/IBLSphH/IBLSphH.vert new file mode 100644 index 0000000000..f7a3c82655 --- /dev/null +++ b/jme3-core/src/main/resources/Common/IBLSphH/IBLSphH.vert @@ -0,0 +1,18 @@ +#import "Common/ShaderLib/GLSLCompat.glsllib" + +/** +*- Riccardo Balbo +*/ +in vec3 inPosition; +in vec2 inTexCoord; + +out vec2 TexCoords; +out vec3 LocalPos; + + +void main() { + LocalPos = inPosition.xyz; + TexCoords = inTexCoord.xy; + vec2 pos = inPosition.xy * 2.0 - 1.0; + gl_Position = vec4(pos, 0.0, 1.0); +} \ No newline at end of file diff --git a/jme3-examples/src/main/java/jme3test/light/pbr/TestPBRLighting.java b/jme3-examples/src/main/java/jme3test/light/pbr/TestPBRLighting.java index 7de1e456c7..50fb4a482b 100644 --- a/jme3-examples/src/main/java/jme3test/light/pbr/TestPBRLighting.java +++ b/jme3-examples/src/main/java/jme3test/light/pbr/TestPBRLighting.java @@ -34,6 +34,7 @@ import com.jme3.app.SimpleApplication; import com.jme3.environment.EnvironmentCamera; import com.jme3.environment.LightProbeFactory; +import com.jme3.environment.FastLightProbeFactory; import com.jme3.environment.generation.JobProgressAdapter; import com.jme3.environment.util.EnvMapUtils; import com.jme3.environment.util.LightsDebugState; @@ -59,7 +60,8 @@ * @author nehon */ public class TestPBRLighting extends SimpleApplication { - + private static final boolean USE_ACCELERATED_BAKING=true; + private static final int RESOLUTION=256; public static void main(String[] args) { TestPBRLighting app = new TestPBRLighting(); app.start(); @@ -111,7 +113,7 @@ public void simpleInitApp() { model.setMaterial(pbrMat); - final EnvironmentCamera envCam = new EnvironmentCamera(256, new Vector3f(0, 3f, 0)); + final EnvironmentCamera envCam = new EnvironmentCamera(RESOLUTION, new Vector3f(0, 3f, 0)); stateManager.attach(envCam); // EnvironmentManager envManager = new EnvironmentManager(); @@ -199,18 +201,23 @@ public void simpleUpdate(float tpf) { if (frame == 2) { modelNode.removeFromParent(); - final LightProbe probe = LightProbeFactory.makeProbe(stateManager.getState(EnvironmentCamera.class), rootNode, new JobProgressAdapter() { + LightProbe probe; - @Override - public void done(LightProbe result) { - System.err.println("Done rendering env maps"); - tex = EnvMapUtils.getCubeMapCrossDebugViewWithMipMaps(result.getPrefilteredEnvMap(), assetManager); - } - }); + if (USE_ACCELERATED_BAKING) { + probe = FastLightProbeFactory.makeProbe(renderManager, assetManager, RESOLUTION, Vector3f.ZERO, 1f, 1000f, rootNode); + } else { + probe = LightProbeFactory.makeProbe(stateManager.getState(EnvironmentCamera.class), rootNode, new JobProgressAdapter() { + + @Override + public void done(LightProbe result) { + System.err.println("Done rendering env maps"); + tex = EnvMapUtils.getCubeMapCrossDebugViewWithMipMaps(result.getPrefilteredEnvMap(), assetManager); + } + }); + } probe.getArea().setRadius(100); rootNode.addLight(probe); //getStateManager().getState(EnvironmentManager.class).addEnvProbe(probe); - } if (frame > 10 && modelNode.getParent() == null) { rootNode.attachChild(modelNode); diff --git a/jme3-examples/src/main/java/jme3test/light/pbr/TestPBRSimple.java b/jme3-examples/src/main/java/jme3test/light/pbr/TestPBRSimple.java new file mode 100644 index 0000000000..4070c2a401 --- /dev/null +++ b/jme3-examples/src/main/java/jme3test/light/pbr/TestPBRSimple.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2009-2023 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 jme3test.light.pbr; + + +import com.jme3.app.SimpleApplication; +import com.jme3.environment.EnvironmentProbeControl; +import com.jme3.input.ChaseCamera; +import com.jme3.material.Material; +import com.jme3.math.FastMath; +import com.jme3.scene.Geometry; +import com.jme3.scene.Spatial; +import com.jme3.util.SkyFactory; +import com.jme3.util.mikktspace.MikktspaceTangentGenerator; + +/** + * A simpler PBR example that uses EnvironmentProbeControl to bake the environment + */ +public class TestPBRSimple extends SimpleApplication { + private boolean REALTIME_BAKING = false; + + public static void main(String[] args) { + new TestPBRSimple().start(); + } + + @Override + public void simpleInitApp() { + + + Geometry model = (Geometry) assetManager.loadModel("Models/Tank/tank.j3o"); + MikktspaceTangentGenerator.generate(model); + + Material pbrMat = assetManager.loadMaterial("Models/Tank/tank.j3m"); + model.setMaterial(pbrMat); + rootNode.attachChild(model); + + ChaseCamera chaseCam = new ChaseCamera(cam, model, inputManager); + chaseCam.setDragToRotate(true); + chaseCam.setMinVerticalRotation(-FastMath.HALF_PI); + chaseCam.setMaxDistance(1000); + chaseCam.setSmoothMotion(true); + chaseCam.setRotationSensitivity(10); + chaseCam.setZoomSensitivity(5); + flyCam.setEnabled(false); + + Spatial sky = SkyFactory.createSky(assetManager, "Textures/Sky/Path.hdr", SkyFactory.EnvMapType.EquirectMap); + rootNode.attachChild(sky); + + // Create baker control + EnvironmentProbeControl envProbe=new EnvironmentProbeControl(assetManager,256); + rootNode.addControl(envProbe); + + // Tag the sky, only the tagged spatials will be rendered in the env map + envProbe.tag(sky); + + + + } + + + float lastBake = 0; + @Override + public void simpleUpdate(float tpf) { + if (REALTIME_BAKING) { + lastBake += tpf; + if (lastBake > 1.4f) { + rootNode.getControl(EnvironmentProbeControl.class).rebake(); + lastBake = 0; + } + } + } +} \ No newline at end of file