From dc33ffa54697f61cd8f579235e7cebdbf0765f7c Mon Sep 17 00:00:00 2001 From: koka Date: Fri, 18 Sep 2026 02:25:48 +0200 Subject: [PATCH] Add optional local colored lights for beta.139 --- README.md | 20 +-- docs/colored-lights-beta139.md | 95 ++++++++++++++ gradle.properties | 4 +- mods/sanctuary/build.gradle | 4 + .../shader/ColoredLights139ClientChecks.java | 80 ++++++++++++ .../shader/PixelShadows122ClientChecks.java | 10 +- .../mixin/Colored139LightmapProbeMixin.java | 17 +++ .../resources/sanctuary-gametest.mixins.json | 1 + .../client/shader/ColoredLightMaterials.java | 58 +++++++++ .../client/shader/ColoredLightVolume.java | 116 ++++++++++++++++++ .../client/shader/ColoredLights.java | 78 ++++++++++++ .../client/shader/ShaderOptionsScreen.java | 3 + .../sanctuary/client/shader/VanillaLight.java | 7 ++ .../mixin/client/PixelShadowDirtyMixin.java | 7 +- .../mixin/client/PixelShadowLevelMixin.java | 7 +- .../client/PixelShadowRendererMixin.java | 6 +- .../assets/sanctuary/lang/en_us.json | 7 +- .../assets/sanctuary/lang/fr_fr.json | 7 +- .../sanctuary/shaders/core/colored_lights.fsh | 61 +++++++++ packwiz/pack.toml | 2 +- 20 files changed, 566 insertions(+), 24 deletions(-) create mode 100644 docs/colored-lights-beta139.md create mode 100644 mods/sanctuary/src/clientGametest/java/fr/koka/sanctuary/client/shader/ColoredLights139ClientChecks.java create mode 100644 mods/sanctuary/src/gametest/java/fr/koka/sanctuary/gametest/mixin/Colored139LightmapProbeMixin.java create mode 100644 mods/sanctuary/src/main/java/fr/koka/sanctuary/client/shader/ColoredLightMaterials.java create mode 100644 mods/sanctuary/src/main/java/fr/koka/sanctuary/client/shader/ColoredLightVolume.java create mode 100644 mods/sanctuary/src/main/java/fr/koka/sanctuary/client/shader/ColoredLights.java create mode 100644 mods/sanctuary/src/main/resources/assets/sanctuary/shaders/core/colored_lights.fsh diff --git a/README.md b/README.md index 04a969c..f4dc2b4 100644 --- a/README.md +++ b/README.md @@ -20,13 +20,11 @@ La [vision complète](docs/vision.md) conserve les intentions ; le L’économie, les claims et les dimensions décrits dans la vision restent à implémenter. -La version **beta.138** adapte l’épaisseur des **rayons solaires à la météo** : -plus denses et diffus dans le brouillard, renforcés sous les précipitations, -avec moins de lumière pendant l’orage. Le curseur reste inchangé. -Le [ticket](docs/shader-beta138.md) détaille le rendu et ses vérifications. -La [vérification Vulkan](docs/shader-vulkan-beta138.md) précise les backends testés -et les limites de la validation par pilote. -Release publiée et même instance Prism synchronisée ; sauvegardes et réglages conservés. +La version **beta.139** ajoute les **lumières colorées** optionnelles : les vrais +blocs lumineux teintent les surfaces voisines, même quand la source est hors +champ. Intensité indépendante, réglée à 35 % ; obstacles natifs et cache local. +Le [ticket](docs/colored-lights-beta139.md) décrit les limites et vérifications. +La [vérification Vulkan antérieure](docs/shader-vulkan-beta138.md) reste disponible. La [livraison précédente](docs/clay-workshop-integration-beta110.md) réunit déjà l’atelier, les œufs et la neige saisonnière dans l’instance **Sanctuary Beta**. Les [synchronisations antérieures](docs/git-sync-beta092.md) restent documentées. @@ -37,6 +35,14 @@ La [carte native beta.005](docs/atlas-beta005.md) prend le relais après l’étude de Xaero et des alternatives libres. +## beta.139 — Lumières colorées + +Teintes chaudes, sources des âmes bleues, redstone rouge, froglights et matériaux lumineux. +Option désactivée initialement, intensité 35 % ; propagation locale avec obstacles. +[Contrat et vérifications](docs/colored-lights-beta139.md) · +[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.139/Sanctuary-beta.139.mrpack) · +[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.139/Sanctuary-Test-beta.139.mrpack). + ## beta.138 — Rayons et brouillard Densité automatique des rayons selon le brouillard et les précipitations. diff --git a/docs/colored-lights-beta139.md b/docs/colored-lights-beta139.md new file mode 100644 index 0000000..c162725 --- /dev/null +++ b/docs/colored-lights-beta139.md @@ -0,0 +1,95 @@ +# LIGHT-139 — Lumières colorées + +Socle beta.138 + vérification Vulkan `9d84a02`, Minecraft 26.3 / Java 25. +Branche `codex/colored-lights-beta139`, livraison beta.139. + +## Contrat + +Option locale « Lumières colorées » avec intensité. Les vrais blocs émetteurs +teintent discrètement leurs environs, y compris hors champ. Torches/lanternes, +lave et feu chauds ; sources des âmes bleues ; redstone active rouge ; +froglights par variante ; sources Sanctuary selon leur texture/matière. +Les blocs éteints n'émettent pas. Le bloom et les inclusions émissives des +minerais restent indépendants ; aucune lumière de gameplay n'est ajoutée. + +Propagation RGB dans un volume local de blocs chargés, obstacles et formes +natives, construction répartie entre les images, cache des couleurs de +matériaux. Aucun chargement forcé de chunk, accès aux mondes personnels, +changement de sauvegarde, réseau ou règles serveur. + +## Réalisation + +Dans les options du shader : « Lumières colorées », désactivées initialement, +et une intensité de 0 à 100 %, réglée à 35 %. Zéro ou OFF libère les ressources. +Les préférences existantes sont conservées. Libellés et aides FR/EN. + +Les familles vanilla demandées ont une palette dédiée. Les autres émetteurs, +y compris ceux de Sanctuary, utilisent la couleur des pixels lumineux de leurs +textures de modèle, calculée puis mise en cache. Ce repli suit les packs de +ressources ; les palettes dédiées vanilla restent constantes. Une texture +illisible ou sans pixels exploitables utilise du blanc. Les blocs purement +émissifs du bloom, sans émission lumineuse native, ne deviennent pas des lampes. + +Le champ couvre un cube de 64 blocs de côté autour de la caméra. Une marge de +14 blocs réserve la propagation aux sources présentes dans le volume ; le rendu +s'atténue progressivement sur 4 blocs à sa périphérie. C'est donc un effet de +proximité, pas une illumination de tous les chunks visibles. Les formes et +l'opacité natives règlent le passage de la lumière. Les canaux utilisent des +maxima indépendants, sans addition illimitée des sources superposées. + +Le calcul CPU est réparti avec un budget visé de 2 ms par image, plus les +allocations et chargements ponctuels de matériaux ; aucun chiffre de FPS n'est +garanti. Un monde immobile réutilise son champ. Les changements rapides et les +déplacements peuvent présenter un bref retard, le temps de publier un champ +complet. Aucun chargement forcé de chunk. Une texture 512 × 512 contient le champ. +La passe GPU teinte les surfaces visibles, atténue l'effet au soleil et dans le +brouillard, puis laisse agir le SSGI et le bloom. Les sources tenues, entités, +particules et le rendu sous l'eau/lave ne sont pas couverts par cette option. + +## Vérifications + +Essais client natifs réussis dans des mondes neufs (graine 122) sur Apple M1 : + +- OpenGL 4.1 Metal : **1 min 46 s**, `build/colored139-opengl.log`. +- Vulkan / MoltenVK 1.4.2, profondeur 0–1 et transparence améliorée : + **1 min 30 s**, `build/colored139-vulkan.log`. + +Les deux journaux contiennent `COLORED139_PASS` et le backend réellement actif. +Torche et torche des âmes hors champ, trois froglights, pierre lumineuse dont +la couleur vient de sa texture, intensité 35/100, cache statique, déplacement +et retour de caméra, mur fermé puis ouvert, lampe redstone allumée puis éteinte, +retrait de source, rechargement des ressources et OFF/zéro vérifiés. +Les préférences persistantes et les libellés FR/EN sont contrôlés ; une dernière +capture active conjointement PBR, SSGI, bloom, ombres, highlights et rayons. + +Les sommes d'écart entre OFF et 35 % sont presque identiques entre backends : +torche 2 025 577 / 2 025 576 ; âmes 964 200 / 964 201. Il s'agit de mesures +sur les pixels de cette scène, pas d'un indice de performance. +Comparatif autonome : `build/Shader-beta.139-comparaison.html`. +Pas de validation du pilote Vulkan Windows de l'utilisateur revendiquée. +Le test fige uniquement le scintillement vanilla des torches afin de comparer +les images ; cette sonde est exclue du JAR distribué. Aucun monde personnel ouvert. + +## Diagnostic des essais + +La première comparaison de stabilité détectait le scintillement natif de la +lightmap des torches, même avec un champ coloré inchangé. La sonde de test +stabilise ce seul facteur ; aucune désactivation de scintillement en production. +La pièce d'essai est désormais construite après le chargement de ses chunks, +et l'absence de lumière du ciel est vérifiée avant les captures. + +## Construction et archives + +`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest` +réussit en **2 min 22 s**, 126 tâches. Le serveur GameTest dédié reste exclu +conformément au refus EULA antérieur. Log : `build/shader139-build.log`. +La comparaison à beta.138 trouve **13 entrées de production** modifiées, +limitées au shader, à son menu et aux aides FR/EN. Les sondes de test sont +absentes du JAR distribué. Sources, archives normale/Test et template complets +vérifiés ; textures des gemmes et clé de Steve conservées à l'identique. +Reçu local : `build/shader139-artifact.json`. + +JAR SHA-256 : `7b958f6576adbabb2b1af589b866cb52b1a65600ea2fa505b05dd283f8d595df`. + +- `Sanctuary-beta.139.mrpack` : 10450317 octets ; SHA-256 `13f3a283c260876d28ce0c6504615b4a8f4d575906badfbb2b39558f312e9fbb`. +- `Sanctuary-Test-beta.139.mrpack` : 10469244 octets ; SHA-256 `c1e0b684dc2650950a0925c4625ef5eb4b78fb5f1c5680f5e302550424fa6d81`. diff --git a/gradle.properties b/gradle.properties index 7503cac..046a179 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,8 +9,8 @@ loom_version=1.17.20 fabric_api_version=0.160.5+26.3 # Release counter: beta.001, .002, .003, ... (see docs/versioning.md). -mod_version=beta.138 -pack_version=beta.138 +mod_version=beta.139 +pack_version=beta.139 resource_pack_version=beta.121 maven_group=fr.koka.sanctuary jei_version=30.32.0-sanctuary.3 diff --git a/mods/sanctuary/build.gradle b/mods/sanctuary/build.gradle index 775a42f..5129128 100644 --- a/mods/sanctuary/build.gradle +++ b/mods/sanctuary/build.gradle @@ -102,6 +102,10 @@ tasks.named('runGameTest') { } tasks.named('processGametestResources') { + def colored139Client = providers.gradleProperty('sanctuaryColored139ClientTests').map { it.toBoolean() }.getOrElse(false) + inputs.property('sanctuaryColored139ClientTests', colored139Client) + if (colored139Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.shader.ColoredLights139ClientChecks') } + def pbr134Client = providers.gradleProperty('sanctuaryPbr134ClientTests').map { it.toBoolean() }.getOrElse(false) inputs.property('sanctuaryPbr134ClientTests', pbr134Client) if (pbr134Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.shader.Pbr134ClientChecks') } diff --git a/mods/sanctuary/src/clientGametest/java/fr/koka/sanctuary/client/shader/ColoredLights139ClientChecks.java b/mods/sanctuary/src/clientGametest/java/fr/koka/sanctuary/client/shader/ColoredLights139ClientChecks.java new file mode 100644 index 0000000..3564b67 --- /dev/null +++ b/mods/sanctuary/src/clientGametest/java/fr/koka/sanctuary/client/shader/ColoredLights139ClientChecks.java @@ -0,0 +1,80 @@ +package fr.koka.sanctuary.client.shader; + +import static fr.koka.sanctuary.client.shader.PixelShadows122ClientChecks.*; +import java.nio.file.Files; +import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest; +import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext; +import net.fabricmc.fabric.api.client.gametest.v1.context.TestServerContext; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.Block; +import net.minecraft.network.chat.Component; + +/** Fresh native world: offscreen sources, true voxel occlusion and real GPU tint. */ +public final class ColoredLights139ClientChecks implements FabricClientGameTest { + private static final BlockPos SOURCE=new BlockPos(98,101,102),PROBE=new BlockPos(99,101,106); + public void runTest(ClientGameTestContext c){ + System.setProperty("sanctuary.test.freezeBlockFlicker","true"); + try{new PixelShadows122ClientChecks(false,false,false,false,false,true).runTest(c);} + finally{System.clearProperty("sanctuary.test.freezeBlockFlicker");} + } + static void run(ClientGameTestContext c,TestServerContext server){ + c.runOnClient(m->{preferences();m.options.particles().set(net.minecraft.server.level.ParticleStatus.MINIMAL);VanillaLight.setEnabled(true);VanillaLight.setSaturation(100);VanillaLight.setPbr(false);VanillaLight.setSsgi(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);VanillaLight.setLensFlare(false);VanillaLight.setEdgeHighlights(false);VanillaLight.setPixelShadows(false);VanillaLight.setSunshafts(false);VanillaLight.setColoredLights(false);VanillaLight.setColoredLightIntensity(35);}); + camera(c,server,100.5,101,104.5,70);c.waitTicks(30); + server.runOnServer(s->{command(s,"time set 18000"); + for(int x=88;x<=112;x++)for(int z=92;z<=116;z++)for(int y=100;y<=109;y++) + s.overworld().setBlockAndUpdate(new BlockPos(x,y,z),(y==100||y==109||x==88||x==112||z==92||z==116)?Blocks.CONCRETE.white().defaultBlockState():Blocks.AIR.defaultBlockState()); + }); + look(c,server,0,35);c.waitTicks(30); + c.waitFor(m->m.level.getBrightness(net.minecraft.world.level.LightLayer.SKY,PROBE)==0,1200); + long warm=0; + for(var block:new Block[]{Blocks.TORCH,Blocks.SOUL_TORCH,Blocks.PEARLESCENT_FROGLIGHT,Blocks.VERDANT_FROGLIGHT,Blocks.OCHRE_FROGLIGHT,Blocks.GLOWSTONE}){ + source(c,server,block);enable(c,false);var off=capture(c,"colored-"+name(block)+"-off");enable(c,true);var on=capture(c,"colored-"+name(block)+"-35"); + int color=c.computeOnClient(m->ColoredLights.sample(PROBE));long delta=difference(off,on); + check(delta>250,"Offscreen source visibly tints native surfaces: "+name(block)+" "+delta); + int r=color>>16&255,g=color>>8&255,b=color&255; + if(block==Blocks.TORCH){check(r>g&&g>b,"Torch is warm");warm=delta;} + if(block==Blocks.SOUL_TORCH)check(b>g&&g>r,"Soul source is blue"); + if(block==Blocks.VERDANT_FROGLIGHT)check(g>r&&g>b,"Verdant froglight is green"); + if(block==Blocks.PEARLESCENT_FROGLIGHT)check(r>g&&b>g,"Pearlescent froglight is pink"); + if(block==Blocks.OCHRE_FROGLIGHT)check(r>g&&g>b,"Ochre froglight is warm"); + if(block==Blocks.GLOWSTONE)check(r>b,"Other emitters derive their colour from the native texture"); + System.out.println("COLORED139_SOURCE "+name(block)+" sample="+Integer.toHexString(color)+" delta="+delta); + } + source(c,server,Blocks.TORCH);enable(c,false);var base=capture(c,null);enable(c,true);var low=capture(c,null); + c.runOnClient(m->VanillaLight.setColoredLightIntensity(100));settle(c);var strong=capture(c,"colored-torch-100");check(difference(base,strong)>difference(base,low)*1.5,"Intensity increases colour, without changing native light"); + int builds=c.computeOnClient(m->ColoredLights.builds());c.waitTicks(25);check(c.computeOnClient(m->ColoredLights.builds())==builds,"Stationary volume is cached");unchanged(strong,capture(c,null),"Static light is stable"); + camera(c,server,104.5,101,104.5,70);look(c,server,0,35);settle(c);capture(c,"colored-moved"); + camera(c,server,100.5,101,104.5,70);look(c,server,0,35);settle(c);unchanged(strong,capture(c,"colored-returned"),"Moving and returning preserves world-anchored light"); + // A complete divider closes the room; light may not take a path around its edges. + server.runOnServer(s->{for(int y=101;y<109;y++)for(int z=93;z<116;z++)s.overworld().setBlockAndUpdate(new BlockPos(99,y,z),Blocks.STONE.defaultBlockState());});c.waitTicks(35);settle(c); + check((c.computeOnClient(m->ColoredLights.sample(new BlockPos(100,101,106)))&0xffffff)==0,"Opaque wall blocks colour propagation"); + enable(c,false);var sealedOff=capture(c,null);enable(c,true);unchanged(sealedOff,capture(c,"colored-sealed"),"No coloured light crosses a sealed wall"); + server.runOnServer(s->{for(int y=101;y<109;y++)for(int z=93;z<116;z++)s.overworld().setBlockAndUpdate(new BlockPos(99,y,z),Blocks.AIR.defaultBlockState());});c.waitTicks(35);settle(c); + check((c.computeOnClient(m->ColoredLights.sample(PROBE))&0xffffff)!=0,"Opening the wall restores light"); + server.runOnServer(s->{s.overworld().setBlockAndUpdate(SOURCE,Blocks.REDSTONE_LAMP.defaultBlockState());s.overworld().setBlockAndUpdate(SOURCE.below(),Blocks.REDSTONE_BLOCK.defaultBlockState());});c.waitTicks(35);settle(c); + int red=c.computeOnClient(m->ColoredLights.sample(PROBE));check((red>>16&255)>(red>>8&255)&& (red>>16&255)>(red&255),"Powered redstone source is red");capture(c,"colored-redstone-on"); + server.runOnServer(s->s.overworld().setBlockAndUpdate(SOURCE.below(),Blocks.CONCRETE.white().defaultBlockState()));c.waitTicks(35);settle(c); + check((c.computeOnClient(m->ColoredLights.sample(PROBE))&0xffffff)==0,"Unpowered lamp removes cached coloured light"); + source(c,server,Blocks.SOUL_TORCH);var beforeReload=capture(c,null); + var reload=c.computeOnClient(net.minecraft.client.Minecraft::reloadResourcePacks);c.waitFor(m->reload.isDone()&&m.gui.overlay()==null,1200);reload.join();settle(c);unchanged(beforeReload,capture(c,"colored-reloaded"),"Resource reload rebuilds materials and light field"); + source(c,server,Blocks.AIR);check((c.computeOnClient(m->ColoredLights.sample(PROBE))&0xffffff)==0,"Removing the source clears its light"); + source(c,server,Blocks.SOUL_TORCH);enable(c,false);var off=capture(c,null);c.runOnClient(m->{VanillaLight.setColoredLights(true);VanillaLight.setColoredLightIntensity(0);});c.waitTicks(10); + c.runOnClient(m->check(!ColoredLights.allocated(),"Zero releases volume and GPU resources"));unchanged(off,capture(c,null),"Zero is identical to OFF"); + c.runOnClient(m->{for(String locale:new String[]{"fr_fr","en_us"}){m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());for(String key:new String[]{"colored_lights","colored_lights_help","colored_light_intensity","colored_light_intensity_help"}){String full="sanctuary.shaders."+key;check(!Component.translatable(full).getString().equals(full),"Translated "+locale+" "+full);}}}); + c.runOnClient(m->{VanillaLight.setColoredLightIntensity(35);VanillaLight.setPbr(true);VanillaLight.setSsgi(true);VanillaLight.setBloom(true);VanillaLight.setBloomIntensity(20);VanillaLight.setEdgeHighlights(true);VanillaLight.setPixelShadows(true);VanillaLight.setSunshafts(true);VanillaLight.setLensFlare(true);VanillaLight.setOreEmission(true);}); + settle(c);c.waitFor(m->ProceduralPbr.applied()&&Bloom.applied(),1200);c.waitTicks(30);capture(c,"colored-with-shader-effects"); + System.out.println("COLORED139_PASS warm="+warm+" offscreen sources, hues, native textured materials, intensity, cache, camera, walls, lamp updates, removal, reload, zero/OFF and FR/EN"); + } + private static String name(Block b){return net.minecraft.core.registries.BuiltInRegistries.BLOCK.getKey(b).getPath();} + private static void source(ClientGameTestContext c,TestServerContext s,Block b){s.runOnServer(server->server.overworld().setBlockAndUpdate(SOURCE,b.defaultBlockState()));c.waitTicks(35);if(c.computeOnClient(m->VanillaLight.coloredLights()))settle(c);} + private static void enable(ClientGameTestContext c,boolean value){c.runOnClient(m->VanillaLight.setColoredLights(value));if(value)settle(c);else c.waitTicks(8);} + private static void settle(ClientGameTestContext c){int before=c.computeOnClient(m->ColoredLights.renderedFrames());c.waitFor(m->ColoredLights.applied()&&ColoredLights.settled()&&ColoredLights.renderedFrames()>before+8,1600);c.waitTicks(8);} + private static long difference(Frame a,Frame b){long sum=0;for(int i=0;i check(VanillaLight.active(), "Native Sanctuary shader is active without an external engine")); float morning = c.computeOnClient(m -> m.gameRenderer.gameRenderState().levelRenderState.skyRenderState.sunAngle); + if(coloredOnly){ColoredLights139ClientChecks.run(c,server);return;} if(sunshaftOnly){Sunshafts137ClientChecks.run(c,server);return;} if(closeupOnly){SsgiCloseup135ClientChecks.run(c,server);Pbr134ClientChecks.run(c,server);return;} if(pbrOnly){Pbr134ClientChecks.run(c,server);return;} @@ -279,7 +281,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest { }); }); c.waitTicks(3); - c.takeScreenshot("sanctuary-beta138-shader-options-" + locale); + c.takeScreenshot("sanctuary-beta139-shader-options-" + locale); } c.runOnClient(m -> m.gui.setScreen(null)); } @@ -563,7 +565,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest { } static Frame capture(ClientGameTestContext c, String name) { - if (name != null) c.takeScreenshot("sanctuary-beta138-shader-" + name); + if (name != null) c.takeScreenshot("sanctuary-beta139-shader-" + name); var result = new CompletableFuture(); c.runOnClient(m -> Screenshot.takeScreenshot(m.gameRenderer.mainRenderTarget(), image -> { try (image) { diff --git a/mods/sanctuary/src/gametest/java/fr/koka/sanctuary/gametest/mixin/Colored139LightmapProbeMixin.java b/mods/sanctuary/src/gametest/java/fr/koka/sanctuary/gametest/mixin/Colored139LightmapProbeMixin.java new file mode 100644 index 0000000..f7242f8 --- /dev/null +++ b/mods/sanctuary/src/gametest/java/fr/koka/sanctuary/gametest/mixin/Colored139LightmapProbeMixin.java @@ -0,0 +1,17 @@ +package fr.koka.sanctuary.gametest.mixin; + +import net.minecraft.client.renderer.Lightmap; +import net.minecraft.client.renderer.state.LightmapRenderState; +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; + +/** Freeze only vanilla torch flicker for image comparisons; never packaged in the mod. */ +@Mixin(Lightmap.class) +public abstract class Colored139LightmapProbeMixin { + @Inject(method="render",at=@At("HEAD")) + private void stableBlockLight(LightmapRenderState state,CallbackInfo ci){ + if(Boolean.getBoolean("sanctuary.test.freezeBlockFlicker")){state.blockFactor=1;state.needsUpdate=true;} + } +} diff --git a/mods/sanctuary/src/gametest/resources/sanctuary-gametest.mixins.json b/mods/sanctuary/src/gametest/resources/sanctuary-gametest.mixins.json index 49a258f..f637543 100644 --- a/mods/sanctuary/src/gametest/resources/sanctuary-gametest.mixins.json +++ b/mods/sanctuary/src/gametest/resources/sanctuary-gametest.mixins.json @@ -10,6 +10,7 @@ "defaultRequire": 1 }, "client": [ + "Colored139LightmapProbeMixin", "Fourneau094ParticleProbeMixin", "Tooltip093FontProbeMixin", "ClientVsync046Mixin", diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/shader/ColoredLightMaterials.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/shader/ColoredLightMaterials.java new file mode 100644 index 0000000..b14a2d6 --- /dev/null +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/shader/ColoredLightMaterials.java @@ -0,0 +1,58 @@ +package fr.koka.sanctuary.client.shader; + +import java.util.*; +import net.minecraft.client.Minecraft; +import net.minecraft.core.Direction; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.Identifier; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.client.renderer.block.dispatch.BlockStateModelPart; + +/** Colours are analysed once per state/resource reload, never from rendered GPU pixels. */ +final class ColoredLightMaterials { + private final Map colors=new IdentityHashMap<>(); + private final Map sprites=new HashMap<>(); + int color(BlockState state){return colors.computeIfAbsent(state,this::analyse);} + private int analyse(BlockState state){ + var id=BuiltInRegistries.BLOCK.getKey(state.getBlock());String name=id.getPath(); + if(id.getNamespace().equals("minecraft")){ + if(name.startsWith("soul_"))return 0x55BDFF; + if(name.contains("redstone"))return 0xFF3020; + if(name.equals("pearlescent_froglight"))return 0xFFC0EC; + if(name.equals("verdant_froglight"))return 0xB4FF82; + if(name.equals("ochre_froglight"))return 0xFFCD72; + if(name.contains("torch")||name.equals("lantern")||name.equals("fire")||name.equals("campfire")||name.equals("lava"))return 0xFFAB55; + } + var parts=new ArrayList(); + Minecraft.getInstance().getModelManager().getBlockStateModelSet().get(state).collectParts(RandomSource.create(0),parts); + var used=new HashSet();long r=0,g=0,b=0;int count=0; + for(var part:parts)for(int f=0;f<7;f++)for(var quad:part.getQuads(f==6?null:Direction.values()[f])){ + var sprite=quad.materialInfo().sprite().contents().name();if(!used.add(sprite))continue; + int color=sprites.computeIfAbsent(sprite,this::sample);if(color<0)continue; + r+=color>>16&255;g+=color>>8&255;b+=color&255;count++; + } + if(count==0)return 0xFFFFFF; + return normalize((float)r/count,(float)g/count,(float)b/count); + } + private int sample(Identifier id){ + try(var image=BloomMaterials.read(id)){ + double r=0,g=0,b=0,weight=0; + // First animation frame; bounded sampling also covers high-resolution packs. + int w=image.getWidth(),h=Math.min(w,image.getHeight()),step=Math.max(1,Math.max(w,h)/32); + for(int y=0;y>>24)<128)continue; + int cr=c>>16&255,cg=c>>8&255,cb=c&255,max=Math.max(cr,Math.max(cg,cb)),min=Math.min(cr,Math.min(cg,cb)); + double k=Math.max(0,(max-96)/159.0);k*=k*(.2+(max-min)/255.0); + r+=cr*k;g+=cg*k;b+=cb*k;weight+=k; + } + return weight<.001?-1:normalize((float)(r/weight),(float)(g/weight),(float)(b/weight)); + }catch(java.io.IOException e){return -1;} + } + private static int normalize(float r,float g,float b){ + float max=Math.max(1,Math.max(r,Math.max(g,b))); + return Math.round(r/max*255)<<16|Math.round(g/max*255)<<8|Math.round(b/max*255); + } + void clear(){colors.clear();sprites.clear();} + int cachedStates(){return colors.size();} +} diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/shader/ColoredLightVolume.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/shader/ColoredLightVolume.java new file mode 100644 index 0000000..3ddd9cf --- /dev/null +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/shader/ColoredLightVolume.java @@ -0,0 +1,116 @@ +package fr.koka.sanctuary.client.shader; + +import com.mojang.blaze3d.platform.NativeImage; +import it.unimi.dsi.fastutil.ints.IntArrayFIFOQueue; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.renderer.texture.DynamicTexture; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.LightLayer; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.status.ChunkStatus; + +/** Client-only RGB propagation. Snapshot, flood and packing share a bounded per-frame work budget. */ +final class ColoredLightVolume implements AutoCloseable { + static final int SIDE=64,COUNT=SIDE*SIDE*SIDE,ATLAS=512; + private static final Direction[] DIRECTIONS=Direction.values(); + private final ColoredLightMaterials materials=new ColoredLightMaterials(); + private ClientLevel level; + private Object models; + private Build build; + private DynamicTexture texture; + private BlockPos origin; + private long revision,published=-1,lastStart; + private int completed; + void update(){ + var mc=Minecraft.getInstance();var modelSet=mc.getModelManager().getBlockStateModelSet(); + if(level!=mc.level||models!=modelSet){close();level=mc.level;models=modelSet;} + var p=mc.gameRenderer.gameRenderState().levelRenderState.cameraRenderState.pos; + var wanted=new BlockPos(anchor(p.x),anchor(p.y),anchor(p.z)); + long now=System.nanoTime(); + // Complete a snapshot even if the world keeps changing, then catch up; moving + // beyond it cancels immediately. Static worlds do no further scanning or flooding. + if(build!=null&&!inside(build.origin,BlockPos.containing(p))){build.close();build=null;} + if(build==null&&(texture==null||!wanted.equals(origin)||published!=revision)&&now-lastStart>=100_000_000L){ + build=new Build(level,wanted,revision,materials);lastStart=now; + } + if(build!=null&&build.advance(now+2_000_000L)){ + if(texture==null)texture=new DynamicTexture(()->"Sanctuary coloured light volume",build.takeImage()); + else {texture.setPixels(build.takeImage());texture.upload();} + origin=build.origin;published=build.revision;build.close();build=null;completed++; + } + } + private static int anchor(double p){return Math.floorDiv((int)Math.floor(p),4)*4-SIDE/2;} + private static boolean inside(BlockPos base,BlockPos p){return p.getX()>=base.getX()&&p.getX()=base.getY()&&p.getY()=base.getZ()&&p.getZ()=(o.getY()>>4)&&y<=((o.getY()+SIDE-1)>>4);} + private static boolean overlaps(BlockPos o,int x,int z){return o!=null&&x>=(o.getX()>>4)&&x<=((o.getX()+SIDE-1)>>4)&&z>=(o.getZ()>>4)&&z<=((o.getZ()+SIDE-1)>>4);} + DynamicTexture texture(){return texture;} + BlockPos origin(){return origin;} + boolean settled(){return texture!=null&&build==null&&revision==published;} + int completed(){return completed;} + int cachedStates(){return materials.cachedStates();} + int sample(BlockPos p){if(texture==null||!inside(origin,p))return 0;int x=p.getX()-origin.getX(),y=p.getY()-origin.getY(),z=p.getZ()-origin.getZ();return texture.getPixels().getPixel(x+(y&7)*SIDE,z+(y>>3)*SIDE);} + public void close(){if(build!=null){build.close();build=null;}if(texture!=null){texture.close();texture=null;}materials.clear();level=null;models=null;origin=null;published=-1;revision=0;lastStart=0;} + + private static final class Build implements AutoCloseable { + final ClientLevel level;final BlockPos origin;final long revision;final ColoredLightMaterials materials; + final BlockState[] states=new BlockState[COUNT]; + final byte[] red=new byte[COUNT],green=new byte[COUNT],blue=new byte[COUNT],sky=new byte[COUNT]; + final boolean[] queued=new boolean[COUNT];final IntArrayFIFOQueue queue=new IntArrayFIFOQueue(); + final LevelChunk[] chunks=new LevelChunk[25];final int cx,cz; + final BlockPos.MutableBlockPos pos=new BlockPos.MutableBlockPos(); + NativeImage image=new NativeImage(ATLAS,ATLAS,false);int scan,packed; + Build(ClientLevel level,BlockPos origin,long revision,ColoredLightMaterials materials){ + this.level=level;this.origin=origin;this.revision=revision;this.materials=materials; + cx=origin.getX()>>4;cz=origin.getZ()>>4; + for(int z=0;z<5;z++)for(int x=0;x<5;x++)chunks[x+z*5]=level.getChunkSource().getChunk(cx+x,cz+z,ChunkStatus.FULL,false); + } + boolean advance(long deadline){ + while(scan=deadline)return false;} + int work=0; + while(!queue.isEmpty()){ + int i=queue.dequeueInt();queued[i]=false;spread(i); + if((++work&127)==0&&System.nanoTime()>=deadline)return false; + } + while(packed>6&63,y=i>>12; + image.setPixel(x+(y&7)*SIDE,z+(y>>3)*SIDE,(sky[i]&255)<<24|(red[i]&255)<<16|(green[i]&255)<<8|(blue[i]&255)); + if((packed&511)==0&&System.nanoTime()>=deadline)return false; + } + return true; + } + void scan(int i){ + int x=origin.getX()+(i&63),z=origin.getZ()+(i>>6&63),y=origin.getY()+(i>>12);pos.set(x,y,z); + var chunk=chunks[((x>>4)-cx)+((z>>4)-cz)*5]; + var state=chunk==null||level.isOutsideBuildHeight(y)?Blocks.BEDROCK.defaultBlockState():chunk.getBlockState(pos);states[i]=state; + if(chunk!=null&&!level.isOutsideBuildHeight(y))sky[i]=(byte)(level.getBrightness(LightLayer.SKY,pos)*17); + int emission=state.getLightEmission();if(emission<=0)return; + int c=materials.color(state);red[i]=(byte)encode(emission,c>>16&255);green[i]=(byte)encode(emission,c>>8&255);blue[i]=(byte)encode(emission,c&255);enqueue(i); + } + void spread(int i){ + int r=red[i]&255,g=green[i]&255,b=blue[i]&255;if(Math.max(r,Math.max(g,b))<=16)return; + int x=i&63,z=i>>6&63,y=i>>12; + for(var d:DIRECTIONS){ + int nx=x+d.getStepX(),ny=y+d.getStepY(),nz=z+d.getStepZ();if(nx<0||nx>=SIDE||ny<0||ny>=SIDE||nz<0||nz>=SIDE)continue; + int n=nx+(nz<<6)+(ny<<12); + if(r-16<=(red[n]&255)&&g-16<=(green[n]&255)&&b-16<=(blue[n]&255))continue; + int loss=net.minecraft.world.level.lighting.LightEngine.getLightDampeningInto(states[i],states[n],d,Math.max(1,states[n].getLightDampening()))*16; + int nr=Math.max(0,r-loss),ng=Math.max(0,g-loss),nb=Math.max(0,b-loss);boolean changed=false; + if(nr>(red[n]&255)){red[n]=(byte)nr;changed=true;}if(ng>(green[n]&255)){green[n]=(byte)ng;changed=true;}if(nb>(blue[n]&255)){blue[n]=(byte)nb;changed=true;} + if(changed)enqueue(n); + } + } + void enqueue(int i){if(!queued[i]){queued[i]=true;queue.enqueue(i);}} + NativeImage takeImage(){var result=image;image=null;return result;} + public void close(){if(image!=null){image.close();image=null;}} + } + // Logarithmic channels keep the source hue as light loses one native level per + // block. Independent maxima mix overlapping colours without summing unbounded light. + static int encode(int emission,int channel){return channel==0?0:Math.clamp((int)Math.round((emission+Math.log(channel/255.0)/Math.log(1.25))*16),0,240);} +} diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/shader/ColoredLights.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/shader/ColoredLights.java new file mode 100644 index 0000000..c00144a --- /dev/null +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/shader/ColoredLights.java @@ -0,0 +1,78 @@ +package fr.koka.sanctuary.client.shader; + +import com.mojang.blaze3d.buffers.Std140Builder; +import com.mojang.blaze3d.pipeline.TextureTarget; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.renderpearl.api.GpuFormat; +import com.mojang.renderpearl.api.buffers.GpuBuffer; +import com.mojang.renderpearl.api.pipeline.*; +import com.mojang.renderpearl.api.textures.FilterMode; +import fr.koka.sanctuary.client.panorama.PanoramaCapture; +import java.util.Optional; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.MappableRingBuffer; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.core.BlockPos; +import net.minecraft.resources.Identifier; +import net.minecraft.world.level.material.FogType; +import org.joml.Matrix4f; + +/** Visual local colour only: native block/sky light and gameplay remain unchanged. */ +public final class ColoredLights { + private static final RenderPipeline APPLY=RenderPipelines.register(RenderPipeline.builder() + .withLocation(Identifier.parse("sanctuary:pipeline/colored_lights")) + .withVertexShader("core/screenquad").withFragmentShader(Identifier.parse("sanctuary:core/colored_lights")) + .withBindGroupLayout(BindGroupLayout.builder().withUniform("ColoredLightScene",UniformType.UNIFORM_BUFFER) + .withUniform("WorldDepth",UniformType.COMBINED_IMAGE_SAMPLER) + .withUniform("SceneColor",UniformType.COMBINED_IMAGE_SAMPLER) + .withUniform("LightVolume",UniformType.COMBINED_IMAGE_SAMPLER).build()) + .withPrimitiveTopology(PrimitiveTopology.TRIANGLES).withCull(false) + .withColorTargetState(new ColorTargetState(Optional.of(new BlendFunction(BlendFactor.ONE,BlendFactor.ZERO,BlendFactor.ZERO,BlendFactor.ONE)),GpuFormat.RGBA8_UNORM,ColorTargetState.WRITE_COLOR)) + .withDepthStencilState(Optional.empty()).build()); + private static final ColoredLightVolume VOLUME=new ColoredLightVolume(); + private static final Matrix4f projection=new Matrix4f(); + private static TextureTarget source; + private static MappableRingBuffer uniforms; + private static boolean applied; + private static int frames; + private ColoredLights(){} + public static void captureProjection(Matrix4f matrix){projection.set(matrix);} + public static void dirtyBlock(BlockPos pos){VOLUME.dirty(pos);} + public static void dirtyChunk(int x,int z){VOLUME.dirtyChunk(x,z);} + public static void dirtySection(int x,int y,int z){VOLUME.dirtySection(x,y,z);} + public static void render(){ + applied=false;var mc=Minecraft.getInstance(); + if(!VanillaLight.active()||!VanillaLight.coloredLights()||VanillaLight.coloredLightIntensity()==0||mc.level==null){release();return;} + if(PanoramaCapture.rendering())return; + var camera=mc.gameRenderer.gameRenderState().levelRenderState.cameraRenderState; + if(camera.fogType!=FogType.NONE)return; + VOLUME.update();if(VOLUME.texture()==null)return; + var main=mc.gameRenderer.mainRenderTarget(); + if(source==null||source.width!=main.width||source.height!=main.height){if(source!=null)source.destroyBuffers();source=new TextureTarget("Sanctuary coloured light source",main.width,main.height,GpuFormat.RGBA8_UNORM,null);} + if(uniforms==null)uniforms=new MappableRingBuffer(()->"Sanctuary coloured light scene",GpuBuffer.USAGE_UNIFORM|GpuBuffer.USAGE_MAP_WRITE,128); + var base=VOLUME.origin();var fog=camera.fogData; + uniforms.rotate();try(var mapped=uniforms.currentBuffer().map(false,true)){ + Std140Builder.intoBuffer(mapped.data()).putMat4f(new Matrix4f(projection).mul(camera.viewRotationMatrix).invert()) + .putVec4((float)(camera.pos.x-base.getX()),(float)(camera.pos.y-base.getY()),(float)(camera.pos.z-base.getZ()),VanillaLight.coloredLightIntensity()/100f) + .putVec4(RenderSystem.getDevice().getDeviceInfo().isZZeroToOne()?1:0,Math.clamp((15-mc.level.getSkyDarken())/15f,0,1),fog.color.w,0) + .putVec4(fog.renderDistanceStart,fog.renderDistanceEnd,fog.environmentalStart,fog.environmentalEnd) + .putVec4(ColoredLightVolume.SIDE,14,4,0); + } + var encoder=RenderSystem.getDevice().createCommandEncoder(); + encoder.copyTextureToTexture(main.getColorTexture(),source.getColorTexture(),0,0,0,0,0,main.width,main.height); + var nearest=RenderSystem.getSamplerCache().getClampToEdge(FilterMode.NEAREST); + try(var pass=encoder.createRenderPass(()->"Sanctuary coloured light",main.getColorTextureView(),Optional.empty())){ + pass.setPipeline(RenderSystem.getCompiledPipeline(APPLY));pass.setUniform("ColoredLightScene",uniforms.currentBuffer()); + pass.setUniform("WorldDepth",main.getDepthTextureView(),nearest);pass.setUniform("SceneColor",source.getColorTextureView(),nearest);pass.setUniform("LightVolume",VOLUME.texture().getTextureView(),nearest);pass.draw(3,1,0,0); + } + applied=true;frames++; + } + public static void release(){VOLUME.close();if(source!=null){source.destroyBuffers();source=null;}if(uniforms!=null){uniforms.close();uniforms=null;}applied=false;} + static boolean applied(){return applied;} + static boolean allocated(){return source!=null||VOLUME.texture()!=null;} + static boolean settled(){return VOLUME.settled();} + static int renderedFrames(){return frames;} + static int builds(){return VOLUME.completed();} + static int sample(BlockPos pos){return VOLUME.sample(pos);} + static int materials(){return VOLUME.cachedStates();} +} diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/shader/ShaderOptionsScreen.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/shader/ShaderOptionsScreen.java index ba3af83..cc36ffe 100644 --- a/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/shader/ShaderOptionsScreen.java +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/shader/ShaderOptionsScreen.java @@ -23,6 +23,9 @@ public final class ShaderOptionsScreen extends OptionsSubScreen { list.addBig(OptionInstance.createBoolean("sanctuary.shaders.pbr",OptionInstance.cachedConstantTooltip( Component.translatable("sanctuary.shaders.pbr_help")),VanillaLight.pbr(),VanillaLight::setPbr)); slider("pbr_intensity",VanillaLight.pbrIntensity(),VanillaLight::setPbrIntensity); + list.addBig(OptionInstance.createBoolean("sanctuary.shaders.colored_lights",OptionInstance.cachedConstantTooltip( + Component.translatable("sanctuary.shaders.colored_lights_help")),VanillaLight.coloredLights(),VanillaLight::setColoredLights)); + slider("colored_light_intensity",VanillaLight.coloredLightIntensity(),VanillaLight::setColoredLightIntensity); list.addBig(OptionInstance.createBoolean("sanctuary.shaders.ssgi",OptionInstance.cachedConstantTooltip( Component.translatable("sanctuary.shaders.ssgi_help")),VanillaLight.ssgi(),VanillaLight::setSsgi)); slider("ssgi_intensity",VanillaLight.ssgiIntensity(),VanillaLight::setSsgiIntensity); diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/shader/VanillaLight.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/shader/VanillaLight.java index 4ddf21f..9056f7a 100644 --- a/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/shader/VanillaLight.java +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/shader/VanillaLight.java @@ -26,6 +26,8 @@ public final class VanillaLight { boolean pbr=true; int pbrIntensity=50; boolean ssgi=true; + boolean coloredLights=false; + int coloredLightIntensity=35; int ssgiIntensity=35; int bloomIntensity=20; int bloomRadius=50; @@ -46,6 +48,7 @@ public final class VanillaLight { preferences.sunshaftIntensity=Math.clamp(preferences.sunshaftIntensity,0,100); preferences.lensFlareIntensity=Math.clamp(preferences.lensFlareIntensity,0,100); preferences.pbrIntensity=Math.clamp(preferences.pbrIntensity,0,100); + preferences.coloredLightIntensity=Math.clamp(preferences.coloredLightIntensity,0,100); preferences.ssgiIntensity=Math.clamp(preferences.ssgiIntensity,0,100); preferences.bloomIntensity=Math.clamp(preferences.bloomIntensity,0,100); preferences.bloomRadius=Math.clamp(preferences.bloomRadius,0,100); @@ -79,6 +82,10 @@ public final class VanillaLight { public static int pbrIntensity(){enabled();return preferences.pbrIntensity;} public static void setPbr(boolean value){enabled();preferences.pbr=value;save();} public static void setPbrIntensity(int value){enabled();preferences.pbrIntensity=Math.clamp(value,0,100);save();} + public static boolean coloredLights(){enabled();return preferences.coloredLights;} + public static int coloredLightIntensity(){enabled();return preferences.coloredLightIntensity;} + public static void setColoredLights(boolean value){enabled();preferences.coloredLights=value;save();} + public static void setColoredLightIntensity(int value){enabled();preferences.coloredLightIntensity=Math.clamp(value,0,100);save();} public static boolean ssgi(){enabled();return preferences.ssgi;} public static int ssgiIntensity(){enabled();return preferences.ssgiIntensity;} public static void setSsgi(boolean value){enabled();preferences.ssgi=value;save();} diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/client/PixelShadowDirtyMixin.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/client/PixelShadowDirtyMixin.java index 39c1500..5aa4928 100644 --- a/mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/client/PixelShadowDirtyMixin.java +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/client/PixelShadowDirtyMixin.java @@ -1,6 +1,7 @@ package fr.koka.sanctuary.mixin.client; import fr.koka.sanctuary.client.shader.PixelShadows; +import fr.koka.sanctuary.client.shader.ColoredLights; import fr.koka.sanctuary.client.shader.ProceduralPbr; import fr.koka.sanctuary.client.shader.Bloom; import fr.koka.sanctuary.client.shader.ScreenSpaceGI; @@ -16,10 +17,10 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; abstract class PixelShadowDirtyMixin { @Inject(method="blockChanged", at=@At("HEAD")) private void sanctuary$dirty(BlockPos pos, int flags, CallbackInfo ci) { - PixelShadows.dirtyBlock(pos); ProceduralPbr.dirtyBlock(pos); Bloom.dirtyBlock(pos); + PixelShadows.dirtyBlock(pos); ProceduralPbr.dirtyBlock(pos); Bloom.dirtyBlock(pos); ColoredLights.dirtyBlock(pos); } @Inject(method="allChanged", at=@At("HEAD")) - private void sanctuary$reload(CallbackInfo ci) { PixelShadows.release(); ProceduralPbr.release(); Bloom.release(); ScreenSpaceGI.release(); } + private void sanctuary$reload(CallbackInfo ci) { PixelShadows.release(); ProceduralPbr.release(); Bloom.release(); ColoredLights.release(); ScreenSpaceGI.release(); } @Inject(method="setSectionDirty", at=@At("HEAD")) - private void sanctuary$pbrLight(int x,int y,int z,CallbackInfo ci){ProceduralPbr.dirtySection(x,y,z);} + private void sanctuary$pbrLight(int x,int y,int z,CallbackInfo ci){ProceduralPbr.dirtySection(x,y,z); ColoredLights.dirtySection(x,y,z);} } diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/client/PixelShadowLevelMixin.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/client/PixelShadowLevelMixin.java index ea28ae9..d22cd8e 100644 --- a/mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/client/PixelShadowLevelMixin.java +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/client/PixelShadowLevelMixin.java @@ -1,6 +1,7 @@ package fr.koka.sanctuary.mixin.client; import fr.koka.sanctuary.client.shader.PixelShadows; +import fr.koka.sanctuary.client.shader.ColoredLights; import fr.koka.sanctuary.client.shader.ProceduralPbr; import fr.koka.sanctuary.client.shader.Bloom; import net.minecraft.client.multiplayer.ClientLevel; @@ -17,11 +18,11 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; abstract class PixelShadowLevelMixin { @Inject(method="setBlocksDirty", at=@At("HEAD")) private void sanctuary$block(BlockPos pos, BlockState before, BlockState after, CallbackInfo ci) { - if (before != after) {PixelShadows.dirtyBlock(pos); ProceduralPbr.dirtyBlock(pos);Bloom.dirtyBlock(pos);} + if (before != after) {PixelShadows.dirtyBlock(pos); ProceduralPbr.dirtyBlock(pos);Bloom.dirtyBlock(pos); ColoredLights.dirtyBlock(pos);} } @Inject(method="onChunkLoaded", at=@At("TAIL")) - private void sanctuary$loaded(ChunkPos pos, CallbackInfo ci) { PixelShadows.dirtyChunk(pos.x(), pos.z()); ProceduralPbr.dirtyChunk(pos.x(), pos.z()); Bloom.dirtyChunk(pos.x(), pos.z()); } + private void sanctuary$loaded(ChunkPos pos, CallbackInfo ci) { PixelShadows.dirtyChunk(pos.x(), pos.z()); ProceduralPbr.dirtyChunk(pos.x(), pos.z()); Bloom.dirtyChunk(pos.x(), pos.z()); ColoredLights.dirtyChunk(pos.x(),pos.z()); } @Inject(method="unload", at=@At("HEAD")) - private void sanctuary$unload(LevelChunk chunk, CallbackInfo ci) { PixelShadows.dirtyChunk(chunk.getPos().x(), chunk.getPos().z()); ProceduralPbr.dirtyChunk(chunk.getPos().x(), chunk.getPos().z()); Bloom.dirtyChunk(chunk.getPos().x(), chunk.getPos().z()); } + private void sanctuary$unload(LevelChunk chunk, CallbackInfo ci) { PixelShadows.dirtyChunk(chunk.getPos().x(), chunk.getPos().z()); ProceduralPbr.dirtyChunk(chunk.getPos().x(), chunk.getPos().z()); Bloom.dirtyChunk(chunk.getPos().x(), chunk.getPos().z()); ColoredLights.dirtyChunk(chunk.getPos().x(),chunk.getPos().z()); } } diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/client/PixelShadowRendererMixin.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/client/PixelShadowRendererMixin.java index 0ecaa43..ebf6d1e 100644 --- a/mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/client/PixelShadowRendererMixin.java +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/client/PixelShadowRendererMixin.java @@ -4,6 +4,7 @@ import com.llamalad7.mixinextras.injector.wrapoperation.Operation; import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; import com.mojang.renderpearl.api.buffers.GpuBufferSlice; import fr.koka.sanctuary.client.shader.PixelShadows; +import fr.koka.sanctuary.client.shader.ColoredLights; import fr.koka.sanctuary.client.shader.ProceduralPbr; import fr.koka.sanctuary.client.shader.EdgeHighlights; import fr.koka.sanctuary.client.shader.Bloom; @@ -25,11 +26,12 @@ abstract class PixelShadowRendererMixin { ProceduralPbr.captureProjection(matrix); EdgeHighlights.captureProjection(matrix); Bloom.captureProjection(matrix); + ColoredLights.captureProjection(matrix); ScreenSpaceGI.captureProjection(matrix); return original.call(buffer, matrix); } @Inject(method="renderLevel", at=@At(value="INVOKE", target="Lnet/minecraft/client/renderer/GameRenderer;render3dHud(Lnet/minecraft/client/renderer/state/level/CameraRenderState;Lnet/minecraft/client/renderer/state/level/PlayerRenderState;Lnet/minecraft/client/renderer/state/OptionsRenderState;Z)V")) - private void sanctuary$shadows(CallbackInfo ci) { PixelShadows.render(); ProceduralPbr.render(); ScreenSpaceGI.render(); EdgeHighlights.render(); Bloom.render(); Sunshafts.render(); } + private void sanctuary$shadows(CallbackInfo ci) { PixelShadows.render(); ProceduralPbr.render(); ColoredLights.render(); ScreenSpaceGI.render(); EdgeHighlights.render(); Bloom.render(); Sunshafts.render(); } @Inject(method={"close", "setLevel"}, at=@At("HEAD")) - private void sanctuary$close(CallbackInfo ci) { PixelShadows.release(); ProceduralPbr.release(); EdgeHighlights.release(); Bloom.release(); ScreenSpaceGI.release(); Sunshafts.release(); } + private void sanctuary$close(CallbackInfo ci) { PixelShadows.release(); ProceduralPbr.release(); EdgeHighlights.release(); Bloom.release(); ColoredLights.release(); ScreenSpaceGI.release(); Sunshafts.release(); } } diff --git a/mods/sanctuary/src/main/resources/assets/sanctuary/lang/en_us.json b/mods/sanctuary/src/main/resources/assets/sanctuary/lang/en_us.json index 6ad000e..82d0e56 100644 --- a/mods/sanctuary/src/main/resources/assets/sanctuary/lang/en_us.json +++ b/mods/sanctuary/src/main/resources/assets/sanctuary/lang/en_us.json @@ -2308,5 +2308,10 @@ "sanctuary.shaders.sunshafts_help": "Light beams through openings in the scenery. Independent of bloom; range follows shadows (up to 64 blocks).", "sanctuary.shaders.sunshaft_intensity": "Sunshaft intensity", "sanctuary.shaders.sunshaft_intensity_help": "Strength of light scattered in the air. Beams automatically thicken with fog and precipitation. Zero disables the effect.", - "sanctuary.shaders.sunshaft_intensity_value": "Sunshafts: %s %%" + "sanctuary.shaders.sunshaft_intensity_value": "Sunshafts: %s %%", + "sanctuary.shaders.colored_lights": "Colored lights", + "sanctuary.shaders.colored_lights_help": "Light-emitting blocks tint their surroundings, even off screen. Local propagation respects obstacles and does not change gameplay light levels.", + "sanctuary.shaders.colored_light_intensity": "Colored light intensity", + "sanctuary.shaders.colored_light_intensity_value": "Colored lights: %s %%", + "sanctuary.shaders.colored_light_intensity_help": "Subtle tint at 35%. Zero disables the calculation. Source changes update progressively." } diff --git a/mods/sanctuary/src/main/resources/assets/sanctuary/lang/fr_fr.json b/mods/sanctuary/src/main/resources/assets/sanctuary/lang/fr_fr.json index 1433666..1c10ab2 100644 --- a/mods/sanctuary/src/main/resources/assets/sanctuary/lang/fr_fr.json +++ b/mods/sanctuary/src/main/resources/assets/sanctuary/lang/fr_fr.json @@ -2308,5 +2308,10 @@ "sanctuary.shaders.sunshafts_help": "Faisceaux de lumière dans les ouvertures du décor. Indépendants du bloom, portée liée aux ombres (64 blocs maximum).", "sanctuary.shaders.sunshaft_intensity": "Intensité des rayons", "sanctuary.shaders.sunshaft_intensity_help": "Force de la lumière dans l’air. Les rayons s’épaississent automatiquement avec le brouillard et les précipitations. Zéro désactive le calcul.", - "sanctuary.shaders.sunshaft_intensity_value": "Rayons : %s %%" + "sanctuary.shaders.sunshaft_intensity_value": "Rayons : %s %%", + "sanctuary.shaders.colored_lights": "Lumières colorées", + "sanctuary.shaders.colored_lights_help": "Les blocs lumineux teintent leurs environs, même hors écran. Propagation locale avec obstacles, sans changer les règles de lumière du jeu.", + "sanctuary.shaders.colored_light_intensity": "Intensité des lumières colorées", + "sanctuary.shaders.colored_light_intensity_value": "Lumières colorées : %s %%", + "sanctuary.shaders.colored_light_intensity_help": "Teinte douce à 35 %. Zéro désactive le calcul. Les changements de sources sont actualisés progressivement." } diff --git a/mods/sanctuary/src/main/resources/assets/sanctuary/shaders/core/colored_lights.fsh b/mods/sanctuary/src/main/resources/assets/sanctuary/shaders/core/colored_lights.fsh new file mode 100644 index 0000000..75c4ce0 --- /dev/null +++ b/mods/sanctuary/src/main/resources/assets/sanctuary/shaders/core/colored_lights.fsh @@ -0,0 +1,61 @@ +#version 330 +#extension GL_ARB_separate_shader_objects : require +uniform sampler2D WorldDepth; +uniform sampler2D SceneColor; +uniform sampler2D LightVolume; +layout(std140) uniform ColoredLightScene { + mat4 InverseViewProjection; + vec4 CameraStrength; + vec4 ClipSkyFog; + vec4 FogDistance; + vec4 VolumeSize; +}; +layout(location=0) in vec2 texCoord; +layout(location=0) out vec4 fragColor; +vec3 positionAt(vec2 uv,float depth){ + vec4 p=InverseViewProjection*vec4(uv*2.0-1.0,ClipSkyFog.x>.5?depth:depth*2.0-1.0,1); + return p.xyz/p.w; +} +vec3 neighbor(vec2 uv,vec3 center){float d=texture(WorldDepth,uv).r;return d>0.0?positionAt(uv,d):center;} +vec3 normalAt(vec3 p){ + vec2 pixel=1.0/vec2(textureSize(WorldDepth,0)); + vec3 l=p-neighbor(texCoord-vec2(pixel.x,0),p),r=neighbor(texCoord+vec2(pixel.x,0),p)-p; + vec3 d=p-neighbor(texCoord-vec2(0,pixel.y),p),u=neighbor(texCoord+vec2(0,pixel.y),p)-p; + vec3 dx=dot(l,l)>0.0&&(dot(l,l)0.0&&(dot(d,d).995?.51:.06); + vec3 edge=min(local,vec3(VolumeSize.x)-local); + float border=smoothstep(VolumeSize.y,VolumeSize.y+VolumeSize.z,min(edge.x,min(edge.y,edge.z)));if(border<=0.0)return; + vec4 light=field(local);float energy=max(light.r,max(light.g,light.b));if(energy<.0001)return; + float luminance=dot(light.rgb,vec3(.2126,.7152,.0722)); + vec3 tint=clamp(light.rgb/max(luminance,.0001),vec3(.15),vec3(2)); + float fog=max(clamp((max(length(p.xz),abs(p.y))-FogDistance.x)/max(.001,FogDistance.y-FogDistance.x),0,1),clamp((length(p)-FogDistance.z)/max(.001,FogDistance.w-FogDistance.z),0,1)); + float strength=CameraStrength.w*.8*sqrt(energy)*border*(1.0-.8*light.a*ClipSkyFog.y)*(1.0-fog*ClipSkyFog.z); + fragColor=vec4(scene*mix(vec3(1),tint,strength),0); +} diff --git a/packwiz/pack.toml b/packwiz/pack.toml index 8a1f763..bbd6702 100644 --- a/packwiz/pack.toml +++ b/packwiz/pack.toml @@ -1,6 +1,6 @@ name = "Sanctuary" author = "KOKA99CAB" -version = "beta.138" +version = "beta.139" pack-format = "packwiz:1.1.0" [index]