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 +}