diff --git a/README.md b/README.md index 9f4cb12..f629703 100644 --- a/README.md +++ b/README.md @@ -20,12 +20,12 @@ 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.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. -Release publiée et même instance Prism synchronisée ; sauvegardes et réglages conservés. -La [vérification Vulkan antérieure](docs/shader-vulkan-beta138.md) reste disponible. +La version **beta.140** relie les lumières colorées aux objets en main et sur +la tête, renforce leur teinte et ajoute une distance réglable de 8 à 64 blocs +avec apparition progressive. Activation par défaut à 30 % ; torches natives, +option « Torches chaudes » désactivée, cuivre vert et redstone rouge saturé. +Les frontières de saturation des lumières bleues s’atténuent progressivement. +Le [ticket](docs/colored-dynamic-beta140.md) décrit le rendu et les vérifications. 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. @@ -36,6 +36,14 @@ La [carte native beta.005](docs/atlas-beta005.md) prend le relais après l’étude de Xaero et des alternatives libres. +## beta.140 — Lumières portées et fondus + +Mains et tête, distances 8/16/32/64, fondus, cuivre vert, redstone rouge et +option pour la chaleur des torches. Lumières colorées activées à 30 % par défaut. +[Contrat et vérifications](docs/colored-dynamic-beta140.md) · +[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.140/Sanctuary-beta.140.mrpack) · +[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.140/Sanctuary-Test-beta.140.mrpack). + ## beta.139 — Lumières colorées Teintes chaudes, sources des âmes bleues, redstone rouge, froglights et matériaux lumineux. diff --git a/docs/colored-dynamic-beta140.md b/docs/colored-dynamic-beta140.md new file mode 100644 index 0000000..a644b48 --- /dev/null +++ b/docs/colored-dynamic-beta140.md @@ -0,0 +1,101 @@ +# LIGHT-140 — Lumières colorées portées et distance + +Socle beta.139 `c05c338`, branche `codex/colored-dynamic-beta140`. +Minecraft 26.3 / Java 25. Livraison beta.140. + +## Contrat + +Relier les couleurs aux vraies sources de lumière dynamique : mains, tête, +cosmétiques, objets au sol et feu, selon le système existant. Renforcer la +teinte et activer les lumières colorées par défaut à 30 %. Ajouter une distance +d'affichage de 8, 16, 32 ou 64 blocs (16 par défaut), indépendante de la portée +native de chaque source. Augmenter la distance coûte davantage en mémoire et +calcul ; aucun chargement forcé de chunk. Les choix sauvegardés sont conservés. + +Sources portées actualisées avec le système dynamique, sans reconstruire tout +le champ statique à chaque mouvement. Aucun changement de monde ou de règle +serveur. Vérifications natives OpenGL et Vulkan, puis construction et archives. + +## Couleurs et fondus + +Torches ordinaires : température native par défaut ; option « Torches chaudes » +pour retrouver l'orangé. Glowstone jaune, lave jaune-rouge, redstone rouge saturé, +cuivre vert (posé, en main ou sur la tête). Le cuivre est reconnu avant les +familles génériques de torches et lanternes. Les palettes explicites restent +indépendantes des packs ; le repli des autres matériaux suit leurs textures. + +Le rendu s'atténue sur le dernier quart de la distance choisie. Le champ précédent +reste disponible pendant un fondu de 300 ms vers le nouveau champ ; l'activation +et les sources dynamiques ont aussi un fondu. Le changement de distance est +interpolé. Les sources mobiles utilisent la même sélection bornée et les mêmes +intensités que la lumière dynamique existante, avec interpolation visuelle des +positions et couleurs. Elles héritent de sa propagation radiale, y compris de +ses limites d'occultation ; les blocs posés conservent leur propagation avec murs. + +Le volume varie réellement selon la distance (56, 72, 104 ou 168 blocs de côté, +avec marge de propagation). Les grandes distances augmentent la mémoire et le +délai de reconstruction ; la construction reste répartie entre les images. +Deux champs coexistent brièvement pour le fondu. Seuls les chunks déjà chargés +sont lus. La portée lumineuse native des torches n'est pas allongée. + +## Frontières de saturation + +Le stockage beta.139 tronquait les canaux faibles séparément en fin de portée : +un bleu pouvait perdre son rouge avant son vert, puis devenir plus saturé sur +une frontière visible. Le stockage logarithmique conserve maintenant une marge +sous le niveau lumineux natif zéro. Le shader applique la coupure à l'énergie +globale en conservant les rapports RGB. La réponse devient linéaire près du noir, +sans amplification par racine quatrième des très faibles contributions. + +La transition des champs interpole le résultat colorimétrique rendu, pas une +énergie ensuite amplifiée. Les sources mobiles ont une atténuation de fondu +compensée pour la même réponse. Le test contrôle les rapports rouge/bleu et +vert/bleu d'une torche des âmes de 1 à 9 blocs, jusqu'au bord de sa portée. + +## Vérifications natives + +Minecraft 26.3 / Java 25 sur Apple M1, mondes de test neufs de graine 122 : + +- OpenGL 4.1 Metal : **2 min 40 s**, `build/colored140-opengl.log`. +- Vulkan / MoltenVK 1.4.2, profondeur 0–1 et transparence améliorée : + **2 min 26 s**, `build/colored140-vulkan.log`. + +Les deux exécutions passent `COLORED139_PASS` et `COLORED140_PASS` : +propagation avec murs, sources hors champ, lampes allumées/éteintes, retrait, +rechargement des ressources, cache, déplacement/retour de caméra, OFF/zéro, +préférences et traductions FR/EN. Les nouveaux contrôles couvrent la teinte +bleue jusqu'à neuf blocs, la torche native inchangée, l'option chaude, le cuivre +vert et le rouge saturé, les deux mains et le cosmétique de tête, la position +en troisième personne, les quatre distances et un fondu sans dépassement. + +Dans la même scène contrôlée, la somme d'écarts OFF/torche chaude à 30 % vaut +3 926 093 / 3 926 092 sur OpenGL/Vulkan, contre 2 025 577 en beta.139 à 35 %. +Cette mesure vérifie l'accentuation demandée, pas la performance. Le test fige +uniquement le scintillement de la lightmap native ; cette sonde et les tests +client ne sont pas distribués. Les captures finales sont conservées sous +`build/colored140-opengl-screenshots/` et `build/colored140-vulkan-screenshots/`. +Comparatif autonome : `build/Shader-beta.140-comparaison.html`. + +Pas de validation des pilotes Windows de l'utilisateur revendiquée. Les +limites de beta.139 pour le rendu sous l'eau/lave et les panoramas restent +ouvertes. Les sources dynamiques gardent l'occultation du système existant. +Aucun monde personnel ouvert ni modifié. + +## Construction et archives + +`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest` +réussit en **2 min 22 s**, 126 tâches. Le GameTest serveur dédié reste exclu +conformément au refus EULA antérieur ; les tests clients ci-dessus sont exécutés +séparément. Log : `build/shader140-build.log`. + +La comparaison avec beta.139 trouve **18 entrées de production modifiées**, +limitées aux lumières dynamiques/colorées, options et aides FR/EN. Les sources +des JAR correspondent aux fichiers du dépôt. Les archives normale/Test contiennent +le même JAR Sanctuary ; la version, les dépendances exactes, les textures de +gemmes et de la clé de Steve, l'absence de classes de test et le template complet +sont vérifiés. Reçu : `build/shader140-artifact.json`. + +SHA-256 du JAR : `853284c1a61c7fce12edd2f7763717321eef56977290eb0e7c5db5e30bff442f`. + +- Sanctuary-beta.140.mrpack : `1b366746e4d6bebfbe0115c849524bdeb81c14eee89a2510f9dbe18808974d6a`. +- Sanctuary-Test-beta.140.mrpack : `50c3cdf759117bb5286d41b4535fde15d9d84deaac833b7f8f61c4a0d29b72e7`. diff --git a/gradle.properties b/gradle.properties index 046a179..2197a50 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.139 -pack_version=beta.139 +mod_version=beta.140 +pack_version=beta.140 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 5129128..94249b8 100644 --- a/mods/sanctuary/build.gradle +++ b/mods/sanctuary/build.gradle @@ -34,6 +34,7 @@ if (clientRenderTests) { if (improvedTransparency.isPresent()) { systemProperty('sanctuary.test.improvedTransparency', improvedTransparency.get()) } + systemProperty('sanctuary.test.colored140', providers.gradleProperty('sanctuaryColored140ClientTests').getOrElse('false')) systemProperty('sanctuary.test.noVsync', providers.gradleProperty('sanctuaryClientNoVsync').getOrElse('false')) systemProperty('sanctuary.test.quick', providers.gradleProperty('sanctuaryQuickTests').getOrElse('false')) systemProperty('sanctuary.test.introVisualOnly', providers.gradleProperty('sanctuaryIntroVisualOnly').getOrElse('false')) 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 index 3564b67..716b13f 100644 --- 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 @@ -20,7 +20,7 @@ public final class ColoredLights139ClientChecks implements FabricClientGameTest 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);}); + c.runOnClient(m->{preferences();VanillaLight.setWarmTorches(true);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++) @@ -36,6 +36,12 @@ public final class ColoredLights139ClientChecks implements FabricClientGameTest 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.SOUL_TORCH)for(int step=1;step<=9;step++){ + final int distance=step;int sample=c.computeOnClient(m->ColoredLights.sample(SOURCE.offset(distance,0,0))); + int sr=sample>>16&255,sg=sample>>8&255,sb=sample&255; + double rr=sr==0?0:Math.pow(.8,(sb-sr)/8.0),gg=sg==0?0:Math.pow(.8,(sb-sg)/8.0); + check(Math.abs(rr-85/255.)<.02&&Math.abs(gg-189/255.)<.02,"Blue hue remains consistent at native distance "+step+" "+rr+" / "+gg); + } 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"); @@ -73,8 +79,8 @@ public final class ColoredLights139ClientChecks implements FabricClientGameTest 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{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.setColoredLightIntensity(30);VanillaLight.setWarmTorches(false);VanillaLight.setColoredLightDistance(16);}); + source(c,server,Blocks.TORCH);enable(c,false);var nativeTorch=capture(c,"torch-native-off");enable(c,true);unchanged(nativeTorch,capture(c,"torch-native-on"),"Normal torch preserves vanilla colour"); + c.runOnClient(m->VanillaLight.setWarmTorches(true));settle(c);var warm=capture(c,"torch-warm-30");long accent=difference(nativeTorch,warm);check(accent>2_025_577,"30 percent exceeds beta139 warm tint at 35 percent: "+accent); + c.runOnClient(m->VanillaLight.setWarmTorches(false));settle(c);unchanged(nativeTorch,capture(c,null),"Warm torch option can restore native tint"); + for(String name:new String[]{"copper_torch","copper_lantern"}){ + var copper=BuiltInRegistries.BLOCK.getValue(Identifier.parse("minecraft:"+name));check(copper!=Blocks.AIR&&copper.defaultBlockState().getLightEmission()>0,"Native copper source exists "+name); + source(c,server,copper);int value=c.computeOnClient(m->ColoredLights.sample(PROBE));check((value>>8&255)>(value>>16&255)&&(value>>8&255)>(value&255),"Copper emits green");capture(c,name+"-green"); + } + server.runOnServer(s->{s.overworld().setBlockAndUpdate(SOURCE,Blocks.REDSTONE_LAMP.defaultBlockState());s.overworld().setBlockAndUpdate(SOURCE.below(),Blocks.REDSTONE_BLOCK.defaultBlockState());});c.waitTicks(40);settle(c); + int red=c.computeOnClient(m->ColoredLights.sample(PROBE));check(Math.pow(.8,((red>>16&255)-(red>>8&255))/8.)<.04,"Redstone uses saturated red");capture(c,"redstone-red-30"); + source(c,server,Blocks.AIR);server.runOnServer(s->s.overworld().setBlockAndUpdate(SOURCE.below(),Blocks.CONCRETE.white().defaultBlockState())); + // Actual carried sources, synchronized through native equipment and accessory packets. + server.runOnServer(s->{command(s,"gamemode creative @a");var p=s.getPlayerList().getPlayers().getFirst();p.setItemSlot(EquipmentSlot.MAINHAND,new ItemStack(Items.SOUL_TORCH));}); + c.waitFor(m->DynamicLightClient.snapshot(m.level).size()==1,200);c.waitTicks(45);enable(c,false);var carriedOff=capture(c,"held-soul-off");enable(c,true);c.waitTicks(25);var carriedOn=capture(c,"held-soul-blue");check(difference(carriedOff,carriedOn)>1000&&c.computeOnClient(m->ColoredLights.dynamicCount())==1,"Held soul torch is visibly blue"); + c.runOnClient(m->{var l=DynamicLightClient.snapshot(m.level).sources().iterator().next();check((l.color()&255)>(l.color()>>16&255),"Dynamic source has actual blue colour");}); + c.runOnClient(m->m.options.setCameraType(net.minecraft.client.CameraType.THIRD_PERSON_BACK));c.waitTicks(25); + c.runOnClient(m->{var pos=ColoredLights.dynamicPosition(m.player.getId());check(pos!=null&&pos.distanceToSqr(m.player.getEyePosition())<1,"Third-person light follows player, not camera");});capture(c,"held-third-person"); + c.runOnClient(m->m.options.setCameraType(net.minecraft.client.CameraType.FIRST_PERSON));c.waitTicks(20);settle(c); + int builds=c.computeOnClient(m->ColoredLights.builds()); + server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.setItemSlot(EquipmentSlot.MAINHAND,ItemStack.EMPTY);p.setItemSlot(EquipmentSlot.OFFHAND,new ItemStack(Items.SOUL_TORCH));});c.waitTicks(35);settle(c);check(c.computeOnClient(m->ColoredLights.builds())==builds,"Moving a light between hands does not rebuild static volume");capture(c,"offhand-soul-blue"); + var copperItem=BuiltInRegistries.ITEM.getValue(Identifier.parse("minecraft:copper_torch")); + server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.setItemSlot(EquipmentSlot.OFFHAND,ItemStack.EMPTY);AccessoryAccess.of(p).setItem(AccessoryContainer.HEAD,new ItemStack(copperItem));AccessoryService.publish(p);});c.waitTicks(45);settle(c); + c.runOnClient(m->{var l=DynamicLightClient.snapshot(m.level).sources().iterator().next();check((l.color()>>8&255)>(l.color()>>16&255)&&(l.color()>>8&255)>(l.color()&255),"Head cosmetic carries copper green");});capture(c,"head-copper-green");check(c.computeOnClient(m->ColoredLights.builds())==builds,"Dynamic colour updates preserve static cache"); + server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();AccessoryAccess.of(p).setItem(AccessoryContainer.HEAD,ItemStack.EMPTY);AccessoryService.publish(p);});c.waitFor(m->DynamicLightClient.snapshot(m.level).size()==0&&ColoredLights.dynamicCount()==0,200);c.waitTicks(25); + server.runOnServer(s->command(s,"gamemode spectator @a")); + camera(c,server,100.5,108,104.5,70);look(c,server,0,18);c.waitTicks(25); + server.runOnServer(s->{for(int x=88;x<=112;x++)for(int z=117;z<=155;z++)s.overworld().setBlockAndUpdate(new BlockPos(x,100,z),Blocks.CONCRETE.white().defaultBlockState()); + for(int x=88;x<=112;x++)for(int y=101;y<=109;y++)s.overworld().setBlockAndUpdate(new BlockPos(x,y,116),Blocks.AIR.defaultBlockState()); + s.overworld().setBlockAndUpdate(new BlockPos(100,101,140),Blocks.VERDANT_FROGLIGHT.defaultBlockState());});c.waitTicks(50); + c.runOnClient(m->VanillaLight.setColoredLightDistance(8));settle(c);var shortRange=capture(c,"distance-8");int small=c.computeOnClient(m->ColoredLights.volumeSide());check(c.computeOnClient(m->ColoredLights.sample(new BlockPos(100,101,141)))==0,"Far field omitted at short distance"); + int before=c.computeOnClient(m->ColoredLights.builds());c.runOnClient(m->VanillaLight.setColoredLightDistance(64));c.waitFor(m->ColoredLights.builds()>before&&ColoredLights.blend()>0&&ColoredLights.blend()<1,1600);var transition=capture(c,"distance-fade");settle(c);var longRange=capture(c,"distance-64"); + check(c.computeOnClient(m->ColoredLights.volumeSide())>small,"Distance changes actual field allocation");check((c.computeOnClient(m->ColoredLights.sample(new BlockPos(100,101,141)))&0xffffff)!=0,"Far light retained at long distance");check(difference(shortRange,longRange)>1000,"Far light is visibly rendered");check(difference(transition,longRange)>100&&difference(shortRange,transition){VanillaLight.setColoredLightDistance(distance);forgetPreferences();check(VanillaLight.coloredLightDistance()==distance,"Persisted distance "+distance);});settle(c);} + 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[]{"warm_torches","warm_torches_help","colored_light_distance","colored_light_distance_help","colored_light_distance_value"}){String full="sanctuary.shaders."+key;check(!Component.translatable(full,16).getString().equals(full),"Translated "+locale+" "+key);}}}); + System.out.println("COLORED140_PASS accent="+accent+" native/warm torches, copper green, saturated red, main/offhand/head, dynamic cache, 8/16/32/64 distance, fade and FR/EN"); + } + private static void source(ClientGameTestContext c,TestServerContext s,Block b){s.runOnServer(server->server.overworld().setBlockAndUpdate(SOURCE,b.defaultBlockState()));c.waitTicks(35);settle(c);} + private static void enable(ClientGameTestContext c,boolean value){c.runOnClient(m->VanillaLight.setColoredLights(value));if(value)settle(c);else c.waitTicks(10);} + private static void settle(ClientGameTestContext c){c.waitFor(m->ColoredLights.applied()&&ColoredLights.settled(),2000);c.waitTicks(12);} + private static long difference(Frame a,Frame b){long total=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(coloredOnly){ColoredLights139ClientChecks.run(c,server);if(Boolean.getBoolean("sanctuary.test.colored140"))ColoredLights140ClientChecks.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;} @@ -281,7 +281,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest { }); }); c.waitTicks(3); - c.takeScreenshot("sanctuary-beta139-shader-options-" + locale); + c.takeScreenshot("sanctuary-beta140-shader-options-" + locale); } c.runOnClient(m -> m.gui.setScreen(null)); } @@ -565,7 +565,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest { } static Frame capture(ClientGameTestContext c, String name) { - if (name != null) c.takeScreenshot("sanctuary-beta139-shader-" + name); + if (name != null) c.takeScreenshot("sanctuary-beta140-shader-" + name); var result = new CompletableFuture(); c.runOnClient(m -> Screenshot.takeScreenshot(m.gameRenderer.mainRenderTarget(), image -> { try (image) { diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/DynamicLightClient.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/DynamicLightClient.java index be06fe3..84fef5b 100644 --- a/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/DynamicLightClient.java +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/DynamicLightClient.java @@ -27,6 +27,15 @@ public final class DynamicLightClient { private static DynamicLightConfig config; private static final Map overrides=new IdentityHashMap<>(); private static int ticks; + private static boolean invalidatingSections; + private static final fr.koka.sanctuary.client.shader.ColoredLightMaterials colors=new fr.koka.sanctuary.client.shader.ColoredLightMaterials(); + private static Object models; + public static boolean invalidatingSections(){return invalidatingSections;} + private static final class ColorMix { + double r,g,b; + void add(int light,int color){if(light<=0)return;double energy=Math.pow(.8,15-light);r=Math.max(r,(color>>16&255)*energy);g=Math.max(g,(color>>8&255)*energy);b=Math.max(b,(color&255)*energy);} + int result(){double m=Math.max(1e-8,Math.max(r,Math.max(g,b)));return (int)Math.round(r/m*255)<<16|(int)Math.round(g/m*255)<<8|(int)Math.round(b/m*255);} + } private DynamicLightClient() {} public static void register(KeyMapping.Category category) { config=DynamicLightConfig.load(); @@ -50,7 +59,7 @@ public final class DynamicLightClient { } public static int pendingSections() {return field.pendingCount();} private static void reset(ClientLevel level) { - field=new DynamicLightField();view=new View(level,field.snapshot());ticks=0; + field=new DynamicLightField();view=new View(level,field.snapshot());ticks=0;colors.clear();models=null; } private static int emission(ItemStack stack) {return overrides.getOrDefault(stack.getItem(),-1)>=0?overrides.get(stack.getItem()):ItemLightEmission.of(stack);} private static void tick(Minecraft m) { @@ -59,6 +68,8 @@ public final class DynamicLightClient { if(ticks++%config.updateIntervalTicks==0) { List sources=List.of(); if(config.enabled) { + var currentModels=m.getModelManager().getBlockStateModelSet();if(models!=currentModels){colors.clear();models=currentModels;} + boolean tinted=fr.koka.sanctuary.client.shader.VanillaLight.coloredLights(); var eye=m.getCameraEntity().getEyePosition(); var collect=new DynamicLightField.Collector(config.dropClusterSize); double distance=config.range*config.range; @@ -66,23 +77,26 @@ public final class DynamicLightClient { if(entity.isRemoved() || entity.distanceToSqr(eye)>distance)continue; boolean drop=entity instanceof ItemEntity; int light=entity.isOnFire()?15:0; + var mix=new ColorMix();if(light>0)mix.add(light,0xFFAB55); double y=entity.getY()+entity.getBbHeight()*.5; - if(entity instanceof ItemEntity item)light=Math.max(light,emission(item.getItem())); + if(entity instanceof ItemEntity item){int emitted=emission(item.getItem());light=Math.max(light,emitted);if(emitted>0&&tinted)mix.add(emitted,colors.color(item.getItem()));} else if(entity instanceof LivingEntity living) { if(living instanceof Player p && p.isSpectator())continue; - light=Math.max(light,Math.max(emission(living.getMainHandItem()),emission(living.getOffhandItem()))); - light=Math.max(light,emission(living.getItemBySlot(EquipmentSlot.HEAD))); - light=Math.max(light,emission(HeadCosmeticService.appearance(living))); + for(var stack:new ItemStack[]{living.getMainHandItem(),living.getOffhandItem(),living.getItemBySlot(EquipmentSlot.HEAD),HeadCosmeticService.appearance(living)}){ + int emitted=emission(stack);light=Math.max(light,emitted);if(emitted>0&&tinted)mix.add(emitted,colors.color(stack)); + } y=entity.getEyeY(); } - if(light>0)collect.add(entity.getId(),drop,entity.getX(),y,entity.getZ(),light); + if(light>0)collect.add(entity.getId(),drop,entity.getX(),y,entity.getZ(),light,tinted?mix.result():0xFFFFFF); } sources=collect.finish(config.maxSources,eye.x,eye.y,eye.z,m.player==null?-1:m.player.getId()); } if(field.update(sources))view=new View(m.level,field.snapshot()); } // Native renderer marks only resident sections; it neither loads chunks nor rebuilds the whole world. - field.drain(config.sectionUpdatesPerTick,key->m.levelExtractor.setSectionDirty( - DynamicLightField.sectionX(key),DynamicLightField.sectionY(key),DynamicLightField.sectionZ(key))); + invalidatingSections=true; + try{field.drain(config.sectionUpdatesPerTick,key->m.levelExtractor.setSectionDirty( + DynamicLightField.sectionX(key),DynamicLightField.sectionY(key),DynamicLightField.sectionZ(key)));} + finally{invalidatingSections=false;} } } diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/shader/ColoredDynamicLights.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/shader/ColoredDynamicLights.java new file mode 100644 index 0000000..f2cafab --- /dev/null +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/shader/ColoredDynamicLights.java @@ -0,0 +1,37 @@ +package fr.koka.sanctuary.client.shader; + +import fr.koka.sanctuary.client.DynamicLightClient; +import fr.koka.sanctuary.lighting.DynamicLightField; +import java.util.*; +import net.minecraft.client.Minecraft; +import net.minecraft.world.phys.Vec3; + +/** Same bounded sources as native dynamic lighting, with visual interpolation only. */ +final class ColoredDynamicLights { + static final int LIMIT=128; + static final class Light { + double x,y,z,r,g,b,visibility;int level;boolean seen; + Light(DynamicLightField.Source s){x=s.x();y=s.y();z=s.z();level=s.light();r=(s.color()>>16&255)/255.;g=(s.color()>>8&255)/255.;b=(s.color()&255)/255.;} + } + private final Map lights=new HashMap<>(); + private long last; + List update(Vec3 camera){ + long now=System.nanoTime();double dt=last==0?0:Math.min(.1,(now-last)/1e9);last=now; + double ease=1-Math.exp(-dt*14),fade=dt/.3; + lights.values().forEach(l->l.seen=false); + var mc=Minecraft.getInstance(); + for(var s:DynamicLightClient.snapshot(mc.level).sources()){ + Light l=lights.computeIfAbsent(s.id(),id->new Light(s));l.seen=true;l.level=s.light(); + boolean own=!s.id().drop()&&mc.player!=null&&s.id().value()==mc.player.getId() + &&mc.getCameraEntity()==mc.player&&mc.options.getCameraType().isFirstPerson(); + double x=own?camera.x:s.x(),y=own?camera.y:s.y(),z=own?camera.z:s.z(); + double step=own?1:ease;l.x+=(x-l.x)*step;l.y+=(y-l.y)*step;l.z+=(z-l.z)*step; + l.r+=((s.color()>>16&255)/255.-l.r)*ease;l.g+=((s.color()>>8&255)/255.-l.g)*ease;l.b+=((s.color()&255)/255.-l.b)*ease; + } + for(var it=lights.values().iterator();it.hasNext();){var l=it.next();l.visibility=Math.clamp(l.visibility+(l.seen?fade:-fade),0,1);if(!l.seen&&l.visibility<=0)it.remove();} + return lights.values().stream().filter(l->camera.distanceToSqr(l.x,l.y,l.z)camera.distanceToSqr(l.x,l.y,l.z))).limit(LIMIT).toList(); + } + Vec3 position(long entity){var l=lights.get(new DynamicLightField.Id(false,entity));return l==null?null:new Vec3(l.x,l.y,l.z);} + void clear(){lights.clear();last=0;} +} 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 index b14a2d6..f6b5419 100644 --- 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 @@ -10,15 +10,30 @@ 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 { +public 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 boolean warm; + public int color(BlockState state){if(warm!=VanillaLight.warmTorches()){warm=VanillaLight.warmTorches();clear();}return colors.computeIfAbsent(state,this::analyse);} + public int color(net.minecraft.world.item.ItemStack stack){ + var state=fr.koka.sanctuary.cosmetics.HeadCosmeticData.state(stack); + if(state!=null)return color(state); + var item=stack.getItem(); + if(item==net.minecraft.world.item.Items.LAVA_BUCKET)return 0xFF6020; + if(item==net.minecraft.world.item.Items.BLAZE_ROD||item==net.minecraft.world.item.Items.BLAZE_POWDER)return 0xFFAB55; + if(item==net.minecraft.world.item.Items.GLOWSTONE_DUST)return color(net.minecraft.world.level.block.Blocks.GLOWSTONE.defaultBlockState()); + if(item==net.minecraft.world.item.Items.GLOW_BERRIES)return 0xFFBD52; + if(item==net.minecraft.world.item.Items.GLOW_INK_SAC||item==net.minecraft.world.item.Items.GLOW_ITEM_FRAME)return 0x66EADD; + return 0xFFFFFF; + } private int analyse(BlockState state){ var id=BuiltInRegistries.BLOCK.getKey(state.getBlock());String name=id.getPath(); if(id.getNamespace().equals("minecraft")){ + if(name.contains("copper"))return 0x62FFA0; if(name.startsWith("soul_"))return 0x55BDFF; - if(name.contains("redstone"))return 0xFF3020; + if(name.contains("redstone"))return 0xFF0602; + if(name.equals("torch")||name.equals("wall_torch"))return VanillaLight.warmTorches()?0xFFAB55:0xFFFFFF; + if(name.equals("lava"))return 0xFF6020; if(name.equals("pearlescent_froglight"))return 0xFFC0EC; if(name.equals("verdant_froglight"))return 0xB4FF82; if(name.equals("ochre_froglight"))return 0xFFCD72; @@ -53,6 +68,6 @@ final class ColoredLightMaterials { 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();} + public 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 index 3ddd9cf..0935ad0 100644 --- 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 @@ -15,93 +15,111 @@ 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; + static int sideFor(int distance){return (distance+20)*2;} + static int columns(int side){return (int)Math.ceil(Math.sqrt(side));} 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 DynamicTexture texture,previous; + private BlockPos previousOrigin; + private int side,previousSide; + private long transitionStarted; + float blend(){return Math.clamp((System.nanoTime()-transitionStarted)/300_000_000f,0,1);} + DynamicTexture previous(){return previous;} + BlockPos previousOrigin(){return previousOrigin;} + int previousSide(){return previousSide;} + int side(){return side;} private BlockPos origin; private long revision,published=-1,lastStart; private int completed; + private boolean warm; void update(){ var mc=Minecraft.getInstance();var modelSet=mc.getModelManager().getBlockStateModelSet(); if(level!=mc.level||models!=modelSet){close();level=mc.level;models=modelSet;} + if(warm!=VanillaLight.warmTorches()){warm=VanillaLight.warmTorches();materials.clear();revision++;if(build!=null){build.close();build=null;}} var p=mc.gameRenderer.gameRenderState().levelRenderState.cameraRenderState.pos; - var wanted=new BlockPos(anchor(p.x),anchor(p.y),anchor(p.z)); + int wantedSide=sideFor(VanillaLight.coloredLightDistance()); + var wanted=new BlockPos(anchor(p.x,wantedSide),anchor(p.y,wantedSide),anchor(p.z,wantedSide)); long now=System.nanoTime(); + if(previous!=null&&blend()>=1){previous.close();previous=null;previousOrigin=null;} // 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.side!=wantedSide||!inside(build.origin,BlockPos.containing(p),build.side))){build.close();build=null;} + if(build==null&&previous==null&&(texture==null||side!=wantedSide||!wanted.equals(origin)||published!=revision)&&now-lastStart>=100_000_000L){ + build=new Build(level,wanted,wantedSide,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();} + previous=texture;previousOrigin=origin;previousSide=side; + texture=new DynamicTexture(()->"Sanctuary coloured light volume",build.takeImage()); + side=build.side;transitionStarted=System.nanoTime(); 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);} + private static int anchor(double p,int side){return Math.floorDiv((int)Math.floor(p),4)*4-side/2;} + private static boolean inside(BlockPos base,BlockPos p,int side){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,int side){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;} + boolean settled(){return texture!=null&&build==null&&revision==published&&blend()>=1;} 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;} + int sample(BlockPos p){if(texture==null||!inside(origin,p,side))return 0;int x=p.getX()-origin.getX(),y=p.getY()-origin.getY(),z=p.getZ()-origin.getZ(),cols=columns(side);return texture.getPixels().getPixel(x+(y%cols)*side,z+(y/cols)*side);} + public void close(){if(build!=null){build.close();build=null;}if(texture!=null){texture.close();texture=null;}if(previous!=null){previous.close();previous=null;}materials.clear();level=null;models=null;origin=null;previousOrigin=null;side=previousSide=0;published=-1;revision=0;lastStart=0;} private static final class Build implements AutoCloseable { + final int side,count,cols,chunkSide; 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 BlockState[] states; + final byte[] red,green,blue,sky; + final boolean[] queued;final IntArrayFIFOQueue queue=new IntArrayFIFOQueue(); + final LevelChunk[] chunks;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){ + NativeImage image;int scan,packed; + Build(ClientLevel level,BlockPos origin,int side,long revision,ColoredLightMaterials materials){ + this.side=side;count=side*side*side;cols=columns(side);chunkSide=(side+15)/16+1; + states=new BlockState[count];red=new byte[count];green=new byte[count];blue=new byte[count];sky=new byte[count];queued=new boolean[count];chunks=new LevelChunk[chunkSide*chunkSide]; + image=new NativeImage(cols*side,((side+cols-1)/cols)*side,false); 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); + for(int z=0;z=deadline)return false;} + 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)); + while(packed=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]; + int x=origin.getX()+(i%side),z=origin.getZ()+(i/side%side),y=origin.getY()+(i/(side*side));pos.set(x,y,z); + var chunk=chunks[((x>>4)-cx)+((z>>4)-cz)*chunkSide]; 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; + int r=red[i]&255,g=green[i]&255,b=blue[i]&255;if(Math.max(r,Math.max(g,b))<=136)return; + int x=i%side,z=i/side%side,y=i/(side*side); 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; + 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*side+ny*side*side; + if(r-8<=(red[n]&255)&&g-8<=(green[n]&255)&&b-8<=(blue[n]&255))continue; + int loss=net.minecraft.world.level.lighting.LightEngine.getLightDampeningInto(states[i],states[n],d,Math.max(1,states[n].getLightDampening()))*8; + int nr=Math.max(0,r-loss),ng=Math.max(0,g-loss),nb=Math.max(0,b-loss);if(Math.max(nr,Math.max(ng,nb))<=128)continue;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); } @@ -110,7 +128,7 @@ final class ColoredLightVolume implements AutoCloseable { 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);} + // An offset preserves dim colour channels below native level zero. The shader + // applies the native cutoff to overall energy, never to each channel separately. + static int encode(int emission,int channel){return channel==0?0:Math.clamp((int)Math.round((emission+16+Math.log(channel/255.0)/Math.log(1.25))*8),0,248);} } 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 index c00144a..1f2b5ba 100644 --- 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 @@ -25,13 +25,19 @@ public final class ColoredLights { .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()) + .withUniform("LightVolume",UniformType.COMBINED_IMAGE_SAMPLER) + .withUniform("PreviousVolume",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 net.minecraft.client.renderer.texture.DynamicTexture black; + private static final ColoredDynamicLights DYNAMIC=new ColoredDynamicLights(); + private static long activated,lastRender; + private static float displayedDistance; + private static int dynamicCount; private static MappableRingBuffer uniforms; private static boolean applied; private static int frames; @@ -39,40 +45,58 @@ public final class 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 dirtySection(int x,int y,int z){if(!fr.koka.sanctuary.client.DynamicLightClient.invalidatingSections())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; + long now=System.nanoTime();if(activated==0){activated=now;displayedDistance=VanillaLight.coloredLightDistance();} + double elapsed=lastRender==0?0:Math.min(.1,(now-lastRender)/1e9);lastRender=now; + displayedDistance+=(VanillaLight.coloredLightDistance()-displayedDistance)*(float)(1-Math.exp(-elapsed*10)); + VOLUME.update(); + if(black==null){var image=new com.mojang.blaze3d.platform.NativeImage(1,1,false);image.setPixel(0,0,0);black=new net.minecraft.client.renderer.texture.DynamicTexture(()->"Empty coloured light",image);} + var lights=DYNAMIC.update(camera.pos);dynamicCount=lights.size(); + var current=VOLUME.texture()==null?black:VOLUME.texture(); + var previous=VOLUME.previous()==null?black:VOLUME.previous(); 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; + if(uniforms==null)uniforms=new MappableRingBuffer(()->"Sanctuary coloured light scene",GpuBuffer.USAGE_UNIFORM|GpuBuffer.USAGE_MAP_WRITE,176+ColoredDynamicLights.LIMIT*32); + var base=VOLUME.origin()==null?BlockPos.containing(camera.pos):VOLUME.origin();var fog=camera.fogData; + var old=VOLUME.previousOrigin()==null?base:VOLUME.previousOrigin(); + int side=VOLUME.texture()==null?1:VOLUME.side(),oldSide=VOLUME.previous()==null?1:VOLUME.previousSide(); uniforms.rotate();try(var mapped=uniforms.currentBuffer().map(false,true)){ - Std140Builder.intoBuffer(mapped.data()).putMat4f(new Matrix4f(projection).mul(camera.viewRotationMatrix).invert()) + var data=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); + .putVec4(displayedDistance,VOLUME.texture()==null?0:VOLUME.blend(),Math.clamp((now-activated)/300_000_000f,0,1),lights.size()) + .putVec4(side,ColoredLightVolume.columns(side),current.getPixels().getWidth(),current.getPixels().getHeight()) + .putVec4((float)(camera.pos.x-old.getX()),(float)(camera.pos.y-old.getY()),(float)(camera.pos.z-old.getZ()),VOLUME.previous()==null?0:1) + .putVec4(oldSide,ColoredLightVolume.columns(oldSide),previous.getPixels().getWidth(),previous.getPixels().getHeight()); + for(int i=0;i"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); + pass.setUniform("WorldDepth",main.getDepthTextureView(),nearest);pass.setUniform("SceneColor",source.getColorTextureView(),nearest);pass.setUniform("LightVolume",current.getTextureView(),nearest);pass.setUniform("PreviousVolume",previous.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;} + public static void release(){VOLUME.close();DYNAMIC.clear();activated=lastRender=0;dynamicCount=0;if(black!=null){black.close();black=null;}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 net.minecraft.world.phys.Vec3 dynamicPosition(long entity){return DYNAMIC.position(entity);} + static int dynamicCount(){return dynamicCount;} + static float blend(){return VOLUME.blend();} + static int volumeSide(){return VOLUME.side();} 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 cc36ffe..3edda8b 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 @@ -25,7 +25,12 @@ public final class ShaderOptionsScreen extends OptionsSubScreen { 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)); + list.addBig(OptionInstance.createBoolean("sanctuary.shaders.warm_torches",OptionInstance.cachedConstantTooltip( + Component.translatable("sanctuary.shaders.warm_torches_help")),VanillaLight.warmTorches(),VanillaLight::setWarmTorches)); slider("colored_light_intensity",VanillaLight.coloredLightIntensity(),VanillaLight::setColoredLightIntensity); + list.addBig(new OptionInstance("sanctuary.shaders.colored_light_distance",OptionInstance.cachedConstantTooltip( + Component.translatable("sanctuary.shaders.colored_light_distance_help")),(caption,value)->Component.translatable("sanctuary.shaders.colored_light_distance_value",value), + new OptionInstance.Enum<>(List.of(8,16,32,64),Codec.INT),VanillaLight.coloredLightDistance(),VanillaLight::setColoredLightDistance)); 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 9056f7a..f6a8774 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,8 +26,10 @@ public final class VanillaLight { boolean pbr=true; int pbrIntensity=50; boolean ssgi=true; - boolean coloredLights=false; - int coloredLightIntensity=35; + boolean coloredLights=true; + boolean warmTorches=false; + int coloredLightIntensity=30; + int coloredLightDistance=16; int ssgiIntensity=35; int bloomIntensity=20; int bloomRadius=50; @@ -49,6 +51,7 @@ public final class VanillaLight { 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.coloredLightDistance=normalizeColoredLightDistance(preferences.coloredLightDistance); preferences.ssgiIntensity=Math.clamp(preferences.ssgiIntensity,0,100); preferences.bloomIntensity=Math.clamp(preferences.bloomIntensity,0,100); preferences.bloomRadius=Math.clamp(preferences.bloomRadius,0,100); @@ -82,10 +85,15 @@ 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 warmTorches(){enabled();return preferences.warmTorches;} + public static void setWarmTorches(boolean value){enabled();preferences.warmTorches=value;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 int coloredLightDistance(){enabled();return preferences.coloredLightDistance;} + public static void setColoredLightDistance(int value){enabled();preferences.coloredLightDistance=normalizeColoredLightDistance(value);save();} + static int normalizeColoredLightDistance(int value){return value<12?8:value<24?16:value<48?32:64;} 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/lighting/DynamicLightField.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/lighting/DynamicLightField.java index 43774a1..0deeb06 100644 --- a/mods/sanctuary/src/main/java/fr/koka/sanctuary/lighting/DynamicLightField.java +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/lighting/DynamicLightField.java @@ -7,7 +7,8 @@ import java.util.function.LongConsumer; /** Immutable render snapshots; entity collection and the dirty queue belong to the client tick. */ public final class DynamicLightField { public record Id(boolean drop, long value) {} - public record Source(Id id, double x, double y, double z, int light) { + public record Source(Id id, double x, double y, double z, int light, int color) { + public Source(Id id,double x,double y,double z,int light){this(id,x,y,z,light,0xFFFFFF);} public Source { if (!Double.isFinite(x+y+z) || light<1 || light>15) throw new IllegalArgumentException("light source"); } @@ -20,9 +21,12 @@ public final class DynamicLightField { private final int cellSize; public Collector(int cellSize) {this.cellSize=Math.max(1,cellSize);} public void add(long entity,boolean drop,double x,double y,double z,int light) { + add(entity,drop,x,y,z,light,0xFFFFFF); + } + public void add(long entity,boolean drop,double x,double y,double z,int light,int color) { if(light<=0)return; Id id=new Id(drop,drop ? sectionKey(floor(x/cellSize),floor(y/cellSize),floor(z/cellSize)) : entity); - Source candidate=new Source(id,snap(x),snap(y),snap(z),Math.min(15,light)); + Source candidate=new Source(id,snap(x),snap(y),snap(z),Math.min(15,light),color); sources.merge(id,candidate,(a,b)->compareRepresentative(a,b)<=0?a:b); } public List finish(int limit,double x,double y,double z,long priorityEntity) { @@ -53,6 +57,7 @@ public final class DynamicLightField { lists.long2ObjectEntrySet().forEach(e->bins.put(e.getLongKey(),e.getValue().toArray(Source[]::new))); } public int size() {return sources.size();} + public Collection sources(){return sources.values();} public int sectionCount() {return bins.size();} public int sampleCount(double x,double y,double z) { Source[] near=bins.get(sectionKey(floor(x)>>4,floor(y)>>4,floor(z)>>4));return near==null?0:near.length; 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 82d0e56..fe95ef1 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 @@ -2310,8 +2310,13 @@ "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.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_lights_help": "Light-emitting blocks and carried items tint their surroundings, even off screen. Smooth appearance without changing 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." + "sanctuary.shaders.colored_light_intensity_help": "Enabled at 30% by default. Zero disables computation. Changes fade in smoothly.", + "sanctuary.shaders.colored_light_distance": "Colored light distance", + "sanctuary.shaders.colored_light_distance_value": "Colored lights: %s blocks", + "sanctuary.shaders.colored_light_distance_help": "View distance: 8 to 64 blocks. Longer distances use more memory and computation. Smooth range fade; each source keeps its native reach.", + "sanctuary.shaders.warm_torches": "Warm torches", + "sanctuary.shaders.warm_torches_help": "Off: ordinary torches keep the native color temperature. On: orange tint. Other sources retain their own colors." } 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 1c10ab2..8e13643 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 @@ -2310,8 +2310,13 @@ "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.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_lights_help": "Les blocs lumineux et les objets portés teintent leurs environs, même hors écran. Apparition progressive, 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." + "sanctuary.shaders.colored_light_intensity_help": "Activé par défaut à 30 %. Zéro désactive le calcul. Les changements apparaissent progressivement.", + "sanctuary.shaders.colored_light_distance": "Distance des lumières colorées", + "sanctuary.shaders.colored_light_distance_value": "Lumières colorées : %s blocs", + "sanctuary.shaders.colored_light_distance_help": "Distance d'affichage : 8 à 64 blocs. Les grandes distances utilisent plus de mémoire et de calcul. Fondu aux limites ; la portée de chaque source reste native.", + "sanctuary.shaders.warm_torches": "Torches chaudes", + "sanctuary.shaders.warm_torches_help": "Désactivé : les torches ordinaires conservent la température de couleur native. Activé : teinte orangée. Les autres sources gardent leurs propres couleurs." } 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 index 75c4ce0..d6e242b 100644 --- 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 @@ -3,12 +3,18 @@ uniform sampler2D WorldDepth; uniform sampler2D SceneColor; uniform sampler2D LightVolume; +uniform sampler2D PreviousVolume; layout(std140) uniform ColoredLightScene { mat4 InverseViewProjection; vec4 CameraStrength; vec4 ClipSkyFog; vec4 FogDistance; vec4 VolumeSize; + vec4 Atlas; + vec4 PreviousCamera; + vec4 PreviousAtlas; + vec4 DynamicPosition[128]; + vec4 DynamicColor[128]; }; layout(location=0) in vec2 texCoord; layout(location=0) out vec4 fragColor; @@ -27,20 +33,32 @@ vec3 normalAt(vec3 p){ vec3 n=cross(dx*inversesqrt(xx),dy*inversesqrt(yy));float nn=dot(n,n);if(nn<1e-12)return vec3(0); n*=inversesqrt(nn);return dot(n,-p)<0.0?-n:n; } -vec4 cell(ivec3 p){ - p=clamp(p,ivec3(0),ivec3(63)); - vec4 v=texelFetch(LightVolume,ivec2(p.x+(p.y%8)*64,p.z+(p.y/8)*64),0); - // Decode Q4.4 logarithmic RGB. Black stays exactly black. - v.rgb=max(vec3(0),(pow(vec3(.8),vec3(15)-v.rgb*(255.0/16.0))-vec3(pow(.8,15.0)))/(1.0-pow(.8,15.0))); +vec4 cell(sampler2D volume,ivec3 p,vec4 atlas){ + int side=int(atlas.x),cols=int(atlas.y); + p=clamp(p,ivec3(0),ivec3(side-1)); + vec4 v=texelFetch(volume,ivec2(p.x+(p.y%cols)*side,p.z+(p.y/cols)*side),0); + vec3 spectrum=pow(vec3(.8),vec3(31)-v.rgb*(255.0/8.0))*step(vec3(.5/255.0),v.rgb); + float peak=max(spectrum.r,max(spectrum.g,spectrum.b)); + float energy=max(0.0,(peak-pow(.8,15.0))/(1.0-pow(.8,15.0))); + v.rgb=spectrum*(energy/max(peak,1e-12)); return v; } -vec4 field(vec3 p){ +vec4 field(sampler2D volume,vec3 p,vec4 atlas){ + vec3 edges=min(p,vec3(atlas.x)-p); + float margin=smoothstep(14.0,18.0,min(edges.x,min(edges.y,edges.z)));if(margin<=0.0)return vec4(0); vec3 at=p-.5;ivec3 base=ivec3(floor(at));vec3 f=fract(at);vec4 result=vec4(0); for(int y=0;y<2;y++)for(int z=0;z<2;z++)for(int x=0;x<2;x++){ float w=(x==0?1.0-f.x:f.x)*(y==0?1.0-f.y:f.y)*(z==0?1.0-f.z:f.z); - result+=cell(base+ivec3(x,y,z))*w; + result+=cell(volume,base+ivec3(x,y,z),atlas)*w; } - return result; + result.rgb*=margin;return result; +} +vec3 tintGain(vec4 light,float amount){ + float energy=max(light.r,max(light.g,light.b));if(energy<1e-12)return vec3(1); + vec3 tint=clamp(light.rgb/max(dot(light.rgb,vec3(.2126,.7152,.0722)),1e-12),vec3(.02),vec3(3)); + // Linear close to darkness: no fourth-root halo at the edge of a light pool. + float response=energy/(energy+.08); + return pow(tint,vec3(clamp(amount*response*(1.0-.8*light.a*ClipSkyFog.y),0.0,.95))); } void main(){ vec3 scene=texture(SceneColor,texCoord).rgb;fragColor=vec4(scene,0); @@ -49,13 +67,25 @@ void main(){ vec3 a=abs(n);float axis=max(a.x,max(a.y,a.z)); // Sample outside opaque block faces. Bilinear atlas filtering would leak // across unrelated slices, so the eight neighbours are explicitly addressed. - vec3 local=p+CameraStrength.xyz+n*(axis>.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)); + vec3 offset=n*(axis>.995?.51:.06); + float border=1.0-smoothstep(VolumeSize.x*.75,VolumeSize.x,length(p));if(border<=0.0)return; + vec4 light=field(LightVolume,p+CameraStrength.xyz+offset,Atlas); + vec4 before=VolumeSize.y<1.0&&PreviousCamera.w>.5?field(PreviousVolume,p+PreviousCamera.xyz+offset,PreviousAtlas):vec4(0); + vec3 dynamic=vec3(0); + for(int i=0;i=radius*radius)continue; + float remaining=radius-sqrt(square); + float power=max(0.0,(pow(.8,15.0-remaining)-pow(.8,15.0))/(1.0-pow(.8,15.0))); + float response=power/(power+.08)*smoothstep(0.0,1.0,DynamicColor[i].a); + float fadedPower=.08*response/max(1e-6,1.0-response); + dynamic=max(dynamic,DynamicColor[i].rgb*fadedPower); + } + light.rgb=max(light.rgb,dynamic);before.rgb=max(before.rgb,dynamic); 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); + float amount=CameraStrength.w*1.35*border*(1.0-fog*ClipSkyFog.z)*smoothstep(0.0,1.0,VolumeSize.z); + vec3 gain=tintGain(light,amount); + // Fade the rendered response, preserving colour and avoiding a bright first frame. + if(VolumeSize.y<1.0)gain=mix(tintGain(before,amount),gain,smoothstep(0.0,1.0,VolumeSize.y)); + fragColor=vec4(scene*gain,0); } diff --git a/mods/sanctuary/src/test/java/fr/koka/sanctuary/lighting/Dynamic030Smoke.java b/mods/sanctuary/src/test/java/fr/koka/sanctuary/lighting/Dynamic030Smoke.java index 77916bd..acc41a7 100644 --- a/mods/sanctuary/src/test/java/fr/koka/sanctuary/lighting/Dynamic030Smoke.java +++ b/mods/sanctuary/src/test/java/fr/koka/sanctuary/lighting/Dynamic030Smoke.java @@ -52,6 +52,13 @@ public final class Dynamic030Smoke { for(var light:lights)expected=Math.max(expected,(int)Math.max(0,(light.light()-Math.sqrt(light.distanceSquared(x,y,z)))*16)); require(indexed.apply(nativeSky,x,y,z)==(nativeSky|expected),"Indexed samples agree with brute-force oracle"); } + var colored=new DynamicLightField.Collector(4);colored.add(42,false,2,3,4,10,0x55BDFF); + var blue=colored.finish(32,0,0,0,42);var blueField=new DynamicLightField.Snapshot(blue); + require(blueField.sources().iterator().next().color()==0x55BDFF,"Snapshot retains source colour"); + require((blueField.apply(nativeSky,2,3,4)&255)==160,"RGB metadata preserves native intensity"); + var changed=new DynamicLightField.Collector(4);changed.add(42,false,2,3,4,10,0x62FFA0); + field.update(blue);require(field.update(changed.finish(32,0,0,0,42)),"Same intensity can change colour"); + require(blueField.sources().iterator().next().color()==0x55BDFF,"Older coloured snapshot remains immutable"); long begin=System.nanoTime();long sum=0; for(int i=0;i<1000000;i++)sum+=stress.apply(0,i%80,2,(i/80)%80); System.out.printf("LIGHT-01 passed: 10,000-drop clustering, 100,000 oracle samples; 1M cached queries %.1f ms (checksum %d)%n",(System.nanoTime()-begin)/1e6,sum); diff --git a/packwiz/pack.toml b/packwiz/pack.toml index bbd6702..27dcc07 100644 --- a/packwiz/pack.toml +++ b/packwiz/pack.toml @@ -1,6 +1,6 @@ name = "Sanctuary" author = "KOKA99CAB" -version = "beta.139" +version = "beta.140" pack-format = "packwiz:1.1.0" [index]