scott 3 недель назад
Родитель
Сommit
fa4cae6221
18 измененных файлов с 2553 добавлено и 4 удалено
  1. 5 0
      jme3-android/src/main/java/com/jme3/renderer/android/AndroidGL.java
  2. 184 0
      jme3-core/src/main/java/com/jme3/renderer/opengl/ComputeShader.java
  3. 10 0
      jme3-core/src/main/java/com/jme3/renderer/opengl/GL.java
  4. 82 2
      jme3-core/src/main/java/com/jme3/renderer/opengl/GL4.java
  5. 6 0
      jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java
  6. 131 0
      jme3-core/src/main/java/com/jme3/renderer/opengl/ShaderStorageBufferObject.java
  7. 8 1
      jme3-core/src/main/java/com/jme3/shadow/AbstractShadowFilter.java
  8. 197 0
      jme3-core/src/main/java/com/jme3/shadow/SdsmDirectionalLightShadowFilter.java
  9. 472 0
      jme3-core/src/main/java/com/jme3/shadow/SdsmDirectionalLightShadowRenderer.java
  10. 481 0
      jme3-core/src/main/java/com/jme3/shadow/SdsmFitter.java
  11. 236 0
      jme3-core/src/main/resources/Common/MatDefs/Shadow/Sdsm/FitLightFrustums.comp
  12. 75 0
      jme3-core/src/main/resources/Common/MatDefs/Shadow/Sdsm/ReduceDepth.comp
  13. 93 0
      jme3-core/src/main/resources/Common/MatDefs/Shadow/Sdsm/SdsmPostShadow.frag
  14. 55 0
      jme3-core/src/main/resources/Common/MatDefs/Shadow/Sdsm/SdsmPostShadow.j3md
  15. 427 0
      jme3-examples/src/main/java/jme3test/light/TestSdsmDirectionalLightShadow.java
  16. 5 0
      jme3-ios/src/main/java/com/jme3/renderer/ios/IosGL.java
  17. 48 0
      jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglGL.java
  18. 38 1
      jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGL.java

+ 5 - 0
jme3-android/src/main/java/com/jme3/renderer/android/AndroidGL.java

@@ -163,6 +163,11 @@ public class AndroidGL implements GL, GL2, GLES_30, GLExt, GLFbo {
         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);

+ 184 - 0
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).
+ * <p>
+ * 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());
+        }
+    }
+}

+ 10 - 0
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);
+
     /**
      * <p><a target="_blank" href="http://docs.gl/gl4/glBufferSubData">Reference Page</a></p>
      * <p>
@@ -824,6 +829,11 @@ public interface GL {
      */
     public void glGetBufferSubData(int target, long offset, ByteBuffer data);
 
+    /**
+     * See {@link #glGetBufferSubData(int, long, ByteBuffer)}
+     */
+    public void glGetBufferSubData(int target, long offset, IntBuffer data);
+
     /**
      * <p><a target="_blank" href="http://docs.gl/gl4/glGetError">Reference Page</a></p>
      *

+ 82 - 2
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);
-    
+
+    /**
+     * <p><a target="_blank" href="http://docs.gl/gl4/glDispatchCompute">Reference Page</a></p>
+     * <p>
+     * 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);
+
+    /**
+     * <p><a target="_blank" href="http://docs.gl/gl4/glMemoryBarrier">Reference Page</a></p>
+     * <p>
+     * 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);
+
+    /**
+     * <p><a target="_blank" href="http://docs.gl/gl4/glFenceSync">Reference Page</a></p>
+     * <p>
+     * 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);
+
+    /**
+     * <p><a target="_blank" href="http://docs.gl/gl4/glClientWaitSync">Reference Page</a></p>
+     * <p>
+     * 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);
+
+    /**
+     * <p><a target="_blank" href="http://docs.gl/gl4/glDeleteSync">Reference Page</a></p>
+     * <p>
+     * Deletes a sync object.
+     *
+     * @param sync the sync object to delete
+     */
+    public void glDeleteSync(long sync);
+
 }

+ 6 - 0
jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java

@@ -138,6 +138,7 @@ public final class GLRenderer implements Renderer {
         generateMipmapsForFramebuffers = v;
     }
 
+
     public void setDebugEnabled(boolean v) {
         debug = v;
     }
@@ -3597,4 +3598,9 @@ public final class GLRenderer implements Renderer {
             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;
+    }
 }

+ 131 - 0
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;
+
+/**
+ * <p><a target="_blank" href="https://wikis.khronos.org/opengl/Shader_Storage_Buffer_Object">Reference Page</a></p>
+ * A Shader Storage Buffer Object (SSBO) for GPU read/write data storage.
+ * <p>
+ * 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;
+    }
+}

+ 8 - 1
jme3-core/src/main/java/com/jme3/shadow/AbstractShadowFilter.java

@@ -41,7 +41,6 @@ import com.jme3.renderer.RenderManager;
 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 abstract class AbstractShadowFilter<T extends AbstractShadowRenderer> ext
         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<T> jmeClone() {

+ 197 - 0
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.
+ * <p>
+ * This filter uses GPU compute shaders to analyze the depth buffer and compute
+ * optimal cascade split positions for rendering shadow maps.
+ * <p>
+ * Key benefits over {@link DirectionalLightShadowFilter}:
+ * <ul>
+ *   <li>Better shadow map utilization through sample-based fitting</li>
+ *   <li>Dynamic cascade adaptation to scene geometry</li>
+ *   <li>Reduced shadow pop-in artifacts</li>
+ * </ul>
+ * <p>
+ * Requires OpenGL 4.3+ for compute shader support. Only works for filter-based shadow mapping.
+ */
+public class SdsmDirectionalLightShadowFilter extends AbstractShadowFilter<SdsmDirectionalLightShadowRenderer> {
+    /**
+     * 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);
+    }
+
+}

+ 472 - 0
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.
+     * <p>
+     * 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.
+     * <p>
+     * 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<Spatial> 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);
+    }
+
+}

+ 481 - 0
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<SdsmResultHolder> resultHoldersInFlight = new LinkedList<>();
+    private final LinkedList<SdsmResultHolder> 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<SplitBounds> splits;
+        public final float minDepth;
+        public final float maxDepth;
+        public final List<SplitInfo> cascadeStarts;
+
+        public SplitFit(List<SplitBounds> splits, float minDepth, float maxDepth, List<SplitInfo> 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<fitResult.length;i++) {
+                fitResult[i] = uintFlip(uintFit[i]);
+            }
+
+            float minDepth = fitResult[24];
+            if (minDepth == Float.POSITIVE_INFINITY) {
+                // No real samples found
+                return null;
+            }
+            float maxDepth = fitResult[25];
+            if (maxDepth == 0) {
+                return null;
+            }
+
+            List<SplitBounds> 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<SplitInfo> 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<SdsmResultHolder> 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;
+    }
+}

+ 236 - 0
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));
+        }
+    }
+}

+ 75 - 0
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));
+    }
+}

+ 93 - 0
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);
+}
+
+
+

+ 55 - 0
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
+        }
+    }
+}

+ 427 - 0
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();
+        }
+    }
+}

+ 5 - 0
jme3-ios/src/main/java/com/jme3/renderer/ios/IosGL.java

@@ -207,6 +207,11 @@ public class IosGL implements GL, GL2, GLES_30, GLExt, GLFbo {
         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);

+ 48 - 0
jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglGL.java

@@ -8,6 +8,7 @@ import com.jme3.renderer.opengl.GL4;
 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 final class LwjglGL implements GL, GL2, GL3, GL4 {
                                    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<GLSync> 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 final class LwjglGL implements GL, GL2, GL3, GL4 {
         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 final class LwjglGL implements GL, GL2, GL3, GL4 {
         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();

+ 38 - 1
jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGL.java

@@ -87,7 +87,32 @@ public class LwjglGL extends LwjglRender implements GL, GL2, GL3, GL4 {
                                    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 class LwjglGL extends LwjglRender implements GL, GL2, GL3, GL4 {
         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 class LwjglGL extends LwjglRender implements GL, GL2, GL3, GL4 {
         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();