diff --git a/fabric/src/main/java/io/vram/canvas/CanvasFabricMod.java b/fabric/src/main/java/io/vram/canvas/CanvasFabricMod.java
index 5cbcc1467..4d66aa7e3 100644
--- a/fabric/src/main/java/io/vram/canvas/CanvasFabricMod.java
+++ b/fabric/src/main/java/io/vram/canvas/CanvasFabricMod.java
@@ -32,6 +32,7 @@
import net.fabricmc.loader.api.FabricLoader;
import grondag.canvas.CanvasMod;
+import grondag.canvas.light.color.LightRegistry;
import grondag.canvas.pipeline.config.PipelineLoader;
import grondag.canvas.texture.MaterialIndexProvider;
@@ -66,6 +67,7 @@ public ResourceLocation getFabricId() {
public void onResourceManagerReload(ResourceManager manager) {
PipelineLoader.reload(manager);
MaterialIndexProvider.reload();
+ LightRegistry.reload(manager);
}
});
}
diff --git a/fabric/src/main/resources/mixins.canvas.client.json b/fabric/src/main/resources/mixins.canvas.client.json
index 313c9038a..155c34be3 100644
--- a/fabric/src/main/resources/mixins.canvas.client.json
+++ b/fabric/src/main/resources/mixins.canvas.client.json
@@ -15,6 +15,7 @@
"MixinBufferUploader",
"MixinChunkRenderDispatcher",
"MixinClientChunkCache",
+ "MixinClientLevel",
"MixinCompiledChunk",
"MixinCyclingOption",
"MixinDebugScreenOverlay",
diff --git a/src/main/java/grondag/canvas/CanvasMod.java b/src/main/java/grondag/canvas/CanvasMod.java
index 19e0f3836..0beeb3984 100644
--- a/src/main/java/grondag/canvas/CanvasMod.java
+++ b/src/main/java/grondag/canvas/CanvasMod.java
@@ -54,7 +54,6 @@
//FEAT: pbr textures
//PERF: disable animated textures when not in view
//PERF: improve light smoothing performance
-//FEAT: colored lights
//FEAT: weather uniforms
//FEAT: biome texture in shader
diff --git a/src/main/java/grondag/canvas/apiimpl/CanvasState.java b/src/main/java/grondag/canvas/apiimpl/CanvasState.java
index 06c45c82e..1323a0a93 100644
--- a/src/main/java/grondag/canvas/apiimpl/CanvasState.java
+++ b/src/main/java/grondag/canvas/apiimpl/CanvasState.java
@@ -20,6 +20,8 @@
package grondag.canvas.apiimpl;
+import java.util.Objects;
+
import net.minecraft.client.Minecraft;
import net.minecraft.client.resources.language.I18n;
@@ -30,9 +32,11 @@
import grondag.canvas.apiimpl.rendercontext.CanvasEntityBlockRenderContext;
import grondag.canvas.apiimpl.rendercontext.CanvasItemRenderContext;
import grondag.canvas.config.Configurator;
+import grondag.canvas.light.color.LightDataManager;
import grondag.canvas.material.property.TextureMaterialState;
import grondag.canvas.perf.ChunkRebuildCounters;
import grondag.canvas.perf.Timekeeper;
+import grondag.canvas.pipeline.Pipeline;
import grondag.canvas.pipeline.PipelineManager;
import grondag.canvas.pipeline.config.PipelineLoader;
import grondag.canvas.shader.GlMaterialProgramManager;
@@ -45,28 +49,73 @@
import grondag.canvas.terrain.util.ChunkColorCache;
public class CanvasState {
- public static void recompileIfNeeded(boolean forceRecompile) {
- while (CanvasMod.RECOMPILE.consumeClick()) {
- forceRecompile = true;
+ public static void handleRecompileKeybind() {
+ final boolean recompilePressed = CanvasMod.RECOMPILE.consumeClick();
+
+ while (true) {
+ // consume all clicks
+ if (!CanvasMod.RECOMPILE.consumeClick()) {
+ break;
+ }
}
- if (forceRecompile) {
- CanvasMod.LOG.info(I18n.get("info.canvas.recompile"));
- PipelineLoader.reload(Minecraft.getInstance().getResourceManager());
- PipelineManager.reload();
- PreReleaseShaderCompat.reload();
- MaterialProgram.reload();
- GlShaderManager.INSTANCE.reload();
- GlProgramManager.INSTANCE.reload();
- GlMaterialProgramManager.INSTANCE.reload();
- // LightmapHdTexture.reload();
- // LightmapHd.reload();
- TextureMaterialState.reload();
- ShaderDataManager.reload();
- Timekeeper.configOrPipelineReload();
+ if (recompilePressed) {
+ recompile(false);
}
}
+ public static void recompile() {
+ recompile(false);
+ }
+
+ private static int loopCounter = 0;
+
+ private static boolean recompilePipeline() {
+ final var prev_coloredLightsConfig = Pipeline.config().coloredLights;
+ final boolean coloredLights_wasDisabled = !Pipeline.coloredLightsEnabled();
+ final boolean advancedCulling_wasDisabled = !Pipeline.advancedTerrainCulling();
+
+ PipelineLoader.reload(Minecraft.getInstance().getResourceManager());
+ PipelineManager.reload();
+
+ final boolean coloredLightsConfigChanged = !Objects.equals(Pipeline.config().coloredLights, prev_coloredLightsConfig);
+ final boolean coloredLights_requiresRebuild = Pipeline.coloredLightsEnabled() && (coloredLightsConfigChanged || coloredLights_wasDisabled);
+ final boolean culling_requiresRebuild = Pipeline.advancedTerrainCulling() && advancedCulling_wasDisabled;
+
+ return coloredLights_requiresRebuild || culling_requiresRebuild;
+ }
+
+ private static void recompile(boolean wasReloaded) {
+ CanvasMod.LOG.info(I18n.get("info.canvas.recompile"));
+
+ final boolean requireReload = recompilePipeline();
+
+ if (!wasReloaded && loopCounter < 2 && requireReload) {
+ CanvasMod.LOG.info(I18n.get("info.canvas.recompile_needs_reload"));
+ loopCounter++;
+ Minecraft.getInstance().levelRenderer.allChanged();
+ return;
+ }
+
+ LightDataManager.reload(wasReloaded);
+ PreReleaseShaderCompat.reload();
+ MaterialProgram.reload();
+ GlShaderManager.INSTANCE.reload();
+ GlProgramManager.INSTANCE.reload();
+ GlMaterialProgramManager.INSTANCE.reload();
+ // LightmapHdTexture.reload();
+ // LightmapHd.reload();
+ TextureMaterialState.reload();
+ ShaderDataManager.reload();
+ Timekeeper.configOrPipelineReload();
+
+ if (loopCounter > 1) {
+ CanvasMod.LOG.warn("Reloading recursively twice or more. This isn't supposed to happen.");
+ }
+
+ loopCounter = 0;
+ }
+
public static void reload() {
CanvasMod.LOG.info(I18n.get("info.canvas.reloading"));
PackedInputRegion.reload();
@@ -77,6 +126,6 @@ public static void reload() {
ChunkColorCache.invalidate();
AoFace.clampExteriorVertices(Configurator.clampExteriorVertices);
- recompileIfNeeded(true);
+ recompile(true);
}
}
diff --git a/src/main/java/grondag/canvas/config/CanvasConfigScreen.java b/src/main/java/grondag/canvas/config/CanvasConfigScreen.java
index e95a7d834..6bbd08914 100644
--- a/src/main/java/grondag/canvas/config/CanvasConfigScreen.java
+++ b/src/main/java/grondag/canvas/config/CanvasConfigScreen.java
@@ -23,6 +23,7 @@
import static grondag.canvas.config.ConfigManager.DEFAULTS;
import static grondag.canvas.config.ConfigManager.Reload.DONT_RELOAD;
import static grondag.canvas.config.ConfigManager.Reload.RELOAD_EVERYTHING;
+import static grondag.canvas.config.ConfigManager.Reload.RELOAD_PIPELINE;
import java.util.List;
@@ -47,6 +48,7 @@
public class CanvasConfigScreen extends BaseScreen {
private boolean reload;
+ private boolean recompile;
private boolean reloadTimekeeper;
private boolean requiresRestart;
@@ -59,6 +61,7 @@ public CanvasConfigScreen(Screen parent) {
super(parent, Component.translatable("config.canvas.title"));
reload = false;
+ recompile = false;
reloadTimekeeper = false;
requiresRestart = false;
@@ -95,7 +98,7 @@ protected void init() {
list.addItem(optionSession.booleanOption("config.canvas.value.wavy_grass",
() -> editing.wavyGrass,
b -> {
- reload |= Configurator.wavyGrass != b;
+ recompile |= Configurator.wavyGrass != b;
editing.wavyGrass = b;
},
DEFAULTS.wavyGrass,
@@ -116,6 +119,24 @@ protected void init() {
DEFAULTS.semiFlatLighting,
"config.canvas.help.semi_flat_lighting").listItem());
+ list.addItem(optionSession.booleanOption("config.canvas.value.colored_lights",
+ () -> editing.coloredLights,
+ b -> {
+ reload |= Configurator.coloredLights != b;
+ editing.coloredLights = b;
+ },
+ DEFAULTS.coloredLights,
+ "config.canvas.help.colored_lights").listItem());
+
+ list.addItem(optionSession.booleanOption("config.canvas.value.entity_light_source",
+ () -> editing.entityLightSource,
+ b -> {
+ reload |= Configurator.entityLightSource != b;
+ editing.entityLightSource = b;
+ },
+ DEFAULTS.entityLightSource,
+ "config.canvas.help.entity_light_source").listItem());
+
// TWEAKS
final int indexTweaks = list.addCategory("config.canvas.category.tweaks");
@@ -332,7 +353,7 @@ protected void init() {
list.addItem(optionSession.booleanOption("config.canvas.value.preprocess_shader_source",
() -> editing.preprocessShaderSource,
b -> {
- reload |= Configurator.preprocessShaderSource != b;
+ recompile |= Configurator.preprocessShaderSource != b;
editing.preprocessShaderSource = b;
},
DEFAULTS.preprocessShaderSource,
@@ -484,6 +505,15 @@ protected void init() {
DEFAULTS.traceTextureLoad,
"config.canvas.help.trace_texture_load").listItem());
+ list.addItem(optionSession.booleanOption("config.canvas.value.debug_shader_flag",
+ () -> editing.debugShaderFlag,
+ b -> {
+ recompile |= Configurator.debugShaderFlag != b;
+ editing.debugShaderFlag = b;
+ },
+ DEFAULTS.debugShaderFlag,
+ "config.canvas.help.debug_shader_flag").listItem());
+
if (sideW > 0) {
final ListWidget tabs = new ListWidget(1, list.getY(), sideW, list.getHeight(), true);
addRenderableWidget(tabs);
@@ -514,12 +544,22 @@ private void save() {
Configurator.readFromConfig(editing);
- // for now Config reload does reload everything including Timekeeper
- if (reloadTimekeeper && !reload) {
- Timekeeper.configOrPipelineReload();
+ final ConfigManager.Reload configReload;
+
+ if (reload) {
+ configReload = RELOAD_EVERYTHING;
+ } else if (recompile) {
+ configReload = RELOAD_PIPELINE;
+ } else {
+ // the other cases already cover timekeeper reload at the moment.
+ if (reloadTimekeeper) {
+ Timekeeper.configOrPipelineReload();
+ }
+
+ configReload = DONT_RELOAD;
}
- ConfigManager.saveUserInput(reload ? RELOAD_EVERYTHING : DONT_RELOAD);
+ ConfigManager.saveCanvasConfig(configReload);
if (requiresRestart) {
this.minecraft.setScreen(new ConfigRestartScreen(this.parent));
diff --git a/src/main/java/grondag/canvas/config/ConfigData.java b/src/main/java/grondag/canvas/config/ConfigData.java
index dd12a1a0c..d20ec6409 100644
--- a/src/main/java/grondag/canvas/config/ConfigData.java
+++ b/src/main/java/grondag/canvas/config/ConfigData.java
@@ -55,6 +55,10 @@ class ConfigData {
//boolean moreLightmap = true;
@Comment("Models with flat lighting have smoother lighting (but no ambient occlusion).")
boolean semiFlatLighting = true;
+ @Comment("Enable colored block lights on pipelines that support it. Replaces vanilla lighting but only visually.")
+ boolean coloredLights = false;
+ @Comment("Enable entity as dynamic light sources. Requires colored lights and supporting pipeline.")
+ boolean entityLightSource = false;
// TWEAKS
@Comment("Adjusts quads on some vanilla models (like iron bars) to avoid z-fighting with neighbor blocks.")
@@ -151,6 +155,8 @@ class ConfigData {
boolean debugSpriteAtlas = false;
@Comment("Log significant events of texture/sprite atlas loading. For debugging use. Will spam the log.")
boolean traceTextureLoad = false;
+ @Comment("Enables debug flag in the shader. Only intended for internal Canvas development purposes.")
+ boolean debugShaderFlag = false;
// GSON doesn't do this automatically
public void clearNulls() {
diff --git a/src/main/java/grondag/canvas/config/ConfigManager.java b/src/main/java/grondag/canvas/config/ConfigManager.java
index e2a4755a4..bb85107fb 100644
--- a/src/main/java/grondag/canvas/config/ConfigManager.java
+++ b/src/main/java/grondag/canvas/config/ConfigManager.java
@@ -120,7 +120,7 @@ public static void savePipelineOptions(OptionConfig[] options) {
CanvasMod.LOG.error("Error loading pipeline config. Using default values.");
}
- CanvasState.recompileIfNeeded(true);
+ CanvasState.recompile();
}
private static void saveConfig() {
@@ -163,12 +163,12 @@ static void loadConfig() {
Configurator.readFromConfig(config, true);
}
- static void saveUserInput(Reload reload) {
+ static void saveCanvasConfig(Reload reload) {
saveConfig();
switch (reload) {
case RELOAD_EVERYTHING -> Minecraft.getInstance().levelRenderer.allChanged();
- case RELOAD_PIPELINE -> CanvasState.recompileIfNeeded(true);
+ case RELOAD_PIPELINE -> CanvasState.recompile();
case DONT_RELOAD -> { }
}
}
diff --git a/src/main/java/grondag/canvas/config/Configurator.java b/src/main/java/grondag/canvas/config/Configurator.java
index 6662b7db4..020dfdebd 100644
--- a/src/main/java/grondag/canvas/config/Configurator.java
+++ b/src/main/java/grondag/canvas/config/Configurator.java
@@ -40,6 +40,8 @@ public class Configurator {
//public static boolean moreLightmap = DEFAULTS.moreLightmap;
//public static int maxLightmapDelayFrames = DEFAULTS.maxLightmapDelayFrames;
public static boolean semiFlatLighting = DEFAULTS.semiFlatLighting;
+ public static boolean coloredLights = DEFAULTS.coloredLights;
+ public static boolean entityLightSource = DEFAULTS.entityLightSource;
public static boolean preventDepthFighting = DEFAULTS.preventDepthFighting;
public static boolean clampExteriorVertices = DEFAULTS.clampExteriorVertices;
@@ -95,6 +97,7 @@ public class Configurator {
public static boolean cullBackfacingTerrain = DEFAULTS.cullBackfacingTerrain;
public static boolean debugSpriteAtlas = DEFAULTS.debugSpriteAtlas;
public static boolean traceTextureLoad = DEFAULTS.traceTextureLoad;
+ public static boolean debugShaderFlag = DEFAULTS.debugShaderFlag;
// @LangKey("config.acuity_fancy_fluids")
// @Comment({"Enable fancy water and lava rendering.",
@@ -136,6 +139,8 @@ static void readFromConfig(ConfigData config, boolean isStartup) {
// lightmapNoise = config.lightmapNoise;
lightSmoothing = config.lightSmoothing;
semiFlatLighting = config.semiFlatLighting;
+ coloredLights = config.coloredLights;
+ entityLightSource = config.entityLightSource;
// disableVanillaChunkMatrix = config.disableVanillaChunkMatrix;
preventDepthFighting = config.preventDepthFighting;
@@ -185,6 +190,7 @@ static void readFromConfig(ConfigData config, boolean isStartup) {
cullBackfacingTerrain = config.cullBackfacingTerrain;
debugSpriteAtlas = config.debugSpriteAtlas;
traceTextureLoad = config.traceTextureLoad;
+ debugShaderFlag = config.debugShaderFlag;
}
static void writeToConfig(ConfigData config) {
@@ -202,6 +208,8 @@ static void writeToConfig(ConfigData config) {
config.lightSmoothing = lightSmoothing;
//config.moreLightmap = moreLightmap;
config.semiFlatLighting = semiFlatLighting;
+ config.coloredLights = coloredLights;
+ config.entityLightSource = entityLightSource;
config.preventDepthFighting = preventDepthFighting;
config.clampExteriorVertices = clampExteriorVertices;
@@ -250,5 +258,6 @@ static void writeToConfig(ConfigData config) {
config.cullBackfacingTerrain = cullBackfacingTerrain;
config.debugSpriteAtlas = debugSpriteAtlas;
config.traceTextureLoad = traceTextureLoad;
+ config.debugShaderFlag = debugShaderFlag;
}
}
diff --git a/src/main/java/grondag/canvas/config/PipelineOptionScreen.java b/src/main/java/grondag/canvas/config/PipelineOptionScreen.java
index f17203081..43332edc2 100644
--- a/src/main/java/grondag/canvas/config/PipelineOptionScreen.java
+++ b/src/main/java/grondag/canvas/config/PipelineOptionScreen.java
@@ -20,7 +20,6 @@
package grondag.canvas.config;
-import static grondag.canvas.config.ConfigManager.Reload.RELOAD_EVERYTHING;
import static grondag.canvas.config.ConfigManager.Reload.RELOAD_PIPELINE;
import java.util.List;
@@ -118,8 +117,7 @@ protected void init() {
private void savePipelineSelection(ResourceLocation newPipelineId) {
Configurator.pipelineId = newPipelineId.toString();
- // When advanced terrain culling is *soft* disabled, better clear the region storage
- ConfigManager.saveUserInput(Configurator.advancedTerrainCulling ? RELOAD_PIPELINE : RELOAD_EVERYTHING);
+ ConfigManager.saveCanvasConfig(RELOAD_PIPELINE);
}
private void save() {
diff --git a/src/main/java/grondag/canvas/light/api/BlockLight.java b/src/main/java/grondag/canvas/light/api/BlockLight.java
new file mode 100644
index 000000000..cb1a46bb7
--- /dev/null
+++ b/src/main/java/grondag/canvas/light/api/BlockLight.java
@@ -0,0 +1,67 @@
+/*
+ * This file is part of Canvas Renderer and is licensed to the project under
+ * terms that are compatible with the GNU Lesser General Public License.
+ * See the NOTICE file distributed with this work for additional information
+ * regarding copyright ownership and licensing.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+package grondag.canvas.light.api;
+
+import grondag.canvas.light.api.impl.FloodFillBlockLight;
+
+/**
+ * BlockLight API draft.
+ */
+public interface BlockLight {
+ /**
+ * The light level. Typically, this represents the light radius after multiplied with the
+ * highest color component, but also affects maximum brightness.
+ *
+ *
Implementation may choose whether to prioritize the radius aspect or brightness aspect.
+ *
+ *
Typical value is in range 0-15. Value outside of this range is implementation-specific.
+ *
+ *
In JSON format, defaults to the vanilla registered light level when missing.
+ * Importantly, light level is attached to block states. Fluid states will attempt
+ * to default to their block state counterpart.
+ */
+ float lightLevel();
+
+ /**
+ * Red intensity. Behavior of values outside of range 0-1 is undefined.
+ * In JSON format, defaults to 0 when missing.
+ */
+ float red();
+
+ /**
+ * Green intensity. Behavior of values outside of range 0-1 is undefined.
+ * In JSON format, defaults to 0 when missing.
+ */
+ float green();
+
+ /**
+ * Blue intensity. Behavior of values outside of range 0-1 is undefined.
+ * In JSON format, defaults to 0 when missing.
+ */
+ float blue();
+
+ /**
+ * Constructs an implementation consistent instance of BlockLight.
+ */
+ static BlockLight of(float lightLevel, float red, float green, float blue) {
+ return new FloodFillBlockLight(lightLevel, red, green, blue, true);
+ }
+}
diff --git a/src/main/java/grondag/canvas/light/api/EntityLightProvider.java b/src/main/java/grondag/canvas/light/api/EntityLightProvider.java
new file mode 100644
index 000000000..4210fec06
--- /dev/null
+++ b/src/main/java/grondag/canvas/light/api/EntityLightProvider.java
@@ -0,0 +1,31 @@
+/*
+ * This file is part of Canvas Renderer and is licensed to the project under
+ * terms that are compatible with the GNU Lesser General Public License.
+ * See the NOTICE file distributed with this work for additional information
+ * regarding copyright ownership and licensing.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+package grondag.canvas.light.api;
+
+/**
+ * Implement this interface to treat a particular entity as a dynamic light source.
+ */
+public interface EntityLightProvider {
+ /**
+ * The block light value of this entity in the current frame.
+ */
+ BlockLight getBlockLight();
+}
diff --git a/src/main/java/grondag/canvas/light/api/impl/BlockLightLoader.java b/src/main/java/grondag/canvas/light/api/impl/BlockLightLoader.java
new file mode 100644
index 000000000..9be14e9a2
--- /dev/null
+++ b/src/main/java/grondag/canvas/light/api/impl/BlockLightLoader.java
@@ -0,0 +1,206 @@
+/*
+ * This file is part of Canvas Renderer and is licensed to the project under
+ * terms that are compatible with the GNU Lesser General Public License.
+ * See the NOTICE file distributed with this work for additional information
+ * regarding copyright ownership and licensing.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+package grondag.canvas.light.api.impl;
+
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.IdentityHashMap;
+import java.util.List;
+
+import com.google.gson.JsonObject;
+
+import net.minecraft.client.renderer.block.BlockModelShaper;
+import net.minecraft.core.registries.BuiltInRegistries;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.server.packs.resources.ResourceManager;
+import net.minecraft.util.GsonHelper;
+import net.minecraft.world.entity.EntityType;
+import net.minecraft.world.level.block.Block;
+import net.minecraft.world.level.block.state.BlockState;
+import net.minecraft.world.level.block.state.StateHolder;
+import net.minecraft.world.level.material.Fluid;
+import net.minecraft.world.level.material.FluidState;
+
+import grondag.canvas.CanvasMod;
+
+public class BlockLightLoader {
+ public static final FloodFillBlockLight DEFAULT_LIGHT = new FloodFillBlockLight(0f, 0f, 0f, 0f, false);
+
+ public static final IdentityHashMap BLOCK_LIGHTS = new IdentityHashMap<>();
+ public static final IdentityHashMap FLUID_LIGHTS = new IdentityHashMap<>();
+ public static final IdentityHashMap, FloodFillBlockLight> ENTITY_LIGHTS = new IdentityHashMap<>();
+
+ public static void reload(ResourceManager manager) {
+ BLOCK_LIGHTS.clear();
+ FLUID_LIGHTS.clear();
+ ENTITY_LIGHTS.clear();
+
+ for (Block block : BuiltInRegistries.BLOCK) {
+ loadBlock(manager, block);
+ }
+
+ for (Fluid fluid : BuiltInRegistries.FLUID) {
+ loadFluid(manager, fluid);
+ }
+
+ for (EntityType> type : BuiltInRegistries.ENTITY_TYPE) {
+ loadEntity(manager, type);
+ }
+ }
+
+ private static void loadBlock(ResourceManager manager, Block block) {
+ final ResourceLocation blockId = BuiltInRegistries.BLOCK.getKey(block);
+
+ final ResourceLocation id = new ResourceLocation(blockId.getNamespace(), "lights/block/" + blockId.getPath() + ".json");
+
+ try {
+ final var res = manager.getResource(id);
+
+ if (res.isPresent()) {
+ deserialize(block.getStateDefinition().getPossibleStates(), id, new InputStreamReader(res.get().open(), StandardCharsets.UTF_8), BLOCK_LIGHTS);
+ }
+ } catch (final Exception e) {
+ CanvasMod.LOG.info("Unable to load block light map " + id.toString() + " due to exception " + e.toString());
+ }
+ }
+
+ private static void loadFluid(ResourceManager manager, Fluid fluid) {
+ final ResourceLocation blockId = BuiltInRegistries.FLUID.getKey(fluid);
+
+ final ResourceLocation id = new ResourceLocation(blockId.getNamespace(), "lights/fluid/" + blockId.getPath() + ".json");
+
+ try {
+ final var res = manager.getResource(id);
+
+ if (res.isPresent()) {
+ deserialize(fluid.getStateDefinition().getPossibleStates(), id, new InputStreamReader(res.get().open(), StandardCharsets.UTF_8), FLUID_LIGHTS);
+ }
+ } catch (final Exception e) {
+ CanvasMod.LOG.info("Unable to load fluid light map " + id.toString() + " due to exception " + e.toString());
+ }
+ }
+
+ private static void loadEntity(ResourceManager manager, EntityType> entityType) {
+ final ResourceLocation entityId = BuiltInRegistries.ENTITY_TYPE.getKey(entityType);
+ final ResourceLocation id = new ResourceLocation(entityId.getNamespace(), "lights/entity/" + entityId.getPath() + ".json");
+
+ try {
+ final var res = manager.getResource(id);
+
+ if (res.isPresent()) {
+ deserialize(entityType, id, new InputStreamReader(res.get().open(), StandardCharsets.UTF_8), ENTITY_LIGHTS);
+ }
+ } catch (final Exception e) {
+ CanvasMod.LOG.info("Unable to load block light map " + id.toString() + " due to exception " + e.toString());
+ }
+ }
+
+ private static > void deserialize(List states, ResourceLocation idForLog, InputStreamReader reader, IdentityHashMap map) {
+ try {
+ final JsonObject json = GsonHelper.parse(reader);
+ final String idString = idForLog.toString();
+
+ final FloodFillBlockLight globalDefaultLight = DEFAULT_LIGHT;
+ final FloodFillBlockLight defaultLight;
+
+ if (json.has("defaultLight")) {
+ defaultLight = loadLight(json.get("defaultLight").getAsJsonObject(), globalDefaultLight);
+ } else {
+ defaultLight = globalDefaultLight;
+ }
+
+ JsonObject variants = null;
+
+ if (json.has("variants")) {
+ variants = json.getAsJsonObject("variants");
+
+ if (variants.isJsonNull()) {
+ CanvasMod.LOG.warn("Unable to load variant lights for " + idString + " because the 'variants' block is empty. Using default map.");
+ variants = null;
+ }
+ }
+
+ for (final T state : states) {
+ FloodFillBlockLight result = defaultLight;
+
+ if (!result.levelIsSet && state instanceof BlockState blockState) {
+ result = result.withLevel(blockState.getLightEmission());
+ }
+
+ if (variants != null) {
+ final String stateId = BlockModelShaper.statePropertiesToString(state.getValues());
+ result = loadLight(variants.getAsJsonObject(stateId), result);
+ }
+
+ if (!result.equals(globalDefaultLight)) {
+ map.put(state, result);
+ }
+ }
+ } catch (final Exception e) {
+ CanvasMod.LOG.warn("Unable to load lights for " + idForLog.toString() + " due to unhandled exception:", e);
+ }
+ }
+
+ private static void deserialize(T type, ResourceLocation idForLog, InputStreamReader reader, IdentityHashMap map) {
+ try {
+ final JsonObject json = GsonHelper.parse(reader);
+ final FloodFillBlockLight globalDefaultLight = BlockLightLoader.DEFAULT_LIGHT;
+ final FloodFillBlockLight result;
+
+ if (json.has("defaultLight")) {
+ result = BlockLightLoader.loadLight(json.get("defaultLight").getAsJsonObject(), globalDefaultLight);
+ } else {
+ result = BlockLightLoader.loadLight(json, globalDefaultLight);
+ }
+
+ if (!result.equals(globalDefaultLight) && result.levelIsSet) {
+ map.put(type, result);
+ }
+ } catch (final Exception e) {
+ CanvasMod.LOG.warn("Unable to load lights for " + idForLog.toString() + " due to unhandled exception:", e);
+ }
+ }
+
+ private static FloodFillBlockLight loadLight(JsonObject obj, FloodFillBlockLight defaultValue) {
+ if (obj == null) {
+ return defaultValue;
+ }
+
+ final var lightLevelObj = obj.get("lightLevel");
+ final var redObj = obj.get("red");
+ final var greenObj = obj.get("green");
+ final var blueObj = obj.get("blue");
+
+ final float defaultLightLevel = defaultValue.levelIsSet ? defaultValue.lightLevel() : 15f;
+ final float lightLevel = lightLevelObj == null ? defaultLightLevel : lightLevelObj.getAsFloat();
+ final float red = redObj == null ? defaultValue.red() : redObj.getAsFloat();
+ final float green = greenObj == null ? defaultValue.green() : greenObj.getAsFloat();
+ final float blue = blueObj == null ? defaultValue.blue() : blueObj.getAsFloat();
+ final boolean levelIsSet = lightLevelObj != null || defaultValue.levelIsSet;
+ final var result = new FloodFillBlockLight(lightLevel, red, green, blue, levelIsSet);
+
+ if (result.equals(defaultValue)) {
+ return defaultValue;
+ } else {
+ return result;
+ }
+ }
+}
diff --git a/src/main/java/grondag/canvas/light/api/impl/FloodFillBlockLight.java b/src/main/java/grondag/canvas/light/api/impl/FloodFillBlockLight.java
new file mode 100644
index 000000000..8babe3596
--- /dev/null
+++ b/src/main/java/grondag/canvas/light/api/impl/FloodFillBlockLight.java
@@ -0,0 +1,114 @@
+/*
+ * This file is part of Canvas Renderer and is licensed to the project under
+ * terms that are compatible with the GNU Lesser General Public License.
+ * See the NOTICE file distributed with this work for additional information
+ * regarding copyright ownership and licensing.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+package grondag.canvas.light.api.impl;
+
+import grondag.canvas.light.api.BlockLight;
+import grondag.canvas.light.color.LightOp;
+
+public final class FloodFillBlockLight implements BlockLight {
+ public final short value;
+ public final boolean levelIsSet;
+ public final float red, green, blue, lightLevel;
+
+ public FloodFillBlockLight(short value, boolean levelIsSet) {
+ this.value = value;
+ this.levelIsSet = levelIsSet;
+ this.lightLevel = Math.max(LightOp.R.of(value), Math.max(LightOp.G.of(value), LightOp.B.of(value)));
+ this.red = LightOp.R.of(value) / 15.0f;
+ this.green = LightOp.G.of(value) / 15.0f;
+ this.blue = LightOp.B.of(value) / 15.0f;
+ }
+
+ public FloodFillBlockLight(float lightLevel, float red, float green, float blue, boolean levelIsSet) {
+ this(computeValue(lightLevel, red, green, blue), levelIsSet);
+ }
+
+ public FloodFillBlockLight withLevel(float lightEmission) {
+ if (this.lightLevel() == lightEmission && this.levelIsSet) {
+ return this;
+ } else {
+ return new FloodFillBlockLight(lightEmission, red(), green(), blue(), true);
+ }
+ }
+
+ static short computeValue(float lightLevel, float red, float green, float blue) {
+ final int blockRadius = lightLevel == 0f ? 0 : org.joml.Math.clamp(1, 15, Math.round(lightLevel));
+ return LightOp.encode(clampLight(blockRadius * red), clampLight(blockRadius * green), clampLight(blockRadius * blue), 0);
+ }
+
+ private static int clampLight(float light) {
+ return org.joml.Math.clamp(0, 15, Math.round(light));
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+
+ if (!(obj instanceof BlockLight that)) {
+ return false;
+ }
+
+ if (this.lightLevel() == 0f && that.lightLevel() == 0f) {
+ // both are completely dark regardless of color
+ return true;
+ }
+
+ if (obj instanceof FloodFillBlockLight floodFillBlockLight) {
+ return this.value == floodFillBlockLight.value && this.levelIsSet == floodFillBlockLight.levelIsSet;
+ }
+
+ if (that.red() != this.red()) {
+ return false;
+ }
+
+ if (that.green() != this.green()) {
+ return false;
+ }
+
+ if (that.blue() != this.blue()) {
+ return false;
+ }
+
+ return that.lightLevel() == this.lightLevel();
+ }
+
+ @Override
+ public float lightLevel() {
+ return lightLevel;
+ }
+
+ @Override
+ public float red() {
+ return red;
+ }
+
+ @Override
+ public float green() {
+ return green;
+ }
+
+ @Override
+ public float blue() {
+ return blue;
+ }
+}
diff --git a/src/main/java/grondag/canvas/light/color/EntityLightTracker.java b/src/main/java/grondag/canvas/light/color/EntityLightTracker.java
new file mode 100644
index 000000000..f4668bac2
--- /dev/null
+++ b/src/main/java/grondag/canvas/light/color/EntityLightTracker.java
@@ -0,0 +1,218 @@
+/*
+ * This file is part of Canvas Renderer and is licensed to the project under
+ * terms that are compatible with the GNU Lesser General Public License.
+ * See the NOTICE file distributed with this work for additional information
+ * regarding copyright ownership and licensing.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+package grondag.canvas.light.color;
+
+import java.util.function.Function;
+
+import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
+
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.multiplayer.ClientLevel;
+import net.minecraft.client.player.LocalPlayer;
+import net.minecraft.core.BlockPos;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.LivingEntity;
+import net.minecraft.world.entity.item.ItemEntity;
+import net.minecraft.world.level.entity.EntityAccess;
+
+import io.vram.frex.api.light.HeldItemLightListener;
+import io.vram.frex.api.light.ItemLight;
+
+import grondag.canvas.light.api.impl.BlockLightLoader;
+
+public class EntityLightTracker {
+ private static EntityLightTracker INSTANCE;
+ private final Int2ObjectOpenHashMap> entities = new Int2ObjectOpenHashMap<>();
+ private final LightLevelAccess lightLevel;
+ private boolean requiresInitialization = true;
+
+ EntityLightTracker(LightLevelAccess lightLevel) {
+ INSTANCE = this;
+ this.lightLevel = lightLevel;
+ }
+
+ void update(ClientLevel level) {
+ if (requiresInitialization) {
+ requiresInitialization = false;
+
+ var entities = level.entitiesForRendering();
+
+ for (var entity:entities) {
+ trackAny(entity);
+ }
+ }
+
+ for (var entity : entities.values()) {
+ entity.update();
+ }
+ }
+
+ public static void levelAddsEntity(Entity entity) {
+ if (INSTANCE != null && !INSTANCE.requiresInitialization) {
+ INSTANCE.trackAny(entity);
+ }
+ }
+
+ public static void levelRemovesEntity(int id) {
+ if (INSTANCE != null && !INSTANCE.requiresInitialization) {
+ INSTANCE.removeAny(id);
+ }
+ }
+
+ void reload(boolean clearLights) {
+ requiresInitialization = true;
+ // might be called twice due to multiple hook (setLevel and allChanged). idempotent.
+ removeAll(clearLights);
+ }
+
+ void close(boolean clear) {
+ if (clear) {
+ removeAll(true);
+ }
+
+ if (INSTANCE == this) {
+ INSTANCE = null;
+ }
+ }
+
+ private void removeAll(boolean clearLights) {
+ // see notes on reload()
+ if (clearLights) {
+ for (var entity : entities.values()) {
+ entity.removeLight();
+ }
+ }
+
+ entities.clear();
+ }
+
+ private void trackAny(Entity entity) {
+ final TrackedEntity> trackedEntity;
+
+ if (BlockLightLoader.ENTITY_LIGHTS.containsKey(entity.getType())) {
+ final short loadedLight = BlockLightLoader.ENTITY_LIGHTS.get(entity.getType()).value;
+ trackedEntity = new TrackedEntity<>(entity, e -> loadedLight);
+ } else if (entity == Minecraft.getInstance().player) {
+ // we already have shader held light
+ return;
+ } else if (entity instanceof LivingEntity livingEntity) {
+ trackedEntity = new TrackedEntity<>(livingEntity, new HeldLightSupplier());
+ } else if (entity instanceof ItemEntity itemEntity) {
+ trackedEntity = new TrackedEntity<>(itemEntity, new ItemLightSupplier());
+ } else {
+ return;
+ }
+
+ entities.put(entity.getId(), trackedEntity);
+ }
+
+ private void removeAny(int id) {
+ if (entities.containsKey(id)) {
+ entities.remove(id).removeLight();
+ }
+ }
+
+ // Unused at the moment
+ private static class ThirdPersonSupplier implements Function {
+ private final HeldLightSupplier heldLightSupplier = new HeldLightSupplier();
+
+ @Override
+ public Short apply(LocalPlayer localPlayer) {
+ final boolean firstPerson = Minecraft.getInstance().options.getCameraType().isFirstPerson();
+ return firstPerson ? 0 : heldLightSupplier.apply(localPlayer);
+ }
+ }
+
+ private static class ItemLightSupplier implements Function {
+ @Override
+ public Short apply(ItemEntity itemEntity) {
+ ItemLight light = ItemLight.get(itemEntity.getItem());
+
+ if (!light.worksInFluid() && itemEntity.isUnderWater()) {
+ light = ItemLight.NONE;
+ }
+
+ return LightRegistry.encodeItem(light);
+ }
+ }
+
+ private static class HeldLightSupplier implements Function {
+ @Override
+ public Short apply(LivingEntity livingEntity) {
+ final var mainItem = livingEntity.getMainHandItem();
+ final var mainLight = ItemLight.get(mainItem);
+
+ var light = HeldItemLightListener.apply(livingEntity, mainItem, mainLight);
+
+ if (light.equals(ItemLight.NONE)) {
+ final var offItem = livingEntity.getOffhandItem();
+ final var offLight = ItemLight.get(offItem);
+ light = HeldItemLightListener.apply(livingEntity, offItem, offLight);
+ }
+
+ if (!light.worksInFluid() && livingEntity.isUnderWater()) {
+ light = ItemLight.NONE;
+ }
+
+ return LightRegistry.encodeItem(light);
+ }
+ }
+
+ private class TrackedEntity {
+ private final E entity;
+ private final Function lightSupplier;
+
+ private final BlockPos.MutableBlockPos lastTrackedPos = new BlockPos.MutableBlockPos();
+ private short lastTrackedLight = 0;
+
+ TrackedEntity(E entity, Function lightSupplier) {
+ this.entity = entity;
+ this.lightSupplier = lightSupplier;
+ lastTrackedPos.set(entity.blockPosition());
+ }
+
+ void update() {
+ final BlockPos pos = entity.blockPosition();
+ final short light = lightSupplier.apply(entity);
+ final boolean changedLight = lastTrackedLight != light;
+ final boolean changedPos = LightOp.lit(light) && !lastTrackedPos.equals(pos);
+
+ if (changedLight || changedPos) {
+ if (LightOp.lit(lastTrackedLight)) {
+ lightLevel.removeVirtualLight(lastTrackedPos, lastTrackedLight);
+ }
+
+ if (LightOp.lit(light)) {
+ lightLevel.placeVirtualLight(pos, light);
+ }
+ }
+
+ lastTrackedLight = light;
+ lastTrackedPos.set(pos);
+ }
+
+ public void removeLight() {
+ if (LightOp.lit(lastTrackedLight)) {
+ lightLevel.removeVirtualLight(entity.blockPosition(), lastTrackedLight);
+ }
+ }
+ }
+}
diff --git a/src/main/java/grondag/canvas/light/color/LightDataAllocator.java b/src/main/java/grondag/canvas/light/color/LightDataAllocator.java
new file mode 100644
index 000000000..955aa30ec
--- /dev/null
+++ b/src/main/java/grondag/canvas/light/color/LightDataAllocator.java
@@ -0,0 +1,334 @@
+/*
+ * This file is part of Canvas Renderer and is licensed to the project under
+ * terms that are compatible with the GNU Lesser General Public License.
+ * See the NOTICE file distributed with this work for additional information
+ * regarding copyright ownership and licensing.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+package grondag.canvas.light.color;
+
+import java.nio.ByteBuffer;
+
+import it.unimi.dsi.fastutil.shorts.Short2LongOpenHashMap;
+import it.unimi.dsi.fastutil.shorts.ShortArrayList;
+import it.unimi.dsi.fastutil.shorts.ShortStack;
+import org.lwjgl.system.MemoryUtil;
+
+import net.minecraft.client.Minecraft;
+import net.minecraft.core.BlockPos;
+
+import grondag.canvas.CanvasMod;
+import grondag.canvas.pipeline.Pipeline;
+import grondag.canvas.shader.data.IntData;
+import grondag.canvas.varia.CanvasGlHelper;
+
+public class LightDataAllocator {
+ private static final boolean DEBUG_LOG_RESIZE = false;
+
+ // unsigned short hard limit, because we are putting addresses in the data texture.
+ // this corresponds to about 1 GiB of light data which is a reasonable hard limit,
+ // but in practice, the texture size limit will be reached far before this limit.
+ private static final int MAX_ADDRESSES = 65536;
+
+ // size of one row as determined by the data size of one region (16^3).
+ // also represents row size of pointer header because we are putting addresses in the data texture.
+ private static final int ROW_SIZE = LightRegionData.Const.SIZE3D;
+
+ // padding to prevent overlap. set to minimum achievable without error
+ private static final int EXTENT_PADDING = 2;
+
+ // address for the static empty region.
+ static final short EMPTY_ADDRESS = 0;
+
+ private static final int INITIAL_ADDRESS_COUNT = 128;
+
+ private int pointerRows;
+ // note: pointer extent always cover the entire view distance, unlike light data manager extent
+ private int pointerExtent = -1;
+ private boolean needUploadPointers = false;
+ private boolean needUploadMeta = false;
+ private ByteBuffer pointerBuffer;
+ private boolean requireTextureRemake;
+
+ private int addressLimit = INITIAL_ADDRESS_COUNT;
+ // dummy value, need immediate initialization
+ private int dynamicMaxAddresses = 1;
+
+ // 0 is reserved for empty address
+ private short nextAllocateAddress = 1;
+ private int addressCount = 1;
+
+ private float dataSize = 0f;
+
+ private final Short2LongOpenHashMap allocatedAddresses = new Short2LongOpenHashMap();
+ private final ShortStack freedAddresses = new ShortArrayList();
+
+ LightDataAllocator() {
+ increaseAddressSize(INITIAL_ADDRESS_COUNT);
+ }
+
+ private void ensureWithinLimits() {
+ final int maxTextureHeight = CanvasGlHelper.maxTextureSize() == 0 ? 16384 : CanvasGlHelper.maxTextureSize();
+ addressLimit = Math.max(INITIAL_ADDRESS_COUNT, Math.min(maxTextureHeight - pointerRows, MAX_ADDRESSES));
+
+ if (dynamicMaxAddresses > addressLimit) {
+ dynamicMaxAddresses = addressLimit;
+ requireTextureRemake = true;
+ }
+
+ // Remove addresses exceeding limit
+ if (addressCount > dynamicMaxAddresses) {
+ for (short addressToRemove = (short) dynamicMaxAddresses; addressToRemove < addressCount; addressToRemove++) {
+ removeAddressIfAllocated(addressToRemove);
+ }
+
+ addressCount = dynamicMaxAddresses;
+
+ if (nextAllocateAddress > addressCount) {
+ nextAllocateAddress = EMPTY_ADDRESS + 1;
+ }
+ }
+ }
+
+ private void resizePointerBuffer(int newPointerExtent) {
+ if (newPointerExtent == pointerExtent) {
+ return;
+ }
+
+ pointerExtent = newPointerExtent;
+
+ final int pointerCountReq = pointerExtent * pointerExtent * pointerExtent;
+ var prevRows = pointerRows;
+ pointerRows = pointerCountReq / ROW_SIZE + ((pointerCountReq % ROW_SIZE == 0) ? 0 : 1);
+ ensureWithinLimits();
+
+ if (DEBUG_LOG_RESIZE) {
+ CanvasMod.LOG.info("Resized pointer storage capacity from " + prevRows + " to " + pointerRows);
+ }
+
+ if (pointerBuffer != null) {
+ pointerBuffer.position(0);
+ MemoryUtil.memFree(pointerBuffer);
+ }
+
+ pointerBuffer = MemoryUtil.memAlloc(pointerRows * ROW_SIZE * 2);
+
+ // reset each storage value
+ while (pointerBuffer.position() < pointerBuffer.limit()) {
+ pointerBuffer.putShort((short) 0);
+ }
+
+ // remap old pointers
+ final var searchPos = new BlockPos.MutableBlockPos();
+
+ for (var entry : allocatedAddresses.short2LongEntrySet()) {
+ setPointer(searchPos.set(entry.getLongValue()), entry.getShortKey());
+ }
+
+ requireTextureRemake = true;
+ needUploadMeta = true;
+ }
+
+ // currently, this only increase size
+ // TODO: shrink (reclaim video memory) - not very necessary though?
+ private void increaseAddressSize(int newSize) {
+ ensureWithinLimits();
+
+ final int cappedNewSize = Math.min(newSize, addressLimit);
+
+ if (dynamicMaxAddresses == cappedNewSize) {
+ return;
+ }
+
+ if (DEBUG_LOG_RESIZE) {
+ CanvasMod.LOG.info("Resized light data address capacity from " + dynamicMaxAddresses + " to " + cappedNewSize);
+ }
+
+ dynamicMaxAddresses = cappedNewSize;
+
+ requireTextureRemake = true;
+ }
+
+ int allocateAddress(LightRegion region) {
+ if (!region.hasData) {
+ return clearAddress(region);
+ }
+
+ short regionAddress = region.texAllocation;
+
+ if (regionAddress == EMPTY_ADDRESS) {
+ regionAddress = allocateAddressInner(region);
+ }
+
+ return regionAddress;
+ }
+
+ private short allocateAddressInner(LightRegion region) {
+ assert region.hasData;
+ final short newAddress;
+
+ // go through freed addresses first
+ if (!freedAddresses.isEmpty()) {
+ newAddress = freedAddresses.popShort();
+ } else {
+ if (addressCount == dynamicMaxAddresses) {
+ // try resize first
+ increaseAddressSize(dynamicMaxAddresses * 2);
+ }
+
+ if (addressCount < dynamicMaxAddresses) {
+ newAddress = nextAllocateAddress;
+ nextAllocateAddress++;
+ addressCount++;
+ } else {
+ if (nextAllocateAddress >= addressCount) {
+ // rolling pointer
+ nextAllocateAddress = EMPTY_ADDRESS + 1;
+ }
+
+ final short nextAddress = nextAllocateAddress;
+ removeAddressIfAllocated(nextAddress);
+ newAddress = nextAddress;
+ nextAllocateAddress++;
+ }
+ }
+
+ return setAddress(region, newAddress);
+ }
+
+ // MAINTENANCE NOTICE: this function is a special casing of setAddress(LightRegion, short)
+ private short clearAddress(LightRegion region) {
+ if (region.texAllocation != EMPTY_ADDRESS) {
+ clearPointerIfWas(region.originPos, region.texAllocation);
+ region.texAllocation = EMPTY_ADDRESS;
+ }
+
+ return EMPTY_ADDRESS;
+ }
+
+ private short setAddress(LightRegion region, short newAddress) {
+ assert newAddress != EMPTY_ADDRESS;
+
+ region.texAllocation = newAddress;
+ setPointer(region.originPos, newAddress);
+ allocatedAddresses.put(newAddress, region.origin);
+
+ return newAddress;
+ }
+
+ private int getBufferIndex(BlockPos regionOrigin) {
+ final int xInExtent = ((regionOrigin.getX() / 16) % pointerExtent + pointerExtent) % pointerExtent;
+ final int yInExtent = ((regionOrigin.getY() / 16) % pointerExtent + pointerExtent) % pointerExtent;
+ final int zInExtent = ((regionOrigin.getZ() / 16) % pointerExtent + pointerExtent) % pointerExtent;
+ final int pointerIndex = xInExtent * pointerExtent * pointerExtent + yInExtent * pointerExtent + zInExtent;
+ return pointerIndex * 2;
+ }
+
+ private void setPointer(BlockPos regionOrigin, short regionAddress) {
+ final int bufferIndex = getBufferIndex(regionOrigin);
+ final short storedAddress = pointerBuffer.getShort(bufferIndex);
+ pointerBuffer.putShort(bufferIndex, regionAddress);
+ needUploadPointers |= storedAddress != regionAddress;
+ }
+
+ // MAINTENANCE NOTICE: this function is a special casing of setPointer(BlockPos, short)
+ private void clearPointerIfWas(BlockPos regionOrigin, short oldAddress) {
+ final int bufferIndex = getBufferIndex(regionOrigin);
+ final short storedAddress = pointerBuffer.getShort(bufferIndex);
+
+ if (storedAddress == oldAddress) {
+ pointerBuffer.putShort(bufferIndex, EMPTY_ADDRESS);
+ needUploadPointers = true;
+ }
+ }
+
+ void freeAddress(LightRegion region) {
+ if (region.texAllocation != EMPTY_ADDRESS) {
+ final short oldAddress = region.texAllocation;
+ freedAddresses.push(oldAddress);
+ allocatedAddresses.remove(oldAddress);
+ clearAddress(region);
+ }
+ }
+
+ void removeAddressIfAllocated(short addressToRemove) {
+ if (allocatedAddresses.containsKey(addressToRemove)) {
+ final long oldOrigin = allocatedAddresses.get(addressToRemove);
+ allocatedAddresses.remove(addressToRemove);
+
+ final LightRegion oldRegion = LightDataManager.INSTANCE.get(oldOrigin);
+
+ if (oldRegion != null) {
+ clearAddress(oldRegion);
+ }
+ }
+ }
+
+ void textureRemade() {
+ if (DEBUG_LOG_RESIZE) {
+ CanvasMod.LOG.info("Light texture was remade, new size : " + textureHeight());
+ }
+
+ requireTextureRemake = false;
+ needUploadPointers = true;
+ needUploadMeta = true;
+ dataSize = (float) (textureWidth() * textureHeight() * 4d / (double) 0x100000);
+ }
+
+ public int dataRowStart() {
+ return pointerRows;
+ }
+
+ public boolean checkInvalid() {
+ final var viewDistance = Minecraft.getInstance().options.renderDistance().get();
+ final var expectedExtent = (viewDistance + EXTENT_PADDING) * 2;
+
+ if (pointerExtent < expectedExtent) {
+ resizePointerBuffer(expectedExtent);
+ }
+
+ return requireTextureRemake;
+ }
+
+ public boolean isInvalid() {
+ return requireTextureRemake;
+ }
+
+ public int textureWidth() {
+ return ROW_SIZE;
+ }
+
+ public int textureHeight() {
+ return pointerRows + dynamicMaxAddresses;
+ }
+
+ void uploadPointersIfNeeded(LightDataTexture texture) {
+ if (needUploadMeta) {
+ needUploadMeta = false;
+ IntData.UINT_DATA.put(IntData.LIGHT_POINTER_EXTENT, pointerExtent);
+ IntData.UINT_DATA.put(IntData.LIGHT_DATA_FIRST_ROW, pointerRows);
+ }
+
+ if (needUploadPointers) {
+ needUploadPointers = false;
+ texture.upload(0, pointerRows, pointerBuffer);
+ }
+ }
+
+ String debugString() {
+ return String.format("ColoredLights%s (%d) %d/%d %5.1fMb", Pipeline.config().coloredLights.useOcclusionData ? "+Occlusion" : "", pointerRows, addressCount, dynamicMaxAddresses, dataSize);
+ }
+}
diff --git a/src/main/java/grondag/canvas/light/color/LightDataManager.java b/src/main/java/grondag/canvas/light/color/LightDataManager.java
new file mode 100644
index 000000000..44a7d9aa2
--- /dev/null
+++ b/src/main/java/grondag/canvas/light/color/LightDataManager.java
@@ -0,0 +1,381 @@
+/*
+ * This file is part of Canvas Renderer and is licensed to the project under
+ * terms that are compatible with the GNU Lesser General Public License.
+ * See the NOTICE file distributed with this work for additional information
+ * regarding copyright ownership and licensing.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+package grondag.canvas.light.color;
+
+import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
+import it.unimi.dsi.fastutil.longs.Long2ObjectMaps;
+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
+import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue;
+import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
+import it.unimi.dsi.fastutil.longs.LongPriorityQueue;
+import it.unimi.dsi.fastutil.longs.LongSet;
+import it.unimi.dsi.fastutil.longs.LongSets;
+import org.apache.logging.log4j.Level;
+import org.lwjgl.system.MemoryUtil;
+
+import net.minecraft.client.multiplayer.ClientLevel;
+import net.minecraft.core.BlockPos;
+
+import io.vram.frex.api.config.FlawlessFrames;
+
+import grondag.canvas.CanvasMod;
+import grondag.canvas.pipeline.Pipeline;
+
+public class LightDataManager {
+ static LightDataManager INSTANCE;
+
+ public static LightRegionAccess allocate(BlockPos regionOrigin) {
+ if (INSTANCE == null) {
+ return LightRegionAccess.EMPTY;
+ }
+
+ return INSTANCE.allocateInner(regionOrigin);
+ }
+
+ public static void reload(boolean chunkReload) {
+ if (Pipeline.coloredLightsEnabled()) {
+ assert Pipeline.config().coloredLights != null;
+
+ // NB: this is a shader recompile, not chunk storage reload. DON'T destroy existing instance.
+ if (INSTANCE == null) {
+ INSTANCE = new LightDataManager();
+ } else if (chunkReload) {
+ // if the chunk storage was reloaded, the light level object needs to be reloaded as well
+ INSTANCE.lightLevel.reload();
+ }
+
+ INSTANCE.useOcclusionData = Pipeline.config().coloredLights.useOcclusionData;
+ } else {
+ // If colored lights state change, we can clean this up as the next state change will trigger chunk storage reload.
+ if (INSTANCE != null) {
+ INSTANCE.close();
+ INSTANCE = null;
+ }
+ }
+ }
+
+ public static void free(BlockPos regionOrigin) {
+ if (INSTANCE != null) {
+ INSTANCE.freeInner(regionOrigin);
+ }
+ }
+
+ public static void update(ClientLevel level, long deadlineNanos, Runnable profilerTask) {
+ if (INSTANCE != null) {
+ profilerTask.run();
+ INSTANCE.profiler.start();
+ INSTANCE.updateInner(level, FlawlessFrames.isActive() ? Long.MAX_VALUE : deadlineNanos);
+ INSTANCE.profiler.end();
+ }
+ }
+
+ public static int texId() {
+ if (INSTANCE != null && INSTANCE.texture != null) {
+ return INSTANCE.texture.texId();
+ }
+
+ return 0;
+ }
+
+ public static String debugString() {
+ if (INSTANCE != null) {
+ return INSTANCE.texAllocator.debugString();
+ }
+
+ return "Colored lights DISABLED";
+ }
+
+ private final Long2ObjectMap allocated = Long2ObjectMaps.synchronize(new Long2ObjectOpenHashMap<>(1024));
+
+ final LongSet publicUpdateQueue = LongSets.synchronize(new LongOpenHashSet());
+ final LongSet publicDrawQueue = LongSets.synchronize(new LongOpenHashSet());
+ private final LongPriorityQueue decreaseQueue = new LongArrayFIFOQueue();
+ private final LongPriorityQueue increaseQueue = new LongArrayFIFOQueue();
+
+ final LongSet publicUrgentUpdateQueue = LongSets.synchronize(new LongOpenHashSet());
+ private final LongPriorityQueue urgentDecreaseQueue = new LongArrayFIFOQueue();
+ private final LongPriorityQueue urgentIncreaseQueue = new LongArrayFIFOQueue();
+
+ private final LightDataAllocator texAllocator;
+ private final LightLevel lightLevel;
+
+ boolean useOcclusionData = false;
+ private LightDataTexture texture;
+
+ private DebugProfiler profiler = new ActiveProfiler();
+
+ public LightDataManager() {
+ texAllocator = new LightDataAllocator();
+ lightLevel = new LightLevel();
+ allocated.defaultReturnValue(null);
+ }
+
+ private void executeRegularUpdates(int minimumUpdates, long deadlineNanos) {
+ synchronized (publicUpdateQueue) {
+ for (long index : publicUpdateQueue) {
+ decreaseQueue.enqueue(index);
+ increaseQueue.enqueue(index);
+ }
+
+ publicUpdateQueue.clear();
+ }
+
+ int count = 0;
+
+ while (!decreaseQueue.isEmpty()) {
+ final long index = decreaseQueue.dequeueLong();
+ final LightRegion lightRegion = allocated.get(index);
+
+ if (lightRegion != null && !lightRegion.isClosed()) {
+ lightRegion.updateDecrease(lightLevel, decreaseQueue, increaseQueue);
+ }
+
+ if (++count > minimumUpdates && System.nanoTime() > deadlineNanos) {
+ break;
+ }
+ }
+
+ count = 0;
+
+ while (!increaseQueue.isEmpty()) {
+ final long index = increaseQueue.dequeueLong();
+ final LightRegion lightRegion = allocated.get(index);
+
+ if (lightRegion != null && !lightRegion.isClosed()) {
+ lightRegion.updateIncrease(lightLevel, increaseQueue);
+ }
+
+ if (++count > minimumUpdates && System.nanoTime() > deadlineNanos) {
+ break;
+ }
+ }
+ }
+
+ private void updateInner(ClientLevel level, long deadlineNanos) {
+ lightLevel.updateOnStartFrame(level);
+
+ synchronized (publicUrgentUpdateQueue) {
+ for (long index : publicUrgentUpdateQueue) {
+ urgentDecreaseQueue.enqueue(index);
+ urgentIncreaseQueue.enqueue(index);
+ publicUpdateQueue.remove(index);
+ }
+
+ publicUrgentUpdateQueue.clear();
+ }
+
+ while (!urgentDecreaseQueue.isEmpty()) {
+ final long index = urgentDecreaseQueue.dequeueLong();
+ final LightRegion lightRegion = allocated.get(index);
+
+ if (lightRegion != null && !lightRegion.isClosed()) {
+ lightRegion.updateDecrease(lightLevel, urgentDecreaseQueue, urgentIncreaseQueue);
+ }
+ }
+
+ executeRegularUpdates(7, deadlineNanos);
+
+ while (!urgentIncreaseQueue.isEmpty()) {
+ final long index = urgentIncreaseQueue.dequeueLong();
+ final LightRegion lightRegion = allocated.get(index);
+
+ if (lightRegion != null && !lightRegion.isClosed()) {
+ lightRegion.updateIncrease(lightLevel, urgentIncreaseQueue);
+ }
+ }
+
+ if (texAllocator.checkInvalid()) {
+ if (texture != null) {
+ texture.close();
+ }
+
+ texture = new LightDataTexture(texAllocator.textureWidth(), texAllocator.textureHeight());
+ texAllocator.textureRemade();
+
+ publicDrawQueue.clear();
+
+ // redraw
+ synchronized (allocated) {
+ for (var lightRegion : allocated.values()) {
+ if (!lightRegion.isClosed()) {
+ drawInner(lightRegion, true);
+ }
+ }
+ }
+ } else if (!publicDrawQueue.isEmpty()) {
+ final int queueLen;
+ final long queueIterator;
+
+ long i = 0L;
+
+ synchronized (publicDrawQueue) {
+ // hopefully faster with native op?
+ queueLen = publicDrawQueue.size();
+ queueIterator = MemoryUtil.nmemAlloc(8L * queueLen);
+
+ for (long index : publicDrawQueue) {
+ MemoryUtil.memPutLong(queueIterator + i * 8L, index);
+ i++;
+ }
+
+ publicDrawQueue.clear();
+ }
+
+ for (i = 0; i < queueLen; i++) {
+ final long index = MemoryUtil.memGetLong(queueIterator + i * 8L);
+ final LightRegion lightRegion = allocated.get(index);
+
+ if (lightRegion != null && !lightRegion.isClosed()) {
+ drawInner(lightRegion, false);
+ }
+ }
+
+ MemoryUtil.nmemFree(queueIterator);
+ }
+
+ texAllocator.uploadPointersIfNeeded(texture);
+ }
+
+ private void drawInner(LightRegion lightRegion, boolean redraw) {
+ if (lightRegion.lightData.hasBuffer() && (lightRegion.lightData.isDirty() || redraw)) {
+ final int targetAddress = texAllocator.allocateAddress(lightRegion);
+
+ if (texAllocator.isInvalid()) {
+ // don't draw to invalid texture, but continue looping to allocate the rest of the region queue
+ return;
+ }
+
+ if (targetAddress != LightDataAllocator.EMPTY_ADDRESS) {
+ final int targetRow = texAllocator.dataRowStart() + targetAddress;
+ texture.upload(targetRow, lightRegion.lightData.getBuffer());
+ }
+
+ lightRegion.lightData.clearDirty();
+ }
+ }
+
+ LightRegion getFromBlock(BlockPos blockPos) {
+ final long key = BlockPos.asLong(
+ blockPos.getX() & ~LightRegionData.Const.WIDTH_MASK,
+ blockPos.getY() & ~LightRegionData.Const.WIDTH_MASK,
+ blockPos.getZ() & ~LightRegionData.Const.WIDTH_MASK);
+ return allocated.get(key);
+ }
+
+ LightRegion get(long originKey) {
+ return allocated.get(originKey);
+ }
+
+ LightLevelAccess lightLevel() {
+ return lightLevel;
+ }
+
+ private void freeInner(BlockPos regionOrigin) {
+ final LightRegion lightRegion = allocated.get(regionOrigin.asLong());
+
+ if (lightRegion != null && !lightRegion.isClosed()) {
+ texAllocator.freeAddress(lightRegion);
+ lightRegion.close();
+ }
+
+ allocated.remove(regionOrigin.asLong());
+ }
+
+ private LightRegion allocateInner(BlockPos regionOrigin) {
+ if (allocated.containsKey(regionOrigin.asLong())) {
+ freeInner(regionOrigin);
+ }
+
+ final LightRegion lightRegion = new LightRegion(regionOrigin);
+ allocated.put(regionOrigin.asLong(), lightRegion);
+
+ return lightRegion;
+ }
+
+ public void close() {
+ texture.close();
+ lightLevel.close();
+
+ synchronized (allocated) {
+ for (var lightRegion : allocated.values()) {
+ if (!lightRegion.isClosed()) {
+ lightRegion.close();
+ }
+ }
+
+ allocated.clear();
+ }
+ }
+
+ private interface DebugProfiler {
+ void start();
+
+ void end();
+ }
+
+ private static class EmptyProfiler implements DebugProfiler {
+ @Override
+ public void start() {
+ }
+
+ @Override
+ public void end() {
+ }
+ }
+
+ private final class ActiveProfiler implements DebugProfiler {
+ private long minUpdateTimeNanos = Long.MAX_VALUE;
+ private long maxUpdateTimeNanos = 0;
+ private long totalUpdateTimeNanos = 0;
+ private long totalUpdatePerformed = 0;
+ private long startTimeNanos = 0;
+ private long startTimeOverall = 0;
+ private boolean init = false;
+
+ @Override
+ public void start() {
+ startTimeNanos = System.nanoTime();
+
+ if (!init) {
+ startTimeOverall = startTimeNanos;
+ init = true;
+ }
+ }
+
+ @Override
+ public void end() {
+ final long elapsedNanos = System.nanoTime() - startTimeNanos;
+ minUpdateTimeNanos = Math.min(elapsedNanos, minUpdateTimeNanos);
+ maxUpdateTimeNanos = Math.max(elapsedNanos, maxUpdateTimeNanos);
+ totalUpdateTimeNanos += elapsedNanos;
+ totalUpdatePerformed++;
+
+ if (System.nanoTime() - startTimeOverall > 10000000000L) {
+ final float minTime = (float) minUpdateTimeNanos / 1000000.0f;
+ final float maxTime = (float) maxUpdateTimeNanos / 1000000.0f;
+ final float avgTime = ((float) totalUpdateTimeNanos / (float) totalUpdatePerformed) / 1000000.0f;
+ // final String score = avgTime <= 2.5 ? "GOOD" : (avgTime <= 6.0 ? "OKAY" : "BAD");
+ CanvasMod.LOG.printf(Level.INFO, "Colored Lights frame time statistics: min %.2G ms; max %.2f ms; avg. %.2f ms; over 10 seconds (%d frames)", minTime, maxTime, avgTime, totalUpdatePerformed);
+ LightDataManager.this.profiler = new EmptyProfiler();
+ }
+ }
+ }
+}
diff --git a/src/main/java/grondag/canvas/light/color/LightDataTexture.java b/src/main/java/grondag/canvas/light/color/LightDataTexture.java
new file mode 100644
index 000000000..6b5306acc
--- /dev/null
+++ b/src/main/java/grondag/canvas/light/color/LightDataTexture.java
@@ -0,0 +1,115 @@
+/*
+ * This file is part of Canvas Renderer and is licensed to the project under
+ * terms that are compatible with the GNU Lesser General Public License.
+ * See the NOTICE file distributed with this work for additional information
+ * regarding copyright ownership and licensing.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+package grondag.canvas.light.color;
+
+import java.nio.ByteBuffer;
+
+import org.lwjgl.opengl.GL11;
+
+import com.mojang.blaze3d.platform.TextureUtil;
+import com.mojang.blaze3d.systems.RenderSystem;
+
+import grondag.canvas.CanvasMod;
+import grondag.canvas.render.CanvasTextureState;
+import grondag.canvas.varia.GFX;
+
+public class LightDataTexture {
+ public static class Format {
+ public static int TARGET = GFX.GL_TEXTURE_2D;
+ public static int PIXEL_BYTES = 2;
+ public static int INTERNAL_FORMAT = GFX.GL_RGBA4;
+ public static int PIXEL_FORMAT = GFX.GL_RGBA;
+ public static int TYPE = GFX.GL_UNSIGNED_SHORT_4_4_4_4;
+ }
+
+ private final int glId;
+ private final int width;
+ private final int height;
+ private boolean closed = false;
+
+ LightDataTexture(int width, int height) {
+ this.width = width;
+ this.height = height;
+
+ glId = TextureUtil.generateTextureId();
+ CanvasTextureState.bindTexture(glId);
+
+ GFX.objectLabel(GL11.GL_TEXTURE, glId, "IMG auto_colored_lights");
+
+ GFX.texParameter(Format.TARGET, GFX.GL_TEXTURE_MIN_FILTER, GFX.GL_NEAREST);
+ GFX.texParameter(Format.TARGET, GFX.GL_TEXTURE_MAG_FILTER, GFX.GL_NEAREST);
+ GFX.texParameter(Format.TARGET, GFX.GL_TEXTURE_WRAP_S, GFX.GL_CLAMP_TO_EDGE);
+ GFX.texParameter(Format.TARGET, GFX.GL_TEXTURE_WRAP_T, GFX.GL_CLAMP_TO_EDGE);
+
+ GFX.texImage2D(Format.TARGET, 0, Format.INTERNAL_FORMAT, width, height, 0, Format.PIXEL_FORMAT, Format.TYPE, (ByteBuffer) null);
+ }
+
+ public int texId() {
+ if (closed) {
+ return 0;
+ }
+
+ return glId;
+ }
+
+ public void close() {
+ if (closed) {
+ return;
+ }
+
+ TextureUtil.releaseTextureId(glId);
+
+ closed = true;
+ }
+
+ public void upload(int row, ByteBuffer buffer) {
+ upload(row, 1, buffer);
+ }
+
+ public void upload(int rowStart, int rowCount, ByteBuffer buffer) {
+ uploadDirect(0, rowStart, width, rowCount, buffer);
+ }
+
+ public void uploadDirect(int x, int y, int width, int height, ByteBuffer buffer) {
+ if (closed) {
+ throw new IllegalStateException("Uploading to a closed light texture!");
+ }
+
+ if (x + width > this.width || y + height > this.height) {
+ CanvasMod.LOG.warn("Drawing light texture out of bounds");
+ }
+
+ RenderSystem.assertOnRenderThread();
+
+ CanvasTextureState.bindTexture(glId);
+
+ // Gotta clean up some states, otherwise will cause memory access violation
+ GFX.pixelStore(GFX.GL_UNPACK_SKIP_PIXELS, 0);
+ GFX.pixelStore(GFX.GL_UNPACK_SKIP_ROWS, 0);
+ GFX.pixelStore(GFX.GL_UNPACK_ROW_LENGTH, 0);
+ GFX.pixelStore(GFX.GL_UNPACK_ALIGNMENT, 2);
+
+ // Importantly, reset the pointer without flip
+ buffer.position(0);
+
+ GFX.glTexSubImage2D(Format.TARGET, 0, x, y, width, height, Format.PIXEL_FORMAT, Format.TYPE, buffer);
+ }
+}
diff --git a/src/main/java/grondag/canvas/light/color/LightLevel.java b/src/main/java/grondag/canvas/light/color/LightLevel.java
new file mode 100644
index 000000000..5d0e21e99
--- /dev/null
+++ b/src/main/java/grondag/canvas/light/color/LightLevel.java
@@ -0,0 +1,221 @@
+/*
+ * This file is part of Canvas Renderer and is licensed to the project under
+ * terms that are compatible with the GNU Lesser General Public License.
+ * See the NOTICE file distributed with this work for additional information
+ * regarding copyright ownership and licensing.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+package grondag.canvas.light.color;
+
+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
+import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue;
+import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
+import it.unimi.dsi.fastutil.shorts.ShortArrayList;
+import org.jetbrains.annotations.Nullable;
+
+import net.minecraft.client.multiplayer.ClientLevel;
+import net.minecraft.core.BlockPos;
+import net.minecraft.world.level.BlockAndTintGetter;
+import net.minecraft.world.level.block.state.BlockState;
+
+import grondag.canvas.CanvasMod;
+import grondag.canvas.config.Configurator;
+import grondag.canvas.pipeline.Pipeline;
+
+class LightLevel implements LightLevelAccess {
+ private BlockAndTintGetter baseLevel = null;
+ private EntityLightTracker entityLightTracker = null;
+
+ private final Long2ObjectOpenHashMap virtualLights = new Long2ObjectOpenHashMap<>();
+ private final LongArrayFIFOQueue virtualQueue = new LongArrayFIFOQueue();
+ private final ObjectOpenHashSet virtualCheckQueue = new ObjectOpenHashSet<>();
+
+ private static final Operator DO_NOTHING = (pos, light) -> { };
+ private static final Getter GET_ZERO = pos -> (short) 0;
+
+ private Operator placer = DO_NOTHING;
+ private Operator remover = DO_NOTHING;
+ private Getter getter = GET_ZERO;
+
+ LightLevel() {
+ reload();
+
+ // dummy test
+ // placeVirtualLight(new BlockPos(-278, 43, 6), LightOp.encodeLight(0x0, 0xf, 0x0, false, true, false));
+ }
+
+ public void reload() {
+ assert Pipeline.config().coloredLights != null;
+
+ final boolean virtualAllowed = Pipeline.config().coloredLights.allowVirtual;
+
+ if (Configurator.entityLightSource && virtualAllowed) {
+ if (entityLightTracker == null) {
+ entityLightTracker = new EntityLightTracker(this);
+ } else {
+ // no need to clear as we are nuking everything
+ entityLightTracker.reload(false);
+ }
+ } else {
+ if (entityLightTracker != null) {
+ // no need to clear as we are nuking everything
+ entityLightTracker.close(false);
+ }
+
+ entityLightTracker = null;
+ }
+
+ // NB: implementations are expected to repopulate every time chunk is reloaded
+ virtualLights.clear();
+
+ if (virtualAllowed) {
+ placer = PLACE_VIRTUAL;
+ remover = REMOVE_VIRTUAL;
+ getter = GET_VIRTUAL;
+ } else {
+ placer = DO_NOTHING;
+ remover = DO_NOTHING;
+ getter = GET_ZERO;
+ }
+ }
+
+ void updateOnStartFrame(ClientLevel level) {
+ if (baseLevel != level) {
+ baseLevel = level;
+
+ if (entityLightTracker != null) {
+ entityLightTracker.reload(true);
+ }
+ }
+
+ if (entityLightTracker != null) {
+ entityLightTracker.update(level);
+ }
+
+ updateInner();
+ }
+
+ @Override
+ public BlockAndTintGetter level() {
+ return baseLevel;
+ }
+
+ @Override
+ public short getRegistered(BlockPos pos) {
+ var registered = baseLevel == null ? 0 : LightRegistry.get(baseLevel.getBlockState(pos));
+ return combineWithBlockLight(registered, getter.apply(pos));
+ }
+
+ @Override
+ public short getRegistered(BlockPos pos, @Nullable BlockState state) {
+ // MAINTENANCE NOTICE: this function is a special casing of getRegistered(BlockPos)
+ var registered = state != null ? LightRegistry.get(state) : (baseLevel == null ? 0 : LightRegistry.get(baseLevel.getBlockState(pos)));
+ return combineWithBlockLight(registered, getter.apply(pos));
+ }
+
+ @Override
+ public void placeVirtualLight(BlockPos blockPos, short light) {
+ placer.apply(blockPos, light);
+ }
+
+ @Override
+ public void removeVirtualLight(BlockPos blockPos, short light) {
+ remover.apply(blockPos, light);
+ }
+
+ private final Operator PLACE_VIRTUAL = (blockPos, light) -> {
+ final long pos = blockPos.asLong();
+ final var list = virtualLights.computeIfAbsent(pos, l -> new ShortArrayList());
+ list.add(encodeVirtualLight(light));
+ virtualQueue.enqueue(pos);
+ };
+
+ private final Operator REMOVE_VIRTUAL = (blockPos, light) -> {
+ final long pos = blockPos.asLong();
+ final var list = virtualLights.get(pos);
+ final int i = list == null ? -1 : list.indexOf(encodeVirtualLight(light));
+
+ if (i != -1) {
+ list.removeShort(i);
+ virtualQueue.enqueue(pos);
+ }
+ };
+
+ void close() {
+ if (entityLightTracker != null) {
+ // no need to clear as we are nuking everything
+ entityLightTracker.close(false);
+ entityLightTracker = null;
+ }
+ }
+
+ private final Getter GET_VIRTUAL = blockPos -> {
+ ShortArrayList lights = virtualLights.get(blockPos.asLong());
+
+ if (lights != null) {
+ short combined = 0;
+
+ for (short light : lights) {
+ combined = LightOp.max(light, combined);
+ }
+
+ return combined;
+ }
+
+ return (short) 0;
+ };
+
+ private void updateInner() {
+ while (!virtualQueue.isEmpty()) {
+ long pos = virtualQueue.dequeueLong();
+ var blockPos = BlockPos.of(pos);
+ var region = LightDataManager.INSTANCE.getFromBlock(blockPos);
+
+ if (region != null) {
+ region.checkBlock(blockPos, null);
+ virtualCheckQueue.add(region);
+ } else if (virtualLights.containsKey(pos)) {
+ // there are virtual lights (placed and never removed) but the region doesn't exist
+ CanvasMod.LOG.warn("Trying to update virtual lights on a null region. ID:" + pos);
+ }
+ }
+
+ for (var region : virtualCheckQueue) {
+ region.markUrgent();
+ region.submitChecks();
+ }
+
+ virtualCheckQueue.clear();
+ }
+
+ public static short encodeVirtualLight(short light) {
+ return LightOp.encodeLight(LightOp.pure(light), false, true, false);
+ }
+
+ private static short combineWithBlockLight(short block, short virtual) {
+ return LightOp.makeEmitter(LightOp.max(block, virtual));
+ }
+
+ @FunctionalInterface
+ private interface Operator {
+ void apply(BlockPos pos, short light);
+ }
+
+ @FunctionalInterface
+ private interface Getter {
+ short apply(BlockPos pos);
+ }
+}
diff --git a/src/main/java/grondag/canvas/light/color/LightLevelAccess.java b/src/main/java/grondag/canvas/light/color/LightLevelAccess.java
new file mode 100644
index 000000000..b834d2301
--- /dev/null
+++ b/src/main/java/grondag/canvas/light/color/LightLevelAccess.java
@@ -0,0 +1,39 @@
+/*
+ * This file is part of Canvas Renderer and is licensed to the project under
+ * terms that are compatible with the GNU Lesser General Public License.
+ * See the NOTICE file distributed with this work for additional information
+ * regarding copyright ownership and licensing.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+package grondag.canvas.light.color;
+
+import org.jetbrains.annotations.Nullable;
+
+import net.minecraft.core.BlockPos;
+import net.minecraft.world.level.BlockAndTintGetter;
+import net.minecraft.world.level.block.state.BlockState;
+
+public interface LightLevelAccess {
+ BlockAndTintGetter level();
+
+ short getRegistered(BlockPos pos);
+
+ short getRegistered(BlockPos pos, @Nullable BlockState state);
+
+ void placeVirtualLight(BlockPos blockPos, short light);
+
+ void removeVirtualLight(BlockPos blockPos, short light);
+}
diff --git a/src/main/java/grondag/canvas/light/color/LightOp.java b/src/main/java/grondag/canvas/light/color/LightOp.java
new file mode 100644
index 000000000..131fd4a80
--- /dev/null
+++ b/src/main/java/grondag/canvas/light/color/LightOp.java
@@ -0,0 +1,177 @@
+/*
+ * This file is part of Canvas Renderer and is licensed to the project under
+ * terms that are compatible with the GNU Lesser General Public License.
+ * See the NOTICE file distributed with this work for additional information
+ * regarding copyright ownership and licensing.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+package grondag.canvas.light.color;
+
+public enum LightOp {
+ R(0xF000, 12, 0),
+ G(0x0F00, 8, 1),
+ B(0x00F0, 4, 2),
+ A(0x000F, 0, 3);
+
+ public final int mask;
+ public final int shift;
+ public final int pos;
+
+ LightOp(int mask, int shift, int pos) {
+ this.mask = mask;
+ this.shift = shift;
+ this.pos = pos;
+ }
+
+ private static final int EMITTER_FLAG = 0b1;
+ private static final int OCCLUDER_FLAG = 0b10;
+ private static final int FULL_FLAG = 0b100;
+ private static final int USEFUL_FLAG = 0b1000;
+
+ public static short EMPTY = 0;
+
+ public static short encode(int r, int g, int b, int a) {
+ return (short) ((r << R.shift) | (g << G.shift) | (b << B.shift) | (a << A.shift));
+ }
+
+ public static short encodeLight(int r, int g, int b, boolean isFull, boolean isEmitter, boolean isOccluding) {
+ return ensureUsefulness(encode(r, g, b, encodeAlpha(isFull, isEmitter, isOccluding)));
+ }
+
+ public static short encodeLight(int pureLight, boolean isFull, boolean isEmitter, boolean isOccluding) {
+ return ensureUsefulness((short) (pureLight | encodeAlpha(isFull, isEmitter, isOccluding)));
+ }
+
+ public static short pure(short light) {
+ return (short) (light & 0xfff0);
+ }
+
+ public static short makeEmitter(short light) {
+ return (short) (light | EMITTER_FLAG);
+ }
+
+ private static int encodeAlpha(boolean isFull, boolean isEmitter, boolean isOccluding) {
+ return (isFull ? FULL_FLAG : 0) | (isEmitter ? EMITTER_FLAG : 0) | (isOccluding ? OCCLUDER_FLAG : 0);
+ }
+
+ private static short ensureUsefulness(short light) {
+ boolean useful = lit(light) || !occluder(light);
+ return useful ? (short) (light | USEFUL_FLAG) : (short) (light & ~USEFUL_FLAG);
+ }
+
+ public static boolean emitter(short light) {
+ return (light & EMITTER_FLAG) != 0;
+ }
+
+ public static boolean occluder(short light) {
+ return (light & OCCLUDER_FLAG) != 0;
+ }
+
+ public static boolean full(short light) {
+ return (light & FULL_FLAG) != 0;
+ }
+
+ public static boolean lit(short light) {
+ return (light & 0xfff0) != 0;
+ }
+
+ public static short max(short master, short sub) {
+ final short max = (short) (Math.max(master & R.mask, sub & R.mask)
+ | Math.max(master & G.mask, sub & G.mask)
+ | Math.max(master & B.mask, sub & B.mask)
+ | master & 0xf);
+ return ensureUsefulness(max);
+ }
+
+ public static short replaceMinusOne(short target, short source, BVec opFlag) {
+ if (opFlag.r) {
+ target = R.replace(target, (short) (R.of(source) - 1));
+ }
+
+ if (opFlag.g) {
+ target = G.replace(target, (short) (G.of(source) - 1));
+ }
+
+ if (opFlag.b) {
+ target = B.replace(target, (short) (B.of(source) - 1));
+ }
+
+ return LightOp.ensureUsefulness(target);
+ }
+
+ public static short remove(short target, BVec opFlag) {
+ int mask = (opFlag.r ? LightOp.R.mask : 0) | (opFlag.g ? LightOp.G.mask : 0) | (opFlag.b ? LightOp.B.mask : 0);
+ return LightOp.ensureUsefulness((short) (target & ~mask));
+ }
+
+ public int of(short light) {
+ return (light >> shift) & 0xF;
+ }
+
+ private short replace(short source, short elemLight) {
+ return (short) ((source & ~mask) | (elemLight << shift));
+ }
+
+ public static String text(short light) {
+ return "(" + R.of(light) + "," + G.of(light) + "," + B.of(light) + ")";
+ }
+
+ static class BVec {
+ boolean r, g, b;
+
+ BVec() {
+ this.r = false;
+ this.g = false;
+ this.b = false;
+ }
+
+ boolean any() {
+ return r || g || b;
+ }
+
+ boolean all() {
+ return r && g && b;
+ }
+
+ BVec not() {
+ r = !r;
+ g = !g;
+ b = !b;
+ return this;
+ }
+
+ BVec lessThan(short left, short right) {
+ r = R.of(left) < R.of(right);
+ g = G.of(left) < G.of(right);
+ b = B.of(left) < B.of(right);
+ return this;
+ }
+
+ BVec lessThanMinusOne(short left, short right) {
+ r = R.of(left) < R.of(right) - 1;
+ g = G.of(left) < G.of(right) - 1;
+ b = B.of(left) < B.of(right) - 1;
+ return this;
+ }
+
+ BVec and(BVec other, BVec another) {
+ r = other.r && another.r;
+ g = other.g && another.g;
+ b = other.b && another.b;
+ return this;
+ }
+ }
+}
diff --git a/src/main/java/grondag/canvas/light/color/LightRegion.java b/src/main/java/grondag/canvas/light/color/LightRegion.java
new file mode 100644
index 000000000..cde412be9
--- /dev/null
+++ b/src/main/java/grondag/canvas/light/color/LightRegion.java
@@ -0,0 +1,528 @@
+/*
+ * This file is part of Canvas Renderer and is licensed to the project under
+ * terms that are compatible with the GNU Lesser General Public License.
+ * See the NOTICE file distributed with this work for additional information
+ * regarding copyright ownership and licensing.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+package grondag.canvas.light.color;
+
+import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue;
+import it.unimi.dsi.fastutil.longs.LongPriorityQueue;
+import it.unimi.dsi.fastutil.longs.LongPriorityQueues;
+import org.jetbrains.annotations.Nullable;
+
+import net.minecraft.core.BlockPos;
+import net.minecraft.core.Direction;
+import net.minecraft.world.level.block.state.BlockState;
+import net.minecraft.world.phys.shapes.Shapes;
+
+// TODO: cluster slab allocation? -> maybe unneeded now?
+// TODO: a way to repopulate cluster if needed
+class LightRegion implements LightRegionAccess {
+ private enum Side {
+ DOWN(0, -1, 0, 1, Direction.DOWN),
+ UP(0, 1, 0, 2, Direction.UP),
+ NORTH(0, 0, -1, 3, Direction.NORTH),
+ SOUTH(0, 0, 1, 4, Direction.SOUTH),
+ WEST(-1, 0, 0, 5, Direction.WEST),
+ EAST(1, 0, 0, 6, Direction.EAST);
+
+ final int x, y, z, id;
+ final Direction vanilla;
+ Side opposite;
+ static final int nullId = 0;
+
+ static {
+ DOWN.opposite = UP;
+ UP.opposite = DOWN;
+ NORTH.opposite = SOUTH;
+ SOUTH.opposite = NORTH;
+ WEST.opposite = EAST;
+ EAST.opposite = WEST;
+ }
+
+ Side(int x, int y, int z, int id, Direction vanilla) {
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ this.id = id;
+ this.vanilla = vanilla;
+ }
+
+ static Side infer(BlockPos from, BlockPos to) {
+ int x = to.getX() - from.getX();
+ int y = to.getY() - from.getY();
+ int z = to.getZ() - from.getZ();
+
+ for (Side side : Side.values()) {
+ if (side.x == x && side.y == y && side.z == z) {
+ return side;
+ }
+ }
+
+ // detects programming error (happened once)
+ throw new IndexOutOfBoundsException("Can't infer side. From: " + from + ", To: " + to);
+ }
+ }
+
+ private static class Queues {
+ static void enqueue(LongPriorityQueue queue, long index, long light) {
+ queue.enqueue(((long) Side.nullId << 48L) | (index << 16L) | light & 0xffffL);
+ }
+
+ static void enqueue(LongPriorityQueue queue, long index, long light, Side target) {
+ queue.enqueue(((long) target.opposite.id << 48L) | (index << 16L) | light & 0xffffL);
+ }
+
+ static int from(long entry) {
+ return (int) (entry >> 48L);
+ }
+
+ static int index(long entry) {
+ return (int) (entry >> 16L);
+ }
+
+ static short light(long entry) {
+ return (short) (entry);
+ }
+ }
+
+ final LightRegionData lightData;
+ final long origin;
+ final BlockPos originPos;
+ private final LightOp.BVec less = new LightOp.BVec();
+ private final BlockPos.MutableBlockPos sourcePos = new BlockPos.MutableBlockPos();
+ private final BlockPos.MutableBlockPos nodePos = new BlockPos.MutableBlockPos();
+ private final LongArrayFIFOQueue incQueue = new LongArrayFIFOQueue();
+ private final LongArrayFIFOQueue decQueue = new LongArrayFIFOQueue();
+
+ // PERF: since increase queue is only honored if it's "latest", it can be optimized with a Map
+ private final LongPriorityQueue globalIncQueue = LongPriorityQueues.synchronize(new LongArrayFIFOQueue());
+ private final LongPriorityQueue globalDecQueue = LongPriorityQueues.synchronize(new LongArrayFIFOQueue());
+
+ short texAllocation = LightDataAllocator.EMPTY_ADDRESS;
+ boolean hasData = false;
+ private boolean needCheckEdges = true;
+
+ LightRegion(BlockPos origin) {
+ this.originPos = new BlockPos(origin);
+ this.origin = origin.asLong();
+ this.lightData = new LightRegionData(origin.getX(), origin.getY(), origin.getZ());
+ }
+
+ @Override
+ public void checkBlock(BlockPos pos, @Nullable BlockState blockState) {
+ if (!lightData.withinExtents(pos)) {
+ return;
+ }
+
+ final int index = lightData.indexify(pos);
+ final short registeredLight = LightDataManager.INSTANCE.lightLevel().getRegistered(pos, blockState);
+ final boolean occluding = LightOp.occluder(registeredLight);
+
+ final short getLight = lightData.get(index);
+ final boolean emitting = LightOp.emitter(registeredLight);
+
+ // Equivalent of "hard reset" on this particular block pos, so we want it to pass specific checks
+ final boolean replaceEmitter = emitting && getLight != registeredLight;
+ final boolean removeEmitter = LightOp.emitter(getLight);
+ final boolean replaceOccluder = LightOp.occluder(getLight) != occluding;
+ final boolean occludeLitBlock = LightOp.lit(getLight) && occluding;
+
+ if (replaceEmitter || removeEmitter || replaceOccluder || occludeLitBlock) {
+ // If emitter, light with be placed later so put 0 here for removal purpose. Otherwise, put occlusion data here
+ lightData.put(index, emitting ? (short) 0 : registeredLight);
+
+ // Removal queue, since we're doing "hard reset"
+ Queues.enqueue(globalDecQueue, index, getLight);
+
+ // Emission queue
+ if (emitting) {
+ Queues.enqueue(globalIncQueue, index, registeredLight);
+ }
+
+ // At this point, no light data yet, but we might have occlusion data
+ if (LightDataManager.INSTANCE.useOcclusionData) {
+ hasData = true;
+ }
+ }
+ }
+
+ private boolean urgent = false;
+
+ @Override
+ public void submitChecks() {
+ if (!globalDecQueue.isEmpty() || !globalIncQueue.isEmpty()) {
+ if (urgent) {
+ LightDataManager.INSTANCE.publicUrgentUpdateQueue.add(origin);
+ } else {
+ LightDataManager.INSTANCE.publicUpdateQueue.add(origin);
+ }
+ }
+
+ urgent = false;
+ }
+
+ @Override
+ public void markUrgent() {
+ urgent = true;
+ }
+
+ private boolean occludeSide(Side dir, LightLevelAccess lightLevel, BlockPos pos) {
+ // vanilla checks state.useShapeForLightOcclusion() but here it's always false for some reason. this is fine...
+ var state = lightLevel.level().getBlockState(pos);
+
+ if (!state.canOcclude()) {
+ return false;
+ }
+
+ return Shapes.faceShapeOccludes(Shapes.empty(), state.getFaceOcclusionShape(lightLevel.level(), pos, dir.vanilla));
+ }
+
+ void updateDecrease(LightLevelAccess lightLevel, LongPriorityQueue neighborDecreaseQueue, LongPriorityQueue neighborIncreaseQueue) {
+ // faster exit when not necessary
+ if (globalDecQueue.isEmpty()) {
+ return;
+ }
+
+ while (!globalDecQueue.isEmpty()) {
+ decQueue.enqueue(globalDecQueue.dequeueLong());
+ }
+
+ final LightOp.BVec removeFlag = new LightOp.BVec();
+ final LightOp.BVec removeMask = new LightOp.BVec();
+
+ boolean didUpdate = false;
+
+ while (!decQueue.isEmpty()) {
+ final long entry = decQueue.dequeueLong();
+ final int index = Queues.index(entry);
+ final short sourcePrevLight = Queues.light(entry);
+ final int from = Queues.from(entry);
+
+ // only remove elements that are less than 1 (zero)
+ final short sourceCurrentLight = lightData.get(index);
+ removeFlag.lessThan(sourceCurrentLight, (short) 0x1110);
+
+ // NB: rarely happens, not worth the "if"
+ // if (!removeFlag.any()) continue;
+
+ lightData.reverseIndexify(index, sourcePos);
+
+ // unused for some reason
+ // final BlockState sourceState = blockView.getBlockState(sourcePos);
+
+ for (var side : Side.values()) {
+ if (side.id == from) {
+ continue;
+ }
+
+ nodePos.setWithOffset(sourcePos, side.x, side.y, side.z);
+
+ final LightRegionData dataAccess;
+ final LongPriorityQueue increaseQueue;
+ final LongPriorityQueue decreaseQueue;
+ boolean isNeighbor = !lightData.withinExtents(nodePos);
+ LightRegion neighbor = null;
+
+ if (isNeighbor) {
+ neighbor = LightDataManager.INSTANCE.getFromBlock(nodePos);
+
+ if (neighbor == null || neighbor.isClosed()) {
+ continue;
+ }
+
+ increaseQueue = neighbor.globalIncQueue;
+ decreaseQueue = neighbor.globalDecQueue;
+ dataAccess = neighbor.lightData;
+ } else {
+ increaseQueue = incQueue;
+ decreaseQueue = decQueue;
+ dataAccess = lightData;
+ }
+
+ final int nodeIndex = dataAccess.indexify(nodePos);
+ final short nodeLight = dataAccess.get(nodeIndex);
+
+ // Important: extremely high frequency redundancy filter (removes 99% of operations)
+ if (!LightOp.lit(nodeLight)) continue;
+
+ // check neighbor occlusion for decrease
+ if (!LightOp.emitter(nodeLight) && occludeSide(side.opposite, lightLevel, nodePos)) {
+ continue;
+ }
+
+ // only propagate removal according to removeFlag
+ removeMask.and(less.lessThan(nodeLight, sourcePrevLight), removeFlag);
+
+ final short registered = lightLevel.getRegistered(nodePos);
+ final boolean restoreLightSource = removeMask.any() && LightOp.emitter(registered);
+ final short repropLight;
+
+ if (removeMask.any()) {
+ final short resultLight = LightOp.remove(nodeLight, removeMask);
+
+ // high frequency redundancy when removing next to a different colored light, low otherwise
+ if (resultLight == nodeLight) continue;
+
+ dataAccess.put(nodeIndex, resultLight);
+ Queues.enqueue(decreaseQueue, nodeIndex, nodeLight, side);
+
+ // congrats, the queued update was not redundant!
+ didUpdate = true;
+
+ if (isNeighbor) {
+ neighborDecreaseQueue.enqueue(neighbor.origin);
+ }
+
+ // restore obliterated light source
+ if (restoreLightSource) {
+ // defer putting light source as to not mess with decrease step
+ repropLight = registered;
+ } else {
+ repropLight = resultLight;
+ }
+ } else {
+ repropLight = nodeLight;
+ }
+
+ if (removeMask.and(less.not(), removeFlag).any() || restoreLightSource) {
+ // increases queued in decrease may propagate to all directions as if a light source
+ Queues.enqueue(increaseQueue, nodeIndex, repropLight);
+
+ if (isNeighbor) {
+ neighborIncreaseQueue.enqueue(neighbor.origin);
+ }
+ }
+ }
+ }
+
+ if (didUpdate) {
+ lightData.markAsDirty();
+ LightDataManager.INSTANCE.publicDrawQueue.add(origin);
+ }
+ }
+
+ void updateIncrease(LightLevelAccess lightLevel, LongPriorityQueue neighborIncreaseQueue) {
+ if (needCheckEdges) {
+ needCheckEdges = false;
+ checkEdges(lightLevel);
+ }
+
+ while (!globalIncQueue.isEmpty()) {
+ incQueue.enqueue(globalIncQueue.dequeueLong());
+ }
+
+ while (!incQueue.isEmpty()) {
+ final long entry = incQueue.dequeueLong();
+ final int index = Queues.index(entry);
+ final short recordedLight = Queues.light(entry);
+ final int from = Queues.from(entry);
+
+ final short getLight = lightData.get(index);
+ final short sourceLight;
+
+ if (getLight != recordedLight) {
+ if (LightOp.emitter(recordedLight)) {
+ lightData.reverseIndexify(index, sourcePos);
+
+ if (lightLevel.getRegistered(sourcePos) != recordedLight) {
+ continue;
+ }
+
+ // take max of current and recorded source
+ sourceLight = LightOp.max(recordedLight, getLight);
+ lightData.put(index, sourceLight);
+ } else {
+ continue;
+ }
+ } else {
+ lightData.reverseIndexify(index, sourcePos);
+
+ sourceLight = getLight;
+ }
+
+ for (var side : Side.values()) {
+ if (side.id == from) {
+ continue;
+ }
+
+ // check self occlusion for increase
+ if (!LightOp.emitter(sourceLight) && occludeSide(side, lightLevel, sourcePos)) {
+ continue;
+ }
+
+ nodePos.setWithOffset(sourcePos, side.x, side.y, side.z);
+
+ final LightRegionData dataAccess;
+ final LongPriorityQueue increaseQueue;
+ boolean isNeighbor = !lightData.withinExtents(nodePos);
+ LightRegion neighbor = null;
+
+ if (isNeighbor) {
+ neighbor = LightDataManager.INSTANCE.getFromBlock(nodePos);
+
+ if (neighbor == null || neighbor.isClosed()) {
+ continue;
+ }
+
+ increaseQueue = neighbor.globalIncQueue;
+ dataAccess = neighbor.lightData;
+ } else {
+ increaseQueue = incQueue;
+ dataAccess = lightData;
+ }
+
+ final int nodeIndex = dataAccess.indexify(nodePos);
+ final short nodeLight = dataAccess.get(nodeIndex);
+
+ // check neighbor occlusion for increase
+ if (occludeSide(side.opposite, lightLevel, nodePos)) {
+ continue;
+ }
+
+ if (less.lessThanMinusOne(nodeLight, sourceLight).any()) {
+ final short resultLight = LightOp.replaceMinusOne(nodeLight, sourceLight, less);
+ dataAccess.put(nodeIndex, resultLight);
+ Queues.enqueue(increaseQueue, nodeIndex, resultLight, side);
+
+ if (isNeighbor) {
+ neighborIncreaseQueue.enqueue(neighbor.origin);
+ }
+ }
+ }
+ }
+
+ // If we reached here we should draw
+ hasData = true;
+ lightData.markAsDirty();
+ LightDataManager.INSTANCE.publicDrawQueue.add(origin);
+ }
+
+ private void checkEdgeBlock(LightRegion neighbor, BlockPos.MutableBlockPos sourcePos, BlockPos.MutableBlockPos targetPos, Side side, LightLevelAccess lightLevel) {
+ final int sourceIndex = neighbor.lightData.indexify(sourcePos);
+ final short sourceLight = neighbor.lightData.get(sourceIndex);
+
+ if (LightOp.lit(sourceLight)) {
+ // TODO: generalize for all increase process, with check-neighbor flag
+ // check self occlusion for increase
+ if (!LightOp.emitter(sourceLight) && occludeSide(side, lightLevel, sourcePos)) {
+ return;
+ }
+
+ final int targetIndex = lightData.indexify(targetPos);
+ final short targetLight = lightData.get(targetIndex);
+
+ // check neighbor occlusion for increase
+ if (occludeSide(side.opposite, lightLevel, targetPos)) {
+ return;
+ }
+
+ if (less.lessThanMinusOne(targetLight, sourceLight).any()) {
+ final short resultLight = LightOp.replaceMinusOne(targetLight, sourceLight, less);
+ lightData.put(targetIndex, resultLight);
+ Queues.enqueue(incQueue, targetIndex, resultLight, side);
+ }
+ }
+ }
+
+ private void checkEdges(LightLevelAccess lightLevel) {
+ final int size = LightRegionData.Const.WIDTH;
+ final BlockPos.MutableBlockPos searchPos = new BlockPos.MutableBlockPos();
+ final BlockPos.MutableBlockPos targetPos = new BlockPos.MutableBlockPos();
+ final int[] searchOffsets = new int[]{-1, size};
+ final int[] targetOffsets = new int[]{0, size - 1};
+
+ for (int i = 0; i < searchOffsets.length; i++) {
+ final int x = searchOffsets[i];
+ final int xTarget = targetOffsets[i];
+
+ searchPos.setWithOffset(originPos, x, 0, 0);
+ targetPos.setWithOffset(originPos, xTarget, 0, 0);
+ final Side side = Side.infer(searchPos, targetPos);
+ final LightRegion neighbor = LightDataManager.INSTANCE.getFromBlock(searchPos);
+
+ if (neighbor == null) {
+ continue;
+ }
+
+ for (int y = 0; y < size; y++) {
+ for (int z = 0; z < size; z++) {
+ searchPos.setWithOffset(originPos, x, y, z);
+ targetPos.setWithOffset(originPos, xTarget, y, z);
+ checkEdgeBlock(neighbor, searchPos, targetPos, side, lightLevel);
+ }
+ }
+ }
+
+ // TODO: generalize with Axis parameter
+ for (int i = 0; i < searchOffsets.length; i++) {
+ final int y = searchOffsets[i];
+ final int yTarget = targetOffsets[i];
+
+ searchPos.setWithOffset(originPos, 0, y, 0);
+ targetPos.setWithOffset(originPos, 0, yTarget, 0);
+ final Side side = Side.infer(searchPos, targetPos);
+ final LightRegion neighbor = LightDataManager.INSTANCE.getFromBlock(searchPos);
+
+ if (neighbor == null) {
+ continue;
+ }
+
+ for (int z = 0; z < size; z++) {
+ for (int x = 0; x < size; x++) {
+ searchPos.setWithOffset(originPos, x, y, z);
+ targetPos.setWithOffset(originPos, x, yTarget, z);
+ checkEdgeBlock(neighbor, searchPos, targetPos, side, lightLevel);
+ }
+ }
+ }
+
+ for (int i = 0; i < searchOffsets.length; i++) {
+ final int z = searchOffsets[i];
+ final int zTarget = targetOffsets[i];
+
+ searchPos.setWithOffset(originPos, 0, 0, z);
+ targetPos.setWithOffset(originPos, 0, 0, zTarget);
+ final Side side = Side.infer(searchPos, targetPos);
+ final LightRegion neighbor = LightDataManager.INSTANCE.getFromBlock(searchPos);
+
+ if (neighbor == null) {
+ continue;
+ }
+
+ for (int x = 0; x < size; x++) {
+ for (int y = 0; y < size; y++) {
+ searchPos.setWithOffset(originPos, x, y, z);
+ targetPos.setWithOffset(originPos, x, y, zTarget);
+ checkEdgeBlock(neighbor, searchPos, targetPos, side, lightLevel);
+ }
+ }
+ }
+ }
+
+ public void close() {
+ if (!lightData.isClosed()) {
+ lightData.close();
+ }
+ }
+
+ @Override
+ public boolean isClosed() {
+ return lightData == null || lightData.isClosed();
+ }
+}
diff --git a/src/main/java/grondag/canvas/light/color/LightRegionAccess.java b/src/main/java/grondag/canvas/light/color/LightRegionAccess.java
new file mode 100644
index 000000000..98ccfbfb8
--- /dev/null
+++ b/src/main/java/grondag/canvas/light/color/LightRegionAccess.java
@@ -0,0 +1,57 @@
+/*
+ * This file is part of Canvas Renderer and is licensed to the project under
+ * terms that are compatible with the GNU Lesser General Public License.
+ * See the NOTICE file distributed with this work for additional information
+ * regarding copyright ownership and licensing.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+package grondag.canvas.light.color;
+
+import org.jetbrains.annotations.Nullable;
+
+import net.minecraft.core.BlockPos;
+import net.minecraft.world.level.block.state.BlockState;
+
+public interface LightRegionAccess {
+ LightRegionAccess EMPTY = new Empty();
+
+ void checkBlock(BlockPos pos, @Nullable BlockState blockState);
+
+ void submitChecks();
+
+ void markUrgent();
+
+ boolean isClosed();
+
+ class Empty implements LightRegionAccess {
+ @Override
+ public void checkBlock(BlockPos pos, BlockState blockState) {
+ }
+
+ @Override
+ public void submitChecks() {
+ }
+
+ @Override
+ public void markUrgent() {
+ }
+
+ @Override
+ public boolean isClosed() {
+ return true;
+ }
+ }
+}
diff --git a/src/main/java/grondag/canvas/light/color/LightRegionData.java b/src/main/java/grondag/canvas/light/color/LightRegionData.java
new file mode 100644
index 000000000..cc7e8b18f
--- /dev/null
+++ b/src/main/java/grondag/canvas/light/color/LightRegionData.java
@@ -0,0 +1,149 @@
+/*
+ * This file is part of Canvas Renderer and is licensed to the project under
+ * terms that are compatible with the GNU Lesser General Public License.
+ * See the NOTICE file distributed with this work for additional information
+ * regarding copyright ownership and licensing.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+package grondag.canvas.light.color;
+
+import java.nio.ByteBuffer;
+
+import org.lwjgl.system.MemoryUtil;
+
+import net.minecraft.core.BlockPos;
+
+class LightRegionData {
+ public static class Const {
+ public static final int WIDTH = 16;
+ public static final int SIZE3D = WIDTH * WIDTH * WIDTH;
+
+ public static final int WIDTH_SHIFT = (int) (Math.log(WIDTH) / Math.log(2));
+ public static final int WIDTH_MASK = WIDTH - 1;
+ }
+
+ final int regionOriginBlockX;
+ final int regionOriginBlockY;
+ final int regionOriginBlockZ;
+ private ByteBuffer buffer = null;
+ private boolean dirty = true;
+ private boolean closed = false;
+
+ LightRegionData(int regionOriginBlockX, int regionOriginBlockY, int regionOriginBlockZ) {
+ this.regionOriginBlockX = regionOriginBlockX;
+ this.regionOriginBlockY = regionOriginBlockY;
+ this.regionOriginBlockZ = regionOriginBlockZ;
+ }
+
+ private void allocateBuffer() {
+ if (buffer != null) {
+ throw new IllegalStateException("Trying to allocate light region buffer twice!");
+ }
+
+ buffer = MemoryUtil.memAlloc(LightDataTexture.Format.PIXEL_BYTES * Const.SIZE3D);
+
+ // clear manually ?
+ while (buffer.position() < LightDataTexture.Format.PIXEL_BYTES * Const.SIZE3D) {
+ buffer.putShort((short) 0);
+ }
+ }
+
+ public void markAsDirty() {
+ dirty = true;
+ }
+
+ public void clearDirty() {
+ dirty = false;
+ }
+
+ public void close() {
+ if (closed) {
+ return;
+ }
+
+ if (buffer != null) {
+ buffer.position(0);
+ // very important
+ MemoryUtil.memFree(buffer);
+ }
+
+ closed = true;
+ }
+
+ public short get(int index) {
+ return buffer == null ? 0 : buffer.getShort(index);
+ }
+
+ public void put(int index, short light) {
+ if (buffer == null) {
+ allocateBuffer();
+ }
+
+ buffer.putShort(index, light);
+ }
+
+ public int indexify(BlockPos pos) {
+ return indexify(pos.getX(), pos.getY(), pos.getZ());
+ }
+
+ public int indexify(int x, int y, int z) {
+ final int localX = x - regionOriginBlockX;
+ final int localY = y - regionOriginBlockY;
+ final int localZ = z - regionOriginBlockZ;
+
+ // x and z are swapped because opengl
+ return ((localZ << (Const.WIDTH_SHIFT * 2)) | (localY << Const.WIDTH_SHIFT) | localX) * LightDataTexture.Format.PIXEL_BYTES;
+ }
+
+ public void reverseIndexify(int index, BlockPos.MutableBlockPos result) {
+ index = index / LightDataTexture.Format.PIXEL_BYTES;
+
+ // x and z are swapped because opengl
+ result.setX((index & Const.WIDTH_MASK) + regionOriginBlockX);
+ result.setY(((index >> Const.WIDTH_SHIFT) & Const.WIDTH_MASK) + regionOriginBlockY);
+ result.setZ(((index >> Const.WIDTH_SHIFT * 2) & Const.WIDTH_MASK) + regionOriginBlockZ);
+ }
+
+ public boolean withinExtents(BlockPos pos) {
+ return withinExtents(pos.getX(), pos.getY(), pos.getZ());
+ }
+
+ public boolean withinExtents(int x, int y, int z) {
+ return (x >= regionOriginBlockX && x < regionOriginBlockX + Const.WIDTH)
+ && (y >= regionOriginBlockY && y < regionOriginBlockY + Const.WIDTH)
+ && (z >= regionOriginBlockZ && z < regionOriginBlockZ + Const.WIDTH);
+ }
+
+ boolean hasBuffer() {
+ return buffer != null;
+ }
+
+ ByteBuffer getBuffer() {
+ if (closed || buffer == null) {
+ throw new IllegalStateException("Attempting to access a closed or null light region buffer!");
+ }
+
+ return buffer;
+ }
+
+ public boolean isDirty() {
+ return dirty;
+ }
+
+ public boolean isClosed() {
+ return closed;
+ }
+}
diff --git a/src/main/java/grondag/canvas/light/color/LightRegistry.java b/src/main/java/grondag/canvas/light/color/LightRegistry.java
new file mode 100644
index 000000000..c9c42fcba
--- /dev/null
+++ b/src/main/java/grondag/canvas/light/color/LightRegistry.java
@@ -0,0 +1,156 @@
+/*
+ * This file is part of Canvas Renderer and is licensed to the project under
+ * terms that are compatible with the GNU Lesser General Public License.
+ * See the NOTICE file distributed with this work for additional information
+ * regarding copyright ownership and licensing.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+package grondag.canvas.light.color;
+
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.jetbrains.annotations.Nullable;
+
+import net.minecraft.core.BlockPos;
+import net.minecraft.server.packs.resources.ResourceManager;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.level.BlockGetter;
+import net.minecraft.world.level.block.Blocks;
+import net.minecraft.world.level.block.entity.BlockEntity;
+import net.minecraft.world.level.block.state.BlockState;
+import net.minecraft.world.level.material.FluidState;
+import net.minecraft.world.level.material.Fluids;
+
+import io.vram.frex.api.light.ItemLight;
+
+import grondag.canvas.light.api.impl.BlockLightLoader;
+import grondag.canvas.light.api.impl.FloodFillBlockLight;
+
+public class LightRegistry {
+ private static final ConcurrentHashMap cachedLights = new ConcurrentHashMap<>();
+ // Only for full block checks. Real MC world is only needed for blocks with positional offset like flowers, etc.
+ private static final DummyWorld DUMMY_WORLD = new DummyWorld();
+
+ public static void reload(ResourceManager manager) {
+ cachedLights.clear();
+ BlockLightLoader.reload(manager);
+ }
+
+ public static short get(BlockState blockState) {
+ // maybe just populate it during reload? don't want to slow down resource reload though
+ return cachedLights.computeIfAbsent(blockState, LightRegistry::generate);
+ }
+
+ private static short generate(BlockState blockState) {
+ final boolean isFullCube = blockState.isCollisionShapeFullBlock(DUMMY_WORLD.set(blockState), DummyWorld.origin);
+ final int lightLevel = blockState.getLightEmission();
+ final short defaultLight = LightOp.encodeLight(lightLevel, lightLevel, lightLevel, isFullCube, lightLevel > 0, blockState.canOcclude());
+
+ FloodFillBlockLight apiLight = BlockLightLoader.BLOCK_LIGHTS.get(blockState);
+
+ if (apiLight == null && !blockState.getFluidState().isEmpty()) {
+ apiLight = BlockLightLoader.FLUID_LIGHTS.get(blockState.getFluidState());
+ }
+
+ if (apiLight != null) {
+ if (!apiLight.levelIsSet) {
+ apiLight = apiLight.withLevel(blockState.getLightEmission());
+ }
+
+ return LightOp.encodeLight(apiLight.value, isFullCube, apiLight.value != 0, blockState.canOcclude());
+ }
+
+ if (lightLevel < 1) {
+ return defaultLight;
+ }
+
+ // Item Light color-only fallback (feature?)
+ final ItemStack stack = new ItemStack(blockState.getBlock(), 1);
+ final ItemLight itemLight = ItemLight.get(stack);
+
+ if (itemLight == ItemLight.NONE) {
+ return defaultLight;
+ }
+
+ float maxValue = Math.max(itemLight.red(), Math.max(itemLight.green(), itemLight.blue()));
+
+ if (maxValue <= 0 || itemLight.intensity() <= 0) {
+ return defaultLight;
+ }
+
+ return encodeItem(itemLight, lightLevel, isFullCube, blockState.canOcclude());
+ }
+
+ public static short encodeItem(ItemLight light) {
+ return encodeItem(light, -1, false, false);
+ }
+
+ public static short encodeItem(ItemLight light, int lightLevel, boolean isFull, boolean isOccluding) {
+ if (light == null || light == ItemLight.NONE) {
+ return 0;
+ }
+
+ if (lightLevel < 0) {
+ lightLevel = 15;
+ }
+
+ final float postLevel = (float) Math.min(15, lightLevel) * org.joml.Math.clamp(0.0f, 1.0f, light.intensity());
+
+ final int r = org.joml.Math.clamp(0, 15, Math.round(postLevel * light.red()));
+ final int g = org.joml.Math.clamp(0, 15, Math.round(postLevel * light.green()));
+ final int b = org.joml.Math.clamp(0, 15, Math.round(postLevel * light.blue()));
+
+ var result = LightOp.encode(r, g, b, 0);
+
+ return LightOp.encodeLight(result, isFull, LightOp.lit(result), isOccluding);
+ }
+
+ private static class DummyWorld implements BlockGetter {
+ private static final BlockPos origin = new BlockPos(0, 0, 0);
+ private static BlockState state;
+
+ private DummyWorld set(BlockState state) {
+ DummyWorld.state = state;
+ return this;
+ }
+
+ @Nullable
+ @Override
+ public BlockEntity getBlockEntity(BlockPos blockPos) {
+ return null;
+ }
+
+ @Override
+ public BlockState getBlockState(BlockPos blockPos) {
+ return blockPos.equals(origin) ? state : Blocks.AIR.defaultBlockState();
+ }
+
+ @Override
+ public FluidState getFluidState(BlockPos blockPos) {
+ return blockPos.equals(origin) ? state.getFluidState() : Fluids.EMPTY.defaultFluidState();
+ }
+
+ @Override
+ public int getHeight() {
+ return 1;
+ }
+
+ @Override
+ public int getMinBuildHeight() {
+ return 0;
+ }
+ }
+}
diff --git a/src/main/java/grondag/canvas/material/state/RenderState.java b/src/main/java/grondag/canvas/material/state/RenderState.java
index b5346fdb1..7ce0872aa 100644
--- a/src/main/java/grondag/canvas/material/state/RenderState.java
+++ b/src/main/java/grondag/canvas/material/state/RenderState.java
@@ -44,6 +44,8 @@
import io.vram.frex.api.texture.MaterialTexture;
import grondag.canvas.config.Configurator;
+import grondag.canvas.light.color.LightDataManager;
+import grondag.canvas.light.color.LightDataTexture;
import grondag.canvas.material.property.BinaryRenderState;
import grondag.canvas.material.property.DecalRenderState;
import grondag.canvas.material.property.DepthTestRenderState;
@@ -214,23 +216,25 @@ private void enableMaterial(int x, int y, int z) {
if (Pipeline.shadowMapDepth != -1) {
CanvasTextureState.ensureTextureOfTextureUnit(TextureData.SHADOWMAP, GFX.GL_TEXTURE_2D_ARRAY, Pipeline.shadowMapDepth);
CanvasTextureState.ensureTextureOfTextureUnit(TextureData.SHADOWMAP_TEXTURE, GFX.GL_TEXTURE_2D_ARRAY, Pipeline.shadowMapDepth);
+ }
- // Set this back so nothing inadvertently tries to do stuff with array texture/shadowmap.
- // Was seeing stray invalid operations errors in GL without.
- CanvasTextureState.activeTextureUnit(TextureData.MC_SPRITE_ATLAS);
+ if (Pipeline.coloredLightsEnabled()) {
+ CanvasTextureState.ensureTextureOfTextureUnit(TextureData.COLORED_LIGHTS_DATA, LightDataTexture.Format.TARGET, LightDataManager.texId());
}
if (Pipeline.config().materialProgram.samplerNames.length > 0) {
// Activate non-frex material program textures
for (int i = 0; i < Pipeline.config().materialProgram.samplerNames.length; i++) {
final int bindTarget = Pipeline.materialTextures().texTargets[i];
- final int bind = Pipeline.materialTextures().texIds[i];
+ final int bind = Pipeline.materialTextures().texIds[i].getAsInt();
CanvasTextureState.ensureTextureOfTextureUnit(TextureData.PROGRAM_SAMPLERS + i, bindTarget, bind);
}
-
- CanvasTextureState.activeTextureUnit(TextureData.MC_SPRITE_ATLAS);
}
+ // Set this back so nothing inadvertently tries to do stuff with array texture/shadowmap.
+ // Was seeing stray invalid operations errors in GL without.
+ CanvasTextureState.activeTextureUnit(TextureData.MC_SPRITE_ATLAS);
+
texture.enable(blur);
transparency.enable();
depthTest.enable();
diff --git a/src/main/java/grondag/canvas/mixin/MixinClientLevel.java b/src/main/java/grondag/canvas/mixin/MixinClientLevel.java
new file mode 100644
index 000000000..67011d2b9
--- /dev/null
+++ b/src/main/java/grondag/canvas/mixin/MixinClientLevel.java
@@ -0,0 +1,44 @@
+/*
+ * This file is part of Canvas Renderer and is licensed to the project under
+ * terms that are compatible with the GNU Lesser General Public License.
+ * See the NOTICE file distributed with this work for additional information
+ * regarding copyright ownership and licensing.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+package grondag.canvas.mixin;
+
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+import net.minecraft.client.multiplayer.ClientLevel;
+import net.minecraft.world.entity.Entity;
+
+import grondag.canvas.light.color.EntityLightTracker;
+
+@Mixin(ClientLevel.class)
+public class MixinClientLevel {
+ @Inject(method = "addEntity", at = @At("TAIL"))
+ void onAddEntity(int i, Entity entity, CallbackInfo ci) {
+ EntityLightTracker.levelAddsEntity(entity);
+ }
+
+ @Inject(method = "removeEntity", at = @At("HEAD"))
+ void onRemoveEntity(int id, Entity.RemovalReason removalReason, CallbackInfo ci) {
+ EntityLightTracker.levelRemovesEntity(id);
+ }
+}
diff --git a/src/main/java/grondag/canvas/mixin/MixinDebugScreenOverlay.java b/src/main/java/grondag/canvas/mixin/MixinDebugScreenOverlay.java
index 07ca582bf..61359d764 100644
--- a/src/main/java/grondag/canvas/mixin/MixinDebugScreenOverlay.java
+++ b/src/main/java/grondag/canvas/mixin/MixinDebugScreenOverlay.java
@@ -50,6 +50,7 @@
import grondag.canvas.buffer.render.TransferBuffers;
import grondag.canvas.buffer.util.DirectBufferAllocator;
import grondag.canvas.buffer.util.GlBufferAllocator;
+import grondag.canvas.light.color.LightDataManager;
import grondag.canvas.render.terrain.cluster.SlabAllocator;
import grondag.canvas.render.world.CanvasWorldRenderer;
import grondag.canvas.terrain.util.TerrainExecutor;
@@ -218,6 +219,8 @@ private ArrayList onGetSystemInformation(Object[] elements) {
result.add(worldRenderState.drawlistDebugSummary());
result.add(SlabAllocator.debugSummary());
+ result.add(LightDataManager.debugString());
+
return result;
}
}
diff --git a/src/main/java/grondag/canvas/pipeline/Pipeline.java b/src/main/java/grondag/canvas/pipeline/Pipeline.java
index bb483a748..ca9ad7632 100644
--- a/src/main/java/grondag/canvas/pipeline/Pipeline.java
+++ b/src/main/java/grondag/canvas/pipeline/Pipeline.java
@@ -97,11 +97,16 @@ public class Pipeline {
private static PipelineConfig config;
private static boolean advancedTerrainCulling;
+ private static boolean coloredLightsEnabled;
public static boolean shadowsEnabled() {
return skyShadowFbo != null;
}
+ public static boolean coloredLightsEnabled() {
+ return coloredLightsEnabled;
+ }
+
public static boolean advancedTerrainCulling() {
return advancedTerrainCulling;
}
@@ -207,6 +212,12 @@ static void activate(PrimaryFrameBuffer primary, int width, int height) {
defaultZenithAngle = 0f;
}
+ if (config.coloredLights != null && Configurator.coloredLights) {
+ coloredLightsEnabled = config.coloredLights.enabled;
+ } else {
+ coloredLightsEnabled = false;
+ }
+
if (isFabulous) {
final FabulousConfig fc = config.fabulosity;
diff --git a/src/main/java/grondag/canvas/pipeline/ProgramTextureData.java b/src/main/java/grondag/canvas/pipeline/ProgramTextureData.java
index 7492bcf73..2be3c1ada 100644
--- a/src/main/java/grondag/canvas/pipeline/ProgramTextureData.java
+++ b/src/main/java/grondag/canvas/pipeline/ProgramTextureData.java
@@ -22,6 +22,8 @@
import static net.minecraft.client.renderer.entity.ItemRenderer.ENCHANTED_GLINT_ITEM;
+import java.util.function.IntSupplier;
+
import org.lwjgl.opengl.GL46;
import net.minecraft.client.Minecraft;
@@ -29,34 +31,40 @@
import net.minecraft.client.renderer.texture.TextureManager;
import net.minecraft.resources.ResourceLocation;
+import grondag.canvas.light.color.LightDataManager;
import grondag.canvas.pipeline.config.ImageConfig;
import grondag.canvas.pipeline.config.util.NamedDependency;
public class ProgramTextureData {
- public final int[] texIds;
+ public final IntSupplier[] texIds;
public final int[] texTargets;
public ProgramTextureData(NamedDependency[] samplerImages) {
- texIds = new int[samplerImages.length];
+ texIds = new IntSupplier[samplerImages.length];
texTargets = new int[samplerImages.length];
for (int i = 0; i < samplerImages.length; ++i) {
final String imageName = samplerImages[i].name;
- int imageBind = 0;
+ IntSupplier imageBind = () -> 0;
int bindTarget = GL46.GL_TEXTURE_2D;
- if (imageName.contains(":")) {
+ // TODO: use a registry if there is more of these
+ if (imageName.equals("frex:textures/auto/colored_lights")) {
+ imageBind = LightDataManager::texId;
+ } else if (imageName.contains(":")) {
final AbstractTexture tex = tryLoadResourceTexture(new ResourceLocation(imageName));
if (tex != null) {
- imageBind = tex.getId();
+ final int id = tex.getId();
+ imageBind = () -> id;
}
} else {
final Image img = Pipeline.getImage(imageName);
if (img != null) {
- imageBind = img.glId();
+ final int id = img.glId();
+ imageBind = () -> id;
bindTarget = img.config.target;
}
}
diff --git a/src/main/java/grondag/canvas/pipeline/config/ColoredLightsConfig.java b/src/main/java/grondag/canvas/pipeline/config/ColoredLightsConfig.java
new file mode 100644
index 000000000..b39288fa8
--- /dev/null
+++ b/src/main/java/grondag/canvas/pipeline/config/ColoredLightsConfig.java
@@ -0,0 +1,58 @@
+/*
+ * This file is part of Canvas Renderer and is licensed to the project under
+ * terms that are compatible with the GNU Lesser General Public License.
+ * See the NOTICE file distributed with this work for additional information
+ * regarding copyright ownership and licensing.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+package grondag.canvas.pipeline.config;
+
+import blue.endless.jankson.JsonObject;
+
+import grondag.canvas.pipeline.config.util.AbstractConfig;
+import grondag.canvas.pipeline.config.util.ConfigContext;
+
+public class ColoredLightsConfig extends AbstractConfig {
+ public final boolean enabled;
+ public final boolean allowVirtual;
+ public final boolean useOcclusionData;
+
+ protected ColoredLightsConfig(ConfigContext ctx, JsonObject config) {
+ super(ctx);
+ enabled = ctx.dynamic.getBoolean(config, "enabled", true);
+ allowVirtual = ctx.dynamic.getBoolean(config, "allowVirtual", true);
+ useOcclusionData = ctx.dynamic.getBoolean(config, "useOcclusionData", false);
+ }
+
+ @Override
+ public boolean validate() {
+ return true;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+
+ final ColoredLightsConfig that = (ColoredLightsConfig) obj;
+ return enabled == that.enabled && allowVirtual == that.allowVirtual && useOcclusionData == that.useOcclusionData;
+ }
+}
diff --git a/src/main/java/grondag/canvas/pipeline/config/PipelineConfig.java b/src/main/java/grondag/canvas/pipeline/config/PipelineConfig.java
index 215ec85fa..ccfab2e1c 100644
--- a/src/main/java/grondag/canvas/pipeline/config/PipelineConfig.java
+++ b/src/main/java/grondag/canvas/pipeline/config/PipelineConfig.java
@@ -54,6 +54,7 @@ public class PipelineConfig {
@Nullable public final DrawTargetsConfig drawTargets;
@Nullable public final SkyShadowConfig skyShadow;
@Nullable public final SkyConfig sky;
+ @Nullable public final ColoredLightsConfig coloredLights;
public final NamedDependency defaultFramebuffer;
@@ -83,6 +84,7 @@ private PipelineConfig() {
fabulosity = null;
skyShadow = null;
sky = null;
+ coloredLights = null;
drawTargets = DrawTargetsConfig.makeDefault(context);
defaultFramebuffer = context.frameBuffers.dependOn("default");
materialProgram = new MaterialProgramConfig(context);
@@ -105,6 +107,7 @@ private PipelineConfig() {
drawTargets = builder.drawTargets;
skyShadow = builder.skyShadow;
sky = builder.sky;
+ coloredLights = builder.coloredLights;
for (final OptionConfig opt : builder.options) {
optionMap.put(opt.includeToken, opt);
diff --git a/src/main/java/grondag/canvas/pipeline/config/PipelineConfigBuilder.java b/src/main/java/grondag/canvas/pipeline/config/PipelineConfigBuilder.java
index cae59bd40..d364594bd 100644
--- a/src/main/java/grondag/canvas/pipeline/config/PipelineConfigBuilder.java
+++ b/src/main/java/grondag/canvas/pipeline/config/PipelineConfigBuilder.java
@@ -63,6 +63,7 @@ public class PipelineConfigBuilder {
@Nullable public DrawTargetsConfig drawTargets;
@Nullable public SkyShadowConfig skyShadow;
@Nullable public SkyConfig sky;
+ @Nullable public ColoredLightsConfig coloredLights;
public boolean smoothBrightnessBidirectionaly = false;
public int brightnessSmoothingFrames = 20;
@@ -78,8 +79,7 @@ public class PipelineConfigBuilder {
/**
* Priority-pass loading. Loads options before anything else. This is necessary for the
- * current design of {@link grondag.canvas.pipeline.config.util.DynamicLoader}, at the cost
- * of reading the disk twice.
+ * current design of {@link grondag.canvas.pipeline.config.util.DynamicLoader}.
*
* @param configJson the json file being read
*/
@@ -140,6 +140,14 @@ public void load(JsonObject configJson) {
}
}
+ if (configJson.containsKey("coloredLights")) {
+ if (coloredLights == null) {
+ coloredLights = LoadHelper.loadObject(context, configJson, "coloredLights", ColoredLightsConfig::new);
+ } else {
+ CanvasMod.LOG.warn("Invalid pipeline config - duplicate 'coloredLights' ignored.");
+ }
+ }
+
if (configJson.containsKey("sky")) {
if (sky == null) {
sky = LoadHelper.loadObject(context, configJson, "sky", SkyConfig::new);
@@ -184,6 +192,7 @@ public boolean validate() {
valid &= (fabulosity == null || fabulosity.validate());
valid &= (skyShadow == null || skyShadow.validate());
+ valid &= (coloredLights == null || coloredLights.validate());
valid &= defaultFramebuffer != null && defaultFramebuffer.validate("Invalid pipeline config - missing or invalid defaultFramebuffer.");
@@ -213,19 +222,13 @@ public boolean validate() {
return null;
}
- final PipelineConfigBuilder result = new PipelineConfigBuilder();
- final ObjectOpenHashSet included = new ObjectOpenHashSet<>();
- final ObjectArrayFIFOQueue readQueue = new ObjectArrayFIFOQueue<>();
- final ObjectArrayFIFOQueue primaryLoadQueue = new ObjectArrayFIFOQueue<>();
- final ObjectArrayFIFOQueue secondLoadQueue = new ObjectArrayFIFOQueue<>();
+ final var result = new PipelineConfigBuilder();
+ final var included = new ObjectOpenHashSet();
+ final var readQueue = new ObjectArrayFIFOQueue();
+ final var primaryLoadQueue = new ObjectArrayFIFOQueue();
+ final var secondLoadQueue = new ObjectArrayFIFOQueue();
- readQueue.enqueue(id);
- included.add(id);
-
- while (!readQueue.isEmpty()) {
- final ResourceLocation target = readQueue.dequeue();
- readResource(target, readQueue, primaryLoadQueue, included, rm);
- }
+ loadResources(id, readQueue, primaryLoadQueue, included, rm);
while (!primaryLoadQueue.isEmpty()) {
final JsonObject target = primaryLoadQueue.dequeue();
@@ -248,6 +251,20 @@ public boolean validate() {
}
}
+ /**
+ * This function is also used in {@link PipelineDescription} to parse the entire pipeline.
+ * Notably, this and subsequently called functions shouldn't contain any building code.
+ */
+ static void loadResources(ResourceLocation id, ObjectArrayFIFOQueue queue, ObjectArrayFIFOQueue loadQueue, ObjectOpenHashSet included, ResourceManager rm) {
+ queue.enqueue(id);
+ included.add(id);
+
+ while (!queue.isEmpty()) {
+ final ResourceLocation target = queue.dequeue();
+ readResource(target, queue, loadQueue, included, rm);
+ }
+ }
+
private static void readResource(ResourceLocation target, ObjectArrayFIFOQueue queue, ObjectArrayFIFOQueue loadQueue, ObjectOpenHashSet included, ResourceManager rm) {
// Allow flexibility on JSON vs JSON5 extensions
if (rm.getResource(target).isEmpty()) {
diff --git a/src/main/java/grondag/canvas/pipeline/config/PipelineDescription.java b/src/main/java/grondag/canvas/pipeline/config/PipelineDescription.java
index 31fae8f5d..fd643d9c1 100644
--- a/src/main/java/grondag/canvas/pipeline/config/PipelineDescription.java
+++ b/src/main/java/grondag/canvas/pipeline/config/PipelineDescription.java
@@ -21,9 +21,12 @@
package grondag.canvas.pipeline.config;
import blue.endless.jankson.JsonObject;
+import it.unimi.dsi.fastutil.objects.ObjectArrayFIFOQueue;
+import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
import net.minecraft.client.resources.language.I18n;
import net.minecraft.resources.ResourceLocation;
+import net.minecraft.server.packs.resources.ResourceManager;
import grondag.canvas.pipeline.config.util.JanksonHelper;
@@ -32,16 +35,49 @@ public class PipelineDescription {
public final String nameKey;
public final String descriptionKey;
public final boolean isFabulous;
+ public final boolean shadowsEnabled;
+ public final boolean coloredLightsEnabled;
- public PipelineDescription (ResourceLocation id, JsonObject config) {
- this.id = id;
+ public static PipelineDescription create(ResourceLocation id, ResourceManager rm) {
+ final var included = new ObjectOpenHashSet();
+ final var reading = new ObjectArrayFIFOQueue();
+ final var objects = new ObjectArrayFIFOQueue();
+
+ PipelineConfigBuilder.loadResources(id, reading, objects, included, rm);
+
+ if (objects.isEmpty()) {
+ return null;
+ }
+
+ var config = objects.dequeue();
+
+ final String getNameKey = JanksonHelper.asString(config.get("nameKey"));
+ final String nameKey = getNameKey == null ? id.toString() : getNameKey;
- final String nameKey = JanksonHelper.asString(config.get("nameKey"));
- this.nameKey = nameKey == null ? id.toString() : nameKey;
- isFabulous = config.containsKey("fabulousTargets");
+ final String getDescriptionKey = JanksonHelper.asString(config.get("descriptionKey"));
+ final String descriptionKey = getDescriptionKey == null ? "pipeline.no_desc" : getDescriptionKey;
- final String descriptionKey = JanksonHelper.asString(config.get("descriptionKey"));
- this.descriptionKey = descriptionKey == null ? "pipeline.no_desc" : descriptionKey;
+ boolean fabulous = config.containsKey("fabulousTargets");
+ boolean shadows = config.containsKey("skyShadows");
+ boolean coloredLights = config.containsKey("coloredLights");
+
+ while (!objects.isEmpty()) {
+ var obj = objects.dequeue();
+ fabulous |= obj.containsKey("fabulousTargets");
+ shadows |= obj.containsKey("skyShadows");
+ coloredLights |= obj.containsKey("coloredLights");
+ }
+
+ return new PipelineDescription(id, nameKey, descriptionKey, fabulous, shadows, coloredLights);
+ }
+
+ public PipelineDescription(ResourceLocation id, String nameKey, String descriptionKey, boolean isFabulous, boolean shadowsEnabled, boolean coloredLightsEnabled) {
+ this.id = id;
+ this.nameKey = nameKey;
+ this.descriptionKey = descriptionKey;
+ this.isFabulous = isFabulous;
+ this.shadowsEnabled = shadowsEnabled;
+ this.coloredLightsEnabled = coloredLightsEnabled;
}
public String name() {
diff --git a/src/main/java/grondag/canvas/pipeline/config/PipelineLoader.java b/src/main/java/grondag/canvas/pipeline/config/PipelineLoader.java
index b3190ebfc..86f8dcb7f 100644
--- a/src/main/java/grondag/canvas/pipeline/config/PipelineLoader.java
+++ b/src/main/java/grondag/canvas/pipeline/config/PipelineLoader.java
@@ -20,17 +20,11 @@
package grondag.canvas.pipeline.config;
-import java.io.InputStream;
-import java.util.function.Function;
-
-import blue.endless.jankson.JsonObject;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
-import net.minecraft.network.chat.Component;
import net.minecraft.server.packs.resources.ResourceManager;
import grondag.canvas.CanvasMod;
-import grondag.canvas.config.ConfigManager;
public class PipelineLoader {
private static boolean hasLoadedOnce = false;
@@ -49,12 +43,12 @@ public static void reload(ResourceManager manager) {
final String stringx = location.toString();
return stringx.endsWith(".json") || stringx.endsWith(".json5");
}).forEach((id, resource) -> {
- try (InputStream inputStream = resource.open()) {
- final JsonObject configJson = ConfigManager.JANKSON.load(inputStream);
- final PipelineDescription p = new PipelineDescription(id, configJson);
+ final PipelineDescription p = PipelineDescription.create(id, manager);
+
+ if (p == null) {
+ CanvasMod.LOG.warn(String.format("Unable to load pipeline configuration %s", id));
+ } else {
MAP.put(id.toString(), p);
- } catch (final Exception e) {
- CanvasMod.LOG.warn(String.format("Unable to load pipeline configuration %s due to unhandled exception.", id), e);
}
});
}
@@ -72,6 +66,4 @@ public static PipelineDescription get(String idString) {
public static PipelineDescription[] array() {
return MAP.values().toArray(new PipelineDescription[MAP.size()]);
}
-
- public static final Function NAME_TEXT_FUNCTION = s -> Component.translatable(get(s).nameKey);
}
diff --git a/src/main/java/grondag/canvas/pipeline/pass/ProgramPass.java b/src/main/java/grondag/canvas/pipeline/pass/ProgramPass.java
index 6ba7a8b53..a7ed90cd4 100644
--- a/src/main/java/grondag/canvas/pipeline/pass/ProgramPass.java
+++ b/src/main/java/grondag/canvas/pipeline/pass/ProgramPass.java
@@ -65,7 +65,7 @@ public void run(int width, int height) {
final int slimit = textures.texIds.length;
for (int i = 0; i < slimit; ++i) {
- CanvasTextureState.ensureTextureOfTextureUnit(GFX.GL_TEXTURE0 + i, textures.texTargets[i], textures.texIds[i]);
+ CanvasTextureState.ensureTextureOfTextureUnit(GFX.GL_TEXTURE0 + i, textures.texTargets[i], textures.texIds[i].getAsInt());
}
program.activate();
diff --git a/src/main/java/grondag/canvas/render/world/CanvasWorldRenderer.java b/src/main/java/grondag/canvas/render/world/CanvasWorldRenderer.java
index fa9fc8dd6..a4aad2813 100644
--- a/src/main/java/grondag/canvas/render/world/CanvasWorldRenderer.java
+++ b/src/main/java/grondag/canvas/render/world/CanvasWorldRenderer.java
@@ -99,6 +99,7 @@
import grondag.canvas.compat.PlayerAnimatorHolder;
import grondag.canvas.config.Configurator;
import grondag.canvas.config.FlawlessFramesController;
+import grondag.canvas.light.color.LightDataManager;
import grondag.canvas.material.property.TargetRenderState;
import grondag.canvas.material.state.RenderContextState;
import grondag.canvas.material.state.RenderState;
@@ -376,6 +377,8 @@ public void renderWorld(PoseStack viewMatrixStack, float tickDelta, long frameSt
Lighting.setupLevel(MatrixData.viewMatrix);
}
+ LightDataManager.update(world, System.nanoTime() + 2000000, () -> WorldRenderDraws.profileSwap(profiler, ProfilerGroup.StartWorld, "colored_lights"));
+
WorldRenderDraws.profileSwap(profiler, ProfilerGroup.StartWorld, "before_entities_event");
// Because we are passing identity stack to entity renders we need to
@@ -848,7 +851,7 @@ public void renderLevel(PoseStack viewMatrixStack, float tickDelta, long frameSt
BufferSynchronizer.checkPoint();
DirectBufferAllocator.update();
TransferBuffers.update();
- CanvasState.recompileIfNeeded(false);
+ CanvasState.handleRecompileKeybind();
FlawlessFramesController.handleToggle();
if (wasFabulous != Pipeline.isFabulous()) {
diff --git a/src/main/java/grondag/canvas/shader/GlShader.java b/src/main/java/grondag/canvas/shader/GlShader.java
index e64d38775..4b30b66f4 100644
--- a/src/main/java/grondag/canvas/shader/GlShader.java
+++ b/src/main/java/grondag/canvas/shader/GlShader.java
@@ -199,14 +199,14 @@ private void outputDebugSource(String source, String error) {
File shaderDir = path.toFile();
if (shaderDir.mkdir()) {
- CanvasMod.LOG.info("Created shader debug output folder" + shaderDir.toString());
+ CanvasMod.LOG.info("Created shader debug output folder " + shaderDir.toString());
}
if (error != null) {
shaderDir = path.resolve("failed").toFile();
if (shaderDir.mkdir()) {
- CanvasMod.LOG.info("Created shader debug output failure folder" + shaderDir.toString());
+ CanvasMod.LOG.info("Created shader debug output failure folder " + shaderDir.toString());
}
source += "\n\n///////// ERROR ////////\n" + error + "\n////////////////////////\n";
@@ -269,6 +269,18 @@ private String getSource() {
result = StringUtils.replace(result, "#define SHADOW_MAP_SIZE 1024", "//#define SHADOW_MAP_SIZE 1024");
}
+ if (Pipeline.coloredLightsEnabled()) {
+ result = StringUtils.replace(result, "//#define COLORED_LIGHTS_ENABLED", "#define COLORED_LIGHTS_ENABLED");
+
+ if (Pipeline.config().coloredLights.useOcclusionData) {
+ result = StringUtils.replace(result, "//#define LIGHT_DATA_HAS_OCCLUSION", "#define LIGHT_DATA_HAS_OCCLUSION");
+ }
+ }
+
+ if (Configurator.debugShaderFlag) {
+ result = StringUtils.replace(result, "//#define _CV_DEBUG", "#define _CV_DEBUG");
+ }
+
result = StringUtils.replace(result, "#define _CV_MAX_SHADER_COUNT 0", "#define _CV_MAX_SHADER_COUNT " + MaterialConstants.MAX_SHADERS);
// prepend GLSL version
diff --git a/src/main/java/grondag/canvas/shader/data/AccessibilityData.java b/src/main/java/grondag/canvas/shader/data/AccessibilityData.java
index 87c592916..f95285cce 100644
--- a/src/main/java/grondag/canvas/shader/data/AccessibilityData.java
+++ b/src/main/java/grondag/canvas/shader/data/AccessibilityData.java
@@ -41,7 +41,7 @@ public class AccessibilityData {
public static void onCloseOptionScreen() {
if (AccessibilityData.checkChanged() && Minecraft.getInstance().level != null) {
- CanvasState.recompileIfNeeded(true);
+ CanvasState.recompile();
}
}
diff --git a/src/main/java/grondag/canvas/shader/data/IntData.java b/src/main/java/grondag/canvas/shader/data/IntData.java
index 0af71e204..aec75dc19 100644
--- a/src/main/java/grondag/canvas/shader/data/IntData.java
+++ b/src/main/java/grondag/canvas/shader/data/IntData.java
@@ -105,7 +105,9 @@ private IntData() { }
static final BitPacker32.BooleanElement FLAG_BAD_OMEN = PLAYER_FLAGS.createBooleanElement();
static final BitPacker32.BooleanElement FLAG_HERO_OF_THE_VILLAGE = PLAYER_FLAGS.createBooleanElement();
- public static final int UINT_COUNT = 1;
+ public static final int UINT_COUNT = 3;
public static final int RENDER_FRAMES = 0;
+ public static final int LIGHT_POINTER_EXTENT = 1;
+ public static final int LIGHT_DATA_FIRST_ROW = 2;
public static final IntBuffer UINT_DATA = BufferUtils.createIntBuffer(UINT_COUNT);
}
diff --git a/src/main/java/grondag/canvas/shader/data/ShaderUniforms.java b/src/main/java/grondag/canvas/shader/data/ShaderUniforms.java
index 2fabc6c55..06373211e 100644
--- a/src/main/java/grondag/canvas/shader/data/ShaderUniforms.java
+++ b/src/main/java/grondag/canvas/shader/data/ShaderUniforms.java
@@ -35,6 +35,7 @@ public class ShaderUniforms {
program.uniformSampler("frxs_shadowMap", UniformRefreshFrequency.ON_LOAD, u -> u.set(TextureData.SHADOWMAP - GL21.GL_TEXTURE0));
program.uniformSampler("frxs_shadowMapTexture", UniformRefreshFrequency.ON_LOAD, u -> u.set(TextureData.SHADOWMAP_TEXTURE - GL21.GL_TEXTURE0));
+ program.uniformSampler("frxs_lightData", UniformRefreshFrequency.ON_LOAD, u -> u.set(TextureData.COLORED_LIGHTS_DATA - GL21.GL_TEXTURE0));
//program.uniformSampler2d("frxs_dither", UniformRefreshFrequency.ON_LOAD, u -> u.set(TextureData.DITHER - GL21.GL_TEXTURE0));
diff --git a/src/main/java/grondag/canvas/terrain/region/RenderRegion.java b/src/main/java/grondag/canvas/terrain/region/RenderRegion.java
index 1a0aad732..d73074d70 100644
--- a/src/main/java/grondag/canvas/terrain/region/RenderRegion.java
+++ b/src/main/java/grondag/canvas/terrain/region/RenderRegion.java
@@ -51,6 +51,8 @@
import grondag.canvas.apiimpl.rendercontext.CanvasTerrainRenderContext;
import grondag.canvas.buffer.input.DrawableVertexCollector;
import grondag.canvas.buffer.input.VertexCollectorList;
+import grondag.canvas.light.color.LightDataManager;
+import grondag.canvas.light.color.LightRegionAccess;
import grondag.canvas.material.state.TerrainRenderStates;
import grondag.canvas.perf.ChunkRebuildCounters;
import grondag.canvas.pipeline.Pipeline;
@@ -79,6 +81,7 @@ public class RenderRegion implements TerrainExecutorTask {
public final CameraRegionVisibility cameraVisibility;
public final ShadowRegionVisibility shadowVisibility;
public final NeighborRegions neighbors;
+ private final LightRegionAccess lightRegion;
private RegionRenderSector renderSector = null;
@@ -126,6 +129,7 @@ public RenderRegion(RenderChunk chunk, long packedPos) {
cameraVisibility = worldRenderState.terrainIterator.cameraVisibility.createRegionState(this);
shadowVisibility = worldRenderState.terrainIterator.shadowVisibility.createRegionState(this);
origin.update();
+ lightRegion = LightDataManager.allocate(origin);
}
private static void addBlockEntity(List chunkEntities, Set globalEntities, E blockEntity) {
@@ -166,6 +170,10 @@ void close() {
if (renderSector != null) {
renderSector = renderSector.release(origin);
}
+
+ if (!lightRegion.isClosed()) {
+ LightDataManager.free(origin);
+ }
}
}
@@ -421,13 +429,14 @@ private void buildTerrain(CanvasTerrainRenderContext context, RegionBuildState b
final RegionOcclusionCalculator occlusionRegion = region.occlusion;
for (int i = 0; i < RenderRegionStateIndexer.INTERIOR_STATE_COUNT; i++) {
+ final BlockState blockState = region.getLocalBlockState(i);
+ final int x = i & 0xF;
+ final int y = (i >> 4) & 0xF;
+ final int z = (i >> 8) & 0xF;
+ searchPos.set(xOrigin + x, yOrigin + y, zOrigin + z);
+
if (occlusionRegion.shouldRender(i)) {
- final BlockState blockState = region.getLocalBlockState(i);
final FluidState fluidState = blockState.getFluidState();
- final int x = i & 0xF;
- final int y = (i >> 4) & 0xF;
- final int z = (i >> 8) & 0xF;
- searchPos.set(xOrigin + x, yOrigin + y, zOrigin + z);
final boolean hasFluid = !fluidState.isEmpty();
// Vanilla only checks not invisible, but filters non-model shape down the line
@@ -456,6 +465,14 @@ private void buildTerrain(CanvasTerrainRenderContext context, RegionBuildState b
}
}
}
+
+ if (!lightRegion.isClosed()) {
+ lightRegion.checkBlock(searchPos, blockState);
+ }
+ }
+
+ if (!lightRegion.isClosed()) {
+ lightRegion.submitChecks();
}
buildState.prepareTranslucentIfNeeded(worldRenderState.sectorManager.cameraPos(), renderSector, collectors);
@@ -526,6 +543,10 @@ public void rebuildOnMainThread() {
final RegionBuildState newBuildState = captureAndSetBuildState(context, origin.isNear());
context.encoder.updateSector(renderSector, origin);
+ if (!lightRegion.isClosed()) {
+ lightRegion.markUrgent();
+ }
+
buildTerrain(context, newBuildState);
if (ChunkRebuildCounters.ENABLED) {
diff --git a/src/main/java/grondag/canvas/texture/TextureData.java b/src/main/java/grondag/canvas/texture/TextureData.java
index 0f3716111..292facbdc 100644
--- a/src/main/java/grondag/canvas/texture/TextureData.java
+++ b/src/main/java/grondag/canvas/texture/TextureData.java
@@ -31,7 +31,7 @@ public class TextureData {
// want these outside of the range managed by Mojang's damn GlStateManager
public static final int SHADOWMAP = GL21.GL_TEXTURE12;
public static final int SHADOWMAP_TEXTURE = SHADOWMAP + 1;
- public static final int HD_LIGHTMAP = SHADOWMAP_TEXTURE + 1;
- public static final int MATERIAL_INFO = HD_LIGHTMAP + 1;
+ public static final int COLORED_LIGHTS_DATA = SHADOWMAP_TEXTURE + 1;
+ public static final int MATERIAL_INFO = COLORED_LIGHTS_DATA + 1;
public static final int PROGRAM_SAMPLERS = MATERIAL_INFO + 1;
}
diff --git a/src/main/java/grondag/canvas/varia/CanvasGlHelper.java b/src/main/java/grondag/canvas/varia/CanvasGlHelper.java
index 712a759be..04f5186f2 100644
--- a/src/main/java/grondag/canvas/varia/CanvasGlHelper.java
+++ b/src/main/java/grondag/canvas/varia/CanvasGlHelper.java
@@ -36,6 +36,7 @@ public class CanvasGlHelper {
private static boolean supportsKhrDebug = false;
private static boolean supportsArbTextureCubeMapArray = false;
private static boolean supportsArbConservativeDepth = false;
+ private static int maxTextureSize = 0;
private static String maxGlVersion = "3.2";
@@ -55,6 +56,10 @@ public static boolean supportsArbConservativeDepth() {
return supportsArbConservativeDepth;
}
+ public static int maxTextureSize() {
+ return maxTextureSize;
+ }
+
public static String maxGlVersion() {
return maxGlVersion;
}
@@ -70,6 +75,7 @@ public static void init() {
supportsArbTextureCubeMapArray = caps.GL_ARB_texture_cube_map_array;
supportsArbConservativeDepth = caps.GL_ARB_conservative_depth;
maxGlVersion = maxGlVersion(caps);
+ maxTextureSize = GFX.getInteger(GFX.GL_MAX_TEXTURE_SIZE);
if (Configurator.logMachineInfo) {
logMachineInfo(caps);
@@ -81,15 +87,12 @@ private static void logMachineInfo(GLCapabilities caps) {
final Minecraft client = Minecraft.getInstance();
log.info("================== CANVAS RENDERER DEBUG INFORMATION ==================");
- log.info(String.format(" Java: %s %dbit Canvas: %s", System.getProperty("java.version"), client.is64Bit() ? 64 : 32, CanvasMod.versionString));
+ log.info(String.format(" Java: %s %dbit Canvas: %s LWJGL: %s", System.getProperty("java.version"), client.is64Bit() ? 64 : 32, CanvasMod.versionString, GLX._getLWJGLVersion()));
log.info(String.format(" CPU: %s", GLX._getCpuInfo()));
- log.info(String.format(" LWJGL: %s", GLX._getLWJGLVersion()));
log.info(String.format(" OpenGL (Reported): %s", GLX.getOpenGLVersionString()));
- log.info(String.format(" OpenGL (Available): %s", maxGlVersion));
- log.info(String.format(" glBufferStorage: %s", caps.glBufferStorage == 0 ? "N" : "Y"));
- log.info(String.format(" KHR_debug: %s", supportsKhrDebug() ? "Y" : "N"));
- log.info(String.format(" ARB_conservative_depth: %s", supportsArbConservativeDepth ? "Y" : "N"));
- log.info(String.format(" ARB_texture_cube_map_array: %s", supportsArbTextureCubeMapArray ? "Y" : "N"));
+ log.info(String.format(" OpenGL (Available): %s Max texture size: %s", maxGlVersion, maxTextureSize));
+ log.info(String.format(" glBufferStorage: %s KHR_debug: %s", caps.glBufferStorage == 0 ? "N" : "Y", supportsKhrDebug() ? "Y" : "N"));
+ log.info(String.format(" ARB_conservative_depth: %s ARB_texture_cube_map_array: %s", supportsArbConservativeDepth ? "Y" : "N", supportsArbTextureCubeMapArray ? "Y" : "N"));
log.info(" (This message can be disabled by configuring logMachineInfo = false.)");
log.info("========================================================================");
}
diff --git a/src/main/resources/assets/canvas/lang/en_us.json b/src/main/resources/assets/canvas/lang/en_us.json
index c6ebb778a..d16540aae 100644
--- a/src/main/resources/assets/canvas/lang/en_us.json
+++ b/src/main/resources/assets/canvas/lang/en_us.json
@@ -53,6 +53,10 @@
"config.canvas.help.lightmap_delay_frames": "Setting > 0 may give slightly;better FPS at cost of potential;flickering when lighting changes.",
"config.canvas.value.semi_flat_lighting": "Semi-Flat Lightmap",
"config.canvas.help.semi_flat_lighting": "Models with flat lighting have smoother lighting;(but no ambient occlusion).",
+ "config.canvas.value.colored_lights": "Colored Lights",
+ "config.canvas.help.colored_lights": "Enable colored block lights on pipelines that support it. Replaces vanilla lighting but only visually.",
+ "config.canvas.value.entity_light_source": "Entity Light Source",
+ "config.canvas.help.entity_light_source": "Enable entity as dynamic light sources. Requires colored lights and supporting pipeline.",
"config.canvas.enum.ao_mode.normal": "Vanilla",
"config.canvas.enum.ao_mode.subtle_always": "Subtle",
"config.canvas.enum.ao_mode.subtle_block_light": "Subtle Torchlit",
@@ -150,6 +154,9 @@
"config.canvas.help.profiler_detail_level": "Profiler level of detail. 0=Collapse all, 1=Expand program passes, 2=Expand all",
"config.canvas.value.profiler_overlay_scale": "Profiler Overlay Scale",
"config.canvas.help.profiler_overlay_scale": "Size of the profiler overlay relative to GUI scale.",
+ "config.canvas.value.debug_shader_flag": "Shader Debug Flag",
+ "config.canvas.help.debug_shader_flag": "Enables debug flag in the shader. Only intended for internal Canvas development purposes.",
+
"key.canvas.debug_toggle": "Toggle Debug View",
"key.canvas.debug_prev": "Debug Previous Image",
"key.canvas.debug_next": "Debug Next Image",
@@ -202,5 +209,6 @@
"config.canvas.help.trace_texture_load": "Log significant events of texture/sprite atlas loading.;For debugging use. Will spam the log.",
"config.canvas.value.bloom_toggle": "Enable Bloom",
"config.canvas.help.bloom_toggle": "Renders glow effect around light sources.;Modest impact on performance.",
- "info.canvas.recompile": "Recompiling shaders"
+ "info.canvas.recompile": "Recompiling shaders",
+ "info.canvas.recompile_needs_reload": "Found a state change that requires reload during recompilation. Reloading renderer..."
}
diff --git a/src/main/resources/assets/canvas/shaders/internal/world.glsl b/src/main/resources/assets/canvas/shaders/internal/world.glsl
index 8f91bfe50..b0232eda1 100644
--- a/src/main/resources/assets/canvas/shaders/internal/world.glsl
+++ b/src/main/resources/assets/canvas/shaders/internal/world.glsl
@@ -74,6 +74,8 @@
// UINT ARRAY
#define _CV_RENDER_FRAMES 0
+#define _CV_LIGHT_POINTER_EXTENT 1
+#define _CV_LIGHT_DATA_FIRST_ROW 2
#define _CV_FLAG_HAS_SKYLIGHT 0
#define _CV_FLAG_IS_OVERWORLD 1
@@ -89,7 +91,7 @@
// update each frame
uniform vec4[32] _cvu_world;
-uniform uint[1] _cvu_world_uint;
+uniform uint[3] _cvu_world_uint;
uniform uint[4] _cvu_flags;
#define _CV_MODEL_TO_WORLD 0
diff --git a/src/main/resources/assets/frex/shaders/api/header.glsl b/src/main/resources/assets/frex/shaders/api/header.glsl
index 1b2b9b731..e6ed3c926 100644
--- a/src/main/resources/assets/frex/shaders/api/header.glsl
+++ b/src/main/resources/assets/frex/shaders/api/header.glsl
@@ -4,6 +4,11 @@
#define ANIMATED_FOLIAGE
#define SHADOW_MAP_PRESENT
#define SHADOW_MAP_SIZE 1024
+//#define COLORED_LIGHTS_ENABLED
+//#define LIGHT_DATA_HAS_OCCLUSION
+#define _CV_LIGHT_DATA_COMPLEX_FILTER
+#define _CV_LIGHT_DATA_SIZE vec3(256.0)
#define MATERIAL_TARGET_UNKNOWN
//#define DEPTH_PASS
//#define PBR_ENABLED
+//#define _CV_DEBUG
\ No newline at end of file
diff --git a/src/main/resources/assets/frex/shaders/api/light.glsl b/src/main/resources/assets/frex/shaders/api/light.glsl
new file mode 100644
index 000000000..df7339431
--- /dev/null
+++ b/src/main/resources/assets/frex/shaders/api/light.glsl
@@ -0,0 +1,160 @@
+#include canvas:shaders/internal/world.glsl
+
+/****************************************************************
+ * frex:shaders/api/light.glsl - Canvas Implementation
+ ***************************************************************/
+
+#ifdef COLORED_LIGHTS_ENABLED
+
+uint _cv_lightAddress(sampler2D lightSampler, vec3 worldPos) {
+ uint EXTENT = _cvu_world_uint[_CV_LIGHT_POINTER_EXTENT];
+ uvec3 local = uvec3(mod(worldPos / 16.0, float(EXTENT)));
+ uint linearized = local.x * EXTENT * EXTENT + local.y * EXTENT + local.z;
+
+ // interpret vec4 as a short
+ uint pointerRow = linearized / 4096u;
+ uvec4 address = uvec4(texelFetch(lightSampler, ivec2(linearized - pointerRow * 4096u, pointerRow), 0) * 15.0);
+ return (address.r << 12u) | (address.g << 8u) | (address.b << 4u) | address.a;
+}
+
+bool _cv_hasLightData(sampler2D lightSampler, vec3 worldPos) {
+ return _cv_lightAddress(lightSampler, worldPos) != 0u;
+}
+
+ivec2 _cv_lightTexelCoords(sampler2D lightSampler, vec3 worldPos) {
+ uint exactRow = _cvu_world_uint[_CV_LIGHT_DATA_FIRST_ROW] + _cv_lightAddress(lightSampler, worldPos);
+ ivec3 local = ivec3(mod(worldPos, 16.0));
+
+ return ivec2(local.z * 16 * 16 + local.y * 16 + local.x, exactRow);
+}
+
+bool _cv_isUseful(float a) {
+ return (int(a * 15.0) & 8) > 0;
+}
+
+vec4 frx_getLightFiltered(sampler2D lightSampler, vec3 worldPos) {
+ if (!_cv_hasLightData(lightSampler, worldPos)) {
+ return vec4(0.0);
+ }
+
+ #ifdef _CV_LIGHT_DATA_COMPLEX_FILTER
+ vec3 pos = floor(worldPos) + vec3(0.5);
+ vec3 H = sign(fract(worldPos) - vec3(0.5));
+ #else
+ vec3 pos = worldPos - vec3(0.5);
+ const vec3 H = vec3(1.0);
+ #endif
+
+ // sample 2x2x2 area
+ vec4 tex000 = texelFetch(lightSampler, _cv_lightTexelCoords(lightSampler, pos + vec3(0.0, 0.0, 0.0)), 0);
+ vec4 tex001 = texelFetch(lightSampler, _cv_lightTexelCoords(lightSampler, pos + vec3(0.0, 0.0, H.z)), 0);
+ vec4 tex010 = texelFetch(lightSampler, _cv_lightTexelCoords(lightSampler, pos + vec3(0.0, H.y, 0.0)), 0);
+ vec4 tex011 = texelFetch(lightSampler, _cv_lightTexelCoords(lightSampler, pos + vec3(0.0, H.y, H.z)), 0);
+ vec4 tex101 = texelFetch(lightSampler, _cv_lightTexelCoords(lightSampler, pos + vec3(H.x, 0.0, H.z)), 0);
+ vec4 tex110 = texelFetch(lightSampler, _cv_lightTexelCoords(lightSampler, pos + vec3(H.x, H.y, 0.0)), 0);
+ vec4 tex100 = texelFetch(lightSampler, _cv_lightTexelCoords(lightSampler, pos + vec3(H.x, 0.0, 0.0)), 0);
+ vec4 tex111 = texelFetch(lightSampler, _cv_lightTexelCoords(lightSampler, pos + vec3(H.x, H.y, H.z)), 0);
+
+ #ifdef _CV_LIGHT_DATA_COMPLEX_FILTER
+ vec3 center = worldPos - pos;
+ vec3 pos000 = vec3(0.0, 0.0, 0.0) - center;
+ vec3 pos001 = vec3(0.0, 0.0, H.z) - center;
+ vec3 pos010 = vec3(0.0, H.y, 0.0) - center;
+ vec3 pos011 = vec3(0.0, H.y, H.z) - center;
+ vec3 pos101 = vec3(H.x, 0.0, H.z) - center;
+ vec3 pos110 = vec3(H.x, H.y, 0.0) - center;
+ vec3 pos100 = vec3(H.x, 0.0, 0.0) - center;
+ vec3 pos111 = vec3(H.x, H.y, H.z) - center;
+
+ float a000 = 1.0; // origin
+ float a001 = float(_cv_isUseful(tex001.a));
+ float a010 = float(_cv_isUseful(tex010.a));
+ float a100 = float(_cv_isUseful(tex100.a));
+ float a011 = float(_cv_isUseful(tex011.a));
+ float a101 = float(_cv_isUseful(tex101.a));
+ float a110 = float(_cv_isUseful(tex110.a));
+ float a111 = float(_cv_isUseful(tex111.a));
+
+ #ifdef _CV_DEBUG
+ // filters out "irrelevant" data from the current blending result.
+ // in theory it should make the resulting light more accurate. in practice, this makes propagation errors stand out more.
+ // as our implementation is not error-free, this is more detrimental than useful to the users. use for debugging.
+ a001 *= float(all(greaterThanEqual(vec3(1.05 / 15.0), abs(tex001.rgb - tex000.rgb))));
+ a010 *= float(all(greaterThanEqual(vec3(1.05 / 15.0), abs(tex010.rgb - tex000.rgb))));
+ a100 *= float(all(greaterThanEqual(vec3(1.05 / 15.0), abs(tex100.rgb - tex000.rgb))));
+ a011 *= float(all(greaterThanEqual(vec3(2.05 / 15.0), abs(tex011.rgb - tex000.rgb))));
+ a101 *= float(all(greaterThanEqual(vec3(2.05 / 15.0), abs(tex101.rgb - tex000.rgb))));
+ a110 *= float(all(greaterThanEqual(vec3(2.05 / 15.0), abs(tex110.rgb - tex000.rgb))));
+ a111 *= float(all(greaterThanEqual(vec3(3.05 / 15.0), abs(tex111.rgb - tex000.rgb))));
+ #endif
+
+ float w000 = a000 * abs(pos111.x * pos111.y * pos111.z);
+ float w001 = a001 * abs(pos110.x * pos110.y * pos110.z);
+ float w010 = a010 * abs(pos101.x * pos101.y * pos101.z);
+ float w011 = a011 * abs(pos100.x * pos100.y * pos100.z);
+ float w101 = a101 * abs(pos010.x * pos010.y * pos010.z);
+ float w110 = a110 * abs(pos001.x * pos001.y * pos001.z);
+ float w100 = a100 * abs(pos011.x * pos011.y * pos011.z);
+ float w111 = a111 * abs(pos000.x * pos000.y * pos000.z);
+
+ float weight = w000 + w001 + w010 + w011 + w101 + w110 + w100 + w111;
+ vec4 finalMix = weight == 0.0 ? vec4(0.0) : vec4((tex000.rgb * w000 + tex001.rgb * w001 + tex010.rgb * w010 + tex011.rgb * w011 + tex101.rgb * w101 + tex110.rgb * w110 + tex100.rgb * w100 + tex111.rgb * w111) / weight, 1.0);
+ #else
+ vec3 fac = fract(pos);
+
+ vec3 mix001 = mix(tex000.rgb, tex001.rgb, fac.z);
+ vec3 mix011 = mix(tex010.rgb, tex011.rgb, fac.z);
+ vec3 mix010 = mix(mix001, mix011, fac.y);
+
+ vec3 mix101 = mix(tex100.rgb, tex101.rgb, fac.z);
+ vec3 mix111 = mix(tex110.rgb, tex111.rgb, fac.z);
+ vec3 mix110 = mix(mix101, mix111, fac.y);
+
+ vec4 finalMix = vec4(mix(mix010, mix110, fac.x), 1.0);
+ #endif
+
+ return finalMix;
+}
+
+vec4 frx_getLightRaw(sampler2D lightSampler, vec3 worldPos) {
+ if (!_cv_hasLightData(lightSampler, worldPos)) {
+ return vec4(0.0);
+ }
+
+ vec4 tex = texelFetch(lightSampler, _cv_lightTexelCoords(lightSampler, worldPos), 0);
+ return vec4(tex.rgb, float(_cv_isUseful(tex.a)));
+}
+
+vec3 frx_getLight(sampler2D lightSampler, vec3 worldPos, vec3 fallback) {
+ vec4 light = frx_getLightFiltered(lightSampler, worldPos);
+ return mix(fallback, light.rgb, light.a);
+}
+
+bool frx_lightDataExists(sampler2D lightSampler, vec3 worldPos) {
+ return _cv_hasLightData(lightSampler, worldPos);
+}
+
+#ifdef LIGHT_DATA_HAS_OCCLUSION
+struct frx_LightData {
+ vec4 light;
+ bool isLightSource;
+ bool isOccluder;
+ bool isFullCube;
+};
+
+frx_LightData frx_getLightOcclusionData(sampler2D lightSampler, vec3 worldPos) {
+ if (!_cv_hasLightData(lightSampler, worldPos)) {
+ return frx_LightData(vec4(0.0), false, false, false);
+ }
+
+ vec4 tex = texelFetch(lightSampler, _cv_lightTexelCoords(lightSampler, worldPos), 0);
+ int flags = int(tex.a * 15.0);
+
+ bool isFullCube = (flags & 4) > 0;
+ bool isOccluder = (flags & 2) > 0;
+ bool isLightSource = (flags & 1) > 0;
+
+ return frx_LightData(vec4(tex.rgb, 1.0), isLightSource, isOccluder, isFullCube);
+}
+#endif
+#endif
diff --git a/src/main/resources/assets/frex/shaders/api/sampler.glsl b/src/main/resources/assets/frex/shaders/api/sampler.glsl
index ed4f5e365..5dcfceb7e 100644
--- a/src/main/resources/assets/frex/shaders/api/sampler.glsl
+++ b/src/main/resources/assets/frex/shaders/api/sampler.glsl
@@ -1,4 +1,5 @@
#include canvas:shaders/internal/program.glsl
+#include frex:shaders/api/light.glsl
/****************************************************************
* frex:shaders/api/sampler.glsl - Canvas Implementation
@@ -24,3 +25,19 @@ uniform sampler2DArrayShadow frxs_shadowMap;
uniform sampler2DArray frxs_shadowMapTexture;
#endif
#endif
+
+#ifdef COLORED_LIGHTS_ENABLED
+uniform sampler2D frxs_lightData;
+
+vec4 frx_getLightFiltered(vec3 worldPos) {
+ return frx_getLightFiltered(frxs_lightData, worldPos);
+}
+
+vec4 frx_getLightRaw(vec3 worldPos) {
+ return frx_getLightRaw(frxs_lightData, worldPos);
+}
+
+vec3 frx_getLight(vec3 worldPos, vec3 fallback) {
+ return frx_getLight(frxs_lightData, worldPos, fallback);
+}
+#endif
diff --git a/src/main/resources/assets/frex/shaders/api/view.glsl b/src/main/resources/assets/frex/shaders/api/view.glsl
index 20997bf0f..46c12e38f 100644
--- a/src/main/resources/assets/frex/shaders/api/view.glsl
+++ b/src/main/resources/assets/frex/shaders/api/view.glsl
@@ -53,6 +53,3 @@
#define frx_cameraInSnow int((_cvu_flags[_CV_WORLD_FLAGS_INDEX] >> 25) & 1u)
#define frx_viewFlag(flag) (((_cvu_flags[_CV_WORLD_FLAGS_INDEX] >> flag) & 1u) == 1u) // DEPRECATED - DO NOT USE
-
-
-
diff --git a/src/main/resources/assets/minecraft/lights/block/blast_furnace.json b/src/main/resources/assets/minecraft/lights/block/blast_furnace.json
new file mode 100644
index 000000000..e9a458490
--- /dev/null
+++ b/src/main/resources/assets/minecraft/lights/block/blast_furnace.json
@@ -0,0 +1,7 @@
+{
+ "defaultLight": {
+ "red": 1.0,
+ "green": 1.0,
+ "blue": 0.8
+ }
+}
diff --git a/src/main/resources/assets/minecraft/lights/block/campfire.json b/src/main/resources/assets/minecraft/lights/block/campfire.json
new file mode 100644
index 000000000..e9a458490
--- /dev/null
+++ b/src/main/resources/assets/minecraft/lights/block/campfire.json
@@ -0,0 +1,7 @@
+{
+ "defaultLight": {
+ "red": 1.0,
+ "green": 1.0,
+ "blue": 0.8
+ }
+}
diff --git a/src/main/resources/assets/minecraft/lights/block/candle.json b/src/main/resources/assets/minecraft/lights/block/candle.json
new file mode 100644
index 000000000..e9a458490
--- /dev/null
+++ b/src/main/resources/assets/minecraft/lights/block/candle.json
@@ -0,0 +1,7 @@
+{
+ "defaultLight": {
+ "red": 1.0,
+ "green": 1.0,
+ "blue": 0.8
+ }
+}
diff --git a/src/main/resources/assets/minecraft/lights/block/cave_vines.json b/src/main/resources/assets/minecraft/lights/block/cave_vines.json
new file mode 100644
index 000000000..e9a458490
--- /dev/null
+++ b/src/main/resources/assets/minecraft/lights/block/cave_vines.json
@@ -0,0 +1,7 @@
+{
+ "defaultLight": {
+ "red": 1.0,
+ "green": 1.0,
+ "blue": 0.8
+ }
+}
diff --git a/src/main/resources/assets/minecraft/lights/block/cave_vines_plant.json b/src/main/resources/assets/minecraft/lights/block/cave_vines_plant.json
new file mode 100644
index 000000000..e9a458490
--- /dev/null
+++ b/src/main/resources/assets/minecraft/lights/block/cave_vines_plant.json
@@ -0,0 +1,7 @@
+{
+ "defaultLight": {
+ "red": 1.0,
+ "green": 1.0,
+ "blue": 0.8
+ }
+}
diff --git a/src/main/resources/assets/minecraft/lights/block/enchanting_table.json b/src/main/resources/assets/minecraft/lights/block/enchanting_table.json
new file mode 100644
index 000000000..56ab6d7a0
--- /dev/null
+++ b/src/main/resources/assets/minecraft/lights/block/enchanting_table.json
@@ -0,0 +1,7 @@
+{
+ "defaultLight": {
+ "red": 0.8,
+ "green": 1.0,
+ "blue": 1.0
+ }
+}
diff --git a/src/main/resources/assets/minecraft/lights/block/fire.json b/src/main/resources/assets/minecraft/lights/block/fire.json
new file mode 100644
index 000000000..e9a458490
--- /dev/null
+++ b/src/main/resources/assets/minecraft/lights/block/fire.json
@@ -0,0 +1,7 @@
+{
+ "defaultLight": {
+ "red": 1.0,
+ "green": 1.0,
+ "blue": 0.8
+ }
+}
diff --git a/src/main/resources/assets/minecraft/lights/block/furnace.json b/src/main/resources/assets/minecraft/lights/block/furnace.json
new file mode 100644
index 000000000..e9a458490
--- /dev/null
+++ b/src/main/resources/assets/minecraft/lights/block/furnace.json
@@ -0,0 +1,7 @@
+{
+ "defaultLight": {
+ "red": 1.0,
+ "green": 1.0,
+ "blue": 0.8
+ }
+}
diff --git a/src/main/resources/assets/minecraft/lights/block/lava_cauldron.json b/src/main/resources/assets/minecraft/lights/block/lava_cauldron.json
new file mode 100644
index 000000000..e9a458490
--- /dev/null
+++ b/src/main/resources/assets/minecraft/lights/block/lava_cauldron.json
@@ -0,0 +1,7 @@
+{
+ "defaultLight": {
+ "red": 1.0,
+ "green": 1.0,
+ "blue": 0.8
+ }
+}
diff --git a/src/main/resources/assets/minecraft/lights/block/nether_portal.json b/src/main/resources/assets/minecraft/lights/block/nether_portal.json
new file mode 100644
index 000000000..d8fa7268e
--- /dev/null
+++ b/src/main/resources/assets/minecraft/lights/block/nether_portal.json
@@ -0,0 +1,8 @@
+{
+ "defaultLight": {
+ "lightLevel": 14.0,
+ "red": 1.0,
+ "green": 0.4,
+ "blue": 1.0
+ }
+}
diff --git a/src/main/resources/assets/minecraft/lights/block/sea_pickle.json b/src/main/resources/assets/minecraft/lights/block/sea_pickle.json
new file mode 100644
index 000000000..56ab6d7a0
--- /dev/null
+++ b/src/main/resources/assets/minecraft/lights/block/sea_pickle.json
@@ -0,0 +1,7 @@
+{
+ "defaultLight": {
+ "red": 0.8,
+ "green": 1.0,
+ "blue": 1.0
+ }
+}
diff --git a/src/main/resources/assets/minecraft/lights/block/smoker.json b/src/main/resources/assets/minecraft/lights/block/smoker.json
new file mode 100644
index 000000000..e9a458490
--- /dev/null
+++ b/src/main/resources/assets/minecraft/lights/block/smoker.json
@@ -0,0 +1,7 @@
+{
+ "defaultLight": {
+ "red": 1.0,
+ "green": 1.0,
+ "blue": 0.8
+ }
+}
diff --git a/src/main/resources/assets/minecraft/lights/block/soul_fire.json b/src/main/resources/assets/minecraft/lights/block/soul_fire.json
new file mode 100644
index 000000000..01e134c9d
--- /dev/null
+++ b/src/main/resources/assets/minecraft/lights/block/soul_fire.json
@@ -0,0 +1,8 @@
+{
+ "defaultLight": {
+ "lightLevel": 13.0,
+ "red": 0.6,
+ "green": 0.8,
+ "blue": 1.0
+ }
+}
diff --git a/src/main/resources/assets/minecraft/lights/fluid/flowing_lava.json b/src/main/resources/assets/minecraft/lights/fluid/flowing_lava.json
new file mode 100644
index 000000000..e9a458490
--- /dev/null
+++ b/src/main/resources/assets/minecraft/lights/fluid/flowing_lava.json
@@ -0,0 +1,7 @@
+{
+ "defaultLight": {
+ "red": 1.0,
+ "green": 1.0,
+ "blue": 0.8
+ }
+}
diff --git a/src/main/resources/assets/minecraft/lights/fluid/lava.json b/src/main/resources/assets/minecraft/lights/fluid/lava.json
new file mode 100644
index 000000000..e9a458490
--- /dev/null
+++ b/src/main/resources/assets/minecraft/lights/fluid/lava.json
@@ -0,0 +1,7 @@
+{
+ "defaultLight": {
+ "red": 1.0,
+ "green": 1.0,
+ "blue": 0.8
+ }
+}
diff --git a/src/main/resources/resourcepacks/abstract/assets/abstract/pipeline/base.json5 b/src/main/resources/resourcepacks/abstract/assets/abstract/pipeline/base.json5
index dc2ef63f3..491966b25 100644
--- a/src/main/resources/resourcepacks/abstract/assets/abstract/pipeline/base.json5
+++ b/src/main/resources/resourcepacks/abstract/assets/abstract/pipeline/base.json5
@@ -10,6 +10,10 @@
rainSmoothingFrames: 500,
glslVersion: 330,
+ coloredLights: {
+ useOcclusionData: false
+ },
+
images: [
// color attachment for solid draws
{
diff --git a/src/main/resources/resourcepacks/abstract/assets/abstract/shaders/pipeline/abstract.frag b/src/main/resources/resourcepacks/abstract/assets/abstract/shaders/pipeline/abstract.frag
index c510a97e9..0370378b5 100644
--- a/src/main/resources/resourcepacks/abstract/assets/abstract/shaders/pipeline/abstract.frag
+++ b/src/main/resources/resourcepacks/abstract/assets/abstract/shaders/pipeline/abstract.frag
@@ -7,6 +7,7 @@
#include frex:shaders/api/player.glsl
#include frex:shaders/api/material.glsl
#include frex:shaders/api/fragment.glsl
+#include frex:shaders/api/sampler.glsl
#include abstract:shaders/pipeline/glint.glsl
#include abstract:basic_light_config
#include abstract:handheld_light_config
@@ -26,7 +27,8 @@ out vec4[2] fragColor;
#if HANDHELD_LIGHT_RADIUS != 0
flat in float _cvInnerAngle;
flat in float _cvOuterAngle;
-in vec4 _cvViewVertex;
+in vec3 _cvViewVertex;
+in vec3 _cvWorldVertex;
#endif
/**
@@ -96,8 +98,9 @@ void frx_pipelineFragment() {
// ambient
float skyCoord = frx_fragEnableDiffuse ? 0.03125 + (frx_fragLight.y - 0.03125) * 0.5 : frx_fragLight.y;
- vec4 light = frx_fromGamma(texture(frxs_lightmap, vec2(frx_fragLight.x, skyCoord)));
- light = mix(light, frx_emissiveColor, frx_fragEmissive);
+ vec4 light = frx_fromGamma(texture(frxs_lightmap, vec2(0.03125, skyCoord)));
+ light += frx_fromGamma(frx_getLightFiltered(_cvWorldVertex));
+ light += frx_emissiveColor * frx_fragEmissive;
#if HANDHELD_LIGHT_RADIUS != 0
vec4 held = frx_heldLight;
diff --git a/src/main/resources/resourcepacks/abstract/assets/abstract/shaders/pipeline/abstract.vert b/src/main/resources/resourcepacks/abstract/assets/abstract/shaders/pipeline/abstract.vert
index 3375eecc0..a46794db4 100644
--- a/src/main/resources/resourcepacks/abstract/assets/abstract/shaders/pipeline/abstract.vert
+++ b/src/main/resources/resourcepacks/abstract/assets/abstract/shaders/pipeline/abstract.vert
@@ -1,4 +1,5 @@
#include frex:shaders/api/view.glsl
+#include frex:shaders/api/world.glsl
#include frex:shaders/api/player.glsl
#include abstract:handheld_light_config
@@ -9,7 +10,8 @@
#if HANDHELD_LIGHT_RADIUS != 0
flat out float _cvInnerAngle;
flat out float _cvOuterAngle;
-out vec4 _cvViewVertex;
+out vec3 _cvViewVertex;
+out vec3 _cvWorldVertex;
#endif
out vec4 shadowPos;
@@ -17,14 +19,16 @@ out vec4 shadowPos;
void frx_pipelineVertex() {
if (frx_isGui) {
gl_Position = frx_guiViewProjectionMatrix * frx_vertex;
+ _cvWorldVertex = (frx_vertex.xyz * frx_normalModelMatrix) * 0.2 + frx_cameraPos;
frx_distance = length(gl_Position.xyz);
} else {
frx_vertex += frx_modelToCamera;
+ _cvWorldVertex = frx_vertex.xyz + frx_cameraPos + frx_normal * 0.05;
vec4 viewCoord = frx_viewMatrix * frx_vertex;
frx_distance = length(viewCoord.xyz);
gl_Position = frx_projectionMatrix * viewCoord;
#if HANDHELD_LIGHT_RADIUS != 0
- _cvViewVertex = viewCoord;
+ _cvViewVertex = viewCoord.xyz;
#endif
shadowPos = frx_shadowViewMatrix * frx_vertex;
diff --git a/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/blast_furnace.json b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/blast_furnace.json
new file mode 100644
index 000000000..99cbe9ba9
--- /dev/null
+++ b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/blast_furnace.json
@@ -0,0 +1,8 @@
+{
+ "defaultLight": {
+ "red": 1.0,
+ "green": 1.0,
+ "blue": 0.8
+ }
+}
+
diff --git a/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/campfire.json b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/campfire.json
new file mode 100644
index 000000000..99cbe9ba9
--- /dev/null
+++ b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/campfire.json
@@ -0,0 +1,8 @@
+{
+ "defaultLight": {
+ "red": 1.0,
+ "green": 1.0,
+ "blue": 0.8
+ }
+}
+
diff --git a/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/cave_vines.json b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/cave_vines.json
new file mode 100644
index 000000000..99cbe9ba9
--- /dev/null
+++ b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/cave_vines.json
@@ -0,0 +1,8 @@
+{
+ "defaultLight": {
+ "red": 1.0,
+ "green": 1.0,
+ "blue": 0.8
+ }
+}
+
diff --git a/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/cave_vines_plant.json b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/cave_vines_plant.json
new file mode 100644
index 000000000..99cbe9ba9
--- /dev/null
+++ b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/cave_vines_plant.json
@@ -0,0 +1,8 @@
+{
+ "defaultLight": {
+ "red": 1.0,
+ "green": 1.0,
+ "blue": 0.8
+ }
+}
+
diff --git a/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/deepslate_redstone_ore.json b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/deepslate_redstone_ore.json
new file mode 100644
index 000000000..2de115850
--- /dev/null
+++ b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/deepslate_redstone_ore.json
@@ -0,0 +1,7 @@
+{
+ "defaultLight": {
+ "red": 1.0,
+ "green": 0.3,
+ "blue": 0.3
+ }
+}
diff --git a/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/enchanting_table.json b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/enchanting_table.json
new file mode 100644
index 000000000..7f7d19652
--- /dev/null
+++ b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/enchanting_table.json
@@ -0,0 +1,8 @@
+{
+ "defaultLight": {
+ "red": 0.8,
+ "green": 1.0,
+ "blue": 1.0
+ }
+}
+
diff --git a/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/end_gateway.json b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/end_gateway.json
new file mode 100644
index 000000000..ac49c2abe
--- /dev/null
+++ b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/end_gateway.json
@@ -0,0 +1,7 @@
+{
+ "defaultLight": {
+ "red": 1.0,
+ "green": 0.6,
+ "blue": 1.0
+ }
+}
diff --git a/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/end_portal.json b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/end_portal.json
new file mode 100644
index 000000000..ac49c2abe
--- /dev/null
+++ b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/end_portal.json
@@ -0,0 +1,7 @@
+{
+ "defaultLight": {
+ "red": 1.0,
+ "green": 0.6,
+ "blue": 1.0
+ }
+}
diff --git a/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/fire.json b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/fire.json
new file mode 100644
index 000000000..99cbe9ba9
--- /dev/null
+++ b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/fire.json
@@ -0,0 +1,8 @@
+{
+ "defaultLight": {
+ "red": 1.0,
+ "green": 1.0,
+ "blue": 0.8
+ }
+}
+
diff --git a/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/furnace.json b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/furnace.json
new file mode 100644
index 000000000..99cbe9ba9
--- /dev/null
+++ b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/furnace.json
@@ -0,0 +1,8 @@
+{
+ "defaultLight": {
+ "red": 1.0,
+ "green": 1.0,
+ "blue": 0.8
+ }
+}
+
diff --git a/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/glowstone.json b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/glowstone.json
new file mode 100644
index 000000000..90c551d76
--- /dev/null
+++ b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/glowstone.json
@@ -0,0 +1,7 @@
+{
+ "defaultLight": {
+ "red": 1.0,
+ "green": 1.0,
+ "blue": 0.8
+ }
+}
diff --git a/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/jack_o_lantern.json b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/jack_o_lantern.json
new file mode 100644
index 000000000..99cbe9ba9
--- /dev/null
+++ b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/jack_o_lantern.json
@@ -0,0 +1,8 @@
+{
+ "defaultLight": {
+ "red": 1.0,
+ "green": 1.0,
+ "blue": 0.8
+ }
+}
+
diff --git a/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/lantern.json b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/lantern.json
new file mode 100644
index 000000000..90c551d76
--- /dev/null
+++ b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/lantern.json
@@ -0,0 +1,7 @@
+{
+ "defaultLight": {
+ "red": 1.0,
+ "green": 1.0,
+ "blue": 0.8
+ }
+}
diff --git a/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/lava_cauldron.json b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/lava_cauldron.json
new file mode 100644
index 000000000..99cbe9ba9
--- /dev/null
+++ b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/lava_cauldron.json
@@ -0,0 +1,8 @@
+{
+ "defaultLight": {
+ "red": 1.0,
+ "green": 1.0,
+ "blue": 0.8
+ }
+}
+
diff --git a/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/light.json b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/light.json
new file mode 100644
index 000000000..99cbe9ba9
--- /dev/null
+++ b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/light.json
@@ -0,0 +1,8 @@
+{
+ "defaultLight": {
+ "red": 1.0,
+ "green": 1.0,
+ "blue": 0.8
+ }
+}
+
diff --git a/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/magma_block.json b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/magma_block.json
new file mode 100644
index 000000000..97a63a40d
--- /dev/null
+++ b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/magma_block.json
@@ -0,0 +1,8 @@
+{
+ "defaultLight": {
+ "red": 1.0,
+ "green": 0.8,
+ "blue": 0.3
+ }
+}
+
diff --git a/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/nether_portal.json b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/nether_portal.json
new file mode 100644
index 000000000..ac49c2abe
--- /dev/null
+++ b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/nether_portal.json
@@ -0,0 +1,7 @@
+{
+ "defaultLight": {
+ "red": 1.0,
+ "green": 0.6,
+ "blue": 1.0
+ }
+}
diff --git a/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/ochre_froglight.json b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/ochre_froglight.json
new file mode 100644
index 000000000..e75c61089
--- /dev/null
+++ b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/ochre_froglight.json
@@ -0,0 +1,7 @@
+{
+ "defaultLight": {
+ "red": 1.0,
+ "green": 1.0,
+ "blue": 0.6
+ }
+}
diff --git a/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/pearlescent_froglight.json b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/pearlescent_froglight.json
new file mode 100644
index 000000000..c96316349
--- /dev/null
+++ b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/pearlescent_froglight.json
@@ -0,0 +1,7 @@
+{
+ "defaultLight": {
+ "red": 1.0,
+ "green": 0.6,
+ "blue": 0.9
+ }
+}
diff --git a/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/redstone_ore.json b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/redstone_ore.json
new file mode 100644
index 000000000..2de115850
--- /dev/null
+++ b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/redstone_ore.json
@@ -0,0 +1,7 @@
+{
+ "defaultLight": {
+ "red": 1.0,
+ "green": 0.3,
+ "blue": 0.3
+ }
+}
diff --git a/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/redstone_torch.json b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/redstone_torch.json
new file mode 100644
index 000000000..2de115850
--- /dev/null
+++ b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/redstone_torch.json
@@ -0,0 +1,7 @@
+{
+ "defaultLight": {
+ "red": 1.0,
+ "green": 0.3,
+ "blue": 0.3
+ }
+}
diff --git a/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/sea_lantern.json b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/sea_lantern.json
new file mode 100644
index 000000000..7f7d19652
--- /dev/null
+++ b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/sea_lantern.json
@@ -0,0 +1,8 @@
+{
+ "defaultLight": {
+ "red": 0.8,
+ "green": 1.0,
+ "blue": 1.0
+ }
+}
+
diff --git a/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/sea_pickle.json b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/sea_pickle.json
new file mode 100644
index 000000000..7f7d19652
--- /dev/null
+++ b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/sea_pickle.json
@@ -0,0 +1,8 @@
+{
+ "defaultLight": {
+ "red": 0.8,
+ "green": 1.0,
+ "blue": 1.0
+ }
+}
+
diff --git a/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/shroomlight.json b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/shroomlight.json
new file mode 100644
index 000000000..99cbe9ba9
--- /dev/null
+++ b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/shroomlight.json
@@ -0,0 +1,8 @@
+{
+ "defaultLight": {
+ "red": 1.0,
+ "green": 1.0,
+ "blue": 0.8
+ }
+}
+
diff --git a/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/smoker.json b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/smoker.json
new file mode 100644
index 000000000..99cbe9ba9
--- /dev/null
+++ b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/smoker.json
@@ -0,0 +1,8 @@
+{
+ "defaultLight": {
+ "red": 1.0,
+ "green": 1.0,
+ "blue": 0.8
+ }
+}
+
diff --git a/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/soul_fire.json b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/soul_fire.json
new file mode 100644
index 000000000..f56044686
--- /dev/null
+++ b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/soul_fire.json
@@ -0,0 +1,8 @@
+{
+ "defaultLight": {
+ "red": 0.6,
+ "green": 0.8,
+ "blue": 1.0
+ }
+}
+
diff --git a/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/soul_lantern.json b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/soul_lantern.json
new file mode 100644
index 000000000..f56044686
--- /dev/null
+++ b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/soul_lantern.json
@@ -0,0 +1,8 @@
+{
+ "defaultLight": {
+ "red": 0.6,
+ "green": 0.8,
+ "blue": 1.0
+ }
+}
+
diff --git a/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/soul_torch.json b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/soul_torch.json
new file mode 100644
index 000000000..f56044686
--- /dev/null
+++ b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/soul_torch.json
@@ -0,0 +1,8 @@
+{
+ "defaultLight": {
+ "red": 0.6,
+ "green": 0.8,
+ "blue": 1.0
+ }
+}
+
diff --git a/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/torch.json b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/torch.json
new file mode 100644
index 000000000..99cbe9ba9
--- /dev/null
+++ b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/torch.json
@@ -0,0 +1,8 @@
+{
+ "defaultLight": {
+ "red": 1.0,
+ "green": 1.0,
+ "blue": 0.8
+ }
+}
+
diff --git a/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/verdant_froglight.json b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/verdant_froglight.json
new file mode 100644
index 000000000..0a14c0503
--- /dev/null
+++ b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/block/verdant_froglight.json
@@ -0,0 +1,7 @@
+{
+ "defaultLight": {
+ "red": 0.6,
+ "green": 1.0,
+ "blue": 0.7
+ }
+}
diff --git a/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/entity/blaze.json b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/entity/blaze.json
new file mode 100644
index 000000000..475d51552
--- /dev/null
+++ b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/entity/blaze.json
@@ -0,0 +1,9 @@
+{
+ "defaultLight": {
+ "lightLevel": 8.0,
+ "red": 1.0,
+ "green": 1.0,
+ "blue": 0.8
+ }
+}
+
diff --git a/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/entity/glow_squid.json b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/entity/glow_squid.json
new file mode 100644
index 000000000..19a304290
--- /dev/null
+++ b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/entity/glow_squid.json
@@ -0,0 +1,9 @@
+{
+ "defaultLight": {
+ "lightLevel": 8.0,
+ "red": 0.6,
+ "green": 1.0,
+ "blue": 0.9
+ }
+}
+
diff --git a/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/fluid/flowing_lava.json b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/fluid/flowing_lava.json
new file mode 100644
index 000000000..99cbe9ba9
--- /dev/null
+++ b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/fluid/flowing_lava.json
@@ -0,0 +1,8 @@
+{
+ "defaultLight": {
+ "red": 1.0,
+ "green": 1.0,
+ "blue": 0.8
+ }
+}
+
diff --git a/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/fluid/lava.json b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/fluid/lava.json
new file mode 100644
index 000000000..99cbe9ba9
--- /dev/null
+++ b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/fluid/lava.json
@@ -0,0 +1,8 @@
+{
+ "defaultLight": {
+ "red": 1.0,
+ "green": 1.0,
+ "blue": 0.8
+ }
+}
+
diff --git a/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/item/ochre_froglight.json b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/item/ochre_froglight.json
new file mode 100644
index 000000000..9b7bc8b94
--- /dev/null
+++ b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/item/ochre_froglight.json
@@ -0,0 +1,7 @@
+{
+ "intensity": 1.0,
+ "red": 1.0,
+ "green": 1.0,
+ "blue": 0.6,
+ "worksInFluid": true
+}
diff --git a/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/item/pearlescent_froglight.json b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/item/pearlescent_froglight.json
new file mode 100644
index 000000000..5835a7e48
--- /dev/null
+++ b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/item/pearlescent_froglight.json
@@ -0,0 +1,7 @@
+{
+ "intensity": 1.0,
+ "red": 1.0,
+ "green": 0.6,
+ "blue": 0.9,
+ "worksInFluid": true
+}
diff --git a/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/item/verdant_froglight.json b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/item/verdant_froglight.json
new file mode 100644
index 000000000..b484907dc
--- /dev/null
+++ b/src/main/resources/resourcepacks/canvas_default/assets/minecraft/lights/item/verdant_froglight.json
@@ -0,0 +1,7 @@
+{
+ "intensity": 1.0,
+ "red": 0.6,
+ "green": 1.0,
+ "blue": 0.7,
+ "worksInFluid": true
+}