From 3fd794c6ef1374facd6ae28e412554e4fe07ab0e Mon Sep 17 00:00:00 2001 From: Meekiavelique Date: Fri, 27 Feb 2026 11:42:09 +0100 Subject: [PATCH 01/14] perf(gl): cache texture targets for multibind --- .../api/client/render/VeilRenderSystem.java | 6 +++++ .../api/client/render/ext/VeilMultiBind.java | 27 ++++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/common/src/main/java/foundry/veil/api/client/render/VeilRenderSystem.java b/common/src/main/java/foundry/veil/api/client/render/VeilRenderSystem.java index 78d2fbf20..7112fb025 100644 --- a/common/src/main/java/foundry/veil/api/client/render/VeilRenderSystem.java +++ b/common/src/main/java/foundry/veil/api/client/render/VeilRenderSystem.java @@ -40,6 +40,7 @@ import foundry.veil.impl.client.render.pipeline.VeilShaderBufferCache; import foundry.veil.impl.client.render.profiler.VeilRenderProfilerImpl; import foundry.veil.impl.client.render.shader.program.ShaderProgramImpl; +import foundry.veil.impl.client.render.light.VoxelShadowGrid; import foundry.veil.mixin.pipeline.accessor.PipelineBufferSourceAccessor; import foundry.veil.platform.VeilEventPlatform; import net.minecraft.Util; @@ -1241,6 +1242,8 @@ public static void close() { if (renderer != null) { renderer.free(); } + VoxelShadowGrid.close(); + VeilMultiBind.clearTargetCache(); glDeleteVertexArrays(screenQuadVao); MemoryUtil.memFree(emptySamplers); SHADER_BUFFER_CACHE.free(); @@ -1277,6 +1280,8 @@ public static boolean drawLights(ProfilerFiller profiler, CullFrustum cullFrustu return false; } + VoxelShadowGrid.beforeRenderLights(); + VeilDebug debug = VeilDebug.get(); debug.pushDebugGroup("Veil Draw Lights"); @@ -1322,5 +1327,6 @@ public static void compositeLights(ProfilerFiller profiler) { @ApiStatus.Internal public static void clearLevel() { NecromancerRenderDispatcher.delete(); + VoxelShadowGrid.clearLevel(); } } diff --git a/common/src/main/java/foundry/veil/api/client/render/ext/VeilMultiBind.java b/common/src/main/java/foundry/veil/api/client/render/ext/VeilMultiBind.java index a0fa1b515..6a5469b31 100644 --- a/common/src/main/java/foundry/veil/api/client/render/ext/VeilMultiBind.java +++ b/common/src/main/java/foundry/veil/api/client/render/ext/VeilMultiBind.java @@ -3,6 +3,7 @@ import com.mojang.blaze3d.platform.GlStateManager; import com.mojang.blaze3d.systems.RenderSystem; import foundry.veil.Veil; +import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap; import org.lwjgl.opengl.ARBMultiBind; import org.lwjgl.opengl.GL; import org.lwjgl.opengl.GLCapabilities; @@ -144,10 +145,28 @@ public void bindSamplers(int first, int... samplers) { GL_TEXTURE_2D_MULTISAMPLE_ARRAY, }; + private static final Int2IntOpenHashMap TARGET_CACHE = new Int2IntOpenHashMap(); + private static final int MISSING_TARGET = Integer.MIN_VALUE; + + static { + TARGET_CACHE.defaultReturnValue(MISSING_TARGET); + } + private static int getTarget(int texture) { + if (texture == 0) { + return GL_TEXTURE_2D; + } + + int cached = TARGET_CACHE.get(texture); + if (cached != MISSING_TARGET) { + return cached; + } + GLCapabilities caps = GL.getCapabilities(); if (caps.glGetTextureParameteriv != 0L && caps.OpenGL45) { // Last ditch effort if the platform has the method anyways - return glGetTextureParameteri(texture, GL_TEXTURE_TARGET); + int target = glGetTextureParameteri(texture, GL_TEXTURE_TARGET); + TARGET_CACHE.put(texture, target); + return target; } // Nothing else I can do, so do the dirty hack to figure out the target @@ -161,12 +180,14 @@ private static int getTarget(int texture) { glBindTexture(target, texture); if (glGetError() == GL_NO_ERROR) { glBindTexture(target, old); + TARGET_CACHE.put(texture, target); return target; } glBindTexture(target, old); } // Should never happen + TARGET_CACHE.put(texture, GL_TEXTURE_2D); return GL_TEXTURE_2D; } @@ -221,4 +242,8 @@ public static VeilMultiBind get() { } return multiBind; } + + public static void clearTargetCache() { + TARGET_CACHE.clear(); + } } From 6aad510ac8e2ce3f17187746e0a27f1352831af4 Mon Sep 17 00:00:00 2001 From: Meekiavelique Date: Fri, 27 Feb 2026 11:42:40 +0100 Subject: [PATCH 02/14] feat(light): runtime voxel shadow grid upload + binding --- .../client/render/light/VoxelShadowGrid.java | 253 ++++++++++++++++++ 1 file changed, 253 insertions(+) create mode 100644 common/src/main/java/foundry/veil/impl/client/render/light/VoxelShadowGrid.java diff --git a/common/src/main/java/foundry/veil/impl/client/render/light/VoxelShadowGrid.java b/common/src/main/java/foundry/veil/impl/client/render/light/VoxelShadowGrid.java new file mode 100644 index 000000000..19e20cb27 --- /dev/null +++ b/common/src/main/java/foundry/veil/impl/client/render/light/VoxelShadowGrid.java @@ -0,0 +1,253 @@ +package foundry.veil.impl.client.render.light; + +import com.mojang.blaze3d.systems.RenderSystem; +import foundry.veil.Veil; +import foundry.veil.api.client.render.VeilRenderSystem; +import foundry.veil.api.client.render.shader.program.ShaderProgram; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.core.BlockPos; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.*; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.Vec3; +import org.lwjgl.system.MemoryUtil; + +import java.nio.ByteBuffer; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import static org.lwjgl.opengl.GL11C.*; +import static org.lwjgl.opengl.GL12C.*; + +public final class VoxelShadowGrid { + + public static final int GRID_SIZE = 64; + private static final int HALF = GRID_SIZE / 2; + private static final double REBUILD_THRESHOLD_SQ = 12.0 * 12.0; + + private static final ResourceLocation POINT_SHADER = Veil.veilPath("light/point"); + private static final ResourceLocation AREA_SHADER = Veil.veilPath("light/area"); + + private static int textureId; + private static Vec3 gridCenter; + + private static volatile int generation; + private static volatile Vec3 pendingCenter; + private static volatile ResourceKey pendingDimension; + private static volatile ByteBuffer pendingBuffer; + private static volatile boolean rebuilding; + + private static final ExecutorService EXECUTOR = Executors.newSingleThreadExecutor(r -> { + Thread t = new Thread(r, "veil-voxel-shadow-grid"); + t.setDaemon(true); + return t; + }); + + private VoxelShadowGrid() { + } + + public static void beforeRenderLights() { + RenderSystem.assertOnRenderThread(); + + Minecraft client = Minecraft.getInstance(); + ClientLevel level = client.level; + if (level == null) { + return; + } + + ensureTexture(); + uploadPending(level); + queueRebuildIfNeeded(level, client.gameRenderer.getMainCamera().getPosition()); + pushUniforms(); + } + + public static void clearLevel() { + RenderSystem.assertOnRenderThreadOrInit(); + + generation++; + gridCenter = null; + pendingCenter = null; + pendingDimension = null; + rebuilding = false; + + ByteBuffer buffer = pendingBuffer; + pendingBuffer = null; + if (buffer != null) { + MemoryUtil.memFree(buffer); + } + } + + public static void close() { + RenderSystem.assertOnRenderThreadOrInit(); + + clearLevel(); + + if (textureId != 0) { + glDeleteTextures(textureId); + textureId = 0; + } + + EXECUTOR.shutdownNow(); + } + + private static void queueRebuildIfNeeded(ClientLevel level, Vec3 cameraPos) { + Vec3 center = gridCenter; + if (center != null && cameraPos.distanceToSqr(center) <= REBUILD_THRESHOLD_SQ) { + return; + } + + if (rebuilding) { + return; + } + + rebuilding = true; + pendingCenter = cameraPos; + pendingDimension = level.dimension(); + int capturedGeneration = generation; + + int cx = (int) Math.floor(cameraPos.x); + int cy = (int) Math.floor(cameraPos.y); + int cz = (int) Math.floor(cameraPos.z); + + ClientLevel capturedLevel = level; + ResourceKey capturedDimension = level.dimension(); + EXECUTOR.submit(() -> { + try { + ByteBuffer buffer = buildBuffer(capturedLevel, cx, cy, cz); + if (generation != capturedGeneration) { + MemoryUtil.memFree(buffer); + return; + } + pendingBuffer = buffer; + pendingDimension = capturedDimension; + } catch (Throwable t) { + rebuilding = false; + } + }); + } + + private static void uploadPending(ClientLevel level) { + ByteBuffer buffer = pendingBuffer; + if (buffer == null) { + return; + } + + pendingBuffer = null; + rebuilding = false; + + if (pendingDimension != null && pendingDimension != level.dimension()) { + MemoryUtil.memFree(buffer); + pendingCenter = null; + pendingDimension = null; + return; + } + + uploadBuffer(buffer); + MemoryUtil.memFree(buffer); + gridCenter = pendingCenter; + pendingCenter = null; + pendingDimension = null; + } + + private static ByteBuffer buildBuffer(ClientLevel level, int cx, int cy, int cz) { + int total = GRID_SIZE * GRID_SIZE * GRID_SIZE; + ByteBuffer buffer = MemoryUtil.memAlloc(total); + + BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); + for (int z = 0; z < GRID_SIZE; z++) { + for (int y = 0; y < GRID_SIZE; y++) { + for (int x = 0; x < GRID_SIZE; x++) { + pos.set(cx - HALF + x, cy - HALF + y, cz - HALF + z); + BlockState state = level.getBlockState(pos); + buffer.put(x + y * GRID_SIZE + z * GRID_SIZE * GRID_SIZE, voxelOccupancy(level, pos, state)); + } + } + } + + buffer.rewind(); + return buffer; + } + + private static byte voxelOccupancy(ClientLevel level, BlockPos pos, BlockState state) { + if (state.isAir()) { + return 0; + } + + if (!state.getFluidState().isEmpty()) { + return 0; + } + + Block block = state.getBlock(); + if (block instanceof SlabBlock) return 0; + if (block instanceof StairBlock) return 0; + if (block instanceof WallBlock) return 0; + if (block instanceof FenceBlock) return 0; + if (block instanceof FenceGateBlock) return 0; + if (block instanceof CarpetBlock) return 0; + if (block instanceof IronBarsBlock) return 0; + if (block instanceof DoorBlock) return 0; + if (block instanceof TrapDoorBlock) return 0; + if (block instanceof LeavesBlock) return 0; + if (block instanceof LiquidBlock) return 0; + + if (!state.canOcclude()) { + return 0; + } + + return state.isSolidRender(level, pos) ? (byte) 0xFF : 0; + } + + private static void uploadBuffer(ByteBuffer buffer) { + glBindTexture(GL_TEXTURE_3D, textureId); + glTexImage3D(GL_TEXTURE_3D, 0, GL_R8, GRID_SIZE, GRID_SIZE, GRID_SIZE, 0, GL_RED, GL_UNSIGNED_BYTE, buffer); + glBindTexture(GL_TEXTURE_3D, 0); + } + + private static void ensureTexture() { + if (textureId != 0) { + return; + } + + textureId = glGenTextures(); + glBindTexture(GL_TEXTURE_3D, textureId); + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); + + int total = GRID_SIZE * GRID_SIZE * GRID_SIZE; + ByteBuffer zeros = MemoryUtil.memCalloc(total); + glTexImage3D(GL_TEXTURE_3D, 0, GL_R8, GRID_SIZE, GRID_SIZE, GRID_SIZE, 0, GL_RED, GL_UNSIGNED_BYTE, zeros); + MemoryUtil.memFree(zeros); + + glBindTexture(GL_TEXTURE_3D, 0); + } + + private static void pushUniforms() { + Vec3 center = gridCenter; + if (center == null) { + center = Minecraft.getInstance().gameRenderer.getMainCamera().getPosition(); + } + + float originX = (float) (Math.floor(center.x) - HALF); + float originY = (float) (Math.floor(center.y) - HALF); + float originZ = (float) (Math.floor(center.z) - HALF); + + pushUniforms(POINT_SHADER, originX, originY, originZ); + pushUniforms(AREA_SHADER, originX, originY, originZ); + } + + private static void pushUniforms(ResourceLocation shader, float originX, float originY, float originZ) { + ShaderProgram program = VeilRenderSystem.renderer().getShaderManager().getShader(shader); + if (program == null || !program.isValid()) { + return; + } + + program.setSampler("BlockGrid", textureId); + program.getUniformSafe("GridOrigin").setVector(originX, originY, originZ); + } +} From b5f841ea42a7c9fd4a6252568e168fad142afa07 Mon Sep 17 00:00:00 2001 From: Meekiavelique Date: Fri, 27 Feb 2026 11:43:15 +0100 Subject: [PATCH 03/14] feat(light): voxelshadow DDA occlusion in point/area shaders --- .../shaders/include/voxel_shadow.glsl | 47 +++++++++++++++++++ .../pinwheel/shaders/program/light/area.fsh | 7 ++- .../pinwheel/shaders/program/light/point.fsh | 8 +++- 3 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 common/src/main/resources/assets/veil/pinwheel/shaders/include/voxel_shadow.glsl diff --git a/common/src/main/resources/assets/veil/pinwheel/shaders/include/voxel_shadow.glsl b/common/src/main/resources/assets/veil/pinwheel/shaders/include/voxel_shadow.glsl new file mode 100644 index 000000000..a3fd1c816 --- /dev/null +++ b/common/src/main/resources/assets/veil/pinwheel/shaders/include/voxel_shadow.glsl @@ -0,0 +1,47 @@ +uniform sampler3D BlockGrid; +uniform vec3 GridOrigin; + +#define VOXELSHADOW_GRID_SIZE 64 +#define VOXELSHADOW_MAX_STEPS 128 + +float voxelshadowVisibility(vec3 fragPos, vec3 lightPos) { + vec3 startG = fragPos - GridOrigin; + vec3 endG = lightPos - GridOrigin; + vec3 delta = endG - startG; + float rayLen = length(delta); + if (rayLen < 0.001) return 1.0; + + vec3 rDir = delta / rayLen; + ivec3 cell = ivec3(floor(startG)); + ivec3 iStep = ivec3(sign(rDir)); + + vec3 invAbs = 1.0 / max(abs(rDir), vec3(1e-5)); + vec3 tDelta = invAbs; + + vec3 cellF = vec3(cell); + vec3 tMax; + tMax.x = (rDir.x >= 0.0) ? (cellF.x + 1.0 - startG.x) * invAbs.x : (startG.x - cellF.x) * invAbs.x; + tMax.y = (rDir.y >= 0.0) ? (cellF.y + 1.0 - startG.y) * invAbs.y : (startG.y - cellF.y) * invAbs.y; + tMax.z = (rDir.z >= 0.0) ? (cellF.z + 1.0 - startG.z) * invAbs.z : (startG.z - cellF.z) * invAbs.z; + + for (int i = 0; i < VOXELSHADOW_MAX_STEPS; i++) { + if (any(lessThan(cell, ivec3(0))) || any(greaterThanEqual(cell, ivec3(VOXELSHADOW_GRID_SIZE)))) break; + if (i > 0 && texelFetch(BlockGrid, cell, 0).r > 0.5) return 0.0; + + if (tMax.x < tMax.y && tMax.x < tMax.z) { + if (tMax.x >= rayLen) break; + tMax.x += tDelta.x; + cell.x += iStep.x; + } else if (tMax.y < tMax.z) { + if (tMax.y >= rayLen) break; + tMax.y += tDelta.y; + cell.y += iStep.y; + } else { + if (tMax.z >= rayLen) break; + tMax.z += tDelta.z; + cell.z += iStep.z; + } + } + + return 1.0; +} diff --git a/common/src/main/resources/assets/veil/pinwheel/shaders/program/light/area.fsh b/common/src/main/resources/assets/veil/pinwheel/shaders/program/light/area.fsh index 8af99a180..079a01398 100644 --- a/common/src/main/resources/assets/veil/pinwheel/shaders/program/light/area.fsh +++ b/common/src/main/resources/assets/veil/pinwheel/shaders/program/light/area.fsh @@ -2,12 +2,14 @@ #include veil:space_helper #include veil:color_utilities #include veil:light +#include veil:voxel_shadow in mat4 lightMat; in vec3 lightColor; in vec2 size; in float maxAngle; in float maxDistance; +in float occluded; uniform sampler2D AlbedoSampler; uniform sampler2D NormalSampler; @@ -69,10 +71,13 @@ void main() { float angleFalloff = clamp(angle, 0.0, maxAngle) / maxAngle; angleFalloff = smoothstep(1.0, 0.0, angleFalloff); diffuse *= angleFalloff; + if (occluded > 0.5) { + vec3 normalWS = normalize((VeilCamera.IViewMat * vec4(normalVS, 0.0)).xyz); + diffuse *= voxelshadowVisibility(pos + normalWS * 0.01, lightPos); + } float reflectivity = 0.05; vec3 diffuseColor = diffuse * lightColor; fragColor = vec4(albedoColor.rgb * diffuseColor * (1.0 - reflectivity) + diffuseColor * reflectivity, 1.0); } - diff --git a/common/src/main/resources/assets/veil/pinwheel/shaders/program/light/point.fsh b/common/src/main/resources/assets/veil/pinwheel/shaders/program/light/point.fsh index 15c2d8fd6..b76e65599 100644 --- a/common/src/main/resources/assets/veil/pinwheel/shaders/program/light/point.fsh +++ b/common/src/main/resources/assets/veil/pinwheel/shaders/program/light/point.fsh @@ -2,10 +2,12 @@ #include veil:space_helper #include veil:color_utilities #include veil:light +#include veil:voxel_shadow in vec3 lightPos; in vec3 lightColor; in float radius; +in float occluded; uniform sampler2D AlbedoSampler; uniform sampler2D NormalSampler; @@ -31,9 +33,13 @@ void main() { vec3 normalVS = texture(NormalSampler, screenUv).xyz; vec3 lightDirection = normalize((VeilCamera.ViewMat * vec4(offset, 0.0)).xyz); - float diffuse = clamp(0.0, 1.0, dot(normalVS, lightDirection)); + float diffuse = clamp(dot(normalVS, lightDirection), 0.0, 1.0); diffuse = (diffuse + MINECRAFT_AMBIENT_LIGHT) / (1.0 + MINECRAFT_AMBIENT_LIGHT); diffuse *= attenuate_no_cusp(length(offset), radius); + if (occluded > 0.5) { + vec3 normalWS = normalize((VeilCamera.IViewMat * vec4(normalVS, 0.0)).xyz); + diffuse *= voxelshadowVisibility(pos + normalWS * 0.01, lightPos); + } float reflectivity = 0.05; vec3 diffuseColor = diffuse * lightColor; From 1fcf200769970d4a6ae603b99142700f817817a7 Mon Sep 17 00:00:00 2001 From: Meekiavelique Date: Fri, 27 Feb 2026 11:48:46 +0100 Subject: [PATCH 04/14] feat(light): add per-light occlusion toggle --- .../client/render/light/data/AreaLightData.java | 17 +++++++++++++++++ .../render/light/data/PointLightData.java | 17 +++++++++++++++++ .../client/render/light/AreaLightRenderer.java | 3 ++- .../light/InstancedPointLightRenderer.java | 3 ++- .../pinwheel/shaders/program/light/area.vsh | 3 +++ .../pinwheel/shaders/program/light/point.vsh | 3 +++ 6 files changed, 44 insertions(+), 2 deletions(-) diff --git a/common/src/main/java/foundry/veil/api/client/render/light/data/AreaLightData.java b/common/src/main/java/foundry/veil/api/client/render/light/data/AreaLightData.java index 85ecec04d..ced4702ab 100644 --- a/common/src/main/java/foundry/veil/api/client/render/light/data/AreaLightData.java +++ b/common/src/main/java/foundry/veil/api/client/render/light/data/AreaLightData.java @@ -31,6 +31,7 @@ public class AreaLightData extends LightData implements InstancedLightData, Edit protected float angle; protected float distance; + protected boolean occluded; public AreaLightData() { this.matrix = new Matrix4d(); @@ -41,6 +42,16 @@ public AreaLightData() { this.angle = (float) Math.toRadians(45); this.distance = 1.0F; + this.occluded = false; + } + + public boolean isOccluded() { + return this.occluded; + } + + public AreaLightData setOccluded(boolean occluded) { + this.occluded = occluded; + return this; } protected void updateMatrix() { @@ -163,6 +174,7 @@ public void store(ByteBuffer buffer) { buffer.putShort((short) Mth.clamp((int) (this.angle * MAX_ANGLE_SIZE), 0, 65535)); buffer.putFloat(this.distance); + buffer.putFloat(this.occluded ? 1.0F : 0.0F); } @Override @@ -201,6 +213,7 @@ public void renderImGuiAttributes() { float[] editAngle = new float[]{this.angle}; float[] editDistance = new float[]{this.distance}; + imgui.type.ImBoolean editOccluded = new imgui.type.ImBoolean(this.occluded); if (ImGui.dragFloat2("size", editSize, 0.02F, 0.0001F)) { this.setSize(editSize[0], editSize[1]); @@ -250,5 +263,9 @@ public void renderImGuiAttributes() { if (ImGui.dragScalar("distance", editDistance, 0.02F, 0.0F)) { this.setDistance(editDistance[0]); } + + if (ImGui.checkbox("occluded", editOccluded)) { + this.occluded = editOccluded.get(); + } } } diff --git a/common/src/main/java/foundry/veil/api/client/render/light/data/PointLightData.java b/common/src/main/java/foundry/veil/api/client/render/light/data/PointLightData.java index dfb45ac3c..1465532a9 100644 --- a/common/src/main/java/foundry/veil/api/client/render/light/data/PointLightData.java +++ b/common/src/main/java/foundry/veil/api/client/render/light/data/PointLightData.java @@ -23,10 +23,21 @@ public class PointLightData extends LightData implements IndirectLightData, Edit protected final Vector3d position; protected float radius; + protected boolean occluded; public PointLightData() { this.position = new Vector3d(); this.radius = 1.0F; + this.occluded = false; + } + + public boolean isOccluded() { + return this.occluded; + } + + public PointLightData setOccluded(boolean occluded) { + this.occluded = occluded; + return this; } @Override @@ -97,6 +108,7 @@ public void store(ByteBuffer buffer) { buffer.putFloat(this.color.green() * this.brightness); buffer.putFloat(this.color.blue() * this.brightness); buffer.putFloat(this.radius); + buffer.putFloat(this.occluded ? 1.0F : 0.0F); } @Override @@ -118,6 +130,7 @@ public void renderImGuiAttributes() { double[] editZ = new double[]{this.position.z()}; float[] editRadius = new float[]{this.radius}; + imgui.type.ImBoolean editOccluded = new imgui.type.ImBoolean(this.occluded); float totalWidth = ImGui.calcItemWidth(); ImGui.pushItemWidth(totalWidth / 3.0F - (ImGui.getStyle().getItemInnerSpacingX() * 0.58F)); @@ -140,5 +153,9 @@ public void renderImGuiAttributes() { if (ImGui.dragScalar("radius", editRadius, 0.02F, 0.0F)) { this.setRadius(editRadius[0]); } + + if (ImGui.checkbox("occluded", editOccluded)) { + this.occluded = editOccluded.get(); + } } } diff --git a/common/src/main/java/foundry/veil/impl/client/render/light/AreaLightRenderer.java b/common/src/main/java/foundry/veil/impl/client/render/light/AreaLightRenderer.java index 80169769c..7813338b0 100644 --- a/common/src/main/java/foundry/veil/impl/client/render/light/AreaLightRenderer.java +++ b/common/src/main/java/foundry/veil/impl/client/render/light/AreaLightRenderer.java @@ -25,7 +25,7 @@ public class AreaLightRenderer extends InstancedLightRenderer { private static final ResourceLocation RENDER_TYPE = Veil.veilPath("light/area"); public AreaLightRenderer() { - super(Float.BYTES * 22 + 2); + super(Float.BYTES * 23 + 2); } @Override @@ -45,6 +45,7 @@ protected void setupBufferState(VertexArrayBuilder builder) { builder.setVertexAttribute(6, 2, 2, VertexArrayBuilder.DataType.FLOAT, false, Float.BYTES * 19); // size builder.setVertexAttribute(7, 2, 1, VertexArrayBuilder.DataType.UNSIGNED_SHORT, true, Float.BYTES * 21); // angle builder.setVertexAttribute(8, 2, 1, VertexArrayBuilder.DataType.FLOAT, false, Float.BYTES * 21 + 2); // distance + builder.setVertexAttribute(9, 2, 1, VertexArrayBuilder.DataType.FLOAT, false, Float.BYTES * 21 + 2 + Float.BYTES); } @Override diff --git a/common/src/main/java/foundry/veil/impl/client/render/light/InstancedPointLightRenderer.java b/common/src/main/java/foundry/veil/impl/client/render/light/InstancedPointLightRenderer.java index d150d44a3..c781d945b 100644 --- a/common/src/main/java/foundry/veil/impl/client/render/light/InstancedPointLightRenderer.java +++ b/common/src/main/java/foundry/veil/impl/client/render/light/InstancedPointLightRenderer.java @@ -25,7 +25,7 @@ public class InstancedPointLightRenderer extends InstancedLightRenderer Date: Sun, 1 Mar 2026 11:14:38 +0100 Subject: [PATCH 05/14] refactor(light): cleaned up the implementation and fixed shadow updates --- .../api/client/render/ext/VeilMultiBind.java | 6 + .../client/render/light/VoxelShadowGrid.java | 449 +++++++++++++----- .../client/PipelineLevelRendererMixin.java | 8 + .../main/resources/veil.pipeline.mixins.json | 1 - 4 files changed, 345 insertions(+), 119 deletions(-) diff --git a/common/src/main/java/foundry/veil/api/client/render/ext/VeilMultiBind.java b/common/src/main/java/foundry/veil/api/client/render/ext/VeilMultiBind.java index 6a5469b31..9d512b38a 100644 --- a/common/src/main/java/foundry/veil/api/client/render/ext/VeilMultiBind.java +++ b/common/src/main/java/foundry/veil/api/client/render/ext/VeilMultiBind.java @@ -246,4 +246,10 @@ public static VeilMultiBind get() { public static void clearTargetCache() { TARGET_CACHE.clear(); } + + public static void registerTextureTarget(int textureId, int target) { + if (textureId != 0) { + TARGET_CACHE.put(textureId, target); + } + } } diff --git a/common/src/main/java/foundry/veil/impl/client/render/light/VoxelShadowGrid.java b/common/src/main/java/foundry/veil/impl/client/render/light/VoxelShadowGrid.java index 19e20cb27..ca957126c 100644 --- a/common/src/main/java/foundry/veil/impl/client/render/light/VoxelShadowGrid.java +++ b/common/src/main/java/foundry/veil/impl/client/render/light/VoxelShadowGrid.java @@ -2,49 +2,64 @@ import com.mojang.blaze3d.systems.RenderSystem; import foundry.veil.Veil; +import foundry.veil.api.client.registry.LightTypeRegistry; import foundry.veil.api.client.render.VeilRenderSystem; +import foundry.veil.api.client.render.ext.VeilMultiBind; +import foundry.veil.api.client.render.light.data.AreaLightData; +import foundry.veil.api.client.render.light.data.PointLightData; +import foundry.veil.api.client.render.light.renderer.LightRenderHandle; +import foundry.veil.api.client.render.light.renderer.LightRenderer; import foundry.veil.api.client.render.shader.program.ShaderProgram; +import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue; +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; import net.minecraft.client.Minecraft; import net.minecraft.client.multiplayer.ClientLevel; import net.minecraft.core.BlockPos; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.level.Level; -import net.minecraft.world.level.block.*; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.phys.Vec3; import org.lwjgl.system.MemoryUtil; import java.nio.ByteBuffer; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; +import java.util.Objects; import static org.lwjgl.opengl.GL11C.*; import static org.lwjgl.opengl.GL12C.*; +import static org.lwjgl.opengl.GL30C.GL_R8; public final class VoxelShadowGrid { public static final int GRID_SIZE = 64; private static final int HALF = GRID_SIZE / 2; - private static final double REBUILD_THRESHOLD_SQ = 12.0 * 12.0; + private static final int GRID_VOLUME = GRID_SIZE * GRID_SIZE * GRID_SIZE; + private static final int SLICE_AREA = GRID_SIZE * GRID_SIZE; + + private static final int MAX_SLICE_UPDATES_PER_FRAME = 2; + private static final long BUILD_BUDGET_NS = 2_000_000L; + private static final int MAX_DIRTY_UPDATES_PER_FRAME = 512; + private static final int MAX_DIRTY_BACKLOG = 16384; private static final ResourceLocation POINT_SHADER = Veil.veilPath("light/point"); private static final ResourceLocation AREA_SHADER = Veil.veilPath("light/area"); private static int textureId; - private static Vec3 gridCenter; - private static volatile int generation; - private static volatile Vec3 pendingCenter; - private static volatile ResourceKey pendingDimension; - private static volatile ByteBuffer pendingBuffer; - private static volatile boolean rebuilding; + private static ResourceKey gridDimension; + private static int originX, originY, originZ; + private static ByteBuffer gridBuffer; + + private static ResourceKey buildDimension; + private static int buildOriginX, buildOriginY, buildOriginZ; + private static int buildIndex; + private static ByteBuffer buildBuffer; - private static final ExecutorService EXECUTOR = Executors.newSingleThreadExecutor(r -> { - Thread t = new Thread(r, "veil-voxel-shadow-grid"); - t.setDaemon(true); - return t; - }); + private static final Object DIRTY_LOCK = new Object(); + private static final LongArrayFIFOQueue DIRTY_QUEUE = new LongArrayFIFOQueue(); + private static final LongOpenHashSet DIRTY_SET = new LongOpenHashSet(); + private static final long[] DRAIN_SCRATCH = new long[MAX_DIRTY_UPDATES_PER_FRAME]; + private static boolean rebuildRequested; private VoxelShadowGrid() { } @@ -59,150 +74,340 @@ public static void beforeRenderLights() { } ensureTexture(); - uploadPending(level); - queueRebuildIfNeeded(level, client.gameRenderer.getMainCamera().getPosition()); - pushUniforms(); + + if (gridDimension != null && !Objects.equals(gridDimension, level.dimension())) { + clearLevel(); + } + + Vec3 cameraPos = client.gameRenderer.getMainCamera().getPosition(); + int cx = (int) Math.floor(cameraPos.x); + int cy = (int) Math.floor(cameraPos.y); + int cz = (int) Math.floor(cameraPos.z); + + if (hasOccludedLights()) { + if (rebuildRequested) { + rebuildRequested = false; + clearDirty(); + startFullBuild(level, cx, cy, cz); + } else if (buildBuffer != null) { + int maxDelta = Math.max( + Math.abs(cx - (buildOriginX + HALF)), + Math.max(Math.abs(cy - (buildOriginY + HALF)), Math.abs(cz - (buildOriginZ + HALF))) + ); + if (!Objects.equals(buildDimension, level.dimension()) || maxDelta >= HALF) { + startFullBuild(level, cx, cy, cz); + } + } else if (gridBuffer == null) { + startFullBuild(level, cx, cy, cz); + } + + if (buildBuffer != null) { + continueFullBuild(level); + } else { + shiftTowards(level, cx, cy, cz); + } + } + + if (applyDirtyUpdates(level)) { + uploadBuffer(gridBuffer); + } + + pushUniforms(level, cx, cy, cz); + } + + public static void markBlockDirty(BlockPos pos) { + long packed = pos.asLong(); + synchronized (DIRTY_LOCK) { + if (!DIRTY_SET.add(packed)) { + return; + } + DIRTY_QUEUE.enqueue(packed); + if (DIRTY_QUEUE.size() > MAX_DIRTY_BACKLOG) { + rebuildRequested = true; + clearDirty(); + } + } } public static void clearLevel() { RenderSystem.assertOnRenderThreadOrInit(); - generation++; - gridCenter = null; - pendingCenter = null; - pendingDimension = null; - rebuilding = false; + gridDimension = null; + if (gridBuffer != null) { + MemoryUtil.memFree(gridBuffer); + gridBuffer = null; + } - ByteBuffer buffer = pendingBuffer; - pendingBuffer = null; - if (buffer != null) { - MemoryUtil.memFree(buffer); + buildDimension = null; + buildIndex = 0; + if (buildBuffer != null) { + MemoryUtil.memFree(buildBuffer); + buildBuffer = null; } + + clearDirty(); } public static void close() { RenderSystem.assertOnRenderThreadOrInit(); - clearLevel(); - if (textureId != 0) { glDeleteTextures(textureId); textureId = 0; } + } - EXECUTOR.shutdownNow(); + private static void clearDirty() { + synchronized (DIRTY_LOCK) { + DIRTY_QUEUE.clear(); + DIRTY_SET.clear(); + } } - private static void queueRebuildIfNeeded(ClientLevel level, Vec3 cameraPos) { - Vec3 center = gridCenter; - if (center != null && cameraPos.distanceToSqr(center) <= REBUILD_THRESHOLD_SQ) { + private static void startFullBuild(ClientLevel level, int cx, int cy, int cz) { + buildDimension = level.dimension(); + buildOriginX = cx - HALF; + buildOriginY = cy - HALF; + buildOriginZ = cz - HALF; + buildIndex = 0; + if (buildBuffer == null) { + buildBuffer = MemoryUtil.memAlloc(GRID_VOLUME); + } + } + + private static void continueFullBuild(ClientLevel level) { + if (!Objects.equals(buildDimension, level.dimension())) { + MemoryUtil.memFree(buildBuffer); + buildBuffer = null; + buildDimension = null; + buildIndex = 0; return; } - if (rebuilding) { + long deadline = System.nanoTime() + BUILD_BUDGET_NS; + BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); + while (buildIndex < GRID_VOLUME && System.nanoTime() < deadline) { + int lx = buildIndex & 63; + int ly = (buildIndex >> 6) & 63; + int lz = buildIndex >> 12; + pos.set(buildOriginX + lx, buildOriginY + ly, buildOriginZ + lz); + BlockState state = level.getBlockState(pos); + buildBuffer.put(buildIndex, voxelOccupancy(level, pos, state)); + buildIndex++; + } + + if (buildIndex < GRID_VOLUME) { return; } - rebuilding = true; - pendingCenter = cameraPos; - pendingDimension = level.dimension(); - int capturedGeneration = generation; + if (gridBuffer != null) { + MemoryUtil.memFree(gridBuffer); + } + gridBuffer = buildBuffer; + gridDimension = buildDimension; + originX = buildOriginX; + originY = buildOriginY; + originZ = buildOriginZ; - int cx = (int) Math.floor(cameraPos.x); - int cy = (int) Math.floor(cameraPos.y); - int cz = (int) Math.floor(cameraPos.z); + buildBuffer = null; + buildDimension = null; + buildIndex = 0; - ClientLevel capturedLevel = level; - ResourceKey capturedDimension = level.dimension(); - EXECUTOR.submit(() -> { - try { - ByteBuffer buffer = buildBuffer(capturedLevel, cx, cy, cz); - if (generation != capturedGeneration) { - MemoryUtil.memFree(buffer); - return; - } - pendingBuffer = buffer; - pendingDimension = capturedDimension; - } catch (Throwable t) { - rebuilding = false; - } - }); + uploadBuffer(gridBuffer); } - private static void uploadPending(ClientLevel level) { - ByteBuffer buffer = pendingBuffer; - if (buffer == null) { + private static void shiftTowards(ClientLevel level, int cx, int cy, int cz) { + if (gridBuffer == null || !Objects.equals(gridDimension, level.dimension())) { return; } - pendingBuffer = null; - rebuilding = false; + int dx = cx - (originX + HALF); + int dy = cy - (originY + HALF); + int dz = cz - (originZ + HALF); - if (pendingDimension != null && pendingDimension != level.dimension()) { - MemoryUtil.memFree(buffer); - pendingCenter = null; - pendingDimension = null; + if (Math.max(Math.abs(dx), Math.max(Math.abs(dy), Math.abs(dz))) >= HALF) { + startFullBuild(level, cx, cy, cz); return; } - uploadBuffer(buffer); - MemoryUtil.memFree(buffer); - gridCenter = pendingCenter; - pendingCenter = null; - pendingDimension = null; + int steps = 0; + boolean changed = false; + while (steps < MAX_SLICE_UPDATES_PER_FRAME && (dx != 0 || dy != 0 || dz != 0)) { + int ax = Math.abs(dx), ay = Math.abs(dy), az = Math.abs(dz); + + if (dx != 0 && ax >= ay && ax >= az) { + if (dx > 0) { shiftXPositive(level); dx--; } + else { shiftXNegative(level); dx++; } + } else if (dz != 0 && az >= ay) { + if (dz > 0) { shiftZPositive(level); dz--; } + else { shiftZNegative(level); dz++; } + } else if (dy != 0) { + if (dy > 0) { shiftYPositive(level); dy--; } + else { shiftYNegative(level); dy++; } + } else { + break; + } + + changed = true; + steps++; + } + + if (changed) { + uploadBuffer(gridBuffer); + } } - private static ByteBuffer buildBuffer(ClientLevel level, int cx, int cy, int cz) { - int total = GRID_SIZE * GRID_SIZE * GRID_SIZE; - ByteBuffer buffer = MemoryUtil.memAlloc(total); + private static boolean applyDirtyUpdates(ClientLevel level) { + if (gridBuffer == null && buildBuffer == null) { + clearDirty(); + return false; + } + + int toDrain; + synchronized (DIRTY_LOCK) { + toDrain = Math.min(DIRTY_QUEUE.size(), MAX_DIRTY_UPDATES_PER_FRAME); + for (int i = 0; i < toDrain; i++) { + DRAIN_SCRATCH[i] = DIRTY_QUEUE.dequeueLong(); + DIRTY_SET.remove(DRAIN_SCRATCH[i]); + } + } + boolean updatedGrid = false; BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); + for (int i = 0; i < toDrain; i++) { + long packed = DRAIN_SCRATCH[i]; + int x = BlockPos.getX(packed); + int y = BlockPos.getY(packed); + int z = BlockPos.getZ(packed); + pos.set(x, y, z); + byte occupancy = voxelOccupancy(level, pos, level.getBlockState(pos)); + + if (buildBuffer != null && Objects.equals(buildDimension, level.dimension())) { + int bx = x - buildOriginX, by = y - buildOriginY, bz = z - buildOriginZ; + if ((bx | by | bz) >= 0 && bx < GRID_SIZE && by < GRID_SIZE && bz < GRID_SIZE) { + buildBuffer.put(bx + by * GRID_SIZE + bz * SLICE_AREA, occupancy); + } + } + + if (gridBuffer != null && Objects.equals(gridDimension, level.dimension())) { + int gx = x - originX, gy = y - originY, gz = z - originZ; + if ((gx | gy | gz) >= 0 && gx < GRID_SIZE && gy < GRID_SIZE && gz < GRID_SIZE) { + gridBuffer.put(gx + gy * GRID_SIZE + gz * SLICE_AREA, occupancy); + updatedGrid = true; + } + } + } + + return updatedGrid; + } + + private static void shiftXPositive(ClientLevel level) { + originX++; + long base = MemoryUtil.memAddress(gridBuffer); for (int z = 0; z < GRID_SIZE; z++) { for (int y = 0; y < GRID_SIZE; y++) { - for (int x = 0; x < GRID_SIZE; x++) { - pos.set(cx - HALF + x, cy - HALF + y, cz - HALF + z); - BlockState state = level.getBlockState(pos); - buffer.put(x + y * GRID_SIZE + z * GRID_SIZE * GRID_SIZE, voxelOccupancy(level, pos, state)); - } + long row = base + (long) z * SLICE_AREA + (long) y * GRID_SIZE; + MemoryUtil.memCopy(row + 1, row, GRID_SIZE - 1); } } + fillSliceX(level, GRID_SIZE - 1, originX + GRID_SIZE - 1, base); + } - buffer.rewind(); - return buffer; + private static void shiftXNegative(ClientLevel level) { + originX--; + long base = MemoryUtil.memAddress(gridBuffer); + for (int z = 0; z < GRID_SIZE; z++) { + for (int y = 0; y < GRID_SIZE; y++) { + long row = base + (long) z * SLICE_AREA + (long) y * GRID_SIZE; + MemoryUtil.memCopy(row, row + 1, GRID_SIZE - 1); + } + } + fillSliceX(level, 0, originX, base); } - private static byte voxelOccupancy(ClientLevel level, BlockPos pos, BlockState state) { - if (state.isAir()) { - return 0; + private static void fillSliceX(ClientLevel level, int writeX, int worldX, long base) { + BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); + for (int z = 0; z < GRID_SIZE; z++) { + for (int y = 0; y < GRID_SIZE; y++) { + pos.set(worldX, originY + y, originZ + z); + BlockState state = level.getBlockState(pos); + MemoryUtil.memPutByte(base + (long) z * SLICE_AREA + (long) y * GRID_SIZE + writeX, voxelOccupancy(level, pos, state)); + } } + } - if (!state.getFluidState().isEmpty()) { - return 0; + private static void shiftYPositive(ClientLevel level) { + originY++; + long base = MemoryUtil.memAddress(gridBuffer); + for (int z = 0; z < GRID_SIZE; z++) { + long plane = base + (long) z * SLICE_AREA; + MemoryUtil.memCopy(plane + GRID_SIZE, plane, (long) GRID_SIZE * (GRID_SIZE - 1)); } + fillSliceY(level, GRID_SIZE - 1, originY + GRID_SIZE - 1, base); + } - Block block = state.getBlock(); - if (block instanceof SlabBlock) return 0; - if (block instanceof StairBlock) return 0; - if (block instanceof WallBlock) return 0; - if (block instanceof FenceBlock) return 0; - if (block instanceof FenceGateBlock) return 0; - if (block instanceof CarpetBlock) return 0; - if (block instanceof IronBarsBlock) return 0; - if (block instanceof DoorBlock) return 0; - if (block instanceof TrapDoorBlock) return 0; - if (block instanceof LeavesBlock) return 0; - if (block instanceof LiquidBlock) return 0; + private static void shiftYNegative(ClientLevel level) { + originY--; + long base = MemoryUtil.memAddress(gridBuffer); + for (int z = 0; z < GRID_SIZE; z++) { + long plane = base + (long) z * SLICE_AREA; + MemoryUtil.memCopy(plane, plane + GRID_SIZE, (long) GRID_SIZE * (GRID_SIZE - 1)); + } + fillSliceY(level, 0, originY, base); + } - if (!state.canOcclude()) { - return 0; + private static void fillSliceY(ClientLevel level, int writeY, int worldY, long base) { + BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); + for (int z = 0; z < GRID_SIZE; z++) { + long row = base + (long) z * SLICE_AREA + (long) writeY * GRID_SIZE; + for (int x = 0; x < GRID_SIZE; x++) { + pos.set(originX + x, worldY, originZ + z); + BlockState state = level.getBlockState(pos); + MemoryUtil.memPutByte(row + x, voxelOccupancy(level, pos, state)); + } } + } + + private static void shiftZPositive(ClientLevel level) { + originZ++; + long base = MemoryUtil.memAddress(gridBuffer); + MemoryUtil.memCopy(base + SLICE_AREA, base, (long) SLICE_AREA * (GRID_SIZE - 1)); + fillSliceZ(level, GRID_SIZE - 1, originZ + GRID_SIZE - 1, base); + } + + private static void shiftZNegative(ClientLevel level) { + originZ--; + long base = MemoryUtil.memAddress(gridBuffer); + MemoryUtil.memCopy(base, base + SLICE_AREA, (long) SLICE_AREA * (GRID_SIZE - 1)); + fillSliceZ(level, 0, originZ, base); + } + private static void fillSliceZ(ClientLevel level, int writeZ, int worldZ, long base) { + BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); + long plane = base + (long) writeZ * SLICE_AREA; + for (int y = 0; y < GRID_SIZE; y++) { + long row = plane + (long) y * GRID_SIZE; + for (int x = 0; x < GRID_SIZE; x++) { + pos.set(originX + x, originY + y, worldZ); + BlockState state = level.getBlockState(pos); + MemoryUtil.memPutByte(row + x, voxelOccupancy(level, pos, state)); + } + } + } + + private static byte voxelOccupancy(ClientLevel level, BlockPos pos, BlockState state) { + if (!state.canOcclude()) return 0; + if (!state.getFluidState().isEmpty()) return 0; return state.isSolidRender(level, pos) ? (byte) 0xFF : 0; } private static void uploadBuffer(ByteBuffer buffer) { + if (buffer == null) { + return; + } + buffer.rewind(); glBindTexture(GL_TEXTURE_3D, textureId); - glTexImage3D(GL_TEXTURE_3D, 0, GL_R8, GRID_SIZE, GRID_SIZE, GRID_SIZE, 0, GL_RED, GL_UNSIGNED_BYTE, buffer); + glTexSubImage3D(GL_TEXTURE_3D, 0, 0, 0, 0, GRID_SIZE, GRID_SIZE, GRID_SIZE, GL_RED, GL_UNSIGNED_BYTE, buffer); glBindTexture(GL_TEXTURE_3D, 0); } @@ -210,44 +415,52 @@ private static void ensureTexture() { if (textureId != 0) { return; } - textureId = glGenTextures(); + VeilMultiBind.registerTextureTarget(textureId, GL_TEXTURE_3D); glBindTexture(GL_TEXTURE_3D, textureId); glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); - - int total = GRID_SIZE * GRID_SIZE * GRID_SIZE; - ByteBuffer zeros = MemoryUtil.memCalloc(total); + ByteBuffer zeros = MemoryUtil.memCalloc(GRID_VOLUME); glTexImage3D(GL_TEXTURE_3D, 0, GL_R8, GRID_SIZE, GRID_SIZE, GRID_SIZE, 0, GL_RED, GL_UNSIGNED_BYTE, zeros); MemoryUtil.memFree(zeros); - glBindTexture(GL_TEXTURE_3D, 0); } - private static void pushUniforms() { - Vec3 center = gridCenter; - if (center == null) { - center = Minecraft.getInstance().gameRenderer.getMainCamera().getPosition(); + private static boolean hasOccludedLights() { + LightRenderer renderer = VeilRenderSystem.renderer().getLightRenderer(); + for (LightRenderHandle handle : renderer.getLights(LightTypeRegistry.POINT.get())) { + if (handle.getLightData().isOccluded()) return true; } + for (LightRenderHandle handle : renderer.getLights(LightTypeRegistry.AREA.get())) { + if (handle.getLightData().isOccluded()) return true; + } + return false; + } - float originX = (float) (Math.floor(center.x) - HALF); - float originY = (float) (Math.floor(center.y) - HALF); - float originZ = (float) (Math.floor(center.z) - HALF); - - pushUniforms(POINT_SHADER, originX, originY, originZ); - pushUniforms(AREA_SHADER, originX, originY, originZ); + private static void pushUniforms(ClientLevel level, int cx, int cy, int cz) { + int ox, oy, oz; + if (gridBuffer != null && Objects.equals(gridDimension, level.dimension())) { + ox = originX; + oy = originY; + oz = originZ; + } else { + ox = cx - HALF; + oy = cy - HALF; + oz = cz - HALF; + } + pushUniforms(POINT_SHADER, ox, oy, oz); + pushUniforms(AREA_SHADER, ox, oy, oz); } - private static void pushUniforms(ResourceLocation shader, float originX, float originY, float originZ) { + private static void pushUniforms(ResourceLocation shader, int ox, int oy, int oz) { ShaderProgram program = VeilRenderSystem.renderer().getShaderManager().getShader(shader); if (program == null || !program.isValid()) { return; } - program.setSampler("BlockGrid", textureId); - program.getUniformSafe("GridOrigin").setVector(originX, originY, originZ); + program.getUniformSafe("GridOrigin").setVector(ox, oy, oz); } } diff --git a/common/src/main/java/foundry/veil/mixin/pipeline/client/PipelineLevelRendererMixin.java b/common/src/main/java/foundry/veil/mixin/pipeline/client/PipelineLevelRendererMixin.java index ccb4353e0..582e1b568 100644 --- a/common/src/main/java/foundry/veil/mixin/pipeline/client/PipelineLevelRendererMixin.java +++ b/common/src/main/java/foundry/veil/mixin/pipeline/client/PipelineLevelRendererMixin.java @@ -12,6 +12,7 @@ import foundry.veil.api.client.render.VeilLevelPerspectiveRenderer; import foundry.veil.api.client.render.VeilRenderBridge; import foundry.veil.api.client.render.VeilRenderSystem; +import foundry.veil.impl.client.render.light.VoxelShadowGrid; import foundry.veil.api.client.render.framebuffer.AdvancedFbo; import foundry.veil.api.client.render.framebuffer.FramebufferManager; import foundry.veil.api.client.render.framebuffer.FramebufferStack; @@ -33,6 +34,8 @@ import net.minecraft.client.renderer.culling.Frustum; import net.minecraft.core.BlockPos; import net.minecraft.util.profiling.ProfilerFiller; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.phys.Vec3; import org.jetbrains.annotations.Nullable; import org.joml.Matrix4f; @@ -154,6 +157,11 @@ public void initTransparency(CallbackInfo ci) { framebufferManager.setFramebuffer(VeilFramebuffers.CLOUDS_TARGET, VeilRenderBridge.wrap(this.cloudsTarget)); } + @Inject(method = "blockChanged", at = @At("TAIL")) + public void veil$onBlockChanged(BlockGetter level, BlockPos pos, BlockState oldState, BlockState newState, int flags, CallbackInfo ci) { + VoxelShadowGrid.markBlockDirty(pos); + } + @Inject(method = "setLevel", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/SectionOcclusionGraph;waitAndReset(Lnet/minecraft/client/renderer/ViewArea;)V")) public void free(ClientLevel level, CallbackInfo ci) { VeilRenderSystem.clearLevel(); diff --git a/common/src/main/resources/veil.pipeline.mixins.json b/common/src/main/resources/veil.pipeline.mixins.json index 1e220b893..1d7a00b22 100644 --- a/common/src/main/resources/veil.pipeline.mixins.json +++ b/common/src/main/resources/veil.pipeline.mixins.json @@ -29,4 +29,3 @@ "defaultRequire": 1 } } - \ No newline at end of file From 4b0693f98f00f2ab72750040ff67832e4f973b84 Mon Sep 17 00:00:00 2001 From: Meekiavelique Date: Fri, 27 Feb 2026 11:42:09 +0100 Subject: [PATCH 06/14] perf(gl): cache texture targets for multibind --- .../api/client/render/VeilRenderSystem.java | 6 +++++ .../api/client/render/ext/VeilMultiBind.java | 27 ++++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/common/src/main/java/foundry/veil/api/client/render/VeilRenderSystem.java b/common/src/main/java/foundry/veil/api/client/render/VeilRenderSystem.java index c51556457..436efc2af 100644 --- a/common/src/main/java/foundry/veil/api/client/render/VeilRenderSystem.java +++ b/common/src/main/java/foundry/veil/api/client/render/VeilRenderSystem.java @@ -40,6 +40,7 @@ import foundry.veil.impl.client.render.pipeline.VeilShaderBufferCache; import foundry.veil.impl.client.render.profiler.VeilRenderProfilerImpl; import foundry.veil.impl.client.render.shader.program.ShaderProgramImpl; +import foundry.veil.impl.client.render.light.VoxelShadowGrid; import foundry.veil.mixin.pipeline.accessor.PipelineBufferSourceAccessor; import foundry.veil.platform.VeilEventPlatform; import net.minecraft.Util; @@ -1241,6 +1242,8 @@ public static void close() { if (renderer != null) { renderer.free(); } + VoxelShadowGrid.close(); + VeilMultiBind.clearTargetCache(); glDeleteVertexArrays(screenQuadVao); MemoryUtil.memFree(emptySamplers); SHADER_BUFFER_CACHE.free(); @@ -1277,6 +1280,8 @@ public static boolean drawLights(ProfilerFiller profiler, CullFrustum cullFrustu return false; } + VoxelShadowGrid.beforeRenderLights(); + VeilDebug debug = VeilDebug.get(); debug.pushDebugGroup("Veil Draw Lights"); @@ -1322,5 +1327,6 @@ public static void compositeLights(ProfilerFiller profiler) { @ApiStatus.Internal public static void clearLevel() { NecromancerRenderDispatcher.delete(); + VoxelShadowGrid.clearLevel(); } } diff --git a/common/src/main/java/foundry/veil/api/client/render/ext/VeilMultiBind.java b/common/src/main/java/foundry/veil/api/client/render/ext/VeilMultiBind.java index a0fa1b515..6a5469b31 100644 --- a/common/src/main/java/foundry/veil/api/client/render/ext/VeilMultiBind.java +++ b/common/src/main/java/foundry/veil/api/client/render/ext/VeilMultiBind.java @@ -3,6 +3,7 @@ import com.mojang.blaze3d.platform.GlStateManager; import com.mojang.blaze3d.systems.RenderSystem; import foundry.veil.Veil; +import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap; import org.lwjgl.opengl.ARBMultiBind; import org.lwjgl.opengl.GL; import org.lwjgl.opengl.GLCapabilities; @@ -144,10 +145,28 @@ public void bindSamplers(int first, int... samplers) { GL_TEXTURE_2D_MULTISAMPLE_ARRAY, }; + private static final Int2IntOpenHashMap TARGET_CACHE = new Int2IntOpenHashMap(); + private static final int MISSING_TARGET = Integer.MIN_VALUE; + + static { + TARGET_CACHE.defaultReturnValue(MISSING_TARGET); + } + private static int getTarget(int texture) { + if (texture == 0) { + return GL_TEXTURE_2D; + } + + int cached = TARGET_CACHE.get(texture); + if (cached != MISSING_TARGET) { + return cached; + } + GLCapabilities caps = GL.getCapabilities(); if (caps.glGetTextureParameteriv != 0L && caps.OpenGL45) { // Last ditch effort if the platform has the method anyways - return glGetTextureParameteri(texture, GL_TEXTURE_TARGET); + int target = glGetTextureParameteri(texture, GL_TEXTURE_TARGET); + TARGET_CACHE.put(texture, target); + return target; } // Nothing else I can do, so do the dirty hack to figure out the target @@ -161,12 +180,14 @@ private static int getTarget(int texture) { glBindTexture(target, texture); if (glGetError() == GL_NO_ERROR) { glBindTexture(target, old); + TARGET_CACHE.put(texture, target); return target; } glBindTexture(target, old); } // Should never happen + TARGET_CACHE.put(texture, GL_TEXTURE_2D); return GL_TEXTURE_2D; } @@ -221,4 +242,8 @@ public static VeilMultiBind get() { } return multiBind; } + + public static void clearTargetCache() { + TARGET_CACHE.clear(); + } } From b5d4c243f64d44cff000cebaf36b3806d9ac3dd9 Mon Sep 17 00:00:00 2001 From: Meekiavelique Date: Fri, 27 Feb 2026 11:42:40 +0100 Subject: [PATCH 07/14] feat(light): runtime voxel shadow grid upload + binding --- .../client/render/light/VoxelShadowGrid.java | 253 ++++++++++++++++++ 1 file changed, 253 insertions(+) create mode 100644 common/src/main/java/foundry/veil/impl/client/render/light/VoxelShadowGrid.java diff --git a/common/src/main/java/foundry/veil/impl/client/render/light/VoxelShadowGrid.java b/common/src/main/java/foundry/veil/impl/client/render/light/VoxelShadowGrid.java new file mode 100644 index 000000000..19e20cb27 --- /dev/null +++ b/common/src/main/java/foundry/veil/impl/client/render/light/VoxelShadowGrid.java @@ -0,0 +1,253 @@ +package foundry.veil.impl.client.render.light; + +import com.mojang.blaze3d.systems.RenderSystem; +import foundry.veil.Veil; +import foundry.veil.api.client.render.VeilRenderSystem; +import foundry.veil.api.client.render.shader.program.ShaderProgram; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.core.BlockPos; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.*; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.Vec3; +import org.lwjgl.system.MemoryUtil; + +import java.nio.ByteBuffer; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import static org.lwjgl.opengl.GL11C.*; +import static org.lwjgl.opengl.GL12C.*; + +public final class VoxelShadowGrid { + + public static final int GRID_SIZE = 64; + private static final int HALF = GRID_SIZE / 2; + private static final double REBUILD_THRESHOLD_SQ = 12.0 * 12.0; + + private static final ResourceLocation POINT_SHADER = Veil.veilPath("light/point"); + private static final ResourceLocation AREA_SHADER = Veil.veilPath("light/area"); + + private static int textureId; + private static Vec3 gridCenter; + + private static volatile int generation; + private static volatile Vec3 pendingCenter; + private static volatile ResourceKey pendingDimension; + private static volatile ByteBuffer pendingBuffer; + private static volatile boolean rebuilding; + + private static final ExecutorService EXECUTOR = Executors.newSingleThreadExecutor(r -> { + Thread t = new Thread(r, "veil-voxel-shadow-grid"); + t.setDaemon(true); + return t; + }); + + private VoxelShadowGrid() { + } + + public static void beforeRenderLights() { + RenderSystem.assertOnRenderThread(); + + Minecraft client = Minecraft.getInstance(); + ClientLevel level = client.level; + if (level == null) { + return; + } + + ensureTexture(); + uploadPending(level); + queueRebuildIfNeeded(level, client.gameRenderer.getMainCamera().getPosition()); + pushUniforms(); + } + + public static void clearLevel() { + RenderSystem.assertOnRenderThreadOrInit(); + + generation++; + gridCenter = null; + pendingCenter = null; + pendingDimension = null; + rebuilding = false; + + ByteBuffer buffer = pendingBuffer; + pendingBuffer = null; + if (buffer != null) { + MemoryUtil.memFree(buffer); + } + } + + public static void close() { + RenderSystem.assertOnRenderThreadOrInit(); + + clearLevel(); + + if (textureId != 0) { + glDeleteTextures(textureId); + textureId = 0; + } + + EXECUTOR.shutdownNow(); + } + + private static void queueRebuildIfNeeded(ClientLevel level, Vec3 cameraPos) { + Vec3 center = gridCenter; + if (center != null && cameraPos.distanceToSqr(center) <= REBUILD_THRESHOLD_SQ) { + return; + } + + if (rebuilding) { + return; + } + + rebuilding = true; + pendingCenter = cameraPos; + pendingDimension = level.dimension(); + int capturedGeneration = generation; + + int cx = (int) Math.floor(cameraPos.x); + int cy = (int) Math.floor(cameraPos.y); + int cz = (int) Math.floor(cameraPos.z); + + ClientLevel capturedLevel = level; + ResourceKey capturedDimension = level.dimension(); + EXECUTOR.submit(() -> { + try { + ByteBuffer buffer = buildBuffer(capturedLevel, cx, cy, cz); + if (generation != capturedGeneration) { + MemoryUtil.memFree(buffer); + return; + } + pendingBuffer = buffer; + pendingDimension = capturedDimension; + } catch (Throwable t) { + rebuilding = false; + } + }); + } + + private static void uploadPending(ClientLevel level) { + ByteBuffer buffer = pendingBuffer; + if (buffer == null) { + return; + } + + pendingBuffer = null; + rebuilding = false; + + if (pendingDimension != null && pendingDimension != level.dimension()) { + MemoryUtil.memFree(buffer); + pendingCenter = null; + pendingDimension = null; + return; + } + + uploadBuffer(buffer); + MemoryUtil.memFree(buffer); + gridCenter = pendingCenter; + pendingCenter = null; + pendingDimension = null; + } + + private static ByteBuffer buildBuffer(ClientLevel level, int cx, int cy, int cz) { + int total = GRID_SIZE * GRID_SIZE * GRID_SIZE; + ByteBuffer buffer = MemoryUtil.memAlloc(total); + + BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); + for (int z = 0; z < GRID_SIZE; z++) { + for (int y = 0; y < GRID_SIZE; y++) { + for (int x = 0; x < GRID_SIZE; x++) { + pos.set(cx - HALF + x, cy - HALF + y, cz - HALF + z); + BlockState state = level.getBlockState(pos); + buffer.put(x + y * GRID_SIZE + z * GRID_SIZE * GRID_SIZE, voxelOccupancy(level, pos, state)); + } + } + } + + buffer.rewind(); + return buffer; + } + + private static byte voxelOccupancy(ClientLevel level, BlockPos pos, BlockState state) { + if (state.isAir()) { + return 0; + } + + if (!state.getFluidState().isEmpty()) { + return 0; + } + + Block block = state.getBlock(); + if (block instanceof SlabBlock) return 0; + if (block instanceof StairBlock) return 0; + if (block instanceof WallBlock) return 0; + if (block instanceof FenceBlock) return 0; + if (block instanceof FenceGateBlock) return 0; + if (block instanceof CarpetBlock) return 0; + if (block instanceof IronBarsBlock) return 0; + if (block instanceof DoorBlock) return 0; + if (block instanceof TrapDoorBlock) return 0; + if (block instanceof LeavesBlock) return 0; + if (block instanceof LiquidBlock) return 0; + + if (!state.canOcclude()) { + return 0; + } + + return state.isSolidRender(level, pos) ? (byte) 0xFF : 0; + } + + private static void uploadBuffer(ByteBuffer buffer) { + glBindTexture(GL_TEXTURE_3D, textureId); + glTexImage3D(GL_TEXTURE_3D, 0, GL_R8, GRID_SIZE, GRID_SIZE, GRID_SIZE, 0, GL_RED, GL_UNSIGNED_BYTE, buffer); + glBindTexture(GL_TEXTURE_3D, 0); + } + + private static void ensureTexture() { + if (textureId != 0) { + return; + } + + textureId = glGenTextures(); + glBindTexture(GL_TEXTURE_3D, textureId); + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); + + int total = GRID_SIZE * GRID_SIZE * GRID_SIZE; + ByteBuffer zeros = MemoryUtil.memCalloc(total); + glTexImage3D(GL_TEXTURE_3D, 0, GL_R8, GRID_SIZE, GRID_SIZE, GRID_SIZE, 0, GL_RED, GL_UNSIGNED_BYTE, zeros); + MemoryUtil.memFree(zeros); + + glBindTexture(GL_TEXTURE_3D, 0); + } + + private static void pushUniforms() { + Vec3 center = gridCenter; + if (center == null) { + center = Minecraft.getInstance().gameRenderer.getMainCamera().getPosition(); + } + + float originX = (float) (Math.floor(center.x) - HALF); + float originY = (float) (Math.floor(center.y) - HALF); + float originZ = (float) (Math.floor(center.z) - HALF); + + pushUniforms(POINT_SHADER, originX, originY, originZ); + pushUniforms(AREA_SHADER, originX, originY, originZ); + } + + private static void pushUniforms(ResourceLocation shader, float originX, float originY, float originZ) { + ShaderProgram program = VeilRenderSystem.renderer().getShaderManager().getShader(shader); + if (program == null || !program.isValid()) { + return; + } + + program.setSampler("BlockGrid", textureId); + program.getUniformSafe("GridOrigin").setVector(originX, originY, originZ); + } +} From e195f2ce673705ea08309dd788cad59a32892424 Mon Sep 17 00:00:00 2001 From: Meekiavelique Date: Fri, 27 Feb 2026 11:43:15 +0100 Subject: [PATCH 08/14] feat(light): voxelshadow DDA occlusion in point/area shaders --- .../shaders/include/voxel_shadow.glsl | 47 +++++++++++++++++++ .../pinwheel/shaders/program/light/area.fsh | 7 ++- .../pinwheel/shaders/program/light/point.fsh | 8 +++- 3 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 common/src/main/resources/assets/veil/pinwheel/shaders/include/voxel_shadow.glsl diff --git a/common/src/main/resources/assets/veil/pinwheel/shaders/include/voxel_shadow.glsl b/common/src/main/resources/assets/veil/pinwheel/shaders/include/voxel_shadow.glsl new file mode 100644 index 000000000..a3fd1c816 --- /dev/null +++ b/common/src/main/resources/assets/veil/pinwheel/shaders/include/voxel_shadow.glsl @@ -0,0 +1,47 @@ +uniform sampler3D BlockGrid; +uniform vec3 GridOrigin; + +#define VOXELSHADOW_GRID_SIZE 64 +#define VOXELSHADOW_MAX_STEPS 128 + +float voxelshadowVisibility(vec3 fragPos, vec3 lightPos) { + vec3 startG = fragPos - GridOrigin; + vec3 endG = lightPos - GridOrigin; + vec3 delta = endG - startG; + float rayLen = length(delta); + if (rayLen < 0.001) return 1.0; + + vec3 rDir = delta / rayLen; + ivec3 cell = ivec3(floor(startG)); + ivec3 iStep = ivec3(sign(rDir)); + + vec3 invAbs = 1.0 / max(abs(rDir), vec3(1e-5)); + vec3 tDelta = invAbs; + + vec3 cellF = vec3(cell); + vec3 tMax; + tMax.x = (rDir.x >= 0.0) ? (cellF.x + 1.0 - startG.x) * invAbs.x : (startG.x - cellF.x) * invAbs.x; + tMax.y = (rDir.y >= 0.0) ? (cellF.y + 1.0 - startG.y) * invAbs.y : (startG.y - cellF.y) * invAbs.y; + tMax.z = (rDir.z >= 0.0) ? (cellF.z + 1.0 - startG.z) * invAbs.z : (startG.z - cellF.z) * invAbs.z; + + for (int i = 0; i < VOXELSHADOW_MAX_STEPS; i++) { + if (any(lessThan(cell, ivec3(0))) || any(greaterThanEqual(cell, ivec3(VOXELSHADOW_GRID_SIZE)))) break; + if (i > 0 && texelFetch(BlockGrid, cell, 0).r > 0.5) return 0.0; + + if (tMax.x < tMax.y && tMax.x < tMax.z) { + if (tMax.x >= rayLen) break; + tMax.x += tDelta.x; + cell.x += iStep.x; + } else if (tMax.y < tMax.z) { + if (tMax.y >= rayLen) break; + tMax.y += tDelta.y; + cell.y += iStep.y; + } else { + if (tMax.z >= rayLen) break; + tMax.z += tDelta.z; + cell.z += iStep.z; + } + } + + return 1.0; +} diff --git a/common/src/main/resources/assets/veil/pinwheel/shaders/program/light/area.fsh b/common/src/main/resources/assets/veil/pinwheel/shaders/program/light/area.fsh index 8af99a180..079a01398 100644 --- a/common/src/main/resources/assets/veil/pinwheel/shaders/program/light/area.fsh +++ b/common/src/main/resources/assets/veil/pinwheel/shaders/program/light/area.fsh @@ -2,12 +2,14 @@ #include veil:space_helper #include veil:color_utilities #include veil:light +#include veil:voxel_shadow in mat4 lightMat; in vec3 lightColor; in vec2 size; in float maxAngle; in float maxDistance; +in float occluded; uniform sampler2D AlbedoSampler; uniform sampler2D NormalSampler; @@ -69,10 +71,13 @@ void main() { float angleFalloff = clamp(angle, 0.0, maxAngle) / maxAngle; angleFalloff = smoothstep(1.0, 0.0, angleFalloff); diffuse *= angleFalloff; + if (occluded > 0.5) { + vec3 normalWS = normalize((VeilCamera.IViewMat * vec4(normalVS, 0.0)).xyz); + diffuse *= voxelshadowVisibility(pos + normalWS * 0.01, lightPos); + } float reflectivity = 0.05; vec3 diffuseColor = diffuse * lightColor; fragColor = vec4(albedoColor.rgb * diffuseColor * (1.0 - reflectivity) + diffuseColor * reflectivity, 1.0); } - diff --git a/common/src/main/resources/assets/veil/pinwheel/shaders/program/light/point.fsh b/common/src/main/resources/assets/veil/pinwheel/shaders/program/light/point.fsh index 15c2d8fd6..b76e65599 100644 --- a/common/src/main/resources/assets/veil/pinwheel/shaders/program/light/point.fsh +++ b/common/src/main/resources/assets/veil/pinwheel/shaders/program/light/point.fsh @@ -2,10 +2,12 @@ #include veil:space_helper #include veil:color_utilities #include veil:light +#include veil:voxel_shadow in vec3 lightPos; in vec3 lightColor; in float radius; +in float occluded; uniform sampler2D AlbedoSampler; uniform sampler2D NormalSampler; @@ -31,9 +33,13 @@ void main() { vec3 normalVS = texture(NormalSampler, screenUv).xyz; vec3 lightDirection = normalize((VeilCamera.ViewMat * vec4(offset, 0.0)).xyz); - float diffuse = clamp(0.0, 1.0, dot(normalVS, lightDirection)); + float diffuse = clamp(dot(normalVS, lightDirection), 0.0, 1.0); diffuse = (diffuse + MINECRAFT_AMBIENT_LIGHT) / (1.0 + MINECRAFT_AMBIENT_LIGHT); diffuse *= attenuate_no_cusp(length(offset), radius); + if (occluded > 0.5) { + vec3 normalWS = normalize((VeilCamera.IViewMat * vec4(normalVS, 0.0)).xyz); + diffuse *= voxelshadowVisibility(pos + normalWS * 0.01, lightPos); + } float reflectivity = 0.05; vec3 diffuseColor = diffuse * lightColor; From 8c63b3dfc6e1b0bbe1d14db0fa7a2d31f0a6b55c Mon Sep 17 00:00:00 2001 From: Meekiavelique Date: Fri, 27 Feb 2026 11:48:46 +0100 Subject: [PATCH 09/14] feat(light): add per-light occlusion toggle --- .../client/render/light/data/AreaLightData.java | 17 +++++++++++++++++ .../render/light/data/PointLightData.java | 17 +++++++++++++++++ .../client/render/light/AreaLightRenderer.java | 3 ++- .../light/InstancedPointLightRenderer.java | 3 ++- .../pinwheel/shaders/program/light/area.vsh | 3 +++ .../pinwheel/shaders/program/light/point.vsh | 3 +++ 6 files changed, 44 insertions(+), 2 deletions(-) diff --git a/common/src/main/java/foundry/veil/api/client/render/light/data/AreaLightData.java b/common/src/main/java/foundry/veil/api/client/render/light/data/AreaLightData.java index 85ecec04d..ced4702ab 100644 --- a/common/src/main/java/foundry/veil/api/client/render/light/data/AreaLightData.java +++ b/common/src/main/java/foundry/veil/api/client/render/light/data/AreaLightData.java @@ -31,6 +31,7 @@ public class AreaLightData extends LightData implements InstancedLightData, Edit protected float angle; protected float distance; + protected boolean occluded; public AreaLightData() { this.matrix = new Matrix4d(); @@ -41,6 +42,16 @@ public AreaLightData() { this.angle = (float) Math.toRadians(45); this.distance = 1.0F; + this.occluded = false; + } + + public boolean isOccluded() { + return this.occluded; + } + + public AreaLightData setOccluded(boolean occluded) { + this.occluded = occluded; + return this; } protected void updateMatrix() { @@ -163,6 +174,7 @@ public void store(ByteBuffer buffer) { buffer.putShort((short) Mth.clamp((int) (this.angle * MAX_ANGLE_SIZE), 0, 65535)); buffer.putFloat(this.distance); + buffer.putFloat(this.occluded ? 1.0F : 0.0F); } @Override @@ -201,6 +213,7 @@ public void renderImGuiAttributes() { float[] editAngle = new float[]{this.angle}; float[] editDistance = new float[]{this.distance}; + imgui.type.ImBoolean editOccluded = new imgui.type.ImBoolean(this.occluded); if (ImGui.dragFloat2("size", editSize, 0.02F, 0.0001F)) { this.setSize(editSize[0], editSize[1]); @@ -250,5 +263,9 @@ public void renderImGuiAttributes() { if (ImGui.dragScalar("distance", editDistance, 0.02F, 0.0F)) { this.setDistance(editDistance[0]); } + + if (ImGui.checkbox("occluded", editOccluded)) { + this.occluded = editOccluded.get(); + } } } diff --git a/common/src/main/java/foundry/veil/api/client/render/light/data/PointLightData.java b/common/src/main/java/foundry/veil/api/client/render/light/data/PointLightData.java index dfb45ac3c..1465532a9 100644 --- a/common/src/main/java/foundry/veil/api/client/render/light/data/PointLightData.java +++ b/common/src/main/java/foundry/veil/api/client/render/light/data/PointLightData.java @@ -23,10 +23,21 @@ public class PointLightData extends LightData implements IndirectLightData, Edit protected final Vector3d position; protected float radius; + protected boolean occluded; public PointLightData() { this.position = new Vector3d(); this.radius = 1.0F; + this.occluded = false; + } + + public boolean isOccluded() { + return this.occluded; + } + + public PointLightData setOccluded(boolean occluded) { + this.occluded = occluded; + return this; } @Override @@ -97,6 +108,7 @@ public void store(ByteBuffer buffer) { buffer.putFloat(this.color.green() * this.brightness); buffer.putFloat(this.color.blue() * this.brightness); buffer.putFloat(this.radius); + buffer.putFloat(this.occluded ? 1.0F : 0.0F); } @Override @@ -118,6 +130,7 @@ public void renderImGuiAttributes() { double[] editZ = new double[]{this.position.z()}; float[] editRadius = new float[]{this.radius}; + imgui.type.ImBoolean editOccluded = new imgui.type.ImBoolean(this.occluded); float totalWidth = ImGui.calcItemWidth(); ImGui.pushItemWidth(totalWidth / 3.0F - (ImGui.getStyle().getItemInnerSpacingX() * 0.58F)); @@ -140,5 +153,9 @@ public void renderImGuiAttributes() { if (ImGui.dragScalar("radius", editRadius, 0.02F, 0.0F)) { this.setRadius(editRadius[0]); } + + if (ImGui.checkbox("occluded", editOccluded)) { + this.occluded = editOccluded.get(); + } } } diff --git a/common/src/main/java/foundry/veil/impl/client/render/light/AreaLightRenderer.java b/common/src/main/java/foundry/veil/impl/client/render/light/AreaLightRenderer.java index 80169769c..7813338b0 100644 --- a/common/src/main/java/foundry/veil/impl/client/render/light/AreaLightRenderer.java +++ b/common/src/main/java/foundry/veil/impl/client/render/light/AreaLightRenderer.java @@ -25,7 +25,7 @@ public class AreaLightRenderer extends InstancedLightRenderer { private static final ResourceLocation RENDER_TYPE = Veil.veilPath("light/area"); public AreaLightRenderer() { - super(Float.BYTES * 22 + 2); + super(Float.BYTES * 23 + 2); } @Override @@ -45,6 +45,7 @@ protected void setupBufferState(VertexArrayBuilder builder) { builder.setVertexAttribute(6, 2, 2, VertexArrayBuilder.DataType.FLOAT, false, Float.BYTES * 19); // size builder.setVertexAttribute(7, 2, 1, VertexArrayBuilder.DataType.UNSIGNED_SHORT, true, Float.BYTES * 21); // angle builder.setVertexAttribute(8, 2, 1, VertexArrayBuilder.DataType.FLOAT, false, Float.BYTES * 21 + 2); // distance + builder.setVertexAttribute(9, 2, 1, VertexArrayBuilder.DataType.FLOAT, false, Float.BYTES * 21 + 2 + Float.BYTES); } @Override diff --git a/common/src/main/java/foundry/veil/impl/client/render/light/InstancedPointLightRenderer.java b/common/src/main/java/foundry/veil/impl/client/render/light/InstancedPointLightRenderer.java index d150d44a3..c781d945b 100644 --- a/common/src/main/java/foundry/veil/impl/client/render/light/InstancedPointLightRenderer.java +++ b/common/src/main/java/foundry/veil/impl/client/render/light/InstancedPointLightRenderer.java @@ -25,7 +25,7 @@ public class InstancedPointLightRenderer extends InstancedLightRenderer Date: Sun, 1 Mar 2026 11:14:38 +0100 Subject: [PATCH 10/14] refactor(light): cleaned up the implementation and fixed shadow updates --- .../api/client/render/ext/VeilMultiBind.java | 6 + .../client/render/light/VoxelShadowGrid.java | 449 +++++++++++++----- .../client/PipelineLevelRendererMixin.java | 8 + .../main/resources/veil.pipeline.mixins.json | 1 - 4 files changed, 345 insertions(+), 119 deletions(-) diff --git a/common/src/main/java/foundry/veil/api/client/render/ext/VeilMultiBind.java b/common/src/main/java/foundry/veil/api/client/render/ext/VeilMultiBind.java index 6a5469b31..9d512b38a 100644 --- a/common/src/main/java/foundry/veil/api/client/render/ext/VeilMultiBind.java +++ b/common/src/main/java/foundry/veil/api/client/render/ext/VeilMultiBind.java @@ -246,4 +246,10 @@ public static VeilMultiBind get() { public static void clearTargetCache() { TARGET_CACHE.clear(); } + + public static void registerTextureTarget(int textureId, int target) { + if (textureId != 0) { + TARGET_CACHE.put(textureId, target); + } + } } diff --git a/common/src/main/java/foundry/veil/impl/client/render/light/VoxelShadowGrid.java b/common/src/main/java/foundry/veil/impl/client/render/light/VoxelShadowGrid.java index 19e20cb27..ca957126c 100644 --- a/common/src/main/java/foundry/veil/impl/client/render/light/VoxelShadowGrid.java +++ b/common/src/main/java/foundry/veil/impl/client/render/light/VoxelShadowGrid.java @@ -2,49 +2,64 @@ import com.mojang.blaze3d.systems.RenderSystem; import foundry.veil.Veil; +import foundry.veil.api.client.registry.LightTypeRegistry; import foundry.veil.api.client.render.VeilRenderSystem; +import foundry.veil.api.client.render.ext.VeilMultiBind; +import foundry.veil.api.client.render.light.data.AreaLightData; +import foundry.veil.api.client.render.light.data.PointLightData; +import foundry.veil.api.client.render.light.renderer.LightRenderHandle; +import foundry.veil.api.client.render.light.renderer.LightRenderer; import foundry.veil.api.client.render.shader.program.ShaderProgram; +import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue; +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; import net.minecraft.client.Minecraft; import net.minecraft.client.multiplayer.ClientLevel; import net.minecraft.core.BlockPos; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.level.Level; -import net.minecraft.world.level.block.*; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.phys.Vec3; import org.lwjgl.system.MemoryUtil; import java.nio.ByteBuffer; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; +import java.util.Objects; import static org.lwjgl.opengl.GL11C.*; import static org.lwjgl.opengl.GL12C.*; +import static org.lwjgl.opengl.GL30C.GL_R8; public final class VoxelShadowGrid { public static final int GRID_SIZE = 64; private static final int HALF = GRID_SIZE / 2; - private static final double REBUILD_THRESHOLD_SQ = 12.0 * 12.0; + private static final int GRID_VOLUME = GRID_SIZE * GRID_SIZE * GRID_SIZE; + private static final int SLICE_AREA = GRID_SIZE * GRID_SIZE; + + private static final int MAX_SLICE_UPDATES_PER_FRAME = 2; + private static final long BUILD_BUDGET_NS = 2_000_000L; + private static final int MAX_DIRTY_UPDATES_PER_FRAME = 512; + private static final int MAX_DIRTY_BACKLOG = 16384; private static final ResourceLocation POINT_SHADER = Veil.veilPath("light/point"); private static final ResourceLocation AREA_SHADER = Veil.veilPath("light/area"); private static int textureId; - private static Vec3 gridCenter; - private static volatile int generation; - private static volatile Vec3 pendingCenter; - private static volatile ResourceKey pendingDimension; - private static volatile ByteBuffer pendingBuffer; - private static volatile boolean rebuilding; + private static ResourceKey gridDimension; + private static int originX, originY, originZ; + private static ByteBuffer gridBuffer; + + private static ResourceKey buildDimension; + private static int buildOriginX, buildOriginY, buildOriginZ; + private static int buildIndex; + private static ByteBuffer buildBuffer; - private static final ExecutorService EXECUTOR = Executors.newSingleThreadExecutor(r -> { - Thread t = new Thread(r, "veil-voxel-shadow-grid"); - t.setDaemon(true); - return t; - }); + private static final Object DIRTY_LOCK = new Object(); + private static final LongArrayFIFOQueue DIRTY_QUEUE = new LongArrayFIFOQueue(); + private static final LongOpenHashSet DIRTY_SET = new LongOpenHashSet(); + private static final long[] DRAIN_SCRATCH = new long[MAX_DIRTY_UPDATES_PER_FRAME]; + private static boolean rebuildRequested; private VoxelShadowGrid() { } @@ -59,150 +74,340 @@ public static void beforeRenderLights() { } ensureTexture(); - uploadPending(level); - queueRebuildIfNeeded(level, client.gameRenderer.getMainCamera().getPosition()); - pushUniforms(); + + if (gridDimension != null && !Objects.equals(gridDimension, level.dimension())) { + clearLevel(); + } + + Vec3 cameraPos = client.gameRenderer.getMainCamera().getPosition(); + int cx = (int) Math.floor(cameraPos.x); + int cy = (int) Math.floor(cameraPos.y); + int cz = (int) Math.floor(cameraPos.z); + + if (hasOccludedLights()) { + if (rebuildRequested) { + rebuildRequested = false; + clearDirty(); + startFullBuild(level, cx, cy, cz); + } else if (buildBuffer != null) { + int maxDelta = Math.max( + Math.abs(cx - (buildOriginX + HALF)), + Math.max(Math.abs(cy - (buildOriginY + HALF)), Math.abs(cz - (buildOriginZ + HALF))) + ); + if (!Objects.equals(buildDimension, level.dimension()) || maxDelta >= HALF) { + startFullBuild(level, cx, cy, cz); + } + } else if (gridBuffer == null) { + startFullBuild(level, cx, cy, cz); + } + + if (buildBuffer != null) { + continueFullBuild(level); + } else { + shiftTowards(level, cx, cy, cz); + } + } + + if (applyDirtyUpdates(level)) { + uploadBuffer(gridBuffer); + } + + pushUniforms(level, cx, cy, cz); + } + + public static void markBlockDirty(BlockPos pos) { + long packed = pos.asLong(); + synchronized (DIRTY_LOCK) { + if (!DIRTY_SET.add(packed)) { + return; + } + DIRTY_QUEUE.enqueue(packed); + if (DIRTY_QUEUE.size() > MAX_DIRTY_BACKLOG) { + rebuildRequested = true; + clearDirty(); + } + } } public static void clearLevel() { RenderSystem.assertOnRenderThreadOrInit(); - generation++; - gridCenter = null; - pendingCenter = null; - pendingDimension = null; - rebuilding = false; + gridDimension = null; + if (gridBuffer != null) { + MemoryUtil.memFree(gridBuffer); + gridBuffer = null; + } - ByteBuffer buffer = pendingBuffer; - pendingBuffer = null; - if (buffer != null) { - MemoryUtil.memFree(buffer); + buildDimension = null; + buildIndex = 0; + if (buildBuffer != null) { + MemoryUtil.memFree(buildBuffer); + buildBuffer = null; } + + clearDirty(); } public static void close() { RenderSystem.assertOnRenderThreadOrInit(); - clearLevel(); - if (textureId != 0) { glDeleteTextures(textureId); textureId = 0; } + } - EXECUTOR.shutdownNow(); + private static void clearDirty() { + synchronized (DIRTY_LOCK) { + DIRTY_QUEUE.clear(); + DIRTY_SET.clear(); + } } - private static void queueRebuildIfNeeded(ClientLevel level, Vec3 cameraPos) { - Vec3 center = gridCenter; - if (center != null && cameraPos.distanceToSqr(center) <= REBUILD_THRESHOLD_SQ) { + private static void startFullBuild(ClientLevel level, int cx, int cy, int cz) { + buildDimension = level.dimension(); + buildOriginX = cx - HALF; + buildOriginY = cy - HALF; + buildOriginZ = cz - HALF; + buildIndex = 0; + if (buildBuffer == null) { + buildBuffer = MemoryUtil.memAlloc(GRID_VOLUME); + } + } + + private static void continueFullBuild(ClientLevel level) { + if (!Objects.equals(buildDimension, level.dimension())) { + MemoryUtil.memFree(buildBuffer); + buildBuffer = null; + buildDimension = null; + buildIndex = 0; return; } - if (rebuilding) { + long deadline = System.nanoTime() + BUILD_BUDGET_NS; + BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); + while (buildIndex < GRID_VOLUME && System.nanoTime() < deadline) { + int lx = buildIndex & 63; + int ly = (buildIndex >> 6) & 63; + int lz = buildIndex >> 12; + pos.set(buildOriginX + lx, buildOriginY + ly, buildOriginZ + lz); + BlockState state = level.getBlockState(pos); + buildBuffer.put(buildIndex, voxelOccupancy(level, pos, state)); + buildIndex++; + } + + if (buildIndex < GRID_VOLUME) { return; } - rebuilding = true; - pendingCenter = cameraPos; - pendingDimension = level.dimension(); - int capturedGeneration = generation; + if (gridBuffer != null) { + MemoryUtil.memFree(gridBuffer); + } + gridBuffer = buildBuffer; + gridDimension = buildDimension; + originX = buildOriginX; + originY = buildOriginY; + originZ = buildOriginZ; - int cx = (int) Math.floor(cameraPos.x); - int cy = (int) Math.floor(cameraPos.y); - int cz = (int) Math.floor(cameraPos.z); + buildBuffer = null; + buildDimension = null; + buildIndex = 0; - ClientLevel capturedLevel = level; - ResourceKey capturedDimension = level.dimension(); - EXECUTOR.submit(() -> { - try { - ByteBuffer buffer = buildBuffer(capturedLevel, cx, cy, cz); - if (generation != capturedGeneration) { - MemoryUtil.memFree(buffer); - return; - } - pendingBuffer = buffer; - pendingDimension = capturedDimension; - } catch (Throwable t) { - rebuilding = false; - } - }); + uploadBuffer(gridBuffer); } - private static void uploadPending(ClientLevel level) { - ByteBuffer buffer = pendingBuffer; - if (buffer == null) { + private static void shiftTowards(ClientLevel level, int cx, int cy, int cz) { + if (gridBuffer == null || !Objects.equals(gridDimension, level.dimension())) { return; } - pendingBuffer = null; - rebuilding = false; + int dx = cx - (originX + HALF); + int dy = cy - (originY + HALF); + int dz = cz - (originZ + HALF); - if (pendingDimension != null && pendingDimension != level.dimension()) { - MemoryUtil.memFree(buffer); - pendingCenter = null; - pendingDimension = null; + if (Math.max(Math.abs(dx), Math.max(Math.abs(dy), Math.abs(dz))) >= HALF) { + startFullBuild(level, cx, cy, cz); return; } - uploadBuffer(buffer); - MemoryUtil.memFree(buffer); - gridCenter = pendingCenter; - pendingCenter = null; - pendingDimension = null; + int steps = 0; + boolean changed = false; + while (steps < MAX_SLICE_UPDATES_PER_FRAME && (dx != 0 || dy != 0 || dz != 0)) { + int ax = Math.abs(dx), ay = Math.abs(dy), az = Math.abs(dz); + + if (dx != 0 && ax >= ay && ax >= az) { + if (dx > 0) { shiftXPositive(level); dx--; } + else { shiftXNegative(level); dx++; } + } else if (dz != 0 && az >= ay) { + if (dz > 0) { shiftZPositive(level); dz--; } + else { shiftZNegative(level); dz++; } + } else if (dy != 0) { + if (dy > 0) { shiftYPositive(level); dy--; } + else { shiftYNegative(level); dy++; } + } else { + break; + } + + changed = true; + steps++; + } + + if (changed) { + uploadBuffer(gridBuffer); + } } - private static ByteBuffer buildBuffer(ClientLevel level, int cx, int cy, int cz) { - int total = GRID_SIZE * GRID_SIZE * GRID_SIZE; - ByteBuffer buffer = MemoryUtil.memAlloc(total); + private static boolean applyDirtyUpdates(ClientLevel level) { + if (gridBuffer == null && buildBuffer == null) { + clearDirty(); + return false; + } + + int toDrain; + synchronized (DIRTY_LOCK) { + toDrain = Math.min(DIRTY_QUEUE.size(), MAX_DIRTY_UPDATES_PER_FRAME); + for (int i = 0; i < toDrain; i++) { + DRAIN_SCRATCH[i] = DIRTY_QUEUE.dequeueLong(); + DIRTY_SET.remove(DRAIN_SCRATCH[i]); + } + } + boolean updatedGrid = false; BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); + for (int i = 0; i < toDrain; i++) { + long packed = DRAIN_SCRATCH[i]; + int x = BlockPos.getX(packed); + int y = BlockPos.getY(packed); + int z = BlockPos.getZ(packed); + pos.set(x, y, z); + byte occupancy = voxelOccupancy(level, pos, level.getBlockState(pos)); + + if (buildBuffer != null && Objects.equals(buildDimension, level.dimension())) { + int bx = x - buildOriginX, by = y - buildOriginY, bz = z - buildOriginZ; + if ((bx | by | bz) >= 0 && bx < GRID_SIZE && by < GRID_SIZE && bz < GRID_SIZE) { + buildBuffer.put(bx + by * GRID_SIZE + bz * SLICE_AREA, occupancy); + } + } + + if (gridBuffer != null && Objects.equals(gridDimension, level.dimension())) { + int gx = x - originX, gy = y - originY, gz = z - originZ; + if ((gx | gy | gz) >= 0 && gx < GRID_SIZE && gy < GRID_SIZE && gz < GRID_SIZE) { + gridBuffer.put(gx + gy * GRID_SIZE + gz * SLICE_AREA, occupancy); + updatedGrid = true; + } + } + } + + return updatedGrid; + } + + private static void shiftXPositive(ClientLevel level) { + originX++; + long base = MemoryUtil.memAddress(gridBuffer); for (int z = 0; z < GRID_SIZE; z++) { for (int y = 0; y < GRID_SIZE; y++) { - for (int x = 0; x < GRID_SIZE; x++) { - pos.set(cx - HALF + x, cy - HALF + y, cz - HALF + z); - BlockState state = level.getBlockState(pos); - buffer.put(x + y * GRID_SIZE + z * GRID_SIZE * GRID_SIZE, voxelOccupancy(level, pos, state)); - } + long row = base + (long) z * SLICE_AREA + (long) y * GRID_SIZE; + MemoryUtil.memCopy(row + 1, row, GRID_SIZE - 1); } } + fillSliceX(level, GRID_SIZE - 1, originX + GRID_SIZE - 1, base); + } - buffer.rewind(); - return buffer; + private static void shiftXNegative(ClientLevel level) { + originX--; + long base = MemoryUtil.memAddress(gridBuffer); + for (int z = 0; z < GRID_SIZE; z++) { + for (int y = 0; y < GRID_SIZE; y++) { + long row = base + (long) z * SLICE_AREA + (long) y * GRID_SIZE; + MemoryUtil.memCopy(row, row + 1, GRID_SIZE - 1); + } + } + fillSliceX(level, 0, originX, base); } - private static byte voxelOccupancy(ClientLevel level, BlockPos pos, BlockState state) { - if (state.isAir()) { - return 0; + private static void fillSliceX(ClientLevel level, int writeX, int worldX, long base) { + BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); + for (int z = 0; z < GRID_SIZE; z++) { + for (int y = 0; y < GRID_SIZE; y++) { + pos.set(worldX, originY + y, originZ + z); + BlockState state = level.getBlockState(pos); + MemoryUtil.memPutByte(base + (long) z * SLICE_AREA + (long) y * GRID_SIZE + writeX, voxelOccupancy(level, pos, state)); + } } + } - if (!state.getFluidState().isEmpty()) { - return 0; + private static void shiftYPositive(ClientLevel level) { + originY++; + long base = MemoryUtil.memAddress(gridBuffer); + for (int z = 0; z < GRID_SIZE; z++) { + long plane = base + (long) z * SLICE_AREA; + MemoryUtil.memCopy(plane + GRID_SIZE, plane, (long) GRID_SIZE * (GRID_SIZE - 1)); } + fillSliceY(level, GRID_SIZE - 1, originY + GRID_SIZE - 1, base); + } - Block block = state.getBlock(); - if (block instanceof SlabBlock) return 0; - if (block instanceof StairBlock) return 0; - if (block instanceof WallBlock) return 0; - if (block instanceof FenceBlock) return 0; - if (block instanceof FenceGateBlock) return 0; - if (block instanceof CarpetBlock) return 0; - if (block instanceof IronBarsBlock) return 0; - if (block instanceof DoorBlock) return 0; - if (block instanceof TrapDoorBlock) return 0; - if (block instanceof LeavesBlock) return 0; - if (block instanceof LiquidBlock) return 0; + private static void shiftYNegative(ClientLevel level) { + originY--; + long base = MemoryUtil.memAddress(gridBuffer); + for (int z = 0; z < GRID_SIZE; z++) { + long plane = base + (long) z * SLICE_AREA; + MemoryUtil.memCopy(plane, plane + GRID_SIZE, (long) GRID_SIZE * (GRID_SIZE - 1)); + } + fillSliceY(level, 0, originY, base); + } - if (!state.canOcclude()) { - return 0; + private static void fillSliceY(ClientLevel level, int writeY, int worldY, long base) { + BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); + for (int z = 0; z < GRID_SIZE; z++) { + long row = base + (long) z * SLICE_AREA + (long) writeY * GRID_SIZE; + for (int x = 0; x < GRID_SIZE; x++) { + pos.set(originX + x, worldY, originZ + z); + BlockState state = level.getBlockState(pos); + MemoryUtil.memPutByte(row + x, voxelOccupancy(level, pos, state)); + } } + } + + private static void shiftZPositive(ClientLevel level) { + originZ++; + long base = MemoryUtil.memAddress(gridBuffer); + MemoryUtil.memCopy(base + SLICE_AREA, base, (long) SLICE_AREA * (GRID_SIZE - 1)); + fillSliceZ(level, GRID_SIZE - 1, originZ + GRID_SIZE - 1, base); + } + + private static void shiftZNegative(ClientLevel level) { + originZ--; + long base = MemoryUtil.memAddress(gridBuffer); + MemoryUtil.memCopy(base, base + SLICE_AREA, (long) SLICE_AREA * (GRID_SIZE - 1)); + fillSliceZ(level, 0, originZ, base); + } + private static void fillSliceZ(ClientLevel level, int writeZ, int worldZ, long base) { + BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); + long plane = base + (long) writeZ * SLICE_AREA; + for (int y = 0; y < GRID_SIZE; y++) { + long row = plane + (long) y * GRID_SIZE; + for (int x = 0; x < GRID_SIZE; x++) { + pos.set(originX + x, originY + y, worldZ); + BlockState state = level.getBlockState(pos); + MemoryUtil.memPutByte(row + x, voxelOccupancy(level, pos, state)); + } + } + } + + private static byte voxelOccupancy(ClientLevel level, BlockPos pos, BlockState state) { + if (!state.canOcclude()) return 0; + if (!state.getFluidState().isEmpty()) return 0; return state.isSolidRender(level, pos) ? (byte) 0xFF : 0; } private static void uploadBuffer(ByteBuffer buffer) { + if (buffer == null) { + return; + } + buffer.rewind(); glBindTexture(GL_TEXTURE_3D, textureId); - glTexImage3D(GL_TEXTURE_3D, 0, GL_R8, GRID_SIZE, GRID_SIZE, GRID_SIZE, 0, GL_RED, GL_UNSIGNED_BYTE, buffer); + glTexSubImage3D(GL_TEXTURE_3D, 0, 0, 0, 0, GRID_SIZE, GRID_SIZE, GRID_SIZE, GL_RED, GL_UNSIGNED_BYTE, buffer); glBindTexture(GL_TEXTURE_3D, 0); } @@ -210,44 +415,52 @@ private static void ensureTexture() { if (textureId != 0) { return; } - textureId = glGenTextures(); + VeilMultiBind.registerTextureTarget(textureId, GL_TEXTURE_3D); glBindTexture(GL_TEXTURE_3D, textureId); glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); - - int total = GRID_SIZE * GRID_SIZE * GRID_SIZE; - ByteBuffer zeros = MemoryUtil.memCalloc(total); + ByteBuffer zeros = MemoryUtil.memCalloc(GRID_VOLUME); glTexImage3D(GL_TEXTURE_3D, 0, GL_R8, GRID_SIZE, GRID_SIZE, GRID_SIZE, 0, GL_RED, GL_UNSIGNED_BYTE, zeros); MemoryUtil.memFree(zeros); - glBindTexture(GL_TEXTURE_3D, 0); } - private static void pushUniforms() { - Vec3 center = gridCenter; - if (center == null) { - center = Minecraft.getInstance().gameRenderer.getMainCamera().getPosition(); + private static boolean hasOccludedLights() { + LightRenderer renderer = VeilRenderSystem.renderer().getLightRenderer(); + for (LightRenderHandle handle : renderer.getLights(LightTypeRegistry.POINT.get())) { + if (handle.getLightData().isOccluded()) return true; } + for (LightRenderHandle handle : renderer.getLights(LightTypeRegistry.AREA.get())) { + if (handle.getLightData().isOccluded()) return true; + } + return false; + } - float originX = (float) (Math.floor(center.x) - HALF); - float originY = (float) (Math.floor(center.y) - HALF); - float originZ = (float) (Math.floor(center.z) - HALF); - - pushUniforms(POINT_SHADER, originX, originY, originZ); - pushUniforms(AREA_SHADER, originX, originY, originZ); + private static void pushUniforms(ClientLevel level, int cx, int cy, int cz) { + int ox, oy, oz; + if (gridBuffer != null && Objects.equals(gridDimension, level.dimension())) { + ox = originX; + oy = originY; + oz = originZ; + } else { + ox = cx - HALF; + oy = cy - HALF; + oz = cz - HALF; + } + pushUniforms(POINT_SHADER, ox, oy, oz); + pushUniforms(AREA_SHADER, ox, oy, oz); } - private static void pushUniforms(ResourceLocation shader, float originX, float originY, float originZ) { + private static void pushUniforms(ResourceLocation shader, int ox, int oy, int oz) { ShaderProgram program = VeilRenderSystem.renderer().getShaderManager().getShader(shader); if (program == null || !program.isValid()) { return; } - program.setSampler("BlockGrid", textureId); - program.getUniformSafe("GridOrigin").setVector(originX, originY, originZ); + program.getUniformSafe("GridOrigin").setVector(ox, oy, oz); } } diff --git a/common/src/main/java/foundry/veil/mixin/pipeline/client/PipelineLevelRendererMixin.java b/common/src/main/java/foundry/veil/mixin/pipeline/client/PipelineLevelRendererMixin.java index ccb4353e0..582e1b568 100644 --- a/common/src/main/java/foundry/veil/mixin/pipeline/client/PipelineLevelRendererMixin.java +++ b/common/src/main/java/foundry/veil/mixin/pipeline/client/PipelineLevelRendererMixin.java @@ -12,6 +12,7 @@ import foundry.veil.api.client.render.VeilLevelPerspectiveRenderer; import foundry.veil.api.client.render.VeilRenderBridge; import foundry.veil.api.client.render.VeilRenderSystem; +import foundry.veil.impl.client.render.light.VoxelShadowGrid; import foundry.veil.api.client.render.framebuffer.AdvancedFbo; import foundry.veil.api.client.render.framebuffer.FramebufferManager; import foundry.veil.api.client.render.framebuffer.FramebufferStack; @@ -33,6 +34,8 @@ import net.minecraft.client.renderer.culling.Frustum; import net.minecraft.core.BlockPos; import net.minecraft.util.profiling.ProfilerFiller; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.phys.Vec3; import org.jetbrains.annotations.Nullable; import org.joml.Matrix4f; @@ -154,6 +157,11 @@ public void initTransparency(CallbackInfo ci) { framebufferManager.setFramebuffer(VeilFramebuffers.CLOUDS_TARGET, VeilRenderBridge.wrap(this.cloudsTarget)); } + @Inject(method = "blockChanged", at = @At("TAIL")) + public void veil$onBlockChanged(BlockGetter level, BlockPos pos, BlockState oldState, BlockState newState, int flags, CallbackInfo ci) { + VoxelShadowGrid.markBlockDirty(pos); + } + @Inject(method = "setLevel", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/SectionOcclusionGraph;waitAndReset(Lnet/minecraft/client/renderer/ViewArea;)V")) public void free(ClientLevel level, CallbackInfo ci) { VeilRenderSystem.clearLevel(); diff --git a/common/src/main/resources/veil.pipeline.mixins.json b/common/src/main/resources/veil.pipeline.mixins.json index 1e220b893..1d7a00b22 100644 --- a/common/src/main/resources/veil.pipeline.mixins.json +++ b/common/src/main/resources/veil.pipeline.mixins.json @@ -29,4 +29,3 @@ "defaultRequire": 1 } } - \ No newline at end of file From 0ab5e65a4b0cb7d2a34b711e52ae2783c5543942 Mon Sep 17 00:00:00 2001 From: Meekiavelique Date: Sun, 1 Mar 2026 22:11:04 +0100 Subject: [PATCH 11/14] fix(light): address PR review --- .../foundry/veil/api/client/render/VeilRenderSystem.java | 1 - .../veil/api/client/render/ext/VeilMultiBind.java | 9 --------- .../veil/api/client/render/light/data/AreaLightData.java | 2 +- .../api/client/render/light/data/PointLightData.java | 2 +- .../veil/impl/client/render/light/VoxelShadowGrid.java | 2 -- 5 files changed, 2 insertions(+), 14 deletions(-) diff --git a/common/src/main/java/foundry/veil/api/client/render/VeilRenderSystem.java b/common/src/main/java/foundry/veil/api/client/render/VeilRenderSystem.java index 436efc2af..dd218e19c 100644 --- a/common/src/main/java/foundry/veil/api/client/render/VeilRenderSystem.java +++ b/common/src/main/java/foundry/veil/api/client/render/VeilRenderSystem.java @@ -1243,7 +1243,6 @@ public static void close() { renderer.free(); } VoxelShadowGrid.close(); - VeilMultiBind.clearTargetCache(); glDeleteVertexArrays(screenQuadVao); MemoryUtil.memFree(emptySamplers); SHADER_BUFFER_CACHE.free(); diff --git a/common/src/main/java/foundry/veil/api/client/render/ext/VeilMultiBind.java b/common/src/main/java/foundry/veil/api/client/render/ext/VeilMultiBind.java index 9d512b38a..5d3a945fe 100644 --- a/common/src/main/java/foundry/veil/api/client/render/ext/VeilMultiBind.java +++ b/common/src/main/java/foundry/veil/api/client/render/ext/VeilMultiBind.java @@ -243,13 +243,4 @@ public static VeilMultiBind get() { return multiBind; } - public static void clearTargetCache() { - TARGET_CACHE.clear(); - } - - public static void registerTextureTarget(int textureId, int target) { - if (textureId != 0) { - TARGET_CACHE.put(textureId, target); - } - } } diff --git a/common/src/main/java/foundry/veil/api/client/render/light/data/AreaLightData.java b/common/src/main/java/foundry/veil/api/client/render/light/data/AreaLightData.java index ced4702ab..6ee7889bc 100644 --- a/common/src/main/java/foundry/veil/api/client/render/light/data/AreaLightData.java +++ b/common/src/main/java/foundry/veil/api/client/render/light/data/AreaLightData.java @@ -264,7 +264,7 @@ public void renderImGuiAttributes() { this.setDistance(editDistance[0]); } - if (ImGui.checkbox("occluded", editOccluded)) { + if (ImGui.checkbox("Occluded", editOccluded)) { this.occluded = editOccluded.get(); } } diff --git a/common/src/main/java/foundry/veil/api/client/render/light/data/PointLightData.java b/common/src/main/java/foundry/veil/api/client/render/light/data/PointLightData.java index 1465532a9..f8b563c58 100644 --- a/common/src/main/java/foundry/veil/api/client/render/light/data/PointLightData.java +++ b/common/src/main/java/foundry/veil/api/client/render/light/data/PointLightData.java @@ -154,7 +154,7 @@ public void renderImGuiAttributes() { this.setRadius(editRadius[0]); } - if (ImGui.checkbox("occluded", editOccluded)) { + if (ImGui.checkbox("Occluded", editOccluded)) { this.occluded = editOccluded.get(); } } diff --git a/common/src/main/java/foundry/veil/impl/client/render/light/VoxelShadowGrid.java b/common/src/main/java/foundry/veil/impl/client/render/light/VoxelShadowGrid.java index ca957126c..c08e85b32 100644 --- a/common/src/main/java/foundry/veil/impl/client/render/light/VoxelShadowGrid.java +++ b/common/src/main/java/foundry/veil/impl/client/render/light/VoxelShadowGrid.java @@ -4,7 +4,6 @@ import foundry.veil.Veil; import foundry.veil.api.client.registry.LightTypeRegistry; import foundry.veil.api.client.render.VeilRenderSystem; -import foundry.veil.api.client.render.ext.VeilMultiBind; import foundry.veil.api.client.render.light.data.AreaLightData; import foundry.veil.api.client.render.light.data.PointLightData; import foundry.veil.api.client.render.light.renderer.LightRenderHandle; @@ -416,7 +415,6 @@ private static void ensureTexture() { return; } textureId = glGenTextures(); - VeilMultiBind.registerTextureTarget(textureId, GL_TEXTURE_3D); glBindTexture(GL_TEXTURE_3D, textureId); glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); From adf1c20372b8b0ccd2fd9c260eb74df799e300a2 Mon Sep 17 00:00:00 2001 From: Meekiavelique Date: Sun, 1 Mar 2026 22:13:31 +0100 Subject: [PATCH 12/14] fix(light): fixing conflict --- .../api/client/render/ext/VeilMultiBind.java | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/common/src/main/java/foundry/veil/api/client/render/ext/VeilMultiBind.java b/common/src/main/java/foundry/veil/api/client/render/ext/VeilMultiBind.java index 5d3a945fe..96e6987ae 100644 --- a/common/src/main/java/foundry/veil/api/client/render/ext/VeilMultiBind.java +++ b/common/src/main/java/foundry/veil/api/client/render/ext/VeilMultiBind.java @@ -1,14 +1,16 @@ package foundry.veil.api.client.render.ext; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; import com.mojang.blaze3d.platform.GlStateManager; import com.mojang.blaze3d.systems.RenderSystem; import foundry.veil.Veil; -import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap; import org.lwjgl.opengl.ARBMultiBind; import org.lwjgl.opengl.GL; import org.lwjgl.opengl.GLCapabilities; import java.nio.IntBuffer; +import java.util.concurrent.TimeUnit; import static org.lwjgl.opengl.ARBMultiBind.glBindSamplers; import static org.lwjgl.opengl.ARBMultiBind.glBindTextures; @@ -145,27 +147,21 @@ public void bindSamplers(int first, int... samplers) { GL_TEXTURE_2D_MULTISAMPLE_ARRAY, }; - private static final Int2IntOpenHashMap TARGET_CACHE = new Int2IntOpenHashMap(); - private static final int MISSING_TARGET = Integer.MIN_VALUE; - - static { - TARGET_CACHE.defaultReturnValue(MISSING_TARGET); - } + private static final Cache TEXTURE_TARGET_CACHE = CacheBuilder.newBuilder() + .maximumSize(100) + .expireAfterAccess(10, TimeUnit.SECONDS) + .build(); private static int getTarget(int texture) { - if (texture == 0) { - return GL_TEXTURE_2D; - } - - int cached = TARGET_CACHE.get(texture); - if (cached != MISSING_TARGET) { + Integer cached = TEXTURE_TARGET_CACHE.getIfPresent(texture); + if (cached != null) { return cached; } GLCapabilities caps = GL.getCapabilities(); if (caps.glGetTextureParameteriv != 0L && caps.OpenGL45) { // Last ditch effort if the platform has the method anyways int target = glGetTextureParameteri(texture, GL_TEXTURE_TARGET); - TARGET_CACHE.put(texture, target); + TEXTURE_TARGET_CACHE.put(texture, target); return target; } // Nothing else I can do, so do the dirty hack to figure out the target @@ -179,15 +175,15 @@ private static int getTarget(int texture) { int old = glGetInteger(CHECK_BINDINGS[i]); glBindTexture(target, texture); if (glGetError() == GL_NO_ERROR) { + TEXTURE_TARGET_CACHE.put(texture, target); glBindTexture(target, old); - TARGET_CACHE.put(texture, target); return target; } glBindTexture(target, old); } // Should never happen - TARGET_CACHE.put(texture, GL_TEXTURE_2D); + TEXTURE_TARGET_CACHE.put(texture, GL_TEXTURE_2D); return GL_TEXTURE_2D; } From 4d2a2307c4985b1a24fd947e76e112a3fb324973 Mon Sep 17 00:00:00 2001 From: Meekiavelique Date: Sun, 1 Mar 2026 22:13:31 +0100 Subject: [PATCH 13/14] fix(light): fixing conflict (ohhhh) --- .../api/client/render/ext/VeilMultiBind.java | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/common/src/main/java/foundry/veil/api/client/render/ext/VeilMultiBind.java b/common/src/main/java/foundry/veil/api/client/render/ext/VeilMultiBind.java index 5d3a945fe..96e6987ae 100644 --- a/common/src/main/java/foundry/veil/api/client/render/ext/VeilMultiBind.java +++ b/common/src/main/java/foundry/veil/api/client/render/ext/VeilMultiBind.java @@ -1,14 +1,16 @@ package foundry.veil.api.client.render.ext; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; import com.mojang.blaze3d.platform.GlStateManager; import com.mojang.blaze3d.systems.RenderSystem; import foundry.veil.Veil; -import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap; import org.lwjgl.opengl.ARBMultiBind; import org.lwjgl.opengl.GL; import org.lwjgl.opengl.GLCapabilities; import java.nio.IntBuffer; +import java.util.concurrent.TimeUnit; import static org.lwjgl.opengl.ARBMultiBind.glBindSamplers; import static org.lwjgl.opengl.ARBMultiBind.glBindTextures; @@ -145,27 +147,21 @@ public void bindSamplers(int first, int... samplers) { GL_TEXTURE_2D_MULTISAMPLE_ARRAY, }; - private static final Int2IntOpenHashMap TARGET_CACHE = new Int2IntOpenHashMap(); - private static final int MISSING_TARGET = Integer.MIN_VALUE; - - static { - TARGET_CACHE.defaultReturnValue(MISSING_TARGET); - } + private static final Cache TEXTURE_TARGET_CACHE = CacheBuilder.newBuilder() + .maximumSize(100) + .expireAfterAccess(10, TimeUnit.SECONDS) + .build(); private static int getTarget(int texture) { - if (texture == 0) { - return GL_TEXTURE_2D; - } - - int cached = TARGET_CACHE.get(texture); - if (cached != MISSING_TARGET) { + Integer cached = TEXTURE_TARGET_CACHE.getIfPresent(texture); + if (cached != null) { return cached; } GLCapabilities caps = GL.getCapabilities(); if (caps.glGetTextureParameteriv != 0L && caps.OpenGL45) { // Last ditch effort if the platform has the method anyways int target = glGetTextureParameteri(texture, GL_TEXTURE_TARGET); - TARGET_CACHE.put(texture, target); + TEXTURE_TARGET_CACHE.put(texture, target); return target; } // Nothing else I can do, so do the dirty hack to figure out the target @@ -179,15 +175,15 @@ private static int getTarget(int texture) { int old = glGetInteger(CHECK_BINDINGS[i]); glBindTexture(target, texture); if (glGetError() == GL_NO_ERROR) { + TEXTURE_TARGET_CACHE.put(texture, target); glBindTexture(target, old); - TARGET_CACHE.put(texture, target); return target; } glBindTexture(target, old); } // Should never happen - TARGET_CACHE.put(texture, GL_TEXTURE_2D); + TEXTURE_TARGET_CACHE.put(texture, GL_TEXTURE_2D); return GL_TEXTURE_2D; } From ca504d22b3e504ebf0c1b3da4e7a4f2ce06ee6cf Mon Sep 17 00:00:00 2001 From: Meekiavelique Date: Sun, 1 Mar 2026 22:28:48 +0100 Subject: [PATCH 14/14] fix(light): fixing conflict (ohhhh) --- .../api/client/render/ext/VeilMultiBind.java | 20 +------------------ 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/common/src/main/java/foundry/veil/api/client/render/ext/VeilMultiBind.java b/common/src/main/java/foundry/veil/api/client/render/ext/VeilMultiBind.java index 96e6987ae..a0fa1b515 100644 --- a/common/src/main/java/foundry/veil/api/client/render/ext/VeilMultiBind.java +++ b/common/src/main/java/foundry/veil/api/client/render/ext/VeilMultiBind.java @@ -1,7 +1,5 @@ package foundry.veil.api.client.render.ext; -import com.google.common.cache.Cache; -import com.google.common.cache.CacheBuilder; import com.mojang.blaze3d.platform.GlStateManager; import com.mojang.blaze3d.systems.RenderSystem; import foundry.veil.Veil; @@ -10,7 +8,6 @@ import org.lwjgl.opengl.GLCapabilities; import java.nio.IntBuffer; -import java.util.concurrent.TimeUnit; import static org.lwjgl.opengl.ARBMultiBind.glBindSamplers; import static org.lwjgl.opengl.ARBMultiBind.glBindTextures; @@ -147,22 +144,10 @@ public void bindSamplers(int first, int... samplers) { GL_TEXTURE_2D_MULTISAMPLE_ARRAY, }; - private static final Cache TEXTURE_TARGET_CACHE = CacheBuilder.newBuilder() - .maximumSize(100) - .expireAfterAccess(10, TimeUnit.SECONDS) - .build(); - private static int getTarget(int texture) { - Integer cached = TEXTURE_TARGET_CACHE.getIfPresent(texture); - if (cached != null) { - return cached; - } - GLCapabilities caps = GL.getCapabilities(); if (caps.glGetTextureParameteriv != 0L && caps.OpenGL45) { // Last ditch effort if the platform has the method anyways - int target = glGetTextureParameteri(texture, GL_TEXTURE_TARGET); - TEXTURE_TARGET_CACHE.put(texture, target); - return target; + return glGetTextureParameteri(texture, GL_TEXTURE_TARGET); } // Nothing else I can do, so do the dirty hack to figure out the target @@ -175,7 +160,6 @@ private static int getTarget(int texture) { int old = glGetInteger(CHECK_BINDINGS[i]); glBindTexture(target, texture); if (glGetError() == GL_NO_ERROR) { - TEXTURE_TARGET_CACHE.put(texture, target); glBindTexture(target, old); return target; } @@ -183,7 +167,6 @@ private static int getTarget(int texture) { } // Should never happen - TEXTURE_TARGET_CACHE.put(texture, GL_TEXTURE_2D); return GL_TEXTURE_2D; } @@ -238,5 +221,4 @@ public static VeilMultiBind get() { } return multiBind; } - }