diff --git a/README.md b/README.md index 335dc71..c6e76b2 100644 --- a/README.md +++ b/README.md @@ -20,13 +20,11 @@ La [vision complète](docs/vision.md) conserve les intentions ; le L’économie, les claims et les dimensions décrits dans la vision restent à implémenter. -La version **beta.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 beta.141 est publiée et synchronisée dans l’instance Sanctuary Beta ; -les 923 fichiers personnels suivis sont conservés. +La version **beta.142** utilise Vulkan comme moteur graphique par défaut, +stabilise les rafraîchissements du relief PBR et règle son intensité initiale +à 80 %. Les lectures de textures et calculs de reflets sont sécurisés contre +les résultats indéfinis. Le [ticket](docs/pbr-refresh-beta142.md) distingue les +corrections vérifiées du bruit signalé en mouvement sur le pilote de l'utilisateur. La [livraison précédente](docs/clay-workshop-integration-beta110.md) réunit déjà l’atelier, les œufs et la neige saisonnière dans l’instance **Sanctuary Beta**. Les [synchronisations antérieures](docs/git-sync-beta092.md) restent documentées. @@ -37,6 +35,14 @@ La [carte native beta.005](docs/atlas-beta005.md) prend le relais après l’étude de Xaero et des alternatives libres. +## beta.142 — Vulkan par défaut et stabilité PBR + +Moteur natif Vulkan prioritaire, relief conservé pendant les reconstructions, +calculs de reflets bornés et intensité initiale 80 %. Choix personnels conservés. +[Contrat et vérifications](docs/pbr-refresh-beta142.md) · +[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.142/Sanctuary-beta.142.mrpack) · +[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.142/Sanctuary-Test-beta.142.mrpack). + ## beta.141 — Direction locale et normales PBR Relief et reflets orientés par les lumières locales. Cache compact, confiance diff --git a/docs/pbr-refresh-beta142.md b/docs/pbr-refresh-beta142.md new file mode 100644 index 0000000..71b9b60 --- /dev/null +++ b/docs/pbr-refresh-beta142.md @@ -0,0 +1,154 @@ +# PBR-142 — Stabilité du relief et Vulkan par défaut + +Socle beta.141 `7ce01c4`, branche `codex/pbr-refresh-beta142`. +Minecraft 26.3 / Java 25. Livraison beta.142. + +## Contrat + +Corriger le clignotement lors de la reconstruction des sections PBR : conserver +le rendu précédent jusqu'à ce que le remplaçant soit disponible et éviter les +reconstructions dues seulement aux lumières dynamiques. Une invalidation pendant +une reconstruction doit être conservée, sans annuler indéfiniment le travail. +Le PBR passe à 80 % par défaut, valeur confirmée par l'utilisateur ; les choix +explicitement enregistrés restent conservés. + +La capture `2026-09-18_04.19.34.png` montre des points très clairs, plus fins +que les pixels des textures, sur des parois et un plafond de grotte. L'utilisateur +confirme OpenGL et leur disparition quand seul le PBR est coupé. Examiner +les lectures de textures et les calculs aux angles rasants séparément du cache. + +Tests natifs prévus : recalcul de section sous observation, invalidations +répétées, remplacement atomique, suppression réelle de blocs, lumière portée, +cache et préférences ; OpenGL et Vulkan. Pas de monde personnel ouvert. + +## Précision du signalement et implémentation + +L'utilisateur précise ensuite que le bruit apparaît **en mouvement**, et demande +le passage à Vulkan. Il ne faut donc pas attribuer les points de sa capture au +seul clignotement de reconstruction : ce sont deux vérifications distinctes. + +- Les sections PBR publiées restent affichées jusqu'au remplacement de leur + maillage. Une nouvelle invalidation durant la construction est mémorisée ; + elle n'annule plus continuellement le travail. Le budget permanent reste + 24 Mio, avec temporairement l'ancien et le nouveau maillage d'une section. +- Les invalidations de lumière dynamique ne régénèrent plus la géométrie PBR. + Les changements de blocs et de lumière du ciel restent pris en compte. +- Lectures explicites des texels de profondeur/scène et du niveau de matériau, + gradients de texture calculés avant les rejets de fragments. Les produits + scalaires des reflets sont bornés, la puissance entière de Fresnel est + explicite, les résultats non finis ne remplacent pas la scène native. + Aucun flou ajouté aux textures. +- Le choix graphique natif « Par défaut » devient « Vulkan (par défaut) » + (FR/EN) et essaie Vulkan en premier. Minecraft conserve son repli OpenGL si + Vulkan ne peut pas être initialisé. Un choix **OpenGL explicite** reste + disponible ; pour une installation ainsi réglée, sélectionner Vulkan dans + les réglages vidéo puis redémarrer. Aucun moteur externe ni VulkanMod ajouté. +- 80 % est la nouvelle intensité PBR initiale. Les valeurs déjà enregistrées + explicitement restent conservées. + +Le code Minecraft 26.3 a été inspecté : `PreferredGraphicsApi.VULKAN` essaie +Vulkan puis OpenGL ; le choix natif `DEFAULT` essayait auparavant OpenGL d'abord. +Tous les effets Sanctuary utilisent déjà l'API graphique native. Le seul hook +OpenGL direct est le correctif spécifique de lecture des panoramas dans +`GlCommandEncoder` ; il ne remplace pas la voie Vulkan native. + +Référence pour les lectures après contrôle divergent : +[documentation Khronos des samplers](https://wikis.khronos.org/opengl/Sampler_%28GLSL%29). +La correction rend ces lectures explicites ; elle ne constitue pas à elle seule +une reproduction du défaut sur le pilote Windows de l'utilisateur. + +## Observations de diagnostic + +Le détecteur de points blancs isolés (minimum RGB 170, au moins cinq voisins +sur huit sous 120) compte 669 points sur l'image utilisateur et zéro sur la +première grotte de contrôle. Il s'agit de scènes, résolutions et pilotes différents, +pas d'un avant/après reproduisant exactement le bug utilisateur. Les rendus +natifs OpenGL de contrôle étaient déjà exempts de ce grain avant les nouvelles +protections du shader ; le signalement Windows reste donc à reconfirmer là-bas. + +Le test de rafraîchissement conserve les mêmes maillages publiés durant douze +invalidations consécutives, compare l'image et vérifie que des remplacements +aboutissent. Le test de torche alterne quatre fois les torches normales/des âmes +sans reconstruction des normales. Le contrôle de mouvement presse réellement +Avancer, maintient la torche et le PBR actifs et vérifie un déplacement d'au moins +un bloc avant de vérifier les 24 captures. Les angles -30°, 0° et 35° sont aussi +contrôlés, ainsi que les effets combinés et les préférences. + +### Domaine invalide du reflet de la torche en main + +Pour la lumière du joueur en vue subjective, la source est la caméra : le +vecteur lumière et le vecteur de vue sont parallèles. L'ancien +`pow(1 - max(dot(v,h),0),5)` n'avait aucune borne supérieure pour ce produit +scalaire. Un calcul reproductible Float32 (100 000 positions, graine 142) +donne 19 991 bases légèrement négatives, jusqu'à -2,3841858e-7, contre zéro +après clamp. C'est une reproduction numérique d'un **domaine de calcul invalide**, +pas une reproduction des pixels produits par le pilote utilisateur. + +GLSL 3.30 ne définit pas `pow` pour une base négative, même avec un exposant +entier : [spécification Khronos, fonctions exponentielles](https://registry.khronos.org/OpenGL/specs/gl/GLSLangSpec.3.30.pdf). +Le correctif borne les cosinus à [0,1] et remplace cette puissance par des +multiplications. La torche alignée sur la caméra ne dépend donc plus du traitement +de cette entrée invalide par le pilote. Reçu numérique ignoré : +`build/pbr142-fresnel-domain.json`. + +### Mise à jour des fixtures antérieures + +La suite Vulkan complète s'est arrêtée au contrôle SSGI de coin : l'oracle +ne trouvait plus de pierre neutre (`rows=0`), car les lumières colorées ajoutées +depuis beta.139/140 teintaient la pierre avec la lave. Le test de proximité SSGI +et le test PBR solaire isolent désormais leur effet en coupant cette autre passe. +Aucun seuil n'a été diminué et aucun changement de rendu SSGI n'est livré. +Le sous-ensemble en échec est relancé séparément ; les étapes précédentes +(ombres, CTM, bloom, lens flare, déplacement SSGI) avaient réussi. + +## Vérifications natives + +Sur Mac Apple M1, Java 25 et Minecraft 26.3 : + +- OpenGL : suite couleurs/directions/rafraîchissement en 3 min 36 s, puis + déplacement continu avec torche active en 1 min 36 s. Zéro point blanc isolé + sur les 24 images de mouvement et les trois angles de grotte. +- Vulkan/MoltenVK 1.4.2 : sélection automatique sans argument de moteur, + couleurs/directions/rafraîchissement en 3 min 47 s. Dernier test continu avec + torche active en 1 min 37 s, zéro point blanc isolé sur les 24 images. +- Ombrages lune/midi/feuilles, distances et précisions, stabilité temps réel, + CTM, bloom (minerais, torche, lampe, lave, soleil), émissifs indépendants et + lens flare validés sous Vulkan dans la suite d'effets. Le déplacement SSGI + sur douze blocs passe également. La suite s'arrête ensuite sur l'oracle + ancien de couleur de pierre décrit ci-dessus : ne pas présenter ce lancement + de 7 min 15 s comme une réussite complète. +- Reprise ciblée après isolation de cet oracle : SSGI près des surfaces puis + PBR extérieur réussis sous Vulkan en 2 min 55 s. Proximité 35/100, haute + résolution, léger mouvement, toit ajouté/retiré, reload, resize, zéro/OFF et + effets combinés. Gain SSGI au coin test : 6,01 sur 99 lignes à 35 %. +- Les nouvelles valeurs initiales et les choix enregistrés sont testés, dont + une intensité PBR explicitement enregistrée à 50 qui reste à 50. + +Les premiers essais du nouveau contrôle de mouvement ont permis de corriger +son entrée clavier : `pressKey` ne maintient pas une touche enfoncée. Le test +final utilise `holdKey` puis `releaseKey`, vérifie le déplacement réel et refuse +la disparition de la lumière portée ou du PBR pendant les captures. + +Les jeux de test utilisent uniquement des mondes neufs et ignorés. La capture +et le pilote Windows utilisateur n'ont pas été reproduits à l'identique ; leur +retest reste ouvert malgré les corrections numériques et les contrôles natifs. + +## Construction et archives + +`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest` +réussit en **2 min 38 s**, 126 tâches. Le GameTest serveur dédié reste exclu +selon le refus EULA antérieur ; les suites clientes ci-dessus ont réellement +utilisé les deux moteurs natifs. `git diff --check` passe. + +Les archives normale/Test embarquent le même JAR, leurs versions/index/hashes +sont vérifiés, leurs sources correspondent aux fichiers courants. Le template +contient exactement les ressources attendues. Les onze entrées de production +modifiées par rapport à beta.141 sont limitées au PBR, au choix graphique et +aux traductions ; les ressources de jeu antérieures sont conservées. + +SHA-256 du JAR : `0bc2da9df2ef004e6dc78932cfde7b31b7717c63c83ceed789d151177c41bc1a`. + +- `Sanctuary-beta.142.mrpack` : 10464746 octets ; SHA-256 `dba4fd8e1e3b20136faa1c1de3494df3cb250c87de199c04e07883c786ca48ee`. +- `Sanctuary-Test-beta.142.mrpack` : 10483672 octets ; SHA-256 `d56b81a4aea6649a40a3c49dedee904989943a518ad0ae7c520b808dd0f9b9a7`. + +Reçu détaillé ignoré : `build/shader142-artifact.json`. diff --git a/gradle.properties b/gradle.properties index bc34012..509bf20 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.141 -pack_version=beta.141 +mod_version=beta.142 +pack_version=beta.142 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 f09dbee..577a118 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.pbr142', providers.gradleProperty('sanctuaryPbr142ClientTests').getOrElse('false')) 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')) 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 index f12ed5b..61c141d 100644 --- 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 @@ -20,6 +20,7 @@ final class DirectionalLights141ClientChecks { 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"); + if(Boolean.getBoolean("sanctuary.test.pbr142"))PbrRefresh142ClientChecks.refresh(c,server,relief); 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); @@ -33,11 +34,13 @@ final class DirectionalLights141ClientChecks { 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"); + if(Boolean.getBoolean("sanctuary.test.pbr142"))PbrRefresh142ClientChecks.held(c,server); 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"); + if(Boolean.getBoolean("sanctuary.test.pbr142"))PbrRefresh142ClientChecks.cave(c,server); 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.;} diff --git a/mods/sanctuary/src/clientGametest/java/fr/koka/sanctuary/client/shader/Pbr134ClientChecks.java b/mods/sanctuary/src/clientGametest/java/fr/koka/sanctuary/client/shader/Pbr134ClientChecks.java index a3b142f..fecddbb 100644 --- a/mods/sanctuary/src/clientGametest/java/fr/koka/sanctuary/client/shader/Pbr134ClientChecks.java +++ b/mods/sanctuary/src/clientGametest/java/fr/koka/sanctuary/client/shader/Pbr134ClientChecks.java @@ -13,7 +13,7 @@ import java.util.Arrays; public final class Pbr134ClientChecks implements FabricClientGameTest { public void runTest(ClientGameTestContext c){new PixelShadows122ClientChecks(false,false,true).runTest(c);} static void run(ClientGameTestContext c,TestServerContext server){ - c.runOnClient(m->{preferences();VanillaLight.setSunshafts(false);VanillaLight.setLensFlare(false);materials();VanillaLight.setEnabled(true);VanillaLight.setSaturation(100);VanillaLight.setPbr(false);VanillaLight.setSsgi(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);VanillaLight.setEdgeHighlights(false);VanillaLight.setPixelShadows(false);}); + c.runOnClient(m->{preferences();VanillaLight.setSunshafts(false);VanillaLight.setLensFlare(false);materials();VanillaLight.setEnabled(true);VanillaLight.setSaturation(100);VanillaLight.setColoredLights(false);VanillaLight.setPbr(false);VanillaLight.setSsgi(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);VanillaLight.setEdgeHighlights(false);VanillaLight.setPixelShadows(false);}); server.runOnServer(s->{command(s,"time set 1000");for(int x=-5;x<=24;x++)for(int z=-5;z<=24;z++){ s.overworld().setBlockAndUpdate(new BlockPos(x,64,z),(x<10?Blocks.BRICKS:x<17?Blocks.OAK_PLANKS:Blocks.IRON_BLOCK).defaultBlockState()); for(int y=65;y<=102;y++)s.overworld().setBlockAndUpdate(new BlockPos(x,y,z),Blocks.AIR.defaultBlockState()); @@ -61,7 +61,7 @@ public final class Pbr134ClientChecks implements FabricClientGameTest { } private static void preferences(){try{ var p=FabricLoader.getInstance().getConfigDir().resolve("sanctuary-shaders.json");Files.writeString(p,"{\"ssgi\":true,\"ssgiIntensity\":35,\"bloom\":false}");forgetPreferences(); - check(VanillaLight.pbr()&&VanillaLight.pbrIntensity()==50&&VanillaLight.ssgi()&&!VanillaLight.bloom(),"Missing PBR setting defaults ON while preserving explicit old choices"); + check(VanillaLight.pbr()&&VanillaLight.pbrIntensity()==80&&VanillaLight.ssgi()&&!VanillaLight.bloom(),"Missing PBR setting defaults ON while preserving explicit old choices"); for(int n:new int[]{-1,0,50,100,101}){VanillaLight.setPbr(true);VanillaLight.setPbrIntensity(n);forgetPreferences();check(VanillaLight.pbr()&&VanillaLight.pbrIntensity()==Math.clamp(n,0,100),"PBR preference persistence/clamp");} VanillaLight.setPbrIntensity(50); }catch(Exception e){throw new AssertionError(e);}} diff --git a/mods/sanctuary/src/clientGametest/java/fr/koka/sanctuary/client/shader/PbrRefresh142ClientChecks.java b/mods/sanctuary/src/clientGametest/java/fr/koka/sanctuary/client/shader/PbrRefresh142ClientChecks.java new file mode 100644 index 0000000..df95e35 --- /dev/null +++ b/mods/sanctuary/src/clientGametest/java/fr/koka/sanctuary/client/shader/PbrRefresh142ClientChecks.java @@ -0,0 +1,63 @@ +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; +import net.minecraft.world.item.*; +import net.minecraft.world.entity.EquipmentSlot; + +final class PbrRefresh142ClientChecks { + static void refresh(ClientGameTestContext c,TestServerContext s,Frame reference){ + int entries=c.computeOnClient(m->ProceduralPbr.terrainEntries()),builds=c.computeOnClient(m->ProceduralPbr.terrainBuilds());check(entries>0,"PBR fixture has visible sections"); + for(int i=0;i<12;i++){ + c.runOnClient(m->{for(var section:m.levelRenderer.visibleSections()){var p=section.getRenderOrigin();ProceduralPbr.dirtySection(p.getX()>>4,p.getY()>>4,p.getZ()>>4);} + check(ProceduralPbr.terrainEntries()==entries,"Invalidation retains every published PBR section");}); + c.waitTicks(1);unchanged(reference,capture(c,null),"PBR remains visible throughout repeated refreshes"); + } + ready(c);check(c.computeOnClient(m->ProceduralPbr.terrainBuilds())>builds,"Replacement meshes actually published");unchanged(reference,capture(c,"refresh-stable"),"Replacement preserves the rendered normals"); + System.out.println("PBR142_REFRESH_PASS entries="+entries+" repeated invalidation, retained rendering, completed replacements"); + } + static void held(ClientGameTestContext c,TestServerContext s){ + ready(c);int builds=c.computeOnClient(m->ProceduralPbr.terrainBuilds()); + for(var item:new Item[]{Items.SOUL_TORCH,Items.TORCH,Items.SOUL_TORCH,Items.TORCH}){ + s.runOnServer(server->server.getPlayerList().getPlayers().getFirst().setItemSlot(EquipmentSlot.MAINHAND,new ItemStack(item)));c.waitTicks(25); + } + check(c.computeOnClient(m->ProceduralPbr.terrainBuilds())==builds,"Carried light updates do not rebuild the normal mesh"); + System.out.println("PBR142_HELD_PASS native dynamic relighting preserves the PBR geometry cache"); + } + static void cave(ClientGameTestContext c,TestServerContext s){ + c.runOnClient(m->{VanillaLight.setColoredLights(true);VanillaLight.setPbr(false);VanillaLight.setPbrIntensity(80);VanillaLight.setColoredLightDistance(16);}); + camera(c,s,0.5,65,2.5,70);look(c,s,0,-12);c.waitTicks(30); + s.runOnServer(server->{for(int x=-5;x<=5;x++)for(int z=-1;z<=18;z++)for(int y=64;y<=70;y++) + server.overworld().setBlockAndUpdate(new BlockPos(x,y,z),(x==-5||x==5||y==64||y==70||z==-1||z==18)?Blocks.DEEPSLATE.defaultBlockState():Blocks.AIR.defaultBlockState()); + for(int y=65;y<=69;y++)server.overworld().setBlockAndUpdate(new BlockPos(-2,y,9),Blocks.DEEPSLATE.defaultBlockState()); + for(int z=5;z<14;z++)server.overworld().setBlockAndUpdate(new BlockPos(3,67,z),Blocks.DEEPSLATE.defaultBlockState()); + server.overworld().setBlockAndUpdate(new BlockPos(3,68,8),Blocks.WATER.defaultBlockState()); + server.getPlayerList().getPlayers().getFirst().setItemSlot(EquipmentSlot.MAINHAND,new ItemStack(Items.TORCH)); + });c.waitTicks(100);var off=capture(c,"cave-pbr-off");c.runOnClient(m->VanillaLight.setPbr(true));ready(c);var on=capture(c,"cave-pbr-80"); + check(specks(on)<=specks(off)+4,"PBR does not introduce isolated white points in the cave"); + System.out.println("PBR142_CAVE_PIXELS off="+specks(off)+" on="+specks(on)); + // Capture while the camera is actually travelling, before settling caches. + s.runOnServer(server->{var player=server.getPlayerList().getPlayers().getFirst();player.getAbilities().flying=true;player.onUpdateAbilities();});c.waitTicks(10); + var start=c.computeOnClient(m->m.player.position()); + c.getInput().holdKey(o->o.keyUp); + int maximumSpecks=0; + try {for(int frame=0;frame<24;frame++){ + c.waitTicks(1);c.runOnClient(m->check(ColoredLights.dynamicCount()>0&&ProceduralPbr.applied(),"Carried light and PBR remain active during motion"));var moving=capture(c,frame%8==0?"cave-moving-"+frame:null); + maximumSpecks=Math.max(maximumSpecks,specks(moving)); + }} finally {c.getInput().releaseKey(o->o.keyUp);} + check(c.computeOnClient(m->m.player.position().distanceToSqr(start))>1.0,"Camera really travels at least one block"); + check(maximumSpecks<=4,"Moving camera has no isolated white grain: "+maximumSpecks); + System.out.println("PBR142_MOVEMENT_PASS frames=24 maxSpecks="+maximumSpecks); + camera(c,s,0.5,65,2.5,70); + for(float pitch:new float[]{-30,0,35}){look(c,s,0,pitch);ready(c);var frame=capture(c,"cave-angle-"+(int)pitch);check(specks(frame)<=4,"Grazing-angle PBR has no white grain");System.out.println("PBR142_CAVE_ANGLE "+pitch+" specks="+specks(frame));} + c.runOnClient(m->{VanillaLight.setSsgi(true);VanillaLight.setBloom(true);VanillaLight.setPixelShadows(true);VanillaLight.setEdgeHighlights(true);VanillaLight.setSunshafts(true);});c.waitTicks(40);ready(c);capture(c,"cave-combined"); + System.out.println("PBR142_PASS refresh, dynamic cache, defaults, cave and grazing-angle captures"); + } + private static int specks(Frame f){int count=0,w=f.width();for(int y=2;y>16&255,Math.min(p>>8&255,p&255));if(value<170)continue; + int dark=0;for(int dy=-1;dy<=1;dy++)for(int dx=-1;dx<=1;dx++){if(dx==0&&dy==0)continue;int q=f.pixels()[i+dy*w+dx];if(Math.max(q>>16&255,Math.max(q>>8&255,q&255))<120)dark++;}if(dark>=5)count++; + }return count;} + private static void ready(ClientGameTestContext c){c.waitTicks(14);c.waitFor(m->ProceduralPbr.ready(),2400);c.waitTicks(8);} +} diff --git a/mods/sanctuary/src/clientGametest/java/fr/koka/sanctuary/client/shader/PixelShadows122ClientChecks.java b/mods/sanctuary/src/clientGametest/java/fr/koka/sanctuary/client/shader/PixelShadows122ClientChecks.java index 52035f6..efd212c 100644 --- a/mods/sanctuary/src/clientGametest/java/fr/koka/sanctuary/client/shader/PixelShadows122ClientChecks.java +++ b/mods/sanctuary/src/clientGametest/java/fr/koka/sanctuary/client/shader/PixelShadows122ClientChecks.java @@ -54,7 +54,9 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest { public void runTest(ClientGameTestContext c) { c.runOnClient(m -> { var info = com.mojang.blaze3d.systems.RenderSystem.getDevice().getDeviceInfo(); - String expected = System.getProperty("sanctuary.test.graphicsBackend", ""); + String expected = System.getProperty("sanctuary.test.graphicsBackend", "vulkan"); + check(net.minecraft.client.PreferredGraphicsApi.DEFAULT.getBackendsToTry()[0] instanceof com.mojang.renderpearl.backend.vulkan.VulkanBackend, "Default tries native Vulkan first"); + check(net.minecraft.client.PreferredGraphicsApi.OPENGL.getBackendsToTry()[0] instanceof com.mojang.renderpearl.backend.opengl.GlBackend, "Explicit OpenGL remains available"); check(expected.isEmpty() || info.backendName().equalsIgnoreCase(expected), "Requested graphics backend must not silently fall back: " + expected + " / " + info.backendName()); System.out.println("SHADER139_BACKEND " + info.backendName() + " / " + info.name() @@ -281,7 +283,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest { }); }); c.waitTicks(3); - c.takeScreenshot("sanctuary-beta141-shader-options-" + locale); + c.takeScreenshot("sanctuary-beta142-shader-options-" + locale); } c.runOnClient(m -> m.gui.setScreen(null)); } @@ -450,11 +452,12 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest { var path=FabricLoader.getInstance().getConfigDir().resolve("sanctuary-shaders.json"); Files.deleteIfExists(path);forgetPreferences(); check(VanillaLight.bloom()&&VanillaLight.bloomIntensity()==20&&VanillaLight.pbr()&&VanillaLight.ssgi(),"Fresh defaults: bloom20, PBR and SSGI ON"); - check(VanillaLight.pbrIntensity()==50&&VanillaLight.ssgiIntensity()==35,"Existing effect strengths retained"); + check(VanillaLight.pbrIntensity()==80&&VanillaLight.ssgiIntensity()==35,"PBR default 80, SSGI strength retained"); Files.writeString(path,"{\"pbr\":false,\"ssgi\":false,\"bloomIntensity\":73}");forgetPreferences(); check(!VanillaLight.pbr()&&!VanillaLight.ssgi()&&VanillaLight.bloomIntensity()==73,"Explicit personal toggles and intensity survive changed defaults"); Files.writeString(path,"{\"bloom\":false}");forgetPreferences(); check(!VanillaLight.bloom()&&VanillaLight.bloomIntensity()==20&&VanillaLight.pbr()&&VanillaLight.ssgi(),"Missing fields get new defaults without replacing explicit values"); + Files.writeString(path,"{\"pbrIntensity\":50}");forgetPreferences();check(VanillaLight.pbrIntensity()==50,"Explicit PBR 50 is preserved"); System.out.println("SHADER135_DEFAULTS_PASS fresh, partial and personal preferences"); } catch(IOException e){throw new AssertionError(e);} } @@ -565,7 +568,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest { } static Frame capture(ClientGameTestContext c, String name) { - if (name != null) c.takeScreenshot("sanctuary-beta141-shader-" + name); + if (name != null) c.takeScreenshot("sanctuary-beta142-shader-" + name); var result = new CompletableFuture(); c.runOnClient(m -> Screenshot.takeScreenshot(m.gameRenderer.mainRenderTarget(), image -> { try (image) { diff --git a/mods/sanctuary/src/clientGametest/java/fr/koka/sanctuary/client/shader/Ssgi132ClientChecks.java b/mods/sanctuary/src/clientGametest/java/fr/koka/sanctuary/client/shader/Ssgi132ClientChecks.java index a0f03aa..504a197 100644 --- a/mods/sanctuary/src/clientGametest/java/fr/koka/sanctuary/client/shader/Ssgi132ClientChecks.java +++ b/mods/sanctuary/src/clientGametest/java/fr/koka/sanctuary/client/shader/Ssgi132ClientChecks.java @@ -13,7 +13,7 @@ import java.nio.file.Files; public final class Ssgi132ClientChecks implements FabricClientGameTest { public void runTest(ClientGameTestContext c){new PixelShadows122ClientChecks(false,true).runTest(c);} static void run(ClientGameTestContext c,TestServerContext server){ - c.runOnClient(m->{preferences();VanillaLight.setSunshafts(false);VanillaLight.setLensFlare(false);VanillaLight.setPbr(false);VanillaLight.setEnabled(true);VanillaLight.setSaturation(100);VanillaLight.setSsgi(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);VanillaLight.setPixelShadows(false);VanillaLight.setEdgeHighlights(false);}); + c.runOnClient(m->{preferences();VanillaLight.setSunshafts(false);VanillaLight.setLensFlare(false);VanillaLight.setPbr(false);VanillaLight.setEnabled(true);VanillaLight.setSaturation(100);VanillaLight.setColoredLights(false);VanillaLight.setSsgi(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);VanillaLight.setPixelShadows(false);VanillaLight.setEdgeHighlights(false);}); server.runOnServer(s->{command(s,"time set 6000");for(int x=-5;x<=24;x++)for(int z=-5;z<=24;z++){ s.overworld().setBlockAndUpdate(new BlockPos(x,64,z),Blocks.CONCRETE.white().defaultBlockState()); for(int y=65;y<=102;y++)s.overworld().setBlockAndUpdate(new BlockPos(x,y,z),Blocks.AIR.defaultBlockState()); diff --git a/mods/sanctuary/src/clientGametest/java/fr/koka/sanctuary/client/shader/SsgiCloseup135ClientChecks.java b/mods/sanctuary/src/clientGametest/java/fr/koka/sanctuary/client/shader/SsgiCloseup135ClientChecks.java index 10216c8..e0c5c9d 100644 --- a/mods/sanctuary/src/clientGametest/java/fr/koka/sanctuary/client/shader/SsgiCloseup135ClientChecks.java +++ b/mods/sanctuary/src/clientGametest/java/fr/koka/sanctuary/client/shader/SsgiCloseup135ClientChecks.java @@ -10,7 +10,7 @@ import net.minecraft.world.level.block.Blocks; public final class SsgiCloseup135ClientChecks implements net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest { public void runTest(ClientGameTestContext c){new PixelShadows122ClientChecks(false,false,false,true).runTest(c);} static void run(ClientGameTestContext c,TestServerContext server){ - c.runOnClient(m->{VanillaLight.setEnabled(true);VanillaLight.setSaturation(100);VanillaLight.setSsgi(false);VanillaLight.setPbr(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);VanillaLight.setPixelShadows(false);VanillaLight.setEdgeHighlights(false);}); + c.runOnClient(m->{VanillaLight.setEnabled(true);VanillaLight.setSaturation(100);VanillaLight.setColoredLights(false);VanillaLight.setSsgi(false);VanillaLight.setPbr(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);VanillaLight.setPixelShadows(false);VanillaLight.setEdgeHighlights(false);}); server.runOnServer(s->{ // Minecraft 26.3 rejects setting a clock to its existing value. command(s,"time add 1");command(s,"time set 6000"); diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/shader/PbrTerrain.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/shader/PbrTerrain.java index c439a48..228ff7a 100644 --- a/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/shader/PbrTerrain.java +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/shader/PbrTerrain.java @@ -30,6 +30,7 @@ final class PbrTerrain implements AutoCloseable { final PbrMaterials materials=new PbrMaterials(); private Build building; private long bytes; + private int completed; private Object models,level; private List
visible=List.of(); record Entry(BlockPos origin,GpuBuffer vertices,int indices){} @@ -45,7 +46,11 @@ final class PbrTerrain implements AutoCloseable { if(chunk==null)continue; var storage=chunk.getSection(mc.level.getSectionIndex(pos.getY())); var section=sections.get(key); - if(section==null||section.storage!=storage){if(section!=null)remove(section);section=new Section(key,pos.immutable(),storage);sections.put(key,section);} + if(section==null||section.storage!=storage){ + Entry retained=section==null?null:section.entry; + if(section!=null){section.entry=null;remove(section);} + section=new Section(key,pos.immutable(),storage);section.entry=retained;sections.put(key,section); + } section.seen=frame;active.add(section); } visible=active; @@ -56,23 +61,30 @@ final class PbrTerrain implements AutoCloseable { if(started++>=4)break; Section next=active.stream().filter(s->s.dirty).min(Comparator.comparingDouble(s->s.origin.distToCenterSqr(camera))).orElse(null); if(next==null)break; - if(!next.storage.maybeHas(materials::candidate)){next.dirty=false;continue;} + if(!next.storage.maybeHas(materials::candidate)){releaseEntry(next);next.dirty=false;continue;} building=new Build(next); } if(!advance(building,deadline))break; var done=building;building=null; try(done){ - done.section.dirty=false; + done.section.dirty=done.section.revision!=done.revision; + Entry replacement=null; if(done.quads>0&&!done.overflow){ try(var mesh=done.out.buildOrThrow()){ long size=mesh.vertexBuffer().remaining(); - if(bytes+size<=MAX_BYTES){ + long replaced=done.section.entry==null?0:done.section.entry.vertices.size(); + if(bytes-replaced+size<=MAX_BYTES){ var buffer=RenderSystem.getDevice().createBuffer(()->"Sanctuary PBR surfaces",GpuBuffer.USAGE_VERTEX|GpuBuffer.USAGE_COPY_DST,mesh.vertexBuffer()); int count=mesh.drawState().indexCount();RenderSystem.getSequentialBuffer(PrimitiveTopology.QUADS).getBuffer(count); - done.section.entry=new Entry(done.section.origin,buffer,count);bytes+=buffer.size(); + replacement=new Entry(done.section.origin,buffer,count); } } } + // Publish atomically. A dirty section stays visible while its new + // geometry is built, including when light updates arrive mid-build. + releaseEntry(done.section);done.section.entry=replacement; + if(replacement!=null)bytes+=replacement.vertices.size(); + completed++; } } return active.stream().map(s->s.entry).filter(Objects::nonNull).toList(); @@ -118,18 +130,22 @@ final class PbrTerrain implements AutoCloseable { void dirtySection(int x,int y,int z){dirty(sections.get(SectionPos.asLong(x,y,z)));} void dirty(BlockPos pos){for(int x=(pos.getX()-1)>>4;x<=(pos.getX()+1)>>4;x++)for(int y=(pos.getY()-1)>>4;y<=(pos.getY()+1)>>4;y++)for(int z=(pos.getZ()-1)>>4;z<=(pos.getZ()+1)>>4;z++)dirty(sections.get(SectionPos.asLong(x,y,z)));} void dirtyChunk(int x,int z){for(var section:sections.values())if(Math.abs((section.origin.getX()>>4)-x)<=1&&Math.abs((section.origin.getZ()>>4)-z)<=1)dirty(section);} - private void dirty(Section section){if(section!=null){remove(section);section.dirty=true;}} - private void remove(Section section){if(building!=null&&building.section==section){building.close();building=null;}if(section.entry!=null){bytes-=section.entry.vertices.size();section.entry.vertices.close();section.entry=null;}} + private void dirty(Section section){if(section!=null){section.revision++;section.dirty=true;}} + private void releaseEntry(Section section){if(section.entry!=null){bytes-=section.entry.vertices.size();section.entry.vertices.close();section.entry=null;}} + private void remove(Section section){if(building!=null&&building.section==section){building.close();building=null;}releaseEntry(section);} + int entryCount(){return (int)visible.stream().filter(s->s.entry!=null).count();} + int completed(){return completed;} + boolean building(){return building!=null;} boolean settled(){return building==null&&visible.stream().noneMatch(s->s.dirty);} public void close(){if(building!=null){building.close();building=null;}for(var section:sections.values())remove(section);sections.clear();visible=List.of();materials.close();models=level=null;bytes=0;} private static final class Section{ - final long key;final BlockPos origin;final LevelChunkSection storage;long seen;boolean dirty=true;Entry entry; + final long key;final BlockPos origin;final LevelChunkSection storage;long seen,revision;boolean dirty=true;Entry entry; Section(long key,BlockPos origin,LevelChunkSection storage){this.key=key;this.origin=origin;this.storage=storage;} } private static final class Build implements AutoCloseable{ - final Section section;final ByteBufferBuilder buffer=new ByteBufferBuilder(65536);final BufferBuilder out=new BufferBuilder(buffer,PrimitiveTopology.QUADS,FORMAT); + final Section section;final long revision;final ByteBufferBuilder buffer=new ByteBufferBuilder(65536);final BufferBuilder out=new BufferBuilder(buffer,PrimitiveTopology.QUADS,FORMAT); int cursor,quads;boolean overflow; - Build(Section section){this.section=section;} + Build(Section section){this.section=section;revision=section.revision;} public void close(){buffer.close();} } } diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/shader/ProceduralPbr.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/shader/ProceduralPbr.java index b2239a1..950bc6a 100644 --- a/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/shader/ProceduralPbr.java +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/shader/ProceduralPbr.java @@ -45,7 +45,7 @@ public final class ProceduralPbr { public static void captureProjection(Matrix4f matrix){projection.set(matrix);} public static void dirtyBlock(BlockPos pos){TERRAIN.dirty(pos);} public static void dirtyChunk(int x,int z){TERRAIN.dirtyChunk(x,z);} - public static void dirtySection(int x,int y,int z){TERRAIN.dirtySection(x,y,z);} + public static void dirtySection(int x,int y,int z){if(!fr.koka.sanctuary.client.DynamicLightClient.invalidatingSections())TERRAIN.dirtySection(x,y,z);} public static void render(){ applied=false;var mc=Minecraft.getInstance(); if(!VanillaLight.active()||!VanillaLight.pbr()||VanillaLight.pbrIntensity()==0||mc.level==null){release();return;} @@ -93,5 +93,8 @@ public final class ProceduralPbr { public static void release(){TERRAIN.close();if(source!=null){source.destroyBuffers();source=null;}if(scene!=null){scene.close();scene=null;}applied=false;} static boolean applied(){return applied;}static boolean ready(){return TERRAIN.settled();} static boolean allocated(){return source!=null;}static int renderedFrames(){return frames;} + static int terrainEntries(){return TERRAIN.entryCount();} + static int terrainBuilds(){return TERRAIN.completed();} + static boolean terrainBuilding(){return TERRAIN.building();} static int materialCount(){return TERRAIN.materials.count();}static int analyses(){return TERRAIN.materials.analyses();} } 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 f6a8774..1b9c638 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 @@ -24,7 +24,7 @@ public final class VanillaLight { boolean lensFlare=true; int lensFlareIntensity=30; boolean pbr=true; - int pbrIntensity=50; + int pbrIntensity=80; boolean ssgi=true; boolean coloredLights=true; boolean warmTorches=false; diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/client/PreferredGraphicsApiMixin.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/client/PreferredGraphicsApiMixin.java new file mode 100644 index 0000000..3bbe89a --- /dev/null +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/client/PreferredGraphicsApiMixin.java @@ -0,0 +1,24 @@ +package fr.koka.sanctuary.mixin.client; + +import com.mojang.renderpearl.api.device.GpuBackend; +import net.minecraft.client.PreferredGraphicsApi; +import net.minecraft.network.chat.Component; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +/** Sanctuary's default uses native Vulkan with Minecraft's OpenGL fallback. */ +@Mixin(PreferredGraphicsApi.class) +abstract class PreferredGraphicsApiMixin { + @Inject(method="getBackendsToTry", at=@At("HEAD"), cancellable=true) + private void sanctuary$vulkanDefault(CallbackInfoReturnable ci) { + if ((Object)this == PreferredGraphicsApi.DEFAULT) + ci.setReturnValue(PreferredGraphicsApi.VULKAN.getBackendsToTry()); + } + @Inject(method="caption", at=@At("HEAD"), cancellable=true) + private void sanctuary$defaultCaption(CallbackInfoReturnable ci) { + if ((Object)this == PreferredGraphicsApi.DEFAULT) + ci.setReturnValue(Component.translatable("sanctuary.graphics.default_vulkan")); + } +} 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 aa9f3c9..2117483 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 @@ -1,4 +1,5 @@ { + "sanctuary.graphics.default_vulkan": "Vulkan (default)", "sanctuary.statuary.clay_ready": "Clay: original model colors.", "sanctuary.weather.selected": "Real Time weather: %s for %s (%s). The daily forecast resumes at midnight.", "sanctuary.weather.overcast": "Overcast", @@ -2295,7 +2296,7 @@ "sanctuary.shaders.ssgi_intensity_help": "Strength of diffuse color bounce. Zero stops rendering and frees its resources. Does not change world light.", "sanctuary.shaders.ssgi_intensity_value": "SSGI: %s%%", "sanctuary.shaders.pbr": "PBR · material relief", - "sanctuary.shaders.pbr_help": "Experimental: procedural block normals and roughness, sun, moon and local light highlights. Enable Colored lights for local direction. Range 32 blocks. ON by default.", + "sanctuary.shaders.pbr_help": "Experimental: procedural block normals and roughness, sun, moon and local light highlights. Enable Colored lights for local direction. Range 32 blocks. ON at 80% by default.", "sanctuary.shaders.pbr_intensity": "PBR strength", "sanctuary.shaders.pbr_intensity_help": "Material relief and highlights. Zero stops rendering and frees the cache. Original textures remain unchanged.", "sanctuary.shaders.pbr_intensity_value": "PBR: %s%%", 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 3ec3e90..beeacf5 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 @@ -1,4 +1,5 @@ { + "sanctuary.graphics.default_vulkan": "Vulkan (par défaut)", "sanctuary.statuary.clay_ready": "Argile : couleurs originales du modèle.", "sanctuary.weather.selected": "Météo Real Time : %s pour la journée du %s (%s). Le tirage quotidien reprend à minuit.", "sanctuary.weather.overcast": "Ciel gris", @@ -2295,7 +2296,7 @@ "sanctuary.shaders.ssgi_intensity_help": "Force de la teinte diffusée. Zéro coupe le calcul et libère ses ressources. Aucun changement de lumière du monde.", "sanctuary.shaders.ssgi_intensity_value": "SSGI : %s %%", "sanctuary.shaders.pbr": "PBR · relief des matériaux", - "sanctuary.shaders.pbr_help": "Expérimental : normales procédurales et rugosité des blocs, reflets du soleil, de la lune et des lumières locales. Activer Lumières colorées pour la direction locale. Portée 32 blocs. Activé par défaut.", + "sanctuary.shaders.pbr_help": "Expérimental : normales procédurales et rugosité des blocs, reflets du soleil, de la lune et des lumières locales. Activer Lumières colorées pour la direction locale. Portée 32 blocs. Activé par défaut à 80 %.", "sanctuary.shaders.pbr_intensity": "Intensité du PBR", "sanctuary.shaders.pbr_intensity_help": "Relief et reflets des matériaux. Zéro coupe le calcul et libère le cache. Ne modifie pas les textures originales.", "sanctuary.shaders.pbr_intensity_value": "PBR : %s %%", diff --git a/mods/sanctuary/src/main/resources/assets/sanctuary/shaders/core/pbr.fsh b/mods/sanctuary/src/main/resources/assets/sanctuary/shaders/core/pbr.fsh index ef40afc..dff78e3 100644 --- a/mods/sanctuary/src/main/resources/assets/sanctuary/shaders/core/pbr.fsh +++ b/mods/sanctuary/src/main/resources/assets/sanctuary/shaders/core/pbr.fsh @@ -48,7 +48,7 @@ float visibility(vec3 p,vec3 n){ vec3 q=light.xyz/light.w;q.xy=q.xy*0.5+0.5; if(DistanceClipFog.y<0.5)q.z=q.z*0.5+0.5; if(any(lessThan(q,vec3(0)))||any(greaterThan(q,vec3(1))))return 1.0; - return q.z>texture(SunDepth,q.xy).r+0.00015?0.0:1.0; + return q.z>textureLod(SunDepth,q.xy,0.0).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. @@ -88,12 +88,14 @@ vec4 localArrival(vec3 p,vec3 normal){ 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 nl=clamp(dot(n,l),0.0,1.0),nv=clamp(dot(n,v),.001,1.0),nh=clamp(dot(n,h),0.0,1.0),vh=clamp(dot(v,h),0.0,1.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); + // Explicit integer power stays defined even at round-off limits on OpenGL drivers. + float grazing=1.0-vh,grazing2=grazing*grazing; + vec3 f0=mix(vec3(.04),albedo,material.a),fresnel=f0+(1.0-f0)*(grazing2*grazing2*grazing); return distribution*geometry*fresnel/max(4.0*nv*nl,.0001)*nl; } void main(){ @@ -102,27 +104,23 @@ void main(){ vec2 tx=dFdx(blockUv),ty=dFdy(blockUv); float det=tx.x*ty.y-tx.y*ty.x; vec3 baseNormal=normalize(cross(dx,dy));if(dot(baseNormal,-relativePosition)<0.0)baseNormal=-baseNormal; - vec2 uv=gl_FragCoord.xy/vec2(textureSize(WorldDepth,0));float depth=texture(WorldDepth,uv).r; + vec2 uv=gl_FragCoord.xy/vec2(textureSize(WorldDepth,0));float depth=texelFetch(WorldDepth,ivec2(gl_FragCoord.xy),0).r; vec4 projected=PbrInverseVP*vec4(uv*2.0-1.0,PbrParams.y>0.5?depth:depth*2.0-1.0,1.0); vec3 visible=projected.xyz/projected.w; if(depth<=0.0||abs(length(visible)-length(relativePosition))>0.035||length(relativePosition)>=PbrParams.w||abs(det)<1e-12)discard; - vec4 texel=texture(BlockAtlas,blockUv);if(texel.a<0.99)discard; + // Keep atlas derivatives valid after per-fragment depth/alpha rejection. + vec4 texel=textureGrad(BlockAtlas,blockUv,tx,ty);if(texel.a<0.99)discard; vec3 tangent=normalize((dx*ty.y-dy*tx.y)/det),bitangent=normalize((dy*tx.x-dx*ty.x)/det); - vec4 material=texture(MaterialAtlas,maskUv); + vec4 material=textureLod(MaterialAtlas,maskUv,0.0); // RG stores signed height derivatives; B roughness, A metalness, not transparency. vec2 slope=(material.rg*255.0-128.0)/127.0*PbrParams.x*2.0; vec3 n=normalize(baseNormal+tangent*slope.x+bitangent*slope.y); - vec3 l=normalize(PbrSun.xyz),v=normalize(-relativePosition),h=normalize(l+v); - float nl=max(dot(n,l),0.0),nv=max(dot(n,v),0.001),nh=max(dot(n,h),0.0),vh=max(dot(v,h),0.0); - float roughness=max(material.b,0.25),alpha=roughness*roughness,a2=alpha*alpha; - float denom=nh*nh*(a2-1.0)+1.0; - float distribution=a2/max(3.14159265*denom*denom,0.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,0.00001); + // Reject degenerate/interpolated geometry without replacing the native scene with NaNs. + if(any(isnan(n))||any(isinf(n))||dot(relativePosition,relativePosition)<.000001)discard; + vec3 l=normalize(PbrSun.xyz),v=normalize(-relativePosition); + float nl=clamp(dot(n,l),0.0,1.0); vec3 albedo=texel.rgb*tint.rgb;albedo*=albedo; - vec3 f0=mix(vec3(0.04),albedo,material.a); - vec3 fresnel=f0+(1.0-f0)*pow(1.0-vh,5.0); - vec3 specular=distribution*geometry*fresnel/max(4.0*nv*nl,0.0001)*nl; + vec3 specular=localShine(n,v,l,material,albedo); float fog=max(clamp((max(length(relativePosition.xz),abs(relativePosition.y))-PbrFog.x)/max(PbrFog.y-PbrFog.x,.001),0.0,1.0), 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) @@ -130,7 +128,7 @@ void main(){ 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; + vec3 scene=texelFetch(SceneColor,ivec2(gl_FragCoord.xy),0).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); @@ -140,5 +138,6 @@ void main(){ } 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)); + if(any(isnan(lit))||any(isinf(lit)))discard; fragColor=vec4(sqrt(clamp(lit,0.0,1.0)),1.0); } diff --git a/mods/sanctuary/src/main/resources/sanctuary.mixins.json b/mods/sanctuary/src/main/resources/sanctuary.mixins.json index f0c4366..4b8b87f 100644 --- a/mods/sanctuary/src/main/resources/sanctuary.mixins.json +++ b/mods/sanctuary/src/main/resources/sanctuary.mixins.json @@ -277,7 +277,8 @@ "client.SculptureParticlesMixin", "client.PixelShadowRendererMixin", "client.PixelShadowLevelMixin", - "client.PixelShadowDirtyMixin" + "client.PixelShadowDirtyMixin", + "client.PreferredGraphicsApiMixin" ], "injectors": { "defaultRequire": 1 diff --git a/packwiz/pack.toml b/packwiz/pack.toml index fa1faba..5b58c01 100644 --- a/packwiz/pack.toml +++ b/packwiz/pack.toml @@ -1,6 +1,6 @@ name = "Sanctuary" author = "KOKA99CAB" -version = "beta.141" +version = "beta.142" pack-format = "packwiz:1.1.0" [index]