diff --git a/jme3-android/src/main/java/com/jme3/renderer/android/AndroidGL.java b/jme3-android/src/main/java/com/jme3/renderer/android/AndroidGL.java index 4c2601e1d4..701da81518 100644 --- a/jme3-android/src/main/java/com/jme3/renderer/android/AndroidGL.java +++ b/jme3-android/src/main/java/com/jme3/renderer/android/AndroidGL.java @@ -163,6 +163,11 @@ public void glGetBufferSubData(int target, long offset, ByteBuffer data) { throw new UnsupportedOperationException("OpenGL ES 2 does not support glGetBufferSubData"); } + @Override + public void glGetBufferSubData(int target, long offset, IntBuffer data) { + throw new UnsupportedOperationException("OpenGL ES 2 does not support glGetBufferSubData"); + } + @Override public void glClear(int mask) { GLES20.glClear(mask); diff --git a/jme3-core/src/main/java/com/jme3/renderer/opengl/ComputeShader.java b/jme3-core/src/main/java/com/jme3/renderer/opengl/ComputeShader.java new file mode 100644 index 0000000000..1d42da0dca --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/renderer/opengl/ComputeShader.java @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2009-2026 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.renderer.opengl; + +import com.jme3.math.Matrix4f; +import com.jme3.math.Vector2f; +import com.jme3.math.Vector3f; +import com.jme3.math.Vector4f; +import com.jme3.renderer.RendererException; +import com.jme3.texture.Texture; +import com.jme3.util.BufferUtils; + +import java.nio.FloatBuffer; +import java.nio.IntBuffer; + +/** + * A compute shader for general-purpose GPU computing (GPGPU). + *

+ * Compute shaders require OpenGL 4.3 or higher. + */ +public class ComputeShader { + + private final GL4 gl; + private final int programId; + /** + * Creates a new compute shader from GLSL source code. + */ + public ComputeShader(GL4 gl, String source) { + this.gl = gl; + + // Create and compile the shader + int shaderId = gl.glCreateShader(GL4.GL_COMPUTE_SHADER); + if (shaderId <= 0) { + throw new RendererException("Failed to create compute shader"); + } + + IntBuffer intBuf = BufferUtils.createIntBuffer(1); + intBuf.clear(); + intBuf.put(0, source.length()); + gl.glShaderSource(shaderId, new String[]{source}, intBuf); + gl.glCompileShader(shaderId); + + // Check compilation status + gl.glGetShader(shaderId, GL.GL_COMPILE_STATUS, intBuf); + if (intBuf.get(0) != GL.GL_TRUE) { + gl.glGetShader(shaderId, GL.GL_INFO_LOG_LENGTH, intBuf); + String infoLog = gl.glGetShaderInfoLog(shaderId, intBuf.get(0)); + gl.glDeleteShader(shaderId); + throw new RendererException("Compute shader compilation failed: " + infoLog); + } + + // Create program and link + programId = gl.glCreateProgram(); + if (programId <= 0) { + gl.glDeleteShader(shaderId); + throw new RendererException("Failed to create shader program"); + } + + gl.glAttachShader(programId, shaderId); + gl.glLinkProgram(programId); + + // Check link status + gl.glGetProgram(programId, GL.GL_LINK_STATUS, intBuf); + if (intBuf.get(0) != GL.GL_TRUE) { + gl.glGetProgram(programId, GL.GL_INFO_LOG_LENGTH, intBuf); + String infoLog = gl.glGetProgramInfoLog(programId, intBuf.get(0)); + gl.glDeleteShader(shaderId); + gl.glDeleteProgram(programId); + throw new RendererException("Compute shader program linking failed: " + infoLog); + } + + // Shader object can be deleted after linking + gl.glDeleteShader(shaderId); + } + + /** + * Activates this compute shader for use. + * Must be called before setting uniforms or dispatching. + */ + public void makeActive() { + gl.glUseProgram(programId); + } + + /** + * Dispatches the compute shader with the specified number of work groups. + */ + public void dispatch(int numGroupsX, int numGroupsY, int numGroupsZ) { + gl.glDispatchCompute(numGroupsX, numGroupsY, numGroupsZ); + } + + public void bindTexture(int bindingPoint, Texture texture) { + gl.glActiveTexture(GL.GL_TEXTURE0 + bindingPoint); + int textureId = texture.getImage().getId(); + int target = convertTextureType(texture); + gl.glBindTexture(target, textureId); + } + + public void setUniform(int location, int value) { + gl.glUniform1i(location, value); + } + + public void setUniform(int location, float value) { + gl.glUniform1f(location, value); + } + + public void setUniform(int location, Vector2f value) { + gl.glUniform2f(location, value.x, value.y); + } + + public void setUniform(int location, Vector3f value) { + gl.glUniform3f(location, value.x, value.y, value.z); + } + + public void setUniform(int location, Vector4f value) { + gl.glUniform4f(location, value.x, value.y, value.z, value.w); + } + + public void setUniform(int location, Matrix4f value) { + FloatBuffer floatBuf16 = BufferUtils.createFloatBuffer(16); + value.fillFloatBuffer(floatBuf16, true); + floatBuf16.clear(); + gl.glUniformMatrix4(location, false, floatBuf16); + } + + public int getUniformLocation(String name) { + return gl.glGetUniformLocation(programId, name); + } + + public void bindShaderStorageBuffer(int location, ShaderStorageBufferObject ssbo) { + gl.glBindBufferBase(GL4.GL_SHADER_STORAGE_BUFFER, location, ssbo.getBufferId()); + } + + /** + * Deletes this compute shader and releases GPU resources. + * The shader should not be used after calling this method. + */ + public void delete() { + gl.glDeleteProgram(programId); + } + + private int convertTextureType(Texture texture) { + switch (texture.getType()) { + case TwoDimensional: + return GL.GL_TEXTURE_2D; + case ThreeDimensional: + return GL2.GL_TEXTURE_3D; + case CubeMap: + return GL.GL_TEXTURE_CUBE_MAP; + case TwoDimensionalArray: + return GLExt.GL_TEXTURE_2D_ARRAY_EXT; + default: + throw new UnsupportedOperationException("Unsupported texture type: " + texture.getType()); + } + } +} \ No newline at end of file diff --git a/jme3-core/src/main/java/com/jme3/renderer/opengl/GL.java b/jme3-core/src/main/java/com/jme3/renderer/opengl/GL.java index 797f8a27ba..7712c52a29 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/opengl/GL.java +++ b/jme3-core/src/main/java/com/jme3/renderer/opengl/GL.java @@ -424,6 +424,11 @@ public interface GL { */ public void glBufferData(int target, ByteBuffer data, int usage); + /** + * See {@link #glBufferData(int, ByteBuffer, int)} + */ + public void glBufferData(int target, IntBuffer data, int usage); + /** *

Reference Page

*

@@ -824,6 +829,11 @@ public void glCompressedTexSubImage2D(int target, int level, int xoffset, int yo */ public void glGetBufferSubData(int target, long offset, ByteBuffer data); + /** + * See {@link #glGetBufferSubData(int, long, ByteBuffer)} + */ + public void glGetBufferSubData(int target, long offset, IntBuffer data); + /** *

Reference Page

* diff --git a/jme3-core/src/main/java/com/jme3/renderer/opengl/GL4.java b/jme3-core/src/main/java/com/jme3/renderer/opengl/GL4.java index 17c0bc4f1c..a911f4ef45 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/opengl/GL4.java +++ b/jme3-core/src/main/java/com/jme3/renderer/opengl/GL4.java @@ -42,6 +42,30 @@ public interface GL4 extends GL3 { public static final int GL_TESS_EVALUATION_SHADER = 0x8E87; public static final int GL_PATCHES = 0xE; + /** + * Accepted by the {@code shaderType} parameter of CreateShader. + */ + public static final int GL_COMPUTE_SHADER = 0x91B9; + + /** + * Accepted by the {@code barriers} parameter of MemoryBarrier. + */ + public static final int GL_SHADER_STORAGE_BARRIER_BIT = 0x00002000; + public static final int GL_TEXTURE_FETCH_BARRIER_BIT = 0x00000008; + + /** + * Accepted by the {@code condition} parameter of FenceSync. + */ + public static final int GL_SYNC_GPU_COMMANDS_COMPLETE = 0x9117; + + /** + * Returned by ClientWaitSync. + */ + public static final int GL_ALREADY_SIGNALED = 0x911A; + public static final int GL_TIMEOUT_EXPIRED = 0x911B; + public static final int GL_CONDITION_SATISFIED = 0x911C; + public static final int GL_WAIT_FAILED = 0x911D; + /** * Accepted by the {@code target} parameter of BindBufferBase and BindBufferRange. */ @@ -104,7 +128,7 @@ public interface GL4 extends GL3 { /** * Binds a single level of a texture to an image unit for the purpose of reading * and writing it from shaders. - * + * * @param unit image unit to bind to * @param texture texture to bind to the image unit * @param level level of the texture to bind @@ -114,5 +138,61 @@ public interface GL4 extends GL3 { * @param format format to use when performing formatted stores */ public void glBindImageTexture(int unit, int texture, int level, boolean layered, int layer, int access, int format); - + + /** + *

Reference Page

+ *

+ * Launches one or more compute work groups. + * + * @param numGroupsX the number of work groups to be launched in the X dimension + * @param numGroupsY the number of work groups to be launched in the Y dimension + * @param numGroupsZ the number of work groups to be launched in the Z dimension + */ + public void glDispatchCompute(int numGroupsX, int numGroupsY, int numGroupsZ); + + /** + *

Reference Page

+ *

+ * Defines a barrier ordering memory transactions. + * + * @param barriers the barriers to insert. One or more of: + * {@link #GL_SHADER_STORAGE_BARRIER_BIT} + * {@link #GL_TEXTURE_FETCH_BARRIER_BIT} + */ + public void glMemoryBarrier(int barriers); + + /** + *

Reference Page

+ *

+ * Creates a new sync object and inserts it into the GL command stream. + * + * @param condition the condition that must be met to set the sync object's state to signaled. + * Must be {@link #GL_SYNC_GPU_COMMANDS_COMPLETE}. + * @param flags must be 0 + * @return the sync object handle + */ + public long glFenceSync(int condition, int flags); + + /** + *

Reference Page

+ *

+ * Causes the client to block and wait for a sync object to become signaled. + * + * @param sync the sync object to wait on + * @param flags flags controlling command flushing behavior. May be 0 or GL_SYNC_FLUSH_COMMANDS_BIT. + * @param timeout the timeout in nanoseconds for which to wait + * @return one of {@link #GL_ALREADY_SIGNALED}, {@link #GL_TIMEOUT_EXPIRED}, + * {@link #GL_CONDITION_SATISFIED}, or {@link #GL_WAIT_FAILED} + */ + public int glClientWaitSync(long sync, int flags, long timeout); + + /** + *

Reference Page

+ *

+ * Deletes a sync object. + * + * @param sync the sync object to delete + */ + public void glDeleteSync(long sync); + } diff --git a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java index 09a560d4a6..62ccf68384 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java +++ b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java @@ -138,6 +138,7 @@ public void setGenerateMipmapsForFrameBuffer(boolean v) { generateMipmapsForFramebuffers = v; } + public void setDebugEnabled(boolean v) { debug = v; } @@ -3605,4 +3606,9 @@ public boolean isMainFrameBufferSrgb() { return gl.glIsEnabled(GLExt.GL_FRAMEBUFFER_SRGB_EXT); } } + + //TODO: How should the GL4 specific functionalities here be exposed? Via the renderer? + public GL4 getGl4(){ + return gl4; + } } diff --git a/jme3-core/src/main/java/com/jme3/renderer/opengl/ShaderStorageBufferObject.java b/jme3-core/src/main/java/com/jme3/renderer/opengl/ShaderStorageBufferObject.java new file mode 100644 index 0000000000..ca9d955df5 --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/renderer/opengl/ShaderStorageBufferObject.java @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2009-2026 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.renderer.opengl; + +import com.jme3.util.BufferUtils; + +import java.nio.IntBuffer; + +/** + *

Reference Page

+ * A Shader Storage Buffer Object (SSBO) for GPU read/write data storage. + *

+ * SSBOs are buffers that can be read from and written to by shaders. + * SSBOs require OpenGL 4.3 or higher. + */ +public class ShaderStorageBufferObject { + + private final GL4 gl; + private final int bufferId; + + /** + * Creates a new SSBO. + * + * @param gl the GL4 interface (required for glBindBufferBase) + */ + public ShaderStorageBufferObject(GL4 gl) { + this.gl = gl; + IntBuffer buf = BufferUtils.createIntBuffer(1); + gl.glGenBuffers(buf); + this.bufferId = buf.get(0); + } + + /** + * Initializes the buffer with integer data. + * @param data the initial data to upload + */ + public void initialize(int[] data) { + IntBuffer buffer = BufferUtils.createIntBuffer(data.length); + buffer.put(data); + buffer.flip(); + initialize(buffer); + } + + /** + * Initializes the buffer with an IntBuffer. + * + * @param data the initial data to upload + */ + public void initialize(IntBuffer data) { + gl.glBindBuffer(GL4.GL_SHADER_STORAGE_BUFFER, bufferId); + gl.glBufferData(GL4.GL_SHADER_STORAGE_BUFFER, data, GL.GL_DYNAMIC_COPY); + } + + /** + * Reads integer data from the buffer. + * + * @param count the number of integers to read + * @return an array containing the buffer data + */ + public int[] read(int count) { + int[] result = new int[count]; + read(result); + return result; + } + + /** + * Reads integer data from the buffer into an existing array. + * + * @param destination the array to read into + */ + public void read(int[] destination) { + IntBuffer buffer = BufferUtils.createIntBuffer(destination.length); + read(buffer); + buffer.get(destination); + } + + /** + * Reads integer data from the buffer into an IntBuffer. + * + * @param destination the buffer to read into + */ + public void read(IntBuffer destination) { + gl.glBindBuffer(GL4.GL_SHADER_STORAGE_BUFFER, bufferId); + gl.glGetBufferSubData(GL4.GL_SHADER_STORAGE_BUFFER, 0, destination); + gl.glBindBuffer(GL4.GL_SHADER_STORAGE_BUFFER, 0); + } + + /** + * Deletes this buffer and releases GPU resources. + * The buffer should not be used after calling this method. + */ + public void delete() { + IntBuffer buf = BufferUtils.createIntBuffer(1); + buf.put(bufferId); + buf.flip(); + gl.glDeleteBuffers(buf); + } + + public int getBufferId() { + return bufferId; + } +} \ No newline at end of file diff --git a/jme3-core/src/main/java/com/jme3/shadow/AbstractShadowFilter.java b/jme3-core/src/main/java/com/jme3/shadow/AbstractShadowFilter.java index 7c76b290a6..e1df51ac1f 100644 --- a/jme3-core/src/main/java/com/jme3/shadow/AbstractShadowFilter.java +++ b/jme3-core/src/main/java/com/jme3/shadow/AbstractShadowFilter.java @@ -41,7 +41,6 @@ import com.jme3.renderer.ViewPort; import com.jme3.renderer.queue.RenderQueue; import com.jme3.texture.FrameBuffer; -import com.jme3.util.TempVars; import com.jme3.util.clone.Cloner; import com.jme3.util.clone.JmeCloneable; @@ -319,6 +318,14 @@ public int getShadowMapSize() { return shadowRenderer.getShadowMapSize(); } + /** + * Displays the shadow frustums for debugging purposes. + * Creates geometry showing the shadow map camera frustums in the scene. + */ + public void displayFrustum() { + shadowRenderer.displayFrustum(); + } + @Override @SuppressWarnings("unchecked") public AbstractShadowFilter jmeClone() { diff --git a/jme3-core/src/main/java/com/jme3/shadow/SdsmDirectionalLightShadowFilter.java b/jme3-core/src/main/java/com/jme3/shadow/SdsmDirectionalLightShadowFilter.java new file mode 100644 index 0000000000..fa7105e6da --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/shadow/SdsmDirectionalLightShadowFilter.java @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2009-2026 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.shadow; + +import com.jme3.asset.AssetManager; +import com.jme3.export.InputCapsule; +import com.jme3.export.JmeExporter; +import com.jme3.export.JmeImporter; +import com.jme3.export.OutputCapsule; +import com.jme3.light.DirectionalLight; +import com.jme3.material.Material; +import com.jme3.renderer.RenderManager; +import com.jme3.renderer.ViewPort; +import com.jme3.renderer.queue.RenderQueue; +import com.jme3.texture.FrameBuffer; +import com.jme3.texture.Texture; + +import java.io.IOException; + +/** + * SDSM (Sample Distribution Shadow Mapping) filter for directional lights. + *

+ * This filter uses GPU compute shaders to analyze the depth buffer and compute + * optimal cascade split positions for rendering shadow maps. + *

+ * Key benefits over {@link DirectionalLightShadowFilter}: + *

+ *

+ * Requires OpenGL 4.3+ for compute shader support. Only works for filter-based shadow mapping. + */ +public class SdsmDirectionalLightShadowFilter extends AbstractShadowFilter { + /** + * For serialization only. Do not use. + * + * @see #SdsmDirectionalLightShadowFilter(AssetManager, int, int) + */ + public SdsmDirectionalLightShadowFilter() { + super(); + } + + /** + * Creates an SDSM directional light shadow filter. + * @param assetManager the application's asset manager + * @param shadowMapSize the size of the rendered shadow maps (512, 1024, 2048, etc.) + * @param splitCount the number of shadow map splits (1-4) + * @throws IllegalArgumentException if splitCount is not between 1 and 4 + */ + public SdsmDirectionalLightShadowFilter(AssetManager assetManager, int shadowMapSize, int splitCount) { + super(assetManager, shadowMapSize, new SdsmDirectionalLightShadowRenderer(assetManager, shadowMapSize, splitCount)); + } + + @Override + protected void initFilter(AssetManager manager, RenderManager renderManager, ViewPort vp, int w, int h) { + shadowRenderer.needsfallBackMaterial = true; + material = new Material(manager, "Common/MatDefs/Shadow/Sdsm/SdsmPostShadow.j3md"); + shadowRenderer.setPostShadowMaterial(material); + shadowRenderer.initialize(renderManager, vp); + this.viewPort = vp; + } + + /** + * Returns the light used to cast shadows. + * + * @return the DirectionalLight + */ + public DirectionalLight getLight() { + return shadowRenderer.getLight(); + } + + /** + * Sets the light to use for casting shadows. + * + * @param light a DirectionalLight + */ + public void setLight(DirectionalLight light) { + shadowRenderer.setLight(light); + } + + /** + * Gets the fit expansion factor. + * + * @return the expansion factor + * @see SdsmDirectionalLightShadowRenderer#getFitExpansionFactor() + */ + public float getFitExpansionFactor() { + return shadowRenderer.getFitExpansionFactor(); + } + + /** + * Sets the expansion factor for fitted shadow frustums. + * + * @param factor the expansion factor (default 1.0) + * @see SdsmDirectionalLightShadowRenderer#setFitExpansionFactor(float) + */ + public void setFitExpansionFactor(float factor) { + shadowRenderer.setFitExpansionFactor(factor); + } + + /** + * Gets the frame delay tolerance. + * + * @return the tolerance value + * @see SdsmDirectionalLightShadowRenderer#getFitFrameDelayTolerance() + */ + public float getFitFrameDelayTolerance() { + return shadowRenderer.getFitFrameDelayTolerance(); + } + + /** + * Sets the frame delay tolerance. + * + * @param tolerance the tolerance (default 0.05) + * @see SdsmDirectionalLightShadowRenderer#setFitFrameDelayTolerance(float) + */ + public void setFitFrameDelayTolerance(float tolerance) { + shadowRenderer.setFitFrameDelayTolerance(tolerance); + } + + @Override + public void setDepthTexture(Texture depthTexture) { + super.setDepthTexture(depthTexture); + shadowRenderer.setDepthTexture(depthTexture); + } + + @Override + protected void postQueue(RenderQueue queue) { + // We need the depth texture from the previous pass, so we defer + // shadow processing to postFrame + } + + @Override + protected void postFrame(RenderManager renderManager, ViewPort viewPort, + FrameBuffer prevFilterBuffer, FrameBuffer sceneBuffer) { + super.postQueue(null); + super.postFrame(renderManager, viewPort, prevFilterBuffer, sceneBuffer); + } + + @Override + protected void cleanUpFilter(com.jme3.renderer.Renderer r) { + super.cleanUpFilter(r); + if (shadowRenderer != null) { + shadowRenderer.cleanup(); + } + } + + public void displayAllFrustums(){ + shadowRenderer.displayAllDebugFrustums(); + } + + @Override + public void write(JmeExporter ex) throws IOException { + super.write(ex); + OutputCapsule oc = ex.getCapsule(this); + oc.write(shadowRenderer, "shadowRenderer", null); + } + + @Override + public void read(JmeImporter im) throws IOException { + super.read(im); + InputCapsule ic = im.getCapsule(this); + shadowRenderer = (SdsmDirectionalLightShadowRenderer) ic.readSavable("shadowRenderer", null); + } + +} \ No newline at end of file diff --git a/jme3-core/src/main/java/com/jme3/shadow/SdsmDirectionalLightShadowRenderer.java b/jme3-core/src/main/java/com/jme3/shadow/SdsmDirectionalLightShadowRenderer.java new file mode 100644 index 0000000000..189197ed8c --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/shadow/SdsmDirectionalLightShadowRenderer.java @@ -0,0 +1,472 @@ +/* + * Copyright (c) 2009-2026 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.shadow; + +import com.jme3.asset.AssetManager; +import com.jme3.export.InputCapsule; +import com.jme3.export.JmeExporter; +import com.jme3.export.JmeImporter; +import com.jme3.export.OutputCapsule; +import com.jme3.light.DirectionalLight; +import com.jme3.material.Material; +import com.jme3.math.Matrix4f; +import com.jme3.math.Vector2f; +import com.jme3.math.Vector3f; +import com.jme3.renderer.Camera; +import com.jme3.renderer.Renderer; +import com.jme3.renderer.opengl.GL4; +import com.jme3.renderer.opengl.GLRenderer; +import com.jme3.renderer.queue.GeometryList; +import com.jme3.renderer.queue.RenderQueue.ShadowMode; +import com.jme3.scene.Geometry; +import com.jme3.scene.Node; +import com.jme3.scene.Spatial; +import com.jme3.shader.VarType; +import com.jme3.texture.Texture; +import com.jme3.util.clone.Cloner; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * See {@link SdsmDirectionalLightShadowFilter} + */ +public class SdsmDirectionalLightShadowRenderer extends AbstractShadowRenderer { + + private DirectionalLight light; + private final Matrix4f lightViewMatrix = new Matrix4f(); + private Camera[] shadowCameras; + private boolean[] shadowCameraEnabled; + + private SdsmFitter sdsmFitter; + private SdsmFitter.SplitFitResult lastFit; + private Texture depthTexture; + + private boolean glInitialized = false; + + /** + * Expansion factor for fitted shadow frustums. + * Larger values reduce shadow pop-in but may waste shadow map resolution. + */ + private float fitExpansionFactor = 1.0f; + + /** + * Tolerance for reusing old fit results when camera hasn't moved much. + * Reduce to eliminate screen-tearing artifacts when rapidly moving or rotating camera, at the cost of lower framerate caused by waiting for SDSM to complete. + */ + private float fitFrameDelayTolerance = 0.05f; + + /** + * Used for serialization. Do not use. + * + * @see #SdsmDirectionalLightShadowRenderer(AssetManager, int, int) + */ + protected SdsmDirectionalLightShadowRenderer() { + super(); + } + + /** + * Creates an SDSM directional light shadow renderer. + * You likely should not use this directly, as it requires an SdsmDirectionalLightShadowFilter. + */ + public SdsmDirectionalLightShadowRenderer(AssetManager assetManager, int shadowMapSize, int nbShadowMaps) { + super(assetManager, shadowMapSize, nbShadowMaps); + init(nbShadowMaps, shadowMapSize); + } + + private void init(int splitCount, int shadowMapSize) { + if (splitCount < 1 || splitCount > 4) { + throw new IllegalArgumentException("Number of splits must be between 1 and 4. Given value: " + splitCount); + } + this.nbShadowMaps = splitCount; + + shadowCameras = new Camera[this.nbShadowMaps]; + shadowCameraEnabled = new boolean[this.nbShadowMaps]; + + for (int i = 0; i < this.nbShadowMaps; i++) { + shadowCameras[i] = new Camera(shadowMapSize, shadowMapSize); + shadowCameras[i].setParallelProjection(true); + shadowCameraEnabled[i] = false; + } + + needsfallBackMaterial = true; + } + + /** + * Initializes the GL interfaces for compute shader operations. + * Called on first frame when RenderManager is available. + */ + private void initGL() { + if (glInitialized) { + return; + } + + Renderer renderer = renderManager.getRenderer(); + if (!(renderer instanceof GLRenderer)) { + throw new UnsupportedOperationException("SdsmDirectionalLightShadowRenderer requires GLRenderer"); + } + + GLRenderer glRenderer = (GLRenderer) renderer; + + + GL4 gl4 = glRenderer.getGl4(); + + if (gl4 == null) { + throw new UnsupportedOperationException("SDSM shadows require OpenGL 4.3 or higher"); + } + + sdsmFitter = new SdsmFitter(gl4, assetManager); + glInitialized = true; + + } + + /** + * Returns the light used to cast shadows. + */ + public DirectionalLight getLight() { + return light; + } + + /** + * Sets the light to use for casting shadows. + */ + public void setLight(DirectionalLight light) { + this.light = light; + if (light != null) { + generateLightViewMatrix(); + } + } + + public void setDepthTexture(Texture depthTexture) { + this.depthTexture = depthTexture; + } + + /** + * Gets the fit expansion factor. + * + * @return the expansion factor + */ + public float getFitExpansionFactor() { + return fitExpansionFactor; + } + + /** + * Sets the expansion factor for fitted shadow frustums. + *

+ * A value of 1.0 uses the exact computed bounds. + * Larger values (e.g., 1.05) add some margin to reduce artifacts + * from frame delay or precision issues. + * + * @param fitExpansionFactor the expansion factor (default 1.0) + */ + public void setFitExpansionFactor(float fitExpansionFactor) { + this.fitExpansionFactor = fitExpansionFactor; + } + + /** + * Gets the frame delay tolerance for reusing old fit results. + * + * @return the tolerance value + */ + public float getFitFrameDelayTolerance() { + return fitFrameDelayTolerance; + } + + /** + * Sets the tolerance for reusing old fit results. + *

+ * When the camera hasn't moved significantly (within this tolerance), + * old fit results can be reused to avoid GPU stalls. + * + * @param fitFrameDelayTolerance the tolerance (default 0.05) + */ + public void setFitFrameDelayTolerance(float fitFrameDelayTolerance) { + this.fitFrameDelayTolerance = fitFrameDelayTolerance; + } + + private void generateLightViewMatrix() { + Vector3f lightDir = light.getDirection(); + Vector3f up = Math.abs(lightDir.y) < 0.9f ? Vector3f.UNIT_Y : Vector3f.UNIT_X; + Vector3f right = lightDir.cross(up).normalizeLocal(); + Vector3f actualUp = right.cross(lightDir).normalizeLocal(); + + lightViewMatrix.set( + right.x, right.y, right.z, 0f, + actualUp.x, actualUp.y, actualUp.z, 0f, + lightDir.x, lightDir.y, lightDir.z, 0f, + 0f, 0f, 0f, 1f + ); + } + + @Override + protected void initFrustumCam() {} + + @Override + protected void updateShadowCams(Camera viewCam) { + if (!glInitialized) { + initGL(); + } + + if (!tryFitShadowCams(viewCam)) { + skipPostPass = true; + } + } + + private boolean tryFitShadowCams(Camera viewCam) { + if (depthTexture == null || light == null) { + return false; + } + + Vector3f lightDir = light.getDirection(); + if(lightDir.x != lightViewMatrix.m30 || lightDir.y != lightViewMatrix.m31 || lightDir.z != lightViewMatrix.m32) { + generateLightViewMatrix(); + } + + // Compute camera-to-light transformation matrix + Matrix4f invViewProj = viewCam.getViewProjectionMatrix().invert(); + Matrix4f cameraToLight = lightViewMatrix.mult(invViewProj, invViewProj); + + // Submit fit request to GPU + sdsmFitter.fit( + depthTexture, + nbShadowMaps, + cameraToLight, + viewCam.getFrustumNear(), + viewCam.getFrustumFar() + ); + + // Try to get result without blocking + SdsmFitter.SplitFitResult fitCallResult = sdsmFitter.getResult(false); + + // If no result yet, try to reuse old fit or wait + if (fitCallResult == null) { + fitCallResult = lastFit; + while (fitCallResult == null || !isOldFitAcceptable(fitCallResult, cameraToLight)) { + fitCallResult = sdsmFitter.getResult(true); + } + } + + lastFit = fitCallResult; + SdsmFitter.SplitFit fitResult = fitCallResult.result; + + if (fitResult != null) { + for (int splitIndex = 0; splitIndex < nbShadowMaps; splitIndex++) { + shadowCameraEnabled[splitIndex] = false; + + SdsmFitter.SplitBounds bounds = fitResult.splits.get(splitIndex); + if (bounds == null) { + continue; + } + + Camera cam = shadowCameras[splitIndex]; + + float centerX = (bounds.minX + bounds.maxX) / 2f; + float centerY = (bounds.minY + bounds.maxY) / 2f; + + // Position in light space + Vector3f lightSpacePos = new Vector3f(centerX, centerY, bounds.minZ); + + // Transform back to world space + Matrix4f invLightView = lightViewMatrix.invert(); + Vector3f worldPos = invLightView.mult(lightSpacePos); + + cam.setLocation(worldPos); + // Use the same up vector that was used to compute the light view matrix + // Row 1 of lightViewMatrix contains the actualUp vector (Y axis in light space) + Vector3f actualUp = new Vector3f(lightViewMatrix.m10, lightViewMatrix.m11, lightViewMatrix.m12); + cam.lookAtDirection(light.getDirection(), actualUp); + + float width = (bounds.maxX - bounds.minX) * fitExpansionFactor; + float height = (bounds.maxY - bounds.minY) * fitExpansionFactor; + float far = (bounds.maxZ - bounds.minZ) * fitExpansionFactor; + + if (width <= 0f || height <= 0f || far <= 0f) { + continue; //Skip updating this particular shadowcam, it likely doesn't have any samples or is degenerate. + } + + cam.setFrustum( + -100f, //This will usually help out with clipping problems, where the shadow camera is positioned such that it would clip out a vertex that might cast a shadow. + far, + -width / 2f, + width / 2f, + height / 2f, + -height / 2f + ); + + shadowCameraEnabled[splitIndex] = true; + if(Float.isNaN(cam.getViewProjectionMatrix().m00)){ + throw new IllegalStateException("Invalid shadow projection detected"); + } + } + return true; + } + + return false; + } + + private boolean isOldFitAcceptable(SdsmFitter.SplitFitResult fit, Matrix4f newCameraToLight) { + return fit.parameters.cameraToLight.isSimilar(newCameraToLight, fitFrameDelayTolerance); + } + + @Override + protected GeometryList getOccludersToRender(int shadowMapIndex, GeometryList shadowMapOccluders) { + if (shadowCameraEnabled[shadowMapIndex]) { + Camera camera = shadowCameras[shadowMapIndex]; + for (Spatial scene : viewPort.getScenes()) { + ShadowUtil.getGeometriesInCamFrustum(scene, camera, ShadowMode.Cast, shadowMapOccluders); + } + } + return shadowMapOccluders; + } + + @Override + protected void getReceivers(GeometryList lightReceivers) { throw new RuntimeException("Only filter mode is implemented for SDSM"); } + + @Override + protected Camera getShadowCam(int shadowMapIndex) { + return shadowCameras[shadowMapIndex]; + } + + @Override + protected void doDisplayFrustumDebug(int shadowMapIndex) { + if (shadowCameraEnabled[shadowMapIndex]) { + createDebugFrustum(shadowCameras[shadowMapIndex], shadowMapIndex); + } + } + + private Spatial cameraFrustumDebug = null; + private List shadowMapFrustumDebug = null; + public void displayAllDebugFrustums() { + if (cameraFrustumDebug != null) { + cameraFrustumDebug.removeFromParent(); + } + if (shadowMapFrustumDebug != null) { + for (Spatial s : shadowMapFrustumDebug) { + s.removeFromParent(); + } + } + + cameraFrustumDebug = createDebugFrustum(viewPort.getCamera(), 4); + shadowMapFrustumDebug = new ArrayList<>(); + for (int i = 0; i < nbShadowMaps; i++) { + if (shadowCameraEnabled[i]) { + shadowMapFrustumDebug.add(createDebugFrustum(shadowCameras[i], i)); + } + } + } + + private Geometry createDebugFrustum(Camera camera, int shadowMapColor) { + Vector3f[] points = new Vector3f[8]; + for (int i = 0; i < 8; i++) { + points[i] = new Vector3f(); + } + ShadowUtil.updateFrustumPoints2(camera, points); + Geometry geom = createFrustum(points, shadowMapColor); + geom.getMaterial().getAdditionalRenderState().setLineWidth(5f); + geom.getMaterial().getAdditionalRenderState().setDepthWrite(false); + ((Node) viewPort.getScenes().get(0)).attachChild(geom); + return geom; + } + + @Override + protected void setMaterialParameters(Material material) { + Vector2f[] splits = getSplits(); + material.setParam("Splits", VarType.Vector2Array, splits); + material.setVector3("LightDir", light == null ? new Vector3f() : light.getDirection()); + } + + private Vector2f[] getSplits() { + Vector2f[] result = new Vector2f[3]; + for (int i = 0; i < 3; i++) { + result[i] = new Vector2f(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY); + } + + if (lastFit != null && lastFit.result != null) { + for (int split = 0; split < nbShadowMaps - 1; split++) { + if (split < lastFit.result.cascadeStarts.size()) { + SdsmFitter.SplitInfo splitInfo = lastFit.result.cascadeStarts.get(split); + result[split].set(splitInfo.start, splitInfo.end); + } + } + } + return result; + } + + @Override + protected void clearMaterialParameters(Material material) { + material.clearParam("Splits"); + material.clearParam("LightDir"); + } + + @Override + protected boolean checkCulling(Camera viewCam) { + // Directional lights are always visible + return true; + } + + /** + * Cleans up GPU resources used by the SDSM fitter. + */ + public void cleanup() { + if (sdsmFitter != null) { + sdsmFitter.cleanup(); + } + } + + @Override + public void cloneFields(final Cloner cloner, final Object original) { + light = cloner.clone(light); + init(nbShadowMaps, (int) shadowMapSize); + super.cloneFields(cloner, original); + } + + @Override + public void read(JmeImporter im) throws IOException { + super.read(im); + InputCapsule ic = im.getCapsule(this); + light = (DirectionalLight) ic.readSavable("light", null); + fitExpansionFactor = ic.readFloat("fitExpansionFactor", 1.0f); + fitFrameDelayTolerance = ic.readFloat("fitFrameDelayTolerance", 0.05f); + init(nbShadowMaps, (int) shadowMapSize); + } + + @Override + public void write(JmeExporter ex) throws IOException { + super.write(ex); + OutputCapsule oc = ex.getCapsule(this); + oc.write(light, "light", null); + oc.write(fitExpansionFactor, "fitExpansionFactor", 1.0f); + oc.write(fitFrameDelayTolerance, "fitFrameDelayTolerance", 0.05f); + } + +} \ No newline at end of file diff --git a/jme3-core/src/main/java/com/jme3/shadow/SdsmFitter.java b/jme3-core/src/main/java/com/jme3/shadow/SdsmFitter.java new file mode 100644 index 0000000000..e3b4f620fc --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/shadow/SdsmFitter.java @@ -0,0 +1,481 @@ +/* + * Copyright (c) 2009-2026 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.shadow; + +import com.jme3.asset.AssetInfo; +import com.jme3.asset.AssetKey; +import com.jme3.asset.AssetManager; +import com.jme3.math.Matrix4f; +import com.jme3.math.Vector2f; +import com.jme3.renderer.RendererException; +import com.jme3.renderer.opengl.ComputeShader; +import com.jme3.renderer.opengl.GL4; +import com.jme3.renderer.opengl.ShaderStorageBufferObject; +import com.jme3.texture.Texture; + +import java.io.IOException; +import java.io.InputStream; +import java.util.*; + +/** + * Compute shader used in SDSM. + */ +public class SdsmFitter { + + private static final String REDUCE_DEPTH_SHADER = "Common/MatDefs/Shadow/Sdsm/ReduceDepth.comp"; + private static final String FIT_FRUSTUMS_SHADER = "Common/MatDefs/Shadow/Sdsm/FitLightFrustums.comp"; + + private final GL4 gl4; + private int maxFrameLag = 3; + + private final ComputeShader depthReduceShader; + private final ComputeShader fitFrustumsShader; + + private final LinkedList resultHoldersInFlight = new LinkedList<>(); + private final LinkedList resultHoldersReady = new LinkedList<>(); + private SplitFitResult readyToYield; + + // Initial values for fit frustum SSBO + // 4 cascades x (minX, minY, maxX, maxY) + 4 x (minZ, maxZ) + globalMin + globalMax + 3 x (splitStart, blendEnd) + private static final int[] FIT_FRUSTUM_INIT = new int[32]; + static { + for (int i = 0; i < 4; i++) { + FIT_FRUSTUM_INIT[i * 4] = -1; //MinX (-1 == maximum UINT value) + FIT_FRUSTUM_INIT[i * 4 + 1] = -1; //MinY + FIT_FRUSTUM_INIT[i * 4 + 2] = 0; //MaxX + FIT_FRUSTUM_INIT[i * 4 + 3] = 0; //MaxY + } + for (int i = 0; i < 4; i++) { + FIT_FRUSTUM_INIT[16 + i * 2] = -1; //MinZ + FIT_FRUSTUM_INIT[16 + i * 2 + 1] = 0; //MaxZ + } + FIT_FRUSTUM_INIT[24] = -1; //Global min + FIT_FRUSTUM_INIT[25] = 0; //Global max + // Split starts (3 splits max) + for (int i = 0; i < 6; i++) { + FIT_FRUSTUM_INIT[26 + i] = 0; + } + } + + /** + * Parameters used for a fit operation. + */ + public static class FitParameters { + public final Matrix4f cameraToLight; + public final int splitCount; + public final float cameraNear; + public final float cameraFar; + + public FitParameters(Matrix4f cameraToLight, int splitCount, float cameraNear, float cameraFar) { + this.cameraToLight = cameraToLight; + this.splitCount = splitCount; + this.cameraNear = cameraNear; + this.cameraFar = cameraFar; + } + + @Override + public String toString() { + return "FitParameters{" + + "cameraToLight=" + cameraToLight + + ", splitCount=" + splitCount + + ", cameraNear=" + cameraNear + + ", cameraFar=" + cameraFar + + '}'; + } + } + + /** + * Bounds for a single cascade split in light space. + */ + public static class SplitBounds { + public final float minX, minY, maxX, maxY; + public final float minZ, maxZ; + + public SplitBounds(float minX, float minY, float maxX, float maxY, float minZ, float maxZ) { + this.minX = minX; + this.minY = minY; + this.maxX = maxX; + this.maxY = maxY; + this.minZ = minZ; + this.maxZ = maxZ; + } + + public boolean isValid() { + return minX != Float.POSITIVE_INFINITY && minY != Float.POSITIVE_INFINITY && minZ != Float.POSITIVE_INFINITY && maxX != Float.NEGATIVE_INFINITY && maxY != Float.NEGATIVE_INFINITY && maxZ != Float.NEGATIVE_INFINITY; + } + + @Override + public String toString() { + return "SplitBounds{" + + "minX=" + minX + + ", minY=" + minY + + ", maxX=" + maxX + + ", maxY=" + maxY + + ", minZ=" + minZ + + ", maxZ=" + maxZ + + '}'; + } + } + + /** + * Information about where a cascade split starts/ends for blending. + */ + public static class SplitInfo { + public final float start; + public final float end; + + public SplitInfo(float start, float end) { + this.start = start; + this.end = end; + } + + @Override + public String toString() { + return "SplitInfo{" + + "start=" + start + + ", end=" + end + + '}'; + } + } + + /** + * Complete fit result for all cascades. + */ + public static class SplitFit { + public final List splits; + public final float minDepth; + public final float maxDepth; + public final List cascadeStarts; + + public SplitFit(List splits, float minDepth, float maxDepth, List cascadeStarts) { + this.splits = splits; + this.minDepth = minDepth; + this.maxDepth = maxDepth; + this.cascadeStarts = cascadeStarts; + } + + @Override + public String toString() { + return "SplitFit{" + + "splits=" + splits + + ", minDepth=" + minDepth + + ", maxDepth=" + maxDepth + + ", cascadeStarts=" + cascadeStarts + + '}'; + } + } + + /** + * Result of a fit operation, including parameters and computed fit. + */ + public static class SplitFitResult { + public final FitParameters parameters; + public final SplitFit result; + + public SplitFitResult(FitParameters parameters, SplitFit result) { + this.parameters = parameters; + this.result = result; + } + + @Override + public String toString() { + return "SplitFitResult{" + + "parameters=" + parameters + + ", result=" + result + + '}'; + } + } + + /** + * Internal holder for in-flight fit operations. + */ + private class SdsmResultHolder { + ShaderStorageBufferObject minMaxDepthSsbo; + ShaderStorageBufferObject fitFrustumSsbo; + FitParameters parameters; + long fence = -1; + + SdsmResultHolder() { + this.minMaxDepthSsbo = new ShaderStorageBufferObject(gl4); + this.fitFrustumSsbo = new ShaderStorageBufferObject(gl4); + } + + boolean isReady(boolean wait) { + if (fence == -1L) { + return true; + } + int status = gl4.glClientWaitSync(fence, 0, wait ? -1 : 0); + return status == GL4.GL_ALREADY_SIGNALED || status == GL4.GL_CONDITION_SATISFIED; + } + + SplitFitResult extract() { + if (fence >= 0) { + gl4.glDeleteSync(fence); + fence = -1; + } + SplitFit fit = extractFit(); + return new SplitFitResult(parameters, fit); + } + + private SplitFit extractFit() { + int[] uintFit = fitFrustumSsbo.read(32); + float[] fitResult = new float[32]; + for(int i=0;i cascadeData = new ArrayList<>(); + for (int idx = 0; idx < parameters.splitCount; idx++) { + int start = idx * 4; + int zStart = 16 + idx * 2; + SplitBounds bounds = new SplitBounds( + fitResult[start], + fitResult[start + 1], + fitResult[start + 2], + fitResult[start + 3], + fitResult[zStart], + fitResult[zStart + 1] + ); + cascadeData.add(bounds.isValid() ? bounds : null); + } + + float minDepthView = getProjectionToViewZ(parameters.cameraNear, parameters.cameraFar, minDepth); + float maxDepthView = getProjectionToViewZ(parameters.cameraNear, parameters.cameraFar, maxDepth); + + List cascadeStarts = new ArrayList<>(); + for (int i = 0; i < parameters.splitCount - 1; i++) { + float splitStart = fitResult[26 + i * 2]; + float splitEnd = fitResult[26 + i * 2 + 1]; + assert !Float.isNaN(splitStart) && !Float.isNaN(splitEnd); + cascadeStarts.add(new SplitInfo(splitStart, splitEnd)); + } + + return new SplitFit(cascadeData, minDepthView, maxDepthView, cascadeStarts); + } + + void cleanup() { + minMaxDepthSsbo.delete(); + fitFrustumSsbo.delete(); + if (fence >= 0) { + gl4.glDeleteSync(fence); + } + } + } + + /** + * Creates a new SDSM fitter. + * + * @param gl the GL4 interface + */ + public SdsmFitter(GL4 gl, AssetManager assetManager) { + this.gl4 = gl; + + // Load compute shaders + String reduceSource = loadShaderSource(assetManager, REDUCE_DEPTH_SHADER); + String fitSource = loadShaderSource(assetManager, FIT_FRUSTUMS_SHADER); + + depthReduceShader = new ComputeShader(gl, reduceSource); + fitFrustumsShader = new ComputeShader(gl, fitSource); + } + + /** + * Initiates an asynchronous fit operation on the given depth texture. + * + * @param depthTexture the depth texture to analyze + * @param splitCount number of cascade splits (1-4) + * @param cameraToLight transformation matrix from camera clip space to light view space + * @param cameraNear camera near plane distance + * @param cameraFar camera far plane distance + */ + public void fit(Texture depthTexture, int splitCount, Matrix4f cameraToLight, + float cameraNear, float cameraFar) { + + SdsmResultHolder holder = getResultHolderForUse(); + holder.parameters = new FitParameters(cameraToLight, splitCount, cameraNear, cameraFar); + + gl4.glMemoryBarrier(GL4.GL_TEXTURE_FETCH_BARRIER_BIT); + + int width = depthTexture.getImage().getWidth(); + int height = depthTexture.getImage().getHeight(); + int xGroups = divRoundUp(width, 32); + int yGroups = divRoundUp(height, 32); + + if (xGroups < 2) { + throw new RendererException("Depth texture too small for SDSM fit"); + } + + // Initialize SSBOs + holder.minMaxDepthSsbo.initialize(new int[]{-1, 0}); // max uint, 0 + + // Pass 1: Reduce depth to find min/max + depthReduceShader.makeActive(); + depthReduceShader.bindTexture(0, depthTexture); + depthReduceShader.bindShaderStorageBuffer(1, holder.minMaxDepthSsbo); + depthReduceShader.dispatch(xGroups, yGroups, 1); + gl4.glMemoryBarrier(GL4.GL_SHADER_STORAGE_BARRIER_BIT); + + // Pass 2: Fit cascade frustums + holder.fitFrustumSsbo.initialize(FIT_FRUSTUM_INIT); + + fitFrustumsShader.makeActive(); + fitFrustumsShader.bindTexture(0, depthTexture); + fitFrustumsShader.bindShaderStorageBuffer(1, holder.minMaxDepthSsbo); + fitFrustumsShader.bindShaderStorageBuffer(2, holder.fitFrustumSsbo); + + fitFrustumsShader.setUniform(3, cameraToLight); + fitFrustumsShader.setUniform(4, splitCount); + fitFrustumsShader.setUniform(5, new Vector2f(cameraNear, cameraFar)); + fitFrustumsShader.setUniform(6, 0.05f); + + fitFrustumsShader.dispatch(xGroups, yGroups, 1); + gl4.glMemoryBarrier(GL4.GL_SHADER_STORAGE_BARRIER_BIT); + + // Create fence for async readback + holder.fence = gl4.glFenceSync(GL4.GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + resultHoldersInFlight.add(holder); + } + + /** + * Gets the next available fit result. + * + * @param wait if true, blocks until a result is available + * @return the fit result, or null if none available (and wait is false) + */ + public SplitFitResult getResult(boolean wait) { + if (readyToYield != null) { + SplitFitResult result = readyToYield; + readyToYield = null; + return result; + } + + SplitFitResult result = null; + Iterator iter = resultHoldersInFlight.iterator(); + while (iter.hasNext()) { + SdsmResultHolder next = iter.next(); + boolean mustHaveResult = result == null && wait; + if (next.isReady(mustHaveResult)) { + iter.remove(); + result = next.extract(); + resultHoldersReady.add(next); + } else { + break; + } + } + if(wait && result == null){ + throw new IllegalStateException(); + } + return result; + } + + /** + * Cleans up GPU resources. + */ + public void cleanup() { + for (SdsmResultHolder holder : resultHoldersInFlight) { + holder.cleanup(); + } + resultHoldersInFlight.clear(); + + for (SdsmResultHolder holder : resultHoldersReady) { + holder.cleanup(); + } + resultHoldersReady.clear(); + + if (depthReduceShader != null) { + depthReduceShader.delete(); + } + if (fitFrustumsShader != null) { + fitFrustumsShader.delete(); + } + } + + private SdsmResultHolder getResultHolderForUse() { + if (!resultHoldersReady.isEmpty()) { + return resultHoldersReady.removeFirst(); + } else if (resultHoldersInFlight.size() <= maxFrameLag) { + return new SdsmResultHolder(); + } else { + SdsmResultHolder next = resultHoldersInFlight.removeFirst(); + next.isReady(true); + readyToYield = next.extract(); + return next; + } + } + + private static String loadShaderSource(AssetManager assetManager, String resourcePath) { + //TODO: Should these shaders get special loaders or something? + AssetInfo info = assetManager.locateAsset(new AssetKey<>(resourcePath)); + try (InputStream is = info.openStream()) { + return new Scanner(is).useDelimiter("\\A").next(); + } catch (IOException e) { + throw new RendererException("Failed to load shader: " + resourcePath); + } + } + + private static float getProjectionToViewZ(float near, float far, float projZPos) { + float a = far / (far - near); + float b = far * near / (near - far); + return b / (projZPos - a); + } + + private static int divRoundUp(int value, int divisor) { + return (value + divisor - 1) / divisor; + } + + /** + * Converts a uint-encoded float back to float. + * This is the inverse of the floatFlip function in the shader. + */ + private static float uintFlip(int u) { + int flipped; + if ((u & 0x80000000) != 0) { + flipped = u ^ 0x80000000; // Was positive, flip sign bit + } else { + flipped = ~u; // Was negative, invert all bits + } + return Float.intBitsToFloat(flipped); + } + + public void setMaxFrameLag(int maxFrameLag) { + this.maxFrameLag = maxFrameLag; + } +} \ No newline at end of file diff --git a/jme3-core/src/main/resources/Common/MatDefs/Shadow/Sdsm/FitLightFrustums.comp b/jme3-core/src/main/resources/Common/MatDefs/Shadow/Sdsm/FitLightFrustums.comp new file mode 100644 index 0000000000..191da705dc --- /dev/null +++ b/jme3-core/src/main/resources/Common/MatDefs/Shadow/Sdsm/FitLightFrustums.comp @@ -0,0 +1,236 @@ +#version 430 + +/** + * Computes tight bounding boxes for each shadow cascade via min/max on lightspace locations of depth samples that fall within each cascade. + */ + +layout(local_size_x = 16, local_size_y = 16) in; + +layout(binding = 0) uniform sampler2D inputDepth; + +layout(std430, binding = 1) readonly buffer MinMaxBuffer { + uint gMin; + uint gMax; +}; + +layout(std430, binding = 2) buffer CascadeBounds { + uvec4 gBounds[4]; // xy = min XY, zw = max XY per cascade + uvec2 gZBounds[4]; // x = min Z, y = max Z per cascade + uint rMin; // Copy of global min for output + uint rMax; // Copy of global max for output + uvec2 rSplitStart[3]; // [split start, blend end] for up to 3 splits +}; + +layout(location = 3) uniform mat4 cameraToLightView; +layout(location = 4) uniform int splitCount; +layout(location = 5) uniform vec2 cameraFrustum; // (near, far) +layout(location = 6) uniform float blendZone; + +// Shared memory for workgroup reduction +// Each workgroup is 16x16 = 256 threads +shared vec4 sharedBounds[4][256]; // minX, minY, maxX, maxY per cascade +shared vec2 sharedZBounds[4][256]; // minZ, maxZ per cascade + +/** + * Computes the start position of cascade i using log/uniform blend. + */ +float computeCascadeSplitStart(int i, float near, float far) { + float idm = float(i) / float(splitCount + 1); + float logSplit = near * pow(far / near, idm); + float uniformSplit = near + (far - near) * idm; + return logSplit * 0.65 + uniformSplit * 0.35; +} + +/** + * Converts projection-space Z to view-space Z (distance from camera). + */ +float getProjectionToViewZ(float projZPos) { + float near = cameraFrustum.x; + float far = cameraFrustum.y; + float a = far / (far - near); + float b = far * near / (near - far); + return b / (projZPos - a); +} + +/** + * Converts view-space Z to projection-space Z. + */ +float getViewToProjectionZ(float viewZPos) { + float near = cameraFrustum.x; + float far = cameraFrustum.y; + float a = far / (far - near); + float b = far * near / (near - far); + return a + b / viewZPos; +} + +/** + * Encodes a float for atomic min/max operations. + */ +uint floatFlip(float f) { + uint u = floatBitsToUint(f); + // If negative (sign bit set): flip ALL bits (turns into small uint) + // If positive (sign bit clear): flip ONLY sign bit (makes it large uint) + return (u & 0x80000000u) != 0u ? ~u : u ^ 0x80000000u; +} + +/** + * Decodes a uint back to float (inverse of floatFlip). + */ +float uintFlip(uint u) { + return uintBitsToFloat((u & 0x80000000u) != 0u ? u ^ 0x80000000u : ~u); +} + +void main() { + // Compute cascade split depths from the global min/max + float minDepth = uintFlip(gMin); + float maxDepth = uintFlip(gMax); + float minDepthFrustum = getProjectionToViewZ(minDepth); + float maxDepthFrustum = getProjectionToViewZ(maxDepth); + + // Compute split boundaries + vec2 splitStart[3]; // [split start, blend end] for up to 3 splits + int lastSplitIndex = splitCount - 1; + float lastSplit = minDepth; + + for (int i = 0; i < lastSplitIndex; i++) { + float viewSplitStart = computeCascadeSplitStart(i + 1, minDepthFrustum, maxDepthFrustum); + float nextSplit = getViewToProjectionZ(viewSplitStart); + float splitBlendStart = nextSplit - (nextSplit - lastSplit) * blendZone; + lastSplit = nextSplit; + splitStart[i] = vec2(splitBlendStart, nextSplit); + } + + ivec2 gid = ivec2(gl_GlobalInvocationID.xy); + ivec2 lid = ivec2(gl_LocalInvocationID.xy); + uint tid = gl_LocalInvocationIndex; + ivec2 inputSize = textureSize(inputDepth, 0); + ivec2 baseCoord = gid * 2; + + // Initialize local bounds to infinity + const float INF = 1.0 / 0.0; + vec4 localBounds[4] = vec4[4]( + vec4(INF, INF, -INF, -INF), + vec4(INF, INF, -INF, -INF), + vec4(INF, INF, -INF, -INF), + vec4(INF, INF, -INF, -INF) + ); + vec2 localZBounds[4] = vec2[4]( + vec2(INF, -INF), + vec2(INF, -INF), + vec2(INF, -INF), + vec2(INF, -INF) + ); + + // Sample 2x2 pixel block + for (int y = 0; y < 2; y++) { + for (int x = 0; x < 2; x++) { + ivec2 coord = baseCoord + ivec2(x, y); + if (coord.x < inputSize.x && coord.y < inputSize.y) { + float depth = texelFetch(inputDepth, coord, 0).r; + // Skip background (depth == 1.0) + if (depth != 1.0) { + // Reconstruct clip-space position from depth + vec2 uv = (vec2(coord) + 0.5) / vec2(textureSize(inputDepth, 0)); + vec4 clipPos = vec4(uv * 2.0 - 1.0, depth * 2.0 - 1.0, 1.0); + + // Transform to light view space + vec4 lightSpacePos = cameraToLightView * clipPos; + lightSpacePos /= lightSpacePos.w; + + // Find which cascade this sample belongs to + int cascadeIndex = 0; + while (cascadeIndex < lastSplitIndex) { + if (depth < splitStart[cascadeIndex].x) { + break; + } + cascadeIndex += 1; + } + + // Update bounds for primary cascade + vec4 exB = localBounds[cascadeIndex]; + localBounds[cascadeIndex] = vec4( + min(exB.xy, lightSpacePos.xy), + max(exB.zw, lightSpacePos.xy) + ); + vec2 exD = localZBounds[cascadeIndex]; + localZBounds[cascadeIndex] = vec2( + min(exD.x, lightSpacePos.z), + max(exD.y, lightSpacePos.z) + ); + + // Handle blend zone - also include in previous cascade + if (cascadeIndex > 0) { + int prevCascade = cascadeIndex - 1; + vec2 split = splitStart[prevCascade]; + if (depth < split.y) { + exB = localBounds[prevCascade]; + localBounds[prevCascade] = vec4( + min(exB.xy, lightSpacePos.xy), + max(exB.zw, lightSpacePos.xy) + ); + exD = localZBounds[prevCascade]; + localZBounds[prevCascade] = vec2( + min(exD.x, lightSpacePos.z), + max(exD.y, lightSpacePos.z) + ); + } + } + } + } + } + } + + // Store local results to shared memory + for (int i = 0; i < splitCount; i++) { + sharedBounds[i][tid] = localBounds[i]; + sharedZBounds[i][tid] = localZBounds[i]; + } + barrier(); + + // Parallel reduction in shared memory + for (uint stride = 128; stride > 0; stride >>= 1) { + if (tid < stride) { + for (int i = 0; i < splitCount; i++) { + vec4 us = sharedBounds[i][tid]; + vec4 other = sharedBounds[i][tid + stride]; + sharedBounds[i][tid] = vec4( + min(us.x, other.x), + min(us.y, other.y), + max(us.z, other.z), + max(us.w, other.w) + ); + + vec2 usZ = sharedZBounds[i][tid]; + vec2 otherZ = sharedZBounds[i][tid + stride]; + sharedZBounds[i][tid] = vec2( + min(usZ.x, otherZ.x), + max(usZ.y, otherZ.y) + ); + } + } + barrier(); + } + + // Global reduction using atomics (first thread in workgroup) + if (lid.x == 0 && lid.y == 0) { + for (int i = 0; i < splitCount; i++) { + vec4 bounds = sharedBounds[i][0]; + atomicMin(gBounds[i].x, floatFlip(bounds.x)); + atomicMin(gBounds[i].y, floatFlip(bounds.y)); + atomicMax(gBounds[i].z, floatFlip(bounds.z)); + atomicMax(gBounds[i].w, floatFlip(bounds.w)); + + vec2 zBounds = sharedZBounds[i][0]; + atomicMin(gZBounds[i].x, floatFlip(zBounds.x)); + atomicMax(gZBounds[i].y, floatFlip(zBounds.y)); + } + } + // Second thread copies output data + else if (gid.x == 1 && gid.y == 0) { + rMin = gMin; + rMax = gMax; + for (int i = 0; i < splitCount - 1; i++) { + rSplitStart[i] = uvec2(floatFlip(splitStart[i].x), floatFlip(splitStart[i].y)); + } + } +} \ No newline at end of file diff --git a/jme3-core/src/main/resources/Common/MatDefs/Shadow/Sdsm/ReduceDepth.comp b/jme3-core/src/main/resources/Common/MatDefs/Shadow/Sdsm/ReduceDepth.comp new file mode 100644 index 0000000000..c6424c43e1 --- /dev/null +++ b/jme3-core/src/main/resources/Common/MatDefs/Shadow/Sdsm/ReduceDepth.comp @@ -0,0 +1,75 @@ +#version 430 + +/** + * Finds the global minimum/maximum values of a depth texture. + */ + +layout(local_size_x = 16, local_size_y = 16) in; + +layout(binding = 0) uniform sampler2D inputDepth; + +layout(std430, binding = 1) buffer MinMaxBuffer { + uint gMin; + uint gMax; +}; + +// Each workgroup thread handles a 2x2 region, so 16x16 threads cover 32x32 pixels +// Then we reduce 256 values down to 1 +shared vec2 sharedMinMax[256]; + +/** + * Encodes a float for atomic min/max operations. + * Positive floats become large uints, negative floats become small uints, + * preserving the ordering relationship. + */ +uint floatFlip(float f) { + uint u = floatBitsToUint(f); + // If negative (sign bit set): flip ALL bits (turns into small uint) + // If positive (sign bit clear): flip ONLY sign bit (makes it large uint) + return (u & 0x80000000u) != 0u ? ~u : u ^ 0x80000000u; +} + +void main() { + ivec2 gid = ivec2(gl_GlobalInvocationID.xy); + ivec2 lid = ivec2(gl_LocalInvocationID.xy); + uint tid = gl_LocalInvocationIndex; + ivec2 inputSize = textureSize(inputDepth, 0); + + // Each thread samples a 2x2 block + ivec2 baseCoord = gid * 2; + vec2 minMax = vec2(1.0 / 0.0, 0.0); // (infinity, 0) + + for (int y = 0; y < 2; y++) { + for (int x = 0; x < 2; x++) { + ivec2 coord = baseCoord + ivec2(x, y); + if (coord.x < inputSize.x && coord.y < inputSize.y) { + float depth = texelFetch(inputDepth, coord, 0).r; + // Discard depth == 1.0 (background/sky) + if (depth != 1.0) { + minMax.x = min(minMax.x, depth); + minMax.y = max(minMax.y, depth); + } + } + } + } + + sharedMinMax[tid] = minMax; + barrier(); + + // Parallel reduction in shared memory + for (uint stride = 128; stride > 0; stride >>= 1) { + if (tid < stride) { + vec2 us = sharedMinMax[tid]; + vec2 other = sharedMinMax[tid + stride]; + sharedMinMax[tid] = vec2(min(us.x, other.x), max(us.y, other.y)); + } + barrier(); + } + + // First thread in workgroup writes to global buffer using atomics + if (lid.x == 0 && lid.y == 0) { + vec2 finalMinMax = sharedMinMax[0]; + atomicMin(gMin, floatFlip(finalMinMax.x)); + atomicMax(gMax, floatFlip(finalMinMax.y)); + } +} \ No newline at end of file diff --git a/jme3-core/src/main/resources/Common/MatDefs/Shadow/Sdsm/SdsmPostShadow.frag b/jme3-core/src/main/resources/Common/MatDefs/Shadow/Sdsm/SdsmPostShadow.frag new file mode 100644 index 0000000000..a38d873a43 --- /dev/null +++ b/jme3-core/src/main/resources/Common/MatDefs/Shadow/Sdsm/SdsmPostShadow.frag @@ -0,0 +1,93 @@ +#import "Common/ShaderLib/GLSLCompat.glsllib" +#import "Common/ShaderLib/Shadows.glsllib" + +//Stripped version of the usual shadow fragment shader for SDSM; it intentionally leaves out some features. +uniform sampler2D m_Texture; +uniform sampler2D m_DepthTexture; +uniform mat4 m_ViewProjectionMatrixInverse; +uniform vec4 m_ViewProjectionMatrixRow2; + +varying vec2 texCoord; + +const mat4 biasMat = mat4(0.5, 0.0, 0.0, 0.0, +0.0, 0.5, 0.0, 0.0, +0.0, 0.0, 0.5, 0.0, +0.5, 0.5, 0.5, 1.0); + +uniform mat4 m_LightViewProjectionMatrix0; +uniform mat4 m_LightViewProjectionMatrix1; +uniform mat4 m_LightViewProjectionMatrix2; +uniform mat4 m_LightViewProjectionMatrix3; + +uniform vec2 g_ResolutionInverse; + +uniform vec3 m_LightDir; + +uniform vec2[3] m_Splits; + +vec3 getPosition(in float depth, in vec2 uv){ + vec4 pos = vec4(uv, depth, 1.0) * 2.0 - 1.0; + pos = m_ViewProjectionMatrixInverse * pos; + return pos.xyz / pos.w; +} + + +float determineShadow(int index, vec4 worldPos){ + vec4 projCoord; + if(index == 0){ + projCoord = biasMat * m_LightViewProjectionMatrix0 * worldPos; + return GETSHADOW(m_ShadowMap0, projCoord); + } else if(index == 1){ + projCoord = biasMat * m_LightViewProjectionMatrix1 * worldPos; + return GETSHADOW(m_ShadowMap1, projCoord); + } else if(index == 2){ + projCoord = biasMat * m_LightViewProjectionMatrix2 * worldPos; + return GETSHADOW(m_ShadowMap2, projCoord); + } else if(index == 3){ + projCoord = biasMat * m_LightViewProjectionMatrix3 * worldPos; + return GETSHADOW(m_ShadowMap3, projCoord); + } + return 1f; +} + +void main() { + float depth = texture2D(m_DepthTexture,texCoord).r; + vec4 color = texture2D(m_Texture,texCoord); + + //Discard shadow computation on the sky + if(depth == 1.0){ + gl_FragColor = color; + return; + } + + vec4 worldPos = vec4(getPosition(depth,texCoord),1.0); + + float shadow = 1.0; + + int primary = 0; + int secondary = -1; + float mixture = 0; + while(primary < 3){ + vec2 split = m_Splits[primary]; + if(depth < split.y){ + if(depth >= split.x){ + secondary = primary + 1; + mixture = (depth - split.x) / (split.y - split.x); + } + break; + } + primary += 1; + } + shadow = determineShadow(primary, worldPos); + if(secondary >= 0){ + float secondaryShadow = determineShadow(secondary, worldPos); + shadow = mix(shadow, secondaryShadow, mixture); + } + + shadow = shadow * m_ShadowIntensity + (1.0 - m_ShadowIntensity); + + gl_FragColor = color * vec4(shadow, shadow, shadow, 1.0); +} + + + diff --git a/jme3-core/src/main/resources/Common/MatDefs/Shadow/Sdsm/SdsmPostShadow.j3md b/jme3-core/src/main/resources/Common/MatDefs/Shadow/Sdsm/SdsmPostShadow.j3md new file mode 100644 index 0000000000..a9080c6945 --- /dev/null +++ b/jme3-core/src/main/resources/Common/MatDefs/Shadow/Sdsm/SdsmPostShadow.j3md @@ -0,0 +1,55 @@ +MaterialDef Post Shadow { + + MaterialParameters { + Int FilterMode + Boolean HardwareShadows + + Texture2D ShadowMap0 + Texture2D ShadowMap1 + Texture2D ShadowMap2 + Texture2D ShadowMap3 + + Float ShadowIntensity + + // SDSM uses Vector2Array for splits: + // Each Vector2 contains (blendStart, blendEnd) for that cascade transition + Vector2Array Splits + + Vector2 FadeInfo + + Matrix4 LightViewProjectionMatrix0 + Matrix4 LightViewProjectionMatrix1 + Matrix4 LightViewProjectionMatrix2 + Matrix4 LightViewProjectionMatrix3 + + Vector3 LightDir + + Float PCFEdge + Float ShadowMapSize + + Matrix4 ViewProjectionMatrixInverse + Vector4 ViewProjectionMatrixRow2 + + Texture2D Texture + Texture2D DepthTexture + + Boolean BackfaceShadows //Not used. + Int NumSamples //Not used. + } + + Technique { + VertexShader GLSL310 GLSL300 GLSL150 : Common/MatDefs/Shadow/PostShadowFilter.vert + FragmentShader GLSL310 GLSL300 GLSL150 : Common/MatDefs/Shadow/Sdsm/SdsmPostShadow.frag + + WorldParameters { + ResolutionInverse + } + + Defines { + HARDWARE_SHADOWS : HardwareShadows + FILTER_MODE : FilterMode + PCFEDGE : PCFEdge + SHADOWMAP_SIZE : ShadowMapSize + } + } +} \ No newline at end of file diff --git a/jme3-examples/src/main/java/jme3test/light/TestSdsmDirectionalLightShadow.java b/jme3-examples/src/main/java/jme3test/light/TestSdsmDirectionalLightShadow.java new file mode 100644 index 0000000000..3320c60415 --- /dev/null +++ b/jme3-examples/src/main/java/jme3test/light/TestSdsmDirectionalLightShadow.java @@ -0,0 +1,427 @@ +/* + * Copyright (c) 2009-2024 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; + + +import com.jme3.app.SimpleApplication; +import com.jme3.asset.plugins.ZipLocator; +import com.jme3.font.BitmapText; +import com.jme3.input.KeyInput; +import com.jme3.input.controls.ActionListener; +import com.jme3.input.controls.KeyTrigger; +import com.jme3.light.AmbientLight; +import com.jme3.light.DirectionalLight; +import com.jme3.light.LightProbe; +import com.jme3.material.Material; +import com.jme3.math.ColorRGBA; +import com.jme3.math.FastMath; +import com.jme3.math.Vector3f; +import com.jme3.post.Filter; +import com.jme3.post.FilterPostProcessor; +import com.jme3.renderer.queue.RenderQueue.ShadowMode; +import com.jme3.scene.Geometry; +import com.jme3.scene.Spatial; +import com.jme3.scene.shape.Box; +import com.jme3.scene.shape.Sphere; +import com.jme3.shadow.DirectionalLightShadowFilter; +import com.jme3.shadow.EdgeFilteringMode; +import com.jme3.shadow.SdsmDirectionalLightShadowFilter; +import com.jme3.util.SkyFactory; + +import java.io.File; + + +/** + * Test application for SDSM (Sample Distribution Shadow Mapping). + */ +public class TestSdsmDirectionalLightShadow extends SimpleApplication implements ActionListener { + + private static final int[] SHADOW_MAP_SIZES = {256, 512, 1024, 2048, 4096}; + private int shadowMapSizeIndex = 2; // Start at 1024 + private int numSplits = 2; + + private DirectionalLight sun; + private FilterPostProcessor fpp; + + private Filter activeFilter; + private SdsmDirectionalLightShadowFilter sdsmFilter; + private DirectionalLightShadowFilter traditionalFilter; + + private boolean useSdsm = true; + + // Light direction parameters (in radians) + private float lightElevation = 1.32f; + private float lightAzimuth = FastMath.QUARTER_PI; + + private BitmapText statusText; + + public static void main(String[] args) { + TestSdsmDirectionalLightShadow app = new TestSdsmDirectionalLightShadow(); + app.start(); + } + + @Override + public void simpleInitApp() { + setupCamera(); + buildScene(); + setupLighting(); + setupShadows(); + setupUI(); + setupInputs(); + } + + private void setupCamera() { + // Start at origin looking along +X + cam.setLocation(new Vector3f(0, 5f, 0)); + flyCam.setMoveSpeed(20); + flyCam.setDragToRotate(true); + inputManager.setCursorVisible(true); + //Note that for any specific scene, the actual frustum sizing matters a lot for non-SDSM results. + //Sometimes values that make the frustums match the usual scene depths will result in pretty good splits + //without SDSM! But then, the creator has to tune for that specific scene. + // If they just use a general frustum, results will be awful. + // Most users will probably not even know about this and want a frustum that shows things really far away and things closer than 1 meter to the camera. + //So what's fair to show off, really? + //(And if a user looks really closely at a shadow on a wall or something, SDSM is always going to win.) + cam.setFrustumPerspective(60f, cam.getAspect(), 0.01f, 500f); + } + + private void buildScene() { + // Add reference objects at origin for orientation + addReferenceObjects(); + + // Load Sponza scene from zip - need to extract to temp file since ZipLocator needs filesystem path + File f = new File("jme3-examples/sponza.zip"); + if(!f.exists()){ + System.out.println("Sponza demo not found. Note that SDSM is most effective with interior environments."); + } else { + assetManager.registerLocator(f.getAbsolutePath(), ZipLocator.class); + Spatial sponza = assetManager.loadModel("NewSponza_Main_glTF_003.gltf"); + sponza.setShadowMode(ShadowMode.CastAndReceive); + sponza.getLocalLightList().clear(); + + rootNode.attachChild(sponza); + + // Light probe for PBR materials + LightProbe probe = (LightProbe) assetManager.loadAsset("lightprobe.j3o"); + probe.getArea().setRadius(Float.POSITIVE_INFINITY); + probe.setPosition(new Vector3f(0f,0f,0f)); + rootNode.addLight(probe); + } + } + + private void addReferenceObjects() { + Material red = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md"); + red.setBoolean("UseMaterialColors", true); + red.setColor("Diffuse", ColorRGBA.Red); + red.setColor("Ambient", ColorRGBA.Red.mult(0.3f)); + + Material green = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md"); + green.setBoolean("UseMaterialColors", true); + green.setColor("Diffuse", ColorRGBA.Green); + green.setColor("Ambient", ColorRGBA.Green.mult(0.3f)); + + Material blue = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md"); + blue.setBoolean("UseMaterialColors", true); + blue.setColor("Diffuse", ColorRGBA.Blue); + blue.setColor("Ambient", ColorRGBA.Blue.mult(0.3f)); + + Material white = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md"); + white.setBoolean("UseMaterialColors", true); + white.setColor("Diffuse", ColorRGBA.White); + white.setColor("Ambient", ColorRGBA.White.mult(0.3f)); + + + Material brown = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md"); + brown.setBoolean("UseMaterialColors", true); + brown.setColor("Diffuse", ColorRGBA.Brown); + brown.setColor("Ambient", ColorRGBA.Brown.mult(0.3f)); + + // Origin sphere (white) + Geometry origin = new Geometry("Origin", new Sphere(16, 16, 1f)); + origin.setMaterial(white); + origin.setLocalTranslation(0, 0, 0); + origin.setShadowMode(ShadowMode.CastAndReceive); + rootNode.attachChild(origin); + + // X axis marker (red) at +10 + Geometry xMarker = new Geometry("X+", new Box(1f, 1f, 1f)); + xMarker.setMaterial(red); + xMarker.setLocalTranslation(10, 0, 0); + xMarker.setShadowMode(ShadowMode.CastAndReceive); + rootNode.attachChild(xMarker); + + // Y axis marker (green) at +10 + Geometry yMarker = new Geometry("Y+", new Box(1f, 1f, 1f)); + yMarker.setMaterial(green); + yMarker.setLocalTranslation(0, 10, 0); + yMarker.setShadowMode(ShadowMode.CastAndReceive); + rootNode.attachChild(yMarker); + + // Z axis marker (blue) at +10 + Geometry zMarker = new Geometry("Z+", new Box(1f, 1f, 1f)); + zMarker.setMaterial(blue); + zMarker.setLocalTranslation(0, 0, 10); + zMarker.setShadowMode(ShadowMode.CastAndReceive); + rootNode.attachChild(zMarker); + + // Ground plane + Geometry ground = new Geometry("Ground", new Box(50f, 0.1f, 50f)); + ground.setMaterial(brown); + ground.setLocalTranslation(0, -1f, 0); + ground.setShadowMode(ShadowMode.CastAndReceive); + rootNode.attachChild(ground); + } + + private void setupLighting() { + sun = new DirectionalLight(); + updateLightDirection(); + sun.setColor(ColorRGBA.White); + rootNode.addLight(sun); + + AmbientLight ambient = new AmbientLight(); + ambient.setColor(new ColorRGBA(0.2f, 0.2f, 0.2f, 1.0f)); + rootNode.addLight(ambient); + + Spatial sky = SkyFactory.createSky(assetManager, "Textures/Sky/Path.hdr", SkyFactory.EnvMapType.EquirectMap); + rootNode.attachChild(sky); + } + + /** + * Updates the light direction based on elevation and azimuth angles. + * Elevation: 0 = horizon, PI/2 = straight down (noon) + * Azimuth: rotation around the Y axis + */ + private void updateLightDirection() { + // Compute direction from spherical coordinates + // The light points DOWN toward the scene, so we negate Y + float cosElev = FastMath.cos(lightElevation); + float sinElev = FastMath.sin(lightElevation); + float cosAz = FastMath.cos(lightAzimuth); + float sinAz = FastMath.sin(lightAzimuth); + + // Direction vector (pointing from sun toward scene) + Vector3f dir = new Vector3f( + cosElev * sinAz, // X component + -sinElev, // Y component (negative = pointing down) + cosElev * cosAz // Z component + ); + sun.setDirection(dir.normalizeLocal()); + if(sdsmFilter != null) { sdsmFilter.setLight(sun); } + if(traditionalFilter != null) { traditionalFilter.setLight(sun); } + } + + private void setupShadows() { + fpp = new FilterPostProcessor(assetManager); + + setActiveFilter(useSdsm); + + viewPort.addProcessor(fpp); + } + + private void setActiveFilter(boolean isSdsm){ + if(activeFilter != null){ fpp.removeFilter(activeFilter); } + int shadowMapSize = SHADOW_MAP_SIZES[shadowMapSizeIndex]; + if(isSdsm){ + // SDSM shadow filter (requires OpenGL 4.3) + sdsmFilter = new SdsmDirectionalLightShadowFilter(assetManager, shadowMapSize, numSplits); + sdsmFilter.setLight(sun); + sdsmFilter.setShadowIntensity(0.7f); + sdsmFilter.setEdgeFilteringMode(EdgeFilteringMode.PCF4); + activeFilter = sdsmFilter; + traditionalFilter = null; + } else { + // Traditional shadow filter for comparison + traditionalFilter = new DirectionalLightShadowFilter(assetManager, shadowMapSize, numSplits); + traditionalFilter.setLight(sun); + traditionalFilter.setLambda(0.55f); + traditionalFilter.setShadowIntensity(0.7f); + traditionalFilter.setEdgeFilteringMode(EdgeFilteringMode.PCF4); + this.activeFilter = traditionalFilter; + sdsmFilter = null; + } + fpp.addFilter(activeFilter); + } + + private void setupUI() { + statusText = new BitmapText(guiFont); + statusText.setSize(guiFont.getCharSet().getRenderedSize() * 0.8f); + statusText.setLocalTranslation(10, cam.getHeight() - 10, 0); + guiNode.attachChild(statusText); + updateStatusText(); + } + + private void updateStatusText() { + StringBuilder sb = new StringBuilder(); + sb.append("SDSM Shadow Test (Requires OpenGL 4.3)\n"); + sb.append("---------------------------------------\n"); + + if (useSdsm) { + sb.append("Mode: SDSM (Sample Distribution Shadow Mapping)\n"); + } else { + sb.append("Mode: Traditional (Lambda-based splits)\n"); + } + + sb.append(String.format("Shadow Map Size: %d | Splits: %d\n", + SHADOW_MAP_SIZES[shadowMapSizeIndex], numSplits)); + sb.append(String.format("Light: Elevation %.0f deg | Azimuth %.0f deg\n", + lightElevation * FastMath.RAD_TO_DEG, lightAzimuth * FastMath.RAD_TO_DEG)); + + sb.append("\n"); + sb.append("Controls:\n"); + sb.append(" T - Toggle SDSM / Traditional\n"); + sb.append(" 1-4 - Set number of splits\n"); + sb.append(" -/+ - Change shadow map size\n"); + sb.append(" Numpad 8/5 - Light elevation\n"); + sb.append(" Numpad 4/6 - Light rotation\n"); + sb.append(" X - Show shadow frustum debug\n"); + + statusText.setText(sb.toString()); + } + + private void setupInputs() { + inputManager.addMapping("toggleMode", new KeyTrigger(KeyInput.KEY_T)); + inputManager.addMapping("splits1", new KeyTrigger(KeyInput.KEY_1)); + inputManager.addMapping("splits2", new KeyTrigger(KeyInput.KEY_2)); + inputManager.addMapping("splits3", new KeyTrigger(KeyInput.KEY_3)); + inputManager.addMapping("splits4", new KeyTrigger(KeyInput.KEY_4)); + inputManager.addMapping("sizeUp", new KeyTrigger(KeyInput.KEY_EQUALS)); + inputManager.addMapping("sizeDown", new KeyTrigger(KeyInput.KEY_MINUS)); + inputManager.addMapping("debug", new KeyTrigger(KeyInput.KEY_X)); + + inputManager.addMapping("elevUp", new KeyTrigger(KeyInput.KEY_NUMPAD8)); + inputManager.addMapping("elevDown", new KeyTrigger(KeyInput.KEY_NUMPAD5)); + inputManager.addMapping("azimLeft", new KeyTrigger(KeyInput.KEY_NUMPAD4)); + inputManager.addMapping("azimRight", new KeyTrigger(KeyInput.KEY_NUMPAD6)); + + inputManager.addListener(this, + "toggleMode", "splits1", "splits2", "splits3", "splits4", + "sizeUp", "sizeDown", "debug", + "elevUp", "elevDown", "azimLeft", "azimRight"); + } + + private boolean elevUp, elevDown, azimLeft, azimRight; + + @Override + public void onAction(String name, boolean isPressed, float tpf) { + // Track light direction key states + switch (name) { + case "elevUp": elevUp = isPressed; return; + case "elevDown": elevDown = isPressed; return; + case "azimLeft": azimLeft = isPressed; return; + case "azimRight": azimRight = isPressed; return; + default: break; + } + + // Other keys only on press + if (!isPressed) { + return; + } + + switch (name) { + case "toggleMode": + useSdsm = !useSdsm; + setActiveFilter(useSdsm); + updateStatusText(); + break; + + case "splits1": + case "splits2": + case "splits3": + case "splits4": + int newSplits = Integer.parseInt(name.substring(6)); + if (newSplits != numSplits) { + numSplits = newSplits; + setActiveFilter(useSdsm); + updateStatusText(); + } + break; + + case "sizeUp": + if (shadowMapSizeIndex < SHADOW_MAP_SIZES.length - 1) { + shadowMapSizeIndex++; + setActiveFilter(useSdsm); + updateStatusText(); + } + break; + + case "sizeDown": + if (shadowMapSizeIndex > 0) { + shadowMapSizeIndex--; + setActiveFilter(useSdsm); + updateStatusText(); + } + break; + + case "debug": + if (useSdsm) { + sdsmFilter.displayAllFrustums(); + } else { + traditionalFilter.displayFrustum(); + } + break; + + default: + break; + } + } + + @Override + public void simpleUpdate(float tpf) { + boolean changed = false; + + // Adjust elevation (clamped between 5 degrees and 90 degrees) + if (elevUp) { + lightElevation = Math.min(FastMath.PI, lightElevation + tpf); + changed = true; + } + if (elevDown) { + lightElevation = Math.max(0f, lightElevation - tpf); + changed = true; + } + + // Adjust azimuth (wraps around) + if (azimLeft) { + lightAzimuth -= tpf; + changed = true; + } + if (azimRight) { + lightAzimuth += tpf; + changed = true; + } + + if (changed) { + updateLightDirection(); + updateStatusText(); + } + } +} \ No newline at end of file diff --git a/jme3-ios/src/main/java/com/jme3/renderer/ios/IosGL.java b/jme3-ios/src/main/java/com/jme3/renderer/ios/IosGL.java index 7a46d6ee9d..b65224d6eb 100644 --- a/jme3-ios/src/main/java/com/jme3/renderer/ios/IosGL.java +++ b/jme3-ios/src/main/java/com/jme3/renderer/ios/IosGL.java @@ -207,6 +207,11 @@ public void glGetBufferSubData(int target, long offset, ByteBuffer data) { throw new UnsupportedOperationException("OpenGL ES 2 does not support glGetBufferSubData"); } + @Override + public void glGetBufferSubData(int target, long offset, IntBuffer data) { + throw new UnsupportedOperationException("OpenGL ES 2 does not support glGetBufferSubData"); + } + @Override public void glClear(int mask) { JmeIosGLES.glClear(mask); diff --git a/jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglGL.java b/jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglGL.java index 97a806c3e3..b6a39abb09 100644 --- a/jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglGL.java +++ b/jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglGL.java @@ -8,6 +8,7 @@ import com.jme3.util.BufferUtils; import org.lwjgl.opengl.*; +import java.lang.reflect.Constructor; import java.nio.*; public final class LwjglGL implements GL, GL2, GL3, GL4 { @@ -66,6 +67,41 @@ public void glBindImageTexture(final int unit, final int texture, final int leve final int access, final int format) { GL42.glBindImageTexture(unit, texture, level, layered, layer, access, format); } + + @Override + public void glDispatchCompute(final int numGroupsX, final int numGroupsY, final int numGroupsZ) { + GL43.glDispatchCompute(numGroupsX, numGroupsY, numGroupsZ); + } + + @Override + public void glMemoryBarrier(final int barriers) { + GL42.glMemoryBarrier(barriers); + } + + @Override + public long glFenceSync(final int condition, final int flags) { + return GL32.glFenceSync(condition, flags).getPointer(); + } + + private Constructor constructor = null; + private GLSync makeGLSync(final long sync){ + try { + if(constructor == null){ + constructor = GLSync.class.getDeclaredConstructor(long.class); + constructor.setAccessible(true); + } + return constructor.newInstance(sync); + } catch(Exception e){ throw new RuntimeException(e); } + } + @Override + public int glClientWaitSync(final long sync, final int flags, final long timeout) { + return GL32.glClientWaitSync(makeGLSync(sync), flags, timeout); + } + + @Override + public void glDeleteSync(final long sync) { + GL32.glDeleteSync(makeGLSync(sync)); + } @Override public void glBlendEquationSeparate(int colorMode, int alphaMode){ @@ -105,6 +141,12 @@ public void glBufferData(int param1, ByteBuffer param2, int param3) { GL15.glBufferData(param1, param2, param3); } + @Override + public void glBufferData(int target, IntBuffer data, int usage) { + checkLimit(data); + GL15.glBufferData(target, data, usage); + } + @Override public void glBufferSubData(int param1, long param2, FloatBuffer param3) { checkLimit(param3); @@ -293,6 +335,12 @@ public void glGetBufferSubData(int target, long offset, ByteBuffer data) { GL15.glGetBufferSubData(target, offset, data); } + @Override + public void glGetBufferSubData(int target, long offset, IntBuffer data) { + checkLimit(data); + GL15.glGetBufferSubData(target, offset, data); + } + @Override public int glGetError() { return GL11.glGetError(); diff --git a/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGL.java b/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGL.java index d2c607d747..a91e5aa1e7 100644 --- a/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGL.java +++ b/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGL.java @@ -87,7 +87,32 @@ public void glBindImageTexture(final int unit, final int texture, final int leve final int access, final int format) { GL42.glBindImageTexture(unit, texture, level, layered, layer, access, format); } - + + @Override + public void glDispatchCompute(final int numGroupsX, final int numGroupsY, final int numGroupsZ) { + GL43.glDispatchCompute(numGroupsX, numGroupsY, numGroupsZ); + } + + @Override + public void glMemoryBarrier(final int barriers) { + GL42.glMemoryBarrier(barriers); + } + + @Override + public long glFenceSync(final int condition, final int flags) { + return GL32.glFenceSync(condition, flags); + } + + @Override + public int glClientWaitSync(final long sync, final int flags, final long timeout) { + return GL32.glClientWaitSync(sync, flags, timeout); + } + + @Override + public void glDeleteSync(final long sync) { + GL32.glDeleteSync(sync); + } + @Override public void glBlendEquationSeparate(final int colorMode, final int alphaMode) { GL20.glBlendEquationSeparate(colorMode, alphaMode); @@ -127,6 +152,12 @@ public void glBufferData(final int target, final ByteBuffer data, final int usag GL15.glBufferData(target, data, usage); } + @Override + public void glBufferData(final int target, final IntBuffer data, final int usage) { + checkLimit(data); + GL15.glBufferData(target, data, usage); + } + @Override public void glBufferSubData(final int target, final long offset, final FloatBuffer data) { checkLimit(data); @@ -321,6 +352,12 @@ public void glGetBufferSubData(final int target, final long offset, final ByteBu GL15.glGetBufferSubData(target, offset, data); } + @Override + public void glGetBufferSubData(final int target, final long offset, final IntBuffer data) { + checkLimit(data); + GL15.glGetBufferSubData(target, offset, data); + } + @Override public int glGetError() { return GL11.glGetError();