From 93ed5574a4f77bd3a083b254498f4994efe31179 Mon Sep 17 00:00:00 2001 From: koka Date: Fri, 18 Sep 2026 04:11:03 +0200 Subject: [PATCH] Drive PBR normals with cached local light directions in beta.141 --- README.md | 22 ++-- docs/directional-lights-beta141.md | 116 ++++++++++++++++++ gradle.properties | 4 +- mods/sanctuary/build.gradle | 2 + .../DirectionalLights141ClientChecks.java | 48 ++++++++ .../shader/PixelShadows122ClientChecks.java | 6 +- .../client/shader/ColoredLightVolume.java | 49 ++++++-- .../client/shader/ColoredLights.java | 23 +++- .../client/shader/ProceduralPbr.java | 12 +- .../client/PixelShadowRendererMixin.java | 2 +- .../assets/sanctuary/lang/en_us.json | 2 +- .../assets/sanctuary/lang/fr_fr.json | 2 +- .../assets/sanctuary/shaders/core/pbr.fsh | 70 ++++++++++- packwiz/pack.toml | 2 +- 14 files changed, 322 insertions(+), 38 deletions(-) create mode 100644 docs/directional-lights-beta141.md create mode 100644 mods/sanctuary/src/clientGametest/java/fr/koka/sanctuary/client/shader/DirectionalLights141ClientChecks.java diff --git a/README.md b/README.md index a13764b..ee0f27b 100644 --- a/README.md +++ b/README.md @@ -20,14 +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.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 beta.140 est publiée et synchronisée dans l’instance Sanctuary Beta, avec -les 923 fichiers personnels suivis conservés. +La version **beta.141** donne une direction approximative aux lumières locales +pour orienter les normales et les reflets PBR. Les blocs posés utilisent une +estimation en cache ; les objets portés réutilisent leur position. Les sources +opposées adoucissent le relief, avec les fondus existants et sans nouveau bouton. +Le [ticket](docs/directional-lights-beta141.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. @@ -38,6 +35,15 @@ La [carte native beta.005](docs/atlas-beta005.md) prend le relais après l’étude de Xaero et des alternatives libres. +## beta.141 — Direction locale et normales PBR + +Relief et reflets orientés par les lumières locales. Cache compact, confiance +réduite pour les sources opposées, objets portés et fondus. Activer PBR et +Lumières colorées ; portée PBR 32 blocs. +[Contrat et vérifications](docs/directional-lights-beta141.md) · +[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.141/Sanctuary-beta.141.mrpack) · +[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.141/Sanctuary-Test-beta.141.mrpack). + ## beta.140 — Lumières portées et fondus Mains et tête, distances 8/16/32/64, fondus, cuivre vert, redstone rouge et diff --git a/docs/directional-lights-beta141.md b/docs/directional-lights-beta141.md new file mode 100644 index 0000000..9f621ea --- /dev/null +++ b/docs/directional-lights-beta141.md @@ -0,0 +1,116 @@ +# LIGHT-141 — Direction locale pour les normales PBR + +Socle beta.140 `c9f5c56`, branche `codex/directional-lights-beta141`. +Minecraft 26.3 / Java 25. Livraison beta.141. + +## Contrat + +Estimer une direction d'arrivée depuis les six voisins du champ lumineux, +avec intensité et confiance. Conserver cette estimation dans une texture +compacte reconstruite avec le champ, uniquement quand le PBR est actif. +Les sources portées utilisent leurs positions déjà disponibles. Brancher ces +directions sur les normales et reflets PBR existants, sans nouveau bouton. +Les sources opposées doivent réduire la directionnalité, sans bascule brutale. +Les champs précédent/courant gardent leur fondu et les obstacles restent pris +en compte pour les blocs posés. Aucun changement de lumière de gameplay, +de sauvegarde ou de génération ; aucune promesse de lancer de rayons exact. + +## Programme de vérification + +Contrôles natifs OpenGL/Vulkan : direction, sources opposées, murs, rendu de +relief en intérieur, déplacement des sources, transitions et PBR OFF. +Régression des lumières beta.139/140 puis check/build et assemblage. GameTest +serveur dédié exclu selon le refus EULA antérieur. Aucun monde personnel ouvert. + +## Réalisation + +Le champ RGB conserve une texture directionnelle RGBA8 supplémentaire : +les trois composantes codent la direction pondérée, l'alpha sa réponse lumineuse. +Les voisins plus lumineux et accessibles contribuent à l'estimation ; les +arrivées opposées s'annulent dans le vecteur, sans supprimer l'éclairage natif. +Le shader interpole les moments pondérés avant normalisation, y compris entre +les champs précédent et courant. La confiance réduit le relief quand aucune +direction ne domine. + +Cette texture est préparée dans le budget de reconstruction existant, avec +un maximum de six voisins par cellule éclairée et une table d'énergie réutilisée. +Aucune recherche de sources ni rayons par pixel pour les blocs posés. La +lecture GPU interpole huit cellules par champ ; les deux champs sont utilisés +pendant le fondu. Les sources mobiles réutilisent les positions, intensités et +transitions déjà collectées, dans la limite existante de 128 sources. + +Une texture directionnelle occupe environ 1,5 Mo à 16 blocs de distance et +19 Mo à 64 blocs, par copie CPU/GPU ; deux champs coexistent lors des transitions. +Le champ directionnel n'est construit que si PBR est actif avec une intensité +non nulle. Sa désactivation libère les directions après la transition. +Le coût est borné par ces réglages, sans garantie de FPS. + +La passe PBR applique la différence entre normale de base et normale perturbée, +puis un reflet GGX local. Elle garde sa portée de 32 blocs et les réglages PBR +existants ; Lumières colorées doit être actif. Le soleil et la lune continuent +à fonctionner lorsque les lumières colorées sont coupées. Les aides FR/EN +expliquent cette dépendance. + +C'est une direction dominante approximative : deux lumières colorées opposées +ne produisent pas deux reflets indépendants. Les sources mobiles gardent la +propagation radiale et les limites d'occultation existantes. Les normales PBR +concernent les surfaces de blocs prises en charge, pas tous les modèles de mobs. +Les exclusions existantes des panoramas et de l'immersion restent inchangées. + +## Premier essai ciblé + +L'essai natif OpenGL ciblé réussit en **1 min 12 s** : direction gauche/droite, +annulation des sources opposées avec énergie conservée, occultation par un mur, +relief sur la pierre en intérieur et avec une torche en main, stabilité au repos, +libération des directions avec PBR OFF et maintien du chemin solaire avec +Lumières colorées OFF. La somme d'écarts PBR OFF/ON sur la pierre vaut 878 114 +au réglage PBR 100 % ; c'est une comparaison de pixels, pas une mesure de FPS. +Log : `build/directional141-focused.log`. + +Les essais de préparation ont révélé un conflit de nom de matrice GLSL, corrigé, +puis des erreurs de fixture (angle 180° normalisé par Minecraft, suppression +d'entités alors que la sélection était vide, commande d'heure déjà à minuit), +corrigées sans changer le rendu. + +## Validation complète OpenGL + +La chaîne complète réussit en **3 min 1 s** sur OpenGL / Apple M1 : +`COLORED139_PASS`, `COLORED140_PASS` et `DIRECTIONAL141_PASS`. +Elle réunit les contrôles de couleurs, objets portés, caméra, occultation, +rechargement des ressources, préférences, distances/fondus et directions PBR. +Le test ciblé est reproductible avec `-PsanctuaryDirectional141Only=true` ; +la chaîne complète utilise `-PsanctuaryDirectional141ClientTests=true` avec +les deux suites de lumières existantes. +Log : `build/directional141-opengl.log`. Captures natives conservées dans +`build/directional141-opengl-screenshots/`. Comparaison autonome sans retraitement : +`build/Shader-beta.141-comparaison.html`. + +## Validation complète Vulkan + +La même chaîne réussit en **2 min 56 s** sur Vulkan / Apple M1, MoltenVK 1.4.2, +profondeur 0–1 et transparence améliorée. Les trois marqueurs de réussite sont +présents dans `build/directional141-vulkan.log`. L'écart PBR local OFF/ON vaut +878 107, contre 878 114 sous OpenGL dans cette scène. Les captures natives sont +conservées dans `build/directional141-vulkan-screenshots/`. +Les deux moteurs vérifient la compilation, les chemins actifs et désactivés, +le cache, les murs, les sources opposées et portées, ainsi que les régressions +beta.139/140. Pas de validation des pilotes Windows de l'utilisateur revendiquée. + +## Construction et archives + +`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest` +réussit en **2 min 35 s**, 126 tâches. Le GameTest serveur dédié reste exclu +selon le refus EULA antérieur. Les suites clientes ci-dessus ont été exécutées +séparément. Log : `build/shader141-build.log`. + +Comparaison à beta.140 : **8 entrées de production modifiées**, limitées au +champ lumineux, à sa préparation avant le PBR, au shader PBR et aux aides FR/EN. +Les archives normale/Test contiennent le même JAR Sanctuary, sans classes +de test. Version, dépendances exactes, sources des JAR, textures de gemmes et +de la clé de Steve, et template complet sont vérifiés. +Reçu : `build/shader141-artifact.json`. + +SHA-256 du JAR : `660f29c3fa77fee4de6a8db5673c61619985804494f70e961998da36f396d0f1`. + +- Sanctuary-beta.141.mrpack : `08df23dac9e98fbfa2935f747345c38244bddc3a3d9371c6416f251e85a7eb19`. +- Sanctuary-Test-beta.141.mrpack : `a300ef5bfff7b6990c9f5c2760e9c3a23d7a27aeb10b8f04d29db9a7409fa0e1`. diff --git a/gradle.properties b/gradle.properties index 2197a50..bc34012 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.140 -pack_version=beta.140 +mod_version=beta.141 +pack_version=beta.141 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 94249b8..f09dbee 100644 --- a/mods/sanctuary/build.gradle +++ b/mods/sanctuary/build.gradle @@ -34,6 +34,8 @@ if (clientRenderTests) { if (improvedTransparency.isPresent()) { systemProperty('sanctuary.test.improvedTransparency', improvedTransparency.get()) } + systemProperty('sanctuary.test.directional141Only', providers.gradleProperty('sanctuaryDirectional141Only').getOrElse('false')) + systemProperty('sanctuary.test.directional141', providers.gradleProperty('sanctuaryDirectional141ClientTests').getOrElse('false')) 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')) diff --git a/mods/sanctuary/src/clientGametest/java/fr/koka/sanctuary/client/shader/DirectionalLights141ClientChecks.java b/mods/sanctuary/src/clientGametest/java/fr/koka/sanctuary/client/shader/DirectionalLights141ClientChecks.java new file mode 100644 index 0000000..f12ed5b --- /dev/null +++ b/mods/sanctuary/src/clientGametest/java/fr/koka/sanctuary/client/shader/DirectionalLights141ClientChecks.java @@ -0,0 +1,48 @@ +package fr.koka.sanctuary.client.shader; + +import static fr.koka.sanctuary.client.shader.PixelShadows122ClientChecks.*; +import net.fabricmc.fabric.api.client.gametest.v1.context.*; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.Blocks; + +/** Real enclosed stone surfaces: local normal response without sunlight. */ +final class DirectionalLights141ClientChecks { + private static final BlockPos LEFT=new BlockPos(96,101,102),RIGHT=new BlockPos(104,101,102),PROBE=new BlockPos(100,101,102); + static void run(ClientGameTestContext c,TestServerContext server){ + c.runOnClient(m->{VanillaLight.setEnabled(true);VanillaLight.setSaturation(100);VanillaLight.setSsgi(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);VanillaLight.setLensFlare(false);VanillaLight.setEdgeHighlights(false);VanillaLight.setPixelShadows(false);VanillaLight.setSunshafts(false);m.options.particles().set(net.minecraft.server.level.ParticleStatus.MINIMAL);VanillaLight.setColoredLights(true);VanillaLight.setColoredLightIntensity(30);VanillaLight.setColoredLightDistance(16);VanillaLight.setWarmTorches(false);VanillaLight.setPbr(false);VanillaLight.setPbrIntensity(100);}); + camera(c,server,100.5,101,98.5,70);look(c,server,0,35);c.waitTicks(30); + server.runOnServer(s->{if(s.clockManager().getInstance(s.overworld().dimensionType().defaultClock().orElseThrow()).totalTicks()!=18000)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)?Blocks.STONE.defaultBlockState():(y==109||x==88||x==112||z==92||z==116)?Blocks.CONCRETE.white().defaultBlockState():Blocks.AIR.defaultBlockState()); + s.overworld().setBlockAndUpdate(LEFT,Blocks.TORCH.defaultBlockState()); + });c.waitTicks(40);settle(c);var flat=capture(c,"direction-left-pbr-off"); + c.runOnClient(m->VanillaLight.setPbr(true));c.waitFor(m->ColoredLights.directionSample(PROBE)!=0&&ProceduralPbr.ready(),2000);settle(c); + int left=c.computeOnClient(m->ColoredLights.directionSample(PROBE));check(dx(left)<-.9&&Math.abs(dy(left))<.02&&Math.abs(dz(left))<.02,"Cached arrival points toward the left source"); + var relief=capture(c,"direction-left-pbr-on");long delta=difference(flat,relief);check(delta>1000,"Local light visibly affects stone normals without skylight: "+delta); + int builds=c.computeOnClient(m->ColoredLights.builds());c.waitTicks(25);check(c.computeOnClient(m->ColoredLights.builds())==builds,"Static directions reuse the cache");unchanged(relief,capture(c,null),"Static local PBR is stable"); + server.runOnServer(s->{s.overworld().setBlockAndUpdate(LEFT,Blocks.AIR.defaultBlockState());s.overworld().setBlockAndUpdate(RIGHT,Blocks.TORCH.defaultBlockState());});c.waitTicks(40);settle(c); + int right=c.computeOnClient(m->ColoredLights.directionSample(PROBE));check(dx(right)>.9,"Moving the source reverses arrival direction");capture(c,"direction-right-pbr-on"); + server.runOnServer(s->s.overworld().setBlockAndUpdate(LEFT,Blocks.TORCH.defaultBlockState()));c.waitTicks(40);settle(c); + int opposed=c.computeOnClient(m->ColoredLights.directionSample(PROBE));check(Math.abs(dx(opposed))<.02&&Math.abs(dy(opposed))<.02&&Math.abs(dz(opposed))<.02&&(opposed>>>24)>0,"Equal opposing lights cancel direction, retain illumination");capture(c,"direction-opposed"); + // A sealed barrier must prevent both intensity and direction reaching the probe. + server.runOnServer(s->{s.overworld().setBlockAndUpdate(RIGHT,Blocks.AIR.defaultBlockState());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(40);settle(c); + check(c.computeOnClient(m->ColoredLights.directionSample(PROBE))==0,"Closed wall blocks local direction and energy");capture(c,"direction-blocked"); + server.runOnServer(s->{s.overworld().setBlockAndUpdate(LEFT,Blocks.AIR.defaultBlockState()); + 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()); + command(s,"gamemode creative @a");s.getPlayerList().getPlayers().getFirst().setItemSlot(net.minecraft.world.entity.EquipmentSlot.MAINHAND,new net.minecraft.world.item.ItemStack(net.minecraft.world.item.Items.TORCH));}); + c.waitTicks(45);settle(c);capture(c,"direction-held-pbr-on"); + c.runOnClient(m->VanillaLight.setPbr(false));c.waitTicks(35);settle(c);var heldFlat=capture(c,"direction-held-pbr-off"); + c.runOnClient(m->VanillaLight.setPbr(true));c.waitTicks(35);settle(c);var heldRelief=capture(c,"direction-held-pbr-on-final"); + long heldDelta=difference(heldFlat,heldRelief);check(heldDelta>1000&&c.computeOnClient(m->ColoredLights.dynamicCount())==1,"Carried light drives local PBR: "+heldDelta); + server.runOnServer(s->s.getPlayerList().getPlayers().getFirst().setItemSlot(net.minecraft.world.entity.EquipmentSlot.MAINHAND,net.minecraft.world.item.ItemStack.EMPTY));c.waitTicks(35); + c.runOnClient(m->VanillaLight.setPbr(false));c.waitTicks(35);settle(c);check(c.computeOnClient(m->ColoredLights.directionSample(LEFT))==0,"PBR OFF releases directional field"); + c.runOnClient(m->{VanillaLight.setColoredLights(false);VanillaLight.setPbr(true);});c.waitTicks(25);capture(c,"direction-colored-off"); + check(!c.computeOnClient(m->ColoredLights.allocated()),"Color OFF releases local field while PBR keeps its solar path"); + System.out.println("DIRECTIONAL141_PASS localPbrDelta="+delta+" native normals, left/right, opposing sources, wall, held light, stable cache, OFF"); + } + private static double dx(int c){return ((c>>16&255)-128)/127.;} + private static double dy(int c){return ((c>>8&255)-128)/127.;} + private static double dz(int c){return ((c&255)-128)/127.;} + private static void settle(ClientGameTestContext c){c.waitFor(m->ColoredLights.applied()&&ColoredLights.settled(),2400);c.waitTicks(14);} + private static long difference(Frame a,Frame b){long n=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);if(Boolean.getBoolean("sanctuary.test.colored140"))ColoredLights140ClientChecks.run(c,server);return;} + if(coloredOnly){if(Boolean.getBoolean("sanctuary.test.directional141Only")){DirectionalLights141ClientChecks.run(c,server);return;}ColoredLights139ClientChecks.run(c,server);if(Boolean.getBoolean("sanctuary.test.colored140"))ColoredLights140ClientChecks.run(c,server);if(Boolean.getBoolean("sanctuary.test.directional141"))DirectionalLights141ClientChecks.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-beta140-shader-options-" + locale); + c.takeScreenshot("sanctuary-beta141-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-beta140-shader-" + name); + if (name != null) c.takeScreenshot("sanctuary-beta141-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/shader/ColoredLightVolume.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/shader/ColoredLightVolume.java index 0935ad0..27cd3ba 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 @@ -22,7 +22,10 @@ final class ColoredLightVolume implements AutoCloseable { private ClientLevel level; private Object models; private Build build; - private DynamicTexture texture,previous; + private DynamicTexture texture,previous,direction,previousDirection; + private boolean directional; + DynamicTexture direction(){return direction;} + DynamicTexture previousDirection(){return previousDirection;} private BlockPos previousOrigin; private int side,previousSide; private long transitionStarted; @@ -40,18 +43,20 @@ final class ColoredLightVolume implements AutoCloseable { 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; + boolean wantedDirection=VanillaLight.pbr()&&VanillaLight.pbrIntensity()>0; 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;} + if(previous!=null&&blend()>=1){previous.close();previous=null;previousOrigin=null;if(previousDirection!=null){previousDirection.close();previousDirection=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&&(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.directional!=wantedDirection||build.side!=wantedSide||!inside(build.origin,BlockPos.containing(p),build.side))){build.close();build=null;} + if(build==null&&previous==null&&(texture==null||directional!=wantedDirection||side!=wantedSide||!wanted.equals(origin)||published!=revision)&&now-lastStart>=100_000_000L){ + build=new Build(level,wanted,wantedSide,revision,materials,wantedDirection);lastStart=now; } if(build!=null&&build.advance(now+2_000_000L)){ - previous=texture;previousOrigin=origin;previousSide=side; + previous=texture;previousOrigin=origin;previousSide=side;previousDirection=direction; + direction=build.directionImage==null?null:new DynamicTexture(()->"Sanctuary light direction",build.takeDirectionImage());directional=build.directional; 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++; @@ -68,9 +73,10 @@ final class ColoredLightVolume implements AutoCloseable { BlockPos origin(){return origin;} boolean settled(){return texture!=null&&build==null&&revision==published&&blend()>=1;} int completed(){return completed;} + int directionSample(BlockPos p){if(direction==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 direction.getPixels().getPixel(x+(y%cols)*side,z+(y/cols)*side);} int cachedStates(){return materials.cachedStates();} 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;} + public void close(){if(build!=null){build.close();build=null;}if(texture!=null){texture.close();texture=null;}if(previous!=null){previous.close();previous=null;}if(direction!=null){direction.close();direction=null;}if(previousDirection!=null){previousDirection.close();previousDirection=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; @@ -80,11 +86,14 @@ final class ColoredLightVolume implements AutoCloseable { final boolean[] queued;final IntArrayFIFOQueue queue=new IntArrayFIFOQueue(); final LevelChunk[] chunks;final int cx,cz; final BlockPos.MutableBlockPos pos=new BlockPos.MutableBlockPos(); - 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; + NativeImage image,directionImage;final boolean directional;int scan,packed; + private static final double[] ENERGY=new double[256]; + static{for(int i=129;i<256;i++)ENERGY[i]=Math.max(0,(Math.pow(.8,31-i/8.)-Math.pow(.8,15))/(1-Math.pow(.8,15)));} + Build(ClientLevel level,BlockPos origin,int side,long revision,ColoredLightMaterials materials,boolean directional){ + this.directional=directional;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); + if(directional)directionImage=new NativeImage(image.getWidth(),image.getHeight(),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=deadline)return false; } return true; @@ -124,9 +134,26 @@ final class ColoredLightVolume implements AutoCloseable { if(changed)enqueue(n); } } + double energy(int i){return ENERGY[Math.max(red[i]&255,Math.max(green[i]&255,blue[i]&255))];} + int directionAt(int i,int x,int y,int z){ + double energy=energy(i);if(energy<=0)return 0; + double vx=0,vy=0,vz=0,total=0; + 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*side+ny*side*side;double w=Math.max(0,energy(n)-energy); + if(w<=0||net.minecraft.world.level.lighting.LightEngine.getLightDampeningInto(states[n],states[i],d.getOpposite(),Math.max(1,states[i].getLightDampening()))>=15)continue; + vx+=d.getStepX()*w;vy+=d.getStepY()*w;vz+=d.getStepZ()*w;total+=w; + } + // The vector length is confidence: opposing arrivals cancel instead of flipping. + double scale=total>1e-12?127/total:0; + int a=(int)Math.round(255*energy/(energy+.08)); + return a<<24|(int)Math.round(128+vx*scale)<<16|(int)Math.round(128+vy*scale)<<8|(int)Math.round(128+vz*scale); + } + NativeImage takeDirectionImage(){var result=directionImage;directionImage=null;return result;} 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;}} + public void close(){if(image!=null){image.close();image=null;}if(directionImage!=null){directionImage.close();directionImage=null;}} } // An offset preserves dim colour channels below native level zero. The shader // applies the native cutoff to overall energy, never to each channel separately. 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 1f2b5ba..2053c2a 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 @@ -39,15 +39,15 @@ public final class ColoredLights { private static float displayedDistance; private static int dynamicCount; private static MappableRingBuffer uniforms; - private static boolean applied; + private static boolean applied,prepared; 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){if(!fr.koka.sanctuary.client.DynamicLightClient.invalidatingSections())VOLUME.dirtySection(x,y,z);} - public static void render(){ - applied=false;var mc=Minecraft.getInstance(); + public static void prepare(){ + applied=false;prepared=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; @@ -78,6 +78,20 @@ public final class ColoredLights { for(int i=0;i"Sanctuary PBR scene",GpuBuffer.USAGE_UNIFORM|GpuBuffer.USAGE_MAP_WRITE,192); + // Padding supplies valid inactive PixelShadows and local-light uniform bindings. + if(scene==null)scene=new MappableRingBuffer(()->"Sanctuary PBR scene",GpuBuffer.USAGE_UNIFORM|GpuBuffer.USAGE_MAP_WRITE,176+ColoredDynamicLights.LIMIT*32); var entries=TERRAIN.update();var atlas=TERRAIN.materials.atlas(); var vp=new Matrix4f(projection).mul(camera.viewRotationMatrix);var fog=camera.fogData; var sun=PixelShadows.sunDirection(sky.sunAngle);boolean lunar=sun.y<=.08f; @@ -65,7 +66,7 @@ public final class ProceduralPbr { Std140Builder.intoBuffer(mapped.data()).putMat4f(new Matrix4f(vp).invert()) .putVec4(sun.x,sun.y,sun.z,strength) .putVec4(VanillaLight.pbrIntensity()/100f,RenderSystem.getDevice().getDeviceInfo().isZZeroToOne()?1:0,shadow?1:0,32) - .putVec4(fog.renderDistanceStart,fog.renderDistanceEnd,fog.environmentalStart,fog.environmentalEnd).putVec4(fog.color.w,0,0,0) + .putVec4(fog.renderDistanceStart,fog.renderDistanceEnd,fog.environmentalStart,fog.environmentalEnd).putVec4(fog.color.w,ColoredLights.directionalReady()?1:0,0,0) .putVec4(0,0,0,0).putVec4(0,0,0,0).putVec4(0,0,0,0).putVec4(0,0,0,0); } var transforms=new ArrayList(entries.size());int max=0; @@ -81,7 +82,10 @@ public final class ProceduralPbr { pass.setUniform("SunDepth",shadow?PixelShadows.materialDepth():main.getDepthTextureView(),nearest); pass.setUniform("WorldDepth",main.getDepthTextureView(),nearest);pass.setUniform("SceneColor",source.getColorTextureView(),nearest); pass.setUniform("BlockAtlas",mc.getTextureManager().getTexture(TextureAtlas.LOCATION_BLOCKS).getTextureView(),nearest); - pass.setUniform("MaterialAtlas",atlas.getTextureView(),nearest);pass.setIndexBuffer(indices.getBuffer(),indices.type()); + pass.setUniform("MaterialAtlas",atlas.getTextureView(),nearest); + if(ColoredLights.directionalReady())ColoredLights.bindDirections(pass); + else{pass.setUniform("ColoredLightScene",scene.currentBuffer());pass.setUniform("LightDirection",source.getColorTextureView(),nearest);pass.setUniform("PreviousDirection",source.getColorTextureView(),nearest);} + pass.setIndexBuffer(indices.getBuffer(),indices.type()); for(int i=0;itexture(SunDepth,q.xy).r+0.00015?0.0:1.0; } +// Cached direction moments; filtering moments before normalization avoids unstable +// switches between nearby/opposed sources. Alpha stores the light response. +vec4 directionField(sampler2D field,vec3 p,vec4 atlas){ + if(textureSize(field,0).x.5?directionField(PreviousDirection,p+PreviousCamera.xyz+offset,PreviousAtlas):vec4(0); + field=mix(before,field,smoothstep(0.0,1.0,VolumeSize.y)); + } + for(int i=0;i=radius||distance<.001)continue; + float power=max(0.0,(pow(.8,15.0-radius+distance)-pow(.8,15.0))/(1.0-pow(.8,15.0))); + float weight=power/(power+.08)*smoothstep(0.0,1.0,DynamicColor[i].a); + field+=vec4(delta/distance*weight,weight); + } + float magnitude=length(field.xyz);if(magnitude<.00001||field.a<.00001)return vec4(0); + float confidence=smoothstep(.02,1.0,magnitude/field.a); + float fade=(1.0-smoothstep(VolumeSize.x*.75,VolumeSize.x,length(p)))*smoothstep(0.0,1.0,VolumeSize.z); + return vec4(field.xyz/magnitude,confidence*min(field.a,1.0)*fade*clamp(CameraStrength.w/0.3,0.0,1.0)); +} +vec3 localShine(vec3 n,vec3 v,vec3 l,vec4 material,vec3 albedo){ + vec3 halfVector=l+v;float size=dot(halfVector,halfVector);if(size<.00001)return vec3(0); + vec3 h=halfVector*inversesqrt(size); + float nl=max(dot(n,l),0.0),nv=max(dot(n,v),.001),nh=max(dot(n,h),0.0),vh=max(dot(v,h),0.0); + float roughness=max(material.b,.25),alpha=roughness*roughness,a2=alpha*alpha; + float denom=nh*nh*(a2-1.0)+1.0,distribution=a2/max(3.14159265*denom*denom,.00001); + float k=(roughness+1.0)*(roughness+1.0)/8.0; + float geometry=nv/(nv*(1.0-k)+k)*nl/max(nl*(1.0-k)+k,.00001); + vec3 f0=mix(vec3(.04),albedo,material.a),fresnel=f0+(1.0-f0)*pow(1.0-vh,5.0); + return distribution*geometry*fresnel/max(4.0*nv*nl,.0001)*nl; +} void main(){ // Derivatives precede divergent discard; UV-space tangents preserve rotated/mirrored block textures. vec3 dx=dFdx(relativePosition),dy=dFdy(relativePosition); @@ -67,12 +127,18 @@ void main(){ clamp((length(relativePosition)-PbrFog.z)/max(PbrFog.w-PbrFog.z,.001),0.0,1.0)); float amount=PbrSun.w*pow(tint.a,8.0)*visibility(relativePosition,baseNormal)*(1.0-fog*PbrEnvironment.x) *(1.0-smoothstep(PbrParams.w*.75,PbrParams.w,length(relativePosition))); - if(amount<=0.0001)discard; + vec4 arrival=localArrival(relativePosition,baseNormal); + float localAmount=arrival.w*(1.0-fog*PbrEnvironment.x)*(1.0-smoothstep(PbrParams.w*.75,PbrParams.w,length(relativePosition))); + if(amount<=0.0001&&localAmount<=0.0001)discard; vec3 scene=texture(SceneColor,uv).rgb;vec3 linear=scene*scene; // Native diffuse light already exists. Apply only the perturbed-normal difference, then add GGX. float relief=(nl-max(dot(baseNormal,l),0.0))*.7; vec3 lit=clamp(linear*(1.0+relief*amount),0.0,1.0); - vec3 shine=min(specular*PbrParams.x*amount*3.0,vec3(0.5)); + if(localAmount>.0001){ + float localRelief=(max(dot(n,arrival.xyz),0.0)-max(dot(baseNormal,arrival.xyz),0.0))*.7; + lit=clamp(lit*(1.0+localRelief*localAmount),0.0,1.0); + } + vec3 shine=min((specular*amount+localShine(n,v,arrival.xyz,material,albedo)*localAmount)*PbrParams.x*3.0,vec3(0.5)); lit+=(1.0-lit)*(1.0-exp(-shine)); fragColor=vec4(sqrt(clamp(lit,0.0,1.0)),1.0); } diff --git a/packwiz/pack.toml b/packwiz/pack.toml index 27dcc07..fa1faba 100644 --- a/packwiz/pack.toml +++ b/packwiz/pack.toml @@ -1,6 +1,6 @@ name = "Sanctuary" author = "KOKA99CAB" -version = "beta.140" +version = "beta.141" pack-format = "packwiz:1.1.0" [index]