From ea430414fca3a3e615acd10e81244da08f08f47c Mon Sep 17 00:00:00 2001 From: douira Date: Sun, 24 Nov 2024 19:42:17 +0100 Subject: [PATCH 1/2] render the closest loaded sections if the origin section is not loaded by iterating cube shells around the origin --- .../chunk/occlusion/OcclusionCuller.java | 78 ++++++++++++++++--- 1 file changed, 69 insertions(+), 9 deletions(-) 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..e47f2471ed 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 @@ -233,18 +233,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(visitor, queue, viewport, useOcclusionCulling, 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 +262,68 @@ private void initWithinWorld(Visitor visitor, WriteQueue queue, V visitNeighbors(queue, section, outgoing, frame); } + private void initAtNonExistingSection(Visitor visitor, WriteQueue queue, Viewport viewport, boolean useOcclusionCulling, float searchDistance, int frame) { + var origin = viewport.getChunkCoord(); + var minY = this.level.getMinSectionY(); + var maxY = this.level.getMaxSectionY(); + var originX = origin.getX(); + var originY = origin.getY(); + var originZ = origin.getZ(); + + // iterate shells until one is found with a loaded and visible section + var foundAny = false; + var radius = 1; + while (!foundAny) { + // iterate a shell around the origin + var bigStep = radius * 2; + for (var dy = -radius; dy <= radius; dy++) { + var y = originY + dy; + // skip layers outside the world's y range + if (y < minY || y > maxY) { + continue; + } + + // iterate only the perimeter with the current radius + var notYFace = !(dy == -radius || dy == radius); + for (var dx = -radius; dx <= radius; dx++) { + var zStep = notYFace && (dx == -radius || dx == radius) ? bigStep : 1; + for (var dz = -radius; dz <= radius; dz += zStep) { + var x = originX + dx; + var z = originZ + dz; + + // visit loaded visible sections and queue their neighbors + var section = this.getRenderSection(x, y, z); + if (section != null && isSectionVisible(section, viewport, searchDistance)) { + foundAny = true; + + // use all directions as incoming, using just some yields a broken result + var incoming = GraphDirectionSet.ALL; + section.setIncomingDirections(incoming); + section.setLastVisibleFrame(frame); + + visitor.visit(section); + + // reduce set of neighbors to visit based on visibility connections + int connections = getOutwardDirections(origin, section); + if (useOcclusionCulling) { + connections &= VisibilityEncoding.getConnections(section.getVisibilityData(), incoming); + } + + visitNeighbors(queue, section, connections, frame); + } + } + } + } + + radius++; + + // don't exceed the search distance with the init search + if (radius << 4 > searchDistance) { + break; + } + } + } + // 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). From b7a5501c2c813af1064c5ad0b45e36d793e508c1 Mon Sep 17 00:00:00 2001 From: douira Date: Mon, 25 Nov 2024 03:56:56 +0100 Subject: [PATCH 2/2] use air bfs traversal instead of shell iteration for better result --- .../chunk/occlusion/OcclusionCuller.java | 123 ++++++++++-------- 1 file changed, 72 insertions(+), 51 deletions(-) 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 e47f2471ed..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) @@ -237,7 +245,7 @@ private void init(Visitor visitor, if (originSection != null) { this.initAtExistingSection(visitor, queue, originSection, useOcclusionCulling, frame); } else { - this.initAtNonExistingSection(visitor, queue, viewport, useOcclusionCulling, searchDistance, frame); + this.initAtNonExistingSection(queue, viewport, searchDistance, frame); } } } @@ -262,64 +270,77 @@ private void initAtExistingSection(Visitor visitor, WriteQueue qu visitNeighbors(queue, section, outgoing, frame); } - private void initAtNonExistingSection(Visitor visitor, WriteQueue queue, Viewport viewport, boolean useOcclusionCulling, float searchDistance, int 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(); - // iterate shells until one is found with a loaded and visible section - var foundAny = false; - var radius = 1; - while (!foundAny) { - // iterate a shell around the origin - var bigStep = radius * 2; - for (var dy = -radius; dy <= radius; dy++) { - var y = originY + dy; - // skip layers outside the world's y range - if (y < minY || y > maxY) { - continue; - } + while (!this.initQueue.isEmpty()) { + var current = this.initQueue.dequeueLong(); - // iterate only the perimeter with the current radius - var notYFace = !(dy == -radius || dy == radius); - for (var dx = -radius; dx <= radius; dx++) { - var zStep = notYFace && (dx == -radius || dx == radius) ? bigStep : 1; - for (var dz = -radius; dz <= radius; dz += zStep) { - var x = originX + dx; - var z = originZ + dz; - - // visit loaded visible sections and queue their neighbors - var section = this.getRenderSection(x, y, z); - if (section != null && isSectionVisible(section, viewport, searchDistance)) { - foundAny = true; - - // use all directions as incoming, using just some yields a broken result - var incoming = GraphDirectionSet.ALL; - section.setIncomingDirections(incoming); - section.setLastVisibleFrame(frame); - - visitor.visit(section); - - // reduce set of neighbors to visit based on visibility connections - int connections = getOutwardDirections(origin, section); - if (useOcclusionCulling) { - connections &= VisibilityEncoding.getConnections(section.getVisibilityData(), incoming); - } - - visitNeighbors(queue, section, connections, frame); - } - } - } + 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); - radius++; + // sections that exist get queued + if (section != null) { + visitNode(queue, section, incoming, frame); + } - // don't exceed the search distance with the init search - if (radius << 4 > searchDistance) { - break; + // 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); } } }