Skip to content

Commit

Permalink
Rework spatial lookup, prepare support for light shapes.
Browse files Browse the repository at this point in the history
  • Loading branch information
LambdAurora committed Nov 15, 2024
1 parent 6931a08 commit e58249e
Show file tree
Hide file tree
Showing 9 changed files with 254 additions and 77 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import dev.lambdaurora.lambdynlights.engine.DynamicLightSource;
import dev.lambdaurora.lambdynlights.engine.DynamicLightSourceBehavior;
import dev.lambdaurora.lambdynlights.engine.DynamicLightingEngine;
import dev.lambdaurora.lambdynlights.engine.LightCollection;
import dev.lambdaurora.lambdynlights.resource.entity.EntityLightSources;
import dev.lambdaurora.lambdynlights.resource.item.ItemLightSources;
import dev.yumi.commons.event.EventManager;
Expand Down Expand Up @@ -72,6 +73,7 @@ public class LambDynLights implements ClientModInitializer {
public final EntityLightSources entityLightSources = new EntityLightSources(this.itemLightSources);
public final DynamicLightingEngine engine = new DynamicLightingEngine();
private final Set<DynamicLightSourceBehavior> dynamicLightSources = new HashSet<>();
private final Set<LightCollection> lightCollections = new HashSet<>(); // @TODO: check usefulness here
private final List<DynamicLightSourceBehavior> toClear = new ArrayList<>();
private final ReentrantReadWriteLock lightSourcesLock = new ReentrantReadWriteLock();
private long lastUpdate = System.currentTimeMillis();
Expand All @@ -94,7 +96,7 @@ public void onInitializeClient() {

ClientTickEvents.END_WORLD_TICK.register(level -> {
this.lightSourcesLock.writeLock().lock();
this.engine.computeSpatialLookup(this.dynamicLightSources);
this.engine.computeSpatialLookup(this.dynamicLightSources, this.lightCollections);
this.toClear.forEach(source -> source.lambdynlights$scheduleTrackedChunksRebuild(Minecraft.getInstance().levelRenderer));
this.toClear.clear();
this.lightSourcesLock.writeLock().unlock();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import dev.lambdaurora.lambdynlights.LambDynLights;
import net.minecraft.world.level.Level;
import org.jetbrains.annotations.Range;

/**
* Represents a dynamic light source.
Expand Down Expand Up @@ -53,6 +54,7 @@ default boolean isDynamicLightEnabled() {
* {@return the luminance of the light source}
* The maximum is 15, values below 1 are ignored.
*/
@Range(from = 0, to = 15)
int getLuminance();

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

import dev.lambdaurora.lambdynlights.LambDynLights;
import dev.lambdaurora.lambdynlights.accessor.DynamicLightHandlerHolder;
import dev.lambdaurora.lambdynlights.engine.lookup.SpatialLookupEntry;
import dev.lambdaurora.lambdynlights.engine.lookup.SpatialLookupLightSourceEntry;
import net.minecraft.client.Minecraft;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Vec3i;
Expand All @@ -26,12 +28,12 @@
* Represents the dynamic lighting engine.
*
* @author LambdAurora, Akarys
* @version 3.1.0
* @version 4.0.0
* @since 3.1.0
*/
public final class DynamicLightingEngine {
private static final double MAX_RADIUS = 7.75;
private static final double MAX_RADIUS_SQUARED = MAX_RADIUS * MAX_RADIUS;
public static final double MAX_RADIUS = 7.75;
public static final double MAX_RADIUS_SQUARED = MAX_RADIUS * MAX_RADIUS;
private static final int CELL_SIZE = MathHelper.ceil(MAX_RADIUS);
public static final int MAX_LIGHT_SOURCES = 1024;
private static final Vec3i[] CELL_OFFSETS;
Expand Down Expand Up @@ -68,95 +70,71 @@ public double getDynamicLightLevel(@NotNull BlockPos pos) {
double result = 0;

var currentCell = new BlockPos.Mutable(
this.positionToCell(pos.getX()),
this.positionToCell(pos.getY()),
this.positionToCell(pos.getZ())
positionToCell(pos.getX()),
positionToCell(pos.getY()),
positionToCell(pos.getZ())
);
var cell = currentCell.immutable();

for (var cellOffset : CELL_OFFSETS) {
currentCell.setWithOffset(cell, cellOffset);

int key = this.getHashFromKey(this.hashCell(currentCell.getX(), currentCell.getY(), currentCell.getZ()));
int key = hashCell(currentCell.getX(), currentCell.getY(), currentCell.getZ());
int startIndex = this.startIndices[key];

for (int i = startIndex; i < this.spatialLookupEntries.length; i++) {
SpatialLookupEntry entry = this.spatialLookupEntries[i];
if (entry == null || entry.cellKey() != key) break;

result = maxDynamicLightLevel(pos, entry.source(), result);
double light = entry.getDynamicLightLevel(pos);
if (light > result) {
result = light;
}
}
}

return MathHelper.clamp(result, 0, 15);
}

/**
* Returns the dynamic light level generated by the light source at the specified position.
*
* @param pos the position
* @param lightSource the light source
* @param currentLightLevel the current surrounding dynamic light level
* @return the dynamic light level at the specified position
*/
public static double maxDynamicLightLevel(@NotNull BlockPos pos, @NotNull DynamicLightSource lightSource, double currentLightLevel) {
int luminance = lightSource.getLuminance();
if (luminance > 0) {
// Can't use Entity#squaredDistanceTo because of eye Y coordinate.
double dx = pos.getX() - lightSource.getDynamicLightX() + 0.5;
double dy = pos.getY() - lightSource.getDynamicLightY() + 0.5;
double dz = pos.getZ() - lightSource.getDynamicLightZ() + 0.5;

double distanceSquared = dx * dx + dy * dy + dz * dz;
// 7.75 because else we would have to update more chunks and that's not a good idea.
// 15 (max range for blocks) would be too much and a bit cheaty.
if (distanceSquared <= MAX_RADIUS_SQUARED) {
double multiplier = 1.0 - Math.sqrt(distanceSquared) / MAX_RADIUS;
double lightLevel = multiplier * (double) luminance;
if (lightLevel > currentLightLevel) {
return lightLevel;
}
}
}
return currentLightLevel;
static int hashAt(int x, int y, int z) {
return hashCell(
positionToCell(x),
positionToCell(y),
positionToCell(z)
);
}

private int positionToCell(int coord) {
private static int positionToCell(int coord) {
return coord / CELL_SIZE;
}

private int hashCell(int cellX, int cellY, int cellZ) {
return Math.abs(cellX * 751 + cellY * 86399 + cellZ * 284593);
}

private int getHashFromKey(int hash) {
return hash % MAX_LIGHT_SOURCES;
private static int hashCell(int cellX, int cellY, int cellZ) {
return Math.abs(cellX * 751 + cellY * 86399 + cellZ * 284593) % MAX_LIGHT_SOURCES;
}

public void computeSpatialLookup(Collection<? extends DynamicLightSource> dynamicLightSources) {
public void computeSpatialLookup(Collection<? extends DynamicLightSource> dynamicLightSources, Collection<LightCollection> lightCollections) {
Arrays.fill(this.spatialLookupEntries, null);
Arrays.fill(this.startIndices, Integer.MAX_VALUE);

int i = 0;

for (var source : dynamicLightSources) {
if (i == MAX_LIGHT_SOURCES) break;

int x = (int) source.getDynamicLightX();
int y = (int) source.getDynamicLightY();
int z = (int) source.getDynamicLightZ();

int cellKey = this.getHashFromKey(
this.hashCell(
this.positionToCell(x),
this.positionToCell(y),
this.positionToCell(z)
)
);

this.spatialLookupEntries[i] = new SpatialLookupEntry(cellKey, source);
int cellKey = hashAt(x, y, z);
this.spatialLookupEntries[i] = new SpatialLookupLightSourceEntry(cellKey, source);

i++;
if (i == MAX_LIGHT_SOURCES) break;
}

var lightCollectionChunks = lightCollections.stream().flatMap(LightCollection::split).toArray(SpatialLookupEntry[]::new);
int maxChunks = Math.min(i + lightCollectionChunks.length, MAX_LIGHT_SOURCES) - i;
System.arraycopy(lightCollectionChunks, 0, this.spatialLookupEntries, i, maxChunks);

Arrays.sort(this.spatialLookupEntries, Comparator.comparingInt(entry -> entry == null ? Integer.MAX_VALUE : entry.cellKey()));

for (i = 0; i < MAX_LIGHT_SOURCES; i++) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Copyright © 2024 LambdAurora <email@lambdaurora.dev>
*
* This file is part of LambDynamicLights.
*
* Licensed under the Lambda License. For more information,
* see the LICENSE file.
*/

package dev.lambdaurora.lambdynlights.engine;

import dev.lambdaurora.lambdynlights.engine.lookup.SpatialLookupCollectionEntry;
import it.unimi.dsi.fastutil.bytes.ByteArrayList;
import it.unimi.dsi.fastutil.bytes.ByteList;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import it.unimi.dsi.fastutil.longs.LongList;

import java.util.ArrayList;
import java.util.Collection;
import java.util.stream.Stream;

public record LightCollection(Collection<LightCollectionEntry> entries) {
public Stream<SpatialLookupCollectionEntry> split() {
record Data(LongList position, ByteList luminance) {}
var cellKeyToData = new Int2ObjectOpenHashMap<Data>();

for (var record : this.entries) {
int cellKey = DynamicLightingEngine.hashAt(record.x(), record.y(), record.z());

var data = cellKeyToData.computeIfAbsent(cellKey, k -> new Data(new LongArrayList(), new ByteArrayList()));

data.position.add(record.x());
data.position.add(record.y());
data.position.add(record.z());
data.luminance.add((byte) record.luminance());
}

return cellKeyToData.int2ObjectEntrySet()
.stream()
.map(entry -> new SpatialLookupCollectionEntry(
entry.getIntKey(),
entry.getValue().position().toLongArray(),
entry.getValue().luminance().toByteArray()
));
}

public static LightCollection cuboid(int startX, int startY, int startZ, int endX, int endY, int endZ, int luminance) {
var entries = new ArrayList<LightCollectionEntry>();

for (int x = startX; x <= endX; x++) {
for (int y = startY; y <= endY; y++) {
for (int z = startZ; z <= endZ; z++) {
entries.add(new LightCollectionEntry(x, y, z, luminance));
}
}
}

return new LightCollection(entries);
}

/*public static LightCollection line(int startX, int startY, int startZ, int endX, int endY, int endZ, int luminance) {
HashMap<Vec3i, Double> blocks = new HashMap<>();
}*/
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* Copyright © 2024 LambdAurora <email@lambdaurora.dev>
*
* This file is part of LambDynamicLights.
*
* Licensed under the Lambda License. For more information,
* see the LICENSE file.
*/

package dev.lambdaurora.lambdynlights.engine;

public record LightCollectionEntry(int x, int y, int z, int luminance) {
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright © 2024 LambdAurora <email@lambdaurora.dev>
*
* This file is part of LambDynamicLights.
*
* Licensed under the Lambda License. For more information,
* see the LICENSE file.
*/

package dev.lambdaurora.lambdynlights.engine.lookup;

import net.minecraft.core.BlockPos;
import org.jetbrains.annotations.NotNull;

/**
* Represents an entry made of a collection of light sources in a spatial lookup.
*
* @param cellKey the cell key of this entry
* @param positions the positions of each light sources in the current cell
* @param luminance the light values in the current cell
* @author LambdAurora, Akarys
* @version 4.0.0
* @since 4.0.0
*/
public record SpatialLookupCollectionEntry(int cellKey, long[] positions, byte[] luminance) implements SpatialLookupEntry {
@Override
public double getDynamicLightLevel(@NotNull BlockPos pos) {
double maxLightLevel = 0.;

for (int i = 0; i < this.luminance.length; i++) {
int posIndex = i * 3;
double x = this.positions[posIndex] + 0.5;
double y = this.positions[posIndex + 1] + 0.5;
double z = this.positions[posIndex + 2] + 0.5;
byte luminance = this.luminance[i];

double lightLevel = SpatialLookupEntry.lightAtPos(x, y, z, pos, luminance);

if (lightLevel > maxLightLevel) {
maxLightLevel = lightLevel;
}
}

return maxLightLevel;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright © 2024 LambdAurora <email@lambdaurora.dev>
*
* This file is part of LambDynamicLights.
*
* Licensed under the Lambda License. For more information,
* see the LICENSE file.
*/

package dev.lambdaurora.lambdynlights.engine.lookup;

import dev.lambdaurora.lambdynlights.engine.DynamicLightingEngine;
import net.minecraft.core.BlockPos;
import org.jetbrains.annotations.NotNull;

/**
* Represents an entry in a spatial lookup.
*
* @author LambdAurora, Akarys
* @version 4.0.0
* @since 3.1.0
*/
public interface SpatialLookupEntry {
/**
* {@return the cell key of this entry}
*/
int cellKey();

/**
* Returns the dynamic light level generated by this entry at the specified position.
*
* @param pos the position
* @return the computed dynamic light level at the specified position
*/
double getDynamicLightLevel(@NotNull BlockPos pos);

static double lightAtPos(double x, double y, double z, @NotNull BlockPos pos, int luminance) {
// Can't use Entity#squaredDistanceTo because of eye Y coordinate.
double dx = pos.getX() - x + 0.5;
double dy = pos.getY() - y + 0.5;
double dz = pos.getZ() - z + 0.5;

double distanceSquared = dx * dx + dy * dy + dz * dz;
// 7.75 because else we would have to update more chunks and that's not a good idea.
// 15 (max range for blocks) would be too much and a bit cheaty.
if (distanceSquared <= DynamicLightingEngine.MAX_RADIUS_SQUARED) {
double multiplier = 1.0 - Math.sqrt(distanceSquared) / DynamicLightingEngine.MAX_RADIUS;
return multiplier * (double) luminance;
}

return 0.;
}
}
Loading

0 comments on commit e58249e

Please sign in to comment.