diff --git a/engine/src/main/java/org/terasology/engine/rendering/world/ChunkMeshWorker.java b/engine/src/main/java/org/terasology/engine/rendering/world/ChunkMeshWorker.java new file mode 100644 index 00000000000..a70eb54fbad --- /dev/null +++ b/engine/src/main/java/org/terasology/engine/rendering/world/ChunkMeshWorker.java @@ -0,0 +1,125 @@ +// Copyright 2021 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.engine.rendering.world; + +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import org.joml.Vector3ic; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.terasology.engine.core.GameScheduler; +import org.terasology.engine.monitoring.chunk.ChunkMonitor; +import org.terasology.engine.rendering.primitives.ChunkMesh; +import org.terasology.engine.rendering.primitives.ChunkTessellator; +import org.terasology.engine.rendering.world.viewDistance.ViewDistance; +import org.terasology.engine.world.ChunkView; +import org.terasology.engine.world.WorldProvider; +import org.terasology.engine.world.chunks.Chunk; +import org.terasology.engine.world.chunks.RenderableChunk; +import reactor.core.publisher.Sinks; +import reactor.function.TupleUtils; +import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; + +import java.util.Collection; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +public final class ChunkMeshWorker { + private static final Logger logger = LoggerFactory.getLogger(ChunkMeshWorker.class); + + private static final int MAX_LOADABLE_CHUNKS = + ViewDistance.MEGA.getChunkDistance().x() * ViewDistance.MEGA.getChunkDistance().y() * ViewDistance.MEGA.getChunkDistance().z(); + + private final Comparator frontToBackComparator; + private final Set chunkMeshProcessing = Sets.newConcurrentHashSet(); + private final Sinks.Many chunkMeshPublisher = Sinks.many().unicast().onBackpressureBuffer(); + private final List chunksInProximityOfCamera = Lists.newArrayListWithCapacity(MAX_LOADABLE_CHUNKS); + + public ChunkMeshWorker(ChunkTessellator chunkTessellator, + WorldProvider worldProvider, + Comparator frontToBackComparator) { + this.frontToBackComparator = frontToBackComparator; + + chunkMeshPublisher.asFlux() + .distinct(Chunk::getPosition, () -> chunkMeshProcessing) + .doOnNext(k -> k.setDirty(false)) + .parallel(5).runOn(GameScheduler.parallel()) + .>>map(c -> { + ChunkView chunkView = worldProvider.getLocalView(c.getPosition()); + if (chunkView != null && chunkView.isValidView() && chunkMeshProcessing.remove(c.getPosition())) { + ChunkMesh newMesh = chunkTessellator.generateMesh(chunkView); + ChunkMonitor.fireChunkTessellated(c, newMesh); + return Optional.of(Tuples.of(c, newMesh)); + } + return Optional.empty(); + }).filter(Optional::isPresent).sequential() + .publishOn(GameScheduler.gameMain()) + .subscribe(result -> result.ifPresent(TupleUtils.consumer((chunk, chunkMesh) -> { + if (chunksInProximityOfCamera.contains(chunk)) { + chunkMesh.updateMesh(); + chunkMesh.discardData(); + if (chunk.hasMesh()) { + chunk.getMesh().dispose(); + } + chunk.setMesh(chunkMesh); + } + + })), throwable -> logger.error("Failed to build mesh {}", throwable)); + } + + + public void add(Chunk chunk) { + if (chunk != null) { + chunksInProximityOfCamera.add(chunk); + } + } + + public void remove(Chunk chunk) { + chunkMeshProcessing.remove(chunk.getPosition()); + + chunksInProximityOfCamera.remove(chunk); + chunk.disposeMesh(); + } + + public void remove(Vector3ic coord) { + chunkMeshProcessing.remove(coord); + + Iterator iterator = chunksInProximityOfCamera.iterator(); + while (iterator.hasNext()) { + Chunk chunk = iterator.next(); + if (chunk.getPosition().equals(coord)) { + chunk.disposeMesh(); + iterator.remove(); + break; + } + } + } + + public int update() { + int statDirtyChunks = 0; + chunksInProximityOfCamera.sort(frontToBackComparator); + for (Chunk chunk : chunksInProximityOfCamera) { + if (chunk.isReady() && chunk.isDirty()) { + statDirtyChunks++; + Sinks.EmitResult result = chunkMeshPublisher.tryEmitNext(chunk); + if (result.isFailure()) { + logger.error("failed to process chunk {} : {}", chunk, result); + } + } + } + return statDirtyChunks; + } + + public int numberChunkMeshProcessing() { + return chunkMeshProcessing.size(); + } + + public Collection chunks() { + return chunksInProximityOfCamera; + } +} diff --git a/engine/src/main/java/org/terasology/engine/rendering/world/RenderableWorldImpl.java b/engine/src/main/java/org/terasology/engine/rendering/world/RenderableWorldImpl.java index 85a2d0cf78a..c073c1838c9 100644 --- a/engine/src/main/java/org/terasology/engine/rendering/world/RenderableWorldImpl.java +++ b/engine/src/main/java/org/terasology/engine/rendering/world/RenderableWorldImpl.java @@ -3,8 +3,6 @@ package org.terasology.engine.rendering.world; import com.google.common.base.Preconditions; -import com.google.common.collect.Lists; -import com.google.common.collect.Sets; import org.joml.Math; import org.joml.Vector3f; import org.joml.Vector3fc; @@ -14,10 +12,7 @@ import org.slf4j.LoggerFactory; import org.terasology.engine.config.Config; import org.terasology.engine.config.RenderingConfig; -import org.terasology.engine.context.Context; -import org.terasology.engine.core.GameScheduler; import org.terasology.engine.monitoring.PerformanceMonitor; -import org.terasology.engine.monitoring.chunk.ChunkMonitor; import org.terasology.engine.registry.CoreRegistry; import org.terasology.engine.rendering.cameras.Camera; import org.terasology.engine.rendering.logic.ChunkMeshRenderer; @@ -32,21 +27,12 @@ import org.terasology.engine.world.chunks.Chunks; import org.terasology.engine.world.chunks.LodChunkProvider; import org.terasology.engine.world.chunks.RenderableChunk; -import org.terasology.engine.world.generator.ScalableWorldGenerator; -import org.terasology.engine.world.generator.WorldGenerator; import org.terasology.joml.geom.AABBfc; -import reactor.core.publisher.Sinks; -import reactor.function.TupleUtils; -import reactor.util.function.Tuple2; -import reactor.util.function.Tuples; import java.util.ArrayList; import java.util.Comparator; -import java.util.Iterator; import java.util.List; -import java.util.Optional; import java.util.PriorityQueue; -import java.util.Set; /** * TODO: write javadoc unless this class gets slated for removal, which might be. @@ -66,7 +52,6 @@ class RenderableWorldImpl implements RenderableWorld { private final LodChunkProvider lodChunkProvider; private final ChunkTessellator chunkTessellator; - private final List chunksInProximityOfCamera = Lists.newArrayListWithCapacity(MAX_LOADABLE_CHUNKS); private BlockRegion renderableRegion = new BlockRegion(BlockRegion.INVALID); private ViewDistance currentViewDistance; private final RenderQueuesHelper renderQueues; @@ -75,68 +60,39 @@ class RenderableWorldImpl implements RenderableWorld { private final Camera playerCamera; private Camera shadowMapCamera; - private final Config config; private final RenderingConfig renderingConfig; private int statDirtyChunks; private int statVisibleChunks; private int statIgnoredPhases; - private final Set chunkMeshProcessing = Sets.newConcurrentHashSet(); - private final Sinks.Many chunkMeshPublisher = Sinks.many().unicast().onBackpressureBuffer(); + private final RenderableWorldImpl.ChunkFrontToBackComparator frontToBackComparator; + private final RenderableWorldImpl.ChunkBackToFrontComparator backToFrontComparator; - RenderableWorldImpl(Context context, Camera playerCamera) { + private final ChunkMeshWorker chunkWorker; - this.worldProvider = context.get(WorldProvider.class); - this.chunkProvider = context.get(ChunkProvider.class); - this.chunkTessellator = context.get(ChunkTessellator.class); - this.config = context.get(Config.class); + RenderableWorldImpl(WorldRenderer worldRenderer, LodChunkProvider lodChunkProvider, ChunkProvider chunkProvider, + ChunkTessellator chunkTessellator, WorldProvider worldProvider, Config config, Camera playerCamera) { + frontToBackComparator = new RenderableWorldImpl.ChunkFrontToBackComparator(worldRenderer); + backToFrontComparator = new RenderableWorldImpl.ChunkBackToFrontComparator(worldRenderer); + this.worldProvider = worldProvider; + this.chunkProvider = chunkProvider; + this.playerCamera = playerCamera; + this.lodChunkProvider = lodChunkProvider; + this.chunkTessellator = chunkTessellator; this.renderingConfig = config.getRendering(); this.maxChunksForShadows = Math.clamp(config.getRendering().getMaxChunksUsedForShadowMapping(), 64, 1024); - this.playerCamera = playerCamera; - WorldGenerator worldGenerator = context.get(WorldGenerator.class); - if (worldGenerator instanceof ScalableWorldGenerator) { - lodChunkProvider = new LodChunkProvider(context, (ScalableWorldGenerator) worldGenerator, - chunkTessellator, renderingConfig.getViewDistance(), (int) renderingConfig.getChunkLods(), - calcCameraCoordinatesInChunkUnits()); - } else { - lodChunkProvider = null; - } - + this.chunkWorker = new ChunkMeshWorker(chunkTessellator, worldProvider, frontToBackComparator); renderQueues = new RenderQueuesHelper(new PriorityQueue<>(MAX_LOADABLE_CHUNKS, - new ChunkFrontToBackComparator()), - new PriorityQueue<>(MAX_LOADABLE_CHUNKS, new ChunkFrontToBackComparator()), - new PriorityQueue<>(MAX_LOADABLE_CHUNKS, new ChunkFrontToBackComparator()), - new PriorityQueue<>(MAX_LOADABLE_CHUNKS, new ChunkFrontToBackComparator()), - new PriorityQueue<>(MAX_LOADABLE_CHUNKS, new ChunkBackToFrontComparator())); - - chunkMeshPublisher.asFlux() - .distinct(Chunk::getPosition, () -> chunkMeshProcessing) - .doOnNext(k -> k.setDirty(false)) - .parallel(5).runOn(GameScheduler.parallel()) - .>>map(c -> { - ChunkView chunkView = worldProvider.getLocalView(c.getPosition()); - if (chunkView != null && chunkView.isValidView() && chunkMeshProcessing.remove(c.getPosition())) { - ChunkMesh newMesh = chunkTessellator.generateMesh(chunkView); - ChunkMonitor.fireChunkTessellated(c, newMesh); - return Optional.of(Tuples.of(c, newMesh)); - } - return Optional.empty(); - }).filter(Optional::isPresent).sequential() - .publishOn(GameScheduler.gameMain()) - .subscribe(result -> result.ifPresent(TupleUtils.consumer((chunk, chunkMesh) -> { - if (chunksInProximityOfCamera.contains(chunk)) { - chunkMesh.updateMesh(); //.generateVBOs(); - chunkMesh.discardData(); - if (chunk.hasMesh()) { - chunk.getMesh().dispose(); - } - chunk.setMesh(chunkMesh); - } + frontToBackComparator), + new PriorityQueue<>(MAX_LOADABLE_CHUNKS, frontToBackComparator), + new PriorityQueue<>(MAX_LOADABLE_CHUNKS, frontToBackComparator), + new PriorityQueue<>(MAX_LOADABLE_CHUNKS, frontToBackComparator), + new PriorityQueue<>(MAX_LOADABLE_CHUNKS, backToFrontComparator)); + - })), throwable -> logger.error("Failed to build mesh {}", throwable)); } @Override @@ -144,8 +100,7 @@ public void onChunkLoaded(Vector3ic chunkCoordinates) { if (renderableRegion.contains(chunkCoordinates)) { Chunk chunk = chunkProvider.getChunk(chunkCoordinates); if (chunk != null) { - chunksInProximityOfCamera.add(chunk); - chunksInProximityOfCamera.sort(new ChunkFrontToBackComparator()); + chunkWorker.add(chunk); if (lodChunkProvider != null) { lodChunkProvider.onRealChunkLoaded(chunkCoordinates); } @@ -163,19 +118,8 @@ public void onChunkLoaded(Vector3ic chunkCoordinates) { @Override public void onChunkUnloaded(Vector3ic chunkCoordinates) { - chunkMeshProcessing.remove(chunkCoordinates); - if (renderableRegion.contains(chunkCoordinates)) { - Chunk chunk; - Iterator iterator = chunksInProximityOfCamera.iterator(); - while (iterator.hasNext()) { - chunk = iterator.next(); - if (chunk.getPosition().equals(chunkCoordinates)) { - chunk.disposeMesh(); - iterator.remove(); - break; - } - } + chunkWorker.remove(chunkCoordinates); } if (lodChunkProvider != null) { lodChunkProvider.onRealChunkUnloaded(chunkCoordinates); @@ -250,36 +194,19 @@ public void update() { */ @Override public boolean updateChunksInProximity(BlockRegion newRenderableRegion) { + if (!newRenderableRegion.equals(renderableRegion)) { - Chunk chunk; for (Vector3ic chunkPositionToRemove : renderableRegion) { if (!newRenderableRegion.contains(chunkPositionToRemove)) { - Iterator nearbyChunks = chunksInProximityOfCamera.iterator(); - while (nearbyChunks.hasNext()) { - chunk = nearbyChunks.next(); - if (chunk.getPosition().equals(chunkPositionToRemove)) { - chunk.disposeMesh(); - nearbyChunks.remove(); - break; - } - - } + chunkWorker.remove(chunkPositionToRemove); } } - boolean chunksHaveBeenAdded = false; for (Vector3ic chunkPositionToAdd : newRenderableRegion) { if (!renderableRegion.contains(chunkPositionToAdd)) { - chunk = chunkProvider.getChunk(chunkPositionToAdd); - if (chunk != null) { - chunksInProximityOfCamera.add(chunk); - chunksHaveBeenAdded = true; - } + chunkWorker.add(chunkProvider.getChunk(chunkPositionToAdd)); } } - if (chunksHaveBeenAdded) { - chunksInProximityOfCamera.sort(new ChunkFrontToBackComparator()); - } renderableRegion = newRenderableRegion; return true; } @@ -338,7 +265,7 @@ public int queueVisibleChunks(boolean isFirstRenderingStageForCurrentFrame) { boolean isDynamicShadows = renderingConfig.isDynamicShadows(); int billboardLimit = (int) renderingConfig.getBillboardLimit(); - List allChunks = new ArrayList<>(chunksInProximityOfCamera); + List allChunks = new ArrayList<>(chunkWorker.chunks()); allChunks.addAll(chunkMeshRenderer.getRenderableChunks()); if (lodChunkProvider != null) { lodChunkProvider.addAllChunks(allChunks); @@ -392,19 +319,11 @@ && isChunkVisibleFromMainLight(chunk)) { } if (isFirstRenderingStageForCurrentFrame) { - for (Chunk chunk : chunksInProximityOfCamera) { - if (isChunkValidForRender(chunk) && chunk.isDirty()) { - statDirtyChunks++; - Sinks.EmitResult result = chunkMeshPublisher.tryEmitNext(chunk); - if (result.isFailure()) { - logger.error("failed to process chunk {} : {}", chunk, result); - } - } - } + statDirtyChunks = chunkWorker.update(); } PerformanceMonitor.endActivity(); - return chunkMeshProcessing.size(); + return chunkWorker.numberChunkMeshProcessing(); } private int triangleCount(ChunkMesh mesh, ChunkMesh.RenderPhase renderPhase) { @@ -497,25 +416,33 @@ private static float squaredDistanceToCamera(RenderableChunk chunk, Vector3f cam // TODO: find the right place to check if the activeCamera has changed, // TODO: so that the comparators can hold an up-to-date reference to it // TODO: and avoid having to find it on a per-comparison basis. - private static class ChunkFrontToBackComparator implements Comparator { + public static class ChunkFrontToBackComparator implements Comparator { + + private final WorldRenderer worldRenderer; + ChunkFrontToBackComparator(WorldRenderer worldRenderer) { + this.worldRenderer = worldRenderer; + } @Override public int compare(RenderableChunk chunk1, RenderableChunk chunk2) { Preconditions.checkNotNull(chunk1); Preconditions.checkNotNull(chunk2); - Vector3f cameraPosition = CoreRegistry.get(WorldRenderer.class).getActiveCamera().getPosition(); + Vector3f cameraPosition = worldRenderer.getActiveCamera().getPosition(); double distance1 = squaredDistanceToCamera(chunk1, cameraPosition); double distance2 = squaredDistanceToCamera(chunk2, cameraPosition); // Using Double.compare as simple d1 < d2 comparison is flagged as problematic by Jenkins // On the other hand Double.compare can return any positive/negative value apparently, // hence the need for Math.signum(). - return (int) Math.signum(Double.compare(distance1, distance2)); + return Math.signum(Double.compare(distance1, distance2)); } } - private static class ChunkBackToFrontComparator implements Comparator { - + public static class ChunkBackToFrontComparator implements Comparator { + private final WorldRenderer worldRenderer; + ChunkBackToFrontComparator(WorldRenderer worldRenderer) { + this.worldRenderer = worldRenderer; + } @Override public int compare(RenderableChunk chunk1, RenderableChunk chunk2) { Preconditions.checkNotNull(chunk1); diff --git a/engine/src/main/java/org/terasology/engine/rendering/world/WorldRendererImpl.java b/engine/src/main/java/org/terasology/engine/rendering/world/WorldRendererImpl.java index 76e1f92f72c..cfe1713bfe5 100644 --- a/engine/src/main/java/org/terasology/engine/rendering/world/WorldRendererImpl.java +++ b/engine/src/main/java/org/terasology/engine/rendering/world/WorldRendererImpl.java @@ -1,4 +1,4 @@ -// Copyright 2021 The Terasology Foundation +// Copyright 2022 The Terasology Foundation // SPDX-License-Identifier: Apache-2.0 package org.terasology.engine.rendering.world; @@ -42,6 +42,12 @@ import org.terasology.engine.rendering.world.viewDistance.ViewDistance; import org.terasology.engine.utilities.Assets; import org.terasology.engine.world.WorldProvider; +import org.terasology.engine.world.block.BlockManager; +import org.terasology.engine.world.chunks.ChunkProvider; +import org.terasology.engine.world.chunks.LodChunkProvider; +import org.terasology.engine.world.chunks.blockdata.ExtraBlockDataManager; +import org.terasology.engine.world.generator.ScalableWorldGenerator; +import org.terasology.engine.world.generator.WorldGenerator; import org.terasology.math.TeraMath; import java.util.List; @@ -156,7 +162,20 @@ public WorldRendererImpl(Context context) { context.put(ChunkTessellator.class, new ChunkTessellator()); - renderableWorld = new RenderableWorldImpl(context, playerCamera); + ChunkProvider chunkProvider = context.get(ChunkProvider.class); + ChunkTessellator chunkTessellator = context.get(ChunkTessellator.class); + BlockManager blockManager = context.get(BlockManager.class); + ExtraBlockDataManager extraDataManager = context.get(ExtraBlockDataManager.class); + Config config = context.get(Config.class); + + + WorldGenerator worldGenerator = context.get(WorldGenerator.class); + LodChunkProvider lodChunkProvider = null; + if (worldGenerator instanceof ScalableWorldGenerator) { + lodChunkProvider = new LodChunkProvider(chunkProvider, blockManager, extraDataManager, + (ScalableWorldGenerator) worldGenerator, chunkTessellator); + } + this.renderableWorld = new RenderableWorldImpl(this, lodChunkProvider, chunkProvider, chunkTessellator, worldProvider, config, playerCamera); renderQueues = renderableWorld.getRenderQueues(); initRenderingSupport(); diff --git a/engine/src/main/java/org/terasology/engine/world/chunks/LodChunkProvider.java b/engine/src/main/java/org/terasology/engine/world/chunks/LodChunkProvider.java index 910fde4fa24..dc61fa4a5e8 100644 --- a/engine/src/main/java/org/terasology/engine/world/chunks/LodChunkProvider.java +++ b/engine/src/main/java/org/terasology/engine/world/chunks/LodChunkProvider.java @@ -6,9 +6,6 @@ import com.google.common.collect.Queues; import org.joml.Vector3i; import org.joml.Vector3ic; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.terasology.engine.context.Context; import org.terasology.engine.rendering.primitives.ChunkMesh; import org.terasology.engine.rendering.primitives.ChunkTessellator; import org.terasology.engine.rendering.world.viewDistance.ViewDistance; @@ -34,17 +31,15 @@ import java.util.concurrent.PriorityBlockingQueue; public class LodChunkProvider { - private static final Logger logger = LoggerFactory.getLogger(LodChunkProvider.class); + private final ChunkProvider chunkProvider; + private final BlockManager blockManager; + private final ExtraBlockDataManager extraDataManager; + private final ChunkTessellator tessellator; + private final ScalableWorldGenerator generator; - private ChunkProvider chunkProvider; - private BlockManager blockManager; - private ExtraBlockDataManager extraDataManager; - private ChunkTessellator tessellator; - private ScalableWorldGenerator generator; - - private Vector3i center; - private ViewDistance viewDistanceSetting; - private int chunkLods; + private Vector3i center = new Vector3i(); + private ViewDistance viewDistanceSetting = ViewDistance.MODERATE; + private int chunkLods = 0; // The chunks that may be actually loaded. private BlockRegion possiblyLoadedRegion = new BlockRegion(BlockRegion.INVALID); // The chunks that should be visible, and therefore shouldn't have LOD chunks even if the chunk there hasn't @@ -53,28 +48,25 @@ public class LodChunkProvider { private BlockRegion[] lodRegions = new BlockRegion[0]; // The sizes of all of the LOD chunks that are meant to exist. All the chunks at the same positions with larger // sizes also may exist, but don't always. - private Map requiredChunks; - private ArrayList> chunks = new ArrayList<>(); - private ClosenessComparator nearby; + private final Map requiredChunks; + private final ArrayList> chunks = new ArrayList<>(); + private final ClosenessComparator nearby; // Communication with the generation threads. - private PriorityBlockingQueue neededChunks; - private BlockingQueue readyChunks = Queues.newLinkedBlockingQueue(); - private List generationThreads = new ArrayList<>(); + private final PriorityBlockingQueue neededChunks; + private final BlockingQueue readyChunks = Queues.newLinkedBlockingQueue(); + private final List generationThreads = new ArrayList<>(); - public LodChunkProvider(Context context, ScalableWorldGenerator generator, ChunkTessellator tessellator, - ViewDistance viewDistance, int chunkLods, Vector3i center) { - chunkProvider = context.get(ChunkProvider.class); - blockManager = context.get(BlockManager.class); - extraDataManager = context.get(ExtraBlockDataManager.class); + public LodChunkProvider(ChunkProvider chunkProvider, BlockManager blockManager, ExtraBlockDataManager extraDataManager, + ScalableWorldGenerator generator, ChunkTessellator tessellator) { + this.chunkProvider = chunkProvider; + this.blockManager = blockManager; + this.extraDataManager = extraDataManager; this.generator = generator; this.tessellator = tessellator; - viewDistanceSetting = viewDistance; - this.chunkLods = chunkLods; - this.center = center; - requiredChunks = new ConcurrentHashMap<>(); - nearby = new ClosenessComparator(center); - neededChunks = new PriorityBlockingQueue<>(11, nearby); + this.requiredChunks = new ConcurrentHashMap<>(); + this.nearby = new ClosenessComparator(center); + this.neededChunks = new PriorityBlockingQueue<>(11, nearby); for (int i = 0; i < 4; i++) { Thread thread = new Thread(this::createChunks, "LOD Chunk Generation " + i); thread.start(); @@ -82,6 +74,7 @@ public LodChunkProvider(Context context, ScalableWorldGenerator generator, Chunk } } + private void createChunks() { Block unloaded = blockManager.getBlock(BlockManager.UNLOADED_ID); try { @@ -150,10 +143,10 @@ public void update(Vector3i newCenter) { } public void updateRenderableRegion(ViewDistance newViewDistance, int newChunkLods, Vector3i newCenter) { - viewDistanceSetting = newViewDistance; - center = new Vector3i(delay(center.x, newCenter.x), delay(center.y, newCenter.y), delay(center.z, newCenter.z)); - chunkLods = newChunkLods; - nearby.pos = center; + this.viewDistanceSetting = newViewDistance; + this.center = new Vector3i(delay(center.x, newCenter.x), delay(center.y, newCenter.y), delay(center.z, newCenter.z)); + this.chunkLods = newChunkLods; + this.nearby.pos = center; Vector3i viewDistance = new Vector3i(newViewDistance.getChunkDistance()).div(2); Vector3i altViewDistance = viewDistance.add(1 - Math.abs(viewDistance.x % 2), 1 - Math.abs(viewDistance.y % 2), 1 - Math.abs(viewDistance.z % 2), new Vector3i());