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}:
+ *
+ * - Better shadow map utilization through sample-based fitting
+ * - Dynamic cascade adaptation to scene geometry
+ * - Reduced shadow pop-in artifacts
+ *
+ *
+ * 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();