diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java index 41b125d200..9a485447d7 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java @@ -1,6 +1,8 @@ package net.caffeinemc.mods.sodium.client.render.chunk.occlusion; import it.unimi.dsi.fastutil.longs.Long2ReferenceMap; +import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue; +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; import net.caffeinemc.mods.sodium.client.render.viewport.CameraTransform; import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; @@ -17,6 +19,8 @@ public class OcclusionCuller { private final Level level; private final DoubleBufferedQueue queue = new DoubleBufferedQueue<>(); + private LongArrayFIFOQueue initQueue; + private LongOpenHashSet initVisited; public OcclusionCuller(Long2ReferenceMap sections, Level level) { this.sections = sections; @@ -180,10 +184,14 @@ private static int getOutwardDirections(SectionPos origin, RenderSection section } private static boolean isWithinRenderDistance(CameraTransform camera, RenderSection section, float maxDistance) { + return isWithinRenderDistance(camera, section.getOriginX(), section.getOriginY(), section.getOriginZ(), maxDistance); + } + + private static boolean isWithinRenderDistance(CameraTransform camera, int originX, int originY, int originZ, float maxDistance) { // origin point of the chunk's bounding box (in view space) - int ox = section.getOriginX() - camera.intX; - int oy = section.getOriginY() - camera.intY; - int oz = section.getOriginZ() - camera.intZ; + int ox = originX - camera.intX; + int oy = originY - camera.intY; + int oz = originZ - camera.intZ; // coordinates of the point to compare (in view space) // this is the closest point within the bounding box to the center (0, 0, 0) @@ -233,18 +241,16 @@ private void init(Visitor visitor, this.initOutsideWorldHeight(queue, viewport, searchDistance, frame, this.level.getMaxSectionY(), GraphDirection.UP); } else { - this.initWithinWorld(visitor, queue, viewport, useOcclusionCulling, frame); + var originSection = this.sections.get(origin.asLong()); + if (originSection != null) { + this.initAtExistingSection(visitor, queue, originSection, useOcclusionCulling, frame); + } else { + this.initAtNonExistingSection(queue, viewport, searchDistance, frame); + } } } - private void initWithinWorld(Visitor visitor, WriteQueue queue, Viewport viewport, boolean useOcclusionCulling, int frame) { - var origin = viewport.getChunkCoord(); - var section = this.getRenderSection(origin.getX(), origin.getY(), origin.getZ()); - - if (section == null) { - return; - } - + private void initAtExistingSection(Visitor visitor, WriteQueue queue, RenderSection section, boolean useOcclusionCulling, int frame) { section.setLastVisibleFrame(frame); section.setIncomingDirections(GraphDirectionSet.NONE); @@ -264,6 +270,81 @@ private void initWithinWorld(Visitor visitor, WriteQueue queue, V visitNeighbors(queue, section, outgoing, frame); } + private void initAtNonExistingSection(WriteQueue queue, Viewport viewport, float searchDistance, int frame) { + var transform = viewport.getTransform(); + + var origin = viewport.getChunkCoord(); + if (this.initQueue == null) { + this.initQueue = new LongArrayFIFOQueue(200); + } else { + this.initQueue.clear(); + } + if (this.initVisited == null) { + this.initVisited = new LongOpenHashSet(200); + } else { + this.initVisited.clear(); + } + + var originPos = origin.asLong(); + this.initQueue.enqueue(originPos); + this.initVisited.add(originPos); + + var minY = this.level.getMinSectionY(); + var maxY = this.level.getMaxSectionY(); + var originX = origin.getX(); + var originY = origin.getY(); + var originZ = origin.getZ(); + + while (!this.initQueue.isEmpty()) { + var current = this.initQueue.dequeueLong(); + + var x = SectionPos.x(current); + var y = SectionPos.y(current); + var z = SectionPos.z(current); + + // visit neighbors and add them to the init queue and/or the main graph traversal queue + if (x <= originX) { + visitEmptyNeighbor(queue, frame, transform, searchDistance, x - 1, y, z, GraphDirection.WEST); + } + if (x >= originX) { + visitEmptyNeighbor(queue, frame, transform, searchDistance, x + 1, y, z, GraphDirection.EAST); + } + if (y <= originY && y > minY) { + visitEmptyNeighbor(queue, frame, transform, searchDistance, x, y - 1, z, GraphDirection.DOWN); + } + if (y >= originY && y < maxY) { + visitEmptyNeighbor(queue, frame, transform, searchDistance, x, y + 1, z, GraphDirection.UP); + } + if (z <= originZ) { + visitEmptyNeighbor(queue, frame, transform, searchDistance, x, y, z - 1, GraphDirection.NORTH); + } + if (z >= originZ) { + visitEmptyNeighbor(queue, frame, transform, searchDistance, x, y, z + 1, GraphDirection.SOUTH); + } + } + } + + private void visitEmptyNeighbor(WriteQueue queue, int frame, CameraTransform transform, float searchDistance, int x, int y, int z, int incoming) { + if (!isWithinRenderDistance(transform, x << 4, y << 4, z << 4, searchDistance)) { + return; + } + + var pos = SectionPos.asLong(x, y, z); + var section = this.sections.get(pos); + + // sections that exist get queued + if (section != null) { + visitNode(queue, section, incoming, frame); + } + + // sections that don't exist or are empty are further traversed in the init process + if (section == null || section.getFlags() == 0) { + if (this.initVisited.add(pos)) { + this.initQueue.enqueue(pos); + } + } + } + // Enqueues sections that are inside the viewport using diamond spiral iteration to avoid sorting and ensure a // consistent order. Innermost layers are enqueued first. Within each layer, iteration starts at the northernmost // section and proceeds counterclockwise (N->W->S->E).