From f24bc112c36b067789e024b004f62798c940de03 Mon Sep 17 00:00:00 2001 From: James Seibel Date: Sat, 22 Jul 2023 11:50:08 -0500 Subject: [PATCH] Create a new lighting engine --- .../common/DhApiWorldGenerationConfig.java | 2 +- .../distanthorizons/core/config/Config.java | 10 +- .../core/enums/ELodDirection.java | 17 +- .../core/generation/DhLightingEngine.java | 226 ++++++++++++++++++ .../distanthorizons/core/pos/DhBlockPos.java | 24 +- .../block/IBlockStateWrapper.java | 15 +- .../chunk/IChunkWrapper.java | 38 ++- .../assets/distanthorizons/lang/en_us.json | 4 +- 8 files changed, 308 insertions(+), 28 deletions(-) create mode 100644 core/src/main/java/com/seibel/distanthorizons/core/generation/DhLightingEngine.java diff --git a/core/src/main/java/com/seibel/distanthorizons/core/api/external/methods/config/common/DhApiWorldGenerationConfig.java b/core/src/main/java/com/seibel/distanthorizons/core/api/external/methods/config/common/DhApiWorldGenerationConfig.java index 45971c836..b69711a46 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/api/external/methods/config/common/DhApiWorldGenerationConfig.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/api/external/methods/config/common/DhApiWorldGenerationConfig.java @@ -52,7 +52,7 @@ public class DhApiWorldGenerationConfig implements IDhApiWorldGenerationConfig @Override public IDhApiConfigValue lightingEngine() - { return new DhApiConfigValue<>(WorldGenerator.lightingEngine); } + { return new DhApiConfigValue<>(WorldGenerator.worldGenLightingEngine); } } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/config/Config.java b/core/src/main/java/com/seibel/distanthorizons/core/config/Config.java index 33f325060..c6c788438 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/config/Config.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/config/Config.java @@ -644,15 +644,15 @@ public class Config */ .build(); - public static ConfigEntry lightingEngine = new ConfigEntry.Builder() - .set(ELightGenerationMode.MINECRAFT) + public static ConfigEntry worldGenLightingEngine = new ConfigEntry.Builder() + .set(ELightGenerationMode.DISTANT_HORIZONS) .comment("" - + " How should distant generation chunk lighting be generated? \n" + + " How should Distant Horizons world generation chunk lighting be handled? \n" + "\n" + ELightGenerationMode.MINECRAFT + ": Use Minecraft's lighting engine to generate chunk lighting. \n" + " Generally higher quality; but may crash MC's lighting engine if there is an issue. \n" - + ELightGenerationMode.DISTANT_HORIZONS + ": Uses Distant Horizons' lighting engine to estimate chunk lighting. \n" - + " Generally lower quality; but more stable for large numbers of world generator threads. \n" + + ELightGenerationMode.DISTANT_HORIZONS + ": Uses Distant Horizons' lighting engine to generate chunk lighting. \n" + + " May not exactly match MC's, but is more stable for large numbers of world generator threads. \n" + "\n" + "This will effect generation speed, but not rendering performance.") .build(); diff --git a/core/src/main/java/com/seibel/distanthorizons/core/enums/ELodDirection.java b/core/src/main/java/com/seibel/distanthorizons/core/enums/ELodDirection.java index 4ffd93525..2c62dbd1d 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/enums/ELodDirection.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/enums/ELodDirection.java @@ -29,7 +29,7 @@ import com.seibel.distanthorizons.coreapi.util.math.Vec3i; /** * An (almost) exact copy of Minecraft's - * Direction enum. + * Direction enum.

* * Up
* Down
@@ -49,7 +49,12 @@ public enum ELodDirection SOUTH(3, 2, 0, "south", ELodDirection.AxisDirection.POSITIVE, ELodDirection.Axis.Z, new Vec3i(0, 0, 1)), WEST(4, 5, 1, "west", ELodDirection.AxisDirection.NEGATIVE, ELodDirection.Axis.X, new Vec3i(-1, 0, 0)), EAST(5, 4, 3, "east", ELodDirection.AxisDirection.POSITIVE, ELodDirection.Axis.X, new Vec3i(1, 0, 0)); - public static final ELodDirection[] DIRECTIONS = new ELodDirection[] { + + /** + * Up, Down, West, East, North, South
+ * Similar to {@link ELodDirection#OPPOSITE_DIRECTIONS}, just with a different order + */ + public static final ELodDirection[] CARDINAL_DIRECTIONS = new ELodDirection[] { ELodDirection.UP, ELodDirection.DOWN, ELodDirection.WEST, @@ -57,6 +62,10 @@ public enum ELodDirection ELodDirection.NORTH, ELodDirection.SOUTH }; + /** + * Up, Down, South, North, East, West
+ * Similar to {@link ELodDirection#CARDINAL_DIRECTIONS}, just with a different order + */ public static final ELodDirection[] OPPOSITE_DIRECTIONS = new ELodDirection[] { ELodDirection.UP, ELodDirection.DOWN, @@ -64,12 +73,14 @@ public enum ELodDirection ELodDirection.NORTH, ELodDirection.EAST, ELodDirection.WEST }; - /** North, South, East, West */ + + /** North, South, East, West */ // TODO rename to state this is just X/Z or flat directions public static final ELodDirection[] ADJ_DIRECTIONS = new ELodDirection[] { ELodDirection.EAST, ELodDirection.WEST, ELodDirection.SOUTH, ELodDirection.NORTH }; + // private final int data3d; // private final int oppositeIndex; // private final int data2d; diff --git a/core/src/main/java/com/seibel/distanthorizons/core/generation/DhLightingEngine.java b/core/src/main/java/com/seibel/distanthorizons/core/generation/DhLightingEngine.java new file mode 100644 index 000000000..6ea2d2380 --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/generation/DhLightingEngine.java @@ -0,0 +1,226 @@ +package com.seibel.distanthorizons.core.generation; + +import com.seibel.distanthorizons.core.enums.ELodDirection; +import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; +import com.seibel.distanthorizons.core.pos.DhBlockPos; +import com.seibel.distanthorizons.core.pos.DhChunkPos; +import com.seibel.distanthorizons.core.util.LodUtil; +import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper; +import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper; +import org.apache.logging.log4j.Logger; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; + +/** + * This logic was roughly based on + * Starlight's technical documentation + * although there were some changes due to how our lighting engine works in comparison to Minecraft's. + */ +public class DhLightingEngine +{ + private static final Logger LOGGER = DhLoggerBuilder.getLogger(); + public static final DhLightingEngine INSTANCE = new DhLightingEngine(); + + + private DhLightingEngine() { } + + + + /** + * Note: depending on the implementation of {@link IChunkWrapper#setDhBlockLight(int, int, int, int)} and {@link IChunkWrapper#setDhSkyLight(int, int, int, int)} + * the light values may be stored in the wrapper itself instead of the wrapped chunk object. + * If that is the case unwrapping the chunk will undo any work this method did. + * + * @param centerChunk the chunk we want to apply lighting to + * @param nearbyChunkList should also contain centerChunk + * @param maxSkyLight should be a value between 0 and 15 + */ + public void lightChunks(IChunkWrapper centerChunk, List nearbyChunkList, int maxSkyLight) + { + DhChunkPos centerChunkPos = centerChunk.getChunkPos(); + + HashMap chunksByChunkPos = new HashMap<>(9); + LinkedList blockLightPosQueue = new LinkedList<>(); + LinkedList skyLightPosQueue = new LinkedList<>(); + + // generate the list of chunk pos we need, + // currently a 3x3 grid + HashSet requestedAdjacentPositions = new HashSet<>(9); + for (int xOffset = -1; xOffset <= 1; xOffset++) + { + for (int zOffset = -1; zOffset <= 1; zOffset++) + { + DhChunkPos adjacentPos = new DhChunkPos(centerChunkPos.x+xOffset, centerChunkPos.z+zOffset); + requestedAdjacentPositions.add(adjacentPos); + } + } + + + // find all adjacent chunks + // and get any necessary info from them + for (IChunkWrapper chunk : nearbyChunkList) + { + if (chunk != null && requestedAdjacentPositions.contains(chunk.getChunkPos())) + { + // remove the newly found position + requestedAdjacentPositions.remove(chunk.getChunkPos()); + + // add the adjacent chunk + chunksByChunkPos.put(chunk.getChunkPos(), chunk); + + + + // get and set the adjacent chunk's initial block lights + List blockLightPosList = chunk.getBlockLightPosList(); + for (DhBlockPos blockLightPos : blockLightPosList) + { + // get the light + DhBlockPos relLightBlockPos = blockLightPos.convertToChunkRelativePos(); + IBlockStateWrapper blockState = chunk.getBlockState(relLightBlockPos); + int lightValue = blockState.getLightEmission(); + blockLightPosQueue.add(new LightPos(blockLightPos, lightValue)); + + // set the light + DhBlockPos relBlockPos = blockLightPos.convertToChunkRelativePos(); + chunk.setDhBlockLight(relBlockPos.x, relBlockPos.y, relBlockPos.z, lightValue); + } + + + // get and set the adjacent chunk's initial skylights, + // if the dimension has skylights + if (maxSkyLight > 0) + { + // get the adjacent chunk's sky lights + for (int relX = 0; relX < LodUtil.CHUNK_WIDTH; relX++) // relative block pos + { + for (int relZ = 0; relZ < LodUtil.CHUNK_WIDTH; relZ++) + { + // get the light + int maxY = chunk.getLightBlockingHeightMapValue(relX, relZ); + DhBlockPos skyLightPos = new DhBlockPos(chunk.getMinX() + relX, maxY, chunk.getMinZ() + relZ); + skyLightPosQueue.add(new LightPos(skyLightPos, maxSkyLight)); + + // set the light + DhBlockPos relBlockPos = skyLightPos.convertToChunkRelativePos(); + chunk.setDhSkyLight(relBlockPos.x, relBlockPos.y, relBlockPos.z, maxSkyLight); + } + } + } + } + + + if (requestedAdjacentPositions.isEmpty()) + { + // we found every chunk we needed, we don't need to keep iterating + break; + } + } + + // validate that at least 1 chunk was found + if (chunksByChunkPos.size() == 0) + { + LOGGER.warn("Attempted to generate lighting for position ["+centerChunkPos+"], but neither that chunk nor any adjacent chunks were found. No chunk lighting was performed."); + return; + } + + + + // block light + this.propagateLightPosList(blockLightPosQueue, chunksByChunkPos, + (neighbourChunk, relBlockPos) -> neighbourChunk.getDhBlockLight(relBlockPos.x, relBlockPos.y, relBlockPos.z), + (neighbourChunk, relBlockPos, newLightValue) -> neighbourChunk.setDhBlockLight(relBlockPos.x, relBlockPos.y, relBlockPos.z, newLightValue)); + + // sky light + this.propagateLightPosList(skyLightPosQueue, chunksByChunkPos, + (neighbourChunk, relBlockPos) -> neighbourChunk.getDhSkyLight(relBlockPos.x, relBlockPos.y, relBlockPos.z), + (neighbourChunk, relBlockPos, newLightValue) -> neighbourChunk.setDhSkyLight(relBlockPos.x, relBlockPos.y, relBlockPos.z, newLightValue)); + + + LOGGER.trace("Finished generating lighting for chunk: ["+centerChunkPos+"]"); + } + + /** Applies each {@link LightPos} from the queue to the given set of {@link IChunkWrapper}'s. */ + private void propagateLightPosList( + LinkedList lightPosQueue, HashMap chunksByChunkPos, + IGetLightFunc getLightFunc, ISetLightFunc setLightFunc) + { + // update each light position + while (!lightPosQueue.isEmpty()) + { + LightPos lightPos = lightPosQueue.poll(); + DhBlockPos pos = lightPos.pos; + int lightValue = lightPos.lightValue; + + // propagate the lighting in each cardinal direction, IE: -x, +x, -y, +y, -z, +z + for (ELodDirection direction : ELodDirection.CARDINAL_DIRECTIONS) + { + DhBlockPos neighbourBlockPos = pos.offset(direction); + DhChunkPos neighbourChunkPos = new DhChunkPos(neighbourBlockPos); + // converting the block pos into a relative position is necessary for accessing the light values in the chunk + DhBlockPos relNeighbourBlockPos = neighbourBlockPos.convertToChunkRelativePos(); + + + // only continue if the light position is inside one of our chunks + IChunkWrapper neighbourChunk = chunksByChunkPos.get(neighbourChunkPos); + if (neighbourChunk == null) + { + // the light pos is outside our generator's range, ignore it + continue; + } + + + int currentBlockLight = getLightFunc.getLight(neighbourChunk, relNeighbourBlockPos); + if (currentBlockLight >= (lightValue - 1)) + { + // short circuit for when the light value at this position + // is already greater-than what we could set it + continue; + } + + + IBlockStateWrapper neighbourBlockState = neighbourChunk.getBlockState(relNeighbourBlockPos); + // Math.max(1, ...) is used so that the propagated light level always drops by at least 1, preventing infinite cycles. + int targetLevel = lightValue - Math.max(1, neighbourBlockState.getOpacity()); + if (targetLevel > currentBlockLight) + { + // this position is darker than the new light value, update/set it + setLightFunc.setLight(neighbourChunk, relNeighbourBlockPos, targetLevel); + + // now that light has been propagated to this blockPos + // we need to queue it up so its neighbours can be propagated as well + lightPosQueue.add(new LightPos(neighbourBlockPos, targetLevel)); + } + } + } + + // propagation complete + } + + + + //================// + // helper classes // + //================// + + @FunctionalInterface + interface IGetLightFunc { int getLight(IChunkWrapper chunk, DhBlockPos pos); } + @FunctionalInterface + interface ISetLightFunc { void setLight(IChunkWrapper chunk, DhBlockPos pos, int lightValue); } + + private static class LightPos + { + public final DhBlockPos pos; + public final int lightValue; + + public LightPos(DhBlockPos pos, int lightValue) + { + this.pos = pos; + this.lightValue = lightValue; + } + } + + +} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/pos/DhBlockPos.java b/core/src/main/java/com/seibel/distanthorizons/core/pos/DhBlockPos.java index 7ee18e412..4e9caee13 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/pos/DhBlockPos.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/pos/DhBlockPos.java @@ -19,6 +19,9 @@ package com.seibel.distanthorizons.core.pos; +import com.seibel.distanthorizons.core.enums.ELodDirection; +import com.seibel.distanthorizons.core.util.LodUtil; + import java.util.Objects; public class DhBlockPos { @@ -110,10 +113,23 @@ public class DhBlockPos { return asLong(x, y, z); } - public DhBlockPos offset(int x, int y, int z) - { - return new DhBlockPos(this.x + x, this.y + y, this.z + z); - } + public DhBlockPos offset(ELodDirection direction) { return this.offset(direction.getNormal().x, direction.getNormal().y, direction.getNormal().z); } + public DhBlockPos offset(int x, int y, int z) { return new DhBlockPos(this.x + x, this.y + y, this.z + z); } + + /** Limits the block position to a value between 0 and 15 (inclusive) */ + public DhBlockPos convertToChunkRelativePos() + { + // move the position into the range -15 and +15 + int relX = (this.x % LodUtil.CHUNK_WIDTH); + // if the position is negative move it into the range 0 and 15 + relX = (relX < 0) ? (relX + LodUtil.CHUNK_WIDTH) : relX; + + int relZ = (this.z % LodUtil.CHUNK_WIDTH); + relZ = (relZ < 0) ? (relZ + LodUtil.CHUNK_WIDTH) : relZ; + + // the y value shouldn't need to be changed + return new DhBlockPos(relX, this.y, relZ); + } /** * Can be used to quickly determine the rough distance between two points
diff --git a/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/block/IBlockStateWrapper.java b/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/block/IBlockStateWrapper.java index c231cf116..5df9357b4 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/block/IBlockStateWrapper.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/block/IBlockStateWrapper.java @@ -2,14 +2,17 @@ package com.seibel.distanthorizons.core.wrapperInterfaces.block; import com.seibel.distanthorizons.api.interfaces.block.IDhApiBlockStateWrapper; -/** - * A Minecraft version independent way of handling Blocks. - * - * @author James Seibel - * @version 2022-11-12 - */ +/** A Minecraft version independent way of handling Blocks. */ public interface IBlockStateWrapper extends IDhApiBlockStateWrapper { String serialize(); + /** + * Returning a value of 0 means the block is completely transparent. getBlockLightPosList(); + + + default boolean blockPosInsideChunk(DhBlockPos blockPos) { return this.blockPosInsideChunk(blockPos.x, blockPos.y, blockPos.z); } default boolean blockPosInsideChunk(int x, int y, int z) { return (x >= this.getMinX() && x <= this.getMaxX() && y >= this.getMinBuildHeight() && y < this.getMaxBuildHeight() && z >= this.getMinZ() && z <= this.getMaxZ()); } + default boolean blockPosInsideChunk(DhBlockPos2D blockPos) + { + return (blockPos.x >= this.getMinX() && blockPos.x <= this.getMaxX() + && blockPos.z >= this.getMinZ() && blockPos.z <= this.getMaxZ()); + } boolean doesNearbyChunksExist(); String toString(); @@ -78,11 +101,12 @@ public interface IChunkWrapper extends IBindable return hash; } - - IBlockStateWrapper getBlockState(int x, int y, int z); - IBiomeWrapper getBiome(int x, int y, int z); - - DhChunkPos getChunkPos(); + + + default IBlockStateWrapper getBlockState(DhBlockPos pos) { return this.getBlockState(pos.x, pos.y, pos.z); } + IBlockStateWrapper getBlockState(int relX, int relY, int relZ); + + IBiomeWrapper getBiome(int relX, int relY, int relZ); boolean isStillValid(); } diff --git a/core/src/main/resources/assets/distanthorizons/lang/en_us.json b/core/src/main/resources/assets/distanthorizons/lang/en_us.json index f55ca4754..6479a91aa 100644 --- a/core/src/main/resources/assets/distanthorizons/lang/en_us.json +++ b/core/src/main/resources/assets/distanthorizons/lang/en_us.json @@ -275,9 +275,9 @@ "Distance Generator Mode", "distanthorizons.config.client.advanced.worldGenerator.distantGeneratorMode.@tooltip": "How complicated the generation should be when generating LODs outside the vanilla render distance.\n\n§6§6Fastest:§r Biome only\n§6Best Quality:§r Features (suggested)", - "distanthorizons.config.client.advanced.worldGenerator.lightingEngine": + "distanthorizons.config.client.advanced.worldGenerator.worldGenLightingEngine": "Lighting Engine", - "distanthorizons.config.client.advanced.worldGenerator.lightingEngine.@tooltip": + "distanthorizons.config.client.advanced.worldGenerator.worldGenLightingEngine.@tooltip": "§6Minecraft:§r use Minecraft's lighting engine, gives accurate lighting.\n§6Distant Horizons:§r estimates lighting, shadows won't be as smooth, but is more stable.\n\nIf the LODs appear black, set this to §6Distant Horizons§r.", "distanthorizons.config.client.advanced.worldGenerator.generationPriority": "Generation Priority",