Create a new lighting engine

This commit is contained in:
James Seibel
2023-07-22 11:50:08 -05:00
parent 57664ff1f1
commit f24bc112c3
8 changed files with 308 additions and 28 deletions
@@ -52,7 +52,7 @@ public class DhApiWorldGenerationConfig implements IDhApiWorldGenerationConfig
@Override
public IDhApiConfigValue<ELightGenerationMode> lightingEngine()
{ return new DhApiConfigValue<>(WorldGenerator.lightingEngine); }
{ return new DhApiConfigValue<>(WorldGenerator.worldGenLightingEngine); }
}
@@ -644,15 +644,15 @@ public class Config
*/
.build();
public static ConfigEntry<ELightGenerationMode> lightingEngine = new ConfigEntry.Builder<ELightGenerationMode>()
.set(ELightGenerationMode.MINECRAFT)
public static ConfigEntry<ELightGenerationMode> worldGenLightingEngine = new ConfigEntry.Builder<ELightGenerationMode>()
.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();
@@ -29,7 +29,7 @@ import com.seibel.distanthorizons.coreapi.util.math.Vec3i;
/**
* An (almost) exact copy of Minecraft's
* Direction enum.
* Direction enum. <Br><Br>
*
* Up <Br>
* Down <Br>
@@ -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 <br>
* 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 <br>
* 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;
@@ -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
* <a href="https://github.com/PaperMC/Starlight/blob/acc8ed9634bbe27ec68e8842e420948bfa9707e7/TECHNICAL_DETAILS.md">Starlight's technical documentation</a>
* 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<IChunkWrapper> nearbyChunkList, int maxSkyLight)
{
DhChunkPos centerChunkPos = centerChunk.getChunkPos();
HashMap<DhChunkPos, IChunkWrapper> chunksByChunkPos = new HashMap<>(9);
LinkedList<LightPos> blockLightPosQueue = new LinkedList<>();
LinkedList<LightPos> skyLightPosQueue = new LinkedList<>();
// generate the list of chunk pos we need,
// currently a 3x3 grid
HashSet<DhChunkPos> 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<DhBlockPos> 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<LightPos> lightPosQueue, HashMap<DhChunkPos, IChunkWrapper> 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;
}
}
}
@@ -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<Br>
@@ -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. <br.
* Returning a value of 15 means the block is completely opaque.
*/
int getOpacity();
int getLightEmission();
}
@@ -19,14 +19,20 @@
package com.seibel.distanthorizons.core.wrapperInterfaces.chunk;
import com.seibel.distanthorizons.core.pos.DhBlockPos;
import com.seibel.distanthorizons.core.pos.DhBlockPos2D;
import com.seibel.distanthorizons.core.pos.DhChunkPos;
import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper;
import com.seibel.distanthorizons.coreapi.interfaces.dependencyInjection.IBindable;
import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IBiomeWrapper;
import java.util.List;
public interface IChunkWrapper extends IBindable
{
DhChunkPos getChunkPos();
default int getHeight() { return this.getMaxBuildHeight() - this.getMinBuildHeight(); }
int getMinBuildHeight();
int getMaxBuildHeight();
@@ -46,18 +52,35 @@ public interface IChunkWrapper extends IBindable
long getLongChunkPos();
void setIsDhLightCorrect(boolean isDhLightCorrect);
boolean isLightCorrect();
default int getBlockLight(int x, int y, int z) {return -1;}
default int getSkyLight(int x, int y, int z) {return -1;}
int getDhSkyLight(int relX, int relY, int relZ);
void setDhSkyLight(int relX, int relY, int relZ, int lightValue);
int getDhBlockLight(int relX, int relY, int relZ);
void setDhBlockLight(int relX, int relY, int relZ, int lightValue);
int getBlockLight(int relX, int relY, int relZ);
int getSkyLight(int relX, int relY, int relZ);
List<DhBlockPos> 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();
}
@@ -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",