From 8eb62506c5a4b450661f11cb26238e79abfa00f6 Mon Sep 17 00:00:00 2001 From: MeFisto94 Date: Tue, 26 Oct 2021 00:46:33 +0200 Subject: [PATCH] Create a basic example that outlines a basic deferred rendering approach --- .../light/deferred/DeferredLightingPass.java | 104 ++++++ .../jme3test/light/deferred/TestDeferred.java | 306 ++++++++++++++++++ .../TestDeferred/MatDefs/GeometryPass.frag | 13 + .../TestDeferred/MatDefs/GeometryPass.j3md | 15 + .../TestDeferred/MatDefs/GeometryPass.vert | 16 + .../TestDeferred/MatDefs/LightingPass.frag | 36 +++ .../TestDeferred/MatDefs/LightingPass.j3md | 20 ++ .../TestDeferred/MatDefs/LightingPass.vert | 7 + 8 files changed, 517 insertions(+) create mode 100644 jme3-examples/src/main/java/jme3test/light/deferred/DeferredLightingPass.java create mode 100644 jme3-examples/src/main/java/jme3test/light/deferred/TestDeferred.java create mode 100644 jme3-examples/src/main/resources/TestDeferred/MatDefs/GeometryPass.frag create mode 100644 jme3-examples/src/main/resources/TestDeferred/MatDefs/GeometryPass.j3md create mode 100644 jme3-examples/src/main/resources/TestDeferred/MatDefs/GeometryPass.vert create mode 100644 jme3-examples/src/main/resources/TestDeferred/MatDefs/LightingPass.frag create mode 100644 jme3-examples/src/main/resources/TestDeferred/MatDefs/LightingPass.j3md create mode 100644 jme3-examples/src/main/resources/TestDeferred/MatDefs/LightingPass.vert diff --git a/jme3-examples/src/main/java/jme3test/light/deferred/DeferredLightingPass.java b/jme3-examples/src/main/java/jme3test/light/deferred/DeferredLightingPass.java new file mode 100644 index 0000000000..e3323060b4 --- /dev/null +++ b/jme3-examples/src/main/java/jme3test/light/deferred/DeferredLightingPass.java @@ -0,0 +1,104 @@ +package jme3test.light.deferred; + +import com.jme3.asset.AssetManager; +import com.jme3.light.PointLight; +import com.jme3.material.Material; +import com.jme3.math.Vector4f; +import com.jme3.post.Filter; +import com.jme3.renderer.RenderManager; +import com.jme3.renderer.ViewPort; +import com.jme3.scene.Node; +import com.jme3.shader.BufferObject; +import com.jme3.shader.VarType; + +import java.nio.ByteBuffer; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +public class DeferredLightingPass extends Filter { + protected AssetManager assetManager; + protected Material material; + protected ViewPort vp; + protected Node rootNode; + + public DeferredLightingPass(Node rootNode) { + super("Deferred: Lighting Pass"); + this.rootNode = rootNode; + } + + @Override + protected void initFilter(AssetManager manager, RenderManager renderManager, ViewPort vp, int w, int h) { + this.assetManager = manager; + this.vp = vp; + if (material == null) { // allow it to be pre-set from somewhere. + material = new Material(assetManager, "TestDeferred/MatDefs/LightingPass.j3md"); + } + } + + @Override + protected Material getMaterial() { + return material; + } + + public void setMaterial(Material material) { + this.material = material; + } + + public void setRootNode(Node rootNode) { + this.rootNode = rootNode; + } + + @Override + protected void preFrame(float tpf) { + super.preFrame(tpf); + material.setVector3("ViewPosition", vp.getCamera().getLocation()); + List lights = StreamSupport.stream(rootNode.getWorldLightList().spliterator(), false) + .filter(l -> l instanceof PointLight) + .map(l -> (PointLight)l).collect(Collectors.toList()); + + Vector4f[] positions = lights.stream().map(pl -> new Vector4f(pl.getPosition().x, pl.getPosition().y, pl.getPosition().z, pl.getRadius())) + .toArray(Vector4f[]::new); + Vector4f[] colors = lights.stream().map(pl -> pl.getColor().toVector4f()).toArray(Vector4f[]::new); + material.setParam("PointLight_Position", VarType.Vector4Array, positions); + material.setParam("PointLight_Color", VarType.Vector4Array, colors); + /*bo.setPointLights(lights); + material.setUniformBufferObject("PointLights", bo);*/ + } + + @Override + protected boolean isRequiresSceneTexture() { + return false; + } + + @Override + protected boolean isRequiresDepthTexture() { + return false; + } + + private static class CustomBufferObject extends BufferObject { + private Iterable lights; + + public CustomBufferObject() { + super(Layout.std140); + setBufferType(BufferType.UniformBufferObject); + } + + public void setPointLights(Iterable lights) { + this.lights = lights; + } + + @Override + public ByteBuffer computeData(int maxSize) { + // TODO: assert lights.length <= 32 + ByteBuffer data = ByteBuffer.allocateDirect(32 * 8 * 4); + for (PointLight light: lights) { + write(data, light.getPosition()); + writeVec4(data, light.getColor()); + data.putFloat(light.getRadius()); + } + + return data; + } + } +} diff --git a/jme3-examples/src/main/java/jme3test/light/deferred/TestDeferred.java b/jme3-examples/src/main/java/jme3test/light/deferred/TestDeferred.java new file mode 100644 index 0000000000..df95827fca --- /dev/null +++ b/jme3-examples/src/main/java/jme3test/light/deferred/TestDeferred.java @@ -0,0 +1,306 @@ +/* + * Copyright (c) 2009-2021 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.deferred; + +import com.jme3.app.SimpleApplication; +import com.jme3.light.PointLight; +import com.jme3.material.Material; +import com.jme3.math.ColorRGBA; +import com.jme3.math.Vector3f; +import com.jme3.post.FilterPostProcessor; +import com.jme3.post.SceneProcessor; +import com.jme3.profile.AppProfiler; +import com.jme3.renderer.RenderManager; +import com.jme3.renderer.ViewPort; +import com.jme3.renderer.queue.RenderQueue; +import com.jme3.scene.Geometry; +import com.jme3.scene.Node; +import com.jme3.scene.Spatial; +import com.jme3.scene.shape.Sphere; +import com.jme3.texture.FrameBuffer; +import com.jme3.texture.Image.Format; +import com.jme3.texture.Texture2D; +import com.jme3.ui.Picture; + +/** + * An example implementation of deferred rendering, where lighting is done after the initial geometry rendering.
+ * It is by no means a competitive rendering implementation to be used on reallife applications, because it lacks the + * critical performance feature of light culling and is only implemented with a very limited lighting model and a + * hardcoded ambience light (and a hardcoded number of lights to be used by the renderer).
+ * Furthermore, it renders to a second viewport instead of the display viewport, so that the rendering result can be + * used as a GUI Picture (PiP) instead.
+ * + * When SSBO/UBO is properly working, you should be looking into using them instead and invoking the filter + * (fragment shader) multiple times, in case you exceed the hardcoded number of lights (UBO: You need to choose them, + * so that sizeof(UBO) <= 16 kiB). + * @author MeFisto94 + */ +public class TestDeferred extends SimpleApplication { + private Picture display1, display2, display3, display4; + private FilterPostProcessor fpp; + private ViewPort gBufferViewPort, lightingViewPort; + + public static void main(String[] args){ + TestDeferred app = new TestDeferred(); + app.start(); + } + + protected Spatial buildScene() { + Node root = new Node("GeometryRoot"); + + Geometry cube1 = buildCube(ColorRGBA.Red); + cube1.setLocalTranslation(-1f, 0f, 0f); + Geometry cube2 = buildCube(ColorRGBA.Green); + cube2.setLocalTranslation(0f, 0f, 0f); + Geometry cube3 = buildCube(ColorRGBA.Blue); + cube3.setLocalTranslation(1f, 0f, 0f); + + ColorRGBA color4 = ColorRGBA.White; + Geometry cube4 = buildCube(color4); + cube4.setLocalTranslation(-0.5f, 1f, 0f); + ColorRGBA color5 = ColorRGBA.Yellow; + Geometry cube5 = buildCube(color5); + cube5.setLocalTranslation(0.5f, 1f, 0f); + + root.attachChild(cube1); + root.attachChild(cube2); + root.attachChild(cube3); + root.attachChild(cube4); + root.attachChild(cube5); + + root.addLight(new PointLight(cube1.getLocalTranslation(), ColorRGBA.Red, 4f)); + root.addLight(new PointLight(cube2.getLocalTranslation(), ColorRGBA.Green, 4f)); + root.addLight(new PointLight(cube3.getLocalTranslation(), ColorRGBA.Blue, 4f)); + root.addLight(new PointLight(cube4.getLocalTranslation(), color4, 4f)); + root.addLight(new PointLight(cube5.getLocalTranslation(), color5, 4f)); + + return root; + } + + private Geometry buildCube(ColorRGBA color) { + Geometry cube = new Geometry("Box", new Sphere(32, 32, 0.5f)); + Material mat = new Material(assetManager, "TestDeferred/MatDefs/GeometryPass.j3md"); + mat.setColor("Albedo", color); + cube.setMaterial(mat); + return cube; + } + + @Override + public void simpleInitApp() { + cam.setLocation(new Vector3f(0f, 0.5f, 3f)); + + // Geometry Pass + gBufferViewPort = renderManager.createMainView("GBuffer", getCamera()); + gBufferViewPort.setClearFlags(true, true, true); + gBufferViewPort.addProcessor(new GBufferSceneProcessor()); + gBufferViewPort.attachScene(buildScene()); + + // Lighting pass + // This uses its own viewPort, because we don't want to render the output to the display fb, but instead to + // another GUI Picture / Texture, so we can display it as a fourth PiP element. Typically, you'd add the filter + // to your main post FX stack and make it directly render to the screen instead. Note that we specifically + // require a dedicated viewport (instead of just setting the output texture), because GUI is rendered _before_ + // Filter passes(!) + lightingViewPort = renderManager.createMainView("LightingPass", getCamera()); + lightingViewPort.setClearFlags(true, true, true); + lightingViewPort.addProcessor(new LightingSceneProcessor()); + + fpp = new FilterPostProcessor(assetManager); + fpp.addFilter(new DeferredLightingPass((Node)gBufferViewPort.getScenes().get(0))); + lightingViewPort.addProcessor(fpp); + + display1 = new Picture("Picture"); + display1.move(0, 0, -1); // make it appear behind stats view + display2 = (Picture) display1.clone(); + display3 = (Picture) display1.clone(); + display4 = (Picture) display1.clone(); + } + + @Override + public void destroy() { + gBufferViewPort.clearProcessors(); + lightingViewPort.clearProcessors(); + super.destroy(); + } + + @Override + public void simpleUpdate(float tpf) { + for (Spatial spatial : gBufferViewPort.getScenes()) { + spatial.updateLogicalState(tpf); + } + + for (Spatial spatial : gBufferViewPort.getScenes()) { + spatial.updateGeometricState(); + } + } + + private class GBufferSceneProcessor implements SceneProcessor { + private boolean initialized = false; + private FrameBuffer gBuffer; + + // Scene Processor from now on + @Override + public void initialize(RenderManager rm, ViewPort vp) { + reshape(vp, vp.getCamera().getWidth(), vp.getCamera().getHeight()); + gBufferViewPort.setOutputFrameBuffer(gBuffer); + guiViewPort.setClearFlags(true, true, true); + + guiNode.attachChild(display1); + guiNode.attachChild(display2); + guiNode.attachChild(display3); + guiNode.attachChild(display4); + guiNode.updateGeometricState(); + } + + @Override + public void reshape(ViewPort vp, int w, int h) { + Texture2D positionTexture = new Texture2D(w, h, Format.RGBA16F); + Texture2D normalsTexture = new Texture2D(w, h, Format.RGBA16F); + Texture2D albedoTexture = new Texture2D(w, h, Format.RGBA16F); + + gBuffer = new FrameBuffer(w, h, 1); + gBuffer.addColorTarget(FrameBuffer.FrameBufferTarget.newTarget(positionTexture)); + gBuffer.addColorTarget(FrameBuffer.FrameBufferTarget.newTarget(normalsTexture)); + gBuffer.addColorTarget(FrameBuffer.FrameBufferTarget.newTarget(albedoTexture)); + gBuffer.setMultiTarget(true); + + display2.setTexture(assetManager, positionTexture, false); + display3.setTexture(assetManager, normalsTexture, false); + display4.setTexture(assetManager, albedoTexture, false); + + display2.setPosition(0, h/2f); + display2.setWidth(w/2f); + display2.setHeight(h/2f); + + display3.setPosition(w/2f, h/2f); + display3.setWidth(w/2f); + display3.setHeight(h/2f); + + display4.setPosition(w/2f, 0f); + display4.setWidth(w/2f); + display4.setHeight(h/2f); + + guiNode.updateGeometricState(); + + Material mat = new Material(assetManager, "TestDeferred/MatDefs/LightingPass.j3md"); + mat.setTexture("WorldPosition", positionTexture); + mat.setTexture("Normal", normalsTexture); + mat.setTexture("Albedo", albedoTexture); + fpp.getFilter(DeferredLightingPass.class).setMaterial(mat); + + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void preFrame(float tpf) { + } + + @Override + public void postQueue(RenderQueue rq) { + } + + @Override + public void postFrame(FrameBuffer out) { + } + + @Override + public void cleanup() { + initialized = false; + } + + @Override + public void setProfiler(AppProfiler profiler) { + // not implemented + } + } + + private class LightingSceneProcessor implements SceneProcessor { + private boolean initialized = false; + private FrameBuffer lightingBuffer; + + @Override + public void initialize(RenderManager rm, ViewPort vp) { + reshape(vp, vp.getCamera().getWidth(), vp.getCamera().getHeight()); + lightingViewPort.setOutputFrameBuffer(lightingBuffer); + + guiNode.attachChild(display1); + guiNode.updateGeometricState(); + } + + @Override + public void reshape(ViewPort vp, int w, int h) { + Texture2D outputTexture = new Texture2D(w, h, Format.RGB111110F); + lightingBuffer = new FrameBuffer(w, h, 1); + lightingBuffer.addColorTarget(FrameBuffer.FrameBufferTarget.newTarget(outputTexture)); + display1.setTexture(assetManager, outputTexture, false); + display1.setPosition(0, 0); + display1.setWidth(w/2f); + display1.setHeight(h/2f); + + guiNode.updateGeometricState(); + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void preFrame(float tpf) { + } + + @Override + public void postQueue(RenderQueue rq) { + } + + @Override + public void postFrame(FrameBuffer out) { + } + + @Override + public void cleanup() { + initialized = false; + } + + @Override + public void setProfiler(AppProfiler profiler) { + // not implemented + } + } +} diff --git a/jme3-examples/src/main/resources/TestDeferred/MatDefs/GeometryPass.frag b/jme3-examples/src/main/resources/TestDeferred/MatDefs/GeometryPass.frag new file mode 100644 index 0000000000..05e50d60fb --- /dev/null +++ b/jme3-examples/src/main/resources/TestDeferred/MatDefs/GeometryPass.frag @@ -0,0 +1,13 @@ +layout (location = 0) out vec4 Position; +layout (location = 1) out vec4 Normals; +layout (location = 2) out vec4 Albedo; +uniform vec4 m_Albedo; +in vec3 worldPosition; +in vec3 worldNormal; + +void main() { + Position = vec4(worldPosition, 1.0); + Normals = vec4(worldNormal, 1.0); + Albedo.rgb = m_Albedo.rgb; + Albedo.a = 1.0; // Specular +} diff --git a/jme3-examples/src/main/resources/TestDeferred/MatDefs/GeometryPass.j3md b/jme3-examples/src/main/resources/TestDeferred/MatDefs/GeometryPass.j3md new file mode 100644 index 0000000000..7666156004 --- /dev/null +++ b/jme3-examples/src/main/resources/TestDeferred/MatDefs/GeometryPass.j3md @@ -0,0 +1,15 @@ +MaterialDef GeometryPass { + MaterialParameters { + Color Albedo + } + + Technique { + VertexShader GLSL330 : TestDeferred/MatDefs/GeometryPass.vert + FragmentShader GLSL330 : TestDeferred/MatDefs/GeometryPass.frag + + WorldParameters { + WorldViewProjectionMatrix + WorldMatrix + } + } +} diff --git a/jme3-examples/src/main/resources/TestDeferred/MatDefs/GeometryPass.vert b/jme3-examples/src/main/resources/TestDeferred/MatDefs/GeometryPass.vert new file mode 100644 index 0000000000..adaae4e7c7 --- /dev/null +++ b/jme3-examples/src/main/resources/TestDeferred/MatDefs/GeometryPass.vert @@ -0,0 +1,16 @@ +#import "Common/ShaderLib/Instancing.glsllib" + +in vec3 inPosition; +in vec3 inNormal; + +out vec3 worldPosition; +out vec3 worldNormal; + +vec4 wp; + +void main() { + wp = vec4(inPosition, 1.0); + gl_Position = TransformWorldViewProjection(wp); + worldPosition = TransformWorld(wp).rgb; + worldNormal = normalize(inNormal); // Is this normalize needed? +} diff --git a/jme3-examples/src/main/resources/TestDeferred/MatDefs/LightingPass.frag b/jme3-examples/src/main/resources/TestDeferred/MatDefs/LightingPass.frag new file mode 100644 index 0000000000..047cb7483e --- /dev/null +++ b/jme3-examples/src/main/resources/TestDeferred/MatDefs/LightingPass.frag @@ -0,0 +1,36 @@ +out vec4 fragColor; +in vec2 texCoord; +uniform sampler2D m_Albedo; +uniform sampler2D m_Normal; +uniform sampler2D m_WorldPosition; +uniform vec3 m_ViewPosition; // TODO: Can we do better and get that from the projection matrix? + +// Migrate this to UBO/SSBO once available +const int NR_POINTLIGHTS = 8; +uniform vec4 m_PointLight_Position[NR_POINTLIGHTS]; +uniform vec4 m_PointLight_Color[NR_POINTLIGHTS]; + +float lightRadiusMultiplier(float radius, float maxRadius) { + return (1.0 - step(maxRadius, radius)); +} + +void main() { + vec3 Albedo = texture2D(m_Albedo, texCoord).rgb; + vec3 FragPos = texture2D(m_WorldPosition, texCoord).rgb; + vec3 Normal = texture2D(m_Normal, texCoord).rgb; + + vec3 lighting = Albedo * 0.1; // Hard-Coded Ambient Component + vec3 viewDir = normalize(m_ViewPosition - FragPos); + + // See https://learnopengl.com/Advanced-Lighting/Deferred-Shading, licensed CC-BY-NC + for(int i = 0; i < NR_POINTLIGHTS; ++i) + { + vec3 lightDir = m_PointLight_Position[i].rgb - FragPos; + vec3 lightDirNormal = normalize(lightDir); + float lightDist = length(lightDir); + vec3 diffuse = max(dot(Normal, lightDirNormal), 0.0) * Albedo * m_PointLight_Color[i].rgb; + lighting += diffuse * lightRadiusMultiplier(lightDist, m_PointLight_Position[i].a); // Handle the radius culling basically. + } + + fragColor = vec4(lighting, 1.0); +} diff --git a/jme3-examples/src/main/resources/TestDeferred/MatDefs/LightingPass.j3md b/jme3-examples/src/main/resources/TestDeferred/MatDefs/LightingPass.j3md new file mode 100644 index 0000000000..e0ccd2eadc --- /dev/null +++ b/jme3-examples/src/main/resources/TestDeferred/MatDefs/LightingPass.j3md @@ -0,0 +1,20 @@ +MaterialDef LightingPass { + MaterialParameters { + Texture2D Albedo + Texture2D Normal + Texture2D WorldPosition + Vector3 ViewPosition + Vector4Array PointLight_Position + Vector4Array PointLight_Color + } + + Technique { + VertexShader GLSL150 : Common/MatDefs/Post/Post15.vert + FragmentShader GLSL330 : TestDeferred/MatDefs/LightingPass.frag + + WorldParameters { + WorldViewProjectionMatrix + WorldMatrix + } + } +} diff --git a/jme3-examples/src/main/resources/TestDeferred/MatDefs/LightingPass.vert b/jme3-examples/src/main/resources/TestDeferred/MatDefs/LightingPass.vert new file mode 100644 index 0000000000..3ebecfeda3 --- /dev/null +++ b/jme3-examples/src/main/resources/TestDeferred/MatDefs/LightingPass.vert @@ -0,0 +1,7 @@ +#import "Common/ShaderLib/Instancing.glsllib" + +in vec3 inPosition; + +void main() { + gl_Position = TransformWorldViewProjection(vec4(inPosition, 1.0)); +}