diff --git a/engine-tests/src/test/java/org/terasology/world/Zones/LayeredZoneRegionFunctionTest.java b/engine-tests/src/test/java/org/terasology/world/Zones/LayeredZoneRegionFunctionTest.java index e0feb3f0717..22a77e8cd05 100644 --- a/engine-tests/src/test/java/org/terasology/world/Zones/LayeredZoneRegionFunctionTest.java +++ b/engine-tests/src/test/java/org/terasology/world/Zones/LayeredZoneRegionFunctionTest.java @@ -78,7 +78,7 @@ public void setup() { borders.put(ElevationFacet.class, new Border3D(0, 0, 0)); region = new RegionImpl(new BlockRegion(0, 0, 0).expand(4, 4, 4), - facetProviderChains, borders); + facetProviderChains, borders, 1); } @Test diff --git a/engine/src/main/java/org/terasology/config/RenderingConfig.java b/engine/src/main/java/org/terasology/config/RenderingConfig.java index 808088aeea0..c50e6505974 100644 --- a/engine/src/main/java/org/terasology/config/RenderingConfig.java +++ b/engine/src/main/java/org/terasology/config/RenderingConfig.java @@ -38,6 +38,7 @@ public class RenderingConfig extends AbstractSubscribable { public static final String RESOLUTION = "Resolution"; public static final String ANIMATED_MENU = "AnimatedMenu"; public static final String VIEW_DISTANCE = "viewDistance"; + public static final String CHUNK_LODS = "chunkLods"; public static final String FLICKERING_LIGHT = "FlickeringLight"; public static final String ANIMATE_GRASS = "AnimateGrass"; public static final String ANIMATE_WATER = "AnimateWater"; @@ -87,6 +88,7 @@ public class RenderingConfig extends AbstractSubscribable { private Resolution resolution; private boolean animatedMenu; private ViewDistance viewDistance; + private float chunkLods; private boolean flickeringLight; private boolean animateGrass; private boolean animateWater; @@ -271,6 +273,16 @@ public void setViewDistance(ViewDistance viewDistance) { propertyChangeSupport.firePropertyChange(VIEW_DISTANCE, oldDistance, viewDistance); } + public float getChunkLods() { + return chunkLods; + } + + public void setChunkLods(float chunkLods) { + float oldLods = this.chunkLods; + this.chunkLods = chunkLods; + propertyChangeSupport.firePropertyChange(CHUNK_LODS, oldLods, chunkLods); + } + public boolean isFlickeringLight() { return flickeringLight; } diff --git a/engine/src/main/java/org/terasology/engine/subsystem/headless/renderer/HeadlessWorldRenderer.java b/engine/src/main/java/org/terasology/engine/subsystem/headless/renderer/HeadlessWorldRenderer.java index c7f17dfa46f..97b6d232a2b 100644 --- a/engine/src/main/java/org/terasology/engine/subsystem/headless/renderer/HeadlessWorldRenderer.java +++ b/engine/src/main/java/org/terasology/engine/subsystem/headless/renderer/HeadlessWorldRenderer.java @@ -142,7 +142,7 @@ public boolean pregenerateChunks() { } @Override - public void setViewDistance(ViewDistance viewDistance) { + public void setViewDistance(ViewDistance viewDistance, int chunkLods) { // TODO Auto-generated method stub } diff --git a/engine/src/main/java/org/terasology/rendering/AABBRenderer.java b/engine/src/main/java/org/terasology/rendering/AABBRenderer.java index 94207edf319..a2e90a949fd 100644 --- a/engine/src/main/java/org/terasology/rendering/AABBRenderer.java +++ b/engine/src/main/java/org/terasology/rendering/AABBRenderer.java @@ -93,7 +93,7 @@ public void render() { glPushMatrix(); Vector3f cameraPosition = CoreRegistry.get(LocalPlayer.class).getViewPosition(new Vector3f()); Vector3f center = aabb.center(new Vector3f()); - glTranslated(center.x - cameraPosition.x, -cameraPosition.y, center.z - cameraPosition.z); + glTranslated(center.x - cameraPosition.x, center.y - cameraPosition.y, center.z - cameraPosition.z); renderLocally(); @@ -126,13 +126,8 @@ public void renderLocally() { if (displayListWire == -1) { generateDisplayListWire(); } - Vector3f center = aabb.center(new Vector3f()); - glPushMatrix(); - glTranslated(0f, center.y, 0f); glCallList(displayListWire); - - glPopMatrix(); } public void renderSolidLocally() { diff --git a/engine/src/main/java/org/terasology/rendering/cameras/PerspectiveCamera.java b/engine/src/main/java/org/terasology/rendering/cameras/PerspectiveCamera.java index eff0123920b..ea6ca6a3d1e 100644 --- a/engine/src/main/java/org/terasology/rendering/cameras/PerspectiveCamera.java +++ b/engine/src/main/java/org/terasology/rendering/cameras/PerspectiveCamera.java @@ -11,6 +11,7 @@ import org.terasology.math.TeraMath; import org.terasology.rendering.nui.layers.mainMenu.videoSettings.CameraSetting; import org.terasology.world.WorldProvider; +import org.terasology.world.chunks.Chunks; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; @@ -48,6 +49,9 @@ public PerspectiveCamera(WorldProvider worldProvider, RenderingConfig renderingC this.cameraSettings = renderingConfig.getCameraSettings(); displayDevice.subscribe(DISPLAY_RESOLUTION_CHANGE, this); + renderingConfig.subscribe(RenderingConfig.VIEW_DISTANCE, this); + renderingConfig.subscribe(RenderingConfig.CHUNK_LODS, this); + updateFarClippingDistance(); } @Override @@ -181,10 +185,23 @@ public void setBobbingVerticalOffsetFactor(float f) { bobbingVerticalOffsetFactor = f; } + private void updateFarClippingDistance() { + float distance = renderingConfig.getViewDistance().getChunkDistance().x() * Chunks.SIZE_X * (1 << (int) renderingConfig.getChunkLods()); + zFar = Math.max(distance, 500) * 2; + // distance is an estimate of how far away the farthest chunks are, and the minimum bound is to ensure that the sky is visible. + } + public void propertyChange(PropertyChangeEvent propertyChangeEvent) { - if (propertyChangeEvent.getPropertyName().equals(DISPLAY_RESOLUTION_CHANGE)) { - cachedFov = -1; // Invalidate the cache, so that matrices get regenerated. - updateMatrices(); + switch (propertyChangeEvent.getPropertyName()) { + case DISPLAY_RESOLUTION_CHANGE: + cachedFov = -1; // Invalidate the cache, so that matrices get regenerated. + updateMatrices(); + return; + case RenderingConfig.VIEW_DISTANCE: + case RenderingConfig.CHUNK_LODS: + updateFarClippingDistance(); + return; + default: } } } diff --git a/engine/src/main/java/org/terasology/rendering/cameras/SubmersibleCamera.java b/engine/src/main/java/org/terasology/rendering/cameras/SubmersibleCamera.java index f273a0549f4..c5c0bffbe89 100644 --- a/engine/src/main/java/org/terasology/rendering/cameras/SubmersibleCamera.java +++ b/engine/src/main/java/org/terasology/rendering/cameras/SubmersibleCamera.java @@ -24,7 +24,7 @@ public abstract class SubmersibleCamera extends Camera { /* Used for Underwater Checks */ private WorldProvider worldProvider; - private RenderingConfig renderingConfig; + RenderingConfig renderingConfig; public SubmersibleCamera(WorldProvider worldProvider, RenderingConfig renderingConfig) { this.worldProvider = worldProvider; diff --git a/engine/src/main/java/org/terasology/rendering/nui/layers/mainMenu/videoSettings/VideoSettingsScreen.java b/engine/src/main/java/org/terasology/rendering/nui/layers/mainMenu/videoSettings/VideoSettingsScreen.java index 4ea0cf3d23f..852cc6d5c49 100644 --- a/engine/src/main/java/org/terasology/rendering/nui/layers/mainMenu/videoSettings/VideoSettingsScreen.java +++ b/engine/src/main/java/org/terasology/rendering/nui/layers/mainMenu/videoSettings/VideoSettingsScreen.java @@ -154,6 +154,15 @@ public String getString(Integer value) { fovSlider.bindValue(BindHelper.bindBeanProperty("fieldOfView", config.getRendering(), Float.TYPE)); } + final UISlider chunkLodSlider = find("chunkLods", UISlider.class); + if (chunkLodSlider != null) { + chunkLodSlider.setIncrement(1); + chunkLodSlider.setPrecision(0); + chunkLodSlider.setMinimum(0); + chunkLodSlider.setRange(10); + chunkLodSlider.bindValue(BindHelper.bindBeanProperty("chunkLods", config.getRendering(), Float.TYPE)); + } + final UISlider frameLimitSlider = find("frameLimit", UISlider.class); if (frameLimitSlider != null) { frameLimitSlider.setIncrement(5.0f); diff --git a/engine/src/main/java/org/terasology/rendering/primitives/BlockMeshGeneratorSingleShape.java b/engine/src/main/java/org/terasology/rendering/primitives/BlockMeshGeneratorSingleShape.java index 2ddf5f49d13..909eddd6a51 100644 --- a/engine/src/main/java/org/terasology/rendering/primitives/BlockMeshGeneratorSingleShape.java +++ b/engine/src/main/java/org/terasology/rendering/primitives/BlockMeshGeneratorSingleShape.java @@ -145,10 +145,14 @@ private boolean isSideVisibleForBlockTypes(Block blockToCheck, Block currentBloc //TODO: This only fixes the "water under block" issue of the top side not being rendered. (see bug #3889) //Note: originally tried .isLiquid() instead of isWater for both checks, but IntelliJ was warning that // !blockToCheck.isWater() is always true, may need further investigation - if (currentBlock.isWater() && (side == Side.TOP) && !blockToCheck.isWater()){ + if (currentBlock.isWater() && (side == Side.TOP) && !blockToCheck.isWater()) { return true; } + if (blockToCheck.getURI().toString().equals("engine:unloaded")) { + return false; + } + return currentBlock.isWaving() != blockToCheck.isWaving() || blockToCheck.getMeshGenerator() == null || !blockToCheck.isFullSide(side.reverse()) || (!currentBlock.isTranslucent() && blockToCheck.isTranslucent()); diff --git a/engine/src/main/java/org/terasology/rendering/primitives/ChunkTessellator.java b/engine/src/main/java/org/terasology/rendering/primitives/ChunkTessellator.java index 90ed5b926c1..bae262854e7 100644 --- a/engine/src/main/java/org/terasology/rendering/primitives/ChunkTessellator.java +++ b/engine/src/main/java/org/terasology/rendering/primitives/ChunkTessellator.java @@ -17,6 +17,7 @@ import com.google.common.base.Stopwatch; import gnu.trove.iterator.TIntIterator; +import gnu.trove.list.TFloatList; import org.lwjgl.BufferUtils; import org.terasology.engine.subsystem.lwjgl.GLBufferPool; import org.terasology.math.Direction; @@ -27,6 +28,7 @@ import org.terasology.world.ChunkView; import org.terasology.world.block.Block; import org.terasology.world.chunks.ChunkConstants; +import org.terasology.world.chunks.Chunks; import java.util.concurrent.TimeUnit; @@ -44,15 +46,20 @@ public ChunkTessellator(GLBufferPool bufferPool) { this.bufferPool = bufferPool; } - public ChunkMesh generateMesh(ChunkView chunkView, int meshHeight, int verticalOffset) { + public ChunkMesh generateMesh(ChunkView chunkView) { + return generateMesh(chunkView, 1, 0); + } + + public ChunkMesh generateMesh(ChunkView chunkView, float scale, int border) { PerformanceMonitor.startActivity("GenerateMesh"); ChunkMesh mesh = new ChunkMesh(bufferPool); final Stopwatch watch = Stopwatch.createStarted(); - for (int x = 0; x < ChunkConstants.SIZE_X; x++) { - for (int z = 0; z < ChunkConstants.SIZE_Z; z++) { - for (int y = verticalOffset; y < verticalOffset + meshHeight; y++) { + // The mesh extends into the borders in the horizontal directions, but not vertically upwards, in order to cover gaps between LOD chunks of different scales, but also avoid multiple overlapping ocean surfaces. + for (int x = 0; x < Chunks.SIZE_X; x++) { + for (int z = 0; z < Chunks.SIZE_Z; z++) { + for (int y = 0; y < Chunks.SIZE_Y - border * 2; y++) { Block block = chunkView.getBlock(x, y, z); if (block != null && block.getMeshGenerator() != null) { block.getMeshGenerator().generateChunkMesh(chunkView, mesh, x, y, z); @@ -65,7 +72,7 @@ public ChunkMesh generateMesh(ChunkView chunkView, int meshHeight, int verticalO mesh.setTimeToGenerateBlockVertices((int) watch.elapsed(TimeUnit.MILLISECONDS)); watch.reset().start(); - generateOptimizedBuffers(chunkView, mesh); + generateOptimizedBuffers(chunkView, mesh, scale, border); watch.stop(); mesh.setTimeToGenerateOptimizedBuffers((int) watch.elapsed(TimeUnit.MILLISECONDS)); statVertexArrayUpdateCount++; @@ -74,7 +81,7 @@ public ChunkMesh generateMesh(ChunkView chunkView, int meshHeight, int verticalO return mesh; } - private void generateOptimizedBuffers(ChunkView chunkView, ChunkMesh mesh) { + private void generateOptimizedBuffers(ChunkView chunkView, ChunkMesh mesh, float scale, float border) { PerformanceMonitor.startActivity("OptimizeBuffers"); for (ChunkMesh.RenderType type : ChunkMesh.RenderType.values()) { @@ -97,9 +104,10 @@ private void generateOptimizedBuffers(ChunkView chunkView, ChunkMesh mesh) { elements.vertices.get(i * 3 + 2)); /* POSITION */ - elements.finalVertices.put(Float.floatToIntBits(vertexPos.x)); - elements.finalVertices.put(Float.floatToIntBits(vertexPos.y)); - elements.finalVertices.put(Float.floatToIntBits(vertexPos.z)); + float totalScale = scale * Chunks.SIZE_X / (Chunks.SIZE_X - 2 * border); + elements.finalVertices.put(Float.floatToIntBits((vertexPos.x - border) * totalScale)); + elements.finalVertices.put(Float.floatToIntBits((vertexPos.y - 2 * border) * totalScale)); + elements.finalVertices.put(Float.floatToIntBits((vertexPos.z - border) * totalScale)); /* UV0 - TEX DATA 0.xy */ elements.finalVertices.put(Float.floatToIntBits(elements.tex.get(i * 2))); diff --git a/engine/src/main/java/org/terasology/rendering/world/ChunkMeshUpdateManager.java b/engine/src/main/java/org/terasology/rendering/world/ChunkMeshUpdateManager.java index b3260c34c6f..a66074ea5eb 100644 --- a/engine/src/main/java/org/terasology/rendering/world/ChunkMeshUpdateManager.java +++ b/engine/src/main/java/org/terasology/rendering/world/ChunkMeshUpdateManager.java @@ -170,7 +170,7 @@ public void run() { */ c.setDirty(false); if (chunkView.isValidView()) { - newMesh = tessellator.generateMesh(chunkView, ChunkConstants.SIZE_Y, 0); + newMesh = tessellator.generateMesh(chunkView); c.setPendingMesh(newMesh); ChunkMonitor.fireChunkTessellated(c.getPosition(new org.joml.Vector3i()), newMesh); diff --git a/engine/src/main/java/org/terasology/rendering/world/RenderQueuesHelper.java b/engine/src/main/java/org/terasology/rendering/world/RenderQueuesHelper.java index b208932cef4..15a07ac14ae 100644 --- a/engine/src/main/java/org/terasology/rendering/world/RenderQueuesHelper.java +++ b/engine/src/main/java/org/terasology/rendering/world/RenderQueuesHelper.java @@ -38,4 +38,15 @@ public class RenderQueuesHelper { this.chunksAlphaReject = chunksAlphaReject; this.chunksAlphaBlend = chunksAlphaBlend; } + + /** + * Remove any remaining data from all queues, to avoid a memory leak in the case that the nodes using that data aren't present. + */ + public void clear() { + chunksOpaque.clear(); + chunksOpaqueShadow.clear(); + chunksOpaqueReflection.clear(); + chunksAlphaReject.clear(); + chunksAlphaBlend.clear(); + } } diff --git a/engine/src/main/java/org/terasology/rendering/world/RenderableWorld.java b/engine/src/main/java/org/terasology/rendering/world/RenderableWorld.java index c26b6ab61e2..2bc18809865 100644 --- a/engine/src/main/java/org/terasology/rendering/world/RenderableWorld.java +++ b/engine/src/main/java/org/terasology/rendering/world/RenderableWorld.java @@ -36,7 +36,7 @@ public interface RenderableWorld { boolean updateChunksInProximity(BlockRegion renderableRegion); - boolean updateChunksInProximity(ViewDistance viewDistance); + boolean updateChunksInProximity(ViewDistance viewDistance, int chunkLods); void generateVBOs(); diff --git a/engine/src/main/java/org/terasology/rendering/world/RenderableWorldImpl.java b/engine/src/main/java/org/terasology/rendering/world/RenderableWorldImpl.java index 8da80748ba6..1804017a4e2 100644 --- a/engine/src/main/java/org/terasology/rendering/world/RenderableWorldImpl.java +++ b/engine/src/main/java/org/terasology/rendering/world/RenderableWorldImpl.java @@ -24,6 +24,7 @@ import org.slf4j.LoggerFactory; import org.terasology.config.Config; import org.terasology.config.RenderingConfig; +import org.terasology.context.Context; import org.terasology.engine.subsystem.lwjgl.GLBufferPool; import org.terasology.joml.geom.AABBi; import org.terasology.math.JomlUtil; @@ -36,12 +37,19 @@ import org.terasology.rendering.world.viewDistance.ViewDistance; import org.terasology.world.ChunkView; import org.terasology.world.WorldProvider; +import org.terasology.world.block.BlockManager; import org.terasology.world.block.BlockRegion; import org.terasology.world.chunks.Chunk; import org.terasology.world.chunks.ChunkConstants; import org.terasology.world.chunks.ChunkProvider; +import org.terasology.world.chunks.Chunks; +import org.terasology.world.chunks.LodChunk; +import org.terasology.world.chunks.LodChunkProvider; import org.terasology.world.chunks.RenderableChunk; +import org.terasology.world.generator.ScalableWorldGenerator; +import org.terasology.world.generator.WorldGenerator; +import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Iterator; @@ -64,6 +72,7 @@ class RenderableWorldImpl implements RenderableWorld { private final WorldProvider worldProvider; private ChunkProvider chunkProvider; + private LodChunkProvider lodChunkProvider; private ChunkTessellator chunkTessellator; private final ChunkMeshUpdateManager chunkMeshUpdateManager; @@ -83,17 +92,20 @@ class RenderableWorldImpl implements RenderableWorld { private int statIgnoredPhases; - RenderableWorldImpl(WorldProvider worldProvider, - ChunkProvider chunkProvider, - GLBufferPool bufferPool, - Camera playerCamera) { + RenderableWorldImpl(Context context, GLBufferPool bufferPool, Camera playerCamera) { - this.worldProvider = worldProvider; - this.chunkProvider = chunkProvider; + worldProvider = context.get(WorldProvider.class); + chunkProvider = context.get(ChunkProvider.class); chunkTessellator = new ChunkTessellator(bufferPool); chunkMeshUpdateManager = new ChunkMeshUpdateManager(chunkTessellator, worldProvider); 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; + } renderQueues = new RenderQueuesHelper(new PriorityQueue<>(MAX_LOADABLE_CHUNKS, new ChunkFrontToBackComparator()), new PriorityQueue<>(MAX_LOADABLE_CHUNKS, new ChunkFrontToBackComparator()), @@ -109,6 +121,9 @@ public void onChunkLoaded(Vector3ic chunkCoordinates) { if (chunk != null) { chunksInProximityOfCamera.add(chunk); Collections.sort(chunksInProximityOfCamera, new ChunkFrontToBackComparator()); + if (lodChunkProvider != null) { + lodChunkProvider.onRealChunkLoaded(chunkCoordinates); + } } else { logger.warn("Warning: onChunkLoaded called for a null chunk!"); } @@ -135,6 +150,9 @@ public void onChunkUnloaded(Vector3ic chunkCoordinates) { } } } + if (lodChunkProvider != null) { + lodChunkProvider.onRealChunkUnloaded(chunkCoordinates); + } } /** @@ -160,7 +178,7 @@ public boolean pregenerateChunks() { } chunk.setDirty(false); - newMesh = chunkTessellator.generateMesh(localView, ChunkConstants.SIZE_Y, 0); + newMesh = chunkTessellator.generateMesh(localView); newMesh.generateVBOs(); if (chunk.hasMesh()) { @@ -190,6 +208,11 @@ public void update() { updateChunksInProximity(calculateRenderableRegion(renderingConfig.getViewDistance())); PerformanceMonitor.endActivity(); + if (lodChunkProvider != null) { + PerformanceMonitor.startActivity("Update LOD Chunks"); + lodChunkProvider.update(calcCameraCoordinatesInChunkUnits()); + PerformanceMonitor.endActivity(); + } } /** @@ -237,10 +260,13 @@ public boolean updateChunksInProximity(BlockRegion newRenderableRegion) { } @Override - public boolean updateChunksInProximity(ViewDistance newViewDistance) { - if (newViewDistance != currentViewDistance) { + public boolean updateChunksInProximity(ViewDistance newViewDistance, int chunkLods) { + if (newViewDistance != currentViewDistance || (lodChunkProvider != null && chunkLods != lodChunkProvider.getChunkLods())) { logger.info("New Viewing Distance: {}", newViewDistance); currentViewDistance = newViewDistance; + if (lodChunkProvider != null) { + lodChunkProvider.updateRenderableRegion(newViewDistance, chunkLods, calcCameraCoordinatesInChunkUnits()); + } return updateChunksInProximity(calculateRenderableRegion(newViewDistance)); } else { return false; @@ -261,9 +287,7 @@ private BlockRegion calculateRenderableRegion(ViewDistance newViewDistance) { */ private Vector3i calcCameraCoordinatesInChunkUnits() { org.joml.Vector3f cameraCoordinates = playerCamera.getPosition(); - return new Vector3i((int) (cameraCoordinates.x() / ChunkConstants.SIZE_X), - (int) (cameraCoordinates.y() / ChunkConstants.SIZE_Y), - (int) (cameraCoordinates.z() / ChunkConstants.SIZE_Z)); + return Chunks.toChunkPos(cameraCoordinates, new Vector3i()); } @Override @@ -304,10 +328,21 @@ public int queueVisibleChunks(boolean isFirstRenderingStageForCurrentFrame) { int processedChunks = 0; int chunkCounter = 0; + + renderQueues.clear(); + ChunkMesh mesh; boolean isDynamicShadows = renderingConfig.isDynamicShadows(); - for (RenderableChunk chunk : chunksInProximityOfCamera) { + List allChunks; + if (lodChunkProvider != null) { + allChunks = new ArrayList<>(chunksInProximityOfCamera); + allChunks.addAll(lodChunkProvider.getChunks()); + } else { + allChunks = chunksInProximityOfCamera; + } + + for (RenderableChunk chunk : allChunks) { if (isChunkValidForRender(chunk)) { mesh = chunk.getMesh(); @@ -376,6 +411,9 @@ private int triangleCount(ChunkMesh mesh, ChunkMesh.RenderPhase renderPhase) { @Override public void dispose() { chunkMeshUpdateManager.shutdown(); + if (lodChunkProvider != null) { + lodChunkProvider.shutdown(); + } } private boolean isChunkValidForRender(RenderableChunk chunk) { diff --git a/engine/src/main/java/org/terasology/rendering/world/WorldRenderer.java b/engine/src/main/java/org/terasology/rendering/world/WorldRenderer.java index bdeab0deacf..c5f0ccd263f 100644 --- a/engine/src/main/java/org/terasology/rendering/world/WorldRenderer.java +++ b/engine/src/main/java/org/terasology/rendering/world/WorldRenderer.java @@ -167,8 +167,9 @@ enum RenderingStage { * Sets how far from the camera chunks are kept in memory and displayed. * * @param viewDistance a viewDistance value. + * @param chunkLods the number of LOD levels to display beyond the loaded chunks. */ - void setViewDistance(ViewDistance viewDistance); + void setViewDistance(ViewDistance viewDistance, int chunkLods); /** * Returns the intensity of the light at a given location due to the combination of main light (sun or moon) diff --git a/engine/src/main/java/org/terasology/rendering/world/WorldRendererImpl.java b/engine/src/main/java/org/terasology/rendering/world/WorldRendererImpl.java index 55f66d06917..9d8fcf5e7b2 100644 --- a/engine/src/main/java/org/terasology/rendering/world/WorldRendererImpl.java +++ b/engine/src/main/java/org/terasology/rendering/world/WorldRendererImpl.java @@ -56,7 +56,6 @@ import org.terasology.rendering.world.viewDistance.ViewDistance; import org.terasology.utilities.Assets; import org.terasology.world.WorldProvider; -import org.terasology.world.chunks.ChunkProvider; import java.util.List; @@ -168,7 +167,7 @@ public WorldRendererImpl(Context context, GLBufferPool bufferPool) { LocalPlayerSystem localPlayerSystem = context.get(LocalPlayerSystem.class); localPlayerSystem.setPlayerCamera(playerCamera); - renderableWorld = new RenderableWorldImpl(worldProvider, context.get(ChunkProvider.class), bufferPool, playerCamera); + renderableWorld = new RenderableWorldImpl(context, bufferPool, playerCamera); renderQueues = renderableWorld.getRenderQueues(); initRenderingSupport(); @@ -393,8 +392,8 @@ public void dispose() { } @Override - public void setViewDistance(ViewDistance viewDistance) { - renderableWorld.updateChunksInProximity(viewDistance); + public void setViewDistance(ViewDistance viewDistance, int chunkLods) { + renderableWorld.updateChunksInProximity(viewDistance, chunkLods); } @Override diff --git a/engine/src/main/java/org/terasology/rendering/world/viewDistance/ClientViewDistanceSystem.java b/engine/src/main/java/org/terasology/rendering/world/viewDistance/ClientViewDistanceSystem.java index c2e318f18a9..069979646e0 100644 --- a/engine/src/main/java/org/terasology/rendering/world/viewDistance/ClientViewDistanceSystem.java +++ b/engine/src/main/java/org/terasology/rendering/world/viewDistance/ClientViewDistanceSystem.java @@ -56,27 +56,35 @@ public class ClientViewDistanceSystem extends BaseComponentSystem { @In private LocalPlayer localPlayer; - private PropertyChangeListener propertyChangeListener; + private PropertyChangeListener viewDistanceListener; + private PropertyChangeListener chunkLodsListener; private TranslationSystem translationSystem; @Override public void initialise() { - propertyChangeListener = evt -> { + viewDistanceListener = evt -> { if (evt.getPropertyName().equals(RenderingConfig.VIEW_DISTANCE)) { onChangeViewDistanceChange(); } }; - config.getRendering().subscribe(propertyChangeListener); + config.getRendering().subscribe(viewDistanceListener); + chunkLodsListener = evt -> { + if (evt.getPropertyName().equals(RenderingConfig.CHUNK_LODS)) { + onChangeViewDistanceChange(); + } + }; + config.getRendering().subscribe(chunkLodsListener); translationSystem = new TranslationSystemImpl(context); } public void onChangeViewDistanceChange() { ViewDistance viewDistance = config.getRendering().getViewDistance(); + int chunkLods = (int) config.getRendering().getChunkLods(); if (worldRenderer != null) { - worldRenderer.setViewDistance(viewDistance); + worldRenderer.setViewDistance(viewDistance, chunkLods); } EntityRef clientEntity = localPlayer.getClientEntity(); @@ -85,7 +93,7 @@ public void onChangeViewDistanceChange() { @Override public void shutdown() { - config.getRendering().unsubscribe(propertyChangeListener); + config.getRendering().unsubscribe(viewDistanceListener); } /** diff --git a/engine/src/main/java/org/terasology/utilities/procedural/SubSampledNoise.java b/engine/src/main/java/org/terasology/utilities/procedural/SubSampledNoise.java index ff5c0ca50e3..f448e0c2025 100644 --- a/engine/src/main/java/org/terasology/utilities/procedural/SubSampledNoise.java +++ b/engine/src/main/java/org/terasology/utilities/procedural/SubSampledNoise.java @@ -80,8 +80,12 @@ public float[] noise(Rect2i region) { } public float[] noise(BlockAreac area) { + return noise(area, 1); + } + + public float[] noise(BlockAreac area, float scale) { BlockArea fullRegion = determineRequiredRegion(area); - float[] keyData = getKeyValues(fullRegion); + float[] keyData = getKeyValues(fullRegion, scale); float[] fullData = mapExpand(keyData, fullRegion); return getSubset(fullData, fullRegion, area); } @@ -120,7 +124,7 @@ private float[] mapExpand(float[] keyData, BlockAreac fullRegion) { return fullData; } - private float[] getKeyValues(BlockAreac fullRegion) { + private float[] getKeyValues(BlockAreac fullRegion, float scale) { int xDim = fullRegion.getSizeX() / sampleRate + 1; int yDim = fullRegion.getSizeY() / sampleRate + 1; float[] fullData = new float[xDim * yDim]; @@ -128,7 +132,7 @@ private float[] getKeyValues(BlockAreac fullRegion) { for (int x = 0; x < xDim; x++) { int actualX = x * sampleRate + fullRegion.minX(); int actualY = y * sampleRate + fullRegion.minY(); - fullData[x + y * xDim] = source.noise(zoom.x * actualX, zoom.y * actualY); + fullData[x + y * xDim] = source.noise(zoom.x * scale * actualX, zoom.y * scale * actualY); } } @@ -168,8 +172,12 @@ public float noise(float x, float y, float z) { } public float[] noise(BlockRegion region) { + return noise(region, 1); + } + + public float[] noise(BlockRegion region, float scale) { BlockRegion fullRegion = determineRequiredRegion(region); - float[] keyData = getKeyValues(fullRegion); + float[] keyData = getKeyValues(fullRegion, scale); float[] fullData = mapExpand(keyData, fullRegion); return getSubset(fullData, fullRegion, region); } @@ -222,7 +230,7 @@ private float[] mapExpand(float[] keyData, BlockRegion fullRegion) { return fullData; } - private float[] getKeyValues(BlockRegion fullRegion) { + private float[] getKeyValues(BlockRegion fullRegion, float scale) { int xDim = fullRegion.getSizeX() / sampleRate + 1; int yDim = fullRegion.getSizeY() / sampleRate + 1; int zDim = fullRegion.getSizeZ() / sampleRate + 1; @@ -233,7 +241,7 @@ private float[] getKeyValues(BlockRegion fullRegion) { int actualX = x * sampleRate + fullRegion.minX(); int actualY = y * sampleRate + fullRegion.minY(); int actualZ = z * sampleRate + fullRegion.minZ(); - fullData[x + xDim * (y + yDim * z)] = source.noise(zoom.x * actualX, zoom.y * actualY, zoom.z * actualZ); + fullData[x + xDim * (y + yDim * z)] = source.noise(zoom.x * scale * actualX, zoom.y * scale * actualY, zoom.z * scale * actualZ); } } } diff --git a/engine/src/main/java/org/terasology/world/RelevanceRegionComponent.java b/engine/src/main/java/org/terasology/world/RelevanceRegionComponent.java index ba10bf1a4df..0819c7adcd2 100644 --- a/engine/src/main/java/org/terasology/world/RelevanceRegionComponent.java +++ b/engine/src/main/java/org/terasology/world/RelevanceRegionComponent.java @@ -23,5 +23,4 @@ public class RelevanceRegionComponent implements Component { public Vector3i distance = new Vector3i(1, 1, 1); - } diff --git a/engine/src/main/java/org/terasology/world/chunks/CoreChunk.java b/engine/src/main/java/org/terasology/world/chunks/CoreChunk.java index 74ad0a763e4..c6f986546eb 100644 --- a/engine/src/main/java/org/terasology/world/chunks/CoreChunk.java +++ b/engine/src/main/java/org/terasology/world/chunks/CoreChunk.java @@ -25,7 +25,7 @@ public interface CoreChunk { /** - * @return Position of the chunk in world, where units of distance from origin are blocks + * @return Position of the chunk in world, where units of distance from origin are chunks * @deprecated This method is scheduled for removal in an upcoming version. * Use the JOML implementation instead: {@link #getPosition(org.joml.Vector3i)}. */ @@ -33,7 +33,7 @@ public interface CoreChunk { Vector3i getPosition(); /** - * Position of the chunk in world, where units of distance from origin are blocks + * Position of the chunk in world, where units of distance from origin are chunks * * @param dest will hold the result * @return dest @@ -174,9 +174,9 @@ public interface CoreChunk { int getExtraData(int index, Vector3ic pos); /** - * Returns offset of this chunk to the world center (0:0:0), with one unit being one chunk. + * Returns offset of this chunk to the world center (0:0:0), with one unit being one block. * - * @return Offset of this chunk from world center in chunks + * @return Offset of this chunk from world center in blocks * @deprecated This method is scheduled for removal in an upcoming version. * Use the JOML implementation instead: {@link #getChunkWorldOffset(org.joml.Vector3i)}. */ @@ -184,31 +184,31 @@ public interface CoreChunk { Vector3i getChunkWorldOffset(); /** - * Returns offset of this chunk to the world center (0:0:0), with one unit being one chunk. + * Returns offset of this chunk to the world center (0:0:0), with one unit being one block. * - * @return Offset of this chunk from world center in chunks + * @return Offset of this chunk from world center in blocks */ org.joml.Vector3i getChunkWorldOffset(org.joml.Vector3i pos); /** - * Returns X offset of this chunk to the world center (0:0:0), with one unit being one chunk. + * Returns X offset of this chunk to the world center (0:0:0), with one unit being one block. * - * @return X offset of this chunk from world center in chunks + * @return X offset of this chunk from world center in blocks */ int getChunkWorldOffsetX(); /** - * Returns Y offset of this chunk to the world center (0:0:0), with one unit being one chunk. + * Returns Y offset of this chunk to the world center (0:0:0), with one unit being one block. * - * @return Y offset of this chunk from world center in chunks + * @return Y offset of this chunk from world center in blocks */ int getChunkWorldOffsetY(); /** - * Returns Z offset of this chunk to the world center (0:0:0), with one unit being one chunk. + * Returns Z offset of this chunk to the world center (0:0:0), with one unit being one block. * - * @return Z offset of this chunk from world center in chunks + * @return Z offset of this chunk from world center in blocks */ int getChunkWorldOffsetZ(); diff --git a/engine/src/main/java/org/terasology/world/chunks/LodChunk.java b/engine/src/main/java/org/terasology/world/chunks/LodChunk.java new file mode 100644 index 00000000000..ffdb7cc224c --- /dev/null +++ b/engine/src/main/java/org/terasology/world/chunks/LodChunk.java @@ -0,0 +1,319 @@ +// Copyright 2021 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.world.chunks; + +import org.joml.Vector3f; +import org.joml.Vector3i; +import org.joml.Vector3ic; +import org.terasology.joml.geom.AABBf; +import org.terasology.joml.geom.AABBfc; +import org.terasology.math.JomlUtil; +import org.terasology.math.geom.BaseVector3i; +import org.terasology.rendering.primitives.ChunkMesh; +import org.terasology.world.block.Block; +import org.terasology.world.block.BlockRegion; + +/** + * A static, far away chunk that has only the data needed for rendering. + */ +public class LodChunk implements RenderableChunk { + private static final String UNSUPPORTED_MESSAGE = "LOD chunks can only be used for certain rendering-related operations."; + public final int scale; + private Vector3ic position; + private ChunkMesh mesh; + + public LodChunk(Vector3ic pos, ChunkMesh mesh, int scale) { + position = pos; + this.mesh = mesh; + this.scale = scale; + } + + @Override + public org.terasology.math.geom.Vector3i getPosition() { + return JomlUtil.from(position); + } + + @Override + public Vector3i getPosition(Vector3i dest) { + return dest.set(position); + } + + @Override + public org.terasology.math.geom.Vector3i getChunkWorldOffset() { + return JomlUtil.from(position).mul(Chunks.SIZE_X, Chunks.SIZE_Y, Chunks.SIZE_Z); + } + + @Override + public Vector3i getChunkWorldOffset(Vector3i dest) { + return position.mul(Chunks.CHUNK_SIZE, dest); + } + + @Override + public AABBfc getAABB() { + Vector3f min = new Vector3f(getChunkWorldOffset(new Vector3i())); + return new AABBf(min, new Vector3f(Chunks.CHUNK_SIZE).mul(1 << scale).add(min)); + } + + @Override + public boolean isAnimated() { + return false; + } + + @Override + public void setAnimated(boolean animated) { + } + + @Override + public boolean isDirty() { + return false; + } + + @Override + public boolean hasMesh() { + return true; + } + + @Override + public ChunkMesh getMesh() { + return mesh; + } + + @Override + public void setMesh(ChunkMesh newMesh) { + mesh = newMesh; + } + + @Override + public void disposeMesh() { + if (mesh != null) { + mesh.dispose(); + mesh = null; + } + } + + @Override + public boolean isReady() { + return mesh != null; + } + + @Override + public Block getBlock(BaseVector3i pos) { + throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE); + } + + @Override + public Block getBlock(Vector3ic pos) { + throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE); + } + + @Override + public Block getBlock(int x, int y, int z) { + throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE); + } + + @Override + public Block setBlock(int x, int y, int z, Block block) { + throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE); + } + + @Override + public Block setBlock(BaseVector3i pos, Block block) { + throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE); + } + + @Override + public Block setBlock(Vector3ic pos, Block block) { + throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE); + } + + @Override + public void setExtraData(int index, int x, int y, int z, int value) { + throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE); + } + + @Override + public void setExtraData(int index, BaseVector3i pos, int value) { + throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE); + } + + @Override + public void setExtraData(int index, Vector3ic pos, int value) { + throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE); + } + + @Override + public int getExtraData(int index, int x, int y, int z) { + throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE); + } + + @Override + public int getExtraData(int index, BaseVector3i pos) { + throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE); + } + + @Override + public int getExtraData(int index, Vector3ic pos) { + throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE); + } + + @Override + public int getChunkWorldOffsetX() { + throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE); + } + + @Override + public int getChunkWorldOffsetY() { + throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE); + } + + @Override + public int getChunkWorldOffsetZ() { + throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE); + } + + @Override + public org.terasology.math.geom.Vector3i chunkToWorldPosition(BaseVector3i blockPos) { + throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE); + } + + @Override + public Vector3i chunkToWorldPosition(Vector3ic blockPos, Vector3i dest) { + throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE); + } + + @Override + public org.terasology.math.geom.Vector3i chunkToWorldPosition(int x, int y, int z) { + throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE); + } + + @Override + public Vector3i chunkToWorldPosition(int x, int y, int z, Vector3i dest) { + throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE); + } + + @Override + public int chunkToWorldPositionX(int x) { + throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE); + } + + @Override + public int chunkToWorldPositionY(int y) { + throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE); + } + + @Override + public int chunkToWorldPositionZ(int z) { + throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE); + } + + @Override + public int getChunkSizeX() { + throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE); + } + + @Override + public int getChunkSizeY() { + throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE); + } + + @Override + public int getChunkSizeZ() { + throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE); + } + + @Override + public BlockRegion getRegion() { + throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE); + } + + @Override + public int getEstimatedMemoryConsumptionInBytes() { + throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE); + } + + @Override + public ChunkBlockIterator getBlockIterator() { + throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE); + } + + @Override + public byte getSunlight(BaseVector3i pos) { + throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE); + } + + @Override + public byte getSunlight(int x, int y, int z) { + throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE); + } + + @Override + public boolean setSunlight(BaseVector3i pos, byte amount) { + throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE); + } + + @Override + public boolean setSunlight(int x, int y, int z, byte amount) { + throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE); + } + + @Override + public byte getSunlightRegen(BaseVector3i pos) { + throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE); + } + + @Override + public byte getSunlightRegen(int x, int y, int z) { + throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE); + } + + @Override + public boolean setSunlightRegen(BaseVector3i pos, byte amount) { + throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE); + } + + @Override + public boolean setSunlightRegen(int x, int y, int z, byte amount) { + throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE); + } + + @Override + public byte getLight(BaseVector3i pos) { + throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE); + } + + @Override + public byte getLight(int x, int y, int z) { + throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE); + } + + @Override + public boolean setLight(BaseVector3i pos, byte amount) { + throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE); + } + + @Override + public boolean setLight(int x, int y, int z, byte amount) { + throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE); + } + + @Override + public void setDirty(boolean dirty) { + throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE); + } + + @Override + public void setPendingMesh(ChunkMesh newPendingMesh) { + throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE); + } + + @Override + public boolean hasPendingMesh() { + throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE); + } + + @Override + public ChunkMesh getPendingMesh() { + throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE); + } +} diff --git a/engine/src/main/java/org/terasology/world/chunks/LodChunkProvider.java b/engine/src/main/java/org/terasology/world/chunks/LodChunkProvider.java new file mode 100644 index 00000000000..852a485e2c2 --- /dev/null +++ b/engine/src/main/java/org/terasology/world/chunks/LodChunkProvider.java @@ -0,0 +1,265 @@ +// Copyright 2021 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.world.chunks; + +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.context.Context; +import org.terasology.math.JomlUtil; +import org.terasology.rendering.primitives.ChunkMesh; +import org.terasology.rendering.primitives.ChunkTessellator; +import org.terasology.rendering.world.viewDistance.ViewDistance; +import org.terasology.world.ChunkView; +import org.terasology.world.block.Block; +import org.terasology.world.block.BlockManager; +import org.terasology.world.block.BlockRegion; +import org.terasology.world.chunks.blockdata.ExtraBlockDataManager; +import org.terasology.world.chunks.internal.PreLodChunk; +import org.terasology.world.generation.impl.EntityBufferImpl; +import org.terasology.world.generator.ScalableWorldGenerator; +import org.terasology.world.internal.ChunkViewCoreImpl; +import org.terasology.world.propagation.light.InternalLightProcessor; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.PriorityBlockingQueue; + +public class LodChunkProvider { + private static final Logger logger = LoggerFactory.getLogger(LodChunkProvider.class); + + 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 BlockRegion possiblyLoadedRegion = new BlockRegion(BlockRegion.INVALID); // The chunks that may be actually loaded. + private BlockRegion probablyLoadedRegion = new BlockRegion(BlockRegion.INVALID); // The chunks that should be visible, and therefore shouldn't have LOD chunks even if the chunk there hasn't loaded yet. + private BlockRegion[] lodRegions = new BlockRegion[0]; + private Map requiredChunks; // The sizes of all of the LOD chunks that are meant to exist. + private Map chunks; + private ClosenessComparator nearby; + + // Communication with the generation threads. + private PriorityBlockingQueue neededChunks; + private BlockingQueue readyChunks = Queues.newLinkedBlockingQueue(); + private 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); + this.generator = generator; + this.tessellator = tessellator; + viewDistanceSetting = viewDistance; + this.chunkLods = chunkLods; + this.center = center; + requiredChunks = new ConcurrentHashMap<>(); + chunks = new HashMap<>(); + nearby = new ClosenessComparator(center); + neededChunks = new PriorityBlockingQueue<>(11, nearby); + for (int i = 0; i < 2; i++) { + Thread thread = new Thread(this::createChunks, "LOD Chunk Generation " + i); + thread.start(); + generationThreads.add(thread); + } + } + + private void createChunks() { + Block unloaded = blockManager.getBlock(BlockManager.UNLOADED_ID); + try { + while (true) { + Vector3ic pos = neededChunks.take(); + Integer scale = requiredChunks.get(pos); // Actually the log scale + if (scale == null) { + // This chunk is being removed in the main thread. + continue; + } + Chunk chunk = new PreLodChunk(scaleDown(pos, scale), blockManager, extraDataManager); + generator.createChunk(chunk, new EntityBufferImpl(), (1 << scale) * (2f / (Chunks.SIZE_X - 2) + 1)); + InternalLightProcessor.generateInternalLighting(chunk, 1 << scale); + //tintChunk(chunk); + ChunkView view = new ChunkViewCoreImpl(new Chunk[]{chunk}, new BlockRegion(chunk.getPosition(new Vector3i())), new Vector3i(), unloaded); + ChunkMesh mesh = tessellator.generateMesh(view, 1 << scale, 1); + readyChunks.add(new LodChunk(pos, mesh, scale)); + } + } catch (InterruptedException ignored) { } + } + + private void processReadyChunks() { + while (!readyChunks.isEmpty()) { + LodChunk chunk = readyChunks.remove(); + Vector3i pos = chunk.getPosition(new Vector3i()); + Integer requiredScale = requiredChunks.get(pos); + if (requiredScale != null && requiredScale == chunk.scale) { // The relevant region may have been updated since this chunk was requested. + chunk.getMesh().generateVBOs(); + chunks.put(pos, chunk); + } + } + } + + public void update(Vector3i newCenter) { + updateRenderableRegion(viewDistanceSetting, chunkLods, newCenter); + processReadyChunks(); + } + + 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; + 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()); + BlockRegion newPossiblyLoadedRegion = new BlockRegion(newCenter).expand(viewDistance); + BlockRegion newProbablyLoadedRegion = new BlockRegion(newPossiblyLoadedRegion).expand(-1, -1, -1); + BlockRegion[] newLodRegions = new BlockRegion[newChunkLods == 0 ? 0 : 1 + newChunkLods]; + boolean lodRegionChange = newLodRegions.length != lodRegions.length; + for (int i = 0; i < newLodRegions.length; i++) { + if (i == 0) { + newLodRegions[i] = new BlockRegion(newPossiblyLoadedRegion); + } else { + // By making viewDistance odd, we ensure that every time a chunk boundary is crossed, at most a single lodRegion changes (except possibly for lodRegions[0], which is more closely tied to the renderable region). + newLodRegions[i] = new BlockRegion(scaleDown(center, i)).expand(altViewDistance); + } + Vector3i min = newLodRegions[i].getMin(new Vector3i()); + Vector3i max = newLodRegions[i].getMax(new Vector3i()); + newLodRegions[i].addToMin(-Math.abs(min.x % 2), -Math.abs(min.y % 2), -Math.abs(min.z % 2)); + newLodRegions[i].addToMax(1 - Math.abs(max.x % 2), 1 - Math.abs(max.y % 2), 1 - Math.abs(max.z % 2)); + if (!lodRegionChange && !newLodRegions[i].equals(lodRegions[i])) { + lodRegionChange = true; + } + } + if (lodRegionChange || !newProbablyLoadedRegion.equals(probablyLoadedRegion) || !newPossiblyLoadedRegion.equals(possiblyLoadedRegion)) { + // Remove previously present chunks. + Set previouslyRequiredChunks = new HashSet<>(requiredChunks.keySet()); + for (Vector3ic pos : previouslyRequiredChunks) { + int scale = requiredChunks.get(pos); + if ( + scale >= newLodRegions.length + || !newLodRegions[scale].contains(scaleDown(pos, scale)) + || scale == 0 && newProbablyLoadedRegion.contains(pos) + || scale > 0 && newLodRegions[scale - 1].contains(scaleDown(pos, scale - 1)) + ) { + removeChunk(pos); + } + } + + // Add new chunks. + for (int scale = 0; scale < newLodRegions.length; scale++) { + for (Vector3ic pos : newLodRegions[scale]) { + if ( + scale == 0 && newProbablyLoadedRegion.contains(pos) + || scale == 0 && newPossiblyLoadedRegion.contains(pos) && chunkProvider.isChunkReady(pos) + || scale > 0 && newLodRegions[scale - 1].contains(pos.mul(2, new Vector3i())) + ) { + continue; + } + Vector3i globalPos = pos.mul(1 << scale, new Vector3i()); + if (!requiredChunks.containsKey(globalPos)) { + addChunk(globalPos, scale); + } + } + } + } + lodRegions = newLodRegions; + probablyLoadedRegion = newProbablyLoadedRegion; + possiblyLoadedRegion = newPossiblyLoadedRegion; + } + + public void onRealChunkUnloaded(Vector3ic pos) { + if (chunkLods > 0 && !probablyLoadedRegion.contains(pos) && lodRegions[0].contains(pos) && !requiredChunks.containsKey(pos)) { + addChunk(pos, 0); + } + } + + private void addChunk(Vector3ic pos, int scale) { + if (requiredChunks.containsKey(pos)) { + logger.warn("Duplicate LOD chunk load."); + } + requiredChunks.put(pos, scale); + neededChunks.add(pos); + } + + public void onRealChunkLoaded(Vector3ic pos) { + if (requiredChunks.get(pos) != null && possiblyLoadedRegion.contains(pos) && chunkProvider.isChunkReady(pos)) { + removeChunk(pos); + } + } + + private void removeChunk(Vector3ic pos) { + neededChunks.remove(pos); + requiredChunks.remove(pos); + LodChunk chunk = chunks.remove(pos); + if (chunk != null) { + chunk.disposeMesh(); + } + } + + public Collection getChunks() { + return chunks.values(); + } + + public void shutdown() { + for (Thread thread : generationThreads) { + thread.interrupt(); + } + for (LodChunk chunk : chunks.values()) { + chunk.disposeMesh(); + } + } + + /** + * Make the chunk a bit darker, so that it can be visually distinguished from an ordinary chunk. + */ + private void tintChunk(Chunk chunk) { + for (Vector3ic pos : Chunks.CHUNK_REGION) { + chunk.setSunlight(JomlUtil.from(pos), (byte) (0.75f * chunk.getSunlight(JomlUtil.from(pos)))); + } + } + + public int getChunkLods() { + return chunkLods; + } + + private Vector3i scaleDown(Vector3ic v, int scale) { + return new Vector3i(v.x() >> scale, v.y() >> scale, v.z() >> scale); + } + + private int delay(int previous, int target) { + if (previous < target) { + return target - 1; + } else if (previous == target) { + return target; + } else { + return target + 1; + } + } + + private static class ClosenessComparator implements Comparator { + Vector3i pos; + + ClosenessComparator(Vector3i pos) { + this.pos = pos; + } + + @Override + public int compare(Vector3ic x0, Vector3ic x1) { + return Long.compare(x0.distanceSquared(pos), x1.distanceSquared(pos)); + } + } +} diff --git a/engine/src/main/java/org/terasology/world/chunks/internal/ChunkImpl.java b/engine/src/main/java/org/terasology/world/chunks/internal/ChunkImpl.java index 48523d174d0..84b87b53e99 100644 --- a/engine/src/main/java/org/terasology/world/chunks/internal/ChunkImpl.java +++ b/engine/src/main/java/org/terasology/world/chunks/internal/ChunkImpl.java @@ -60,7 +60,8 @@ public class ChunkImpl implements Chunk { private static final DecimalFormat PERCENT_FORMAT = new DecimalFormat("0.##"); private static final DecimalFormat SIZE_FORMAT = new DecimalFormat("#,###"); - private final Vector3i chunkPos = new Vector3i(); + protected final Vector3i chunkPos = new Vector3i(); + protected BlockRegion region; private BlockManager blockManager; @@ -74,7 +75,6 @@ public class ChunkImpl implements Chunk { private volatile TeraArray[] extraDataSnapshots; private AABBf aabb = new AABBf(); - private BlockRegion region; private boolean disposed; private boolean ready; diff --git a/engine/src/main/java/org/terasology/world/chunks/internal/PreLodChunk.java b/engine/src/main/java/org/terasology/world/chunks/internal/PreLodChunk.java new file mode 100644 index 00000000000..02df6b334a8 --- /dev/null +++ b/engine/src/main/java/org/terasology/world/chunks/internal/PreLodChunk.java @@ -0,0 +1,38 @@ +// Copyright 2021 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.world.chunks.internal; + +import org.joml.Vector3i; +import org.terasology.math.JomlUtil; +import org.terasology.world.block.BlockManager; +import org.terasology.world.block.BlockRegion; +import org.terasology.world.chunks.Chunks; +import org.terasology.world.chunks.blockdata.ExtraBlockDataManager; + +/** + * A chunk that has a full set of data, but will be turned into + * a LOD chunk later. + */ +public class PreLodChunk extends ChunkImpl { + public PreLodChunk(Vector3i pos, BlockManager blockManager, ExtraBlockDataManager extraDataManager) { + super(JomlUtil.from(pos), blockManager, extraDataManager); + Vector3i min = Chunks.CHUNK_SIZE.sub(2, 4, 2, new Vector3i()).mul(pos).sub(1, 2, 1); + region = new BlockRegion(min, min.add(Chunks.CHUNK_SIZE, new Vector3i())); + } + + @Override + public int getChunkWorldOffsetX() { + return chunkPos.x * (Chunks.SIZE_X - 2) - 1; + } + + @Override + public int getChunkWorldOffsetY() { + return chunkPos.y * (Chunks.SIZE_Y - 4) - 2; + } + + @Override + public int getChunkWorldOffsetZ() { + return chunkPos.z * (Chunks.SIZE_Z - 2) - 1; + } +} diff --git a/engine/src/main/java/org/terasology/world/chunks/localChunkProvider/LocalChunkProvider.java b/engine/src/main/java/org/terasology/world/chunks/localChunkProvider/LocalChunkProvider.java index 90d5698a5f5..e63095b4513 100644 --- a/engine/src/main/java/org/terasology/world/chunks/localChunkProvider/LocalChunkProvider.java +++ b/engine/src/main/java/org/terasology/world/chunks/localChunkProvider/LocalChunkProvider.java @@ -63,6 +63,7 @@ import java.util.concurrent.BlockingQueue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Future; +import java.util.function.Consumer; import java.util.stream.Collectors; import java.util.stream.StreamSupport; @@ -441,7 +442,7 @@ public void purgeWorld() { loadingPipeline = new ChunkProcessingPipeline(this::getChunk, relevanceSystem.createChunkTaskComporator()); loadingPipeline.addStage( ChunkTaskProvider.create("Chunk generate internal lightning", - InternalLightProcessor::generateInternalLighting)) + (Consumer) InternalLightProcessor::generateInternalLighting)) .addStage(ChunkTaskProvider.create("Chunk deflate", Chunk::deflate)) .addStage(ChunkTaskProvider.createMulti("Light merging", chunks -> { @@ -484,7 +485,7 @@ public void setRelevanceSystem(RelevanceSystem relevanceSystem) { loadingPipeline = new ChunkProcessingPipeline(this::getChunk, relevanceSystem.createChunkTaskComporator()); loadingPipeline.addStage( ChunkTaskProvider.create("Chunk generate internal lightning", - InternalLightProcessor::generateInternalLighting)) + (Consumer) InternalLightProcessor::generateInternalLighting)) .addStage(ChunkTaskProvider.create("Chunk deflate", Chunk::deflate)) .addStage(ChunkTaskProvider.createMulti("Light merging", chunks -> { diff --git a/engine/src/main/java/org/terasology/world/chunks/pipeline/ChunkProcessingPipeline.java b/engine/src/main/java/org/terasology/world/chunks/pipeline/ChunkProcessingPipeline.java index fafc5f1eccc..d13c4d9aaad 100644 --- a/engine/src/main/java/org/terasology/world/chunks/pipeline/ChunkProcessingPipeline.java +++ b/engine/src/main/java/org/terasology/world/chunks/pipeline/ChunkProcessingPipeline.java @@ -25,6 +25,7 @@ import java.util.Objects; import java.util.Set; import java.util.concurrent.Callable; +import java.util.concurrent.CancellationException; import java.util.concurrent.CompletionService; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorCompletionService; @@ -141,6 +142,7 @@ private void onStageDone(PositionFuture future, ChunkProcessingInfo chunk chunkProcessingInfo.getPosition(), stageName), e); chunkProcessingInfo.getExternalFuture().setException(e); + } catch (CancellationException ignored) { } } @@ -258,6 +260,10 @@ public void restart() { */ public void stopProcessingAt(Vector3ic pos) { ChunkProcessingInfo removed = chunkProcessingInfoMap.remove(pos); + if (removed == null) { + return; + } + removed.getExternalFuture().cancel(true); Future currentFuture = removed.getCurrentFuture(); diff --git a/engine/src/main/java/org/terasology/world/chunks/remoteChunkProvider/RemoteChunkProvider.java b/engine/src/main/java/org/terasology/world/chunks/remoteChunkProvider/RemoteChunkProvider.java index f4041d90508..a02b64f7230 100644 --- a/engine/src/main/java/org/terasology/world/chunks/remoteChunkProvider/RemoteChunkProvider.java +++ b/engine/src/main/java/org/terasology/world/chunks/remoteChunkProvider/RemoteChunkProvider.java @@ -40,6 +40,7 @@ import java.util.Map; import java.util.concurrent.BlockingQueue; import java.util.concurrent.Future; +import java.util.function.Consumer; import java.util.stream.Collectors; import java.util.stream.StreamSupport; @@ -72,7 +73,7 @@ public RemoteChunkProvider(BlockManager blockManager, LocalPlayer localPlayer) { loadingPipeline.addStage( ChunkTaskProvider.create("Chunk generate internal lightning", - InternalLightProcessor::generateInternalLighting)) + (Consumer) InternalLightProcessor::generateInternalLighting)) .addStage(ChunkTaskProvider.create("Chunk deflate", Chunk::deflate)) .addStage(ChunkTaskProvider.createMulti("Light merging", chunks -> { diff --git a/engine/src/main/java/org/terasology/world/generation/BaseFacetedWorldGenerator.java b/engine/src/main/java/org/terasology/world/generation/BaseFacetedWorldGenerator.java index 0fbd72fb5bd..63936be340d 100644 --- a/engine/src/main/java/org/terasology/world/generation/BaseFacetedWorldGenerator.java +++ b/engine/src/main/java/org/terasology/world/generation/BaseFacetedWorldGenerator.java @@ -17,6 +17,7 @@ import org.terasology.engine.SimpleUri; import org.terasology.world.chunks.CoreChunk; +import org.terasology.world.generator.ScalableWorldGenerator; import org.terasology.world.generator.WorldConfigurator; import org.terasology.world.generator.WorldGenerator; import org.terasology.world.zones.Zone; @@ -26,7 +27,7 @@ /** * The most commonly used implementation of {@link WorldGenerator} based on the idea of Facets */ -public abstract class BaseFacetedWorldGenerator implements WorldGenerator { +public abstract class BaseFacetedWorldGenerator implements ScalableWorldGenerator { protected WorldBuilder worldBuilder; @@ -75,6 +76,11 @@ public void createChunk(CoreChunk chunk, EntityBuffer buffer) { world.rasterizeChunk(chunk, buffer); } + @Override + public void createChunk(CoreChunk chunk, EntityBuffer buffer, float scale) { + world.rasterizeChunk(chunk, buffer, scale); + } + @Override public WorldConfigurator getConfigurator() { if (configurator == null) { diff --git a/engine/src/main/java/org/terasology/world/generation/RegionImpl.java b/engine/src/main/java/org/terasology/world/generation/RegionImpl.java index 94188e7b44f..93dc6f99a38 100644 --- a/engine/src/main/java/org/terasology/world/generation/RegionImpl.java +++ b/engine/src/main/java/org/terasology/world/generation/RegionImpl.java @@ -17,15 +17,17 @@ public class RegionImpl implements Region, GeneratingRegion { private final BlockRegion region; private final ListMultimap, FacetProvider> facetProviderChains; private final Map, Border3D> borders; + private final float scale; private final TypeMap generatingFacets = TypeMap.create(); private final Set processedProviders = Sets.newHashSet(); private final TypeMap generatedFacets = TypeMap.create(); - public RegionImpl(BlockRegion region, ListMultimap, FacetProvider> facetProviderChains, Map, Border3D> borders) { + public RegionImpl(BlockRegion region, ListMultimap, FacetProvider> facetProviderChains, Map, Border3D> borders, float scale) { this.region = region; this.facetProviderChains = facetProviderChains; this.borders = borders; + this.scale = scale; } @Override @@ -33,7 +35,11 @@ public T getFacet(Class dataType) { T facet = generatedFacets.get(dataType); if (facet == null) { facetProviderChains.get(dataType).stream().filter(provider -> !processedProviders.contains(provider)).forEach(provider -> { - provider.process(this); + if (scale == 1) { + provider.process(this); + } else { + ((ScalableFacetProvider) provider).process(this, scale); + } processedProviders.add(provider); }); facet = generatingFacets.get(dataType); diff --git a/engine/src/main/java/org/terasology/world/generation/ScalableFacetProvider.java b/engine/src/main/java/org/terasology/world/generation/ScalableFacetProvider.java new file mode 100644 index 00000000000..561ff13c2bb --- /dev/null +++ b/engine/src/main/java/org/terasology/world/generation/ScalableFacetProvider.java @@ -0,0 +1,12 @@ +// Copyright 2021 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.world.generation; + +public interface ScalableFacetProvider extends FacetProvider { + void process(GeneratingRegion region, float scale); + + default void process(GeneratingRegion region) { + process(region, 1); + } +} diff --git a/engine/src/main/java/org/terasology/world/generation/ScalableWorldRasterizer.java b/engine/src/main/java/org/terasology/world/generation/ScalableWorldRasterizer.java new file mode 100644 index 00000000000..9be22b107d9 --- /dev/null +++ b/engine/src/main/java/org/terasology/world/generation/ScalableWorldRasterizer.java @@ -0,0 +1,15 @@ +// Copyright 2021 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.world.generation; + +import org.terasology.world.chunks.CoreChunk; + +public interface ScalableWorldRasterizer extends WorldRasterizer { + void generateChunk(CoreChunk chunk, Region chunkRegion, float scale); + + @Override + default void generateChunk(CoreChunk chunk, Region chunkRegion) { + generateChunk(chunk, chunkRegion, 1); + } +} diff --git a/engine/src/main/java/org/terasology/world/generation/World.java b/engine/src/main/java/org/terasology/world/generation/World.java index 61e78acf748..0554bcdeca2 100644 --- a/engine/src/main/java/org/terasology/world/generation/World.java +++ b/engine/src/main/java/org/terasology/world/generation/World.java @@ -11,7 +11,11 @@ */ public interface World { - Region getWorldData(BlockRegion region); + Region getWorldData(BlockRegion region, float scale); + + default Region getWorldData(BlockRegion region) { + return getWorldData(region, 1); + } /** * @return the sea level, measured in blocks. May be used for setting @@ -21,6 +25,8 @@ public interface World { void rasterizeChunk(CoreChunk chunk, EntityBuffer buffer); + void rasterizeChunk(CoreChunk chunk, EntityBuffer buffer, float scale); + /** * @return a new set containing all facet classes */ diff --git a/engine/src/main/java/org/terasology/world/generation/WorldBuilder.java b/engine/src/main/java/org/terasology/world/generation/WorldBuilder.java index ad4302d7f55..bd9c68c285d 100644 --- a/engine/src/main/java/org/terasology/world/generation/WorldBuilder.java +++ b/engine/src/main/java/org/terasology/world/generation/WorldBuilder.java @@ -102,9 +102,19 @@ public World build() { for (FacetProvider provider : providersList) { provider.setSeed(seed); } - ListMultimap, FacetProvider> providerChains = determineProviderChains(); - List orderedRasterizers = ensureRasterizerOrdering(); - return new WorldImpl(providerChains, orderedRasterizers, entityProviders, determineBorders(providerChains, orderedRasterizers), seaLevel); + ListMultimap, FacetProvider> providerChains = determineProviderChains(false); + ListMultimap, FacetProvider> scalableProviderChains = determineProviderChains(true); + List orderedRasterizers = ensureRasterizerOrdering(providerChains, false); + List scalableRasterizers = ensureRasterizerOrdering(scalableProviderChains, true); + return new WorldImpl( + providerChains, + scalableProviderChains, + orderedRasterizers, + scalableRasterizers, + entityProviders, + determineBorders(providerChains, orderedRasterizers), + seaLevel + ); } private Map, Border3D> determineBorders(ListMultimap, FacetProvider> providerChains, List worldRasterizers) { @@ -170,7 +180,7 @@ private Map, Border3D> determineBorders(ListMultimap return borders; } - private ListMultimap, FacetProvider> determineProviderChains() { + private ListMultimap, FacetProvider> determineProviderChains(boolean scalable) { ListMultimap, FacetProvider> result = ArrayListMultimap.create(); Set> facets = new LinkedHashSet<>(); for (FacetProvider provider : providersList) { @@ -186,7 +196,7 @@ private ListMultimap, FacetProvider> determineProvid } } for (Class facet : facets) { - determineProviderChainFor(facet, result); + determineProviderChainFor(facet, result, scalable); if (logger.isDebugEnabled()) { StringBuilder text = new StringBuilder(facet.getSimpleName()); text.append(" --> "); @@ -204,7 +214,7 @@ private ListMultimap, FacetProvider> determineProvid return result; } - private void determineProviderChainFor(Class facet, ListMultimap, FacetProvider> result) { + private void determineProviderChainFor(Class facet, ListMultimap, FacetProvider> result, boolean scalable) { if (result.containsKey(facet)) { return; } @@ -216,19 +226,31 @@ private void determineProviderChainFor(Class facet, ListMu // first add all @Produces facet providers FacetProvider producer = null; for (FacetProvider provider : providersList) { - if (producesFacet(provider, facet)) { + if (producesFacet(provider, facet) && (!scalable || provider instanceof ScalableFacetProvider)) { if (producer != null) { logger.warn("Facet already produced by {} and overwritten by {}", producer, provider); } // add all required facets for producing provider for (Facet requirement : requiredFacets(provider)) { - determineProviderChainFor(requirement.value(), result); - orderedProviders.addAll(result.get(requirement.value())); + determineProviderChainFor(requirement.value(), result, scalable); + List requirementChain = result.get(requirement.value()); + if (requirementChain != null) { + orderedProviders.addAll(requirementChain); + } else { + facetCalculationInProgress.remove(facet); + return; + } } // add all updated facets for producing provider for (Facet updated : updatedFacets(provider)) { - determineProviderChainFor(updated.value(), result); - orderedProviders.addAll(result.get(updated.value())); + determineProviderChainFor(updated.value(), result, scalable); + List requirementChain = result.get(updated.value()); + if (requirementChain != null) { + orderedProviders.addAll(requirementChain); + } else { + facetCalculationInProgress.remove(facet); + return; + } } orderedProviders.add(provider); producer = provider; @@ -236,15 +258,25 @@ private void determineProviderChainFor(Class facet, ListMu } if (producer == null) { - logger.warn("No facet provider found that produces {}", facet); + if (!scalable) { + logger.warn("No facet provider found that produces {}", facet); + } + facetCalculationInProgress.remove(facet); + return; } // then add all @Updates facet providers - providersList.stream().filter(provider -> updatesFacet(provider, facet)).forEach(provider -> { + providersList.stream().filter(provider -> updatesFacet(provider, facet) && (!scalable || provider instanceof ScalableFacetProvider)).forEach(provider -> { + Set localOrderedProviders = Sets.newLinkedHashSet(); // add all required facets for updating provider for (Facet requirement : requiredFacets(provider)) { - determineProviderChainFor(requirement.value(), result); - orderedProviders.addAll(result.get(requirement.value())); + determineProviderChainFor(requirement.value(), result, scalable); + List requirementChain = result.get(requirement.value()); + if (requirementChain != null) { + localOrderedProviders.addAll(result.get(requirement.value())); + } else { + return; + } } // the provider updates this and other facets // just add producers for the other facets @@ -252,10 +284,15 @@ private void determineProviderChainFor(Class facet, ListMu for (FacetProvider fp : providersList) { // only add @Produces providers to avoid infinite recursion if (producesFacet(fp, updated.value())) { - orderedProviders.add(fp); + if (!scalable || fp instanceof ScalableFacetProvider) { + localOrderedProviders.add(fp); + } else { + return; + } } } } + orderedProviders.addAll(localOrderedProviders); orderedProviders.add(provider); }); result.putAll(facet, orderedProviders); @@ -301,7 +338,7 @@ private boolean updatesFacet(FacetProvider provider, Class // Ensure that rasterizers that must run after others are in the correct order. This ensures that blocks from // the dependent raterizer are not being overwritten by any antecedent rasterizer. // TODO: This will only handle first-order dependencies and does not check for circular dependencies - private List ensureRasterizerOrdering() { + private List ensureRasterizerOrdering(ListMultimap, FacetProvider> providerChains, boolean scalable) { List orderedRasterizers = Lists.newArrayList(); Set> addedRasterizers = new HashSet<>(); @@ -318,21 +355,36 @@ private List ensureRasterizerOrdering() { // Add all antecedents to the list first antecedents.forEach(dependency -> { if (!addedRasterizers.contains(dependency.getClass())) { - orderedRasterizers.add(dependency); + tryAddRasterizer(orderedRasterizers, dependency, providerChains, scalable); addedRasterizers.add(dependency.getClass()); } }); // Then add this one - orderedRasterizers.add(rasterizer); + tryAddRasterizer(orderedRasterizers, rasterizer, providerChains, scalable); } else if (!addedRasterizers.contains(rasterizer.getClass())) { - orderedRasterizers.add(rasterizer); + tryAddRasterizer(orderedRasterizers, rasterizer, providerChains, scalable); addedRasterizers.add(rasterizer.getClass()); } } return orderedRasterizers; } + private void tryAddRasterizer(List orderedRasterizers, WorldRasterizer rasterizer, ListMultimap, FacetProvider> providerChains, boolean scalable) { + if (scalable && !(rasterizer instanceof ScalableWorldRasterizer)) { + return; + } + Requires requires = rasterizer.getClass().getAnnotation(Requires.class); + if (requires != null) { + for (Facet facet : requires.value()) { + if (!providerChains.containsKey(facet.value())) { + return; + } + } + } + orderedRasterizers.add(rasterizer); + } + public FacetedWorldConfigurator createConfigurator() { List configurables = new ArrayList<>(); diff --git a/engine/src/main/java/org/terasology/world/generation/WorldImpl.java b/engine/src/main/java/org/terasology/world/generation/WorldImpl.java index 3ad2055bb8c..7da5418ed97 100644 --- a/engine/src/main/java/org/terasology/world/generation/WorldImpl.java +++ b/engine/src/main/java/org/terasology/world/generation/WorldImpl.java @@ -17,26 +17,32 @@ */ public class WorldImpl implements World { private final ListMultimap, FacetProvider> facetProviderChains; + private final ListMultimap, FacetProvider> scalableFacetProviderChains; private final List worldRasterizers; + private final List scalableWorldRasterizers; private final List entityProviders; private final Map, Border3D> borders; private final int seaLevel; public WorldImpl(ListMultimap, FacetProvider> facetProviderChains, + ListMultimap, FacetProvider> scalableFacetProviderChains, List worldRasterizers, + List scalableWorldRasterizers, List entityProviders, Map, Border3D> borders, int seaLevel) { this.facetProviderChains = facetProviderChains; + this.scalableFacetProviderChains = scalableFacetProviderChains; this.worldRasterizers = worldRasterizers; + this.scalableWorldRasterizers = scalableWorldRasterizers; this.entityProviders = entityProviders; this.borders = borders; this.seaLevel = seaLevel; } @Override - public Region getWorldData(BlockRegion region) { - return new RegionImpl(region, facetProviderChains, borders); + public Region getWorldData(BlockRegion region, float scale) { + return new RegionImpl(region, scale == 1 ? facetProviderChains : scalableFacetProviderChains, borders, scale); } @Override @@ -46,7 +52,7 @@ public int getSeaLevel() { @Override public void rasterizeChunk(CoreChunk chunk, EntityBuffer buffer) { - Region chunkRegion = getWorldData(chunk.getRegion()); + Region chunkRegion = getWorldData(chunk.getRegion(), 1); for (WorldRasterizer rasterizer : worldRasterizers) { rasterizer.generateChunk(chunk, chunkRegion); } @@ -55,6 +61,17 @@ public void rasterizeChunk(CoreChunk chunk, EntityBuffer buffer) { } } + @Override + public void rasterizeChunk(CoreChunk chunk, EntityBuffer buffer, float scale) { + Region chunkRegion = getWorldData(chunk.getRegion(), scale); + for (WorldRasterizer rasterizer : scalableWorldRasterizers) { + ((ScalableWorldRasterizer) rasterizer).generateChunk(chunk, chunkRegion, scale); + } + for (EntityProvider entityProvider : entityProviders) { + entityProvider.process(chunkRegion, buffer); + } + } + @Override public Set> getAllFacets() { return Sets.newHashSet(facetProviderChains.keySet()); diff --git a/engine/src/main/java/org/terasology/world/generator/ScalableWorldGenerator.java b/engine/src/main/java/org/terasology/world/generator/ScalableWorldGenerator.java new file mode 100644 index 00000000000..8cb6b298de5 --- /dev/null +++ b/engine/src/main/java/org/terasology/world/generator/ScalableWorldGenerator.java @@ -0,0 +1,17 @@ +// Copyright 2021 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.world.generator; + +import org.terasology.world.chunks.CoreChunk; +import org.terasology.world.generation.EntityBuffer; + +public interface ScalableWorldGenerator extends WorldGenerator { + /** + * Generates all contents of given chunk + * @param chunk Chunk to generate + * @param buffer Buffer to queue entities to spawn to + * @param scale The scale to generate at (larger numbers make the world's features smaller) + */ + void createChunk(CoreChunk chunk, EntityBuffer buffer, float scale); +} diff --git a/engine/src/main/java/org/terasology/world/propagation/PropagationRules.java b/engine/src/main/java/org/terasology/world/propagation/PropagationRules.java index 57912413ecc..c313e07a9bf 100644 --- a/engine/src/main/java/org/terasology/world/propagation/PropagationRules.java +++ b/engine/src/main/java/org/terasology/world/propagation/PropagationRules.java @@ -61,9 +61,10 @@ default byte getFixedValue(Block block, Vector3i pos) { * @param existingValue The value to propagate * @param side The side the value is leaving by * @param from The block the value is leaving + * @param scale The scale of the chunk * @return The new value to set at the block position */ - byte propagateValue(byte existingValue, Side side, Block from); + byte propagateValue(byte existingValue, Side side, Block from, int scale); /** * @return The maximum value possible for this data diff --git a/engine/src/main/java/org/terasology/world/propagation/StandardBatchPropagator.java b/engine/src/main/java/org/terasology/world/propagation/StandardBatchPropagator.java index 811d3d4f4d9..59e89727f80 100644 --- a/engine/src/main/java/org/terasology/world/propagation/StandardBatchPropagator.java +++ b/engine/src/main/java/org/terasology/world/propagation/StandardBatchPropagator.java @@ -28,6 +28,7 @@ public class StandardBatchPropagator implements BatchPropagator { private PropagationRules rules; private PropagatorWorldView world; + private int scale; /* Queues are stored in reverse order. Ie, strongest light is 0. */ private Set[] reduceQueues; @@ -36,8 +37,13 @@ public class StandardBatchPropagator implements BatchPropagator { private Map chunkEdgeDeltas = Maps.newEnumMap(Side.class); public StandardBatchPropagator(PropagationRules rules, PropagatorWorldView world) { + this(rules, world, 1); + } + + public StandardBatchPropagator(PropagationRules rules, PropagatorWorldView world, int scale) { this.world = world; this.rules = rules; + this.scale = scale; for (Side side : Side.getAllSides()) { Vector3i delta = new Vector3i(side.direction()); @@ -116,7 +122,7 @@ private void reviewChange(BlockChange blockChange) { reduce(blockChangePosition, existingValue); side.getAdjacentPos(blockChangePosition, adjPos); byte adjValue = world.getValueAt(adjPos); - if (adjValue == rules.propagateValue(existingValue, side, blockChange.getFrom())) { + if (adjValue == rules.propagateValue(existingValue, side, blockChange.getFrom(), scale)) { reduce(adjPos, adjValue); } @@ -157,7 +163,7 @@ private void purge(Vector3ic pos, byte oldValue) { Vector3i adjPos = new Vector3i(); for (Side side : Side.getAllSides()) { /* Handle this value being reset to the default by updating sides as needed */ - byte expectedValue = rules.propagateValue(oldValue, side, block); + byte expectedValue = rules.propagateValue(oldValue, side, block, scale); if (rules.canSpreadOutOf(block, side)) { side.getAdjacentPos(pos, adjPos); byte adjValue = world.getValueAt(adjPos); @@ -225,7 +231,7 @@ private void push(Vector3ic pos, byte value) { Block block = world.getBlockAt(pos); Vector3i adjPos = new Vector3i(); for (Side side : Side.getAllSides()) { - byte propagatedValue = rules.propagateValue(value, side, block); + byte propagatedValue = rules.propagateValue(value, side, block, scale); if (rules.canSpreadOutOf(block, side)) { side.getAdjacentPos(pos, adjPos); diff --git a/engine/src/main/java/org/terasology/world/propagation/SunlightRegenBatchPropagator.java b/engine/src/main/java/org/terasology/world/propagation/SunlightRegenBatchPropagator.java index db4d450a67a..f78d6d2ef07 100644 --- a/engine/src/main/java/org/terasology/world/propagation/SunlightRegenBatchPropagator.java +++ b/engine/src/main/java/org/terasology/world/propagation/SunlightRegenBatchPropagator.java @@ -141,7 +141,7 @@ private void push(Vector3ic pos, byte value) { Block block = regenWorld.getBlockAt(pos); Vector3i position = new Vector3i(pos); while (regenRules.canSpreadOutOf(block, Side.BOTTOM)) { - regenValue = regenRules.propagateValue(regenValue, Side.BOTTOM, block); + regenValue = regenRules.propagateValue(regenValue, Side.BOTTOM, block, 1); position.y -= 1; byte adjValue = regenWorld.getValueAt(position); if (adjValue < regenValue && adjValue != PropagatorWorldView.UNAVAILABLE) { diff --git a/engine/src/main/java/org/terasology/world/propagation/light/InternalLightProcessor.java b/engine/src/main/java/org/terasology/world/propagation/light/InternalLightProcessor.java index f68a57a3617..da3c18fd46b 100644 --- a/engine/src/main/java/org/terasology/world/propagation/light/InternalLightProcessor.java +++ b/engine/src/main/java/org/terasology/world/propagation/light/InternalLightProcessor.java @@ -39,9 +39,13 @@ private InternalLightProcessor() { } public static void generateInternalLighting(LitChunk chunk) { - populateSunlightRegen(chunk); - populateSunlight(chunk); - populateLight(chunk); + generateInternalLighting(chunk, 1); + } + + public static void generateInternalLighting(LitChunk chunk, int scale) { + populateSunlightRegen(chunk, scale); + populateSunlight(chunk, scale); + populateLight(chunk, scale); } /** @@ -49,8 +53,8 @@ public static void generateInternalLighting(LitChunk chunk) { * * @param chunk The chunk to populate through */ - private static void populateLight(LitChunk chunk) { - BatchPropagator lightPropagator = new StandardBatchPropagator(LIGHT_RULES, new SingleChunkView(LIGHT_RULES, chunk)); + private static void populateLight(LitChunk chunk, int scale) { + BatchPropagator lightPropagator = new StandardBatchPropagator(LIGHT_RULES, new SingleChunkView(LIGHT_RULES, chunk), scale); Vector3i pos = new Vector3i(); for (int x = 0; x < Chunks.SIZE_X; x++) { for (int z = 0; z < Chunks.SIZE_Z; z++) { @@ -71,15 +75,15 @@ private static void populateLight(LitChunk chunk) { * * @param chunk The chunk to set in */ - private static void populateSunlight(LitChunk chunk) { + private static void populateSunlight(LitChunk chunk, int scale) { PropagationRules sunlightRules = new SunlightPropagationRules(chunk); - BatchPropagator lightPropagator = new StandardBatchPropagator(sunlightRules, new SingleChunkView(sunlightRules, chunk)); + BatchPropagator lightPropagator = new StandardBatchPropagator(sunlightRules, new SingleChunkView(sunlightRules, chunk), scale); Vector3i pos = new Vector3i(); for (int x = 0; x < Chunks.SIZE_X; x++) { for (int z = 0; z < Chunks.SIZE_Z; z++) { /* Start at the bottom of the chunk and then move up until the max sunlight level */ - for (int y = 0; y < Chunks.MAX_SUNLIGHT; y++) { + for (int y = 0; y < Chunks.SIZE_Y; y++) { pos.set(x, y, z); Block block = chunk.getBlock(x, y, z); byte light = sunlightRules.getFixedValue(block, pos); @@ -98,18 +102,18 @@ private static void populateSunlight(LitChunk chunk) { * * @param chunk The chunk to populate the regeneration values through */ - private static void populateSunlightRegen(LitChunk chunk) { + private static void populateSunlightRegen(LitChunk chunk, int scale) { int top = Chunks.SIZE_Y - 1; /* Scan through each column in the chunk & propagate light from the top down */ for (int x = 0; x < Chunks.SIZE_X; x++) { for (int z = 0; z < Chunks.SIZE_Z; z++) { - byte regen = 0; + byte regen = chunk.getSunlightRegen(x, top, z); Block lastBlock = chunk.getBlock(x, top, z); for (int y = top - 1; y >= 0; y--) { Block block = chunk.getBlock(x, y, z); /* If the regeneration can propagate down into this block */ if (SUNLIGHT_REGEN_RULES.canSpreadOutOf(lastBlock, Side.BOTTOM) && SUNLIGHT_REGEN_RULES.canSpreadInto(block, Side.TOP)) { - regen = SUNLIGHT_REGEN_RULES.propagateValue(regen, Side.BOTTOM, lastBlock); + regen = SUNLIGHT_REGEN_RULES.propagateValue(regen, Side.BOTTOM, lastBlock, scale); chunk.setSunlightRegen(x, y, z, regen); } else { regen = 0; diff --git a/engine/src/main/java/org/terasology/world/propagation/light/LightPropagationRules.java b/engine/src/main/java/org/terasology/world/propagation/light/LightPropagationRules.java index 4741edcbc51..461b4938c5e 100644 --- a/engine/src/main/java/org/terasology/world/propagation/light/LightPropagationRules.java +++ b/engine/src/main/java/org/terasology/world/propagation/light/LightPropagationRules.java @@ -44,8 +44,8 @@ public byte getFixedValue(Block block, Vector3ic pos) { * {@inheritDoc} */ @Override - public byte propagateValue(byte existingValue, Side side, Block from) { - return (byte) (existingValue - 1); + public byte propagateValue(byte existingValue, Side side, Block from, int scale) { + return (byte) Math.max(existingValue - scale, 0); } /** diff --git a/engine/src/main/java/org/terasology/world/propagation/light/SunlightPropagationRules.java b/engine/src/main/java/org/terasology/world/propagation/light/SunlightPropagationRules.java index 23952337ce9..d50541fd8ef 100644 --- a/engine/src/main/java/org/terasology/world/propagation/light/SunlightPropagationRules.java +++ b/engine/src/main/java/org/terasology/world/propagation/light/SunlightPropagationRules.java @@ -56,8 +56,8 @@ public byte getFixedValue(Block block, Vector3ic pos) { * {@inheritDoc} */ @Override - public byte propagateValue(byte existingValue, Side side, Block from) { - return (byte) Math.max(existingValue - 1, 0); + public byte propagateValue(byte existingValue, Side side, Block from, int scale) { + return (byte) Math.max(existingValue - scale, 0); } /** diff --git a/engine/src/main/java/org/terasology/world/propagation/light/SunlightRegenPropagationRules.java b/engine/src/main/java/org/terasology/world/propagation/light/SunlightRegenPropagationRules.java index 5c93781a486..a663c2858cf 100644 --- a/engine/src/main/java/org/terasology/world/propagation/light/SunlightRegenPropagationRules.java +++ b/engine/src/main/java/org/terasology/world/propagation/light/SunlightRegenPropagationRules.java @@ -45,9 +45,9 @@ public byte getFixedValue(Block block, Vector3ic pos) { * {@inheritDoc} */ @Override - public byte propagateValue(byte existingValue, Side side, Block from) { + public byte propagateValue(byte existingValue, Side side, Block from, int scale) { if (side == Side.BOTTOM) { - return (existingValue == Chunks.MAX_SUNLIGHT_REGEN) ? Chunks.MAX_SUNLIGHT_REGEN : (byte) (existingValue + 1); + return (byte) Math.min(Chunks.MAX_SUNLIGHT_REGEN, existingValue + scale); } return 0; } diff --git a/engine/src/main/resources/assets/i18n/menu.lang b/engine/src/main/resources/assets/i18n/menu.lang index 403cb143d3e..8bbef966eae 100644 --- a/engine/src/main/resources/assets/i18n/menu.lang +++ b/engine/src/main/resources/assets/i18n/menu.lang @@ -80,6 +80,7 @@ "category-nui": "category-nui", "change-keybind-popup-message": "change-keybind-popup-message", "change-keybind-popup-title": "change-keybind-popup-title", + "chunk-lods": "chunk-lods", "clamp-lighting": "clamp-lighting", "cloud-shadows": "cloud-shadows", "config": "config", diff --git a/engine/src/main/resources/assets/i18n/menu_en.lang b/engine/src/main/resources/assets/i18n/menu_en.lang index 3b243c704f6..4117a0f6509 100644 --- a/engine/src/main/resources/assets/i18n/menu_en.lang +++ b/engine/src/main/resources/assets/i18n/menu_en.lang @@ -81,6 +81,7 @@ "category-nui": "NUI", "change-keybind-popup-message": "is already bound to an action. Rebind anyway?", "change-keybind-popup-title": "Key already bound", + "chunk-lods": "Chunk LODs", "clamp-lighting": "Clamp Lighting", "cloud-shadows": "Cloud Shadows", "config": "Configure", diff --git a/engine/src/main/resources/assets/ui/menu/videoMenuScreen.ui b/engine/src/main/resources/assets/ui/menu/videoMenuScreen.ui index 47dfa93ca89..614504b7270 100644 --- a/engine/src/main/resources/assets/ui/menu/videoMenuScreen.ui +++ b/engine/src/main/resources/assets/ui/menu/videoMenuScreen.ui @@ -222,6 +222,14 @@ "text": "${engine:menu#reset-fov-amount}", "id": "fovReset" }, + { + "type": "UILabel", + "text": "${engine:menu#chunk-lods}: " + }, + { + "type": "UISlider", + "id": "chunkLods" + }, { "type": "UILabel", "text": "${engine:menu#framerate-limit}: "