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 5969a1d5bd..aa50608acc 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); @@ -1336,4 +1339,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