Compare commits

..
2 Commits
Author SHA1 Message Date
koka b97ec6c9e8 Fix dimension inventory synchronization and opaque PBR sheen in beta.151
Build Sanctuary / build (push) Waiting to run
2026-09-18 22:11:32 +02:00
koka 448e2febff Keep soil matte and align solar wave glints with native sunset in beta.150
Build Sanctuary / build (push) Waiting to run
2026-09-18 21:47:33 +02:00
18 changed files with 426 additions and 28 deletions
+1
View File
@@ -11,6 +11,7 @@ La vision est dans `docs/vision.md` ; elle décrit aussi des fonctionnalités fu
- Ne pas modifier un monde existant, régénérer des chunks, changer un format de sauvegarde ou activer une expansion sans contrat de migration explicite.
- Garder les identifiants `sanctuary:*` stables et documenter toute évolution de la génération avec graine et version.
- Ajouter les libellés FR/EN des nouvelles interfaces. Vérifier les dépendances pour la version Minecraft exacte ; aucune compatibilité supposée à partir du nom d'un mod.
- Les tests graphiques Sanctuary ciblent uniquement Vulkan. OpenGL est abandonné comme cible de validation depuis le 18 septembre 2026 : ne plus lancer de suite OpenGL ni revendiquer sa prise en charge à partir des essais historiques.
- Lancer `./gradlew check build` pour livrer du code, et `./gradlew assemblePack` si la distribution change. Ajouter seulement les tests utiles au comportement touché.
- Pour chaque nouvelle livraison du mod, incrémenter le compteur `beta.xxx` (départ `beta.001`) dans `mod_version` et `pack_version` de `gradle.properties`, synchroniser `packwiz/pack.toml` et documenter le changement. Le tag reprend cette version exacte, sans préfixe `v` ; voir `docs/versioning.md`. Une simple modification documentaire n'incrémente pas les binaires.
- Le pack Beta suit un canal packwiz stable et une seule instance Prism. Pour une mise à jour demandée, suivre `docs/packwiz.md` : publier un artefact vérifié et immuable, avancer le canal, puis synchroniser l'instance existante en conservant ses sauvegardes et réglages.
+16 -2
View File
@@ -1,5 +1,19 @@
# Sanctuary
## beta.151 — Nether, reflets des blocs et SSR (pack local)
Synchronisation de l’inventaire au changement de dimension, retour de l’intensité
spéculaire des blocs solides à son niveau antérieur et bois mat avec relief.
SSR activé par défaut à 20 %, choix sauvegardés conservés.
[Contrat et vérifications](docs/nether-pbr-beta151.md).
## beta.150 — Terre mate et reflets du coucher de soleil (pack local)
Terre mate avec relief conservé. Le soleil et son reflet se colorent ensemble
en suivant la couleur et la transition du coucher de soleil natif de Minecraft.
Les accents lumineux suivent les normales animées des vaguelettes et leur
orientation vers le soleil et la caméra. [Contrat et vérifications](docs/matte-dirt-beta150.md).
## beta.149 — Distances PBR / SSR et feuillage (pack local)
Deux distances séparées, 64 blocs par défaut chacune, jusqu’à 256 blocs.
@@ -2354,7 +2368,7 @@ Depuis beta.090, la cible est **Minecraft 26.3 finale**, publiée le 15 septembr
| Fabric API | 0.160.5+26.3 |
| Fabric Loom | 1.17.20 |
| Gradle Wrapper | 9.5.1, distribution vérifiée par SHA-256 |
| Sanctuary / pack | beta.149 |
| Sanctuary / pack | beta.151 |
Java 25 et Python 3.11 ou plus récent sont nécessaires. Le script pack utilise
uniquement la bibliothèque standard et repère aussi une installation Python
@@ -2383,7 +2397,7 @@ décrits dans [Validation](docs/testing.md).
Résultats :
- `mods/sanctuary/build/libs/sanctuary-beta.149.jar` : mod à installer avec
- `mods/sanctuary/build/libs/sanctuary-beta.151.jar` : mod à installer avec
Fabric API sur la version Minecraft indiquée.
- `build/packwiz/` : pack de développement complet, avec le mod construit et
l'index vérifié. Voir [Installation packwiz](packwiz/README.md).
+96
View File
@@ -0,0 +1,96 @@
# RENDER-150 — Terre mate et reflets solaires colorés
Branche `codex/matte-dirt-beta150`, socle beta.149 `1c23d0c`.
La terre sèche utilise un profil entièrement mat (rugosité 0,98, métal 0),
comme le feuillage corrigé en beta.149. Cela enlève l’éclat blanc/gris diffus
sans supprimer les normales ni le contraste de relief. Le SSR de ces matières
est également atténué par leur rugosité, sans changer son réglage global.
Blocs concernés : dirt, coarse_dirt, rooted_dirt, grass_block, dirt_path,
farmland, podzol et mycelium. Les faces terreuses suivent ainsi le même profil.
Les autres profils de matière sont inchangés.
Le disque du soleil et son reflet utilisent la même couleur native
`SkyRenderState.sunriseAndSunsetColor`, issue de l’attribut environnemental
Minecraft `SUNRISE_SUNSET_COLOR`. Son alpha pilote la transition depuis le
blanc : aucune palette horaire ou altitude artificielle. Le soleil reste
neutre tant que le dégradé natif est absent et se colore avec le ciel.
La teinte solaire est convertie en lumière linéaire pour le PBR. Le mixin
modifie seulement la couleur du disque solaire, préserve l’alpha de pluie,
la texture native et la lune, et respecte la désactivation globale de Vanilla Light.
La fusion diffuse de l’eau et sa normale animée d’origine sont conservées.
Les accents suivent les pentes des vaguelettes qui réfléchissent le soleil
vers la caméra. Leur pulsation vient des normales animées de la texture,
sans oscillateur indépendant ni inversion artificielle du relief.
Une faible variation stable par texel évite des crêtes toutes identiques ;
elle module seulement l’intensité, sans produire des points isolés aléatoires.
Les détails s’atténuent lorsqu’ils deviennent plus petits qu’un pixel écran.
Les accents restent limités au reflet solaire et conservent sa couleur.
Les lampes et la lune gardent leurs
couleurs propres.
PBR 50 %, SSR 20 % et deux portées indépendantes de 64 blocs par défaut conservés.
Minecraft 26.3, Java 25 et dépendances inchangés ; aucune sauvegarde modifiée.
## Vérifications
Tests des profils de terre et scènes GPU comparant PBR OFF/ON sur terre et
feuillage : conservation de la couleur native et présence du relief.
- Suites PBR finales Vulkan/OpenGL réussies, dont profils des huit types de
sol, proximité, intensités, préférences et feuillage.
Journaux : `build/beta150-pbr-vulkan-final.log` et
`build/beta150-pbr-opengl-final.log`.
- Terre : 180 469 pixels contrôlés, saturation relative 0,99877 sur les deux
moteurs ; relief visible sur 145 415 pixels. Captures conservées dans
`build/evidence/beta150-pbr-vulkan-final/` et
`build/evidence/beta150-pbr-opengl-final/`.
Les tests comparent la contribution PBR au rendu natif et capturent aussi
le disque du soleil, avant puis pendant la transition native du ciel.
Le contrôle serveur `./gradlew check build`, exécuté dans une copie isolée
`build/server-validation` avant les derniers ajouts exclusivement client,
compte 229 réussites et 23 échecs sur 252 tests. Les 23 identifiants d’échec
sont identiques à beta.149 ; aucun nouvel identifiant. Le contrôle global
n’est donc pas vert. Journaux : `build/beta150-check-build-isolated.log` et
`build/beta150-server-failures.json`.
Les scènes solaires comparent le rendu PBR au natif, avant et pendant le
coucher de soleil. À 2 000 ticks, la teinte native est blanche ; à 11 500 ticks
sa transition commence légèrement (alpha 0,051) ; à 12 400 ticks l’alpha
natif atteint 0,831 et le disque devient chaud (centre RGB 255/255/135).
Les deux moteurs ont validé cette synchronisation native.
Un lancement sans sélection PBR a également rencontré l’assertion client
Blocodex « The full snapshot must become visible together », avant les scènes
GPU (`build/beta150-water-motion-vulkan.log`). Il ne constitue pas une
validation de la suite client générale.
La suite finale Vulkan des éclats liés aux vaguelettes passe
(`build/beta150-wave-glints-vulkan.log`) : animation native, masquage F5,
distances indépendantes, reflets solaires et lunaires, matériaux transparents,
rechargement des ressources et redimensionnement. Dans la scène solaire,
150 563 pixels sont éclaircis, sans pixel blanc saturé.
La suite OpenGL finale passe également
(`build/beta150-wave-glints-opengl.log`) : 148 180 pixels éclaircis, aucun
pixel blanc saturé. Les captures finales sont conservées dans
`build/evidence/beta150-wave-glints-vulkan/` et
`build/evidence/beta150-wave-glints-opengl/`.
Le ressenti des vaguelettes en mouvement reste à confirmer par le créateur
sur sa configuration ; les tests ne constituent pas une validation esthétique.
## Pack local
Construction et assemblage réussis avec
`./gradlew build assemblePack -x :sanctuary:check`, après le contrôle complet
ci-dessus. L’exclusion ne transforme pas ses échecs connus en réussite.
Export packwiz puis vérification ZIP, version Fabric, Minecraft 26.3,
Loader 0.19.5, JAR unique identique au construit, shader final et mixin solaire.
- Pack : `/Users/koka/Documents/sanctuary/sanctuary-beta/build/Sanctuary-beta.150.mrpack`
- Taille : 10522942 octets.
- SHA-256 pack : `65a4a44948dcdcf0325b5e24c076a03f8832184f25f5c1ff16f69bd3ee641b93`.
- SHA-256 JAR : `4962e6f8cafd6ef3e81e088bf9b0092a264441632cfa095d4fe7394b855f093d`.
- Reçu : `build/beta150-artifacts.json`, également copié à côté du pack.
Aucun canal publié, aucune instance Prism ni sauvegarde personnelle modifiée.
+93
View File
@@ -0,0 +1,93 @@
# FIX-151 — Nether, PBR opaque et SSR
Branche `codex/nether-wood-beta151`, socle beta.150 `448e2fe`.
## Déconnexion Nether
Le rapport Windows beta.150 transmis le 18 septembre 2026 indique
`IndexOutOfBoundsException: Index 46 out of bounds for length 46` lors de
`ClientPacketListener.handleContainerContent`, paquet `container_set_content`.
Le passage au Nether dans le test client local reproduit exactement cette trace
(journal `build/beta151-client-first.log`). Ce défaut n’est pas un crash GPU.
Minecraft recrée le joueur client et son menu lors de `handleRespawn`, utilisé
aussi pour les changements de dimension. La progression n’était réattachée
qu’en fin de tick ; un paquet d’inventaire dans le même lot arrivait avant la
création des 18 cases supplémentaires et des 3 accessoires. Le correctif lie la
progression déjà reçue au nouveau joueur dès la fin du traitement du paquet de
respawn, avant le paquet suivant. Aucun paquet ni objet n’est ignoré et aucun
format de sauvegarde ne change. Sans progression Sanctuary active, le chemin
vanilla reste inchangé.
## Rendu
La beta.146 avait porté le gain solaire des surfaces opaques de 3 à 20
(4 × 5), soit environ 6,7 fois plus. Le renforcement prévu pour les reflets de
l’eau avait ainsi créé un voile sur les matières sombres, dont l’obsidienne.
Les surfaces opaques retrouvent le gain 3 et le budget spéculaire total 0,5,
partagé entre soleil et lampes. La teinte native du coucher de soleil, les
normales et le relief sont conservés. Eau et verre conservent leur traitement.
Le bois utilise le profil mat 0,98 : planches, bûches et variantes de construction,
bambou, tiges et hyphes du Nether. Ce classement ne transforme pas le nylium en bois.
SSR est activé par défaut à 20 %, avec portée 64 blocs. Un choix OFF déjà
sauvegardé reste OFF. PBR reste à 50 % par défaut.
## Cible graphique
Décision du créateur pendant ce ticket : **Vulkan uniquement** pour les nouvelles
validations. Le passage OpenGL de ce ticket s’était terminé avant cette demande ;
il reste une trace historique, pas une cible à conserver ni à relancer.
Cette décision est reportée dans `AGENTS.md` et `docs/testing.md`.
## Vérification
Les assertions GPU Vulkan et les quatre transferts successifs
Overworld → Nether → Overworld → Nether → Overworld passent sur un nouveau monde
plat de développement, graine 122. Après chaque transfert : menu de 67 cases,
pile de 7 diamants en inventaire natif, 3 émeraudes dans la première case
supplémentaire et 5 lingots d’or dans la dernière, toujours présents côté client.
Les scènes PBR conservent 99,878 % de la saturation relative du chêne et 99,968 %
de celle de l’épicéa écorcé sur 185 776 pixels chacune. Le relief affecte
respectivement 101 382 et 51 671 pixels. De face, l’obsidienne ne gagne en moyenne
que 5,114 niveaux RGB sur 255 (102 480 pixels), avec relief visible sur 95 225
pixels. Captures inspectées : bois et obsidienne PBR OFF/ON.
Les contrôles couvrent aussi les réglages SSR absents (ON/20 %), la persistance
d’un choix OFF, les profils bois, les petits déplacements de caméra, les
intensités, le rechargement des ressources et le redimensionnement.
Le premier passage Vulkan corrigé valide toutes les assertions, mais se bloque
ensuite à la fermeture dans les barrières de synchronisation du programme de
test Fabric ; son processus est arrêté après capture des piles de threads.
Premier journal : `build/beta151-client-vulkan-final.log` ; diagnostic :
`build/beta151-client-shutdown-threads.txt`. Le second passage Vulkan réussit entièrement, fermeture comprise, en **3 min 9 s**
(`build/beta151-client-vulkan-clean.log`). La suite OpenGL terminée avant l’abandon de
cette cible a réussi en 2 min 42 s ; aucun nouveau test OpenGL n’est prévu.
Le contrôle complet `./gradlew check build` dans `build/server-validation`
(3 068 fichiers de production identiques aux sources courantes) termine en
11 min 13 s : **229 réussites, 23 échecs sur 252**. Les 23 identifiants sont
strictement identiques à beta.150 : aucun nouveau, aucun résolu.
Le contrôle global reste rouge. Journaux et liste :
`build/beta151-check-build.log`, `build/beta151-server-failures.json`.
Ces échecs restent ouverts et ne constituent pas la validation du correctif
réseau ; celui-ci repose sur la reproduction puis le test client des transferts.
## Pack local
`./gradlew build assemblePack -x :sanctuary:check` réussit en 11 s,
indépendamment du contrôle complet en échec ci-dessus. Export packwiz réussi ;
ZIP, version Fabric, Minecraft 26.3, Loader 0.19.5, JAR Sanctuary unique,
mixin de respawn et shader exacts, classes conformes aux sorties compilées et
absence des tests dans le JAR vérifiés.
- Pack : `/Users/koka/Documents/sanctuary/sanctuary-beta/build/Sanctuary-beta.151.mrpack`.
- SHA-256 pack : `1d87804d75edda0a95c318ea3feaf47ecadeeb29d86ca55483a42741d7a5fcab`.
- SHA-256 JAR : `6676c67f8be4e650a9b6a8213c8b0fdf3cf7e62b030c3d1a6d4805d438a8e543`.
- Reçu : `build/beta151-artifacts.json`.
Les essais sont effectués sur Apple M1 ; la validation sur le PC Windows du
créateur reste à faire.
Aucune sauvegarde personnelle ouverte, aucun canal publié ni instance installée.
+8
View File
@@ -1,5 +1,13 @@
# Vérifier Sanctuary
## Cible graphique — décision du 18 septembre 2026
Les nouvelles validations graphiques Sanctuary utilisent **Vulkan uniquement**.
OpenGL est abandonné comme cible de validation à la demande du créateur.
Les anciens résultats OpenGL ci-dessous sont historiques et ne constituent
pas un engagement de compatibilité. Ne plus programmer de suite OpenGL.
Utiliser Java 25 et les dépendances épinglées dans `gradle.properties`.
```sh
+2 -2
View File
@@ -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.149
pack_version=beta.149
mod_version=beta.151
pack_version=beta.151
resource_pack_version=beta.121
maven_group=fr.koka.sanctuary
jei_version=30.32.0-sanctuary.3
@@ -50,28 +50,73 @@ public final class Pbr134ClientChecks implements FabricClientGameTest {
c.runOnClient(m->m.getWindow().setWindowed(oldWidth,oldHeight));c.waitFor(m->m.gameRenderer.mainRenderTarget().width==on.width(),200);frames(c);
c.runOnClient(m->{VanillaLight.setPixelShadows(true);VanillaLight.setSsgi(true);VanillaLight.setBloom(true);VanillaLight.setOreEmission(true);VanillaLight.setEdgeHighlights(true);});
c.waitFor(m->PixelShadows.applied()&&PixelShadows.ready()&&ScreenSpaceGI.applied()&&Bloom.applied()&&EdgeHighlights.applied()&&ProceduralPbr.applied(),1200);frames(c);capture(c,"pbr-combined");
foliage(c,server);
matteGround(c,server,Blocks.OAK_LEAVES,"leaves",true);
matteGround(c,server,Blocks.DIRT,"dirt",false);
matteGround(c,server,Blocks.OAK_PLANKS,"oak-planks",false);
matteGround(c,server,Blocks.STRIPPED_SPRUCE_LOG,"stripped-spruce",false);
darkStone(c,server);
netherRoundTrip(c,server);
c.runOnClient(m->VanillaLight.setEnabled(false));c.waitTicks(8);c.runOnClient(m->check(!ProceduralPbr.allocated()&&ProceduralPbr.materialCount()==0,"Global OFF releases PBR"));
c.runOnClient(m->{VanillaLight.setEnabled(true);VanillaLight.setPbr(false);VanillaLight.setSsgi(false);});
System.out.println("PBR134_PASS normals; profiles; texture cache; directional lighting; intensity; OFF/zero; tiny camera; reload; resize; combined; default="+delta+" high="+high+" directional="+directions);
}
private static void foliage(ClientGameTestContext c,TestServerContext server){
private static void darkStone(ClientGameTestContext c,TestServerContext server){
server.runOnServer(s->{for(int x=-5;x<=24;x++)for(int z=-5;z<=24;z++)
s.overworld().setBlockAndUpdate(new BlockPos(x,64,z),Blocks.OBSIDIAN.defaultBlockState());});
camera(c,server,10.5,69,5.5,65);look(c,server,0,90);settle(c);
enable(c,false);var off=capture(c,"pbr151-obsidian-face-off");enable(c,true);var on=capture(c,"pbr151-obsidian-face-on");
double gain=0;int samples=0,relief=0;
for(int y=off.height()/4;y<off.height()*3/4;y++)for(int x=off.width()/4;x<off.width()*3/4;x++){
int i=y*off.width()+x,a=off.pixels()[i],b=on.pixels()[i];
double before=((a>>16&255)+(a>>8&255)+(a&255))/3.0,after=((b>>16&255)+(b>>8&255)+(b&255))/3.0;
gain+=Math.max(0,after-before);samples++;if(Math.abs(after-before)>1)relief++;
}
System.out.println("PBR151_OBSIDIAN samples="+samples+" meanBrightening="+gain/samples+" relief="+relief);
check(samples>1000&&gain/samples<18,"Dark stone keeps its dark face without amplified white sheen");
check(relief>100,"Obsidian PBR remains visible");
look(c,server,0,20);settle(c);capture(c,"pbr151-obsidian-grazing");
}
private static void netherRoundTrip(ClientGameTestContext c,TestServerContext server){
c.runOnClient(m->{VanillaLight.setEnabled(true);VanillaLight.setPbr(true);VanillaLight.setSsr(true);VanillaLight.setColoredLights(true);VanillaLight.setPixelShadows(true);});
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();
p.getInventory().setItem(0,new net.minecraft.world.item.ItemStack(net.minecraft.world.item.Items.DIAMOND,7));
var overflow=fr.koka.sanctuary.inventory.SanctuaryOverflowAccess.of(p);
overflow.setItem(0,new net.minecraft.world.item.ItemStack(net.minecraft.world.item.Items.EMERALD,3));
overflow.setItem(17,new net.minecraft.world.item.ItemStack(net.minecraft.world.item.Items.GOLD_INGOT,5));
p.inventoryMenu.broadcastFullState();});
for(String dimension:new String[]{"the_nether","overworld","the_nether","overworld"}){
server.runOnServer(s->command(s,"execute in minecraft:"+dimension+" run tp @a 10.5 100 5.5"));
c.waitFor(m->m.level!=null&&m.player!=null&&m.level.dimension().identifier().getPath().equals(dimension)&&m.gui.screen()==null,2400);
c.waitTicks(40);
c.runOnClient(m->{
check(m.player.inventoryMenu.slots.size()==67,"Inventory has exactly 46 native, 18 extra and 3 accessory slots");
check(m.player.getInventory().getItem(0).is(net.minecraft.world.item.Items.DIAMOND)&&m.player.getInventory().getItem(0).getCount()==7,"Native stack survives transfer");
var overflow=fr.koka.sanctuary.inventory.SanctuaryOverflowAccess.of(m.player);
check(overflow.getItem(0).is(net.minecraft.world.item.Items.EMERALD)&&overflow.getItem(0).getCount()==3,"First overflow stack survives transfer");
check(overflow.getItem(17).is(net.minecraft.world.item.Items.GOLD_INGOT)&&overflow.getItem(17).getCount()==5,"Last overflow stack survives transfer");
});
capture(c,"nether151-"+dimension);
server.runOnServer(s->check(s.getPlayerList().getPlayers().size()==1,"Player stays connected after dimension transfer"));
System.out.println("NETHER151_TRANSFER_PASS "+dimension);
}
}
private static void matteGround(ClientGameTestContext c,TestServerContext server,net.minecraft.world.level.block.Block block,String name,boolean leaves){
c.runOnClient(m->{VanillaLight.setSsr(false);VanillaLight.setPbrIntensity(50);VanillaLight.setPixelShadows(false);VanillaLight.setSsgi(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);VanillaLight.setEdgeHighlights(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),Blocks.OAK_LEAVES.defaultBlockState().setValue(net.minecraft.world.level.block.LeavesBlock.PERSISTENT,true));});
server.runOnServer(s->{if(leaves)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),leaves?block.defaultBlockState().setValue(net.minecraft.world.level.block.LeavesBlock.PERSISTENT,true):block.defaultBlockState());});
camera(c,server,10.5,69,5.5,65);look(c,server,0,40);settle(c);
enable(c,false);var off=capture(c,"pbr149-leaves-off");enable(c,true);var on=capture(c,"pbr149-leaves-on");
enable(c,false);var off=capture(c,"pbr150-"+name+"-off");enable(c,true);var on=capture(c,"pbr150-"+name+"-on");
double nativeChroma=0,pbrChroma=0;int samples=0,relief=0;
for(int y=off.height()/3;y<off.height()*9/10;y++)for(int x=off.width()/10;x<off.width()*9/10;x++){
int i=y*off.width()+x,a=off.pixels()[i],b=on.pixels()[i],r=a>>16&255,g=a>>8&255,blue=a&255;
if(g<30||g<r*1.1||g<blue*1.1)continue;
if(leaves?(g<30||g<r*1.1||g<blue*1.1):(r<30||r<g*1.05||g<blue*1.1))continue;
nativeChroma+=chroma(a);pbrChroma+=chroma(b);samples++;
if(Math.abs((b>>16&255)-r)+Math.abs((b>>8&255)-g)+Math.abs((b&255)-blue)>6)relief++;
}
double retained=pbrChroma/nativeChroma;
System.out.println("PBR149_LEAVES samples="+samples+" chromaRetained="+retained+" relief="+relief);
check(samples>1000&&retained>.985,"Leaves preserve green colour without specular veil: "+retained);
check(relief>100,"Leaf normal relief remains visible");
System.out.println("PBR150_MATTE material="+name+" samples="+samples+" chromaRetained="+retained+" relief="+relief);
check(samples>1000&&retained>.985,name+" preserves native colour without specular veil: "+retained);
check(relief>100,name+" normal relief remains visible");
}
private static double chroma(int pixel){
int r=pixel>>16&255,g=pixel>>8&255,b=pixel&255,max=Math.max(r,Math.max(g,b)),min=Math.min(r,Math.min(g,b));
@@ -105,6 +150,11 @@ public final class Pbr134ClientChecks implements FabricClientGameTest {
check(PbrMaterials.profile("water").metal()==0&&PbrMaterials.profile("glass").metal()==0,"Water and glass stay dielectric");
int[] glass={0x00FFFFFF,0x00000000,0x00000000,0x00FFFFFF};
for(int p:PbrMaterials.generate(glass,2,2,PbrMaterials.profile("glass")))check((p>>8&0xFFFF)==0x8080,"Transparent glass texels fall back to geometric normal");
for(String soil:new String[]{"dirt","coarse_dirt","rooted_dirt","grass_block","dirt_path","farmland","podzol","mycelium"})
check(PbrMaterials.profile(soil).roughness()>=.98f&&PbrMaterials.profile(soil).metal()==0,"Dry soil has a fully matte profile: "+soil);
for(String wood:new String[]{"oak_planks","spruce_stairs","dark_oak_slab","stripped_spruce_log","stripped_crimson_stem","warped_hyphae","bamboo_mosaic","pale_oak_trapdoor"})
check(PbrMaterials.profile(wood).roughness()>=.98f&&PbrMaterials.profile(wood).metal()==0,"Wood remains fully matte: "+wood);
check(PbrMaterials.profile("crimson_nylium").roughness()<.9f,"Nether ground is not classified as wood");
check(PbrMaterials.profile("oak_planks").roughness()>PbrMaterials.profile("polished_andesite").roughness(),"Wood remains rougher than polished stone");
}
private static void preferences(){try{
@@ -121,7 +171,7 @@ public final class Pbr134ClientChecks implements FabricClientGameTest {
VanillaLight.setSsrDistance(64);
for(int n:new int[]{0,1,2,4,8}){VanillaLight.setWaterBlendRadius(n);forgetPreferences();check(VanillaLight.waterBlendRadius()==n,"Water blend persistence");}
VanillaLight.setPbrDistance(32);VanillaLight.setWaterBlendRadius(4);
check(!VanillaLight.ssr()&&VanillaLight.ssrIntensity()==20,"Missing SSR defaults OFF at 20 percent");
check(VanillaLight.ssr()&&VanillaLight.ssrIntensity()==20,"Missing SSR defaults ON at 20 percent");
for(int n:new int[]{-1,0,50,100,101}){VanillaLight.setSsr(true);VanillaLight.setSsrIntensity(n);forgetPreferences();check(VanillaLight.ssr()&&VanillaLight.ssrIntensity()==Math.clamp(n,0,100),"SSR preference persistence/clamp");}
VanillaLight.setSsr(false);VanillaLight.setSsrIntensity(20);forgetPreferences();check(!VanillaLight.ssr(),"SSR OFF persists");
var manager=net.minecraft.client.Minecraft.getInstance().getLanguageManager();var oldLanguage=manager.getSelected();
@@ -89,6 +89,37 @@ final class PixelReflectionsClientChecks {
var sunLook=c.computeOnClient(m->{var sun=PixelShadows.sunDirection(m.gameRenderer.gameRenderState().levelRenderState.skyRenderState.sunAngle);return new float[]{(float)Math.toDegrees(Math.atan2(-sun.x,sun.z)),(float)Math.toDegrees(Math.asin(sun.y))};});
look(c,server,sunLook[0],sunLook[1]);frames(c);capture(c,"ssr146-solar-path-low-sun");
c.runOnClient(m->VanillaLight.setBloom(true));frames(c);capture(c,"ssr146-solar-path-low-sun-bloom");
for(int time:new int[]{2000,11500,12400}){
c.runOnClient(m->{VanillaLight.setBloom(false);VanillaLight.setSsr(false);});
server.runOnServer(s->command(s,"time set "+time));c.waitTicks(15);
var solarLook=c.computeOnClient(m->{var sun=PixelShadows.sunDirection(m.gameRenderer.gameRenderState().levelRenderState.skyRenderState.sunAngle);return new float[]{(float)Math.toDegrees(Math.atan2(-sun.x,sun.z)),(float)Math.toDegrees(Math.asin(sun.y))};});
look(c,server,solarLook[0],solarLook[1]);pbrFrames(c);
var warm=capture(c,"ssr150-solar-"+time);
c.runOnClient(m->VanillaLight.setPbr(false));c.waitTicks(8);var nativeWater=capture(c,"ssr150-solar-native-"+time);
long red=0,green=0,blue=0;int samples=0;
for(int y=warm.height()/3;y<warm.height()*9/10;y++)for(int x=warm.width()/3;x<warm.width()*2/3;x++){
int i=y*warm.width()+x,a=nativeWater.pixels()[i],b=warm.pixels()[i];
int dr=(b>>16&255)-(a>>16&255),dg=(b>>8&255)-(a>>8&255),db=(b&255)-(a&255);
if(dr>3){red+=dr;green+=dg;blue+=db;samples++;}
}
System.out.println("PBR150_SOLAR_COLOUR time="+time+" pitch="+solarLook[1]+" samples="+samples+" red="+red+" green="+green+" blue="+blue);
var tint=c.computeOnClient(m->SolarColour.tint(m.gameRenderer.gameRenderState().levelRenderState.skyRenderState));
float nativeFade=c.computeOnClient(m->m.gameRenderer.gameRenderState().levelRenderState.skyRenderState.sunriseAndSunsetColor.w());
System.out.println("PBR150_NATIVE_SUNSET time="+time+" fade="+nativeFade+" tint="+tint);
check(samples>500,"Solar reflection remains visible: "+time);
if(nativeFade>.1)check(red>green*1.1&&red>blue*1.1,"Reflection follows warm native horizon colour: "+time);
if(nativeFade==0)check(tint.equals(new org.joml.Vector3f(1)),"No early tint before native sunset");
c.runOnClient(m->{VanillaLight.setPbr(true);VanillaLight.setSsr(true);VanillaLight.setBloom(true);});frames(c);capture(c,"ssr150-solar-bloom-"+time);
c.runOnClient(m->VanillaLight.setBloom(false));
look(c,server,solarLook[0],-solarLook[1]);frames(c);var sunDisc=capture(c,"ssr150-sun-disc-"+time);
long sr=0,sg=0,sb=0;int sunSamples=0;
for(int y=sunDisc.height()/2-5;y<sunDisc.height()/2+5;y++)for(int x=sunDisc.width()/2-5;x<sunDisc.width()/2+5;x++){
int pixel=sunDisc.pixels()[y*sunDisc.width()+x];sr+=pixel>>16&255;sg+=pixel>>8&255;sb+=pixel&255;sunSamples++;
}
System.out.println("PBR150_SUN_DISC time="+time+" red="+sr/sunSamples+" green="+sg/sunSamples+" blue="+sb/sunSamples);
if(nativeFade>.1)check(sr>sb*1.05,"Sun disc follows native warm tint: "+time);
look(c,server,solarLook[0],solarLook[1]);
}
c.runOnClient(m->{VanillaLight.setBloom(false);VanillaLight.setSsrIntensity(100);});
server.runOnServer(s->command(s,"time set 6000"));c.waitTicks(10);
camera(c,server,.5,68,0,70);look(c,server,0,12);settle(c);
@@ -228,6 +259,7 @@ final class PixelReflectionsClientChecks {
int i=y*a.width()+x,aa=a.pixels()[i],bb=b.pixels()[i];
if(Math.abs((aa>>16&255)-(bb>>16&255))+Math.abs((aa>>8&255)-(bb>>8&255))+Math.abs((aa&255)-(bb&255))>8)n++;
}return n;}
private static void pbrFrames(ClientGameTestContext c){int start=c.computeOnClient(m->ProceduralPbr.renderedFrames());c.waitFor(m->ProceduralPbr.applied()&&ProceduralPbr.renderedFrames()>start+8,1200);}
private static void frames(ClientGameTestContext c){int start=c.computeOnClient(m->PixelReflections.frames());c.waitFor(m->PixelReflections.applied()&&ProceduralPbr.ready()&&PixelReflections.frames()>start+8,1200);}
private static void settle(ClientGameTestContext c){c.waitTicks(45);c.waitFor(m->m.levelRenderer.hasRenderedAllSections()&&ProceduralPbr.ready(),1200);c.waitTicks(10);}
}
@@ -56,7 +56,6 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
var info = com.mojang.blaze3d.systems.RenderSystem.getDevice().getDeviceInfo();
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()
@@ -32,8 +32,14 @@ final class PbrMaterials implements AutoCloseable {
if(name.equals("iron_block")||name.equals("gold_block")||name.equals("netherite_block")||name.contains("copper")&&!name.contains("ore")&&!name.contains("raw"))
return new Profile(name.contains("oxidized")?.65f:.32f,name.contains("oxidized")?.35f:.85f);
if(name.contains("polished")||name.contains("glazed")||name.equals("diamond_block")||name.equals("emerald_block")||name.equals("ruby_block")||name.equals("sapphire_block"))return new Profile(.42f,0);
// Dry soil keeps its normal relief without a broad white specular coating.
if(java.util.Set.of("dirt","coarse_dirt","rooted_dirt","grass_block","dirt_path","farmland","podzol","mycelium").contains(name))return new Profile(.98f,0);
if(name.contains("wool")||name.contains("leaves"))return new Profile(.98f,0);
if(name.contains("planks")||name.contains("log")||name.contains("wood"))return new Profile(.88f,0);
// All wooden shapes share a matte response, including Nether stems/hyphae.
String woodName=name.startsWith("stripped_")?name.substring(9):name;
for(String wood:new String[]{"oak","spruce","birch","jungle","acacia","dark_oak","mangrove","cherry","pale_oak","bamboo","crimson","warped"})
if(woodName.startsWith(wood+"_")&&Set.of("planks","log","wood","stem","hyphae","slab","stairs","fence","fence_gate","door","trapdoor","button","pressure_plate","sign","wall_sign","hanging_sign","wall_hanging_sign","block","mosaic","mosaic_slab","mosaic_stairs").contains(woodName.substring(wood.length()+1)))return new Profile(.98f,0);
if(name.contains("planks")||name.contains("log")||name.contains("wood"))return new Profile(.98f,0);
return new Profile(.82f,0);
}
Slot slot(TextureAtlasSprite sprite,BlockState state){
@@ -66,13 +66,16 @@ public final class ProceduralPbr {
if(lunar)sun=PixelShadows.sunDirection(sky.moonAngle);
float moon=sky.moonPhase==null?1:(float)(.5+.5*Math.cos(sky.moonPhase.index()*Math.PI/4));
float strength=(lunar?.12f*moon:1)*sky.rainBrightness*Math.clamp((sun.y-.04f)/.16f,0,1);
var sunColour=lunar?new org.joml.Vector3f(1):SolarColour.tint(sky);
boolean shadow=PixelShadows.applied();
scene.rotate();try(var mapped=scene.currentBuffer().map(false,true)){
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,VanillaLight.pbrDistance())
.putVec4(fog.renderDistanceStart,fog.renderDistanceEnd,fog.environmentalStart,fog.environmentalEnd).putVec4(fog.color.w,ColoredLights.directionalReady()?1:0,lunar?0:1,0)
.putVec4(0,0,0,0).putVec4(0,0,0,0).putVec4(0,0,0,0).putVec4(0,0,0,0);
.putVec4(sunColour.x*sunColour.x,sunColour.y*sunColour.y,sunColour.z*sunColour.z,1)
.putVec4((float)(camera.pos.x%256),(float)(camera.pos.y%256),(float)(camera.pos.z%256),0)
.putVec4(0,0,0,0).putVec4(0,0,0,0);
}
var transforms=new ArrayList<GpuBufferSlice>(entries.size());int max=0;
for(var entry:entries){var p=entry.origin();var offset=new Vector3f((float)(p.getX()-camera.pos.x),(float)(p.getY()-camera.pos.y),(float)(p.getZ()-camera.pos.z));
@@ -0,0 +1,15 @@
package fr.koka.sanctuary.client.shader;
import net.minecraft.client.renderer.state.level.SkyRenderState;
import org.joml.Vector3f;
/** Uses the exact colour and fade already extracted for Minecraft's horizon. */
public final class SolarColour {
private SolarColour(){}
public static Vector3f tint(SkyRenderState sky){
var colour=sky.sunriseAndSunsetColor;
if(colour==null)return new Vector3f(1);
float weight=Math.clamp(colour.w(),0,1);
return new Vector3f(1).lerp(new Vector3f(colour.x(),colour.y(),colour.z()),weight);
}
}
@@ -34,7 +34,7 @@ public final class VanillaLight {
int pbrDistance=64;
int ssrDistance=64;
int reflectionDistanceVersion;
boolean ssr=false;
boolean ssr=true;
int ssrIntensity=20;
boolean ssgi=true;
boolean coloredLights=true;
@@ -0,0 +1,22 @@
package fr.koka.sanctuary.mixin.client;
import fr.koka.sanctuary.client.ProgressionClient;
import fr.koka.sanctuary.progression.ProgressEffects;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ClientPacketListener;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
/** Respawn creates a new player/menu; extend it before the next inventory packet, not next tick. */
@Mixin(ClientPacketListener.class)
abstract class InventoryRespawnMixin {
@Inject(method = "handleRespawn", at = @At("RETURN"))
private void sanctuary$restoreInventoryLayout(CallbackInfo ci) {
var player = Minecraft.getInstance().player;
if (player != null && ProgressionClient.active()) {
ProgressEffects.bind(player, ProgressionClient.progress);
}
}
}
@@ -0,0 +1,28 @@
package fr.koka.sanctuary.mixin.client;
import com.mojang.renderpearl.api.buffers.GpuBufferSlice;
import fr.koka.sanctuary.client.shader.SolarColour;
import fr.koka.sanctuary.client.shader.VanillaLight;
import net.minecraft.client.renderer.SkyRenderer;
import net.minecraft.client.renderer.state.level.SkyRenderState;
import org.joml.Vector3f;
import org.joml.Vector4f;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.ModifyArg;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(SkyRenderer.class)
abstract class SolarColourMixin {
@Unique private Vector3f sanctuary$sunTint=new Vector3f(1);
@Inject(method="render",at=@At("HEAD"))
private void sanctuary$nativeSunset(GpuBufferSlice fog,SkyRenderState sky,CallbackInfo ci){
sanctuary$sunTint=VanillaLight.active()?SolarColour.tint(sky):new Vector3f(1);
}
@ModifyArg(method="renderSun",at=@At(value="INVOKE",target="Lnet/minecraft/client/renderer/DynamicGpuData;writeTransform(Lorg/joml/Matrix4f;Lorg/joml/Vector4f;)Lcom/mojang/renderpearl/api/buffers/GpuBufferSlice;"),index=1)
private Vector4f sanctuary$tintSun(Vector4f nativeColour){
return new Vector4f(nativeColour.x*sanctuary$sunTint.x,nativeColour.y*sanctuary$sunTint.y,nativeColour.z*sanctuary$sunTint.z,nativeColour.w);
}
}
@@ -25,6 +25,8 @@ layout(std140) uniform PbrScene {
vec4 PbrParams;
vec4 PbrFog;
vec4 PbrEnvironment;
vec4 PbrSunColour;
vec4 PbrWaterOrigin;
};
layout(std140) uniform PixelShadows {
mat4 InverseViewProjection;
@@ -102,6 +104,8 @@ void main(){
// Derivatives precede divergent discard; UV-space tangents preserve rotated/mirrored block textures.
vec3 dx=dFdx(relativePosition),dy=dFdy(relativePosition);
vec2 tx=dFdx(blockUv),ty=dFdy(blockUv);
vec2 atlasSize=vec2(textureSize(BlockAtlas,0));
float texelFootprint=max(length(tx*atlasSize),length(ty*atlasSize));
float det=tx.x*ty.y-tx.y*ty.x;
vec3 baseNormal=normalize(cross(dx,dy));if(dot(baseNormal,-relativePosition)<0.0)baseNormal=-baseNormal;
// Exact receiver depth is tested by the GPU; final scene depth also masks entities.
@@ -119,6 +123,14 @@ void main(){
// RG stores signed height derivatives; B roughness, A metalness, not transparency.
float intensity=PbrParams.x;
vec2 slope=(material.rg*255.0-128.0)/127.0*intensity*(family>1.5?.24:2.0);
// Slight world-anchored variation avoids identical crests on every water block.
float waterVariation=1.0;
if(family>1.5){
uvec3 cell=uvec3(floor(mod(relativePosition+PbrWaterOrigin.xyz,256.0)*16.0+.001));
uint seed=cell.x*1597334677u^cell.y*3812015801u^cell.z*2798796415u;
seed=(seed^(seed>>16u))*2246822519u;seed^=seed>>13u;
waterVariation=.75+.25*float(seed&65535u)/65535.0;
}
vec3 n=normalize(baseNormal+tangent*slope.x+bitangent*slope.y);
// Reject degenerate/interpolated geometry without replacing the native scene with NaNs.
if(any(isnan(n))||any(isinf(n))||dot(relativePosition,relativePosition)<.000001)discard;
@@ -131,7 +143,9 @@ void main(){
solarMaterial.b=max(material.b,family>1.5?.42:.24);
vec3 specular=localShine(n,v,l,solarMaterial,albedo);
// PbrEnvironment.z identifies sunlight: moonlight keeps its modest base radiance.
float solarRadiance=mix(1.0,family>1.5?6.0:4.0,PbrEnvironment.z);
float solarRadiance=family<.5?1.0:mix(1.0,family>1.5?6.0:4.0,PbrEnvironment.z);
// Same native sunset tint as the sun disc, converted to linear light.
vec3 sunColour=PbrSunColour.rgb;
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)
@@ -147,20 +161,35 @@ void main(){
float localRelief=family<.5?(max(dot(n,arrival.xyz),0.0)-max(dot(baseNormal,arrival.xyz),0.0))*.7:0.0;
lit=clamp(lit*(1.0+localRelief*localAmount),0.0,1.0);
}
// Preserve bright solar highlights instead of capping them at a medium grey.
// Fully matte foliage/fibres receive normal relief, not a broad white specular veil.
// Stone/wood (roughness <= .88), polished surfaces, metals and water are unchanged.
// Restore the pre-water response for opaque blocks. Water/glass retain their own highlights.
// Fully matte wood, soil, foliage and fibres receive normal relief, not a broad white specular veil.
// Keep normal relief independent from the additive specular response.
float fullyMatte=smoothstep(.9,.98,material.b)*(1.0-material.a);
float shineWeight=family<.5?1.0-fullyMatte:1.0;
vec3 skyShine=min(specular*amount*solarRadiance*intensity*5.0*shineWeight,vec3(8.0));
vec3 lampShine=min(localShine(n,v,arrival.xyz,material,albedo)*localAmount*intensity*5.0*shineWeight,vec3(8.0));
float shineGain=family<.5?3.0:5.0;
vec3 skyShine=min(specular*amount*solarRadiance*intensity*shineGain*shineWeight,vec3(8.0));
vec3 lampShine=min(localShine(n,v,arrival.xyz,material,albedo)*localAmount*intensity*shineGain*shineWeight,vec3(8.0));
if(family<.5){
// Share the original 0.5 budget across sun and lamps, preserving the sunset tint.
vec3 total=skyShine+lampShine;
vec3 scale=min(vec3(1.0),vec3(.5)/max(total,vec3(.00001)));
skyShine*=scale;lampShine*=scale;
}
if(family>1.5&&PbrEnvironment.z>.5){
// Soft-light/screen blend per native texel: broad daylight lifts the water
// without replacing its colour and detail with a saturated white cone.
vec3 weight=skyShine/(1.0+skyShine);
vec3 highlight=mix(vec3(1.0),sqrt(lit),.55);
// Glints follow animated wave facets reflecting the sun towards the eye.
// Their pulse comes from the native wave animation, not an unrelated timer.
vec3 halfSum=l+v;
float alignment=pow(max(dot(n,halfSum*inversesqrt(max(dot(halfSum,halfSum),.00001))),0.0),384.0);
float crest=smoothstep(.025,.18,length((material.rg*255.0-128.0)/127.0));
float detail=1.0-smoothstep(.75,2.0,texelFootprint);
float glint=alignment*crest*waterVariation*detail;
vec3 glintShine=skyShine*(1.0+glint*1.2);
vec3 weight=glintShine/(1.0+glintShine);
vec3 highlight=max(lit,mix(sunColour,sqrt(lit)*sunColour,mix(.55,.30,glint)));
lit=mix(lit,highlight,weight);
}else lit+=(1.0-lit)*(1.0-exp(-skyShine));
}else lit+=(1.0-lit)*(1.0-exp(-skyShine*sunColour));
lit+=(1.0-lit)*(1.0-exp(-lampShine));
if(any(isnan(lit))||any(isinf(lit)))discard;
fragColor=vec4(sqrt(clamp(lit,0.0,1.0)),1.0);
@@ -213,6 +213,7 @@
"client.AbstractContainerScreenAccessor",
"client.SlotPositionAccessor",
"client.InventoryScreenMixin",
"client.InventoryRespawnMixin",
"client.InventoryEquipmentTooltipMixin",
"client.RecipeBookScreenMixin",
"client.RecipeBookComponentMixin",
@@ -263,6 +264,7 @@
"client.InventoryPointerAccess",
"client.OperatorWorldOptionsMixin",
"client.StarSkyMixin",
"client.SolarColourMixin",
"client.ProgressiveFogMixin",
"client.ShaderSaturationMixin",
"client.WeatherEnvironmentMixin",
+1 -1
View File
@@ -1,6 +1,6 @@
name = "Sanctuary"
author = "KOKA99CAB"
version = "beta.149"
version = "beta.151"
pack-format = "packwiz:1.1.0"
[index]