Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b97ec6c9e8 | ||
|
|
448e2febff | ||
|
|
1c23d0c17e | ||
|
|
6f871446bb | ||
|
|
96a36d99be | ||
|
|
cf9a670f44 | ||
|
|
ed2d088272 | ||
|
|
0cfd51eb4f | ||
|
|
38f4d6c9a9 |
@@ -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.
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## beta.144 — Code d’accès à la création
|
||||
|
||||
- Hello World demande le code `SANC-XXXX-XXXX` quand le serveur a l’URL et le jeton du site.
|
||||
- Le client ne contacte pas le site. Le serveur vérifie, puis consomme le code au moment de créer l’habitant.
|
||||
- Le lien Discord est enregistré à part. Une création interrompue après `redeemed` peut se terminer sans nouveau code si le compte est encore connu.
|
||||
- [Contrat](docs/access-code-beta144.md).
|
||||
|
||||
## beta.110 — Atelier d’argile inclus
|
||||
|
||||
- Une seule livraison réunit l’atelier d’argile, les statuaires et l’import GLB de beta.106 avec les œufs et la neige saisonnière de beta.107 à beta.109.
|
||||
|
||||
@@ -1,5 +1,54 @@
|
||||
# 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.
|
||||
Option renommée « SSR » et suppression du voile spéculaire sur le feuillage.
|
||||
[Contrat et vérifications](docs/independent-distances-beta149.md).
|
||||
|
||||
## beta.148 — PBR 50 % et soleil diffus (pack local)
|
||||
|
||||
PBR à 50 %, reflet solaire plus doux avec fusion par texel préservant
|
||||
la texture de l’eau. SSR à 20 %, portée commune jusqu’à 256 blocs.
|
||||
[Contrat et vérifications](docs/pbr-soft-sun-beta148.md).
|
||||
|
||||
## beta.147 — PBR proche, lune et portée 256 (pack local)
|
||||
|
||||
Correction du scintillement à très courte distance, reflet lunaire adouci
|
||||
et distance commune PBR/SSR jusqu’à 256 blocs.
|
||||
[Contrat et vérifications](docs/pbr-close-range-beta147.md).
|
||||
|
||||
## beta.146 — Reflets et transitions de l’eau (pack local)
|
||||
|
||||
Correction du masquage des personnages en F5 et sélection stricte des surfaces PBR.
|
||||
PBR à 80 % et SSR à 20 % par défaut, distance commune PBR/SSR
|
||||
(16/32/64/128 blocs), reflet solaire large et blanc avec léger bloom des matières.
|
||||
Eau naturelle selon le type de monde et mélange 1/3/5/9/17 blocs.
|
||||
[Contrat et vérifications](docs/reflections-water-beta146.md).
|
||||
|
||||
## beta.145 — PBR de l’eau et reflets pixélisés (livraison locale)
|
||||
|
||||
L’eau et le verre rejoignent le PBR natif. L’option expérimentale de reflets
|
||||
alimente les texels avec le paysage visible selon leurs normales.
|
||||
[Contrat, vérifications et limites](docs/pbr-ssr.md).
|
||||
Cette branche intègre un instantané des sources d’eau beta.144 ; elle ne
|
||||
publie pas le canal et ne synchronise pas Prism.
|
||||
|
||||
|
||||
Sanctuary est une extension gratuite de Minecraft qui transforme le jeu en un
|
||||
monde flottant d'exploration, de production et de progression collective.
|
||||
Les joueurs commencent ensemble sur Sanctuary Island et construisent les
|
||||
@@ -20,6 +69,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.144** prépare une option d’**eau naturelle pixélisée** :
|
||||
couleur du biome, trois profils selon le fond et visibilité adaptée en immersion.
|
||||
Les océans gardent leur couleur native et le profil clair.
|
||||
[Contrat et vérifications](docs/natural-water-beta144.md). Livraison en cours.
|
||||
|
||||
La version **beta.143** garde le sampler natif de précipitation pour la neige
|
||||
saisonnière, mais ralentit son rythme : l'accumulation est espacée par un
|
||||
facteur 12 et la fonte par le facteur temps réel 72. Les protections existantes
|
||||
@@ -2314,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.122 |
|
||||
| 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
|
||||
@@ -2343,7 +2397,7 @@ décrits dans [Validation](docs/testing.md).
|
||||
|
||||
Résultats :
|
||||
|
||||
- `mods/sanctuary/build/libs/sanctuary-beta.122.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).
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
# beta.144 — Code d’accès à la création
|
||||
|
||||
Le site accepte la candidature, whitelist le pseudo et envoie un code `SANC-XXXX-XXXX`.
|
||||
Hello World demande ce code avant de créer le personnage. Le client ne parle pas
|
||||
au site. Seul le serveur appelle l’API, avec le jeton `SANCTUARY_API_TOKEN`.
|
||||
|
||||
Sans ces réglages, Hello World reste celui de beta.143 : biographie, couleur et
|
||||
familier, sans code. Dès que l’URL et le jeton sont présents, un nouveau
|
||||
personnage exige un code reconnu. Un habitant déjà enregistré entre sans écran.
|
||||
|
||||
## Réglage serveur
|
||||
|
||||
Variables d’environnement, ou fichier `config/sanctuary/access.json` si une
|
||||
variable manque :
|
||||
|
||||
```json
|
||||
{"appUrl":"https://exemple.sanctuary","token":"..."}
|
||||
```
|
||||
|
||||
`appUrl` est l’origine du site, sans barre finale. Le serveur appelle
|
||||
`POST {appUrl}/api/v1/access-codes/verify` puis `redeem`. Le jeton n’est pas
|
||||
écrit dans les logs, le client ou le pack. Une URL ou un jeton illisible ferme
|
||||
la création : aucun personnage n’est inventé hors ligne.
|
||||
|
||||
## Parcours
|
||||
|
||||
1. La whitelist laisse passer le pseudo. L’écran s’ouvre tant que l’UUID n’a pas
|
||||
d’habitant.
|
||||
2. Le joueur saisit le code. L’affichage du champ et de l’exemple utilise
|
||||
l’alphabet galactique standard (`minecraft:alt`, la police de la table
|
||||
d’enchantement). La valeur envoyée reste le texte tapé. Un format complet
|
||||
déclenche `verify`, pas chaque frappe. « Code reconnu » n’écrit rien.
|
||||
3. Confirmer envoie le code de la session, la biographie, la couleur et le
|
||||
familier. Le pseudo envoyé au site est celui de la session.
|
||||
4. `redeem` répond `redeemed` avec `discord_id` : le lien est enregistré, puis
|
||||
l’habitant est créé. Les connexions suivantes sautent l’écran.
|
||||
5. Erreur, site injoignable ou jeton refusé : le joueur reste sur Hello World.
|
||||
|
||||
Une déconnexion avant `redeemed` ne consomme pas le code. Si le site a répondu
|
||||
`redeemed` et que l’écriture de l’habitant est interrompue, le lien Discord
|
||||
reste dans `data/sanctuary-access.json` (`pending`, schéma 1, graine du monde).
|
||||
La confirmation suivante termine le personnage sans rappeler `redeem`.
|
||||
|
||||
Si le code est déjà consommé et que le site ne renvoie plus `discord_id`, le
|
||||
joueur reste bloqué avec un message de reprise. Le site doit alors renvoyer
|
||||
`minecraft_username` et `discord_id` pour le même pseudo. Le mod accepte cette
|
||||
réponse, que `valid` soit vrai ou faux, et finit la création.
|
||||
|
||||
Le registre des habitants ne change pas. Le Discord est un fichier à part.
|
||||
Un fichier illisible est conservé et refuse l’accueil.
|
||||
|
||||
## Limites
|
||||
|
||||
Le gel dans le monde n’est pas ajouté : Hello World reste avant l’entrée, comme
|
||||
aujourd’hui. Inventaire, commandes et dimensions ne sont pas accessibles tant
|
||||
que l’habitant n’existe pas. La whitelist RCON reste celle du site. Un pseudo
|
||||
ajouté à la main, sans code, voit l’écran sans pouvoir le valider.
|
||||
@@ -0,0 +1,68 @@
|
||||
# RENDER-149 — Distances PBR et SSR indépendantes
|
||||
|
||||
Branche `codex/separate-reflection-distances-beta149`, socle beta.148 `6f87144`.
|
||||
|
||||
- Deux réglages : Distance PBR et Distance SSR, chacun à 64 blocs par défaut.
|
||||
Choix 16, 32, 64, 128, 256 blocs, enregistrés séparément.
|
||||
- Le PBR conserve son propre fondu et sa propre limite de portée. Le SSR
|
||||
utilise sa distance pour les surfaces réfléchissantes et sa recherche de
|
||||
paysage. Le maillage et la sélection des surfaces couvrent le maximum des
|
||||
deux portées actives, afin que réduire l’une ne coupe pas l’autre.
|
||||
- Ancien défaut commun 32 migré une fois vers PBR 64 ; nouveau réglage SSR 64.
|
||||
Les anciennes valeurs PBR différentes de 32 restent conservées. Après la
|
||||
migration, un choix explicite 32 est conservé comme les autres valeurs.
|
||||
- Option nommée exactement « SSR » en français et en anglais, sans la mention
|
||||
« expérimental ». Les autres options expérimentales du jeu sont inchangées.
|
||||
- Intensités PBR 50 % et SSR 20 %, rendu solaire diffus de beta.148 conservés.
|
||||
SSR conserve sa dépendance au PBR et les limites du paysage visible/chargé
|
||||
et du budget de maillage.
|
||||
|
||||
- Les feuilles (et fibres de laine, même profil entièrement mat) ne reçoivent
|
||||
plus de large éclat spéculaire blanc. Leur relief normal reste présent.
|
||||
La pierre et le bois retrouvent exactement leur poids spéculaire précédent,
|
||||
comme les surfaces polies, les métaux, l’eau et le verre. La suppression
|
||||
concerne les profils non métalliques de rugosité 0,98, avec transition 0,90–0,98.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Contrôles de persistance indépendante, valeurs par défaut et libellés FR/EN.
|
||||
Scènes graphiques avec PBR variable/SSR 256 puis PBR 256/SSR variable.
|
||||
- Suite PBR/préférences Vulkan réussie : deux défauts 64 et persistance
|
||||
indépendante sur 16/32/64/128/256 ; libellés FR/EN et nom SSR vérifiés.
|
||||
Journal : `build/beta149-pbr-vulkan.log`.
|
||||
- Suites SSR Vulkan et OpenGL réussies avec les portées dissociées, F5,
|
||||
intersections, eau/verre/métal, soleil/lune, OFF/zéro et rechargement.
|
||||
Journaux : `build/beta149-ssr-vulkan.log`, `build/beta149-ssr-opengl.log`.
|
||||
Ces passes précèdent l’ajustement final du profil entièrement mat.
|
||||
- Suite PBR Vulkan finale avec feuillage réussie : 170 172 pixels de feuilles
|
||||
contrôlés, saturation relative 1,00007, relief visible sur 63 236 pixels.
|
||||
Journal : `build/beta149-foliage-vulkan.log` ; captures correspondantes
|
||||
dans `build/evidence/beta149-foliage-vulkan/`.
|
||||
- Bois et briques après recentrage sur le feuillage : aucun pixel modifié
|
||||
par rapport à la capture précédant l’ajustement, sur les zones de contrôle
|
||||
de 26 304 et 40 896 pixels respectivement.
|
||||
- `./gradlew check build` exécuté dans l’instantané isolé : 252 tests serveur,
|
||||
229 réussis et 23 échecs, mêmes identifiants qu’en beta.148. Aucun nouvel échec.
|
||||
Journal : `build/beta149-check-build-isolated.log` ; comparaison :
|
||||
`build/beta149-server-failures.json`. L’ajustement final du feuillage concerne
|
||||
seulement le shader et son test client. Le contrôle global reste en échec.
|
||||
|
||||
- Suite PBR OpenGL finale réussie également : 170 199 pixels de feuilles,
|
||||
saturation relative 1,00001, relief visible sur 63 263 pixels.
|
||||
Journal : `build/beta149-foliage-opengl.log` ; captures correspondantes
|
||||
dans `build/evidence/beta149-foliage-opengl/`.
|
||||
- Assemblage final réussi : `./gradlew build assemblePack -x :sanctuary:check`,
|
||||
sans répéter les contrôles purs/serveur exécutés dans l’instantané.
|
||||
Journal : `build/beta149-build-pack.log`. Cette exclusion ne transforme
|
||||
pas le contrôle complet en réussite.
|
||||
|
||||
## Artefact
|
||||
|
||||
[Sanctuary-beta.149.mrpack](../build/Sanctuary-beta.149.mrpack), copié aussi dans
|
||||
`sanctuary-beta/build/` avec les autres versions. Archive vérifiée, un seul JAR
|
||||
Sanctuary beta.149 identique au JAR construit, shaders identiques aux sources,
|
||||
libellés SSR et deux portées FR/EN vérifiés. Minecraft 26.3 / Loader 0.19.5.
|
||||
Taille : 10 520 465 octets. SHA-256 :
|
||||
`ef5519d9a0e2006b865b62371a6f8712db02ab138aa9698339d6fdb02fb4befa`.
|
||||
Reçu : `build/beta149-artifacts.json`. Aucune installation Prism ni publication.
|
||||
|
||||
@@ -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.
|
||||
@@ -0,0 +1,121 @@
|
||||
# WATER-144 — Eau naturelle pixélisée
|
||||
|
||||
Branche `codex/water-beta144`, socle beta.143 publiée (`97ff2b5`) et suivi
|
||||
`580b241`. Minecraft 26.3, Java 25, dépendances conservées.
|
||||
|
||||
## Contrat
|
||||
|
||||
**Options → Shaders → Eau naturelle** est activée par défaut avec Sanctuary.
|
||||
L’intensité (0–100 %, défaut 100 %) agit sur la surface et l’immersion.
|
||||
L’interrupteur ou 0 % retrouve le rendu précédent. Les préférences existantes
|
||||
restent conservées ; un moteur externe suspend l’effet comme les autres effets
|
||||
natifs Sanctuary.
|
||||
|
||||
La couleur native du biome reste la base, y compris les variantes d’océans.
|
||||
Les océans (`minecraft:is_ocean`) imposent le profil clair.
|
||||
Hors océan, le premier support situé au maximum huit blocs sous la cellule
|
||||
observée choisit le profil :
|
||||
|
||||
| Profil | Fond | Mélange olive #788448 | Alpha natif | Distance de brume native |
|
||||
| --- | --- | ---: | ---: | ---: |
|
||||
| Claire | Pierre, matériaux construits, inconnus | 0 % | ×0,55 | ×1 |
|
||||
| Légèrement teintée | Sable, sable rouge, gravier, argile | 10 % | ×0,80 | ×0,70 |
|
||||
| Étang | Terre, terre stérile/enracinée, herbe, podzol, mycélium, boue, racines de palétuvier boueuses | 85 % | ×1 | ×0,40 |
|
||||
|
||||
L’eau et les plantes aquatiques sans collision sont traversées. Un support
|
||||
immergé reste un fond ; absence de support, air ou données manquantes donnent
|
||||
une eau claire. La référence visuelle fournie pendant l’implémentation demande un véritable
|
||||
vert olive légèrement jaunâtre : le profil étang passe donc de 45 % à 85 %
|
||||
de mélange, tout en laissant les eaux claires et océaniques natives.
|
||||
Les surfaces profondes sans fond dans la limite restent donc
|
||||
claires. Les profils se mélangent aux sommets sur un bloc, sans filtrer les
|
||||
textures. L’eau sous un toit suit son fond, pas l’éclairage du bâtiment.
|
||||
|
||||
La ressource client `assets/sanctuary/water_materials.json` accepte deux listes
|
||||
d’identifiants de blocs, `light` et `pond`, remplaçables par un resource pack.
|
||||
Les blocs absents des listes sont clairs. Une ressource invalide est signalée
|
||||
et reprend les familles embarquées. Le template personnel inclut cette ressource.
|
||||
|
||||
## Rendu et coût
|
||||
|
||||
Modification des seules couleurs/alpha des sommets de `FluidRenderer`, sans
|
||||
passe écran ni duplication de maillage. Textures, UV, animation, éclairage
|
||||
et tri des transparences restent natifs. Les calculs de profils sont différés
|
||||
jusqu’à l’émission d’un sommet visible, puis mutualisés dans la région compilée.
|
||||
Un test de palette élimine les sections sans eau avant toute copie de biomes.
|
||||
Les états de blocs viennent des copies natives ; les appartenances océaniques
|
||||
sont capturées avant l’envoi au worker. Aucun chargement de chunk n’est demandé.
|
||||
|
||||
Un changement de bloc invalide la zone dépendante, huit blocs au-dessus et
|
||||
un bloc horizontalement. Les reconstructions natives recopient leurs données ;
|
||||
l’effet ne déclenche aucune reconstruction pour une lumière mobile.
|
||||
Un changement d’option reconstruit seulement les sections chargées contenant de
|
||||
l’eau ; les caches des autres effets restent disponibles. Le cache temporaire
|
||||
disparaît avec sa compilation, sans historique de monde.
|
||||
|
||||
Sous l’eau, le profil est échantillonné au maximum une fois par tick, avec
|
||||
transition de 0,5 seconde. La couleur et les distances environnementales sont
|
||||
adaptées dans le rendu natif du brouillard. La teinte finale est appliquée après
|
||||
la normalisation de la vision aquatique, pour éviter de transformer l’olive en
|
||||
jaune fluorescent ; la luminosité issue des modificateurs natifs de vision
|
||||
et le bonus du compagnon calmar sont conservés. Lave et brume atmosphérique ne changent
|
||||
pas. Il n’y a pas de simulation d’absorption volumétrique ni de reflet ajouté.
|
||||
|
||||
## Validation native
|
||||
|
||||
Les essais utilisent un monde plat jetable, graine 122, avec bassins sur quartz,
|
||||
sable et boue, support du sable, plantes aquatiques, toit, frontière de sections
|
||||
et changement de biome océan. Une copie personnelle de ressource remplace
|
||||
réellement les familles de matériaux puis est désactivée.
|
||||
|
||||
Les suites graphiques OpenGL et Vulkan ont passé les contrôles de surface,
|
||||
d’immersion, de fond modifié, d’océan, de ressources, de préférences et de
|
||||
cascades pendant le développement. La dernière optimisation reconstruit
|
||||
seulement les sections contenant de l’eau lors du changement d’option.
|
||||
Sa série de validation finale reste à terminer avant publication.
|
||||
|
||||
### Protocole de performance
|
||||
|
||||
Apple M1, macOS, Java 25, 854×480, distance de rendu 6 chunks,
|
||||
ombres 32 blocs / 16 pixels, PBR 50 %, autres effets activés.
|
||||
VSync désactivée, limite 260 (illimitée), limitation AFK désactivée.
|
||||
Pour chaque scène : OFF/ON/ON/OFF/ON/OFF/OFF/ON, dix secondes par passage,
|
||||
avec stabilisation des sections puis quatre secondes de chauffe entre passages.
|
||||
Le déplacement avance de 120 blocs et laisse les sections se compiler
|
||||
naturellement, sans vidange artificielle des caches des autres effets.
|
||||
Les moyennes des quatre médianes et P95 par état sont comparées avec les seuils
|
||||
+5 % et +2 ms. Les temps de compilation de sections sont consignés également.
|
||||
|
||||
Les essais courts initiaux ont montré une forte variabilité. Une limitation AFK
|
||||
à 30 FPS puis des clients Minecraft concurrents dans le chantier PBR/SSR ont
|
||||
été identifiés. Ces essais restent dans les preuves de développement ; ils
|
||||
ne valident pas la livraison. La série finale attend la fin du chantier
|
||||
concurrent, conformément à la demande du créateur. Les résultats ne sont pas
|
||||
présentés comme acquis, et aucun gain de performance universel n’est promis.
|
||||
|
||||
Pour reproduire, avec Java 25 et un environnement graphique disponible :
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
|
||||
-PsanctuaryPixelShadows122ClientTests=true -PsanctuaryWater144ClientTests=true \
|
||||
-PsanctuaryClientNoVsync=true -PsanctuaryQuickTests=true \
|
||||
-PsanctuaryClientGraphicsBackend=vulkan > build/water144-vulkan.log 2>&1
|
||||
python3 scripts/check_water_performance.py build/water144-vulkan.log
|
||||
```
|
||||
|
||||
Répéter avec `opengl`, sans autre client graphique simultané. Le vérificateur
|
||||
refuse les séries incomplètes et les dépassements ; il écrit le reçu détaillé
|
||||
près du journal. Le marqueur `WATER144_PASS` atteste les contrôles fonctionnels.
|
||||
|
||||
### Assemblage et publication
|
||||
|
||||
Le GameTest serveur dédié reste exclu conformément au refus antérieur de son
|
||||
EULA. La commande de livraison est `./gradlew check build assemblePack
|
||||
assembleTestPack -x :sanctuary:runGameTest` ; la suite complète non exclue
|
||||
n’est pas déclarée réussie. Les contrôles natifs de l’eau utilisent le serveur
|
||||
intégré d’un monde jetable. Aucun essai Windows n’est revendiqué.
|
||||
|
||||
La release, le canal et l’instance ne sont pas encore mis à jour.
|
||||
Les preuves détaillées restent dans les dossiers ignorés :
|
||||
`build/water144-*.log`, `build/water144-*-performance.json`,
|
||||
`build/water144-*-isolation.json` et `build/water144-evidence/`.
|
||||
@@ -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.
|
||||
@@ -14,6 +14,14 @@ JAR Sanctuary sont des pièces jointes de releases Gitea. Aucun JAR de mod ou
|
||||
dépendance téléchargée n'entre dans l'historique Git. Le canal Beta est distinct
|
||||
de l'ancien pack 26.2.
|
||||
|
||||
## beta.144 — Eau naturelle pixélisée (préparation)
|
||||
|
||||
Trois profils de couleur/transparence selon le fond, océans clairs et brume
|
||||
aquatique adaptée. Le [ticket de livraison](natural-water-beta144.md) décrit
|
||||
la teinte olive retenue, les contrôles et le protocole de performance.
|
||||
La publication et la synchronisation attendent la validation finale isolée.
|
||||
Les textures restent à beta.121 ; Minecraft 26.3 et les dépendances sont conservés.
|
||||
|
||||
## beta.143 — Neige saisonnière en temps réel
|
||||
|
||||
Accumulation espacée par un facteur 12 et fonte par le facteur temps réel 72,
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
# RENDER-147 — PBR proche, lune et portée 256 blocs
|
||||
|
||||
Branche `codex/pbr-close-range-beta147`, socle beta.146 `cf9a670`.
|
||||
Minecraft 26.3, Java 25 ; dépendances et formats de sauvegarde inchangés.
|
||||
|
||||
## Résultat
|
||||
|
||||
- Le prépassage des récepteurs PBR comparait encore les profondeurs natives
|
||||
avec un epsilon fixe de 0,0000001. En profondeur inversée, cette marge
|
||||
devenait trop stricte au voisinage de la caméra : de nombreux fragments
|
||||
alternaient entre le rendu natif et le PBR lors de mouvements minuscules.
|
||||
- La comparaison utilise maintenant une distance reconstruite avec la marge
|
||||
bornée de 5 mm déjà employée par les passes couleur. La sélection de la
|
||||
surface PBR la plus proche et son test de profondeur EQUAL restent stricts.
|
||||
Aucun décalage du maillage ni fondu supprimant le PBR près de la caméra.
|
||||
- La lune ne reçoit plus le renforcement spéculaire du soleil (×20 pour
|
||||
l’eau, ×4 pour les autres matières). Son coefficient lunaire et ses phases
|
||||
restent appliqués ; le grand reflet solaire est conservé.
|
||||
- Distance commune PBR/SSR : 16, 32 (défaut), 64, 128 ou 256 blocs.
|
||||
À 256, le rayon SSR peut aussi rechercher jusqu’à 256 blocs. Nombre de pas
|
||||
et budgets de maillage inchangés ; les limites du terrain visible/chargé,
|
||||
des reflets hors écran et du budget de maillage subsistent.
|
||||
- PBR 80 %, SSR 20 %, mélange des eaux et préférences de beta.146 conservés.
|
||||
Libellés FR/EN mis à jour.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Mondes de test jetables, macOS Apple M1. Aucune sauvegarde personnelle utilisée.
|
||||
|
||||
- Reproduction avant correction sous Vulkan : à 12 cm du sol de briques,
|
||||
59 442 / 262 272 pixels changent de plus de 12 niveaux RVB cumulés sous une
|
||||
rotation de 0,00002 degré. Le test échoue. Journal :
|
||||
`build/beta147-close-before-vulkan.log`.
|
||||
- Même scène après correction : 1 / 262 272 pixels à 12 cm ; également testé
|
||||
à 25 cm, 50 cm et 1 m. Suite PBR Vulkan réussie :
|
||||
`build/beta147-close-after-vulkan.log`.
|
||||
- Preuves avant/après dans `build/evidence/close-before-vulkan/` et
|
||||
`build/evidence/close-after-vulkan/`.
|
||||
|
||||
- Suite PBR et préférences OpenGL finale réussie : mêmes scènes proches,
|
||||
persistance des distances dont 256 blocs, FR/EN, OFF/zéro, intensités,
|
||||
rechargement, redimensionnement et effets combinés.
|
||||
Journal : `build/beta147-close-final-opengl.log` ; preuves :
|
||||
`build/evidence/beta147-close-final-opengl/`.
|
||||
|
||||
- Suite SSR Vulkan finale réussie : F5, intersections, portées dont 256,
|
||||
eau/verre/métal, OFF/zéro, rechargement et redimensionnement.
|
||||
Journal : `build/beta147-ssr-final-vulkan.log` ; preuves :
|
||||
`build/evidence/beta147-ssr-final-vulkan/`.
|
||||
- À PBR 80 % / SSR 20 %, le soleil conserve 22 697 pixels quasi blancs sur
|
||||
la scène aquatique 854 × 480. La lune seule en compte zéro, tout en modifiant
|
||||
visiblement 40 808 pixels par rapport au PBR OFF. Capture du bloom lunaire
|
||||
inspectée : reflet doux, sans plateau blanc.
|
||||
|
||||
- Suite SSR OpenGL finale réussie également : zéro pixel quasi blanc pour
|
||||
la lune, 22 111 pour le soleil et 40 749 pixels de reflet lunaire visibles.
|
||||
Intersections : 91 / 518 400 pixels changés sous rotation minuscule.
|
||||
Journal : `build/beta147-ssr-final-opengl.log` ; captures correspondantes
|
||||
dans `build/evidence/beta147-ssr-final-opengl/`.
|
||||
- `./gradlew check build` dans un instantané isolé : 252 tests serveur,
|
||||
229 réussis, 23 échecs, avec exactement les mêmes identifiants que beta.146.
|
||||
Journal : `build/beta147-check-build-isolated.log` ; comparaison :
|
||||
`build/beta147-server-failures.json`. La vérification globale reste en échec.
|
||||
Les derniers ajouts lune/256 concernent exclusivement le rendu client,
|
||||
ses réglages et ses tests ; ils sont validés par les suites finales ci-dessus.
|
||||
- Assemblage final réussi : `./gradlew build assemblePack -x :sanctuary:check`,
|
||||
après les contrôles purs et serveur exécutés dans l’instantané, sans les
|
||||
répéter. Journal : `build/beta147-build-pack.log`. Cette exclusion ne
|
||||
transforme pas le contrôle complet en réussite.
|
||||
|
||||
## Artefact
|
||||
|
||||
[Sanctuary-beta.147.mrpack](../build/Sanctuary-beta.147.mrpack), également copié
|
||||
à côté de beta.146 dans `sanctuary-beta/build/`. Aucun ancien pack remplacé,
|
||||
aucune installation Prism ou publication effectuée.
|
||||
|
||||
Archive ZIP vérifiée, un seul JAR Sanctuary beta.147, identique au JAR construit,
|
||||
shaders identiques aux sources finales, options FR/EN 256 présentes,
|
||||
Minecraft 26.3 / Fabric Loader 0.19.5 conservés. Taille : 10 519 851 octets.
|
||||
SHA-256 : `b8044243dbe160d5171ea584052cf5732393e4d91352c29c33b727af80c0a5bc`.
|
||||
Reçu : `build/beta147-artifacts.json`.
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
# RENDER-148 — PBR 50 % et reflet solaire diffus
|
||||
|
||||
Branche `codex/pbr-soft-sun-beta148`, socle beta.147 `96a36d9`.
|
||||
Minecraft 26.3, Java 25, dépendances et sauvegardes inchangés.
|
||||
|
||||
## Résultat
|
||||
|
||||
- PBR à 50 % par défaut, SSR toujours à 20 % et portée commune jusqu’à 256.
|
||||
- Les configurations anciennes contenant exactement PBR 80 % passent à 50 %.
|
||||
Un marqueur de migration conserve ensuite tout choix explicite, y compris
|
||||
un nouveau choix 80 %. Les autres anciennes intensités restent conservées.
|
||||
- Le reflet solaire de l’eau avait un renforcement ×20 et une compression
|
||||
exponentielle qui effaçait les détails dans une large zone blanche.
|
||||
Le renforcement passe à ×6 et le lobe est plus diffus (rugosité minimale
|
||||
de 0,42 au lieu de 0,34 pour le lobe céleste de l’eau).
|
||||
- Pour le soleil sur l’eau, fusion par texel entre éclaircissement vers le
|
||||
blanc et lumière douce fondée sur la couleur native. La pondération emploie
|
||||
une compression rationnelle, sans atteindre brutalement un plateau blanc.
|
||||
La texture et ses animations restent natives ; aucun bruit ajouté.
|
||||
- La lune conserve son intensité atténuée, sans renforcement solaire.
|
||||
Les lampes et matières solides conservent leur fusion spéculaire précédente.
|
||||
Le bloom reste réglable et suit la lumière réellement ajoutée.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Scènes plates jetables, Apple M1, Vulkan et OpenGL. Le test solaire contrôle
|
||||
une lumière largement visible et limite les pixels quasi blancs à moins de
|
||||
0,5 % de l’image, au lieu d’imposer une grande tache blanche.
|
||||
|
||||
Premier contrôle Vulkan à PBR 50 % / SSR 20 % : 149 908 pixels éclaircis,
|
||||
zéro pixel quasi blanc sur 854 × 480. Captures vues du dessus et à soleil bas
|
||||
inspectées : la texture reste visible dans la traînée lumineuse.
|
||||
Preuves : `build/evidence/beta148-preview/`.
|
||||
Rendu de la traînée solaire approuvé par le créateur après inspection de la capture.
|
||||
|
||||
- Suites SSR complètes Vulkan et OpenGL réussies : F5, intersections, portées,
|
||||
eau/verre/métal, OFF/zéro, lune, bloom, rechargement et redimensionnement.
|
||||
Journaux : `build/beta148-ssr-vulkan.log`, `build/beta148-ssr-opengl.log`.
|
||||
Sous OpenGL, 146 680 pixels éclaircis et zéro pixel quasi blanc dans la scène
|
||||
solaire. La lune reste visible sans saturation (35 865 pixels modifiés).
|
||||
Preuves : `build/evidence/beta148-ssr-vulkan/` et
|
||||
`build/evidence/beta148-ssr-opengl/`.
|
||||
|
||||
- Suites PBR/préférences Vulkan et OpenGL réussies : défaut 50 %, migration
|
||||
de l’ancien 80 %, conservation d’un nouveau choix explicite 80 %, valeurs
|
||||
personnalisées, FR/EN, proximité, OFF/zéro, intensités et rechargement.
|
||||
Journaux : `build/beta148-pbr-vulkan.log`, `build/beta148-pbr-opengl.log`.
|
||||
- `./gradlew check build` dans l’instantané isolé : 252 tests serveur,
|
||||
229 réussis, 23 échecs. Exactement les mêmes identifiants que beta.147,
|
||||
aucun nouvel échec. Journal : `build/beta148-check-build-isolated.log` ;
|
||||
comparaison : `build/beta148-server-failures.json`. Le contrôle global
|
||||
demeure donc en échec.
|
||||
- Assemblage final réussi : `./gradlew build assemblePack -x :sanctuary:check`,
|
||||
sans répéter les contrôles purs et serveur déjà exécutés dans l’instantané.
|
||||
Journal : `build/beta148-build-pack.log`. L’exclusion ne constitue pas
|
||||
une réussite du contrôle complet.
|
||||
|
||||
## Artefact
|
||||
|
||||
[Sanctuary-beta.148.mrpack](../build/Sanctuary-beta.148.mrpack), également copié
|
||||
à côté des autres versions dans `sanctuary-beta/build/`. Archive ZIP vérifiée,
|
||||
un seul JAR Sanctuary beta.148 identique au JAR construit, shaders finaux
|
||||
identiques aux sources, textes FR/EN 50 % et dépendances exactes vérifiés.
|
||||
Taille : 10 520 050 octets. SHA-256 :
|
||||
`597cc2a96eabc3cf0ebf8ac59b4f94cf7cabf0c773e1dce13c10736ca6fcba38`.
|
||||
Reçu : `build/beta148-artifacts.json`.
|
||||
Aucune publication ni installation Prism effectuée.
|
||||
|
||||
+221
@@ -0,0 +1,221 @@
|
||||
# PBR-SSR — eau, verre et reflets pixélisés
|
||||
|
||||
Livraison locale **beta.145**, branche `codex/pbr-ssr`. Socle PBR beta.142 /
|
||||
suivi beta.143 et copie figée des sources du chantier d’eau beta.144. Minecraft 26.3, API graphique native.
|
||||
|
||||
## Contrat
|
||||
|
||||
Les reflets colorent les texels de matière sur une grille 16 × 16 liée aux UV,
|
||||
avec les normales procédurales existantes. Eau et verre sont diélectriques.
|
||||
L’eau reprend les faces du tessellateur natif (niveaux, pentes, écoulements,
|
||||
waterlogging), sans modification de géométrie ni simulation de vagues.
|
||||
Le verre réfléchit aussi sur les zones sans pigment de sa texture. Les pixels
|
||||
pigmentés conservent leur relief même avec un alpha faible ; l’alpha nul
|
||||
utilise la normale géométrique.
|
||||
|
||||
Les images des normales d’eau sont préparées au chargement du matériau ; leur
|
||||
séquence suit l’horloge effective des animations de l’atlas, avec interpolation.
|
||||
Seules les petites tuiles de normales changent sur le GPU, pas le maillage.
|
||||
Les normales des textures haute résolution sont rééchantillonnées à 16 × 16.
|
||||
Au-delà de 1 024 images par sprite, un avertissement signale le repli vers
|
||||
une normale fixe pour borner la mémoire.
|
||||
|
||||
L’option FR/EN « Reflets du paysage — expérimental » est OFF initialement,
|
||||
intensité 50 %, bornée entre 0 et 100. Elle exige le PBR actif. L’eau naturelle
|
||||
reste indépendante pour ses couleurs, sa transparence et sa brume.
|
||||
|
||||
## Pipeline et limites
|
||||
|
||||
Couleur et profondeur du paysage sont copiées après les opaques et avant les
|
||||
transparences, dans les deux chemins natifs. Le chemin classique ferme la passe
|
||||
opaque avant la copie, puis reprend la transparence native dans une nouvelle
|
||||
passe. Aucune copie d’attachement Vulkan encore ouvert.
|
||||
|
||||
Une profondeur propre sélectionne le récepteur PBR le plus proche devant le
|
||||
paysage opaque. Une vitre devant l’eau reçoit seule le SSR ; les couches
|
||||
suivantes conservent leur rendu natif. La profondeur du fond n’est pas utilisée
|
||||
comme profondeur de surface de l’eau.
|
||||
|
||||
8 à 32 pas déterministes adaptés à la longueur écran, puis quatre raffinements maximum, récepteurs jusqu’à
|
||||
32 blocs et rayons jusqu’à 128 blocs. Pas de bruit aléatoire, historique,
|
||||
réflexion récursive, réfraction nouvelle, lecture GPU vers CPU ou ajout de
|
||||
lumière de gameplay. Les objets hors écran ou masqués ne sont pas retrouvés. Les détails fins entre
|
||||
deux pas peuvent être manqués ; une interpolation finale affine le point
|
||||
d’intersection sans augmenter le nombre de recherches.
|
||||
Un échec revient progressivement au rendu existant ; les transparences et les
|
||||
effets Sanctuary postérieurs à la capture ne figurent pas dans le reflet.
|
||||
|
||||
Les reflets sont composés après le SSGI, avant contours, bloom et rayons.
|
||||
La main et le HUD restent en dehors du traitement. Le PBR conserve son
|
||||
périmètre Overworld hors immersion et s’efface avec les moteurs externes.
|
||||
|
||||
Budget de maillages : 24 Mio de solides, 8 Mio supplémentaires de transparences.
|
||||
Deux profondeurs plein écran servent aux surfaces PBR ; la couleur de paysage
|
||||
SSR est allouée seulement à ON/intensité positive. La copie de composition PBR
|
||||
existante est réutilisée. Aucun buffer de normales plein écran.
|
||||
La grille visuelle ne signifie pas qu’un seul fragment GPU est exécuté par texel.
|
||||
|
||||
## Intégration
|
||||
|
||||
Le commit `38f4d6c` apporte le SSR et les récepteurs transparents. Le commit
|
||||
`0cfd51e` importe une copie contrôlée des sources beta.144 depuis le chantier
|
||||
`codex/water-beta144`, fondé sur `580b241`. Les empreintes des fichiers ont été
|
||||
comparées avant et après la copie ; le reçu local est dans
|
||||
`build/water144-snapshot/manifest.json`. Cette intégration ne prétend pas que
|
||||
beta.144 a été publiée ou que son benchmark isolé est terminé. Le code de
|
||||
production de cet instantané est identique à celui du commit
|
||||
`7eef945` ensuite créé sur la branche d’eau. Les essais ci-dessous portent
|
||||
sur les sources réunies dans cette branche.
|
||||
|
||||
Points d’entrée : `PbrMaterials` et `PbrAnimation` pour les normales animées,
|
||||
`PbrFluidMesh` pour le tessellateur natif, `PbrTerrain` pour les deux budgets,
|
||||
`ReflectionSceneMixin` pour la capture opaque, `PixelReflections` et
|
||||
`pixel_reflections.fsh` pour la sélection et le rayon. Les horloges de sprites
|
||||
sont référencées faiblement des deux côtés afin de ne pas retenir les anciens
|
||||
atlas après rechargement.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Machine : Apple M1, Minecraft 26.3, Java 25, macOS. Mondes plats de test neufs,
|
||||
graine 122 ; aucune sauvegarde personnelle utilisée. Les captures conservent
|
||||
le préfixe historique `sanctuary-beta142-shader` du banc de test.
|
||||
|
||||
- Client OpenGL 4.1 Metal 90.5, transparence classique : suite SSR réussie avec les sources d’eau
|
||||
intégrées (`build/ssr145-opengl-final.log`).
|
||||
- Client Vulkan/MoltenVK : suite SSR avec transparence améliorée réussie
|
||||
(`build/ssr145-vulkan-final.log`).
|
||||
- Régression du PBR historique et nouvelles assertions de profils, normales
|
||||
transparentes, persistance, bornes et traductions SSR : réussies
|
||||
(`build/ssr145-pbr-regression-retry.log`). Une première tentative interrompue
|
||||
s’était arrêtée sur un timeout de sémaphore Vulkan ; elle reste un échec
|
||||
consigné dans `build/ssr145-pbr-regression.log`.
|
||||
- Suite fonctionnelle eau beta.144 : réussie, y compris profils, fond modifié,
|
||||
brume, océan, rechargement, préférences, cascades et pack de matériaux
|
||||
(`build/ssr145-water-functional.log`). Son long benchmark autonome a été
|
||||
explicitement sauté ; le coût SSR est mesuré séparément ci-dessous.
|
||||
- Le relief rouge puis bleu produit deux contributions de couleur distinctes
|
||||
dans le bassin ; caché par un mur opaque, ce changement ne traverse pas le mur.
|
||||
- Eau animée sans nouvelle analyse de texture, verre transparent devant l’eau,
|
||||
métal, déplacement minime de caméra, rotation, OFF/zéro, immersion,
|
||||
rechargement de ressources et redimensionnement sont exercés par assertions.
|
||||
L’horloge d’eau reste active après rechargement ; les profils sable et étang
|
||||
restent corrects avec PBR et SSR simultanément actifs.
|
||||
- Des captures couvrent également les effets combinés, la lune, les angles
|
||||
rasants, la translation de caméra, la sortie du paysage hors écran, la pierre
|
||||
rugueuse OFF/ON, des vitres colorées superposées, un écoulement, une cascade
|
||||
et une dalle gorgée d’eau. Elles ne remplacent pas une inspection
|
||||
artistique de tous les angles et de tous les packs.
|
||||
|
||||
Captures locales OpenGL : [OFF](../build/ssr-evidence/beta145-opengl-final/0000_sanctuary-beta142-shader-ssr-water-off.png),
|
||||
[ON](../build/ssr-evidence/beta145-opengl-final/0001_sanctuary-beta142-shader-ssr-water-on.png).
|
||||
Le dossier `build/ssr-evidence/beta145-opengl-final/` contient les autres captures.
|
||||
Les mêmes cas Vulkan sont conservés dans
|
||||
`build/ssr-evidence/beta145-vulkan-final/`. Les comparaisons eau, métal et
|
||||
verre empilé ont été inspectées visuellement. Ces pièces sont ignorées par Git.
|
||||
|
||||
## Coût mesuré
|
||||
|
||||
1920 × 1080, PBR à 80 %, SSR à 100 % pour accentuer le résultat, VSync OFF,
|
||||
limite de FPS et réduction AFK désactivées. Chaque scène chauffe 30 ticks par
|
||||
état avant 80 ticks de mesure, soit 324 à 483 images pour OpenGL et 317 à 477 pour Vulkan. Ordre OFF puis
|
||||
ON ; il s’agit d’un diagnostic local court, pas d’une campagne statistique.
|
||||
Le temps de frame mesure `Minecraft.renderFrame`, présentation comprise.
|
||||
|
||||
| OpenGL | Médiane OFF / ON (ms) | p95 OFF / ON (ms) | Écart médian |
|
||||
| --- | --- | --- | --- |
|
||||
| Petit étang | 7,57 / 9,42 | 10,35 / 12,50 | +1,85 ms |
|
||||
| Eau dominante | 7,76 / 11,67 | 10,67 / 13,96 | +3,91 ms |
|
||||
| Construction vitrée | 7,85 / 10,12 | 10,67 / 12,65 | +2,27 ms |
|
||||
|
||||
| Vulkan | Médiane OFF / ON (ms) | p95 OFF / ON (ms) | Écart médian |
|
||||
| --- | --- | --- | --- |
|
||||
| Petit étang | 7,97 / 10,36 | 8,97 / 11,49 | +2,40 ms |
|
||||
| Eau dominante | 10,10 / 12,23 | 10,97 / 12,97 | +2,13 ms |
|
||||
| Construction vitrée | 8,52 / 11,25 | 9,49 / 12,20 | +2,72 ms |
|
||||
|
||||
La mesure GPU Vulkan autour de `PixelReflections.render` donne, à ON,
|
||||
**2,07 / 2,18 ms** (médiane / p95) pour l’étang, **2,56 / 2,63 ms** pour
|
||||
l’eau dominante et **2,09 / 2,21 ms** pour le verre. À OFF, la sonde mesure
|
||||
0,03 ms environ. Ce périmètre couvre la copie de composition et le dessin
|
||||
SSR ; il exclut la capture opaque antérieure et la profondeur partagée PBR.
|
||||
La sonde utilise les valeurs rendues par la requête native, pas une mesure
|
||||
GPU du jeu complet.
|
||||
|
||||
La requête GPU native ne fournit pas de mesure exploitable sur ce pilote
|
||||
OpenGL ; ce champ est indisponible, pas nul. Les premiers essais contaminés
|
||||
par la réduction AFK à 30 FPS sont exclus des résultats retenus.
|
||||
**L’objectif de surcoût médian inférieur à 2 ms n’est pas respecté dans l’ensemble des
|
||||
scènes.** L’option reste expérimentale et désactivée par défaut.
|
||||
|
||||
À 1080p : profondeurs partagées par le PBR = 15,82 Mio ; copie couleur SSR
|
||||
supplémentaire = 7,91 Mio, libérée à OFF/zéro. Total de ces cibles à ON :
|
||||
23,73 Mio. Les maillages observés occupent environ 0,51–0,55 Mio de solides et
|
||||
1,09–1,21 Mio de transparences, en dessous des plafonds 24 / 8 Mio vérifiés
|
||||
par assertion. Ces compteurs n’incluent ni la mémoire du moteur, ni la copie
|
||||
PBR existante, ni les atlas de matériaux de 6,25 Mio chacun ; ce ne sont pas
|
||||
des mesures de mémoire totale du processus.
|
||||
|
||||
## Construction et contrôles généraux
|
||||
|
||||
`./gradlew check build` a été exécuté jusqu’au bout : **229 GameTests serveur
|
||||
réussis sur 252, 23 échecs**, en 8 min 45 s pour la commande complète.
|
||||
Les échecs concernent les tombes/familiers, le catalogue, deux invariants de
|
||||
génération, les placements de blocs/progression/construction et les menus
|
||||
natifs. Les tests concernés et le code de gameplay serveur ne sont pas
|
||||
modifiés par ce lot. Aucun contre-test de toute la branche de départ n’est
|
||||
revendiqué. Le journal complet est `build/ssr145-check-build.log` ; le relevé
|
||||
exhaustif est `build/ssr145-server-failures.json`.
|
||||
|
||||
L’assemblage est repris avec `./gradlew check build assemblePack assembleTestPack
|
||||
-x :sanctuary:runGameTest`, en excluant uniquement cette tâche déjà exécutée.
|
||||
Cette commande réussit en 2 min 56 s (126 tâches) ; vérification du pack,
|
||||
compilation, contrôles hors GameTests serveur et assemblages passent. Les
|
||||
exports MRpack sont produits depuis leurs dossiers packwiz de staging.
|
||||
L’intégrité ZIP, les métadonnées, les traductions et mixins SSR ainsi que
|
||||
l’identité du JAR embarqué dans les deux packs sont contrôlées.
|
||||
Cette exclusion ne transforme pas le contrôle complet en succès.
|
||||
|
||||
## Reproduire les essais client
|
||||
|
||||
Base de chaque commande :
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runClientGameTest \
|
||||
-PsanctuaryClientTests=true -PsanctuaryQuickTests=true \
|
||||
-PsanctuaryPbr134ClientTests=true -PsanctuaryClientNoVsync=true
|
||||
```
|
||||
|
||||
- SSR Vulkan : ajouter `-PsanctuarySsrClientTests=true
|
||||
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryClientImprovedTransparency=true`.
|
||||
- SSR OpenGL : ajouter `-PsanctuarySsrClientTests=true
|
||||
-PsanctuaryClientGraphicsBackend=opengl -PsanctuaryClientImprovedTransparency=false`.
|
||||
- Régression PBR : ajouter uniquement `-PsanctuaryClientGraphicsBackend=vulkan`.
|
||||
- Profils et rendu de l’eau : ajouter `-PsanctuaryWater144ClientTests=true
|
||||
-PsanctuaryWater144FunctionalOnly=true -PsanctuaryClientGraphicsBackend=vulkan`.
|
||||
|
||||
Le mode fonctionnel saute uniquement le long benchmark autonome beta.144.
|
||||
Les sondes de temps de frame/GPU et lectures d’images appartiennent au mod
|
||||
de test ; elles ne sont pas embarquées dans le JAR de production.
|
||||
|
||||
## Artefacts locaux
|
||||
|
||||
Les archives finales sont rafraîchies depuis le JAR testé, puis revérifiées
|
||||
(`build/ssr145-artifacts.json`). Aucun tag ni canal n’est publié.
|
||||
|
||||
| Artefact | Octets | SHA-256 |
|
||||
| --- | ---: | --- |
|
||||
| [sanctuary-beta.145.jar](../mods/sanctuary/build/libs/sanctuary-beta.145.jar) | 11369411 | `55341e2f5c8dcc352190fe96d15e86cd1ee6bb4ea2908441b70491d48f86f608` |
|
||||
| [Sanctuary-beta.145.mrpack](../build/Sanctuary-beta.145.mrpack) | 10508045 | `8bebe6227cbfe629c714a7c7547640ab5ccfbefaaf211484a93c042cfb42b5c5` |
|
||||
| [Sanctuary-Test-beta.145.mrpack](../build/Sanctuary-Test-beta.145.mrpack) | 10526966 | `54402ff8414ed1ae0d12b3b97af70c547c44dde2f3aa183712ae3769f99efbe7` |
|
||||
| [Sanctuary-Template-beta.145.zip](../build/Sanctuary-Template-beta.145.zip) | 4358316 | `226d92362a5ce922b8757b36f30c312081aca5cf73a299b82a221a12f384a95b` |
|
||||
|
||||
## Limites de validation
|
||||
|
||||
Le banc utilise un relief construit reproductible, pas une montagne générée
|
||||
représentative de tous les biomes. La validation artistique sur terrain généré,
|
||||
les textures personnalisées avec ordre/durées/interpolation atypiques et la
|
||||
stabilité sur un long trajet restent à compléter. Les surfaces peuvent perdre les petits détails entre
|
||||
les pas SSR et les zones hors écran ne sont jamais reconstruites. Windows,
|
||||
autres GPU et moteurs graphiques externes ne sont pas validés ici.
|
||||
|
||||
Aucune publication de canal ni synchronisation Prism dans cette livraison.
|
||||
@@ -0,0 +1,112 @@
|
||||
# RENDER-146 — Reflets, portée commune et transitions de l’eau
|
||||
|
||||
Branche `codex/reflections-water-beta146`, socle beta.145 `ed2d088`.
|
||||
Minecraft 26.3, Java 25 et dépendances inchangés. Préparation locale uniquement.
|
||||
|
||||
## Contrat
|
||||
|
||||
- Les passes PBR et SSR vérifient la profondeur finale du rendu natif pour
|
||||
masquer les personnages, notamment en F5. La profondeur du paysage capturée
|
||||
avant les transparences reste celle utilisée pour rechercher le reflet.
|
||||
- Le PBR utilise le test matériel EQUAL contre la profondeur du récepteur
|
||||
sélectionné. L’ancienne tolérance de 3,5 cm pouvait accepter des faces
|
||||
concurrentes près des coins ; elle est supprimée. La comparaison avec la
|
||||
profondeur native conserve seulement une marge de 5 mm pour les différences
|
||||
d’ordre de calcul des matrices, sans relâcher la sélection stricte du récepteur.
|
||||
La position du vertex est déclarée invariante entre les passes.
|
||||
- PBR à 80 % et SSR à 20 % par défaut, pour toutes les matières, eau comprise.
|
||||
Deux intensités d’effets distinctes, sans réglage séparé solide/liquide.
|
||||
Les préférences explicites déjà enregistrées restent conservées.
|
||||
- Distance PBR/SSR commune : 16, 32 (défaut), 64, 128 blocs. Maillages,
|
||||
sélection des récepteurs et fondu final emploient ce réglage. Le rayon SSR
|
||||
conserve sa recherche maximale de 128 blocs dans le paysage visible.
|
||||
Budgets de maillages inchangés : solides 24 Mio, transparences 8 Mio.
|
||||
Une forte densité à grande portée peut donc limiter les surfaces disponibles.
|
||||
- Le reflet du soleil adopte un lobe plus large sur l’eau, avec une irradiance
|
||||
renforcée : grande empreinte blanche vue du dessus et traînée étendue aux
|
||||
angles rasants. Les lampes conservent leur lobe local.
|
||||
Les éclats spéculaires ne sont plus plafonnés au gris moyen. Leur compression
|
||||
conserve une valeur finie pouvant approcher le blanc. Une contribution douce
|
||||
des seules passes PBR/SSR alimente le bloom existant, sans faire briller tous
|
||||
les blocs blancs. Le réglage Bloom et son intensité gardent autorité.
|
||||
- Eau naturelle OFF par défaut dans une dimension ordinaire, ON pour une
|
||||
dimension dont le générateur est Sanctuary / ExpansionChunkGenerator.
|
||||
Le serveur transmet ce contexte à la connexion, au changement de dimension
|
||||
et à la réapparition, y compris en multijoueur. Le défaut automatique nécessite
|
||||
ce contexte envoyé par un serveur beta.146 ; sans lui, le client utilise le
|
||||
défaut ordinaire OFF, toujours réglable. Aucune donnée de monde modifiée.
|
||||
Les choix ON/OFF sont mémorisés séparément pour ces deux catégories.
|
||||
Un ancien OFF explicite est conservé dans les deux ; un ancien ON global
|
||||
adopte les nouveaux défauts par contexte.
|
||||
- Mélange des profils sur 1×1, 3×3, 5×5, 9×9 (défaut) ou 17×17 blocs, puis
|
||||
interpolation aux sommets. Océans toujours clairs. La texture demeure native.
|
||||
Surface et immersion utilisent la même étendue. Les changements du fond
|
||||
invalident toute la largeur dépendante ; changer le réglage reconstruit les
|
||||
sections chargées contenant de l’eau.
|
||||
|
||||
Le filtre de surface est calculé dans la copie de région native, avec sommes
|
||||
préfixées par hauteur pour borner le coût des sommets. Aucun accès au monde
|
||||
vivant depuis le worker, chargement de chunk ou modification de sauvegarde.
|
||||
Les textes des nouvelles options sont fournis en français et en anglais.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Machine : Apple M1, macOS, Java 25, Minecraft 26.3. Mondes plats jetables,
|
||||
graine 122 ; aucune sauvegarde personnelle utilisée.
|
||||
|
||||
- Suite SSR/PBR Vulkan avec transparence améliorée : réussie
|
||||
(`build/beta146-final-settings-vulkan.log`). F5 avec personnage réel en créatif,
|
||||
portée 16/32/64/128, couleur du paysage, eau/verre/métal, OFF/zéro,
|
||||
immersion, rechargement de ressources et redimensionnement.
|
||||
- Suite SSR/PBR OpenGL finale réussie également
|
||||
(`build/beta146-final-settings-opengl.log`), avec 22 450 pixels quasi blancs
|
||||
dans la scène solaire et 99 / 518 400 pixels modifiés dans le contrôle
|
||||
des intersections. Captures dans `build/evidence/final-settings-opengl/`.
|
||||
- F5 : les 1 900 pixels du torse contrôlés sont strictement identiques entre
|
||||
SSR OFF et ON, tandis que le reflet reste présent autour du personnage.
|
||||
- Soleil : 22 111 pixels quasi blancs sur 854 × 480 à PBR 80 % / SSR 20 %,
|
||||
sans bloom, sur la scène de contrôle Vulkan. Les captures vues du dessus et
|
||||
à soleil bas montrent une empreinte large et une traînée allongée.
|
||||
Bloom OFF libère sa cible.
|
||||
- Mur, sol et escaliers en contact : 86 pixels sur 518 400 changent de plus
|
||||
de 12 niveaux cumulés lors d’une rotation de 0,00002 degré ; seuil du test
|
||||
0,1 %. Le cadre exclut l’eau animée, qui ne constitue pas une référence
|
||||
de stabilité pour les blocs statiques.
|
||||
|
||||
- Eau : suites fonctionnelles Vulkan et OpenGL réussies
|
||||
(`build/beta146-water-vulkan.log`, `build/beta146-water-opengl.log`).
|
||||
Profils, mélange 5/9/17, changement de contexte transmis par le serveur,
|
||||
préférences séparées, immersion, fond modifié, océans, eau courante,
|
||||
waterlogging et rechargement. OpenGL contrôle aussi l’égalité des couleurs
|
||||
aux frontières des copies de région et avec l’échantillonnage de brume.
|
||||
- `./gradlew check build` exécuté dans un instantané isolé des sources :
|
||||
252 tests serveur, 229 réussis et 23 échecs. Les identifiants des échecs
|
||||
correspondent exactement au reçu archivé beta.145 ; aucun nouvel identifiant.
|
||||
Il ne s’agit pas d’une nouvelle exécution comparative de beta.145.
|
||||
Journal : `build/beta146-check-build-isolated.log` ; comparaison :
|
||||
`build/beta146-server-failures.json`. Les derniers changements après cet
|
||||
instantané concernent le rendu client, ses réglages et ses tests.
|
||||
|
||||
Les preuves Vulkan finales sont dans `build/evidence/final-settings-vulkan/`.
|
||||
La suite de régression PBR et de préférences finale réussit
|
||||
(`build/beta146-final-pbr-preferences.log`) : PBR 80 %, SSR 20 %, persistance,
|
||||
migration des anciennes préférences, FR/EN, OFF/zéro et rechargement.
|
||||
La commande finale `./gradlew check build assemblePack -x :sanctuary:runGameTest`
|
||||
réussit (125 tâches), sans répéter la suite serveur isolée déjà exécutée.
|
||||
Journal : `build/beta146-final-build-pack.log`. Cette exclusion ne transforme
|
||||
pas les 23 échecs serveur en validation globale réussie.
|
||||
|
||||
## Artefact local
|
||||
|
||||
Le pack [Sanctuary-beta.146.mrpack](../build/Sanctuary-beta.146.mrpack) est exporté
|
||||
et copié dans le dossier `sanctuary-beta/build/`, à côté des autres versions.
|
||||
Intégrité ZIP, version beta.146, Minecraft 26.3 et identité du JAR embarqué
|
||||
avec le JAR construit contrôlées ; shaders finaux présents et identiques aux sources.
|
||||
Taille : 10 519 634 octets. SHA-256 :
|
||||
`9f3f7a840652d401741cbe3c85bbb5626326542179754cb599479b01a6b4c955`.
|
||||
Reçu : `build/beta146-artifacts.json`.
|
||||
|
||||
Aucun benchmark isolé de performance de beta.146 n’est revendiqué : des
|
||||
contrôles serveur peuvent tourner simultanément. Les limites SSR hors écran,
|
||||
de maillage à grande portée et de compatibilité avec d’autres GPU subsistent.
|
||||
Aucune publication ou synchronisation Prism.
|
||||
@@ -107,3 +107,9 @@ Les contrôles ne revendiquent pas un essai avec Iris ou un autre moteur externe
|
||||
Les ombres portées pixélisées sont livrées dans [beta.127](shader-beta127.md),
|
||||
intégrées sur la beta.126. Leur préparation locale portait un numéro beta.122
|
||||
non publié ; la release beta.122 existante n'est pas remplacée.
|
||||
|
||||
## beta.144 — Eau naturelle
|
||||
|
||||
Le [ticket WATER-144](natural-water-beta144.md) ajoute les profils clair,
|
||||
légèrement teinté et étang au rendu natif des fluides et à leur brouillard.
|
||||
La couleur du biome et les textures pixélisées restent la base.
|
||||
|
||||
@@ -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
@@ -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.144
|
||||
pack_version=beta.144
|
||||
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
|
||||
|
||||
@@ -34,6 +34,11 @@ if (clientRenderTests) {
|
||||
if (improvedTransparency.isPresent()) {
|
||||
systemProperty('sanctuary.test.improvedTransparency', improvedTransparency.get())
|
||||
}
|
||||
systemProperty('sanctuary.test.ssr', providers.gradleProperty('sanctuarySsrClientTests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.water144', providers.gradleProperty('sanctuaryWater144ClientTests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.water144ExtrasOnly', providers.gradleProperty('sanctuaryWater144ExtrasOnly').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.water144FunctionalOnly', providers.gradleProperty('sanctuaryWater144FunctionalOnly').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.water144PerfOnly', providers.gradleProperty('sanctuaryWater144PerfOnly').getOrElse('false'))
|
||||
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'))
|
||||
@@ -1137,14 +1142,6 @@ tasks.register('intro031Smoke', JavaExec) {
|
||||
}
|
||||
tasks.named('check') { dependsOn('intro031Smoke') }
|
||||
|
||||
tasks.register('access144Smoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.progression.AccessCodeSmoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('access144Smoke') }
|
||||
|
||||
if (providers.gradleProperty('sanctuaryQuickTests').map { it.toBoolean() }.getOrElse(false)) {
|
||||
dependencies { gametestRuntimeOnly project(':sanctuary-test') }
|
||||
}
|
||||
|
||||
+127
-3
@@ -27,12 +27,14 @@ public final class Pbr134ClientChecks implements FabricClientGameTest {
|
||||
c.runOnClient(m->check(analyses==ProceduralPbr.analyses()&&ProceduralPbr.materialCount()>0,"Source textures are cached, not regenerated every frame"));
|
||||
unchanged(on,capture(c,null),"Static PBR has no animated noise");
|
||||
look(c,server,.00002f,40.00002f);frames(c);unchanged(on,capture(c,null),"PBR tiny camera motion");look(c,server,0,40);frames(c);
|
||||
closeRange(c,server);
|
||||
camera(c,server,10.5,69,5.5,65);look(c,server,0,40);settle(c);
|
||||
c.runOnClient(m->VanillaLight.setPbrIntensity(20));frames(c);var low=difference(off,capture(c,"pbr-20"));
|
||||
check(delta.total>low.total*1.3,"PBR strength affects actual GPU output");
|
||||
c.runOnClient(m->VanillaLight.setPbrIntensity(100));frames(c);var high=difference(off,capture(c,"pbr-100"));check(high.total>delta.total*1.2,"Strong PBR visibly increases relief");
|
||||
c.runOnClient(m->VanillaLight.setPbrIntensity(0));c.waitTicks(8);unchanged(off,capture(c,null),"PBR zero restores native image");
|
||||
c.runOnClient(m->{VanillaLight.setPbrIntensity(0);});c.waitTicks(8);unchanged(off,capture(c,null),"PBR zero restores native image");
|
||||
c.runOnClient(m->check(!ProceduralPbr.allocated()&&ProceduralPbr.materialCount()==0,"Zero releases targets and material cache"));
|
||||
c.runOnClient(m->VanillaLight.setPbrIntensity(50));frames(c);
|
||||
c.runOnClient(m->{VanillaLight.setPbrIntensity(50);});frames(c);
|
||||
server.runOnServer(s->command(s,"time set 11000"));settle(c);enable(c,false);var eveningOff=capture(c,null);enable(c,true);var eveningOn=capture(c,"pbr-evening");
|
||||
int directions=0;for(int i=0;i<on.pixels().length;i++){int am=(on.pixels()[i]>>16&255)-(off.pixels()[i]>>16&255),pm=(eveningOn.pixels()[i]>>16&255)-(eveningOff.pixels()[i]>>16&255);if(Math.abs(am-pm)>5)directions++;}
|
||||
check(directions>500,"Relief follows the moving sun: "+directions);
|
||||
@@ -48,21 +50,143 @@ 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");
|
||||
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 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->{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,"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(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("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));
|
||||
return (max-min)/(double)Math.max(1,max);
|
||||
}
|
||||
private static void closeRange(ClientGameTestContext c,TestServerContext server){
|
||||
int failures=0;
|
||||
c.runOnClient(m->VanillaLight.setPbrIntensity(80));
|
||||
for(double distance:new double[]{.12,.25,.5,1.0}){
|
||||
double eye=c.computeOnClient(m->(double)m.player.getEyeHeight());
|
||||
camera(c,server,4.5,65-eye+distance,5.5,65);look(c,server,17,55);settle(c);frames(c);
|
||||
var before=capture(c,"pbr147-close-"+distance);
|
||||
look(c,server,17.00002f,55.00002f);frames(c);var after=capture(c,"pbr147-close-moved-"+distance);
|
||||
int changed=0,count=0;
|
||||
for(int y=before.height()/10;y<before.height()*9/10;y++)for(int x=before.width()/10;x<before.width()*9/10;x++){
|
||||
int i=y*before.width()+x,a=before.pixels()[i],b=after.pixels()[i];count++;
|
||||
if(Math.abs((a>>16&255)-(b>>16&255))+Math.abs((a>>8&255)-(b>>8&255))+Math.abs((a&255)-(b&255))>12)changed++;
|
||||
}
|
||||
System.out.println("PBR147_CLOSE distance="+distance+" changed="+changed+" samples="+count);
|
||||
if(changed>count*.001)failures++;
|
||||
}
|
||||
c.runOnClient(m->VanillaLight.setPbrIntensity(50));
|
||||
check(failures==0,"Close PBR stays stable under subpixel motion: failing distances="+failures);
|
||||
}
|
||||
private static void materials(){
|
||||
int[] flat=new int[16];Arrays.fill(flat,0xFF808080);var profile=PbrMaterials.profile("bricks");
|
||||
for(int p:PbrMaterials.generate(flat,4,4,profile))check((p>>8&0xFFFF)==0x8080,"Flat source has neutral normal");
|
||||
flat[5]=0xFFFFFFFF;var slope=PbrMaterials.generate(flat,4,4,profile);check((slope[4]>>16&255)<128&&(slope[6]>>16&255)>128,"Height gradients produce opposite normal slopes");
|
||||
check(PbrMaterials.profile("iron_block").metal()>.8&&PbrMaterials.profile("iron_ore").metal()==0,"Metal block profile does not turn ore host rock into metal");
|
||||
check(PbrMaterials.family(Blocks.WATER.defaultBlockState())==2&&PbrMaterials.family(Blocks.GLASS.defaultBlockState())==1&&PbrMaterials.family(Blocks.GLASS_PANE.defaultBlockState())==1,"Water and glass receiver families");
|
||||
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{
|
||||
var p=FabricLoader.getInstance().getConfigDir().resolve("sanctuary-shaders.json");Files.writeString(p,"{\"ssgi\":true,\"ssgiIntensity\":35,\"bloom\":false}");forgetPreferences();
|
||||
check(VanillaLight.pbr()&&VanillaLight.pbrIntensity()==80&&VanillaLight.ssgi()&&!VanillaLight.bloom(),"Missing PBR setting defaults ON while preserving explicit old choices");
|
||||
check(VanillaLight.pbr()&&VanillaLight.pbrIntensity()==50&&VanillaLight.ssgi()&&!VanillaLight.bloom(),"Missing PBR setting defaults ON while preserving explicit old choices");
|
||||
Files.writeString(p,"{\"pbrIntensity\":80}");forgetPreferences();
|
||||
check(VanillaLight.pbrIntensity()==50,"Previous 80 percent baseline migrates to 50 once");
|
||||
VanillaLight.setPbrIntensity(80);forgetPreferences();check(VanillaLight.pbrIntensity()==80,"New explicit 80 percent choice persists after migration");
|
||||
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");}
|
||||
check(VanillaLight.pbrDistance()==64&&VanillaLight.ssrDistance()==64,"Independent distances default to 64");
|
||||
check(!VanillaLight.naturalWater(false)&&VanillaLight.naturalWater(true),"World-specific water defaults");
|
||||
for(int n:new int[]{16,32,64,128,256}){VanillaLight.setPbrDistance(n);forgetPreferences();check(VanillaLight.pbrDistance()==n&&VanillaLight.ssrDistance()==64,"PBR distance changes independently");}
|
||||
for(int n:new int[]{16,32,64,128,256}){VanillaLight.setSsrDistance(n);forgetPreferences();check(VanillaLight.ssrDistance()==n&&VanillaLight.pbrDistance()==256,"SSR distance changes independently");}
|
||||
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 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();
|
||||
for(String locale:new String[]{"fr_fr","en_us"}){
|
||||
manager.setSelected(locale);manager.onResourceManagerReload(net.minecraft.client.Minecraft.getInstance().getResourceManager());
|
||||
for(String key:new String[]{"ssr_distance","ssr_distance_help","ssr_distance_value","pbr_distance","pbr_distance_help","pbr_distance_value","water_blend","water_blend_help","water_blend_value","ssr","ssr_help","ssr_intensity","ssr_intensity_help","ssr_intensity_value"}){
|
||||
String full="sanctuary.shaders."+key;check(!net.minecraft.network.chat.Component.translatable(full,50).getString().equals(full),"SSR FR/EN "+full);
|
||||
}
|
||||
}
|
||||
check(net.minecraft.network.chat.Component.translatable("sanctuary.shaders.ssr").getString().equals("SSR"),"SSR label without experimental qualifier");
|
||||
manager.setSelected(oldLanguage);manager.onResourceManagerReload(net.minecraft.client.Minecraft.getInstance().getResourceManager());
|
||||
Files.writeString(p,"{\"naturalWater\":false,\"pbrIntensity\":37}");forgetPreferences();
|
||||
check(!VanillaLight.naturalWater(false)&&!VanillaLight.naturalWater(true)&&VanillaLight.pbrIntensity()==37,"Legacy water OFF and personal PBR intensity are preserved");
|
||||
Files.writeString(p,"{\"naturalWater\":true}");forgetPreferences();
|
||||
check(!VanillaLight.naturalWater(false)&&VanillaLight.naturalWater(true),"Legacy global ON adopts contextual defaults");
|
||||
VanillaLight.setPbrIntensity(50);
|
||||
}catch(Exception e){throw new AssertionError(e);}}
|
||||
private record Delta(int changed,int brighter,int darker,long total){}
|
||||
|
||||
+265
@@ -0,0 +1,265 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import static fr.koka.sanctuary.client.shader.PixelShadows122ClientChecks.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.TestServerContext;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
|
||||
/** Native screenshots on a new development world; source-colour and transparent receiver checks. */
|
||||
final class PixelReflectionsClientChecks {
|
||||
static void run(ClientGameTestContext c,TestServerContext server){
|
||||
c.runOnClient(m->{VanillaLight.setEnabled(true);VanillaLight.setSaturation(100);VanillaLight.setPbr(true);VanillaLight.setPbrIntensity(80);
|
||||
VanillaLight.setNaturalWater(true);VanillaLight.setWaterIntensity(100);VanillaLight.setWaterBlendRadius(0);VanillaLight.setSsr(false);VanillaLight.setSsrIntensity(100);VanillaLight.setColoredLights(false);VanillaLight.setSsgi(false);
|
||||
VanillaLight.setSunshafts(false);VanillaLight.setLensFlare(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);
|
||||
VanillaLight.setEdgeHighlights(false);VanillaLight.setPixelShadows(false);});
|
||||
server.runOnServer(s->{
|
||||
command(s,"time set 6000");
|
||||
for(int x=-18;x<=18;x++)for(int z=-4;z<=24;z++){
|
||||
for(int y=65;y<=85;y++)s.overworld().setBlockAndUpdate(new BlockPos(x,y,z),Blocks.AIR.defaultBlockState());
|
||||
s.overworld().setBlockAndUpdate(new BlockPos(x,64,z),Blocks.STONE.defaultBlockState());
|
||||
if(z>=3&&z<=18)s.overworld().setBlockAndUpdate(new BlockPos(x,65,z),Blocks.WATER.defaultBlockState());
|
||||
}
|
||||
for(int x=-12;x<=12;x++)for(int y=65;y<=77-Math.abs(x)/2;y++)
|
||||
s.overworld().setBlockAndUpdate(new BlockPos(x,y,19),Blocks.CONCRETE.red().defaultBlockState());
|
||||
});
|
||||
camera(c,server,.5,68,0,70);look(c,server,0,12);settle(c);
|
||||
int analysis=c.computeOnClient(m->ProceduralPbr.waterAnalyses()),updates=c.computeOnClient(m->ProceduralPbr.waterUpdates());
|
||||
c.waitTicks(20);c.runOnClient(m->check(ProceduralPbr.waterAnalyses()==analysis&&ProceduralPbr.waterUpdates()>updates,"Water normals follow native animation without reanalysis"));
|
||||
var off=capture(c,"ssr-water-off");
|
||||
c.runOnClient(m->VanillaLight.setSsr(true));frames(c);var on=capture(c,"ssr-water-on");
|
||||
dumpLandscape(c);
|
||||
long changed=changed(off,on);System.out.println("SSR_WATER_DIFF "+changed);
|
||||
check(changed>100,"SSR changes water receiver pixels: "+changed);
|
||||
double redSignal=colourSignal(off,on);
|
||||
server.runOnServer(s->{command(s,"gamemode creative @a");var player=s.getPlayerList().getPlayers().getFirst();player.getAbilities().flying=true;player.onUpdateAbilities();});
|
||||
c.waitTicks(10);
|
||||
c.runOnClient(m->m.options.setCameraType(net.minecraft.client.CameraType.THIRD_PERSON_BACK));c.waitTicks(20);
|
||||
c.runOnClient(m->VanillaLight.setSsr(false));c.waitTicks(8);var f5Off=capture(c,"ssr146-f5-off");
|
||||
c.runOnClient(m->VanillaLight.setSsr(true));frames(c);var f5On=capture(c,"ssr146-f5-on");
|
||||
check(changed(f5Off,f5On)>100,"SSR remains visible around third-person player");
|
||||
int torsoChange=0,torsoPixels=0;
|
||||
for(int y=f5Off.height()*55/100;y<f5Off.height()*71/100;y++)for(int x=f5Off.width()*485/1000;x<f5Off.width()*515/1000;x++){
|
||||
int index=y*f5Off.width()+x;torsoPixels++;if(f5Off.pixels()[index]!=f5On.pixels()[index])torsoChange++;
|
||||
}
|
||||
check(torsoChange<torsoPixels*.01,"SSR does not draw through the visible F5 torso: "+torsoChange+" / "+torsoPixels);
|
||||
c.runOnClient(m->m.options.setCameraType(net.minecraft.client.CameraType.FIRST_PERSON));c.waitTicks(20);
|
||||
server.runOnServer(s->command(s,"gamemode spectator @a"));c.waitTicks(10);
|
||||
for(int distance:new int[]{16,64,128,256,32}){
|
||||
c.runOnClient(m->{VanillaLight.setPbrDistance(distance);VanillaLight.setSsrDistance(256);});settle(c);frames(c);capture(c,"ssr146-distance-"+distance);
|
||||
c.runOnClient(m->check(ProceduralPbr.applied()&&PixelReflections.applied(),"Independent material distances render at PBR "+distance+" / SSR 256"));
|
||||
}
|
||||
|
||||
for(int distance:new int[]{16,64,256}){
|
||||
c.runOnClient(m->{VanillaLight.setPbrDistance(256);VanillaLight.setSsrDistance(distance);});settle(c);frames(c);
|
||||
capture(c,"ssr149-independent-"+distance);
|
||||
c.runOnClient(m->check(ProceduralPbr.applied()&&PixelReflections.applied(),"PBR 256 remains active with independent SSR "+distance));
|
||||
}
|
||||
c.runOnClient(m->VanillaLight.setPbrDistance(32));
|
||||
c.runOnClient(m->VanillaLight.setSsrDistance(32));
|
||||
c.runOnClient(m->{VanillaLight.setPbrIntensity(50);VanillaLight.setSsrIntensity(20);});
|
||||
camera(c,server,.5,68,10,70);settle(c);frames(c);
|
||||
c.runOnClient(m->VanillaLight.setPbr(false));c.waitTicks(8);var solarOff=capture(c,"ssr148-solar-water-off");
|
||||
c.runOnClient(m->VanillaLight.setPbr(true));frames(c);
|
||||
var solar=capture(c,"ssr146-solar-water");
|
||||
int white=0;for(int pixel:solar.pixels())if((pixel>>16&255)>235&&(pixel>>8&255)>235&&(pixel&255)>235)white++;
|
||||
int brightened=0;for(int i=0;i<solar.pixels().length;i++){
|
||||
int a=solarOff.pixels()[i],b=solar.pixels()[i];
|
||||
if((b>>16&255)+(b>>8&255)+(b&255)>(a>>16&255)+(a>>8&255)+(a&255)+24)brightened++;
|
||||
}
|
||||
check(brightened>5000,"Soft solar reflection remains broadly visible: "+brightened);
|
||||
check(white<solar.pixels().length*.005,"Soft solar reflection preserves texture instead of a white plateau: "+white);
|
||||
System.out.println("PBR148_SOLAR brightened="+brightened+" white="+white);
|
||||
System.out.println("SSR146_SOLAR_WHITE pixels="+white+" width="+solar.width()+" height="+solar.height());
|
||||
c.runOnClient(m->VanillaLight.setBloom(true));frames(c);capture(c,"ssr146-solar-water-bloom");
|
||||
c.runOnClient(m->check(MaterialBloom.contribution()!=null,"Material light reaches bloom"));
|
||||
c.runOnClient(m->VanillaLight.setBloom(false));frames(c);
|
||||
c.runOnClient(m->check(MaterialBloom.contribution()==null,"Bloom OFF releases material halo"));
|
||||
server.runOnServer(s->command(s,"time set 18000"));c.waitTicks(15);
|
||||
c.runOnClient(m->VanillaLight.setSsr(false));c.waitTicks(8);
|
||||
var moon=capture(c,"ssr147-moon-water");
|
||||
int moonWhite=0;for(int pixel:moon.pixels())if((pixel>>16&255)>235&&(pixel>>8&255)>235&&(pixel&255)>235)moonWhite++;
|
||||
check(moonWhite<Math.max(10,white*.05),"Moon reflection stays much softer than the sun: "+moonWhite+" / "+white);
|
||||
c.runOnClient(m->VanillaLight.setPbr(false));c.waitTicks(8);var moonOff=capture(c,"ssr147-moon-water-off");
|
||||
check(changed(moonOff,moon)>100,"Soft moon reflection remains visible");
|
||||
System.out.println("PBR147_MOON white="+moonWhite+" sunWhite="+white+" changed="+changed(moonOff,moon));
|
||||
c.runOnClient(m->{VanillaLight.setPbr(true);VanillaLight.setSsr(true);VanillaLight.setBloom(true);});frames(c);capture(c,"ssr147-moon-water-bloom");
|
||||
c.runOnClient(m->VanillaLight.setBloom(false));
|
||||
server.runOnServer(s->command(s,"time set 1000"));c.waitTicks(10);
|
||||
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);
|
||||
server.runOnServer(s->{for(int x=-12;x<=12;x++)for(int y=65;y<=77-Math.abs(x)/2;y++)s.overworld().setBlockAndUpdate(new BlockPos(x,y,19),Blocks.CONCRETE.blue().defaultBlockState());});
|
||||
settle(c);c.runOnClient(m->VanillaLight.setSsr(false));c.waitTicks(8);var blueOff=capture(c,"ssr-blue-off");
|
||||
c.runOnClient(m->VanillaLight.setSsr(true));frames(c);var blueOn=capture(c,"ssr-blue-on");double blueSignal=colourSignal(blueOff,blueOn);
|
||||
System.out.println("SSR_SOURCE_COLOUR red="+redSignal+" blue="+blueSignal);
|
||||
check(redSignal>blueSignal+3,"Water reflection follows source colour rather than animation: "+redSignal+" / "+blueSignal);
|
||||
server.runOnServer(s->{for(int x=-12;x<=12;x++)for(int y=65;y<=77-Math.abs(x)/2;y++)s.overworld().setBlockAndUpdate(new BlockPos(x,y,19),Blocks.CONCRETE.red().defaultBlockState());});settle(c);
|
||||
c.runOnClient(m->VanillaLight.setSsrIntensity(0));c.waitTicks(8);
|
||||
check(!c.computeOnClient(m->PixelReflections.applied()||PixelReflections.allocated()),"Zero removes SSR contribution and colour allocation");
|
||||
c.runOnClient(m->VanillaLight.setSsrIntensity(100));frames(c);
|
||||
for(var bed:new net.minecraft.world.level.block.Block[]{Blocks.SAND,Blocks.MUD}){
|
||||
server.runOnServer(s->{for(int x=-18;x<=18;x++)for(int z=3;z<=18;z++){s.overworld().setBlockAndUpdate(new BlockPos(x,63,z),Blocks.STONE.defaultBlockState());s.overworld().setBlockAndUpdate(new BlockPos(x,64,z),bed.defaultBlockState());}});
|
||||
settle(c);frames(c);
|
||||
c.runOnClient(m->check(NaturalWater.sample(m.level,.5,65.5,8.5).equals(bed==Blocks.SAND?NaturalWater.LIGHT:NaturalWater.POND),"Natural water profile remains independent of PBR and SSR: "+NaturalWater.sample(m.level,.5,65.5,8.5)+" above "+m.level.getBlockState(new BlockPos(0,64,8))));
|
||||
capture(c,bed==Blocks.SAND?"ssr-sand-profile":"ssr-pond-profile");
|
||||
}
|
||||
server.runOnServer(s->{for(int x=-18;x<=18;x++)for(int z=3;z<=18;z++)s.overworld().setBlockAndUpdate(new BlockPos(x,64,z),Blocks.STONE.defaultBlockState());});settle(c);frames(c);
|
||||
for(float yaw:new float[]{-25,0,25}){look(c,server,yaw,12);frames(c);capture(c,"ssr-water-angle-"+(int)yaw);}
|
||||
look(c,server,0,2);frames(c);capture(c,"ssr-grazing");
|
||||
look(c,server,75,12);frames(c);capture(c,"ssr-landscape-offscreen");
|
||||
camera(c,server,2.5,68,0,70);look(c,server,0,12);settle(c);frames(c);capture(c,"ssr-translated");
|
||||
camera(c,server,.5,68,0,70);look(c,server,0,12);settle(c);
|
||||
// A hidden landscape must not leak through an opaque foreground wall.
|
||||
server.runOnServer(s->{for(int x=-12;x<=12;x++)for(int y=65;y<=78;y++)s.overworld().setBlockAndUpdate(new BlockPos(x,y,18),Blocks.CONCRETE.white().defaultBlockState());});
|
||||
settle(c);c.runOnClient(m->VanillaLight.setSsr(false));c.waitTicks(8);var coveredOff=capture(c,"ssr-covered-off");
|
||||
c.runOnClient(m->VanillaLight.setSsr(true));frames(c);var coveredOn=capture(c,"ssr-covered-on");
|
||||
double coveredSignal=colourSignal(coveredOff,coveredOn);
|
||||
server.runOnServer(s->{for(int x=-12;x<=12;x++)for(int y=65;y<=77-Math.abs(x)/2;y++)s.overworld().setBlockAndUpdate(new BlockPos(x,y,19),Blocks.CONCRETE.blue().defaultBlockState());});settle(c);
|
||||
c.runOnClient(m->VanillaLight.setSsr(false));c.waitTicks(8);var hiddenBlueOff=capture(c,"ssr-hidden-blue-off");
|
||||
c.runOnClient(m->VanillaLight.setSsr(true));frames(c);var hiddenBlueOn=capture(c,"ssr-hidden-blue-on");
|
||||
double hiddenBlueSignal=colourSignal(hiddenBlueOff,hiddenBlueOn);
|
||||
check(Math.abs(coveredSignal-hiddenBlueSignal)<1.5,"Hidden landscape colour cannot leak through opaque wall: "+coveredSignal+" / "+hiddenBlueSignal);
|
||||
server.runOnServer(s->{for(int x=-12;x<=12;x++){
|
||||
for(int y=65;y<=78;y++)s.overworld().setBlockAndUpdate(new BlockPos(x,y,18),Blocks.AIR.defaultBlockState());
|
||||
for(int y=65;y<=77-Math.abs(x)/2;y++)s.overworld().setBlockAndUpdate(new BlockPos(x,y,19),Blocks.CONCRETE.red().defaultBlockState());
|
||||
}});settle(c);frames(c);
|
||||
benchmark(c,"pond");
|
||||
look(c,server,0,35);frames(c);benchmark(c,"water-fullscreen");look(c,server,0,12);frames(c);
|
||||
// The nearest glass layer must replace the water receiver, including alpha-zero glass texels.
|
||||
server.runOnServer(s->{for(int x=-7;x<=7;x++)for(int z=4;z<=14;z++)s.overworld().setBlockAndUpdate(new BlockPos(x,66,z),Blocks.GLASS.defaultBlockState());});
|
||||
settle(c);capture(c,"ssr-glass-over-water");
|
||||
c.runOnClient(m->VanillaLight.setSsr(false));c.waitTicks(8);var glassOff=capture(c,"ssr-glass-off");
|
||||
c.runOnClient(m->VanillaLight.setSsr(true));frames(c);var glassOn=capture(c,"ssr-glass-on");
|
||||
check(changed(glassOff,glassOn)>100,"Transparent glass reflects landscape");
|
||||
benchmark(c,"glass");
|
||||
server.runOnServer(s->{for(int x=-7;x<=7;x++)for(int z=4;z<=14;z++)s.overworld().setBlockAndUpdate(new BlockPos(x,66,z),Blocks.IRON_BLOCK.defaultBlockState());});
|
||||
settle(c);c.runOnClient(m->VanillaLight.setSsr(false));c.waitTicks(8);var metalOff=capture(c,"ssr-metal-off");
|
||||
c.runOnClient(m->VanillaLight.setSsr(true));frames(c);var metalOn=capture(c,"ssr-metal-on");
|
||||
check(changed(metalOff,metalOn)>100,"Solid PBR reflects landscape");
|
||||
look(c,server,.00002f,12.00002f);frames(c);var tiny=capture(c,"ssr-metal-tiny-motion");
|
||||
check(stableMetal(metalOn,tiny),"Tiny camera movement keeps material texels stable");look(c,server,0,12);frames(c);
|
||||
server.runOnServer(s->{
|
||||
command(s,"fill -7 67 9 7 70 9 iron_block");
|
||||
for(int x=-6;x<=6;x+=2)command(s,"setblock "+x+" 67 8 polished_andesite_stairs[facing=south]");
|
||||
});settle(c);frames(c);var intersections=capture(c,"ssr146-intersections");
|
||||
look(c,server,.00002f,12.00002f);frames(c);var shifted=capture(c,"ssr146-intersections-tiny-motion");
|
||||
check(stableIntersections(intersections,shifted),"Block intersections remain stable during subpixel motion");
|
||||
look(c,server,0,12);frames(c);
|
||||
c.runOnClient(m->{VanillaLight.setPixelShadows(true);VanillaLight.setSsgi(true);VanillaLight.setColoredLights(true);VanillaLight.setBloom(true);VanillaLight.setSunshafts(true);});
|
||||
frames(c);capture(c,"ssr-combined");
|
||||
server.runOnServer(s->command(s,"time set 18000"));c.waitTicks(15);frames(c);capture(c,"ssr-moon");
|
||||
server.runOnServer(s->command(s,"time set 6000"));c.waitTicks(15);frames(c);
|
||||
c.runOnClient(m->{VanillaLight.setPixelShadows(false);VanillaLight.setSsgi(false);VanillaLight.setColoredLights(false);VanillaLight.setBloom(false);VanillaLight.setSunshafts(false);});
|
||||
server.runOnServer(s->{for(int x=-7;x<=7;x++)for(int z=4;z<=14;z++)s.overworld().setBlockAndUpdate(new BlockPos(x,66,z),Blocks.STONE.defaultBlockState());});
|
||||
settle(c);c.runOnClient(m->VanillaLight.setSsr(false));c.waitTicks(8);capture(c,"ssr-rough-stone-off");
|
||||
c.runOnClient(m->VanillaLight.setSsr(true));frames(c);capture(c,"ssr-rough-stone-on");
|
||||
// Native panes, sloped water, cascade sides and a waterlogged slab share the receiver pass.
|
||||
server.runOnServer(s->{
|
||||
for(int y=66;y<=69;y++)s.overworld().setBlockAndUpdate(new BlockPos(-8,y,8),Blocks.GLASS_PANE.defaultBlockState());
|
||||
s.overworld().setBlockAndUpdate(new BlockPos(8,69,8),Blocks.WATER.defaultBlockState());
|
||||
s.overworld().setBlockAndUpdate(new BlockPos(-8,65,10),Blocks.OAK_SLAB.defaultBlockState().setValue(net.minecraft.world.level.block.state.properties.BlockStateProperties.WATERLOGGED,true));
|
||||
});settle(c);frames(c);capture(c,"ssr-flow-pane-waterlogged");
|
||||
server.runOnServer(s->{command(s,"fill -3 66 6 3 69 6 glass");command(s,"fill -3 66 8 3 69 8 light_blue_stained_glass");
|
||||
command(s,"setblock -4 67 6 blue_stained_glass_pane");});
|
||||
settle(c);c.runOnClient(m->VanillaLight.setSsr(false));c.waitTicks(8);capture(c,"ssr-stacked-coloured-glass-off");
|
||||
c.runOnClient(m->VanillaLight.setSsr(true));frames(c);capture(c,"ssr-stacked-coloured-glass-on");
|
||||
camera(c,server,12,64,8,70);look(c,server,0,0);c.waitTicks(15);
|
||||
c.runOnClient(m->check(!PixelReflections.allocated(),"Immersion suspends and releases SSR"));
|
||||
camera(c,server,.5,68,0,70);look(c,server,0,12);settle(c);frames(c);
|
||||
var reload=c.computeOnClient(net.minecraft.client.Minecraft::reloadResourcePacks);c.waitFor(m->reload.isDone()&&m.gui.overlay()==null,1200);reload.join();frames(c);capture(c,"ssr-reloaded");
|
||||
int reloadedUpdates=c.computeOnClient(m->ProceduralPbr.waterUpdates());c.waitTicks(20);
|
||||
c.runOnClient(m->check(ProceduralPbr.waterUpdates()>reloadedUpdates,"Reloaded atlas supplies a live native water clock"));
|
||||
int w=c.computeOnClient(m->m.getWindow().getScreenWidth()),h=c.computeOnClient(m->m.getWindow().getScreenHeight());
|
||||
c.runOnClient(m->m.getWindow().setWindowed(961,541));c.waitTicks(15);frames(c);capture(c,"ssr-resized");
|
||||
c.runOnClient(m->m.getWindow().setWindowed(w,h));c.waitTicks(15);frames(c);
|
||||
c.runOnClient(m->VanillaLight.setPbr(false));c.waitTicks(8);c.runOnClient(m->check(!PixelReflections.allocated(),"PBR OFF releases reflection resources"));
|
||||
System.out.println("SSR_PASS water; transparent glass; solids; OFF/zero; motion; reload; resize");
|
||||
}
|
||||
private static boolean stableIntersections(Frame a,Frame b){
|
||||
// The surrounding animated water is not a stability oracle for static blocks.
|
||||
int changed=0,count=0;
|
||||
for(int y=a.height()*30/100;y<a.height()*80/100;y++)for(int x=a.width()/4;x<a.width()*3/4;x++){
|
||||
int i=y*a.width()+x,aa=a.pixels()[i],bb=b.pixels()[i];count++;
|
||||
if(Math.abs((aa>>16&255)-(bb>>16&255))+Math.abs((aa>>8&255)-(bb>>8&255))+Math.abs((aa&255)-(bb&255))>12)changed++;
|
||||
}
|
||||
System.out.println("SSR146_INTERSECTIONS changed="+changed+" samples="+count);
|
||||
return changed<count*.001;
|
||||
}
|
||||
private static boolean stableMetal(Frame a,Frame b){int changes=0,count=0;
|
||||
for(int y=a.height()*58/100;y<a.height()*68/100;y++)for(int x=a.width()*3/10;x<a.width()*7/10;x++){
|
||||
int i=y*a.width()+x,aa=a.pixels()[i],bb=b.pixels()[i];count++;
|
||||
if(Math.abs((aa>>16&255)-(bb>>16&255))+Math.abs((aa>>8&255)-(bb>>8&255))+Math.abs((aa&255)-(bb&255))>12)changes++;
|
||||
}return changes<count*.02;
|
||||
}
|
||||
private static double colourSignal(Frame a,Frame b){long total=0;int count=0;
|
||||
for(int y=a.height()/2;y<a.height();y++)for(int x=a.width()/4;x<a.width()*3/4;x++){
|
||||
int i=y*a.width()+x,aa=a.pixels()[i],bb=b.pixels()[i];total+=(bb>>16&255)-(aa>>16&255)-(bb&255)+(aa&255);count++;
|
||||
}return total/(double)count;
|
||||
}
|
||||
private static void benchmark(ClientGameTestContext c,String name){
|
||||
c.runOnClient(m->{m.options.enableVsync().set(false);m.options.framerateLimit().set(net.minecraft.client.Options.UNLIMITED_FRAMERATE_CUTOFF);
|
||||
m.options.inactivityFpsLimit().set(net.minecraft.client.InactivityFpsLimit.MINIMIZED);m.getWindow().setWindowed(1920,1080);});
|
||||
c.waitTicks(20);frames(c);
|
||||
c.runOnClient(m->check(m.gameRenderer.mainRenderTarget().width==1920&&m.gameRenderer.mainRenderTarget().height==1080,"Benchmark uses 1080p"));
|
||||
for(boolean on:new boolean[]{false,true}){
|
||||
c.runOnClient(m->VanillaLight.setSsr(on));c.waitTicks(30);
|
||||
c.runOnClient(m->fr.koka.sanctuary.gametest.ReflectionMetrics.start());c.waitTicks(80);
|
||||
var result=c.computeOnClient(m->fr.koka.sanctuary.gametest.ReflectionMetrics.stop());
|
||||
c.runOnClient(m->{check(ProceduralPbr.solidBytes()<=24L*1024*1024&&ProceduralPbr.transparentBytes()<=8L*1024*1024,"Mesh memory budgets");
|
||||
System.out.println("SSR_MEMORY "+name+" enabled="+on+" solid="+ProceduralPbr.solidBytes()+" transparent="+ProceduralPbr.transparentBytes()+" targets="+PixelReflections.targetBytes());});
|
||||
check(result[0]>=30,"Benchmark has enough frame samples");
|
||||
System.out.println("SSR_BENCH "+name+" enabled="+on+" count="+result[0]+" medianMs="+result[1]+" p95Ms="+result[2]+" gpuCount="+result[3]+" gpuMedianMs="+result[4]+" gpuP95Ms="+result[5]);
|
||||
}
|
||||
}
|
||||
private static void dumpLandscape(ClientGameTestContext c){
|
||||
var done=new java.util.concurrent.CompletableFuture<Void>();
|
||||
c.runOnClient(m->{
|
||||
var src=PixelReflections.landscape();
|
||||
var target=new com.mojang.blaze3d.pipeline.TextureTarget("SSR test source",src.getWidth(0),src.getHeight(0),com.mojang.renderpearl.api.GpuFormat.RGBA8_UNORM,null);
|
||||
com.mojang.blaze3d.systems.RenderSystem.getDevice().createCommandEncoder().copyTextureToTexture(src,target.getColorTexture(),0,0,0,0,0,target.width,target.height);
|
||||
net.minecraft.client.Screenshot.takeScreenshot(target,image->{try(image){image.writeToFile(java.nio.file.Path.of("screenshots/ssr-opaque-source.png"));done.complete(null);}catch(Exception e){done.completeExceptionally(e);}finally{target.destroyBuffers();}});
|
||||
});c.waitFor(m->done.isDone(),200);done.join();
|
||||
}
|
||||
private static long changed(Frame a,Frame b){long n=0;for(int y=a.height()/2;y<a.height();y++)for(int x=0;x<a.width();x++){
|
||||
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);}
|
||||
}
|
||||
+4
-3
@@ -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()
|
||||
@@ -151,9 +150,11 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
||||
c.runOnClient(m -> check(VanillaLight.active(), "Native Sanctuary shader is active without an external engine"));
|
||||
float morning = c.computeOnClient(m -> m.gameRenderer.gameRenderState().levelRenderState.skyRenderState.sunAngle);
|
||||
|
||||
if(Boolean.getBoolean("sanctuary.test.water144")){Water144ClientChecks.run(c,server);return;}
|
||||
if(coloredOnly){if(Boolean.getBoolean("sanctuary.test.directional141Only")){DirectionalLights141ClientChecks.run(c,server);return;}ColoredLights139ClientChecks.run(c,server);if(Boolean.getBoolean("sanctuary.test.colored140"))ColoredLights140ClientChecks.run(c,server);if(Boolean.getBoolean("sanctuary.test.directional141"))DirectionalLights141ClientChecks.run(c,server);return;}
|
||||
if(sunshaftOnly){Sunshafts137ClientChecks.run(c,server);return;}
|
||||
if(closeupOnly){SsgiCloseup135ClientChecks.run(c,server);Pbr134ClientChecks.run(c,server);return;}
|
||||
if(Boolean.getBoolean("sanctuary.test.ssr")){PixelReflectionsClientChecks.run(c,server);return;}
|
||||
if(pbrOnly){Pbr134ClientChecks.run(c,server);return;}
|
||||
if(ssgiOnly){Ssgi132ClientChecks.run(c,server);return;}
|
||||
if(bloomOnly){Bloom130ClientChecks.run(c,server);return;}
|
||||
@@ -452,7 +453,7 @@ 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()==80&&VanillaLight.ssgiIntensity()==35,"PBR default 80, SSGI strength retained");
|
||||
check(VanillaLight.pbrIntensity()==50&&VanillaLight.ssrIntensity()==20&&VanillaLight.ssgiIntensity()==35,"PBR 50 and SSR 20 defaults, 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();
|
||||
@@ -568,7 +569,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
||||
}
|
||||
|
||||
static Frame capture(ClientGameTestContext c, String name) {
|
||||
if (name != null) c.takeScreenshot("sanctuary-beta142-shader-" + name);
|
||||
if (name != null) c.takeScreenshot((Boolean.getBoolean("sanctuary.test.water144")?"sanctuary-beta144-water-":"sanctuary-beta142-shader-") + name);
|
||||
var result = new CompletableFuture<Frame>();
|
||||
c.runOnClient(m -> Screenshot.takeScreenshot(m.gameRenderer.mainRenderTarget(), image -> {
|
||||
try (image) {
|
||||
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
import static fr.koka.sanctuary.client.shader.PixelShadows122ClientChecks.*;
|
||||
import fr.koka.sanctuary.gametest.Water144Metrics;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.*;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
|
||||
final class Water144ClientChecks {
|
||||
static void run(ClientGameTestContext c,TestServerContext server){
|
||||
c.runOnClient(m->{
|
||||
VanillaLight.setEnabled(true);VanillaLight.setNaturalWater(true);VanillaLight.setWaterIntensity(100);VanillaLight.setWaterBlendRadius(0);
|
||||
VanillaLight.setPbr(false);VanillaLight.setSsgi(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);
|
||||
VanillaLight.setPixelShadows(false);VanillaLight.setColoredLights(false);VanillaLight.setSunshafts(false);VanillaLight.setLensFlare(false);VanillaLight.setEdgeHighlights(false);
|
||||
});
|
||||
server.runOnServer(s->{
|
||||
command(s,"fill 47 94 -1 96 95 33 quartz_block");
|
||||
command(s,"fill 47 96 -1 47 100 33 glass");command(s,"fill 96 96 -1 96 100 33 glass");
|
||||
command(s,"fill 47 96 -1 96 100 -1 glass");command(s,"fill 47 96 33 96 100 33 glass");
|
||||
command(s,"fill 64 95 0 79 95 32 sand");command(s,"fill 80 95 0 95 95 32 mud");
|
||||
command(s,"fill 48 96 0 95 99 32 water");
|
||||
command(s,"setblock 84 96 8 seagrass");
|
||||
for(int x=50;x<95;x+=4)command(s,"setblock "+x+" 96 20 gold_block");
|
||||
});
|
||||
camera(c,server,72,125,16,70);c.waitTicks(50);settle(c);
|
||||
if(Boolean.getBoolean("sanctuary.test.water144ExtrasOnly")){
|
||||
camera(c,server,88.5,96.2,8.5,70);look(c,server,0,0);c.waitTicks(20);capture(c,"pond-final-fog");
|
||||
var green=c.computeOnClient(m->new org.joml.Vector4f(m.gameRenderer.gameRenderState().levelRenderState.cameraRenderState.fogData.color));
|
||||
enable(c,false);var blue=c.computeOnClient(m->new org.joml.Vector4f(m.gameRenderer.gameRenderState().levelRenderState.cameraRenderState.fogData.color));capture(c,"native-final-fog");
|
||||
check(green.y>green.z&&green.y<Math.max(blue.x,Math.max(blue.y,blue.z))*.8,"Olive fog stays muted after native vision adaptation");
|
||||
enable(c,true);extras(c,server);return;
|
||||
}
|
||||
if(!Boolean.getBoolean("sanctuary.test.water144PerfOnly")){
|
||||
c.runOnClient(m->{
|
||||
check(NaturalWater.sample(m.level,56.5,99.1,8.5).equals(NaturalWater.CLEAR),"Constructed bed clear");
|
||||
check(NaturalWater.sample(m.level,72.5,99.1,8.5).equals(NaturalWater.LIGHT),"Sand lightly tinted");
|
||||
check(NaturalWater.sample(m.level,88.5,99.1,8.5).equals(NaturalWater.POND),"Mud pond");
|
||||
check(NaturalWater.sample(m.level,84.5,99.1,8.5).equals(NaturalWater.POND),"Seagrass is not a floor");
|
||||
check(NaturalWater.sample(m.level,88.5,110.1,8.5).equals(NaturalWater.CLEAR),"No bed in bounded search uses clear");
|
||||
var boundary=NaturalWater.sample(m.level,80,99.1,8.5);check(boundary.tint()>.1&&boundary.tint()<.85,"Section boundary mixes profiles");
|
||||
});
|
||||
c.runOnClient(m->check(!NaturalWater.sanctuaryWorld(),"Server identifies ordinary test dimension"));
|
||||
server.runOnServer(s->net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking.send(s.getPlayerList().getPlayers().getFirst(),new fr.koka.sanctuary.water.WaterWorldSettings.Context(true)));
|
||||
c.waitTicks(5);c.runOnClient(m->{check(NaturalWater.sanctuaryWorld()&&VanillaLight.naturalWater(),"Sanctuary context defaults ON");VanillaLight.setNaturalWater(false);});
|
||||
server.runOnServer(s->net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking.send(s.getPlayerList().getPlayers().getFirst(),new fr.koka.sanctuary.water.WaterWorldSettings.Context(false)));
|
||||
c.waitTicks(5);c.runOnClient(m->check(VanillaLight.naturalWater(),"Ordinary explicit ON is independent of Sanctuary OFF"));
|
||||
server.runOnServer(s->net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking.send(s.getPlayerList().getPlayers().getFirst(),new fr.koka.sanctuary.water.WaterWorldSettings.Context(true)));
|
||||
c.waitTicks(5);c.runOnClient(m->{check(!VanillaLight.naturalWater(),"Sanctuary explicit OFF survives context round trip");VanillaLight.setNaturalWater(true);});
|
||||
server.runOnServer(s->net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking.send(s.getPlayerList().getPlayers().getFirst(),new fr.koka.sanctuary.water.WaterWorldSettings.Context(false)));
|
||||
c.waitTicks(5);
|
||||
for(int radius:new int[]{2,4,8}){
|
||||
c.runOnClient(m->VanillaLight.setWaterBlendRadius(radius));settle(c);
|
||||
c.runOnClient(m->{
|
||||
var left=NaturalWater.sample(m.level,79.5,99.1,8.5);
|
||||
var right=NaturalWater.sample(m.level,80.5,99.1,8.5);
|
||||
check(left.tint()>.1&&right.tint()<.85&&right.tint()>left.tint(),"Wider transition smooths both sides of section seam");
|
||||
var west=new NaturalWater.Region(m.level,3,5,-1).corner(m.level,new BlockPos(79,99,8),1,0);
|
||||
var east=new NaturalWater.Region(m.level,4,5,-1).corner(m.level,new BlockPos(80,99,8),0,0);
|
||||
var expected=NaturalWater.sample(m.level,80,99,8);
|
||||
check(Math.abs(west.tint()-east.tint())<.0002&&Math.abs(west.alpha()-east.alpha())<.0002,"Adjacent compiled sections share continuous vertex tint and alpha");
|
||||
check(Math.abs(west.tint()-expected.tint())<.0002&&Math.abs(west.alpha()-expected.alpha())<.0002,"Compiled surface blend matches immersion sampling");
|
||||
});
|
||||
capture(c,"blend146-"+(radius*2+1));
|
||||
}
|
||||
c.runOnClient(m->VanillaLight.setWaterBlendRadius(0));settle(c);
|
||||
enable(c,false);var off=capture(c,"surface-off");enable(c,true);var on=capture(c,"surface-on");
|
||||
check(difference(off,on)>100000,"Visible surface colour and opacity change");
|
||||
c.runOnClient(m->VanillaLight.setWaterIntensity(0));settle(c);var zero=capture(c,"surface-zero");
|
||||
check(difference(off,zero)<difference(off,on)/4,"Zero restores native water (animated texture tolerance)");
|
||||
c.runOnClient(m->VanillaLight.setWaterIntensity(100));settle(c);
|
||||
camera(c,server,88.5,103,4.5,70);look(c,server,0,28);capture(c,"pond-closeup");
|
||||
camera(c,server,72,125,16,70);
|
||||
server.runOnServer(s->command(s,"fill 80 95 0 95 95 32 quartz_block"));c.waitTicks(30);settle(c);var changed=capture(c,"bed-changed");
|
||||
check(difference(changed,on)>100000,"Changing bed four blocks below surface refreshes water");
|
||||
server.runOnServer(s->command(s,"fill 80 95 0 95 95 32 mud"));c.waitTicks(25);settle(c);
|
||||
server.runOnServer(s->command(s,"fill 80 103 0 95 103 12 stone"));c.waitTicks(25);settle(c);
|
||||
c.runOnClient(m->check(NaturalWater.sample(m.level,88.5,99.1,8.5).equals(NaturalWater.POND),"Roof does not replace floor rule"));capture(c,"covered-pond");
|
||||
server.runOnServer(s->command(s,"fill 80 103 0 95 103 12 air"));c.waitTicks(20);
|
||||
for(int x:new int[]{56,72,88}){
|
||||
camera(c,server,x+.5,96.2,8.5,70);look(c,server,0,0);c.waitTicks(20);capture(c,"immersed-"+x);
|
||||
c.runOnClient(m->{check(m.gameRenderer.mainCamera().getFluidInCamera()==net.minecraft.world.level.material.FogType.WATER,"Camera immersed");
|
||||
float expected=x==56?1:x==72?.7f:.4f;check(Math.abs(NaturalWater.fogProfile().fog()-expected)<.001,"Immersion uses corresponding profile");});
|
||||
}
|
||||
var pondColor=c.computeOnClient(m->new org.joml.Vector4f(m.gameRenderer.gameRenderState().levelRenderState.cameraRenderState.fogData.color));
|
||||
check(pondColor.y>pondColor.z&&pondColor.y<.8f,"Olive fog avoids native neon normalization");
|
||||
float pondFog=c.computeOnClient(m->m.gameRenderer.gameRenderState().levelRenderState.cameraRenderState.fogData.environmentalEnd);
|
||||
enable(c,false);float nativeFog=c.computeOnClient(m->m.gameRenderer.gameRenderState().levelRenderState.cameraRenderState.fogData.environmentalEnd);
|
||||
check(Math.abs(pondFog/nativeFog-.4)<.01,"Rendered underwater fog distance matches pond profile");capture(c,"immersed-native");enable(c,true);
|
||||
camera(c,server,72,125,16,70);
|
||||
server.runOnServer(s->command(s,"fillbiome 80 88 0 95 104 32 minecraft:ocean"));c.waitTicks(35);settle(c);
|
||||
c.runOnClient(m->check(NaturalWater.sample(m.level,88.5,99.1,8.5).equals(NaturalWater.CLEAR),"Ocean overrides muddy bed"));capture(c,"ocean-over-mud");
|
||||
server.runOnServer(s->command(s,"fillbiome 80 88 0 95 104 32 minecraft:plains"));c.waitTicks(30);
|
||||
var reload=c.computeOnClient(net.minecraft.client.Minecraft::reloadResourcePacks);c.waitFor(m->reload.isDone()&&m.gui.overlay()==null,1200);reload.join();settle(c);capture(c,"resources-reloaded");
|
||||
resourceOverride(c);
|
||||
camera(c,server,400,120,16,70);c.waitTicks(30);camera(c,server,72,125,16,70);settle(c);
|
||||
c.runOnClient(m->{check(NaturalWater.sample(m.level,88.5,99.1,8.5).equals(NaturalWater.POND),"Reloaded chunks keep correct profile");
|
||||
VanillaLight.setNaturalWater(false);VanillaLight.setWaterIntensity(37);forgetPreferences();check(!VanillaLight.naturalWater()&&VanillaLight.waterIntensity()==37,"Water preferences persist");
|
||||
VanillaLight.setNaturalWater(true);VanillaLight.setWaterIntensity(100);
|
||||
for(String locale:new String[]{"fr_fr","en_us"}){m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());
|
||||
for(String key:new String[]{"natural_water","natural_water_help","water_intensity","water_intensity_help","water_intensity_value"}){String full="sanctuary.shaders."+key;check(!Component.translatable(full,100).getString().equals(full),"FR/EN "+full);}}
|
||||
});settle(c);
|
||||
}
|
||||
// All effects enabled; fixed inherited test quality (shadows 32 blocks, PBR 50%) for A/B comparison.
|
||||
c.runOnClient(m->{VanillaLight.setPbr(true);VanillaLight.setSsgi(true);VanillaLight.setBloom(true);VanillaLight.setOreEmission(true);
|
||||
VanillaLight.setPixelShadows(true);VanillaLight.setColoredLights(true);VanillaLight.setSunshafts(true);VanillaLight.setLensFlare(true);VanillaLight.setEdgeHighlights(true);});
|
||||
c.waitTicks(80);capture(c,"combined");
|
||||
if(!Boolean.getBoolean("sanctuary.test.water144FunctionalOnly"))benchmark(c,server);
|
||||
extras(c,server);
|
||||
System.out.println("WATER144_PASS surface, immersion, beds, ocean, section seam, roof, plants, invalidation, reload, preferences, FR/EN and combined rendering");
|
||||
}
|
||||
private static void enable(ClientGameTestContext c,boolean value){c.runOnClient(m->VanillaLight.setNaturalWater(value));settle(c);}
|
||||
private static void settle(ClientGameTestContext c){c.waitTicks(8);c.waitFor(m->m.levelRenderer.hasRenderedAllSections(),1200);c.waitTicks(20);}
|
||||
private static long difference(Frame a,Frame b){long value=0;for(int i=0;i<a.pixels().length;i++)value+=channelDifference(a.pixels()[i],b.pixels()[i]);return value;}
|
||||
private static void extras(ClientGameTestContext c,TestServerContext server){
|
||||
server.runOnServer(s->{
|
||||
for(int x:new int[]{100,108}){
|
||||
command(s,"fill "+x+" 95 0 "+(x+4)+" 104 16 glass");
|
||||
command(s,"fill "+(x+1)+" 96 1 "+(x+3)+" 103 15 air");
|
||||
command(s,"fill "+(x+1)+" 95 1 "+(x+3)+" 95 15 mud");
|
||||
}
|
||||
command(s,"setblock 102 102 2 water");command(s,"setblock 110 102 2 lava");
|
||||
command(s,"setblock 56 99 8 oak_slab[waterlogged=true]");
|
||||
});
|
||||
c.waitTicks(80);camera(c,server,106,103,25,70);look(c,server,179,20);settle(c);
|
||||
c.runOnClient(m->{
|
||||
var flow=m.level.getFluidState(new BlockPos(102,101,2));check(flow.is(net.minecraft.tags.FluidTags.WATER)&&!flow.isSource(),"Real falling water is rendered");
|
||||
check(m.level.getFluidState(new BlockPos(110,101,2)).is(net.minecraft.tags.FluidTags.LAVA),"Lava cascade remains lava");
|
||||
check(m.level.getBlockState(new BlockPos(56,99,8)).getValue(net.minecraft.world.level.block.state.properties.BlockStateProperties.WATERLOGGED),"Waterlogged slab preserved");
|
||||
});
|
||||
enable(c,false);var off=capture(c,"cascades-off");enable(c,true);var on=capture(c,"cascades-on");
|
||||
check(difference(off,on)>1000,"Waterfall responds to water option");
|
||||
int lava=0,stable=0;for(int i=0;i<off.pixels().length;i++){int a=off.pixels()[i];int r=a>>16&255,g=a>>8&255,b=a&255;
|
||||
if(r>160&&r>g*1.3&&b<90){lava++;if(channelDifference(a,on.pixels()[i])<70)stable++;}}
|
||||
check(lava>100&&stable>lava*.8,"Lava colours remain native (animation tolerance)");
|
||||
camera(c,server,56.5,102,8.5,70);capture(c,"waterlogged-slab");
|
||||
System.out.println("WATER144_EXTRAS_PASS falling water, unchanged lava and waterlogged slab");
|
||||
}
|
||||
private static void resourceOverride(ClientGameTestContext c){
|
||||
var directory=c.computeOnClient(m->m.getResourcePackDirectory().resolve("water144-fixture"));
|
||||
try{
|
||||
Files.createDirectories(directory.resolve("assets/sanctuary"));
|
||||
var mod=net.fabricmc.loader.api.FabricLoader.getInstance().getModContainer("sanctuary").orElseThrow();
|
||||
Files.copy(mod.findPath("resourcepacks/template/pack.mcmeta").orElseThrow(),directory.resolve("pack.mcmeta"),StandardCopyOption.REPLACE_EXISTING);
|
||||
Files.writeString(directory.resolve("assets/sanctuary/water_materials.json"),"{\"light\":[],\"pond\":[\"minecraft:quartz_block\"]}");
|
||||
}catch(Exception e){throw new AssertionError(e);}
|
||||
c.runOnClient(m->{var repo=m.getResourcePackRepository();repo.reload();check(repo.addPack("file/water144-fixture"),"Custom materials pack discovered");});
|
||||
var future=c.computeOnClient(net.minecraft.client.Minecraft::reloadResourcePacks);c.waitFor(m->future.isDone()&&m.gui.overlay()==null,1200);future.join();settle(c);
|
||||
c.runOnClient(m->{check(NaturalWater.sample(m.level,56.5,99.1,8.5).equals(NaturalWater.POND),"Resource pack replaces material family");
|
||||
check(NaturalWater.sample(m.level,88.5,99.1,8.5).equals(NaturalWater.CLEAR),"Replacement removes bundled assignment");});
|
||||
capture(c,"custom-materials");
|
||||
c.runOnClient(m->m.getResourcePackRepository().removePack("file/water144-fixture"));
|
||||
var restore=c.computeOnClient(net.minecraft.client.Minecraft::reloadResourcePacks);c.waitFor(m->restore.isDone()&&m.gui.overlay()==null,1200);restore.join();settle(c);
|
||||
c.runOnClient(m->check(NaturalWater.sample(m.level,88.5,99.1,8.5).equals(NaturalWater.POND),"Disabling custom pack restores defaults"));
|
||||
}
|
||||
private static void benchmark(ClientGameTestContext c,TestServerContext s){
|
||||
int oldLimit=c.computeOnClient(m->m.options.framerateLimit().get());
|
||||
var oldInactivity=c.computeOnClient(m->m.options.inactivityFpsLimit().get());
|
||||
c.runOnClient(m->{m.options.framerateLimit().set(net.minecraft.client.Options.UNLIMITED_FRAMERATE_CUTOFF);m.options.inactivityFpsLimit().set(net.minecraft.client.InactivityFpsLimit.MINIMIZED);});
|
||||
var results=new LinkedHashMap<String,Object>();
|
||||
try {
|
||||
for(String scene:new String[]{"mixed","large","moving"}){
|
||||
if(scene.equals("large")){s.runOnServer(server->{command(server,"fill 31 94 -17 112 100 -17 glass");command(server,"fill 31 94 48 112 100 48 glass");
|
||||
command(server,"fill 31 94 -17 31 100 48 glass");command(server,"fill 112 94 -17 112 100 48 glass");
|
||||
command(server,"fill 32 93 -16 111 93 47 quartz_block");command(server,"fill 32 94 -16 111 94 47 sand");command(server,"fill 32 95 -16 111 99 47 water");});c.waitTicks(50);}
|
||||
camera(c,s,72,125,16,70);settle(c);
|
||||
for(boolean enabled:new boolean[]{false,true,true,false,true,false,false,true}){
|
||||
camera(c,s,72,125,16,70);enable(c,enabled);c.waitTicks(80);c.runOnClient(m->Water144Metrics.start());
|
||||
if(scene.equals("moving")){
|
||||
for(int i=0;i<200;i++){final int step=i;c.runOnClient(m->{m.player.setPos(48+step*.6,125,16);m.player.setDeltaMovement(net.minecraft.world.phys.Vec3.ZERO);});c.waitTicks(1);}
|
||||
}else c.waitTicks(200);
|
||||
var values=c.computeOnClient(m->Water144Metrics.stop());String key=scene+"-"+enabled+"-"+results.size();results.put(key,values);
|
||||
System.out.println("WATER144_PERF "+key+" "+Arrays.toString(values));
|
||||
}
|
||||
}
|
||||
try{Files.writeString(Path.of("water144-performance.json"),new com.google.gson.GsonBuilder().setPrettyPrinting().create().toJson(results));}catch(Exception e){throw new AssertionError(e);}
|
||||
}finally{c.runOnClient(m->{m.options.framerateLimit().set(oldLimit);m.options.inactivityFpsLimit().set(oldInactivity);});}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
import java.util.ArrayList;
|
||||
public final class ReflectionMetrics {
|
||||
private static final ArrayList<Long> samples=new ArrayList<>(),gpu=new ArrayList<>();
|
||||
public static volatile boolean recording;
|
||||
public static synchronized void add(long time){if(recording)samples.add(time);}
|
||||
public static synchronized void gpu(long ns){if(recording)gpu.add(ns);}
|
||||
public static synchronized void start(){samples.clear();gpu.clear();recording=true;}
|
||||
public static synchronized double[] stop(){recording=false;var a=samples.stream().mapToLong(Long::longValue).sorted().toArray();
|
||||
if(a.length==0)throw new AssertionError("No frame timing samples");
|
||||
var g=gpu.stream().mapToLong(Long::longValue).sorted().toArray();
|
||||
return new double[]{a.length,a[a.length/2]/1e6,a[Math.min(a.length-1,(int)(a.length*.95))]/1e6,g.length,g.length==0?-1:g[g.length/2]/1e6,g.length==0?-1:g[Math.min(g.length-1,(int)(g.length*.95))]/1e6};}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
import java.util.*;
|
||||
public final class Water144Metrics {
|
||||
public static volatile boolean recording;
|
||||
private static final List<Long> frames=new ArrayList<>(),sections=new ArrayList<>();
|
||||
public static synchronized void frame(long ns){if(recording)frames.add(ns);}
|
||||
public static synchronized void section(long ns){if(recording)sections.add(ns);}
|
||||
public static synchronized void start(){frames.clear();sections.clear();recording=true;}
|
||||
public static synchronized double[] stop(){recording=false;return new double[]{frames.size(),quantile(frames,.5),quantile(frames,.95),sections.size(),quantile(sections,.5),quantile(sections,.95)};}
|
||||
private static double quantile(List<Long> values,double q){if(values.isEmpty())return 0;var sorted=values.stream().mapToLong(Long::longValue).sorted().toArray();return sorted[Math.min(sorted.length-1,(int)(sorted.length*q))]/1e6;}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package fr.koka.sanctuary.gametest.mixin;
|
||||
import fr.koka.sanctuary.gametest.ReflectionMetrics;
|
||||
import net.minecraft.client.Minecraft;
|
||||
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.callback.CallbackInfo;
|
||||
@Mixin(Minecraft.class)
|
||||
abstract class ReflectionFrameProbeMixin {
|
||||
@Unique private long sanctuary$reflectionStart;
|
||||
@Inject(method="renderFrame",at=@At("HEAD")) private void begin(boolean tick,CallbackInfo ci){if(ReflectionMetrics.recording)sanctuary$reflectionStart=System.nanoTime();}
|
||||
@Inject(method="renderFrame",at=@At("RETURN")) private void end(boolean tick,CallbackInfo ci){if(ReflectionMetrics.recording)ReflectionMetrics.add(System.nanoTime()-sanctuary$reflectionStart);}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package fr.koka.sanctuary.gametest.mixin;
|
||||
import com.mojang.blaze3d.systems.TimerQuery;
|
||||
import fr.koka.sanctuary.client.shader.PixelReflections;
|
||||
import fr.koka.sanctuary.gametest.ReflectionMetrics;
|
||||
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.callback.CallbackInfo;
|
||||
@Mixin(value=PixelReflections.class,remap=false)
|
||||
abstract class ReflectionGpuProbeMixin {
|
||||
@Unique private static TimerQuery sanctuary$query;
|
||||
@Unique private static boolean sanctuary$started;
|
||||
@Inject(method="render",at=@At("HEAD")) private static void begin(CallbackInfo ci){
|
||||
sanctuary$started=false;
|
||||
if(!ReflectionMetrics.recording){if(sanctuary$query!=null){sanctuary$query.close();sanctuary$query=null;}return;}
|
||||
if(sanctuary$query==null)sanctuary$query=new TimerQuery();
|
||||
if(sanctuary$query.getStatus()==TimerQuery.Status.NOT_RECORDING){
|
||||
long previous=sanctuary$query.get();if(previous>0)ReflectionMetrics.gpu(previous);
|
||||
sanctuary$query.beginProfile();sanctuary$started=true;
|
||||
}
|
||||
}
|
||||
@Inject(method="render",at=@At("RETURN")) private static void end(CallbackInfo ci){if(sanctuary$started)sanctuary$query.endProfile();}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package fr.koka.sanctuary.gametest.mixin;
|
||||
import fr.koka.sanctuary.gametest.Water144Metrics;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import org.spongepowered.asm.mixin.*;
|
||||
import org.spongepowered.asm.mixin.injection.*;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
@Mixin(Minecraft.class)
|
||||
abstract class Water144FrameProbeMixin {
|
||||
@Unique private long sanctuary$start;
|
||||
@Inject(method="renderFrame",at=@At("HEAD")) private void start(boolean tick,CallbackInfo ci){if(Water144Metrics.recording)sanctuary$start=System.nanoTime();}
|
||||
@Inject(method="renderFrame",at=@At("RETURN")) private void end(boolean tick,CallbackInfo ci){if(Water144Metrics.recording)Water144Metrics.frame(System.nanoTime()-sanctuary$start);}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package fr.koka.sanctuary.gametest.mixin;
|
||||
import fr.koka.sanctuary.gametest.Water144Metrics;
|
||||
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
|
||||
import net.minecraft.client.renderer.chunk.*;
|
||||
import net.minecraft.client.renderer.SectionBufferBuilderPack;
|
||||
import net.minecraft.core.SectionPos;
|
||||
import com.mojang.blaze3d.vertex.VertexSorting;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
@Mixin(SectionCompiler.class)
|
||||
abstract class Water144SectionProbeMixin {
|
||||
@WrapMethod(method="compile")
|
||||
private SectionCompiler.Results compile(SectionPos pos,RenderSectionRegion region,VertexSorting sorting,SectionBufferBuilderPack buffers,Operation<SectionCompiler.Results> original){
|
||||
boolean record=Water144Metrics.recording;long start=record?System.nanoTime():0;
|
||||
try{return original.call(pos,region,sorting,buffers);}finally{if(record)Water144Metrics.section(System.nanoTime()-start);}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,8 @@
|
||||
"defaultRequire": 1
|
||||
},
|
||||
"client": [
|
||||
"ReflectionFrameProbeMixin",
|
||||
"ReflectionGpuProbeMixin",
|
||||
"Colored139LightmapProbeMixin",
|
||||
"Fourneau094ParticleProbeMixin",
|
||||
"Tooltip093FontProbeMixin",
|
||||
@@ -19,6 +21,8 @@
|
||||
"Atlas026TextureProbeMixin",
|
||||
"Inventory013MerchantAccess",
|
||||
"Dynamic030TerrainProbeMixin",
|
||||
"Star040RenderProbeMixin"
|
||||
"Star040RenderProbeMixin",
|
||||
"Water144FrameProbeMixin",
|
||||
"Water144SectionProbeMixin"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@ public final class SanctuaryMod implements ModInitializer {
|
||||
fr.koka.sanctuary.notifications.DiscoveryNotifications.register();
|
||||
fr.koka.sanctuary.realtime.RealtimeService.register();
|
||||
fr.koka.sanctuary.weather.WeatherService.register();
|
||||
fr.koka.sanctuary.water.WaterWorldSettings.register();
|
||||
fr.koka.sanctuary.plans.PlanService.register();
|
||||
fr.koka.sanctuary.plans.Statuary.register();
|
||||
fr.koka.sanctuary.food.FoodSync.register();
|
||||
|
||||
@@ -12,20 +12,13 @@ import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.chat.FontDescription;
|
||||
import net.minecraft.network.chat.Style;
|
||||
import net.minecraft.util.FormattedCharSequence;
|
||||
import net.minecraft.ChatFormatting;
|
||||
|
||||
/** Configuration-stage identity and starter slots. Closing cancels connection, never the saved offer. */
|
||||
public final class HelloWorldScreen extends Screen {
|
||||
private static final FontDescription GALACTIC = new FontDescription.Resource(Identifier.withDefaultNamespace("alt"));
|
||||
private static final Style GALACTIC_STYLE = Style.EMPTY.withFont(GALACTIC);
|
||||
private ProgressionPayloads.Hello offer;private final Runnable disconnect;
|
||||
private EditBox bio,familiarName;private int color=-1;private Button submit;private boolean waiting,identityLocked;
|
||||
private String draft="",starter="",nameDraft="",codeDraft="",codeStatus="",lastChecked="";
|
||||
private boolean accessRequired,accessPending,admitting;private EditBox codeBox;
|
||||
private List<StarterCompanions.Choice> starterChoices=List.of();
|
||||
private String draft="",starter="",nameDraft="";private List<StarterCompanions.Choice> starterChoices=List.of();
|
||||
private final List<Button> colors=new ArrayList<>(),starters=new ArrayList<>();
|
||||
private int panelX,panelY,panelWidth,bioLabelY,colorLabelY,starterLabelY,nameLabelY;private boolean compact;
|
||||
public HelloWorldScreen(ProgressionPayloads.Hello offer,Runnable disconnect){super(ProgressionService.label("hello_title"));this.offer=offer;this.disconnect=disconnect;}
|
||||
@@ -36,29 +29,18 @@ public final class HelloWorldScreen extends Screen {
|
||||
if(identityLocked){draft=welcome.bio();color=offer.colors().getFirst();}
|
||||
if(starterChoices.stream().noneMatch(c->c.mob().equals(starter)))starter="";
|
||||
}
|
||||
public void prepareAccess(boolean pending){accessRequired=true;accessPending=pending;if(pending)codeStatus="code_recognized";}
|
||||
public void requireAccess(boolean pending){boolean changed=!accessRequired||accessPending!=pending;prepareAccess(pending);if(changed&&minecraft!=null&&minecraft.gui.screen()==this){keepDrafts();rebuildWidgets();}}
|
||||
public void acceptCode(String code,String reason){if(admitting)return;if(!AccessCodes.normalize(code).equals(AccessCodes.normalize(codeDraft))){if(AccessCodes.validFormat(AccessCodes.normalize(codeDraft)))sendCheck(AccessCodes.normalize(codeDraft));return;}codeStatus=reason;waiting=false;updateSelection();}
|
||||
public void accept(StarterPayloads.Welcome welcome){keepDrafts();load(welcome);waiting=false;admitting=false;noteCodeError();if(!offer.colors().contains(color))color=-1;rebuildWidgets();}
|
||||
public void accept(ProgressionPayloads.Hello offer){keepDrafts();this.offer=offer;waiting=false;admitting=false;noteCodeError();if(!offer.colors().contains(color))color=-1;rebuildWidgets();}
|
||||
private void keepDrafts(){if(bio!=null)draft=bio.getValue();if(codeBox!=null)codeDraft=codeBox.getValue();if(familiarName!=null)nameDraft=familiarName.getValue();}
|
||||
private void noteCodeError(){if(offer.error().startsWith("code_"))codeStatus=offer.error();}
|
||||
public void accept(StarterPayloads.Welcome welcome){if(bio!=null)draft=bio.getValue();load(welcome);waiting=false;if(!offer.colors().contains(color))color=-1;rebuildWidgets();}
|
||||
public void accept(ProgressionPayloads.Hello offer){this.offer=offer;waiting=false;if(bio!=null)draft=bio.getValue();if(!offer.colors().contains(color))color=-1;rebuildWidgets();}
|
||||
@Override protected void init(){
|
||||
boolean naming=!starterChoices.isEmpty();int shift=accessRequired?36:0;compact=naming&&height<278+shift;
|
||||
panelWidth=Math.min(520,width-20);panelX=(width-panelWidth)/2;panelY=Math.max(0,(height-((naming&&!compact?270:238)+shift))/2);colors.clear();starters.clear();familiarName=null;codeBox=null;
|
||||
bioLabelY=panelY+(compact?37:44)+shift;colorLabelY=panelY+(compact?71:81)+shift;starterLabelY=panelY+(compact?145:177)+shift;nameLabelY=starterLabelY+36;
|
||||
boolean naming=!starterChoices.isEmpty();compact=naming&&height<278;
|
||||
panelWidth=Math.min(520,width-20);panelX=(width-panelWidth)/2;panelY=Math.max(0,(height-(naming&&!compact?270:238))/2);colors.clear();starters.clear();familiarName=null;
|
||||
bioLabelY=panelY+(compact?37:44);colorLabelY=panelY+(compact?71:81);starterLabelY=panelY+(compact?145:177);nameLabelY=starterLabelY+36;
|
||||
var skin=addRenderableWidget(new PlayerSkinWidget(64,145,minecraft.getEntityModels(),minecraft.getSkinManager().createLookup(minecraft.getGameProfile(),false)));skin.setPosition(panelX,panelY+35);
|
||||
int right=panelX+72,w=panelWidth-72;
|
||||
if(accessRequired){
|
||||
codeBox=addRenderableWidget(new EditBox(font,right,panelY+(compact?48:57),w,18,ProgressionService.label("code")));
|
||||
codeBox.setMaxLength(32);codeBox.setHint(Component.translatable("sanctuary.progression.code_hint").withStyle(style -> style.withFont(GALACTIC)));codeBox.setValue(codeDraft);
|
||||
codeBox.addFormatter((text,offset) -> FormattedCharSequence.forward(text, GALACTIC_STYLE));
|
||||
codeBox.setResponder(value->{codeDraft=value;if(accessPending){updateSelection();return;}String normalized=AccessCodes.normalize(value);if(!AccessCodes.validFormat(normalized)){codeStatus=normalized.isEmpty()?"":"code_malformed";lastChecked="";updateSelection();return;}if(!normalized.equals(lastChecked))sendCheck(normalized);else updateSelection();});
|
||||
}
|
||||
bio=addRenderableWidget(new EditBox(font,right,panelY+(compact?48:57)+shift,w,18,ProgressionService.label("bio")));bio.setMaxLength(256);bio.setHint(ProgressionService.label("bio_hint"));bio.setValue(draft);bio.setResponder(value->draft=value);
|
||||
bio=addRenderableWidget(new EditBox(font,right,panelY+(compact?48:57),w,18,ProgressionService.label("bio")));bio.setMaxLength(256);bio.setHint(ProgressionService.label("bio_hint"));bio.setValue(draft);bio.setResponder(value->draft=value);
|
||||
int cell=w/4;
|
||||
for(int i=0;i<offer.colors().size();i++){
|
||||
final int rgb=offer.colors().get(i);int rowHeight=compact?12:16;Button button=addRenderableWidget(new ColorButton(right+(i%4)*cell,panelY+(compact?82:94)+shift+(i/4)*rowHeight,cell-2,rowHeight,rgb,i+1,b->{color=rgb;updateSelection();}));
|
||||
final int rgb=offer.colors().get(i);int rowHeight=compact?12:16;Button button=addRenderableWidget(new ColorButton(right+(i%4)*cell,panelY+(compact?82:94)+(i/4)*rowHeight,cell-2,rowHeight,rgb,i+1,b->{color=rgb;updateSelection();}));
|
||||
button.setTooltip(Tooltip.create(InhabitantColorNames.label(rgb)));colors.add(button);
|
||||
}
|
||||
for(int i=0;i<starterChoices.size();i++)starters.add(addRenderableWidget(new StarterSlot(right+i*28,starterLabelY+11,starterChoices.get(i))));
|
||||
@@ -67,16 +49,10 @@ public final class HelloWorldScreen extends Screen {
|
||||
familiarName.setMaxLength(StarterCompanions.NAME_LIMIT);familiarName.setHint(Component.translatable("sanctuary.starter.name_hint"));familiarName.setValue(nameDraft);
|
||||
familiarName.setResponder(value->{nameDraft=value;updateSelection();});
|
||||
}
|
||||
int actionsY=panelY+(naming&&!compact?248:216)+shift;
|
||||
int actionsY=panelY+(naming&&!compact?248:216);
|
||||
addRenderableWidget(Button.builder(ProgressionService.label("cancel"),b->onClose()).bounds(panelX,actionsY,90,20).build());
|
||||
submit=addRenderableWidget(Button.builder(ProgressionService.label("enter"),b->{
|
||||
if(waiting||color<0||(!starterChoices.isEmpty()&&(starter.isEmpty()||!StarterCompanions.validName(nameDraft))))return;
|
||||
if(accessRequired){
|
||||
if(!(accessPending||"code_recognized".equals(codeStatus)))return;
|
||||
admitting=true;waiting=true;updateSelection();
|
||||
ClientConfigurationNetworking.send(new AccessPayloads.Admit(bio.getValue(),color,starterChoices.isEmpty()?"":starter,nameDraft,accessPending||codeBox==null?"":codeBox.getValue()));
|
||||
return;
|
||||
}
|
||||
if(!starterChoices.isEmpty()&&!nameDraft.isBlank()&&!ClientConfigurationNetworking.canSend(StarterPayloads.NamedCreate.TYPE)){
|
||||
offer=new ProgressionPayloads.Hello(offer.name(),offer.colors(),"starter_name_version_required");return;
|
||||
}
|
||||
@@ -86,12 +62,9 @@ public final class HelloWorldScreen extends Screen {
|
||||
else ClientConfigurationNetworking.send(new StarterPayloads.Create(bio.getValue(),color,starter));
|
||||
}).bounds(panelX+96,actionsY,panelWidth-96,20).build());updateSelection();
|
||||
}
|
||||
private void sendCheck(String normalized){lastChecked=normalized;codeStatus="code_checking";updateSelection();if(ClientConfigurationNetworking.canSend(AccessPayloads.Check.TYPE))ClientConfigurationNetworking.send(new AccessPayloads.Check(normalized));}
|
||||
private void updateSelection(){
|
||||
boolean codeOk=!accessRequired||accessPending||"code_recognized".equals(codeStatus);
|
||||
if(submit!=null)submit.active=!waiting&&color>=0&&(starterChoices.isEmpty()||(!starter.isEmpty()&&StarterCompanions.validName(nameDraft)))&&codeOk;
|
||||
if(submit!=null)submit.active=!waiting&&color>=0&&(starterChoices.isEmpty()||(!starter.isEmpty()&&StarterCompanions.validName(nameDraft)));
|
||||
if(familiarName!=null)familiarName.setEditable(!waiting);
|
||||
if(codeBox!=null)codeBox.setEditable(!waiting&&!accessPending);
|
||||
if(bio!=null)bio.setEditable(!waiting&&!identityLocked);colors.forEach(b->b.active=!waiting&&!identityLocked);starters.forEach(b->b.active=!waiting);
|
||||
}
|
||||
@Override public void extractBackground(GuiGraphicsExtractor g,int mx,int my,float a){
|
||||
@@ -100,19 +73,13 @@ public final class HelloWorldScreen extends Screen {
|
||||
@Override public void extractRenderState(GuiGraphicsExtractor g,int mx,int my,float a){
|
||||
super.extractRenderState(g,mx,my,a);g.centeredText(font,title,width/2,panelY+7,0xFFFFFFFF);
|
||||
g.text(font,Component.literal(offer.name()).withStyle(s->color<0?s:s.withColor(color)),panelX+72,panelY+(compact?24:29),0xFFFFFFFF);
|
||||
if(accessRequired){
|
||||
String codeKey=codeStatus.isEmpty()?"code":codeStatus;
|
||||
int codeColor="code_recognized".equals(codeStatus)?0xFF88CC88:codeStatus.isEmpty()?0xFFBBBBBB:0xFFFFFFAA;
|
||||
g.text(font,ProgressionService.label(codeKey),panelX+72,panelY+(compact?37:44),codeColor);
|
||||
}
|
||||
g.text(font,ProgressionService.label("bio"),panelX+72,bioLabelY,0xFFBBBBBB);g.text(font,ProgressionService.label("choose_color"),panelX+72,colorLabelY,0xFFBBBBBB);
|
||||
String shown=offer.error().startsWith("code_")?"":offer.error();
|
||||
String status=waiting?"joining":shown;
|
||||
String status=waiting?"joining":offer.error();
|
||||
if(!starterChoices.isEmpty()){
|
||||
g.text(font,status.isEmpty()?Component.translatable("sanctuary.starter.slots"):ProgressionService.label(status),panelX+72,starterLabelY,status.isEmpty()?0xFFBBBBBB:0xFFFFFFAA);
|
||||
g.text(font,Component.translatable("sanctuary.starter.name"),panelX+72,nameLabelY,0xFFBBBBBB);
|
||||
}
|
||||
else if(!status.isEmpty())g.centeredText(font,ProgressionService.label(status),width/2,panelY+204+(accessRequired?36:0),0xFFFFFFAA);
|
||||
else if(!status.isEmpty())g.centeredText(font,ProgressionService.label(status),width/2,panelY+204,0xFFFFFFAA);
|
||||
}
|
||||
private final class ColorButton extends Button.Plain {
|
||||
ColorButton(int x,int y,int w,int h,int rgb,int number,OnPress press){super(x,y,w,h,InhabitantColorNames.label(rgb).copy().withStyle(s->s.withColor(rgb)),press,ignored->ProgressionService.label("color_number",number).copy().append(": ").append(InhabitantColorNames.label(rgb)));setOverrideRenderHighlightedSprite(()->color==rgb||isHoveredOrFocused());}
|
||||
|
||||
@@ -11,7 +11,6 @@ import net.minecraft.client.gui.screens.Screen;
|
||||
|
||||
public final class ProgressionClient {
|
||||
public static ProgressRecord progress;
|
||||
private static boolean accessGate,accessPending;
|
||||
public static boolean operator;
|
||||
public static InhabitantStore.Identity identity;
|
||||
public static ProgressionConfig config=ProgressionConfig.DEFAULT;
|
||||
@@ -39,21 +38,14 @@ public final class ProgressionClient {
|
||||
CycleClient.register();
|
||||
EntityRendererRegistry.register(SanctuarySitting.SEAT,NoopRenderer::new);
|
||||
ClientConfigurationNetworking.registerGlobalReceiver(ProgressionPayloads.InventoryLayout.TYPE,(payload,context) -> {});
|
||||
ClientConfigurationNetworking.registerGlobalReceiver(AccessPayloads.Gate.TYPE,(payload,context)->context.client().execute(()->{
|
||||
accessGate=payload.required();accessPending=payload.pending();
|
||||
if(payload.required()&&context.client().gui.screen() instanceof HelloWorldScreen screen)screen.requireAccess(payload.pending());
|
||||
}));
|
||||
ClientConfigurationNetworking.registerGlobalReceiver(AccessPayloads.Status.TYPE,(payload,context)->context.client().execute(()->{
|
||||
if(context.client().gui.screen() instanceof HelloWorldScreen screen)screen.acceptCode(payload.code(),payload.reason());
|
||||
}));
|
||||
ClientConfigurationNetworking.registerGlobalReceiver(StarterPayloads.Welcome.TYPE,(payload,context)->context.client().execute(()->{
|
||||
if(context.client().gui.screen() instanceof HelloWorldScreen screen)screen.accept(payload);
|
||||
else showHello(context.client(),new HelloWorldScreen(payload,()->context.responseSender().disconnect(ProgressionService.label("cancelled"))));
|
||||
else context.client().gui.setScreen(new HelloWorldScreen(payload,()->context.responseSender().disconnect(ProgressionService.label("cancelled"))));
|
||||
}));
|
||||
ClientConfigurationNetworking.registerGlobalReceiver(ProgressionPayloads.Hello.TYPE,(payload,context) ->
|
||||
context.client().execute(() -> {
|
||||
if (context.client().gui.screen() instanceof HelloWorldScreen screen) screen.accept(payload);
|
||||
else showHello(context.client(),new HelloWorldScreen(payload,
|
||||
else context.client().gui.setScreen(new HelloWorldScreen(payload,
|
||||
() -> context.responseSender().disconnect(ProgressionService.label("cancelled"))));
|
||||
}));
|
||||
ClientPlayNetworking.registerGlobalReceiver(ProgressionPayloads.Snapshot.TYPE,(payload,context) ->
|
||||
@@ -99,11 +91,9 @@ public final class ProgressionClient {
|
||||
if (active() && ClientPlayNetworking.canSend(ProgressionPayloads.Action.TYPE))
|
||||
ClientPlayNetworking.send(new ProgressionPayloads.Action(target,progress.revision()));
|
||||
}
|
||||
private static void showHello(Minecraft client,HelloWorldScreen screen){if(accessGate)screen.prepareAccess(accessPending);client.gui.setScreen(screen);}
|
||||
private static void reset(Minecraft client) {
|
||||
if (client.player != null) ProgressEffects.clear(client.player);
|
||||
identity=null;progress=null;operator=false;config=ProgressionConfig.DEFAULT;names=MobNamesConfig.DEFAULT;
|
||||
accessGate=false;accessPending=false;
|
||||
wasSneaking=false;pendingSit=false;lastSneak=-100;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ public final class SanctuaryKnowledgeClient implements ClientModInitializer {
|
||||
|
||||
public static void registerClient() {
|
||||
SanctuaryResourcePack.register();
|
||||
fr.koka.sanctuary.client.shader.NaturalWater.register();
|
||||
VortexCloudRenderer.register();
|
||||
fr.koka.sanctuary.client.shader.PixelShadows.register();
|
||||
fr.koka.sanctuary.client.shader.EdgeHighlights.register();
|
||||
|
||||
@@ -42,8 +42,9 @@ public final class Bloom {
|
||||
var builder=RenderPipeline.builder().withLocation(id("pipeline/"+name)).withVertexShader("core/screenquad").withFragmentShader(id("core/"+name)).withBindGroupLayout(layout.build());
|
||||
if(scene)builder.withBindGroupLayout(SCENE);
|
||||
return RenderPipelines.register(builder.withPrimitiveTopology(PrimitiveTopology.TRIANGLES).withCull(false)
|
||||
.withColorTargetState(new ColorTargetState(Optional.ofNullable(blend),name.equals("bloom_blur")||name.equals("bloom_downsample")||name.equals("lens_flare_source")?GpuFormat.RGBA16_FLOAT:GpuFormat.RGBA8_UNORM,blend==null?ColorTargetState.WRITE_ALL:ColorTargetState.WRITE_COLOR)).withDepthStencilState(Optional.empty()).build());
|
||||
.withColorTargetState(new ColorTargetState(Optional.ofNullable(blend),name.equals("bloom_material_add")||name.equals("bloom_blur")||name.equals("bloom_downsample")||name.equals("lens_flare_source")?GpuFormat.RGBA16_FLOAT:GpuFormat.RGBA8_UNORM,blend==null?ColorTargetState.WRITE_ALL:ColorTargetState.WRITE_COLOR)).withDepthStencilState(Optional.empty()).build());
|
||||
}
|
||||
private static final RenderPipeline MATERIAL=screen("bloom_material_add",new BlendFunction(BlendFactor.ONE,BlendFactor.ONE,BlendFactor.ZERO,BlendFactor.ONE),false,false);
|
||||
private static final RenderPipeline SKY=screen("bloom_sky",null,true,false);
|
||||
private static final RenderPipeline DOWNSAMPLE=screen("bloom_downsample",null,false,false);
|
||||
private static final RenderPipeline BLUR=screen("bloom_blur",null,false,true);
|
||||
@@ -121,6 +122,7 @@ public final class Bloom {
|
||||
}
|
||||
if(halo){
|
||||
screen(DOWNSAMPLE,small.getColorTextureView(),emission.getColorTextureView(),false,null);
|
||||
if(MaterialBloom.contribution()!=null)screen(MATERIAL,small.getColorTextureView(),MaterialBloom.contribution(),false,null);
|
||||
screen(BLUR,ping.getColorTextureView(),small.getColorTextureView(),false,horizontal);
|
||||
screen(BLUR,small.getColorTextureView(),ping.getColorTextureView(),false,vertical);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import com.mojang.blaze3d.pipeline.TextureTarget;
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.renderpearl.api.GpuFormat;
|
||||
import com.mojang.renderpearl.api.pipeline.*;
|
||||
import com.mojang.renderpearl.api.textures.*;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import org.joml.Vector4f;
|
||||
import java.util.Optional;
|
||||
|
||||
/** Extract only the positive contribution of each material pass, before other effects. */
|
||||
final class MaterialBloom {
|
||||
private static final RenderPipeline EXTRACT=RenderPipelines.register(RenderPipeline.builder()
|
||||
.withLocation(Identifier.parse("sanctuary:pipeline/material_bloom"))
|
||||
.withVertexShader("core/screenquad").withFragmentShader(Identifier.parse("sanctuary:core/material_bloom"))
|
||||
.withBindGroupLayout(BindGroupLayout.builder().withUniform("Before",UniformType.COMBINED_IMAGE_SAMPLER)
|
||||
.withUniform("After",UniformType.COMBINED_IMAGE_SAMPLER).build())
|
||||
.withPrimitiveTopology(PrimitiveTopology.TRIANGLES).withCull(false)
|
||||
.withColorTargetState(new ColorTargetState(Optional.of(new BlendFunction(BlendFactor.ONE,BlendFactor.ONE,BlendFactor.ZERO,BlendFactor.ONE)),GpuFormat.RGBA16_FLOAT,ColorTargetState.WRITE_COLOR))
|
||||
.withDepthStencilState(Optional.empty()).build());
|
||||
private static TextureTarget contribution;
|
||||
private static boolean ready;
|
||||
private MaterialBloom(){}
|
||||
static void beginFrame(){ready=false;if(!VanillaLight.bloom()||VanillaLight.bloomIntensity()==0)release();}
|
||||
static void accumulate(GpuTextureView before){
|
||||
if(!VanillaLight.bloom()||VanillaLight.bloomIntensity()==0)return;
|
||||
var main=Minecraft.getInstance().gameRenderer.mainRenderTarget();
|
||||
int w=Math.max(1,main.width/8),h=Math.max(1,main.height/8);
|
||||
if(contribution==null||contribution.width!=w||contribution.height!=h){release();contribution=new TextureTarget("Sanctuary material glow",w,h,GpuFormat.RGBA16_FLOAT,null);}
|
||||
try(var pass=RenderSystem.getDevice().createCommandEncoder().createRenderPass(()->"Sanctuary material glow",contribution.getColorTextureView(),ready?Optional.empty():Optional.of(new Vector4f()))){
|
||||
pass.setPipeline(RenderSystem.getCompiledPipeline(EXTRACT));
|
||||
var nearest=RenderSystem.getSamplerCache().getClampToEdge(FilterMode.NEAREST);
|
||||
pass.setUniform("Before",before,nearest);pass.setUniform("After",main.getColorTextureView(),nearest);pass.draw(3,1,0,0);
|
||||
}
|
||||
ready=true;
|
||||
}
|
||||
static GpuTextureView contribution(){return ready?contribution.getColorTextureView():null;}
|
||||
static void release(){if(contribution!=null){contribution.destroyBuffers();contribution=null;}ready=false;}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import com.google.gson.*;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import java.util.*;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.resource.v1.ResourceLoader;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import net.minecraft.client.renderer.block.BlockAndTintGetter;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.packs.PackType;
|
||||
import net.minecraft.server.packs.resources.ResourceManagerReloadListener;
|
||||
import net.minecraft.tags.BiomeTags;
|
||||
import net.minecraft.tags.FluidTags;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
/** Client-only water materials; no world writes, per-frame terrain scan or additional render pass. */
|
||||
public final class NaturalWater {
|
||||
public static final int DEPTH=8;
|
||||
public record Profile(float tint,float alpha,float fog) {
|
||||
public Profile mix(Profile b,float t){if(this==b||t==0)return this;if(t==1)return b;return new Profile(tint+(b.tint-tint)*t,alpha+(b.alpha-alpha)*t,fog+(b.fog-fog)*t);}
|
||||
}
|
||||
public static final Profile CLEAR=new Profile(0,.55f,1),LIGHT=new Profile(.10f,.80f,.70f),POND=new Profile(.85f,1,.40f);
|
||||
private static final Profile[] PROFILES={CLEAR,LIGHT,POND};
|
||||
private static volatile Map<Block,Integer> materials=Map.of();
|
||||
private static volatile float strength;
|
||||
private static ClientLevel previousLevel;
|
||||
private static boolean sanctuaryWorld;
|
||||
private static volatile int blendRadius=4;
|
||||
public static boolean sanctuaryWorld(){return sanctuaryWorld;}
|
||||
private static Profile fogFrom=CLEAR,fogTarget=CLEAR;
|
||||
private static long fogChanged;
|
||||
private static boolean wasUnderwater;
|
||||
private NaturalWater(){}
|
||||
public static void register(){
|
||||
net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking.registerGlobalReceiver(
|
||||
fr.koka.sanctuary.water.WaterWorldSettings.Context.TYPE,(payload,context)->context.client().execute(()->sanctuaryWorld=payload.sanctuary()));
|
||||
net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents.DISCONNECT.register((listener,client)->sanctuaryWorld=false);
|
||||
net.fabricmc.fabric.api.client.networking.v1.ClientConfigurationConnectionEvents.INIT.register((listener,client)->sanctuaryWorld=false);
|
||||
ResourceLoader.get(PackType.CLIENT_RESOURCES).registerReloadListener(Identifier.parse("sanctuary:water_materials"),
|
||||
(ResourceManagerReloadListener) manager->{
|
||||
try(var reader=manager.getResourceOrThrow(Identifier.parse("sanctuary:water_materials.json")).openAsReader()){
|
||||
var root=JsonParser.parseReader(reader).getAsJsonObject();var next=new HashMap<Block,Integer>();
|
||||
for(var entry:Map.of("light",1,"pond",2).entrySet()) for(var id:root.getAsJsonArray(entry.getKey())){
|
||||
var key=Identifier.parse(id.getAsString());
|
||||
if(!BuiltInRegistries.BLOCK.containsKey(key))throw new IllegalArgumentException("Unknown water material "+key);
|
||||
if(next.put(BuiltInRegistries.BLOCK.getValue(key),entry.getValue())!=null)throw new IllegalArgumentException("Duplicate water material "+key);
|
||||
}
|
||||
materials=Map.copyOf(next);
|
||||
}catch(Exception e){SanctuaryMod.LOGGER.warn("Invalid water materials; using bundled defaults",e);materials=defaults();}
|
||||
});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(client->{
|
||||
float next=VanillaLight.active()&&VanillaLight.naturalWater()?VanillaLight.waterIntensity()/100f:0;
|
||||
if(previousLevel!=client.level||strength!=next||blendRadius!=VanillaLight.waterBlendRadius()){previousLevel=client.level;strength=next;blendRadius=VanillaLight.waterBlendRadius();wasUnderwater=false;
|
||||
if(client.level!=null)invalidateWaterSections(client);}
|
||||
if(client.level==null||next==0){wasUnderwater=false;return;}
|
||||
var camera=client.gameRenderer.mainCamera();
|
||||
if(camera.getFluidInCamera()!=net.minecraft.world.level.material.FogType.WATER){wasUnderwater=false;return;}
|
||||
var pos=camera.position();var target=sample(client.level,pos.x,pos.y,pos.z);
|
||||
if(!wasUnderwater){fogFrom=fogTarget=target;fogChanged=System.nanoTime();wasUnderwater=true;}
|
||||
else if(!target.equals(fogTarget)){fogFrom=fogProfile();fogTarget=target;fogChanged=System.nanoTime();}
|
||||
});
|
||||
}
|
||||
/** Preference changes only rebuild loaded water meshes, retaining unrelated shader caches. */
|
||||
private static void invalidateWaterSections(Minecraft client){
|
||||
var level=client.level;
|
||||
var center=BlockPos.containing(client.gameRenderer.mainCamera().position());
|
||||
int radius=client.options.getEffectiveRenderDistance()+1;
|
||||
for(int z=(center.getZ()>>4)-radius;z<=(center.getZ()>>4)+radius;z++)
|
||||
for(int x=(center.getX()>>4)-radius;x<=(center.getX()>>4)+radius;x++){
|
||||
var chunk=level.getChunkSource().getChunk(x,z,net.minecraft.world.level.chunk.status.ChunkStatus.FULL,false);
|
||||
if(chunk==null)continue;
|
||||
var sections=chunk.getSections();
|
||||
for(int i=0;i<sections.length;i++)if(!sections[i].hasOnlyAir()
|
||||
&§ions[i].getStates().maybeHas(state->state.getFluidState().is(FluidTags.WATER)))
|
||||
client.levelExtractor.setSectionDirty(x,level.getSectionYFromSectionIndex(i),z);
|
||||
}
|
||||
}
|
||||
private static Map<Block,Integer> defaults(){
|
||||
var result=new HashMap<Block,Integer>();
|
||||
for(String name:new String[]{"sand","red_sand","gravel","clay"})result.put(BuiltInRegistries.BLOCK.getValue(Identifier.withDefaultNamespace(name)),1);
|
||||
for(String name:new String[]{"dirt","coarse_dirt","rooted_dirt","grass_block","podzol","mycelium","mud","muddy_mangrove_roots"})result.put(BuiltInRegistries.BLOCK.getValue(Identifier.withDefaultNamespace(name)),2);
|
||||
return Map.copyOf(result);
|
||||
}
|
||||
public static float strength(){return strength;}
|
||||
public static Profile fogProfile(){return fogFrom.mix(fogTarget,Math.clamp((System.nanoTime()-fogChanged)/500_000_000f,0,1));}
|
||||
public static boolean ocean(ClientLevel level,BlockPos pos){return level.hasChunk(pos.getX()>>4,pos.getZ()>>4)&&level.getBiome(pos).is(BiomeTags.IS_OCEAN);}
|
||||
private static int classify(java.util.function.Function<BlockPos,BlockState> read,BlockPos pos,Map<Block,Integer> table){
|
||||
var cursor=pos.mutable();
|
||||
for(int i=1;i<=DEPTH;i++){
|
||||
cursor.set(pos.getX(),pos.getY()-i,pos.getZ());var state=read.apply(cursor);
|
||||
if(state==null||state.isAir())return 0;
|
||||
if(state.isSolid()||!state.getCollisionShape(net.minecraft.world.level.EmptyBlockGetter.INSTANCE,cursor).isEmpty()){
|
||||
// Plants have no collision; a waterlogged solid remains a basin floor.
|
||||
return table.getOrDefault(state.getBlock(),0);
|
||||
}
|
||||
if(!state.getFluidState().is(FluidTags.WATER))return 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
public static Profile sample(ClientLevel level,double x,double y,double z){
|
||||
var pos=BlockPos.containing(x,y,z);if(ocean(level,pos))return CLEAR;
|
||||
int bx=(int)Math.floor(x-.5),bz=(int)Math.floor(z-.5),by=pos.getY();
|
||||
float dx=(float)(x-.5-bx),dz=(float)(z-.5-bz);
|
||||
return live(level,bx,by,bz).mix(live(level,bx+1,by,bz),dx).mix(live(level,bx,by,bz+1).mix(live(level,bx+1,by,bz+1),dx),dz);
|
||||
}
|
||||
private static Profile live(ClientLevel l,int x,int y,int z){
|
||||
var p=new BlockPos(x,y,z);if(!l.hasChunk(x>>4,z>>4)||ocean(l,p))return CLEAR;
|
||||
float tint=0,alpha=0,fog=0;int count=0;
|
||||
for(int dz=-blendRadius;dz<=blendRadius;dz++)for(int dx=-blendRadius;dx<=blendRadius;dx++){
|
||||
var q=p.offset(dx,0,dz);
|
||||
Profile v=!l.hasChunk(q.getX()>>4,q.getZ()>>4)||ocean(l,q)?CLEAR:PROFILES[classify(l::getBlockState,q,materials)];
|
||||
tint+=v.tint;alpha+=v.alpha;fog+=v.fog;count++;
|
||||
}
|
||||
return new Profile(tint/count,alpha/count,fog/count);
|
||||
}
|
||||
/** Owned by a single native section compilation, with ocean data captured before worker submission. */
|
||||
public static final class Region {
|
||||
private static final int PAD=9,SIDE=34;
|
||||
private final int radius=blendRadius;
|
||||
private final float[][][] sums=new float[16][][];
|
||||
private final int ox,oy,oz;
|
||||
private final net.minecraft.world.level.biome.BiomeManager biomes;
|
||||
private byte[] oceanKinds;
|
||||
private byte[] profiles;
|
||||
private final Map<Block,Integer> table=materials;
|
||||
public final float intensity=strength;
|
||||
public Region(ClientLevel level,int minX,int minY,int minZ){
|
||||
ox=(minX+1)*16;oy=(minY+1)*16;oz=(minZ+1)*16;
|
||||
// Copy quart holders, then keep the native seeded biome zoom on the immutable copy.
|
||||
// Sampling only quart centres would misclassify the jagged edges of ocean biomes.
|
||||
var copied=new java.util.ArrayList<net.minecraft.core.Holder<net.minecraft.world.level.biome.Biome>>(600);
|
||||
int qx=(ox>>2)-3,qy=(oy>>2)-1,qz=(oz>>2)-3;
|
||||
for(int y=0;y<6;y++)for(int z=0;z<10;z++)for(int x=0;x<10;x++)
|
||||
copied.add(level.hasChunk((qx+x)>>2,(qz+z)>>2)?level.getBiomeManager().getNoiseBiomeAtQuart(qx+x,qy+y,qz+z):null);
|
||||
biomes=level.getBiomeManager().withDifferentSource((x,y,z)->copied.get(((y-qy)*10+z-qz)*10+x-qx));
|
||||
}
|
||||
public boolean ocean(int x,int y,int z){
|
||||
if(oceanKinds==null)oceanKinds=new byte[SIDE*16*SIDE];
|
||||
int key=((y-oy)*SIDE+z-oz+PAD)*SIDE+x-ox+PAD;
|
||||
int code=oceanKinds[key];if(code==0){var biome=biomes.getBiome(x,y,z);code=biome!=null&&biome.is(BiomeTags.IS_OCEAN)?2:1;oceanKinds[key]=(byte)code;}
|
||||
return code==2;
|
||||
}
|
||||
private Profile profile(BlockAndTintGetter region,int x,int y,int z){
|
||||
if(x<ox-PAD||x>=ox-PAD+SIDE||y<oy||y>oy+15||z<oz-PAD||z>=oz-PAD+SIDE)return CLEAR;
|
||||
if(ocean(x,y,z))return CLEAR;
|
||||
int key=((y-oy)*SIDE+z-oz+PAD)*SIDE+x-ox+PAD;
|
||||
if(profiles==null)profiles=new byte[SIDE*16*SIDE];
|
||||
int code=profiles[key];if(code==0){code=1+classify(region::getBlockState,new BlockPos(x,y,z),table);profiles[key]=(byte)code;}
|
||||
return PROFILES[code-1];
|
||||
}
|
||||
private Profile blended(BlockAndTintGetter region,int x,int y,int z){
|
||||
if(radius==0)return profile(region,x,y,z);
|
||||
int layer=y-oy,stride=SIDE+1;
|
||||
if(sums[layer]==null){
|
||||
var sum=new float[3][stride*stride];
|
||||
for(int iz=0;iz<SIDE;iz++)for(int ix=0;ix<SIDE;ix++){
|
||||
var p=profile(region,ox-PAD+ix,y,oz-PAD+iz);int at=(iz+1)*stride+ix+1;
|
||||
float[] values={p.tint,p.alpha,p.fog};
|
||||
for(int c=0;c<3;c++)sum[c][at]=values[c]+sum[c][at-1]+sum[c][at-stride]-sum[c][at-stride-1];
|
||||
}
|
||||
sums[layer]=sum;
|
||||
}
|
||||
int left=x-ox+PAD-radius,top=z-oz+PAD-radius,right=left+radius*2+1,bottom=top+radius*2+1;
|
||||
float[] values=new float[3];float area=(radius*2+1)*(radius*2+1);
|
||||
for(int c=0;c<3;c++){var a=sums[layer][c];values[c]=(a[bottom*stride+right]-a[top*stride+right]-a[bottom*stride+left]+a[top*stride+left])/area;}
|
||||
return new Profile(values[0],values[1],values[2]);
|
||||
}
|
||||
public Profile corner(BlockAndTintGetter region,BlockPos p,int dx,int dz){
|
||||
if(ocean(p.getX(),p.getY(),p.getZ()))return CLEAR;
|
||||
int x=p.getX()+dx-1,y=p.getY(),z=p.getZ()+dz-1;
|
||||
return blended(region,x,y,z).mix(blended(region,x+1,y,z),.5f).mix(blended(region,x,y,z+1).mix(blended(region,x+1,y,z+1),.5f),.5f);
|
||||
}
|
||||
}
|
||||
public interface RegionAccess { Region sanctuary$waterRegion(); }
|
||||
public static int color(int color,int base,Profile profile,float intensity){
|
||||
float tint=profile.tint*intensity;
|
||||
int r=color>>16&255,g=color>>8&255,b=color&255;
|
||||
// Restore directional face shading after tinting, including custom biome/resource-pack colours.
|
||||
float shade=(r+g+b)/(float)Math.max(1,(base>>16&255)+(base>>8&255)+(base&255));
|
||||
int rr=Math.clamp(Math.round(r*(1-tint)+120*shade*tint),0,255);
|
||||
int gg=Math.clamp(Math.round(g*(1-tint)+132*shade*tint),0,255);
|
||||
int bb=Math.clamp(Math.round(b*(1-tint)+72*shade*tint),0,255);
|
||||
int a=Math.round((color>>>24)*(1+(profile.alpha-1)*intensity));
|
||||
return a<<24|rr<<16|gg<<8|bb;
|
||||
}
|
||||
public static void dirty(BlockPos pos){
|
||||
if(strength==0)return;
|
||||
var renderer=Minecraft.getInstance().levelExtractor;
|
||||
int margin=blendRadius+1;
|
||||
renderer.setSectionRangeDirty((pos.getX()-margin)>>4,pos.getY()>>4,(pos.getZ()-margin)>>4,
|
||||
(pos.getX()+margin)>>4,(pos.getY()+DEPTH)>>4,(pos.getZ()+margin)>>4);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Map;
|
||||
import java.util.WeakHashMap;
|
||||
import net.minecraft.client.renderer.texture.SpriteContents;
|
||||
|
||||
/** References the actual atlas clock, so pause, reload and custom frame durations stay aligned. */
|
||||
public final class PbrAnimation {
|
||||
public interface Clock { int sanctuary$frame(); int sanctuary$subFrame(); }
|
||||
// AnimationState retains its owning sprite; both sides must be weak across atlas reloads.
|
||||
private static final Map<SpriteContents,WeakReference<Clock>> clocks=new WeakHashMap<>();
|
||||
private PbrAnimation(){}
|
||||
public static void register(SpriteContents sprite,SpriteContents.AnimationState state){
|
||||
if(state!=null)clocks.put(sprite,new WeakReference<>((Clock)state));
|
||||
}
|
||||
static Clock clock(SpriteContents sprite){var clock=clocks.get(sprite);return clock==null?null:clock.get();}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import com.mojang.blaze3d.vertex.VertexConsumer;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.block.FluidRenderer;
|
||||
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
/** Replays the native fluid tessellator, including slope, flow UVs and waterlogged faces. */
|
||||
final class PbrFluidMesh implements VertexConsumer {
|
||||
private final VertexConsumer target;
|
||||
private final PbrMaterials materials;
|
||||
private final TextureAtlasSprite[] sprites;
|
||||
private final float[][] vertices=new float[4][5];
|
||||
private final int sky;
|
||||
private int cursor,quads;
|
||||
private PbrFluidMesh(VertexConsumer target,PbrMaterials materials,TextureAtlasSprite[] sprites,int sky){
|
||||
this.target=target;this.materials=materials;this.sprites=sprites;this.sky=sky;
|
||||
}
|
||||
static int emit(VertexConsumer target,PbrMaterials materials,net.minecraft.client.multiplayer.ClientLevel level,BlockPos pos,BlockState state){
|
||||
var models=Minecraft.getInstance().getModelManager().getFluidStateModelSet();
|
||||
var model=models.get(state.getFluidState());
|
||||
var sink=new PbrFluidMesh(target,materials,new TextureAtlasSprite[]{model.stillMaterial().sprite(),model.flowingMaterial().sprite(),model.overlayMaterial()==null?null:model.overlayMaterial().sprite()},
|
||||
level.getBrightness(net.minecraft.world.level.LightLayer.SKY,pos.above()));
|
||||
new FluidRenderer(models).tesselate(level,pos,layer->sink,state,state.getFluidState());
|
||||
return sink.quads;
|
||||
}
|
||||
@Override public void addVertex(float x,float y,float z,int color,float u,float v,int overlay,int light,float nx,float ny,float nz){
|
||||
vertices[cursor++]=new float[]{x,y,z,u,v};
|
||||
if(cursor!=4)return;
|
||||
cursor=0;float cu=0,cv=0;for(var p:vertices){cu+=p[3]*.25f;cv+=p[4]*.25f;}
|
||||
TextureAtlasSprite sprite=null;
|
||||
for(var candidate:sprites)if(candidate!=null&&cu>=candidate.getU0()&&cu<=candidate.getU1()&&cv>=candidate.getV0()&&cv<=candidate.getV1()){sprite=candidate;break;}
|
||||
if(sprite==null)return;
|
||||
var slot=materials.slot(sprite,net.minecraft.world.level.block.Blocks.WATER.defaultBlockState());
|
||||
if(slot.empty())return;
|
||||
for(var p:vertices)target.addVertex(p[0],p[1],p[2]).setColor((Math.round(sky/15f*255)<<24)|(color&0xFFFFFF))
|
||||
.setUv(p[3],p[4]).setUv3(slot.u((p[3]-sprite.getU0())/(sprite.getU1()-sprite.getU0())),slot.v((p[4]-sprite.getV0())/(sprite.getV1()-sprite.getV0()))+4);
|
||||
quads++;
|
||||
}
|
||||
// FluidRenderer uses the packed overload above. Fail visibly if that native contract changes.
|
||||
public VertexConsumer addVertex(float x,float y,float z){throw new UnsupportedOperationException("Unpacked fluid vertex");}
|
||||
public VertexConsumer setColor(int r,int g,int b,int a){return this;}
|
||||
public VertexConsumer setColor(int c){return this;}
|
||||
public VertexConsumer setUv(float u,float v){return this;}
|
||||
public VertexConsumer setUv1(int u,int v){return this;}
|
||||
public VertexConsumer setUv2(int u,int v){return this;}
|
||||
public VertexConsumer setUv3(float u,float v){return this;}
|
||||
public VertexConsumer setNormal(float x,float y,float z){return this;}
|
||||
public VertexConsumer setLineWidth(float w){return this;}
|
||||
}
|
||||
@@ -17,15 +17,29 @@ final class PbrMaterials implements AutoCloseable {
|
||||
}
|
||||
private final Map<String,Slot> slots=new HashMap<>();
|
||||
private final Map<BlockState,Profile> profiles=new IdentityHashMap<>();
|
||||
private final List<WaterAnimation> animations=new ArrayList<>();
|
||||
private DynamicTexture atlas;private boolean dirty;private int analyses;
|
||||
boolean candidate(BlockState state){return !state.isAir()&&!state.liquid();}
|
||||
static int family(BlockState state){
|
||||
String name=BuiltInRegistries.BLOCK.getKey(state.getBlock()).getPath();
|
||||
return state.getFluidState().is(net.minecraft.tags.FluidTags.WATER)&&state.liquid()?2:
|
||||
(name.equals("glass")||name.endsWith("_glass")||name.equals("glass_pane")||name.endsWith("_glass_pane"))?1:0;
|
||||
}
|
||||
boolean candidate(BlockState state){return !state.isAir();}
|
||||
Profile profile(BlockState state){return profiles.computeIfAbsent(state,s->profile(BuiltInRegistries.BLOCK.getKey(s.getBlock()).getPath()));}
|
||||
static Profile profile(String name){
|
||||
if(name.equals("water"))return new Profile(.12f,0);
|
||||
if(name.contains("glass"))return new Profile(.16f,0);
|
||||
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){
|
||||
@@ -39,7 +53,13 @@ final class PbrMaterials implements AutoCloseable {
|
||||
int fw=Math.min(image.getWidth(),sprite.contents().width()),fh=Math.min(image.getHeight(),sprite.contents().height());
|
||||
int[] colors=new int[INNER*INNER];
|
||||
for(int py=0;py<INNER;py++)for(int px=0;px<INNER;px++)colors[py*INNER+px]=image.getPixel(px*fw/INNER,py*fh/INNER);
|
||||
int family=family(state);
|
||||
// Pigmented glass can have low alpha and still carry real texture relief.
|
||||
// Fully transparent glass texels keep the geometric-normal fallback.
|
||||
if(family==2||family==1)for(int j=0;j<colors.length;j++)
|
||||
if(family==2||(colors[j]>>>24)>0)colors[j]|=0xFF000000;
|
||||
int[] pixels=generate(colors,INNER,INNER,profile);analyses++;
|
||||
if(family(state)==2)animations.add(new WaterAnimation(sprite,image,profile,x,y));
|
||||
for(int py=0;py<TILE;py++)for(int px=0;px<TILE;px++)atlas.getPixels().setPixel(x+px,y+py,pixels[Math.clamp(py-2,0,INNER-1)*INNER+Math.clamp(px-2,0,INNER-1)]);
|
||||
}catch(java.io.IOException e){empty=true;fr.koka.sanctuary.SanctuaryMod.LOGGER.warn("Cannot analyse PBR texture {}",sprite.contents().name(),e);}
|
||||
var result=new Slot(x,y,empty);slots.put(key,result);dirty=true;return result;
|
||||
@@ -59,7 +79,52 @@ final class PbrMaterials implements AutoCloseable {
|
||||
}
|
||||
private static float height(int color,float fallback){return (color>>>24)<128?fallback:luma(color);}
|
||||
private static float luma(int c){return ((c>>16&255)*.2126f+(c>>8&255)*.7152f+(c&255)*.0722f)/255;}
|
||||
DynamicTexture atlas(){if(dirty){atlas.upload();dirty=false;}return atlas;}
|
||||
DynamicTexture atlas(){
|
||||
if(dirty){atlas.upload();dirty=false;for(var animation:animations)animation.lastFrame=-1;}
|
||||
if(atlas!=null)for(var animation:animations)animation.update(atlas);
|
||||
return atlas;
|
||||
}
|
||||
private static final class WaterAnimation {
|
||||
final TextureAtlasSprite sprite;final int x,y;final int[][] normals;final int[] frames,times;final boolean interpolate;
|
||||
final NativeImage tile=new NativeImage(TILE,TILE,false);int lastFrame=-1,lastSub=-1,updates;
|
||||
WaterAnimation(TextureAtlasSprite sprite,NativeImage image,Profile profile,int x,int y)throws java.io.IOException{
|
||||
this.sprite=sprite;this.x=x;this.y=y;
|
||||
int w=sprite.contents().width(),h=sprite.contents().height(),columns=image.getWidth()/w,count=columns*(image.getHeight()/h);
|
||||
// Bound custom resource-pack memory; unsupported huge animations keep their initial normal.
|
||||
boolean capped=count>1024;
|
||||
if(capped){fr.koka.sanctuary.SanctuaryMod.LOGGER.warn("PBR water animation {} exceeds 1024 frames; using a fixed normal",sprite.contents().name());count=1;}
|
||||
normals=new int[count][];
|
||||
for(int f=0;f<count;f++){
|
||||
int[] colors=new int[INNER*INNER];
|
||||
for(int py=0;py<INNER;py++)for(int px=0;px<INNER;px++)colors[py*INNER+px]=image.getPixel((f%columns)*w+px*w/INNER,(f/columns)*h+py*h/INNER)|0xFF000000;
|
||||
normals[f]=generate(colors,INNER,INNER,profile);
|
||||
}
|
||||
var metadata=net.minecraft.client.Minecraft.getInstance().getResourceManager().getResourceOrThrow(BloomMaterials.texture(sprite.contents().name()))
|
||||
.metadata().getSection(net.minecraft.client.resources.metadata.animation.AnimationMetadataSection.TYPE);
|
||||
var sequence=new ArrayList<Integer>();var durations=new ArrayList<Integer>();
|
||||
if(metadata.isPresent()&&!capped){
|
||||
var m=metadata.get();interpolate=m.interpolatedFrames();
|
||||
if(m.frames().isPresent())for(var frame:m.frames().get()){
|
||||
if(frame.index()>=0&&frame.index()<count&&frame.timeOr(m.defaultFrameTime())>0){sequence.add(frame.index());durations.add(frame.timeOr(m.defaultFrameTime()));}
|
||||
}else for(int f=0;f<count;f++){sequence.add(f);durations.add(m.defaultFrameTime());}
|
||||
}else interpolate=false;
|
||||
if(sequence.isEmpty()){sequence.add(0);durations.add(1);}
|
||||
frames=sequence.stream().mapToInt(Integer::intValue).toArray();times=durations.stream().mapToInt(Integer::intValue).toArray();
|
||||
}
|
||||
void update(DynamicTexture atlas){
|
||||
var clock=PbrAnimation.clock(sprite.contents());int f=clock==null?0:Math.floorMod(clock.sanctuary$frame(),frames.length),sub=clock==null||!interpolate?0:clock.sanctuary$subFrame();
|
||||
if(f==lastFrame&&sub==lastSub)return;lastFrame=f;lastSub=sub;updates++;
|
||||
float t=interpolate?Math.clamp(sub/(float)times[f],0,1):0;
|
||||
int[] a=normals[frames[f]],b=normals[frames[(f+1)%frames.length]];
|
||||
for(int py=0;py<TILE;py++)for(int px=0;px<TILE;px++){
|
||||
int i=Math.clamp(py-2,0,INNER-1)*INNER+Math.clamp(px-2,0,INNER-1);
|
||||
int r=Math.round((a[i]>>16&255)*(1-t)+(b[i]>>16&255)*t),g=Math.round((a[i]>>8&255)*(1-t)+(b[i]>>8&255)*t);
|
||||
tile.setPixel(px,py,(a[i]&0xFF0000FF)|(r<<16)|(g<<8));
|
||||
}
|
||||
com.mojang.blaze3d.systems.RenderSystem.getDevice().createCommandEncoder().writeToTexture(atlas.getTexture(),tile,0,0,x,y);
|
||||
}
|
||||
}
|
||||
int animationUpdates(){return animations.stream().mapToInt(a->a.updates).sum();}
|
||||
int count(){return slots.size();}int analyses(){return analyses;}
|
||||
public void close(){if(atlas!=null){atlas.close();atlas=null;}slots.clear();profiles.clear();dirty=false;analyses=0;}
|
||||
public void close(){if(atlas!=null){atlas.close();atlas=null;}for(var animation:animations)animation.tile.close();animations.clear();slots.clear();profiles.clear();dirty=false;analyses=0;}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,14 @@ final class PbrTerrain implements AutoCloseable {
|
||||
static final VertexFormat FORMAT=VertexFormat.builder(0).addAttribute("Position",GpuFormat.RGB32_FLOAT)
|
||||
.addAttribute("Color",GpuFormat.RGBA8_UNORM).addAttribute("UV0",GpuFormat.RG32_FLOAT)
|
||||
.addAttribute("UV3",GpuFormat.RG32_FLOAT).build();
|
||||
private static final long MAX_BYTES=24L*1024*1024;
|
||||
private final boolean transparent;
|
||||
private final long maxBytes;
|
||||
PbrTerrain(){this(false);}
|
||||
PbrTerrain(boolean transparent){this.transparent=transparent;maxBytes=(transparent?8L:24L)*1024*1024;}
|
||||
private boolean candidate(net.minecraft.world.level.block.state.BlockState state){
|
||||
return transparent ? PbrMaterials.family(state)==1||state.getFluidState().is(net.minecraft.tags.FluidTags.WATER)
|
||||
: materials.candidate(state)&&!state.liquid()&&PbrMaterials.family(state)==0;
|
||||
}
|
||||
private final Map<Long,Section> sections=new HashMap<>();
|
||||
final PbrMaterials materials=new PbrMaterials();
|
||||
private Build building;
|
||||
@@ -41,7 +48,7 @@ final class PbrTerrain implements AutoCloseable {
|
||||
long frame=mc.level.getGameTime();var active=new ArrayList<Section>();
|
||||
for(var render:mc.levelRenderer.visibleSections()){
|
||||
long key=render.getSectionNode();var pos=render.getRenderOrigin();
|
||||
if(pos.offset(8,8,8).distToCenterSqr(camera)>48*48)continue;
|
||||
if(pos.offset(8,8,8).distToCenterSqr(camera)>Math.pow(VanillaLight.materialDistance()+16,2))continue;
|
||||
var chunk=mc.level.getChunkSource().getChunk(pos.getX()>>4,pos.getZ()>>4,ChunkStatus.FULL,false);
|
||||
if(chunk==null)continue;
|
||||
var storage=chunk.getSection(mc.level.getSectionIndex(pos.getY()));
|
||||
@@ -61,7 +68,7 @@ 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)){releaseEntry(next);next.dirty=false;continue;}
|
||||
if(!next.storage.maybeHas(this::candidate)){releaseEntry(next);next.dirty=false;continue;}
|
||||
building=new Build(next);
|
||||
}
|
||||
if(!advance(building,deadline))break;
|
||||
@@ -73,7 +80,7 @@ final class PbrTerrain implements AutoCloseable {
|
||||
try(var mesh=done.out.buildOrThrow()){
|
||||
long size=mesh.vertexBuffer().remaining();
|
||||
long replaced=done.section.entry==null?0:done.section.entry.vertices.size();
|
||||
if(bytes-replaced+size<=MAX_BYTES){
|
||||
if(bytes-replaced+size<=maxBytes){
|
||||
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);
|
||||
replacement=new Entry(done.section.origin,buffer,count);
|
||||
@@ -94,7 +101,13 @@ final class PbrTerrain implements AutoCloseable {
|
||||
var parts=new ArrayList<BlockStateModelPart>();var random=RandomSource.create();
|
||||
while(build.cursor<4096){
|
||||
int i=build.cursor++,x=i&15,z=i>>4&15,y=i>>8;var state=build.section.storage.getBlockState(x,y,z);
|
||||
if(materials.candidate(state)){
|
||||
if(candidate(state)){
|
||||
if(transparent&&state.getFluidState().is(net.minecraft.tags.FluidTags.WATER)){
|
||||
int emitted=PbrFluidMesh.emit(build.out,materials,mc.level,build.section.origin.offset(x,y,z),state);
|
||||
build.quads+=emitted;
|
||||
if(build.quads>=16384){build.overflow=true;return true;}
|
||||
}
|
||||
if(state.liquid() || (transparent&&PbrMaterials.family(state)!=1)){if(System.nanoTime()>=deadline)return build.cursor==4096;continue;}
|
||||
var pos=build.section.origin.offset(x,y,z);
|
||||
parts.clear();random.setSeed(state.getSeed(pos));if(!state.liquid())models.get(state).collectParts(random,parts);
|
||||
var tintSources=mc.getBlockColors().getTintSources(state);
|
||||
@@ -116,7 +129,7 @@ final class PbrTerrain implements AutoCloseable {
|
||||
var p=quad.position(v);float u=UVPair.unpackU(quad.packedUV(v)),tv=UVPair.unpackV(quad.packedUV(v));
|
||||
float localU=(u-sprite.getU0())/(sprite.getU1()-sprite.getU0()),localV=(tv-sprite.getV0())/(sprite.getV1()-sprite.getV0());
|
||||
build.out.addVertex(x+p.x()+(float)offset.x,y+p.y()+(float)offset.y,z+p.z()+(float)offset.z)
|
||||
.setColor(color).setUv(u,tv).setUv3(slot.u(localU),slot.v(localV));
|
||||
.setColor(color).setUv(u,tv).setUv3(slot.u(localU),slot.v(localV)+2*PbrMaterials.family(state));
|
||||
}
|
||||
build.quads++;
|
||||
}
|
||||
@@ -133,6 +146,7 @@ final class PbrTerrain implements AutoCloseable {
|
||||
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);}
|
||||
long allocatedBytes(){return bytes;}
|
||||
int entryCount(){return (int)visible.stream().filter(s->s.entry!=null).count();}
|
||||
int completed(){return completed;}
|
||||
boolean building(){return building!=null;}
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import com.mojang.blaze3d.buffers.Std140Builder;
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.renderpearl.api.GpuFormat;
|
||||
import com.mojang.renderpearl.api.buffers.GpuBuffer;
|
||||
import com.mojang.renderpearl.api.commands.RenderPassDescriptor;
|
||||
import com.mojang.renderpearl.api.pipeline.*;
|
||||
import com.mojang.renderpearl.api.textures.*;
|
||||
import fr.koka.sanctuary.client.panorama.PanoramaCapture;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.BindGroupLayouts;
|
||||
import net.minecraft.client.renderer.MappableRingBuffer;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.client.renderer.texture.DynamicTexture;
|
||||
import net.minecraft.client.renderer.texture.TextureAtlas;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.level.material.FogType;
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Vector3f;
|
||||
import org.joml.Vector4f;
|
||||
import java.util.*;
|
||||
|
||||
/** Bounded single-bounce landscape reflection. Never samples the target being written. */
|
||||
public final class PixelReflections {
|
||||
private static final BindGroupLayout LAYOUT=BindGroupLayout.builder()
|
||||
.withUniform("ReflectionScene",UniformType.UNIFORM_BUFFER)
|
||||
.withUniform("OpaqueDepth",UniformType.COMBINED_IMAGE_SAMPLER)
|
||||
.withUniform("OpaqueColor",UniformType.COMBINED_IMAGE_SAMPLER)
|
||||
.withUniform("ReceiverDepth",UniformType.COMBINED_IMAGE_SAMPLER)
|
||||
.withUniform("SceneColor",UniformType.COMBINED_IMAGE_SAMPLER)
|
||||
.withUniform("BlockAtlas",UniformType.COMBINED_IMAGE_SAMPLER)
|
||||
.withUniform("MaterialAtlas",UniformType.COMBINED_IMAGE_SAMPLER).build();
|
||||
private static RenderPipeline pipeline(String name,boolean depth){
|
||||
var b=RenderPipeline.builder().withLocation(Identifier.parse("sanctuary:pipeline/"+name))
|
||||
.withVertexShader(Identifier.parse("sanctuary:core/pbr"))
|
||||
.withFragmentShader(Identifier.parse("sanctuary:core/"+name))
|
||||
.withBindGroupLayout(BindGroupLayouts.DYNAMIC_TRANSFORMS).withBindGroupLayout(LAYOUT)
|
||||
.withVertexBinding(0,PbrTerrain.FORMAT).withPrimitiveTopology(PrimitiveTopology.QUADS).withCull(true);
|
||||
if(depth)b.withDepthStencilState(new DepthStencilState(CompareOp.GREATER_THAN,true));
|
||||
else b.withColorTargetState(new ColorTargetState(Optional.empty(),GpuFormat.RGBA8_UNORM,ColorTargetState.WRITE_COLOR)).withDepthStencilState(new DepthStencilState(CompareOp.EQUAL,false));
|
||||
return RenderPipelines.register(b.build());
|
||||
}
|
||||
private static final RenderPipeline DEPTH=pipeline("pbr_receiver",true),REFLECT=pipeline("pixel_reflections",false);
|
||||
private static GpuTexture opaqueDepth,opaqueColor;
|
||||
private static GpuTextureView opaqueDepthView,opaqueColorView;
|
||||
private static int width,height;
|
||||
private static GpuTexture receiver;
|
||||
private static GpuTextureView receiverView;
|
||||
private static MappableRingBuffer uniform;
|
||||
private static Matrix4f vp=new Matrix4f();
|
||||
private static List<PbrTerrain.Entry> solid=List.of(),transparent=List.of();
|
||||
private static DynamicTexture solidAtlas,transparentAtlas;
|
||||
private static boolean captured,ready,applied;
|
||||
private static int frames;
|
||||
private PixelReflections(){}
|
||||
public static boolean eligible(){
|
||||
var mc=Minecraft.getInstance();
|
||||
return mc.level!=null&&VanillaLight.active()&&VanillaLight.pbr()&&VanillaLight.pbrIntensity()>0&&!PanoramaCapture.rendering()
|
||||
&&mc.gameRenderer.gameRenderState().levelRenderState.cameraRenderState.fogType==FogType.NONE
|
||||
&&mc.gameRenderer.gameRenderState().levelRenderState.skyRenderState.skybox==net.minecraft.world.level.dimension.DimensionType.Skybox.OVERWORLD;
|
||||
}
|
||||
public static void beginFrame(){MaterialBloom.beginFrame();captured=ready=applied=false;if(!eligible())release();}
|
||||
/** Called only with the opaque render pass closed, on both native transparency paths. */
|
||||
public static void captureOpaque(){
|
||||
if(!eligible())return;
|
||||
var mc=Minecraft.getInstance();var main=mc.gameRenderer.mainRenderTarget();
|
||||
var device=RenderSystem.getDevice();
|
||||
int usage=GpuTexture.USAGE_RENDER_ATTACHMENT|GpuTexture.USAGE_TEXTURE_BINDING|GpuTexture.USAGE_COPY_DST|GpuTexture.USAGE_COPY_SRC;
|
||||
if(opaqueDepth==null||width!=main.width||height!=main.height){
|
||||
release();width=main.width;height=main.height;
|
||||
opaqueDepth=device.createTexture(()->"Sanctuary opaque depth",usage,main.getDepthTexture().getFormat(),width,height,1,1);
|
||||
opaqueDepthView=device.createTextureView(opaqueDepth);
|
||||
receiver=device.createTexture(()->"Sanctuary nearest PBR surface",usage,main.getDepthTexture().getFormat(),width,height,1,1);
|
||||
receiverView=device.createTextureView(receiver);
|
||||
}
|
||||
boolean enabled=VanillaLight.ssr()&&VanillaLight.ssrIntensity()>0;
|
||||
if(enabled&&opaqueColor==null){
|
||||
opaqueColor=device.createTexture(()->"Sanctuary reflection landscape",usage,GpuFormat.RGBA8_UNORM,width,height,1,1);
|
||||
opaqueColorView=device.createTextureView(opaqueColor);
|
||||
}else if(!enabled&&opaqueColor!=null){opaqueColorView.close();opaqueColor.close();opaqueColorView=null;opaqueColor=null;}
|
||||
var encoder=device.createCommandEncoder();
|
||||
encoder.copyTextureToTexture(main.getDepthTexture(),opaqueDepth,0,0,0,0,0,width,height);
|
||||
if(enabled)encoder.copyTextureToTexture(main.getColorTexture(),opaqueColor,0,0,0,0,0,width,height);
|
||||
captured=true;
|
||||
}
|
||||
static void prepare(Matrix4f matrix,List<PbrTerrain.Entry> solids,DynamicTexture sa,List<PbrTerrain.Entry> liquids,DynamicTexture ta){
|
||||
if(!captured)return;
|
||||
vp.set(matrix);solid=solids;transparent=liquids;solidAtlas=sa;transparentAtlas=ta;
|
||||
if(uniform==null)uniform=new MappableRingBuffer(()->"Sanctuary reflection parameters",GpuBuffer.USAGE_UNIFORM|GpuBuffer.USAGE_MAP_WRITE,160);
|
||||
uniform.rotate();try(var map=uniform.currentBuffer().map(false,true)){
|
||||
Std140Builder.intoBuffer(map.data()).putMat4f(vp).putMat4f(new Matrix4f(vp).invert())
|
||||
.putVec4(RenderSystem.getDevice().getDeviceInfo().isZZeroToOne()?1:0,VanillaLight.ssrIntensity()/100f,VanillaLight.pbrIntensity()/100f,VanillaLight.ssrDistance())
|
||||
.putVec4(Math.max(128,VanillaLight.ssrDistance()),32,VanillaLight.materialDistance(),0);
|
||||
}
|
||||
int max=java.util.stream.Stream.concat(solid.stream(),transparent.stream()).mapToInt(PbrTerrain.Entry::indices).max().orElse(0);
|
||||
RenderSystem.getSequentialBuffer(PrimitiveTopology.QUADS).requestIndexCount(max);
|
||||
var encoder=RenderSystem.getDevice().createCommandEncoder();
|
||||
try(var pass=encoder.createRenderPass(RenderPassDescriptor.builder(()->"Sanctuary nearest material")
|
||||
.withDepthAttachment(receiverView,OptionalDouble.of(0)).build())){
|
||||
pass.setPipeline(RenderSystem.getCompiledPipeline(DEPTH));
|
||||
bind(pass,Minecraft.getInstance().gameRenderer.mainRenderTarget().getColorTextureView(),false);
|
||||
draw(pass,solid,solidAtlas);draw(pass,transparent,transparentAtlas);
|
||||
}
|
||||
ready=true;
|
||||
}
|
||||
static GpuTextureView depth(){return ready?receiverView:null;}
|
||||
private static void bind(com.mojang.renderpearl.api.commands.RenderPass pass,GpuTextureView scene,boolean reflection){
|
||||
var nearest=RenderSystem.getSamplerCache().getClampToEdge(FilterMode.NEAREST);
|
||||
pass.setUniform("ReflectionScene",uniform.currentBuffer());
|
||||
pass.setUniform("OpaqueDepth",opaqueDepthView,nearest);
|
||||
pass.setUniform("OpaqueColor",opaqueColorView!=null?opaqueColorView:scene,nearest);
|
||||
// Never bind the receiver as a sampled texture while it is attached for depth writes.
|
||||
pass.setUniform("ReceiverDepth",reflection?Minecraft.getInstance().gameRenderer.mainRenderTarget().getDepthTextureView():opaqueDepthView,nearest);
|
||||
pass.setUniform("SceneColor",scene,nearest);
|
||||
pass.setUniform("BlockAtlas",Minecraft.getInstance().getTextureManager().getTexture(TextureAtlas.LOCATION_BLOCKS).getTextureView(),nearest);
|
||||
}
|
||||
private static void draw(com.mojang.renderpearl.api.commands.RenderPass pass,List<PbrTerrain.Entry> entries,DynamicTexture atlas){
|
||||
if(atlas==null||entries.isEmpty())return;
|
||||
pass.setUniform("MaterialAtlas",atlas.getTextureView(),RenderSystem.getSamplerCache().getClampToEdge(FilterMode.NEAREST));
|
||||
var indices=RenderSystem.getSequentialBuffer(PrimitiveTopology.QUADS);
|
||||
var camera=Minecraft.getInstance().gameRenderer.gameRenderState().levelRenderState.cameraRenderState;
|
||||
for(var e:entries){
|
||||
pass.setIndexBuffer(indices.getBuffer(),indices.type());
|
||||
var p=e.origin();var offset=new Vector3f((float)(p.getX()-camera.pos.x),(float)(p.getY()-camera.pos.y),(float)(p.getZ()-camera.pos.z));
|
||||
pass.setUniform("DynamicTransforms",RenderSystem.getDynamicUniforms().writeTransform(new Matrix4f(vp).translate(offset),new Vector4f(1,1,1,1),offset,new Matrix4f()));
|
||||
pass.setVertexBuffer(0,e.vertices().slice());pass.drawIndexed(e.indices(),1,0,0,0);
|
||||
}
|
||||
}
|
||||
public static void render(){
|
||||
if(!ready||!VanillaLight.ssr()||VanillaLight.ssrIntensity()==0)return;
|
||||
var main=Minecraft.getInstance().gameRenderer.mainRenderTarget();
|
||||
var scene=ProceduralPbr.copyScene();
|
||||
try(var pass=RenderSystem.getDevice().createCommandEncoder().createRenderPass(()->"Sanctuary pixel reflections",main.getColorTextureView(),Optional.empty(),receiverView,OptionalDouble.empty())){
|
||||
pass.setPipeline(RenderSystem.getCompiledPipeline(REFLECT));bind(pass,scene,true);
|
||||
draw(pass,solid,solidAtlas);draw(pass,transparent,transparentAtlas);
|
||||
}
|
||||
MaterialBloom.accumulate(scene);
|
||||
applied=true;frames++;
|
||||
}
|
||||
public static void release(){
|
||||
if(opaqueColorView!=null){opaqueColorView.close();opaqueColorView=null;}if(opaqueColor!=null){opaqueColor.close();opaqueColor=null;}
|
||||
if(opaqueDepthView!=null){opaqueDepthView.close();opaqueDepthView=null;}if(opaqueDepth!=null){opaqueDepth.close();opaqueDepth=null;}if(receiverView!=null){receiverView.close();receiverView=null;}
|
||||
if(receiver!=null){receiver.close();receiver=null;}if(uniform!=null){uniform.close();uniform=null;}
|
||||
solid=transparent=List.of();solidAtlas=transparentAtlas=null;captured=ready=applied=false;
|
||||
}
|
||||
static long targetBytes(){return (long)width*height*((opaqueDepth!=null?4:0)+(receiver!=null?4:0)+(opaqueColor!=null?4:0));}
|
||||
static GpuTexture landscape(){return opaqueColor;}
|
||||
static boolean applied(){return applied;}static int frames(){return frames;}
|
||||
static boolean allocated(){return opaqueColor!=null;}
|
||||
}
|
||||
@@ -33,19 +33,20 @@ public final class ProceduralPbr {
|
||||
private static final RenderPipeline PIPELINE=RenderPipelines.register(RenderPipeline.builder()
|
||||
.withLocation(Identifier.parse("sanctuary:pipeline/pbr")).withVertexShader(Identifier.parse("sanctuary:core/pbr"))
|
||||
.withFragmentShader(Identifier.parse("sanctuary:core/pbr")).withBindGroupLayout(BindGroupLayouts.DYNAMIC_TRANSFORMS).withBindGroupLayout(SCENE)
|
||||
.withVertexBinding(0,PbrTerrain.FORMAT).withPrimitiveTopology(PrimitiveTopology.QUADS).withCull(false)
|
||||
.withVertexBinding(0,PbrTerrain.FORMAT).withPrimitiveTopology(PrimitiveTopology.QUADS).withCull(true)
|
||||
.withColorTargetState(new ColorTargetState(Optional.empty(),GpuFormat.RGBA8_UNORM,ColorTargetState.WRITE_COLOR))
|
||||
.withDepthStencilState(Optional.empty()).build());
|
||||
.withDepthStencilState(new DepthStencilState(CompareOp.EQUAL,false)).build());
|
||||
private static final PbrTerrain TERRAIN=new PbrTerrain();
|
||||
private static final PbrTerrain TRANSPARENT=new PbrTerrain(true);
|
||||
private static final Matrix4f projection=new Matrix4f();
|
||||
private static TextureTarget source;
|
||||
private static MappableRingBuffer scene;
|
||||
private static boolean applied;private static int frames;
|
||||
private 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){if(!fr.koka.sanctuary.client.DynamicLightClient.invalidatingSections())TERRAIN.dirtySection(x,y,z);}
|
||||
public static void dirtyBlock(BlockPos pos){TERRAIN.dirty(pos);TRANSPARENT.dirty(pos);}
|
||||
public static void dirtyChunk(int x,int z){TERRAIN.dirtyChunk(x,z);TRANSPARENT.dirtyChunk(x,z);}
|
||||
public static void dirtySection(int x,int y,int z){if(!fr.koka.sanctuary.client.DynamicLightClient.invalidatingSections()){TERRAIN.dirtySection(x,y,z);TRANSPARENT.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;}
|
||||
@@ -55,19 +56,26 @@ public final class ProceduralPbr {
|
||||
if(source==null||source.width!=main.width||source.height!=main.height){if(source!=null)source.destroyBuffers();source=new TextureTarget("Sanctuary PBR source",main.width,main.height,GpuFormat.RGBA8_UNORM,null);}
|
||||
// Padding supplies valid inactive PixelShadows and local-light uniform bindings.
|
||||
if(scene==null)scene=new MappableRingBuffer(()->"Sanctuary PBR scene",GpuBuffer.USAGE_UNIFORM|GpuBuffer.USAGE_MAP_WRITE,176+ColoredDynamicLights.LIMIT*32);
|
||||
var entries=TERRAIN.update();var atlas=TERRAIN.materials.atlas();
|
||||
var solidEntries=TERRAIN.update();var atlas=TERRAIN.materials.atlas();
|
||||
var transparentEntries=TRANSPARENT.update();var transparentAtlas=TRANSPARENT.materials.atlas();
|
||||
var entries=new ArrayList<PbrTerrain.Entry>(solidEntries);entries.addAll(transparentEntries);
|
||||
var vp=new Matrix4f(projection).mul(camera.viewRotationMatrix);var fog=camera.fogData;
|
||||
PixelReflections.prepare(vp,solidEntries,atlas,transparentEntries,transparentAtlas);
|
||||
if(PixelReflections.depth()==null)return;
|
||||
var sun=PixelShadows.sunDirection(sky.sunAngle);boolean lunar=sun.y<=.08f;
|
||||
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,32)
|
||||
.putVec4(fog.renderDistanceStart,fog.renderDistanceEnd,fog.environmentalStart,fog.environmentalEnd).putVec4(fog.color.w,ColoredLights.directionalReady()?1:0,0,0)
|
||||
.putVec4(0,0,0,0).putVec4(0,0,0,0).putVec4(0,0,0,0).putVec4(0,0,0,0);
|
||||
.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(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));
|
||||
@@ -76,22 +84,33 @@ public final class ProceduralPbr {
|
||||
var encoder=RenderSystem.getDevice().createCommandEncoder();
|
||||
encoder.copyTextureToTexture(main.getColorTexture(),source.getColorTexture(),0,0,0,0,0,main.width,main.height);
|
||||
var nearest=RenderSystem.getSamplerCache().getClampToEdge(FilterMode.NEAREST);
|
||||
if(atlas!=null&&!entries.isEmpty())try(var pass=encoder.createRenderPass(()->"Sanctuary PBR materials",main.getColorTextureView(),Optional.empty())){
|
||||
if((atlas!=null||transparentAtlas!=null)&&!entries.isEmpty())try(var pass=encoder.createRenderPass(()->"Sanctuary PBR materials",main.getColorTextureView(),Optional.empty(),PixelReflections.depth(),OptionalDouble.empty())){
|
||||
pass.setPipeline(RenderSystem.getCompiledPipeline(PIPELINE));pass.setUniform("PbrScene",scene.currentBuffer());
|
||||
pass.setUniform("PixelShadows",shadow?PixelShadows.materialUniforms():scene.currentBuffer());
|
||||
pass.setUniform("SunDepth",shadow?PixelShadows.materialDepth():main.getDepthTextureView(),nearest);
|
||||
pass.setUniform("WorldDepth",main.getDepthTextureView(),nearest);pass.setUniform("SceneColor",source.getColorTextureView(),nearest);
|
||||
pass.setUniform("BlockAtlas",mc.getTextureManager().getTexture(TextureAtlas.LOCATION_BLOCKS).getTextureView(),nearest);
|
||||
pass.setUniform("MaterialAtlas",atlas.getTextureView(),nearest);
|
||||
pass.setUniform("MaterialAtlas",(atlas!=null?atlas:transparentAtlas).getTextureView(),nearest);
|
||||
if(ColoredLights.directionalReady())ColoredLights.bindDirections(pass);
|
||||
else{pass.setUniform("ColoredLightScene",scene.currentBuffer());pass.setUniform("LightDirection",source.getColorTextureView(),nearest);pass.setUniform("PreviousDirection",source.getColorTextureView(),nearest);}
|
||||
pass.setIndexBuffer(indices.getBuffer(),indices.type());
|
||||
for(int i=0;i<entries.size();i++){var entry=entries.get(i);pass.setUniform("DynamicTransforms",transforms.get(i));pass.setVertexBuffer(0,entry.vertices().slice());pass.drawIndexed(entry.indices(),1,0,0,0);}
|
||||
for(int i=0;i<entries.size();i++){var entry=entries.get(i);
|
||||
pass.setUniform("MaterialAtlas",(i<solidEntries.size()?atlas:transparentAtlas).getTextureView(),nearest);pass.setUniform("DynamicTransforms",transforms.get(i));pass.setVertexBuffer(0,entry.vertices().slice());pass.drawIndexed(entry.indices(),1,0,0,0);}
|
||||
}
|
||||
MaterialBloom.accumulate(source.getColorTextureView());
|
||||
applied=true;frames++;
|
||||
}
|
||||
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 com.mojang.renderpearl.api.textures.GpuTextureView copyScene(){
|
||||
var main=Minecraft.getInstance().gameRenderer.mainRenderTarget();
|
||||
RenderSystem.getDevice().createCommandEncoder().copyTextureToTexture(main.getColorTexture(),source.getColorTexture(),0,0,0,0,0,main.width,main.height);
|
||||
return source.getColorTextureView();
|
||||
}
|
||||
public static void release(){MaterialBloom.release();TERRAIN.close();TRANSPARENT.close();PixelReflections.release();if(source!=null){source.destroyBuffers();source=null;}if(scene!=null){scene.close();scene=null;}applied=false;}
|
||||
static long solidBytes(){return TERRAIN.allocatedBytes();}
|
||||
static long transparentBytes(){return TRANSPARENT.allocatedBytes();}
|
||||
static int waterUpdates(){return TRANSPARENT.materials.animationUpdates();}
|
||||
static int waterAnalyses(){return TRANSPARENT.materials.analyses();}
|
||||
static boolean applied(){return applied;}static boolean ready(){return TERRAIN.settled()&&TRANSPARENT.settled();}
|
||||
static boolean allocated(){return source!=null;}static int renderedFrames(){return frames;}
|
||||
static int terrainEntries(){return TERRAIN.entryCount();}
|
||||
static int terrainBuilds(){return TERRAIN.completed();}
|
||||
|
||||
@@ -14,6 +14,12 @@ public final class ShaderOptionsScreen extends OptionsSubScreen {
|
||||
list.addBig(OptionInstance.createBoolean("sanctuary.shaders.vanilla_light",OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable(VanillaLight.externalEngine()?"sanctuary.shaders.external":"sanctuary.shaders.help")),
|
||||
VanillaLight.enabled(),VanillaLight::setEnabled));
|
||||
list.addBig(OptionInstance.createBoolean("sanctuary.shaders.natural_water",OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders.natural_water_help")),VanillaLight.naturalWater(),VanillaLight::setNaturalWater));
|
||||
slider("water_intensity",VanillaLight.waterIntensity(),VanillaLight::setWaterIntensity);
|
||||
list.addBig(new OptionInstance<Integer>("sanctuary.shaders.water_blend",OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders.water_blend_help")),(caption,value)->Component.translatable("sanctuary.shaders.water_blend_value",value*2+1,value*2+1),
|
||||
new OptionInstance.Enum<>(List.of(0,1,2,4,8),Codec.INT),VanillaLight.waterBlendRadius(),VanillaLight::setWaterBlendRadius));
|
||||
list.addBig(new OptionInstance<Integer>("sanctuary.shaders.saturation",OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders.saturation_help")),(caption,value)->Component.translatable("sanctuary.shaders.saturation_value",value),
|
||||
new OptionInstance.IntRange(0,200),VanillaLight.saturation(),VanillaLight::setSaturation));
|
||||
@@ -23,6 +29,15 @@ public final class ShaderOptionsScreen extends OptionsSubScreen {
|
||||
list.addBig(OptionInstance.createBoolean("sanctuary.shaders.pbr",OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders.pbr_help")),VanillaLight.pbr(),VanillaLight::setPbr));
|
||||
slider("pbr_intensity",VanillaLight.pbrIntensity(),VanillaLight::setPbrIntensity);
|
||||
list.addBig(new OptionInstance<Integer>("sanctuary.shaders.pbr_distance",OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders.pbr_distance_help")),(caption,value)->Component.translatable("sanctuary.shaders.pbr_distance_value",value),
|
||||
new OptionInstance.Enum<>(List.of(16,32,64,128,256),Codec.INT),VanillaLight.pbrDistance(),VanillaLight::setPbrDistance));
|
||||
list.addBig(OptionInstance.createBoolean("sanctuary.shaders.ssr",OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders.ssr_help")),VanillaLight.ssr(),VanillaLight::setSsr));
|
||||
slider("ssr_intensity",VanillaLight.ssrIntensity(),VanillaLight::setSsrIntensity);
|
||||
list.addBig(new OptionInstance<Integer>("sanctuary.shaders.ssr_distance",OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders.ssr_distance_help")),(caption,value)->Component.translatable("sanctuary.shaders.ssr_distance_value",value),
|
||||
new OptionInstance.Enum<>(List.of(16,32,64,128,256),Codec.INT),VanillaLight.ssrDistance(),VanillaLight::setSsrDistance));
|
||||
list.addBig(OptionInstance.createBoolean("sanctuary.shaders.colored_lights",OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders.colored_lights_help")),VanillaLight.coloredLights(),VanillaLight::setColoredLights));
|
||||
list.addBig(OptionInstance.createBoolean("sanctuary.shaders.warm_torches",OptionInstance.cachedConstantTooltip(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,11 @@ public final class VanillaLight {
|
||||
private VanillaLight() { }
|
||||
private static final class Preferences {
|
||||
boolean enabled=true;
|
||||
Boolean naturalWater; // Legacy OFF is an explicit opt-out in both world types.
|
||||
Boolean sanctuaryNaturalWater;
|
||||
Boolean ordinaryNaturalWater;
|
||||
int waterBlendRadius=4;
|
||||
int waterIntensity=100;
|
||||
int saturation=142;
|
||||
boolean pixelShadows=true;
|
||||
boolean bloom=true;
|
||||
@@ -24,7 +29,13 @@ public final class VanillaLight {
|
||||
boolean lensFlare=true;
|
||||
int lensFlareIntensity=30;
|
||||
boolean pbr=true;
|
||||
int pbrIntensity=80;
|
||||
int pbrIntensity=50;
|
||||
int pbrBalanceVersion;
|
||||
int pbrDistance=64;
|
||||
int ssrDistance=64;
|
||||
int reflectionDistanceVersion;
|
||||
boolean ssr=true;
|
||||
int ssrIntensity=20;
|
||||
boolean ssgi=true;
|
||||
boolean coloredLights=true;
|
||||
boolean warmTorches=false;
|
||||
@@ -47,8 +58,21 @@ public final class VanillaLight {
|
||||
try{if(Files.isRegularFile(path()))preferences=new GsonBuilder().create().fromJson(Files.readString(path()),Preferences.class);}
|
||||
catch(Exception e){SanctuaryMod.LOGGER.warn("Cannot read Sanctuary shader preference",e);}
|
||||
if(preferences==null)preferences=new Preferences();
|
||||
if(Boolean.FALSE.equals(preferences.naturalWater)){
|
||||
if(preferences.sanctuaryNaturalWater==null)preferences.sanctuaryNaturalWater=false;
|
||||
if(preferences.ordinaryNaturalWater==null)preferences.ordinaryNaturalWater=false;
|
||||
}
|
||||
preferences.naturalWater=null;
|
||||
preferences.sunshaftIntensity=Math.clamp(preferences.sunshaftIntensity,0,100);
|
||||
preferences.lensFlareIntensity=Math.clamp(preferences.lensFlareIntensity,0,100);
|
||||
preferences.ssrIntensity=Math.clamp(preferences.ssrIntensity,0,100);
|
||||
if(preferences.reflectionDistanceVersion<1&&preferences.pbrDistance==32)preferences.pbrDistance=64;
|
||||
preferences.reflectionDistanceVersion=1;
|
||||
preferences.pbrDistance=normalizePbrDistance(preferences.pbrDistance);
|
||||
preferences.ssrDistance=normalizePbrDistance(preferences.ssrDistance);
|
||||
// Migrate the previous 80% baseline once; later explicit 80% choices persist.
|
||||
if(preferences.pbrBalanceVersion<1&&preferences.pbrIntensity==80)preferences.pbrIntensity=50;
|
||||
preferences.pbrBalanceVersion=1;
|
||||
preferences.pbrIntensity=Math.clamp(preferences.pbrIntensity,0,100);
|
||||
preferences.coloredLightIntensity=Math.clamp(preferences.coloredLightIntensity,0,100);
|
||||
preferences.coloredLightDistance=normalizeColoredLightDistance(preferences.coloredLightDistance);
|
||||
@@ -58,6 +82,8 @@ public final class VanillaLight {
|
||||
preferences.oreIntensity=Math.clamp(preferences.oreIntensity,0,100);
|
||||
preferences.edgeIntensity=Math.clamp(preferences.edgeIntensity,0,100);
|
||||
preferences.edgeDistance=normalizeShadowDistance(preferences.edgeDistance);
|
||||
preferences.waterBlendRadius=normalizeWaterBlendRadius(preferences.waterBlendRadius);
|
||||
preferences.waterIntensity=Math.clamp(preferences.waterIntensity,0,100);
|
||||
preferences.saturation=Math.clamp(preferences.saturation,0,200);
|
||||
preferences.shadowPixels=normalizePrecision(preferences.shadowPixels);
|
||||
preferences.shadowDistance=normalizeShadowDistance(preferences.shadowDistance);
|
||||
@@ -66,6 +92,14 @@ public final class VanillaLight {
|
||||
}
|
||||
public static boolean externalEngine(){return EXTERNAL_ENGINE;}
|
||||
public static boolean active(){return ProgressionClient.active()&&!EXTERNAL_ENGINE&&enabled();}
|
||||
public static boolean naturalWater(){return naturalWater(NaturalWater.sanctuaryWorld());}
|
||||
static boolean naturalWater(boolean sanctuary){enabled();Boolean value=sanctuary?preferences.sanctuaryNaturalWater:preferences.ordinaryNaturalWater;return value!=null?value:sanctuary;}
|
||||
public static int waterBlendRadius(){enabled();return preferences.waterBlendRadius;}
|
||||
public static void setWaterBlendRadius(int value){enabled();preferences.waterBlendRadius=normalizeWaterBlendRadius(value);save();}
|
||||
static int normalizeWaterBlendRadius(int value){return value<1?0:value<2?1:value<3?2:value<6?4:8;}
|
||||
public static int waterIntensity(){enabled();return preferences.waterIntensity;}
|
||||
public static void setNaturalWater(boolean value){enabled();if(NaturalWater.sanctuaryWorld())preferences.sanctuaryNaturalWater=value;else preferences.ordinaryNaturalWater=value;save();}
|
||||
public static void setWaterIntensity(int value){enabled();preferences.waterIntensity=Math.clamp(value,0,100);save();}
|
||||
public static int saturation(){enabled();return preferences.saturation;}
|
||||
public static boolean edgeHighlights(){enabled();return preferences.edgeHighlights;}
|
||||
public static int edgeDistance(){enabled();return preferences.edgeDistance;}
|
||||
@@ -81,6 +115,16 @@ public final class VanillaLight {
|
||||
public static int lensFlareIntensity(){enabled();return preferences.lensFlareIntensity;}
|
||||
public static void setLensFlare(boolean value){enabled();preferences.lensFlare=value;save();}
|
||||
public static void setLensFlareIntensity(int value){enabled();preferences.lensFlareIntensity=Math.clamp(value,0,100);save();}
|
||||
public static boolean ssr(){enabled();return preferences.ssr;}
|
||||
public static int ssrIntensity(){enabled();return preferences.ssrIntensity;}
|
||||
public static void setSsr(boolean value){enabled();preferences.ssr=value;save();}
|
||||
public static void setSsrIntensity(int value){enabled();preferences.ssrIntensity=Math.clamp(value,0,100);save();}
|
||||
public static int ssrDistance(){enabled();return preferences.ssrDistance;}
|
||||
public static void setSsrDistance(int value){enabled();preferences.ssrDistance=normalizePbrDistance(value);save();}
|
||||
static int materialDistance(){return Math.max(pbrDistance(),ssr()&&ssrIntensity()>0?ssrDistance():0);}
|
||||
public static int pbrDistance(){enabled();return preferences.pbrDistance;}
|
||||
public static void setPbrDistance(int value){enabled();preferences.pbrDistance=normalizePbrDistance(value);save();}
|
||||
static int normalizePbrDistance(int value){return value<24?16:value<48?32:value<96?64:value<192?128:256;}
|
||||
public static boolean pbr(){enabled();return preferences.pbr;}
|
||||
public static int pbrIntensity(){enabled();return preferences.pbrIntensity;}
|
||||
public static void setPbr(boolean value){enabled();preferences.pbr=value;save();}
|
||||
|
||||
+22
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package fr.koka.sanctuary.mixin.client;
|
||||
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.*;
|
||||
import com.mojang.blaze3d.vertex.VertexConsumer;
|
||||
import fr.koka.sanctuary.client.shader.NaturalWater;
|
||||
import net.minecraft.client.color.block.BlockTintSource;
|
||||
import net.minecraft.client.renderer.block.*;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.tags.FluidTags;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.material.FluidState;
|
||||
import org.spongepowered.asm.mixin.*;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
/** Native renderer instances belong to one section compiler. Context is restored even on failure. */
|
||||
@Mixin(FluidRenderer.class)
|
||||
abstract class NaturalWaterFluidMixin {
|
||||
@Unique private NaturalWater.Profile[] sanctuary$corners;
|
||||
@Unique private NaturalWater.Region sanctuary$snapshot;
|
||||
@Unique private BlockAndTintGetter sanctuary$region;
|
||||
@Unique private BlockPos sanctuary$pos;
|
||||
@Unique private float sanctuary$strength;
|
||||
@Unique private int sanctuary$x,sanctuary$z,sanctuary$base;
|
||||
@WrapMethod(method="tesselate")
|
||||
private void sanctuary$water(BlockAndTintGetter region,BlockPos pos,FluidRenderer.Output output,BlockState state,FluidState fluid,Operation<Void> original){
|
||||
var previous=sanctuary$snapshot;
|
||||
try{
|
||||
sanctuary$corners=null;sanctuary$snapshot=null;sanctuary$base=-1;
|
||||
if(fluid.is(FluidTags.WATER)&®ion instanceof NaturalWater.RegionAccess access){
|
||||
var snapshot=access.sanctuary$waterRegion();
|
||||
if(snapshot!=null){
|
||||
sanctuary$strength=snapshot.intensity;sanctuary$x=pos.getX()&15;sanctuary$z=pos.getZ()&15;
|
||||
sanctuary$snapshot=snapshot;sanctuary$region=region;sanctuary$pos=pos;
|
||||
}
|
||||
}
|
||||
original.call(region,pos,output,state,fluid);
|
||||
}finally{sanctuary$snapshot=previous;sanctuary$corners=null;}
|
||||
}
|
||||
@WrapOperation(method="tesselate",at=@At(value="INVOKE",target="Lnet/minecraft/client/color/block/BlockTintSource;colorInWorld(Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/client/renderer/block/BlockAndTintGetter;Lnet/minecraft/core/BlockPos;)I"))
|
||||
private int sanctuary$base(BlockTintSource source,BlockState state,BlockAndTintGetter region,BlockPos pos,Operation<Integer> original){return sanctuary$base=original.call(source,state,region,pos);}
|
||||
@WrapOperation(method="vertex",at=@At(value="INVOKE",target="Lcom/mojang/blaze3d/vertex/VertexConsumer;addVertex(FFFIFFIIFFF)V"))
|
||||
private void sanctuary$vertex(VertexConsumer consumer,float x,float y,float z,int color,float u,float v,int overlay,int light,float nx,float ny,float nz,Operation<Void> original){
|
||||
if(sanctuary$snapshot!=null){
|
||||
if(sanctuary$corners==null){var s=sanctuary$snapshot;var r=sanctuary$region;var p=sanctuary$pos;
|
||||
sanctuary$corners=new NaturalWater.Profile[]{s.corner(r,p,0,0),s.corner(r,p,1,0),s.corner(r,p,0,1),s.corner(r,p,1,1)};}
|
||||
int corner=(x-sanctuary$x>.5f?1:0)+(z-sanctuary$z>.5f?2:0);color=NaturalWater.color(color,sanctuary$base,sanctuary$corners[corner],sanctuary$strength);}
|
||||
original.call(consumer,x,y,z,color,u,v,overlay,light,nx,ny,nz);
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package fr.koka.sanctuary.mixin.client;
|
||||
import fr.koka.sanctuary.client.shader.NaturalWater;
|
||||
import net.minecraft.client.Camera;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import net.minecraft.client.renderer.fog.FogRenderer;
|
||||
import net.minecraft.world.level.material.FogType;
|
||||
import org.joml.Vector4f;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.*;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
@Mixin(FogRenderer.class)
|
||||
abstract class NaturalWaterFogColorMixin {
|
||||
@Inject(method="computeFogColor",at=@At("RETURN"))
|
||||
private void sanctuary$waterColor(Camera camera,float ticks,ClientLevel level,int distance,float darken,Vector4f color,CallbackInfo ci){
|
||||
if(NaturalWater.strength()==0||camera.getFluidInCamera()!=FogType.WATER)return;
|
||||
float amount=NaturalWater.strength()*NaturalWater.fogProfile().tint();if(amount==0)return;
|
||||
// Apply after native water-vision normalization: normalizing olive to max=1 turns it neon yellow.
|
||||
// Retain native darkness/vision brightness, including a fully black blinded environment.
|
||||
float brightness=Math.max(color.x,Math.max(color.y,color.z));
|
||||
color.x+=(120/255f*brightness-color.x)*amount;
|
||||
color.y+=(132/255f*brightness-color.y)*amount;
|
||||
color.z+=(72/255f*brightness-color.z)*amount;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package fr.koka.sanctuary.mixin.client;
|
||||
import fr.koka.sanctuary.client.shader.NaturalWater;
|
||||
import net.minecraft.client.Camera;
|
||||
import net.minecraft.client.DeltaTracker;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import net.minecraft.client.renderer.fog.FogData;
|
||||
import net.minecraft.client.renderer.fog.environment.WaterFogEnvironment;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.*;
|
||||
import org.spongepowered.asm.mixin.injection.callback.*;
|
||||
@Mixin(WaterFogEnvironment.class)
|
||||
abstract class NaturalWaterFogMixin {
|
||||
@Inject(method="setupFog",at=@At("TAIL"))
|
||||
private void sanctuary$fog(FogData fog,Camera camera,ClientLevel level,float distance,DeltaTracker delta,CallbackInfo ci){
|
||||
float amount=NaturalWater.strength();if(amount==0)return;
|
||||
float factor=1+(NaturalWater.fogProfile().fog()-1)*amount;
|
||||
fog.environmentalStart*=factor;fog.environmentalEnd*=factor;fog.skyEnd*=factor;fog.cloudEnd*=factor;
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package fr.koka.sanctuary.mixin.client;
|
||||
import fr.koka.sanctuary.client.shader.NaturalWater;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import net.minecraft.client.renderer.chunk.RenderSectionRegion;
|
||||
import org.spongepowered.asm.mixin.*;
|
||||
import org.spongepowered.asm.mixin.injection.*;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
@Mixin(RenderSectionRegion.class)
|
||||
abstract class NaturalWaterRegionMixin implements NaturalWater.RegionAccess {
|
||||
@Shadow @Final private ClientLevel level;
|
||||
@Shadow @Final private net.minecraft.client.renderer.chunk.SectionCopy[] sections;
|
||||
@Shadow @Final private int minSectionX,minSectionY,minSectionZ;
|
||||
@Unique private NaturalWater.Region sanctuary$water;
|
||||
@Inject(method="<init>",at=@At("RETURN"))
|
||||
private void sanctuary$snapshot(CallbackInfo ci){
|
||||
if(NaturalWater.strength()==0)return;
|
||||
// Native 3×3×3 region, centre index 13. Palette rejection avoids 216 biome reads for dry terrain.
|
||||
var states=((NaturalWaterSectionCopyAccess)sections[13]).sanctuary$states();
|
||||
if(states!=null&&states.maybeHas(state->state.getFluidState().is(net.minecraft.tags.FluidTags.WATER)))
|
||||
sanctuary$water=new NaturalWater.Region(level,minSectionX,minSectionY,minSectionZ);
|
||||
}
|
||||
public NaturalWater.Region sanctuary$waterRegion(){return sanctuary$water;}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package fr.koka.sanctuary.mixin.client;
|
||||
import net.minecraft.client.renderer.chunk.SectionCopy;
|
||||
import net.minecraft.world.level.chunk.PalettedContainer;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.gen.Accessor;
|
||||
@Mixin(SectionCopy.class)
|
||||
interface NaturalWaterSectionCopyAccess {
|
||||
@Accessor("section") PalettedContainer<BlockState> sanctuary$states();
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package fr.koka.sanctuary.mixin.client;
|
||||
import fr.koka.sanctuary.client.shader.PbrAnimation;
|
||||
import net.minecraft.client.renderer.texture.SpriteContents;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.gen.Accessor;
|
||||
@Mixin(SpriteContents.AnimationState.class)
|
||||
public interface PbrAnimationClockMixin extends PbrAnimation.Clock {
|
||||
@Accessor("frame") int sanctuary$frame();
|
||||
@Accessor("subFrame") int sanctuary$subFrame();
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package fr.koka.sanctuary.mixin.client;
|
||||
import fr.koka.sanctuary.client.shader.PbrAnimation;
|
||||
import net.minecraft.client.renderer.texture.SpriteContents;
|
||||
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;
|
||||
@Mixin(SpriteContents.class)
|
||||
abstract class PbrAnimationSpriteMixin {
|
||||
@Inject(method="createAnimationState",at=@At("RETURN"))
|
||||
private void sanctuary$clock(CallbackInfoReturnable<SpriteContents.AnimationState> ci){
|
||||
PbrAnimation.register((SpriteContents)(Object)this,ci.getReturnValue());
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
abstract class PixelShadowDirtyMixin {
|
||||
@Inject(method="blockChanged", at=@At("HEAD"))
|
||||
private void sanctuary$dirty(BlockPos pos, int flags, CallbackInfo ci) {
|
||||
fr.koka.sanctuary.client.shader.NaturalWater.dirty(pos);
|
||||
PixelShadows.dirtyBlock(pos); ProceduralPbr.dirtyBlock(pos); Bloom.dirtyBlock(pos); ColoredLights.dirtyBlock(pos);
|
||||
}
|
||||
@Inject(method="allChanged", at=@At("HEAD"))
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@ abstract class PixelShadowRendererMixin {
|
||||
return original.call(buffer, matrix);
|
||||
}
|
||||
@Inject(method="renderLevel", at=@At(value="INVOKE", target="Lnet/minecraft/client/renderer/GameRenderer;render3dHud(Lnet/minecraft/client/renderer/state/level/CameraRenderState;Lnet/minecraft/client/renderer/state/level/PlayerRenderState;Lnet/minecraft/client/renderer/state/OptionsRenderState;Z)V"))
|
||||
private void sanctuary$shadows(CallbackInfo ci) { ColoredLights.prepare(); PixelShadows.render(); ProceduralPbr.render(); ColoredLights.render(); ScreenSpaceGI.render(); EdgeHighlights.render(); Bloom.render(); Sunshafts.render(); }
|
||||
private void sanctuary$shadows(CallbackInfo ci) { ColoredLights.prepare(); PixelShadows.render(); ProceduralPbr.render(); ColoredLights.render(); ScreenSpaceGI.render(); fr.koka.sanctuary.client.shader.PixelReflections.render(); EdgeHighlights.render(); Bloom.render(); Sunshafts.render(); }
|
||||
@Inject(method={"close", "setLevel"}, at=@At("HEAD"))
|
||||
private void sanctuary$close(CallbackInfo ci) { PixelShadows.release(); ProceduralPbr.release(); EdgeHighlights.release(); Bloom.release(); ColoredLights.release(); ScreenSpaceGI.release(); Sunshafts.release(); }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package fr.koka.sanctuary.mixin.client;
|
||||
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.renderpearl.api.commands.RenderPass;
|
||||
import fr.koka.sanctuary.client.shader.PixelReflections;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.LevelRenderer;
|
||||
import net.minecraft.client.renderer.chunk.ChunkSectionsToRender;
|
||||
import net.minecraft.client.renderer.feature.FeatureRenderDispatcher;
|
||||
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.callback.CallbackInfo;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalDouble;
|
||||
|
||||
/** Splits only the classic pass; copying an active Vulkan attachment is not legal. */
|
||||
@Mixin(LevelRenderer.class)
|
||||
abstract class ReflectionSceneMixin {
|
||||
@Unique private java.util.function.Consumer<RenderPass> sanctuary$transparency;
|
||||
@Inject(method="render",at=@At("HEAD"))
|
||||
private void sanctuary$begin(CallbackInfo ci){sanctuary$transparency=null;PixelReflections.beginFrame();}
|
||||
@WrapOperation(method="lambda$addMainPass$0",at=@At(value="INVOKE",target="Lnet/minecraft/client/renderer/LevelRenderer;executeClassicTransparency(Lnet/minecraft/client/renderer/chunk/ChunkSectionsToRender;Lnet/minecraft/client/renderer/feature/FeatureRenderDispatcher$PreparedFrame;Lcom/mojang/renderpearl/api/commands/RenderPass;)V"))
|
||||
private void sanctuary$defer(LevelRenderer self,ChunkSectionsToRender chunks,FeatureRenderDispatcher.PreparedFrame features,RenderPass pass,Operation<Void> original){
|
||||
if(!PixelReflections.eligible()){original.call(self,chunks,features,pass);return;}
|
||||
sanctuary$transparency=next->original.call(self,chunks,features,next);
|
||||
}
|
||||
@Inject(method="executeOit",at=@At("HEAD"))
|
||||
private void sanctuary$oit(CallbackInfo ci){PixelReflections.captureOpaque();}
|
||||
@Inject(method="executeOutline",at=@At("HEAD"))
|
||||
private void sanctuary$classic(CallbackInfo ci){
|
||||
var action=sanctuary$transparency;sanctuary$transparency=null;if(action==null)return;
|
||||
PixelReflections.captureOpaque();var main=Minecraft.getInstance().gameRenderer.mainRenderTarget();
|
||||
try(var pass=RenderSystem.getDevice().createCommandEncoder().createRenderPass(()->"Sanctuary native transparency",main.getColorTextureView(),Optional.empty(),main.getDepthTextureView(),OptionalDouble.empty())){
|
||||
RenderSystem.bindDefaultUniforms(pass);action.accept(pass);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
package fr.koka.sanctuary.progression;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/** Site access-code contract. The client never calls the site; only these decisions run on the server. */
|
||||
public final class AccessCodes {
|
||||
private static final Pattern CODE = Pattern.compile("SANC-[A-Z0-9]{4}-[A-Z0-9]{4}");
|
||||
private static final Pattern USER = Pattern.compile("[A-Za-z0-9_]{3,16}");
|
||||
private static final Pattern DISCORD = Pattern.compile("[0-9]{1,20}");
|
||||
private AccessCodes() { }
|
||||
public record Result(boolean valid, String reason, String minecraftUsername, String discordId) {
|
||||
public static Result of(boolean valid, String reason, String username, String discordId) {
|
||||
return new Result(valid, reason == null ? "" : reason, username == null ? "" : username, discordId == null ? "" : discordId);
|
||||
}
|
||||
public static Result unreachable() { return of(false, "unreachable", "", ""); }
|
||||
public static Result unauthorized() { return of(false, "unauthorized", "", ""); }
|
||||
public static Result unconfigured() { return of(false, "unconfigured", "", ""); }
|
||||
}
|
||||
public record Redeem(boolean accepted, String reason, String minecraftUsername, String discordId) {
|
||||
public static Redeem accept(String username, String discordId) { return new Redeem(true, "code_recognized", username, discordId); }
|
||||
public static Redeem reject(String reason) { return new Redeem(false, reason, "", ""); }
|
||||
}
|
||||
public static String normalize(String code) { return code == null ? "" : code.trim().toUpperCase(Locale.ROOT); }
|
||||
public static boolean validFormat(String normalized) { return normalized != null && CODE.matcher(normalized).matches(); }
|
||||
public static boolean validUsername(String name) { return name != null && USER.matcher(name).matches(); }
|
||||
public static boolean validDiscord(String id) { return id != null && DISCORD.matcher(id).matches(); }
|
||||
public static boolean sameUser(String returned, String session) {
|
||||
return validUsername(session) && returned != null && returned.equalsIgnoreCase(session) && validUsername(returned);
|
||||
}
|
||||
public static Result parse(int status, String body) {
|
||||
if (status == 401) return Result.unauthorized();
|
||||
if (status == 429 || status < 200 || status >= 500) return Result.unreachable();
|
||||
try {
|
||||
var object = JsonParser.parseString(body).getAsJsonObject();
|
||||
if (!object.has("valid") || !object.has("reason")) return Result.unreachable();
|
||||
return Result.of(object.get("valid").getAsBoolean(), text(object, "reason"), text(object, "minecraft_username"), text(object, "discord_id"));
|
||||
} catch (RuntimeException error) { return Result.unreachable(); }
|
||||
}
|
||||
public static String verifyKey(Result result, String username) {
|
||||
if (result == null) return "code_unreachable";
|
||||
return switch (result.reason()) {
|
||||
case "unconfigured" -> "code_unconfigured";
|
||||
case "unauthorized" -> "code_unauthorized";
|
||||
case "unreachable" -> "code_unreachable";
|
||||
case "unused" -> result.valid() && sameUser(result.minecraftUsername(), username) && validDiscord(result.discordId()) ? "code_recognized" : "code_invalid";
|
||||
case "invalid" -> "code_invalid";
|
||||
case "already_used" -> "code_used";
|
||||
default -> "code_invalid";
|
||||
};
|
||||
}
|
||||
/** Redeem commits the code. An already used code is accepted only when the site still returns the same account. */
|
||||
public static Redeem redeem(Result result, String username) {
|
||||
if (result == null) return Redeem.reject("code_unreachable");
|
||||
return switch (result.reason()) {
|
||||
case "unconfigured" -> Redeem.reject("code_unconfigured");
|
||||
case "unauthorized" -> Redeem.reject("code_unauthorized");
|
||||
case "unreachable" -> Redeem.reject("code_unreachable");
|
||||
case "redeemed", "already_used" -> account(result, username);
|
||||
case "invalid" -> Redeem.reject("code_invalid");
|
||||
default -> Redeem.reject("code_invalid");
|
||||
};
|
||||
}
|
||||
private static Redeem account(Result result, String username) {
|
||||
if (!result.minecraftUsername().isEmpty() && !result.minecraftUsername().equalsIgnoreCase(username))
|
||||
return Redeem.reject("code_mismatch");
|
||||
if (!sameUser(result.minecraftUsername(), username) || !validDiscord(result.discordId()))
|
||||
return Redeem.reject("code_replay");
|
||||
return Redeem.accept(result.minecraftUsername(), result.discordId());
|
||||
}
|
||||
private static String text(JsonObject object, String key) {
|
||||
if (!object.has(key) || object.get(key).isJsonNull() || !object.get(key).isJsonPrimitive()) return "";
|
||||
return object.get(key).getAsString();
|
||||
}
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
package fr.koka.sanctuary.progression;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/** Server-only site access. The token stays in the environment or the server config, never in the client. */
|
||||
public final class AccessConfig {
|
||||
private static final Logger LOG = LoggerFactory.getLogger("Sanctuary/Access");
|
||||
private static final HttpClient HTTP = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).followRedirects(HttpClient.Redirect.NEVER).build();
|
||||
public interface Directory {
|
||||
AccessCodes.Result verify(String username, String code);
|
||||
AccessCodes.Result redeem(String username, String code);
|
||||
}
|
||||
private static volatile boolean enabled;
|
||||
private static volatile Directory directory = new Disabled();
|
||||
private AccessConfig() { }
|
||||
public static boolean enabled() { return enabled; }
|
||||
public static Directory directory() { return directory; }
|
||||
public static void load(Path configDir) {
|
||||
String url = env("SANCTUARY_APP_URL");
|
||||
String token = env("SANCTUARY_API_TOKEN");
|
||||
if (url.isEmpty() || token.isEmpty()) {
|
||||
var file = readFile(configDir.resolve("access.json"));
|
||||
if (url.isEmpty()) url = file.url;
|
||||
if (token.isEmpty()) token = file.token;
|
||||
}
|
||||
if (url.isEmpty() && token.isEmpty()) {
|
||||
enabled = false; directory = new Disabled();
|
||||
LOG.info("Sanctuary access codes are off. Set SANCTUARY_APP_URL and SANCTUARY_API_TOKEN to require a code.");
|
||||
return;
|
||||
}
|
||||
URI uri;
|
||||
try { uri = URI.create(url.endsWith("/") ? url.substring(0, url.length() - 1) : url); }
|
||||
catch (IllegalArgumentException error) { failClosed("Sanctuary access URL is invalid. Character creation stays closed."); return; }
|
||||
if (uri.getUserInfo() != null || uri.getRawQuery() != null || uri.getHost() == null) {
|
||||
failClosed("Sanctuary access URL must be a bare https origin. Character creation stays closed."); return;
|
||||
}
|
||||
String scheme = uri.getScheme();
|
||||
if (!"https".equalsIgnoreCase(scheme) && !"http".equalsIgnoreCase(scheme)) {
|
||||
failClosed("Sanctuary access URL must use https. Character creation stays closed."); return;
|
||||
}
|
||||
if (token.isEmpty()) { failClosed("Sanctuary access token is missing. Character creation stays closed."); return; }
|
||||
if ("http".equalsIgnoreCase(scheme)) LOG.warn("Sanctuary access URL is not HTTPS. Use HTTPS outside a local site.");
|
||||
enabled = true;
|
||||
directory = new HttpDirectory(uri, token);
|
||||
LOG.info("Sanctuary access codes are required before character creation.");
|
||||
}
|
||||
private static void failClosed(String message) {
|
||||
enabled = true; directory = new Disabled(); LOG.error(message);
|
||||
}
|
||||
private static String env(String key) {
|
||||
String value = System.getenv(key);
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
private record FileConfig(String url, String token) { }
|
||||
private static FileConfig readFile(Path file) {
|
||||
if (!Files.isRegularFile(file)) return new FileConfig("", "");
|
||||
try {
|
||||
var object = JsonParser.parseString(Files.readString(file, StandardCharsets.UTF_8)).getAsJsonObject();
|
||||
return new FileConfig(text(object, "appUrl"), text(object, "token"));
|
||||
} catch (Exception error) {
|
||||
LOG.error("Sanctuary access config cannot be read. Character creation stays closed.");
|
||||
return new FileConfig("http://invalid", "");
|
||||
}
|
||||
}
|
||||
private static String text(JsonObject object, String key) {
|
||||
if (!object.has(key) || object.get(key).isJsonNull()) return "";
|
||||
return object.get(key).getAsString().trim();
|
||||
}
|
||||
private static final class Disabled implements Directory {
|
||||
public AccessCodes.Result verify(String username, String code) { return AccessCodes.Result.unconfigured(); }
|
||||
public AccessCodes.Result redeem(String username, String code) { return AccessCodes.Result.unconfigured(); }
|
||||
}
|
||||
private static final class HttpDirectory implements Directory {
|
||||
private final URI origin;
|
||||
private final String token;
|
||||
HttpDirectory(URI origin, String token) { this.origin = origin; this.token = token; }
|
||||
public AccessCodes.Result verify(String username, String code) { return post("/api/v1/access-codes/verify", username, code); }
|
||||
public AccessCodes.Result redeem(String username, String code) { return post("/api/v1/access-codes/redeem", username, code); }
|
||||
private AccessCodes.Result post(String path, String username, String code) {
|
||||
var body = new JsonObject();
|
||||
body.addProperty("code", code);
|
||||
body.addProperty("minecraft_username", username);
|
||||
var request = HttpRequest.newBuilder(URI.create(origin.toString() + path)).timeout(Duration.ofSeconds(10))
|
||||
.header("Authorization", "Bearer " + token).header("Content-Type", "application/json").header("Accept", "application/json")
|
||||
.POST(HttpRequest.BodyPublishers.ofString(body.toString())).build();
|
||||
try {
|
||||
var response = HTTP.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
|
||||
if (response.statusCode() == 401) LOG.warn("Sanctuary access token was rejected");
|
||||
return AccessCodes.parse(response.statusCode(), response.body());
|
||||
} catch (Exception error) {
|
||||
LOG.warn("Sanctuary access endpoint is unreachable ({})", error.getClass().getSimpleName());
|
||||
return AccessCodes.Result.unreachable();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
package fr.koka.sanctuary.progression;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonObject;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
import java.util.TreeMap;
|
||||
import java.util.UUID;
|
||||
|
||||
/** Discord binding for an inhabitant. Pending entries finish a creation interrupted after the site consumed the code. */
|
||||
public final class AccessLedger {
|
||||
private static final int LIMIT = 1024 * 1024;
|
||||
private final Path file;
|
||||
private final long seed;
|
||||
private byte[] previous;
|
||||
private final TreeMap<UUID, Binding> pending = new TreeMap<>();
|
||||
private final TreeMap<UUID, Binding> bound = new TreeMap<>();
|
||||
private boolean failed;
|
||||
public record Binding(String name, String discordId) { }
|
||||
public AccessLedger(Path file, long seed) throws IOException {
|
||||
this.file = file; this.seed = seed;
|
||||
previous = AtomicJson.read(file, LIMIT);
|
||||
if (previous == null) return;
|
||||
var root = AtomicJson.parse(new String(previous, StandardCharsets.UTF_8)).getAsJsonObject();
|
||||
AtomicJson.fields(root, "schema", "seed", "pending", "bound");
|
||||
if (root.get("schema").getAsInt() != 1 || root.get("seed").getAsLong() != seed)
|
||||
throw new IllegalArgumentException("Invalid access ledger");
|
||||
read(root.getAsJsonObject("pending"), pending);
|
||||
read(root.getAsJsonObject("bound"), bound);
|
||||
}
|
||||
public Binding known(UUID id) {
|
||||
var open = pending.get(id);
|
||||
return open != null ? open : bound.get(id);
|
||||
}
|
||||
public void remember(UUID id, String name, String discordId) throws IOException {
|
||||
if (failed) throw new IOException("Access ledger failed earlier");
|
||||
if (!AccessCodes.validUsername(name) || !AccessCodes.validDiscord(discordId))
|
||||
throw new IllegalArgumentException("Invalid access binding");
|
||||
if (bound.containsKey(id)) return;
|
||||
pending.put(id, new Binding(name, discordId));
|
||||
save();
|
||||
}
|
||||
public void commit(UUID id) throws IOException {
|
||||
if (failed) throw new IOException("Access ledger failed earlier");
|
||||
var binding = pending.remove(id);
|
||||
if (binding == null) return;
|
||||
bound.put(id, binding);
|
||||
save();
|
||||
}
|
||||
private static void read(JsonObject object, TreeMap<UUID, Binding> into) {
|
||||
for (var entry : object.entrySet()) {
|
||||
AtomicJson.fields(entry.getValue(), "name", "discordId");
|
||||
UUID id = UUID.fromString(entry.getKey());
|
||||
var value = entry.getValue().getAsJsonObject();
|
||||
String name = value.get("name").getAsString();
|
||||
String discordId = value.get("discordId").getAsString();
|
||||
if (!AccessCodes.validUsername(name) || !AccessCodes.validDiscord(discordId))
|
||||
throw new IllegalArgumentException("Invalid access binding");
|
||||
if (into.put(id, new Binding(name, discordId)) != null) throw new IllegalArgumentException("Duplicate access binding");
|
||||
}
|
||||
}
|
||||
private void save() throws IOException {
|
||||
var root = new JsonObject();
|
||||
root.addProperty("schema", 1);
|
||||
root.addProperty("seed", seed);
|
||||
root.add("pending", write(pending));
|
||||
root.add("bound", write(bound));
|
||||
try {
|
||||
previous = AtomicJson.write(file, previous, new GsonBuilder().setPrettyPrinting().create().toJson(root), LIMIT);
|
||||
} catch (IOException | RuntimeException error) { failed = true; throw error; }
|
||||
}
|
||||
private static JsonObject write(TreeMap<UUID, Binding> map) {
|
||||
var object = new JsonObject();
|
||||
map.forEach((id, binding) -> {
|
||||
var value = new JsonObject();
|
||||
value.addProperty("name", binding.name);
|
||||
value.addProperty("discordId", binding.discordId);
|
||||
object.add(id.toString(), value);
|
||||
});
|
||||
return object;
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package fr.koka.sanctuary.progression;
|
||||
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
|
||||
/** Additive Hello World handshake. Existing create packets stay unchanged when access codes are off. */
|
||||
public final class AccessPayloads {
|
||||
private AccessPayloads() { }
|
||||
public record Gate(boolean required, boolean pending) implements CustomPacketPayload {
|
||||
public static final Type<Gate> TYPE = new Type<>(SanctuaryMod.id("access_gate_v1"));
|
||||
public static final StreamCodec<FriendlyByteBuf, Gate> CODEC = CustomPacketPayload.codec(
|
||||
(p, b) -> { b.writeBoolean(p.required); b.writeBoolean(p.pending); },
|
||||
b -> new Gate(b.readBoolean(), b.readBoolean()));
|
||||
public Type<Gate> type() { return TYPE; }
|
||||
}
|
||||
public record Check(String code) implements CustomPacketPayload {
|
||||
public static final Type<Check> TYPE = new Type<>(SanctuaryMod.id("access_check_v1"));
|
||||
public static final StreamCodec<FriendlyByteBuf, Check> CODEC = CustomPacketPayload.codec(
|
||||
(p, b) -> b.writeUtf(p.code, 32), b -> new Check(b.readUtf(32)));
|
||||
public Type<Check> type() { return TYPE; }
|
||||
}
|
||||
public record Status(String code, String reason) implements CustomPacketPayload {
|
||||
public static final Type<Status> TYPE = new Type<>(SanctuaryMod.id("access_status_v1"));
|
||||
public static final StreamCodec<FriendlyByteBuf, Status> CODEC = CustomPacketPayload.codec(
|
||||
(p, b) -> { b.writeUtf(p.code, 32); b.writeUtf(p.reason, 64); },
|
||||
b -> new Status(b.readUtf(32), b.readUtf(64)));
|
||||
public Type<Status> type() { return TYPE; }
|
||||
}
|
||||
public record Admit(String bio, int color, String mob, String name, String code) implements CustomPacketPayload {
|
||||
public static final Type<Admit> TYPE = new Type<>(SanctuaryMod.id("access_admit_v1"));
|
||||
public static final StreamCodec<FriendlyByteBuf, Admit> CODEC = CustomPacketPayload.codec(
|
||||
(p, b) -> { b.writeUtf(p.bio, 256); b.writeInt(p.color); b.writeUtf(p.mob, 64); b.writeUtf(p.name, StarterCompanions.NAME_LIMIT); b.writeUtf(p.code, 32); },
|
||||
b -> new Admit(b.readUtf(256), b.readInt(), b.readUtf(64), b.readUtf(StarterCompanions.NAME_LIMIT), b.readUtf(32)));
|
||||
public Type<Admit> type() { return TYPE; }
|
||||
}
|
||||
}
|
||||
+1
-113
@@ -50,7 +50,6 @@ public final class ProgressionService {
|
||||
public StarterCompanions.Store starters;
|
||||
public CycleConfig cycleConfig=CycleConfig.DEFAULT;
|
||||
public TutorialConfig tutorialConfig=TutorialConfig.DEFAULT;
|
||||
public AccessLedger access;
|
||||
Session(InhabitantStore inhabitants, ProgressionConfig config, MobNamesConfig names) {
|
||||
this.inhabitants=inhabitants; this.config=config; this.names=names;
|
||||
}
|
||||
@@ -62,10 +61,6 @@ public final class ProgressionService {
|
||||
PayloadTypeRegistry.clientboundConfiguration().register(StarterPayloads.Welcome.TYPE,StarterPayloads.Welcome.CODEC);
|
||||
PayloadTypeRegistry.serverboundConfiguration().register(StarterPayloads.Create.TYPE,StarterPayloads.Create.CODEC);
|
||||
PayloadTypeRegistry.serverboundConfiguration().register(StarterPayloads.NamedCreate.TYPE,StarterPayloads.NamedCreate.CODEC);
|
||||
PayloadTypeRegistry.clientboundConfiguration().register(AccessPayloads.Gate.TYPE,AccessPayloads.Gate.CODEC);
|
||||
PayloadTypeRegistry.clientboundConfiguration().register(AccessPayloads.Status.TYPE,AccessPayloads.Status.CODEC);
|
||||
PayloadTypeRegistry.serverboundConfiguration().register(AccessPayloads.Check.TYPE,AccessPayloads.Check.CODEC);
|
||||
PayloadTypeRegistry.serverboundConfiguration().register(AccessPayloads.Admit.TYPE,AccessPayloads.Admit.CODEC);
|
||||
SanctuarySitting.register();
|
||||
fr.koka.sanctuary.inventory.HotbarService.register();
|
||||
fr.koka.sanctuary.inventory.InventorySortService.register();
|
||||
@@ -92,9 +87,6 @@ public final class ProgressionService {
|
||||
});
|
||||
ServerConfigurationConnectionEvents.CONFIGURE.register((handler,server) -> {
|
||||
if (session(server) == null) return;
|
||||
if (session(server).access != null && !ServerConfigurationNetworking.canSend(handler,AccessPayloads.Gate.TYPE)) {
|
||||
handler.disconnect(label("access_version_required")); return;
|
||||
}
|
||||
if (!ServerConfigurationNetworking.canSend(handler,ProgressionPayloads.Hello.TYPE)) {
|
||||
handler.disconnect(label("client_required")); return;
|
||||
}
|
||||
@@ -155,12 +147,6 @@ public final class ProgressionService {
|
||||
ServerConfigurationNetworking.registerGlobalReceiver(StarterPayloads.NamedCreate.TYPE,(payload,context)->context.server().execute(()->{
|
||||
HelloTask task=PENDING.get(context.packetListener());if(task!=null)task.createWithStarter(new StarterPayloads.Create(payload.bio(),payload.color(),payload.mob()),payload.name());
|
||||
}));
|
||||
ServerConfigurationNetworking.registerGlobalReceiver(AccessPayloads.Check.TYPE,(payload,context)->context.server().execute(()->{
|
||||
HelloTask task=PENDING.get(context.packetListener());if(task!=null)task.check(payload.code());
|
||||
}));
|
||||
ServerConfigurationNetworking.registerGlobalReceiver(AccessPayloads.Admit.TYPE,(payload,context)->context.server().execute(()->{
|
||||
HelloTask task=PENDING.get(context.packetListener());if(task!=null)task.admit(payload);
|
||||
}));
|
||||
ServerConfigurationNetworking.registerGlobalReceiver(ProgressionPayloads.IntroFinished.TYPE, (payload,context) ->
|
||||
context.server().execute(() -> {
|
||||
HelloTask task = PENDING.get(context.packetListener());
|
||||
@@ -208,7 +194,6 @@ public final class ProgressionService {
|
||||
private static void start(MinecraftServer server) {
|
||||
if (!SanctuaryGameplay.supported(server)) return;
|
||||
try {
|
||||
AccessConfig.load(CONFIG);
|
||||
var config=ProgressionConfig.load(CONFIG.resolve("progression.json"));
|
||||
var names=MobNamesConfig.load(CONFIG.resolve("mob-names.json"));
|
||||
Path world=server.getWorldPath(LevelResource.ROOT);
|
||||
@@ -225,7 +210,6 @@ public final class ProgressionService {
|
||||
session.cycleConfig=CycleConfig.load(CONFIG.resolve("cycle-factions.json"));
|
||||
session.tutorialConfig=TutorialConfig.load(CONFIG.resolve("tutorial.json"));
|
||||
session.cycles=new CycleStore(world.resolve("data/sanctuary-cycle-factions.json"),server.overworld().getSeed());
|
||||
if (AccessConfig.enabled()) session.access=new AccessLedger(world.resolve("data/sanctuary-access.json"),server.overworld().getSeed());
|
||||
for (var event:session.cycles.events()) {
|
||||
if (inhabitants.get(event.actor())==null || (!event.target().isEmpty() && inhabitants.get(UUID.fromString(event.target()))==null))
|
||||
throw new java.io.IOException("Cycle journal references a missing inhabitant");
|
||||
@@ -249,10 +233,7 @@ public final class ProgressionService {
|
||||
private static final Type TYPE=new Type("sanctuary:hello_world");
|
||||
private final ServerConfigurationPacketListenerImpl handler;
|
||||
private final MinecraftServer server;
|
||||
private boolean awaitingIntro,awaitingStarter,admitted,busy;
|
||||
private String lastChecked="",queuedCheck;
|
||||
private AccessPayloads.Admit queuedAdmit;
|
||||
private long lastCheckAt;
|
||||
private boolean awaitingIntro,awaitingStarter;
|
||||
HelloTask(ServerConfigurationPacketListenerImpl handler,MinecraftServer server) { this.handler=handler;this.server=server; }
|
||||
public Type type() { return TYPE; }
|
||||
public void start(Consumer<Packet<?>> sender) {
|
||||
@@ -274,8 +255,6 @@ public final class ProgressionService {
|
||||
void offer(String error) {
|
||||
var profile=((ProgressionConfigurationAccess)handler).sanctuary$profile();
|
||||
var session=session(server);var inhabitant=session.inhabitants.get(profile.id());var starters=session.starters.get(profile.id());
|
||||
if (session.access != null && ServerConfigurationNetworking.canSend(handler,AccessPayloads.Gate.TYPE))
|
||||
ServerConfigurationNetworking.send(handler,new AccessPayloads.Gate(true,session.access.known(profile.id())!=null));
|
||||
if(starters!=null){
|
||||
var identity=new ProgressionPayloads.Hello(profile.name(),inhabitant==null?session.inhabitants.available():java.util.List.of(inhabitant.color()),error);
|
||||
ServerConfigurationNetworking.send(handler,new StarterPayloads.Welcome(identity,inhabitant==null?"":inhabitant.bio(),inhabitant!=null,starters.choices()));
|
||||
@@ -284,7 +263,6 @@ public final class ProgressionService {
|
||||
void create(ProgressionPayloads.Create payload) {
|
||||
if (awaitingIntro || awaitingStarter) return;
|
||||
if (!handler.isAcceptingMessages()) { PENDING.remove(handler); return; }
|
||||
if (AccessConfig.enabled() && !admitted) { offer("code_required"); return; }
|
||||
Session session=session(server);
|
||||
if (session == null || !session.healthy) { handler.disconnect(label("storage_invalid")); return; }
|
||||
try {
|
||||
@@ -310,7 +288,6 @@ public final class ProgressionService {
|
||||
}
|
||||
void createWithStarter(StarterPayloads.Create payload,String name) {
|
||||
if(awaitingIntro||!handler.isAcceptingMessages())return;
|
||||
if (AccessConfig.enabled() && !admitted) { offer("code_required"); return; }
|
||||
var session=session(server);if(session==null||!session.healthy){handler.disconnect(label("storage_invalid"));return;}
|
||||
try {
|
||||
var profile=((ProgressionConfigurationAccess)handler).sanctuary$profile();var uuid=profile.id();
|
||||
@@ -326,94 +303,6 @@ public final class ProgressionService {
|
||||
awaitingStarter=false;beginIntro(uuid);
|
||||
}catch(Exception error){fail(error);}
|
||||
}
|
||||
void check(String code) {
|
||||
if (awaitingIntro || !handler.isAcceptingMessages()) return;
|
||||
var session=session(server);
|
||||
if (session == null || session.access == null) return;
|
||||
var profile=((ProgressionConfigurationAccess)handler).sanctuary$profile();
|
||||
String normalized=AccessCodes.normalize(code);
|
||||
if (session.access.known(profile.id()) != null) { status(normalized,"code_recognized"); return; }
|
||||
if (!AccessCodes.validFormat(normalized)) { status(normalized,"code_malformed"); return; }
|
||||
if (!AccessCodes.validUsername(profile.name())) { status(normalized,"code_username"); return; }
|
||||
if (busy) { queuedCheck=normalized; queuedAdmit=null; return; }
|
||||
if (normalized.equals(lastChecked) && System.nanoTime()-lastCheckAt < 1_000_000_000L) return;
|
||||
verify(profile.name(),normalized);
|
||||
}
|
||||
void verify(String username,String code) {
|
||||
busy=true; lastChecked=code; lastCheckAt=System.nanoTime(); queuedCheck=null;
|
||||
var directory=AccessConfig.directory();
|
||||
java.util.concurrent.CompletableFuture.supplyAsync(() -> directory.verify(username,code)).whenComplete((result,error) -> server.execute(() -> {
|
||||
busy=false;
|
||||
if (PENDING.get(handler) != this || !handler.isAcceptingMessages()) return;
|
||||
status(code, error != null || result == null ? "code_unreachable" : AccessCodes.verifyKey(result,username));
|
||||
if (queuedAdmit != null) { var next=queuedAdmit; queuedAdmit=null; admit(next); return; }
|
||||
if (queuedCheck != null && !queuedCheck.equals(code) && AccessCodes.validFormat(queuedCheck)) {
|
||||
String next=queuedCheck; queuedCheck=null; verify(username,next);
|
||||
}
|
||||
}));
|
||||
}
|
||||
void status(String code,String reason) {
|
||||
if (handler.isAcceptingMessages() && ServerConfigurationNetworking.canSend(handler,AccessPayloads.Status.TYPE))
|
||||
ServerConfigurationNetworking.send(handler,new AccessPayloads.Status(code,reason));
|
||||
}
|
||||
void admit(AccessPayloads.Admit payload) {
|
||||
if (awaitingIntro || !handler.isAcceptingMessages()) return;
|
||||
String normalized=AccessCodes.normalize(payload.code());
|
||||
if (busy) { queuedAdmit=payload; return; }
|
||||
var session=session(server);
|
||||
if (session == null || !session.healthy) { handler.disconnect(label("storage_invalid")); return; }
|
||||
if (session.access == null) { offer("code_unconfigured"); return; }
|
||||
var profile=((ProgressionConfigurationAccess)handler).sanctuary$profile();
|
||||
var uuid=profile.id();
|
||||
if (!ready(session,uuid,payload)) return;
|
||||
if (session.access.known(uuid) != null) { finishAdmit(session,uuid,payload); drain(); return; }
|
||||
if (!AccessCodes.validFormat(normalized)) { offer("code_malformed"); return; }
|
||||
if (!AccessCodes.validUsername(profile.name())) { offer("code_username"); return; }
|
||||
busy=true;
|
||||
String username=profile.name();
|
||||
var directory=AccessConfig.directory();
|
||||
java.util.concurrent.CompletableFuture.supplyAsync(() -> directory.redeem(username,normalized)).whenComplete((result,error) -> server.execute(() -> {
|
||||
busy=false;
|
||||
if (PENDING.get(handler) != this || !handler.isAcceptingMessages()) return;
|
||||
if (error != null || result == null) { offer("code_unreachable"); drain(); return; }
|
||||
var decision=AccessCodes.redeem(result,username);
|
||||
if (!decision.accepted()) { offer(decision.reason()); drain(); return; }
|
||||
try {
|
||||
session.access.remember(uuid,decision.minecraftUsername(),decision.discordId());
|
||||
finishAdmit(session,uuid,payload);
|
||||
} catch (Exception failure) { fail(failure); }
|
||||
drain();
|
||||
}));
|
||||
}
|
||||
boolean ready(Session session,UUID uuid,AccessPayloads.Admit payload) {
|
||||
var starters=session.starters.get(uuid);
|
||||
if (starters != null) {
|
||||
if (starters.choices().stream().noneMatch(choice -> choice.mob().equals(payload.mob()))) { offer("starter_required"); return false; }
|
||||
if (!StarterCompanions.validName(payload.name())) { offer("starter_name_invalid"); return false; }
|
||||
} else if (!payload.mob().isEmpty()) { offer("starter_required"); return false; }
|
||||
if (session.inhabitants.get(uuid) == null) {
|
||||
if (!InhabitantStore.validBio(payload.bio())) { offer("bio_invalid"); return false; }
|
||||
if (!session.inhabitants.available().contains(payload.color())) { offer("color_taken"); return false; }
|
||||
}
|
||||
return true;
|
||||
}
|
||||
void drain() {
|
||||
if (!handler.isAcceptingMessages() || PENDING.get(handler) != this) return;
|
||||
if (queuedAdmit != null) { var next=queuedAdmit; queuedAdmit=null; admit(next); }
|
||||
else if (queuedCheck != null && AccessCodes.validFormat(queuedCheck)) {
|
||||
String next=queuedCheck; queuedCheck=null;
|
||||
verify(((ProgressionConfigurationAccess)handler).sanctuary$profile().name(),next);
|
||||
}
|
||||
}
|
||||
void finishAdmit(Session session,UUID uuid,AccessPayloads.Admit payload) {
|
||||
admitted=true;
|
||||
try {
|
||||
if (payload.mob().isEmpty()) create(new ProgressionPayloads.Create(payload.bio(),payload.color()));
|
||||
else createWithStarter(new StarterPayloads.Create(payload.bio(),payload.color(),payload.mob()),payload.name());
|
||||
} finally { admitted=false; }
|
||||
try { if (session.inhabitants.get(uuid) != null) session.access.commit(uuid); }
|
||||
catch (Exception error) { fail(error); }
|
||||
}
|
||||
void fail(Exception error){
|
||||
session(server).healthy=false;LOG.error("Could not persist Hello World starter",error);handler.disconnect(label("storage_invalid"));
|
||||
}
|
||||
@@ -436,7 +325,6 @@ public final class ProgressionService {
|
||||
if (!restore(player)) return;
|
||||
sendNames(player,session);
|
||||
try {
|
||||
if (session.access != null) session.access.commit(player.getUUID());
|
||||
StarterCompanions.deliver(player,session.starters);
|
||||
if (session.inhabitants.arrive(player.getUUID())) player.level().getServer().getPlayerList()
|
||||
.broadcastSystemMessage(Component.literal("helloworld ").append(player.getDisplayName()),false);
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package fr.koka.sanctuary.water;
|
||||
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.expansion.ExpansionChunkGenerator;
|
||||
import net.fabricmc.fabric.api.networking.v1.*;
|
||||
import net.fabricmc.fabric.api.entity.event.v1.ServerEntityLevelChangeEvents;
|
||||
import net.fabricmc.fabric.api.entity.event.v1.ServerPlayerEvents;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
|
||||
/** Read-only dimension context; no persistent world or generation changes. */
|
||||
public final class WaterWorldSettings {
|
||||
private WaterWorldSettings() {}
|
||||
public record Context(boolean sanctuary) implements CustomPacketPayload {
|
||||
public static final Type<Context> TYPE=new Type<>(SanctuaryMod.id("water_world_v1"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf,Context> CODEC=CustomPacketPayload.codec(
|
||||
(p,b)->b.writeBoolean(p.sanctuary),b->new Context(b.readBoolean()));
|
||||
public Type<Context> type(){return TYPE;}
|
||||
}
|
||||
public static void register(){
|
||||
PayloadTypeRegistry.clientboundPlay().register(Context.TYPE,Context.CODEC);
|
||||
ServerPlayConnectionEvents.JOIN.register((handler,sender,server)->send(handler.player));
|
||||
ServerEntityLevelChangeEvents.AFTER_PLAYER_CHANGE_LEVEL.register((player,origin,destination)->send(player));
|
||||
ServerPlayerEvents.AFTER_RESPAWN.register((oldPlayer,player,alive)->send(player));
|
||||
}
|
||||
private static void send(ServerPlayer player){
|
||||
if(ServerPlayNetworking.canSend(player,Context.TYPE))
|
||||
ServerPlayNetworking.send(player,new Context(player.level().getChunkSource().getGenerator() instanceof ExpansionChunkGenerator));
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,9 @@
|
||||
{
|
||||
"sanctuary.shaders.ssr_intensity_value": "Reflection strength: %s%%",
|
||||
"sanctuary.shaders.ssr_intensity_help": "Strength of visible landscape reflections on solids and water. Default: 20%.",
|
||||
"sanctuary.shaders.ssr": "SSR",
|
||||
"sanctuary.shaders.ssr_help": "Colours material texels with the visible landscape. Requires PBR; off-screen objects cannot be reflected.",
|
||||
"sanctuary.shaders.ssr_intensity": "Reflection strength",
|
||||
"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.",
|
||||
@@ -267,21 +272,6 @@
|
||||
"sanctuary.progression.choose_color": "Choose your colour",
|
||||
"sanctuary.progression.cancel": "Cancel",
|
||||
"sanctuary.progression.enter": "Enter Sanctuary",
|
||||
"sanctuary.progression.code": "Access code",
|
||||
"sanctuary.progression.code_hint": "SANC-XXXX-XXXX",
|
||||
"sanctuary.progression.code_recognized": "Code recognised.",
|
||||
"sanctuary.progression.code_checking": "Checking the code…",
|
||||
"sanctuary.progression.code_malformed": "The code should look like SANC-XXXX-XXXX.",
|
||||
"sanctuary.progression.code_invalid": "This code does not match this name.",
|
||||
"sanctuary.progression.code_used": "This code has already been used.",
|
||||
"sanctuary.progression.code_replay": "This code is already used and Sanctuary cannot recover the account. Try again later.",
|
||||
"sanctuary.progression.code_unreachable": "Sanctuary is unreachable, try again.",
|
||||
"sanctuary.progression.code_unauthorized": "The server cannot reach Sanctuary.",
|
||||
"sanctuary.progression.code_unconfigured": "The server has no Sanctuary token.",
|
||||
"sanctuary.progression.code_required": "Enter the code sent in a private message.",
|
||||
"sanctuary.progression.code_username": "This name cannot receive a code.",
|
||||
"sanctuary.progression.code_mismatch": "The code does not match this session.",
|
||||
"sanctuary.progression.access_version_required": "This server requires the Sanctuary access code. Update the client.",
|
||||
"sanctuary.progression.joining": "Character saved, joining…",
|
||||
"sanctuary.progression.bio_invalid": "Use plain text for your bio (256 characters).",
|
||||
"sanctuary.progression.color_taken": "Colour just taken. Choose another one.",
|
||||
@@ -2312,9 +2302,9 @@
|
||||
"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 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%%",
|
||||
"sanctuary.shaders.pbr_intensity": "PBR intensity",
|
||||
"sanctuary.shaders.pbr_intensity_help": "Material relief and highlights, including water. Default: 50%.",
|
||||
"sanctuary.shaders.pbr_intensity_value": "PBR intensity: %s %%",
|
||||
"sanctuary.shaders.lens_flare": "Lens flare · soft rectangles",
|
||||
"sanctuary.shaders.lens_flare_help": "Soft rectangular sun and moon reflections. Obstacles hide the sources. Independent of the bloom halo.",
|
||||
"sanctuary.shaders.lens_flare_intensity": "Lens flare intensity",
|
||||
@@ -2334,5 +2324,19 @@
|
||||
"sanctuary.shaders.colored_light_distance_value": "Colored lights: %s blocks",
|
||||
"sanctuary.shaders.colored_light_distance_help": "View distance: 8 to 64 blocks. Longer distances use more memory and computation. Smooth range fade; each source keeps its native reach.",
|
||||
"sanctuary.shaders.warm_torches": "Warm torches",
|
||||
"sanctuary.shaders.warm_torches_help": "Off: ordinary torches keep the native color temperature. On: orange tint. Other sources retain their own colors."
|
||||
"sanctuary.shaders.warm_torches_help": "Off: ordinary torches keep the native color temperature. On: orange tint. Other sources retain their own colors.",
|
||||
"sanctuary.shaders.natural_water": "Natural water",
|
||||
"sanctuary.shaders.natural_water_help": "Bed-dependent tint, transparency and fog. Defaults OFF in ordinary worlds and ON on Sanctuary islands. Preferences are stored separately for each world type.",
|
||||
"sanctuary.shaders.water_intensity": "Water intensity",
|
||||
"sanctuary.shaders.water_intensity_help": "At 0%, keeps native rendering. At 100%, applies all three profiles.",
|
||||
"sanctuary.shaders.water_intensity_value": "Water intensity: %s%%",
|
||||
"sanctuary.shaders.pbr_distance": "PBR distance",
|
||||
"sanctuary.shaders.pbr_distance_value": "PBR distance: %s blocks",
|
||||
"sanctuary.shaders.pbr_distance_help": "Independent PBR range: 16, 32, 64, 128 or 256 blocks. Default: 64 blocks. Limited to visible terrain and mesh budget.",
|
||||
"sanctuary.shaders.water_blend": "Water blending",
|
||||
"sanctuary.shaders.water_blend_value": "Water blending: %s × %s blocks",
|
||||
"sanctuary.shaders.water_blend_help": "Transition width between bed-dependent water colours: 1, 3, 5, 9 or 17 blocks. Default: 9 × 9.",
|
||||
"sanctuary.shaders.ssr_distance": "SSR distance",
|
||||
"sanctuary.shaders.ssr_distance_value": "SSR distance: %s blocks",
|
||||
"sanctuary.shaders.ssr_distance_help": "Independent SSR range: 16, 32, 64, 128 or 256 blocks. Default: 64 blocks. Limited to visible terrain and mesh budget."
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
{
|
||||
"sanctuary.shaders.ssr_intensity_value": "Intensité des reflets : %s %%",
|
||||
"sanctuary.shaders.ssr_intensity_help": "Intensité des reflets du paysage visible, sur les solides et l’eau. Défaut : 20 %.",
|
||||
"sanctuary.shaders.ssr": "SSR",
|
||||
"sanctuary.shaders.ssr_help": "Colore les pixels de matière avec le paysage visible. PBR requis ; les objets hors écran ne sont pas réfléchis.",
|
||||
"sanctuary.shaders.ssr_intensity": "Intensité des reflets",
|
||||
"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.",
|
||||
@@ -267,21 +272,6 @@
|
||||
"sanctuary.progression.choose_color": "Choisis ta couleur",
|
||||
"sanctuary.progression.cancel": "Annuler",
|
||||
"sanctuary.progression.enter": "Entrer dans Sanctuary",
|
||||
"sanctuary.progression.code": "Code d’accès",
|
||||
"sanctuary.progression.code_hint": "SANC-XXXX-XXXX",
|
||||
"sanctuary.progression.code_recognized": "Code reconnu.",
|
||||
"sanctuary.progression.code_checking": "Vérification du code…",
|
||||
"sanctuary.progression.code_malformed": "Le code doit ressembler à SANC-XXXX-XXXX.",
|
||||
"sanctuary.progression.code_invalid": "Ce code ne correspond pas à ce pseudo.",
|
||||
"sanctuary.progression.code_used": "Ce code a déjà été utilisé.",
|
||||
"sanctuary.progression.code_replay": "Ce code est déjà consommé et Sanctuary ne peut pas retrouver le compte. Réessaie plus tard.",
|
||||
"sanctuary.progression.code_unreachable": "Sanctuary est injoignable, réessaie.",
|
||||
"sanctuary.progression.code_unauthorized": "Le serveur ne peut pas joindre Sanctuary.",
|
||||
"sanctuary.progression.code_unconfigured": "Le serveur n’a pas de jeton Sanctuary.",
|
||||
"sanctuary.progression.code_required": "Entre le code reçu en message privé.",
|
||||
"sanctuary.progression.code_username": "Ce pseudo ne peut pas recevoir de code.",
|
||||
"sanctuary.progression.code_mismatch": "Le code ne correspond pas à cette session.",
|
||||
"sanctuary.progression.access_version_required": "Ce serveur exige le code d’accès Sanctuary. Mets le client à jour.",
|
||||
"sanctuary.progression.joining": "Création enregistrée, arrivée en cours…",
|
||||
"sanctuary.progression.bio_invalid": "La bio doit contenir du texte simple (256 caractères).",
|
||||
"sanctuary.progression.color_taken": "Couleur déjà prise. Choisis-en une autre.",
|
||||
@@ -2312,9 +2302,9 @@
|
||||
"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 à 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 %%",
|
||||
"sanctuary.shaders.pbr_intensity": "Intensité PBR",
|
||||
"sanctuary.shaders.pbr_intensity_help": "Relief et éclats des matières, eau comprise. Défaut : 50 %.",
|
||||
"sanctuary.shaders.pbr_intensity_value": "Intensité PBR : %s %%",
|
||||
"sanctuary.shaders.lens_flare": "Lens flare · rectangles doux",
|
||||
"sanctuary.shaders.lens_flare_help": "Reflets rectangulaires du soleil et de la lune, aux bords dégradés. Les obstacles masquent les sources. Indépendant du halo de bloom.",
|
||||
"sanctuary.shaders.lens_flare_intensity": "Intensité du lens flare",
|
||||
@@ -2334,5 +2324,19 @@
|
||||
"sanctuary.shaders.colored_light_distance_value": "Lumières colorées : %s blocs",
|
||||
"sanctuary.shaders.colored_light_distance_help": "Distance d'affichage : 8 à 64 blocs. Les grandes distances utilisent plus de mémoire et de calcul. Fondu aux limites ; la portée de chaque source reste native.",
|
||||
"sanctuary.shaders.warm_torches": "Torches chaudes",
|
||||
"sanctuary.shaders.warm_torches_help": "Désactivé : les torches ordinaires conservent la température de couleur native. Activé : teinte orangée. Les autres sources gardent leurs propres couleurs."
|
||||
"sanctuary.shaders.warm_torches_help": "Désactivé : les torches ordinaires conservent la température de couleur native. Activé : teinte orangée. Les autres sources gardent leurs propres couleurs.",
|
||||
"sanctuary.shaders.natural_water": "Eau naturelle",
|
||||
"sanctuary.shaders.natural_water_help": "Teintes, transparence et brume selon le fond. OFF par défaut dans les mondes ordinaires, ON sur les îles Sanctuary. Choix mémorisé séparément pour chaque type de monde.",
|
||||
"sanctuary.shaders.water_intensity": "Intensité de l’eau",
|
||||
"sanctuary.shaders.water_intensity_help": "À 0 %, conserve le rendu natif. À 100 %, applique les trois profils.",
|
||||
"sanctuary.shaders.water_intensity_value": "Intensité de l’eau: %s%%",
|
||||
"sanctuary.shaders.pbr_distance": "Distance PBR",
|
||||
"sanctuary.shaders.pbr_distance_value": "Distance PBR: %s blocs",
|
||||
"sanctuary.shaders.pbr_distance_help": "Portée indépendante du PBR : 16, 32, 64, 128 ou 256 blocs. Défaut : 64 blocs. Limitée au terrain visible et au budget de maillage.",
|
||||
"sanctuary.shaders.water_blend": "Mélange des eaux",
|
||||
"sanctuary.shaders.water_blend_value": "Mélange des eaux : %s × %s blocs",
|
||||
"sanctuary.shaders.water_blend_help": "Étendue de la transition des teintes selon le fond : 1, 3, 5, 9 ou 17 blocs. Défaut : 9 × 9.",
|
||||
"sanctuary.shaders.ssr_distance": "Distance SSR",
|
||||
"sanctuary.shaders.ssr_distance_value": "Distance SSR: %s blocs",
|
||||
"sanctuary.shaders.ssr_distance_help": "Portée indépendante du SSR : 16, 32, 64, 128 ou 256 blocs. Défaut : 64 blocs. Limitée au terrain visible et au budget de maillage."
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
#version 330
|
||||
#extension GL_ARB_separate_shader_objects : require
|
||||
uniform sampler2D InputTexture;
|
||||
layout(location=0) in vec2 texCoord;
|
||||
layout(location=0) out vec4 fragColor;
|
||||
void main(){fragColor=vec4(texture(InputTexture,texCoord).rgb,0);}
|
||||
@@ -0,0 +1,16 @@
|
||||
#version 330
|
||||
#extension GL_ARB_separate_shader_objects : require
|
||||
uniform sampler2D Before;
|
||||
uniform sampler2D After;
|
||||
layout(location=0) in vec2 texCoord;
|
||||
layout(location=0) out vec4 fragColor;
|
||||
void main(){
|
||||
vec2 pixel=1.0/vec2(textureSize(After,0));vec3 glow=vec3(0);
|
||||
for(int y=0;y<8;y++)for(int x=0;x<8;x++){
|
||||
vec2 uv=texCoord+(vec2(x,y)-3.5)*pixel;
|
||||
vec3 a=texture(After,uv).rgb,b=texture(Before,uv).rgb;
|
||||
// A restrained halo for material light, stronger for near-white solar glints.
|
||||
glow+=max(a*a-b*b,vec3(0))*(.12+.28*smoothstep(.75,1.0,max(a.r,max(a.g,a.b))));
|
||||
}
|
||||
fragColor=vec4(glow/64.0,0);
|
||||
}
|
||||
@@ -25,6 +25,8 @@ layout(std140) uniform PbrScene {
|
||||
vec4 PbrParams;
|
||||
vec4 PbrFog;
|
||||
vec4 PbrEnvironment;
|
||||
vec4 PbrSunColour;
|
||||
vec4 PbrWaterOrigin;
|
||||
};
|
||||
layout(std140) uniform PixelShadows {
|
||||
mat4 InverseViewProjection;
|
||||
@@ -89,38 +91,61 @@ 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=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 roughness=max(material.b,.16),alpha=roughness*roughness,a2=alpha*alpha;
|
||||
float denom=nh*nh*(a2-1.0)+1.0,distribution=a2/max(3.14159265*denom*denom,.00000001);
|
||||
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);
|
||||
// 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);
|
||||
vec3 f0=mix(vec3(maskUv.y>=4.0?.02:.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(){
|
||||
// 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;
|
||||
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;
|
||||
// Exact receiver depth is tested by the GPU; final scene depth also masks entities.
|
||||
float visibleDepth=texelFetch(WorldDepth,ivec2(gl_FragCoord.xy),0).r;
|
||||
vec2 screenUv=gl_FragCoord.xy/vec2(textureSize(WorldDepth,0));
|
||||
vec4 visiblePoint=PbrInverseVP*vec4(screenUv*2.0-1.0,PbrParams.y>.5?visibleDepth:visibleDepth*2.0-1.0,1);
|
||||
// Native terrain and material matrices have different evaluation order. Use a
|
||||
// bounded 5 mm tolerance only for native occlusion; receiver selection stays exact.
|
||||
if((visibleDepth>0.0&&length(visiblePoint.xyz/visiblePoint.w)+.005<length(relativePosition))||length(relativePosition)>=PbrParams.w||abs(det)<1e-12)discard;
|
||||
// Keep atlas derivatives valid after per-fragment depth/alpha rejection.
|
||||
vec4 texel=textureGrad(BlockAtlas,blockUv,tx,ty);if(texel.a<0.99)discard;
|
||||
float family=floor(maskUv.y*.5);
|
||||
vec4 texel=textureGrad(BlockAtlas,blockUv,tx,ty);if(family<.5&&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=textureLod(MaterialAtlas,maskUv,0.0);
|
||||
vec4 material=textureLod(MaterialAtlas,vec2(maskUv.x,maskUv.y-family*2.0),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;
|
||||
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;
|
||||
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 specular=localShine(n,v,l,material,albedo);
|
||||
// A broad solar lobe gives water an extended glitter path at grazing angles.
|
||||
// Local lamps retain the material's original narrow lobe.
|
||||
vec4 solarMaterial=material;
|
||||
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=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)
|
||||
@@ -130,14 +155,42 @@ void main(){
|
||||
if(amount<=0.0001&&localAmount<=0.0001)discard;
|
||||
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;
|
||||
float relief=family<.5?(nl-max(dot(baseNormal,l),0.0))*.7:0.0;
|
||||
vec3 lit=clamp(linear*(1.0+relief*amount),0.0,1.0);
|
||||
if(localAmount>.0001){
|
||||
float localRelief=(max(dot(n,arrival.xyz),0.0)-max(dot(baseNormal,arrival.xyz),0.0))*.7;
|
||||
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);
|
||||
}
|
||||
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));
|
||||
// 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;
|
||||
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.
|
||||
// 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*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);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#version 330
|
||||
#extension GL_ARB_separate_shader_objects : require
|
||||
#include <minecraft:dynamictransforms.glsl>
|
||||
// Receiver and colour passes must produce bit-identical depth on every backend.
|
||||
invariant gl_Position;
|
||||
layout(location=0) in vec3 Position;
|
||||
layout(location=1) in vec4 Color;
|
||||
layout(location=2) in vec2 UV0;
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
#version 330
|
||||
#extension GL_ARB_separate_shader_objects : require
|
||||
uniform sampler2D OpaqueDepth;
|
||||
layout(std140) uniform ReflectionScene { mat4 ReflectionVP; mat4 ReflectionInverseVP; vec4 ReflectionParams; vec4 ReflectionLimits; };
|
||||
uniform sampler2D BlockAtlas;
|
||||
layout(location=0) in vec3 relativePosition;
|
||||
layout(location=1) in vec2 blockUv;
|
||||
layout(location=2) in vec2 maskUv;
|
||||
layout(location=3) in vec4 tint;
|
||||
void main(){
|
||||
vec2 dx=dFdx(blockUv),dy=dFdy(blockUv);
|
||||
if(length(relativePosition)>=ReflectionLimits.z)discard;
|
||||
// Minecraft's world projection uses reverse depth: zero is the far plane.
|
||||
float opaque=texelFetch(OpaqueDepth,ivec2(gl_FragCoord.xy),0).r;
|
||||
// Native and material transforms differ slightly in evaluation order. A fixed
|
||||
// depth-buffer epsilon becomes too strict near the camera (reverse-Z).
|
||||
// Compare world distances with the same bounded margin as the colour passes;
|
||||
// the receiver attachment still selects the nearest material with exact depth.
|
||||
vec2 uv=gl_FragCoord.xy/vec2(textureSize(OpaqueDepth,0));
|
||||
vec4 visible=ReflectionInverseVP*vec4(uv*2.0-1.0,ReflectionParams.x>.5?opaque:opaque*2.0-1.0,1);
|
||||
if(opaque>0.0&&length(visible.xyz/visible.w)+.005<length(relativePosition))discard;
|
||||
if(maskUv.y<2.0&&textureGrad(BlockAtlas,blockUv,dx,dy).a<.99)discard;
|
||||
}
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
#version 330
|
||||
#extension GL_ARB_separate_shader_objects : require
|
||||
uniform sampler2D OpaqueDepth;
|
||||
uniform sampler2D OpaqueColor;
|
||||
uniform sampler2D ReceiverDepth;
|
||||
uniform sampler2D SceneColor;
|
||||
uniform sampler2D BlockAtlas;
|
||||
uniform sampler2D MaterialAtlas;
|
||||
layout(std140) uniform ReflectionScene {
|
||||
mat4 ReflectionVP;
|
||||
mat4 ReflectionInverseVP;
|
||||
vec4 ReflectionParams; // zero-to-one, strength, normal strength, receiver distance
|
||||
vec4 ReflectionLimits; // ray distance, steps, reserved
|
||||
};
|
||||
layout(location=0) in vec3 relativePosition;
|
||||
layout(location=1) in vec2 blockUv;
|
||||
layout(location=2) in vec2 maskUv;
|
||||
layout(location=3) in vec4 tint;
|
||||
layout(location=0) out vec4 fragColor;
|
||||
vec3 reconstruct(vec2 uv,float d){
|
||||
vec4 p=ReflectionInverseVP*vec4(uv*2.0-1.0,ReflectionParams.x>.5?d:d*2.0-1.0,1);
|
||||
return p.xyz/p.w;
|
||||
}
|
||||
bool sampleRay(vec3 origin,vec3 direction,float t,out vec2 uv,out float gap){
|
||||
vec3 p=origin+direction*t;vec4 clip=ReflectionVP*vec4(p,1);
|
||||
if(clip.w<=.001)return false;
|
||||
uv=clip.xy/clip.w*.5+.5;
|
||||
if(any(lessThanEqual(uv,vec2(0)))||any(greaterThanEqual(uv,vec2(1))))return false;
|
||||
float depth=textureLod(OpaqueDepth,uv,0).r;
|
||||
if(depth<=0.0){gap=-1e6;return true;}
|
||||
float rayDepth=ReflectionParams.x>.5?clip.z/clip.w:clip.z/clip.w*.5+.5;
|
||||
gap=depth-rayDepth;return true;
|
||||
}
|
||||
vec4 trace(vec3 p,vec3 n,vec3 geometricNormal){
|
||||
vec3 direction=reflect(normalize(p),n),origin=p+n*.035;
|
||||
// A perturbed normal must not send the ray back through its own geometric face.
|
||||
if(dot(direction,geometricNormal)<=.0001)return vec4(0);
|
||||
vec4 a=ReflectionVP*vec4(origin,1),b=ReflectionVP*vec4(origin+direction*ReflectionLimits.x,1);
|
||||
if(a.w<=.001)return vec4(0);
|
||||
float distance=ReflectionLimits.x;
|
||||
if(b.w<=.01){distance*=clamp((a.w-.01)/(a.w-b.w),0.0,1.0);b=ReflectionVP*vec4(origin+direction*distance,1);}
|
||||
if(distance<.1)return vec4(0);
|
||||
// Perspective-correct interpolation spaces the bounded march in screen coordinates.
|
||||
vec2 start=a.xy/a.w*.5+.5,end=b.xy/b.w*.5+.5,delta=end-start;
|
||||
float screenLimit=1.0;
|
||||
if(delta.x>0.0)screenLimit=min(screenLimit,(.9999-start.x)/delta.x);
|
||||
if(delta.x<0.0)screenLimit=min(screenLimit,(.0001-start.x)/delta.x);
|
||||
if(delta.y>0.0)screenLimit=min(screenLimit,(.9999-start.y)/delta.y);
|
||||
if(delta.y<0.0)screenLimit=min(screenLimit,(.0001-start.y)/delta.y);
|
||||
if(screenLimit<=0.0)return vec4(0);
|
||||
// Adaptive coarse march: large landscape silhouettes need fewer probes than screen pixels.
|
||||
// The four refinements and final intersection interpolation preserve a sharp hit colour.
|
||||
int steps=int(clamp(ceil(length(delta*screenLimit*vec2(textureSize(OpaqueDepth,0)))/128.0),8.0,32.0));
|
||||
float previous=0.0,oldGap=-1e6;vec2 hit=vec2(0);float gap;
|
||||
for(int i=1;i<=32;i++){
|
||||
if(i>steps)break;
|
||||
float f=float(i)/float(steps)*screenLimit;
|
||||
float t=(f*distance/b.w)/mix(1.0/a.w,1.0/b.w,f);
|
||||
vec2 uv;if(!sampleRay(origin,direction,t,uv,gap))return vec4(0);
|
||||
if(gap>=0.0&&oldGap<0.0){
|
||||
float lo=previous,hi=t,lowGap=oldGap,highGap=gap;
|
||||
for(int j=0;j<4;j++){
|
||||
float mid=(lo+hi)*.5;vec2 q;float delta;
|
||||
if(!sampleRay(origin,direction,mid,q,delta))return vec4(0);
|
||||
if(delta>=0.0){hi=mid;highGap=delta;}else{lo=mid;lowGap=delta;}
|
||||
}
|
||||
hi=mix(lo,hi,clamp(-lowGap/max(highGap-lowGap,1e-12),0.0,1.0));
|
||||
if(!sampleRay(origin,direction,hi,hit,gap))return vec4(0);
|
||||
// A bounded thickness rejects a silhouette jump into unrelated distant geometry.
|
||||
float thickness=.15+.002*length(origin+direction*hi);
|
||||
gap=length(origin+direction*hi)-length(reconstruct(hit,textureLod(OpaqueDepth,hit,0).r));
|
||||
if(abs(gap)>thickness||hi<.08)return vec4(0);
|
||||
float edge=min(min(hit.x,hit.y),min(1.0-hit.x,1.0-hit.y));
|
||||
float confidence=smoothstep(0.0,.04,edge)*(1.0-smoothstep(distance*.85,distance,hi))
|
||||
*(1.0-smoothstep(thickness*.65,thickness,abs(gap)))*smoothstep(.08,.2,hi);
|
||||
return vec4(hit,hi,confidence);
|
||||
}
|
||||
previous=t;oldGap=gap;
|
||||
}
|
||||
return vec4(0);
|
||||
}
|
||||
void main(){
|
||||
vec3 dx=dFdx(relativePosition),dy=dFdy(relativePosition);
|
||||
vec2 mx=dFdx(maskUv),my=dFdy(maskUv),tx=dFdx(blockUv),ty=dFdy(blockUv);
|
||||
float det=mx.x*my.y-mx.y*my.x;
|
||||
float family=floor(maskUv.y*.5);
|
||||
vec2 materialUv=vec2(maskUv.x,maskUv.y-family*2.0);
|
||||
vec3 base=normalize(cross(dx,dy));if(dot(base,-relativePosition)<0.0)base=-base;
|
||||
if(abs(det)<1e-14||length(relativePosition)>=ReflectionParams.w)discard;
|
||||
// The final native depth includes entities rendered after the landscape capture.
|
||||
float finalDepth=texelFetch(ReceiverDepth,ivec2(gl_FragCoord.xy),0).r;
|
||||
vec2 screenUv=gl_FragCoord.xy/vec2(textureSize(ReceiverDepth,0));
|
||||
if(finalDepth>0.0&&length(reconstruct(screenUv,finalDepth))+.005<length(relativePosition))discard;
|
||||
float intensity=ReflectionParams.z;
|
||||
if(intensity<=0.0)discard;
|
||||
vec4 texel=textureGrad(BlockAtlas,blockUv,tx,ty);if(family<.5&&texel.a<.99)discard;
|
||||
vec2 center=(floor(materialUv*1280.0)+.5)/1280.0;
|
||||
vec3 u=(dx*my.y-dy*mx.y)/det,v=(dy*mx.x-dx*my.x)/det;
|
||||
vec3 point=relativePosition+u*(center.x-materialUv.x)+v*(center.y-materialUv.y);
|
||||
vec4 material=textureLod(MaterialAtlas,center,0);
|
||||
vec2 slope=(material.rg*255.0-128.0)/127.0*intensity*(family>1.5?.24:2.0);
|
||||
vec3 normal=normalize(base+normalize(u)*slope.x+normalize(v)*slope.y);
|
||||
if(any(isnan(normal))||any(isinf(normal)))discard;
|
||||
vec4 hit=trace(point,normal,base);if(hit.w<=0.0)discard;
|
||||
vec3 reflected=textureLod(OpaqueColor,hit.xy,0).rgb;
|
||||
if(material.b>.3){
|
||||
vec2 stepUv=vec2(material.b*3.0)/vec2(textureSize(OpaqueColor,0));
|
||||
float reference=length(reconstruct(hit.xy,textureLod(OpaqueDepth,hit.xy,0).r));
|
||||
vec3 sum=vec3(0);float weight=0;
|
||||
for(int i=0;i<4;i++){
|
||||
vec2 uv=hit.xy+vec2(i%2==0?-1:1,i<2?-1:1)*stepUv;
|
||||
float d=textureLod(OpaqueDepth,uv,0).r;
|
||||
if(d>0.0&&abs(length(reconstruct(uv,d))-reference)<.25+reference*.002){sum+=textureLod(OpaqueColor,uv,0).rgb;weight+=1.0;}
|
||||
}
|
||||
if(weight>0.0)reflected=sum/weight;
|
||||
}
|
||||
float nv=clamp(dot(normal,normalize(-point)),0.0,1.0),grazing=1.0-nv,g2=grazing*grazing;
|
||||
// Metal tint is sampled at the same texel centre, never at the individual screen fragment.
|
||||
|
||||
vec2 localDelta=center-materialUv;
|
||||
vec2 centerBlock=blockUv+(tx*my.y-ty*mx.y)/det*localDelta.x+(ty*mx.x-tx*my.x)/det*localDelta.y;
|
||||
vec3 albedo=textureLod(BlockAtlas,centerBlock,0).rgb*tint.rgb;
|
||||
vec3 f0=mix(vec3(family>1.5?.02:.04),albedo*albedo,material.a);
|
||||
vec3 fresnel=f0+(1.0-f0)*(g2*g2*grazing);
|
||||
float fade=(1.0-smoothstep(ReflectionParams.w*.75,ReflectionParams.w,length(point)))*hit.w;
|
||||
vec3 weight=clamp(fresnel*ReflectionParams.y*(1.0-material.b*material.b)*fade,0.0,.9);
|
||||
vec3 scene=texelFetch(SceneColor,ivec2(gl_FragCoord.xy),0).rgb;
|
||||
vec3 result=mix(scene*scene,reflected*reflected,weight);
|
||||
if(any(isnan(result))||any(isinf(result)))discard;
|
||||
fragColor=vec4(sqrt(clamp(result,0.0,1.0)),1);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"light": ["minecraft:sand", "minecraft:red_sand", "minecraft:gravel", "minecraft:clay"],
|
||||
"pond": ["minecraft:dirt", "minecraft:coarse_dirt", "minecraft:rooted_dirt", "minecraft:grass_block", "minecraft:podzol", "minecraft:mycelium", "minecraft:mud", "minecraft:muddy_mangrove_roots"]
|
||||
}
|
||||
@@ -184,6 +184,9 @@
|
||||
"NaturalSnowEditMixin"
|
||||
],
|
||||
"client": [
|
||||
"client.ReflectionSceneMixin",
|
||||
"client.PbrAnimationClockMixin",
|
||||
"client.PbrAnimationSpriteMixin",
|
||||
"client.PackSelectionScreenMixin",
|
||||
"client.FourneauFireMixin",
|
||||
"client.BoatPassengerPickMixin",
|
||||
@@ -210,6 +213,7 @@
|
||||
"client.AbstractContainerScreenAccessor",
|
||||
"client.SlotPositionAccessor",
|
||||
"client.InventoryScreenMixin",
|
||||
"client.InventoryRespawnMixin",
|
||||
"client.InventoryEquipmentTooltipMixin",
|
||||
"client.RecipeBookScreenMixin",
|
||||
"client.RecipeBookComponentMixin",
|
||||
@@ -260,6 +264,7 @@
|
||||
"client.InventoryPointerAccess",
|
||||
"client.OperatorWorldOptionsMixin",
|
||||
"client.StarSkyMixin",
|
||||
"client.SolarColourMixin",
|
||||
"client.ProgressiveFogMixin",
|
||||
"client.ShaderSaturationMixin",
|
||||
"client.WeatherEnvironmentMixin",
|
||||
@@ -278,7 +283,12 @@
|
||||
"client.PixelShadowRendererMixin",
|
||||
"client.PixelShadowLevelMixin",
|
||||
"client.PixelShadowDirtyMixin",
|
||||
"client.PreferredGraphicsApiMixin"
|
||||
"client.PreferredGraphicsApiMixin",
|
||||
"client.NaturalWaterRegionMixin",
|
||||
"client.NaturalWaterFluidMixin",
|
||||
"client.NaturalWaterFogMixin",
|
||||
"client.NaturalWaterFogColorMixin",
|
||||
"client.NaturalWaterSectionCopyAccess"
|
||||
],
|
||||
"injectors": {
|
||||
"defaultRequire": 1
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
package fr.koka.sanctuary.progression;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class AccessCodeSmoke {
|
||||
public static void main(String[] args) throws Exception {
|
||||
require(AccessCodes.normalize(" sanc-ab3d-k7pq ").equals("SANC-AB3D-K7PQ"), "Codes are trimmed and uppercased");
|
||||
require(AccessCodes.validFormat("SANC-AB3D-K7PQ"), "Documented code shape is accepted");
|
||||
require(!AccessCodes.validFormat("SANC-AB3D-K7P") && !AccessCodes.validFormat("SANC-AB3D-K7PQX"), "Partial codes are refused");
|
||||
require(AccessCodes.validUsername("Steve") && !AccessCodes.validUsername("A") && !AccessCodes.validUsername("bad name"), "Session names stay Minecraft names");
|
||||
var unused = AccessCodes.parse(200, "{\"valid\":true,\"reason\":\"unused\",\"minecraft_username\":\"Steve\",\"discord_id\":\"123456789012345678\"}");
|
||||
require(AccessCodes.verifyKey(unused, "steve").equals("code_recognized"), "Verify matches the session name without consuming the code");
|
||||
require(AccessCodes.verifyKey(AccessCodes.parse(200, "{\"valid\":false,\"reason\":\"invalid\"}"), "Steve").equals("code_invalid"), "Unknown code stays on the screen");
|
||||
require(AccessCodes.verifyKey(AccessCodes.parse(200, "{\"valid\":false,\"reason\":\"already_used\"}"), "Steve").equals("code_used"), "Used code is not a fresh verification");
|
||||
require(AccessCodes.parse(401, "").reason().equals("unauthorized"), "A bad token is not a character");
|
||||
require(AccessCodes.parse(503, "nope").reason().equals("unreachable"), "An outage does not invent a character");
|
||||
var redeemed = AccessCodes.parse(200, "{\"valid\":true,\"reason\":\"redeemed\",\"minecraft_username\":\"Steve\",\"discord_id\":\"42\"}");
|
||||
var decision = AccessCodes.redeem(redeemed, "Steve");
|
||||
require(decision.accepted() && decision.discordId().equals("42"), "Redeem is the commitment");
|
||||
require(!AccessCodes.redeem(redeemed, "Alex").accepted(), "Another username cannot use the code");
|
||||
var replay = AccessCodes.parse(200, "{\"valid\":false,\"reason\":\"already_used\",\"minecraft_username\":\"Steve\",\"discord_id\":\"42\"}");
|
||||
require(AccessCodes.redeem(replay, "Steve").accepted(), "A consumed code can finish an interrupted creation when the account comes back");
|
||||
var stuck = AccessCodes.parse(200, "{\"valid\":false,\"reason\":\"already_used\"}");
|
||||
require(AccessCodes.redeem(stuck, "Steve").reason().equals("code_replay"), "A consumed code without an account does not create anyone");
|
||||
|
||||
var path = Files.createTempDirectory("sanctuary-access-").resolve("access.json");
|
||||
UUID id = UUID.fromString("01234567-89ab-cdef-fedc-ba9876543210");
|
||||
var ledger = new AccessLedger(path, 42L);
|
||||
ledger.remember(id, "Steve", "42");
|
||||
var reopened = new AccessLedger(path, 42L);
|
||||
require(reopened.known(id).discordId().equals("42"), "A crash after redeem keeps the Discord account");
|
||||
reopened.commit(id);
|
||||
var committed = new AccessLedger(path, 42L);
|
||||
require(committed.known(id).name().equals("Steve"), "The binding survives the character write");
|
||||
committed.commit(id);
|
||||
committed.remember(id, "Steve", "42");
|
||||
String saved = Files.readString(path);
|
||||
require(saved.indexOf(id.toString()) == saved.lastIndexOf(id.toString()), "An existing character is not returned to pending");
|
||||
byte[] broken = "{".getBytes();
|
||||
Files.write(path, broken);
|
||||
boolean refused = false;
|
||||
try { new AccessLedger(path, 42L); } catch (IllegalArgumentException error) { refused = true; }
|
||||
require(refused && Files.readAllBytes(path).length == 1, "A corrupt ledger is preserved and refused");
|
||||
System.out.println("ACCESS-01: code contract, replay and ledger passed");
|
||||
}
|
||||
private static void require(boolean value, String message) { if (!value) throw new AssertionError(message); }
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
name = "Sanctuary"
|
||||
author = "KOKA99CAB"
|
||||
version = "beta.144"
|
||||
version = "beta.151"
|
||||
pack-format = "packwiz:1.1.0"
|
||||
|
||||
[index]
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check the eight alternating water benchmark passages per scene.
|
||||
|
||||
Usage: python3 scripts/check_water_performance.py build/water144-vulkan.log
|
||||
Writes the detailed receipt beside the log, and rejects incomplete/over-budget runs.
|
||||
The caller must also verify that no other graphics test ran during measurement.
|
||||
"""
|
||||
import json,re,sys
|
||||
from pathlib import Path
|
||||
log_path=Path(sys.argv[1])
|
||||
backend=log_path.stem
|
||||
log=log_path.read_text()
|
||||
rows={key:json.loads(data) for key,data in re.findall(r"WATER144_PERF (\S+) (\[[^\n]+\])",log)}
|
||||
assert len(rows)==24, ("Incomplete measurements",len(rows))
|
||||
result={"backend":backend,"samples":rows,"scenes":{}}
|
||||
for scene in ("mixed","large","moving"):
|
||||
off=[v for k,v in rows.items() if k.startswith(scene+"-false-")]
|
||||
on=[v for k,v in rows.items() if k.startswith(scene+"-true-")]
|
||||
assert len(off)==len(on)==4 and all(v[0]>50 for v in off+on)
|
||||
median_off=sum(v[1] for v in off)/len(off);median_on=sum(v[1] for v in on)/len(on)
|
||||
p95_off=sum(v[2] for v in off)/len(off);p95_on=sum(v[2] for v in on)/len(on)
|
||||
value={"offMedianMs":median_off,"onMedianMs":median_on,"medianIncreasePercent":100*(median_on/median_off-1),"offP95Ms":p95_off,"onP95Ms":p95_on,"p95IncreaseMs":p95_on-p95_off}
|
||||
value["passed"]=value["medianIncreasePercent"]<=5 and value["p95IncreaseMs"]<=2
|
||||
result["scenes"][scene]=value
|
||||
log_path.with_name(log_path.stem+"-performance.json").write_text(json.dumps(result,indent=2)+"\n")
|
||||
print(json.dumps(result["scenes"],indent=2))
|
||||
assert all(v["passed"] for v in result["scenes"].values()), "Performance acceptance not met"
|
||||
@@ -8,3 +8,10 @@ ajoute les ombres portées pixélisées optionnelles, configurables depuis
|
||||
Ce dossier reste réservé aux futurs packs externes. Leur ajout au manifeste
|
||||
packwiz nécessitera version, URL, hash, licence et compatibilité vérifiée pour
|
||||
la version exacte du jeu et du moteur Iris.
|
||||
|
||||
La beta.144 ajoute l’[eau naturelle](../docs/natural-water-beta144.md) dans
|
||||
le rendu natif : profils selon le fond, océans clairs et brume sous-marine.
|
||||
|
||||
La livraison locale beta.145 ajoute le [PBR de l’eau et du verre ainsi que les
|
||||
reflets pixélisés](../docs/pbr-ssr.md). SSR désactivé par défaut ; aucune
|
||||
dépendance graphique externe.
|
||||
|
||||
Reference in New Issue
Block a user