Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b3eb791fe | ||
|
|
83c377acc1 | ||
|
|
7eef9454d4 |
@@ -20,14 +20,20 @@ La [vision complète](docs/vision.md) conserve les intentions ; le
|
|||||||
L’économie, les claims et les dimensions décrits dans la vision restent
|
L’économie, les claims et les dimensions décrits dans la vision restent
|
||||||
à implémenter.
|
à implémenter.
|
||||||
|
|
||||||
|
La version **beta.144** ajoute une option d’**eau naturelle pixélisée** :
|
||||||
|
couleur du biome, trois profils selon le fond, vert olive d’étang 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).
|
||||||
|
La beta.144 est publiée et synchronisée dans **Sanctuary Beta** ;
|
||||||
|
les 923 fichiers personnels suivis sont conservés.
|
||||||
|
|
||||||
La version **beta.143** garde le sampler natif de précipitation pour la neige
|
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
|
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
|
facteur 12 et la fonte par le facteur temps réel 72. Les protections existantes
|
||||||
restent inchangées : neige construite, biomes froids, hiver, profils neigeux
|
restent inchangées : neige construite, biomes froids, hiver, profils neigeux
|
||||||
et météo Vanilla. Le [ticket](docs/realtime-snow-melt-beta143.md) documente le
|
et météo Vanilla. Le [ticket](docs/realtime-snow-melt-beta143.md) documente le
|
||||||
contrat et les vérifications.
|
contrat et les vérifications.
|
||||||
La beta.143 est publiée et synchronisée dans l’instance Sanctuary Beta ;
|
|
||||||
les 923 fichiers personnels suivis sont conservés.
|
|
||||||
La [livraison précédente](docs/clay-workshop-integration-beta110.md) réunit déjà
|
La [livraison précédente](docs/clay-workshop-integration-beta110.md) réunit déjà
|
||||||
l’atelier, les œufs et la neige saisonnière dans l’instance **Sanctuary Beta**.
|
l’atelier, les œufs et la neige saisonnière dans l’instance **Sanctuary Beta**.
|
||||||
Les [synchronisations antérieures](docs/git-sync-beta092.md) restent documentées.
|
Les [synchronisations antérieures](docs/git-sync-beta092.md) restent documentées.
|
||||||
@@ -38,6 +44,16 @@ La [carte native beta.005](docs/atlas-beta005.md) prend le relais après
|
|||||||
l’étude de Xaero et des alternatives libres.
|
l’étude de Xaero et des alternatives libres.
|
||||||
|
|
||||||
|
|
||||||
|
## beta.144 — Eau naturelle pixélisée
|
||||||
|
|
||||||
|
Eau claire, légèrement teintée ou vert olive d’étang selon le fond, avec
|
||||||
|
textures et animation natives. Océans clairs, visibilité sous-marine adaptée,
|
||||||
|
interrupteur et intensité dans les options Shaders.
|
||||||
|
[Contrat et mesures](docs/natural-water-beta144.md) ·
|
||||||
|
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.144/Sanctuary-beta.144.mrpack) ·
|
||||||
|
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.144/Sanctuary-Test-beta.144.mrpack) ·
|
||||||
|
[Tous les livrables](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.144).
|
||||||
|
|
||||||
## beta.143 — Neige saisonnière en temps réel
|
## beta.143 — Neige saisonnière en temps réel
|
||||||
|
|
||||||
Accumulation ralentie par un facteur 12 et fonte saisonnière ralentie par le
|
Accumulation ralentie par un facteur 12 et fonte saisonnière ralentie par le
|
||||||
|
|||||||
@@ -0,0 +1,209 @@
|
|||||||
|
# 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. Aux bords océaniques, les sommets partagés restent clairs et la
|
||||||
|
transition occupe le bloc voisin hors océan. 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. La présence du terrain est vérifiée dans le cache client avec chargement
|
||||||
|
désactivé, sans utiliser `ClientLevel.hasChunk` (toujours vrai). Aucun
|
||||||
|
chargement de chunk n’est demandé par le classificateur.
|
||||||
|
|
||||||
|
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 finales OpenGL et Vulkan sont réussies le 18 septembre
|
||||||
|
2026 (`WATER144_PASS` et `WATER144_EXTRAS_PASS`). Elles couvrent surface,
|
||||||
|
immersion, fond modifié, priorité océan, toit, plantes, cascades, lave inchangée,
|
||||||
|
blocs gorgés d’eau, préférences FR/EN et rechargement des ressources/chunks.
|
||||||
|
Les sommets partagés aux limites océaniques et le repli clair lorsque le cache
|
||||||
|
client ne contient pas le terrain font l’objet d’assertions supplémentaires.
|
||||||
|
Aucun autre client de test concurrent n’a été détecté pendant ces séries.
|
||||||
|
Deux passages ciblés supplémentaires (`WATER144_TEXTURE_PASS`), Vulkan en
|
||||||
|
1 min 18 s et OpenGL en 1 min 4 s, chargent de vraies textures personnelles
|
||||||
|
32×32 à deux images animées pour l’eau calme et courante. Grille, animation
|
||||||
|
visible et retour aux textures natives sont vérifiés après désactivation.
|
||||||
|
Les fixtures, leurs alpha et les captures restent dans les preuves locales.
|
||||||
|
|
||||||
|
### Protocole de performance
|
||||||
|
|
||||||
|
MacBookPro17,1, Apple M1, 8 Gio, macOS 26.5.1 (25F80), Java 25,
|
||||||
|
fenêtre logique 854×480, cible Retina **1708×960 pixels**, 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.
|
||||||
|
Le mod de test vérifie le limiteur effectif et journalise le mode de présentation.
|
||||||
|
Pendant les échantillons uniquement, il omet l’attente artificielle de
|
||||||
|
`FramerateLimiter.limitDisplayFPS`, afin de mesurer sans temporisation
|
||||||
|
applicative. Cette sonde reste absente du JAR de production. Les temps couvrent
|
||||||
|
la frame native, présentation comprise, et ne sont pas des temps GPU isolés.
|
||||||
|
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.
|
||||||
|
La scène de déplacement prolonge le bassin hors de la vue initiale : son terrain
|
||||||
|
est préparé côté serveur de test, puis découvert normalement par le client.
|
||||||
|
Le vérificateur exige des compilations réelles dans chaque passage en mouvement.
|
||||||
|
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, les exécutions concurrentes et les vérifications
|
||||||
|
incomplètes du limiteur restent conservés comme preuves de développement ;
|
||||||
|
ils ne fondent pas ces résultats. Les séries finales ont été exécutées après
|
||||||
|
la fin du chantier SSR. Les variations négatives sont du bruit de mesure,
|
||||||
|
pas une promesse d’accélération. Vulkan reste proche de 120 images/s dans
|
||||||
|
cette chaîne de présentation, malgré le mode `IMMEDIATE` et l’absence de
|
||||||
|
limiteur applicatif : les conclusions sont limitées à la machine et aux
|
||||||
|
conditions ci-dessus.
|
||||||
|
|
||||||
|
| Moteur / scène | Médiane OFF → ON (ms) | Variation | P95 OFF → ON (ms) | Δ P95 (ms) |
|
||||||
|
| --- | --- | ---: | --- | ---: |
|
||||||
|
| vulkan / mixed | 8.069 → 8.092 | +0.29 % | 9.091 → 9.101 | +0.010 |
|
||||||
|
| vulkan / large | 8.078 → 8.062 | -0.20 % | 9.126 → 9.114 | -0.012 |
|
||||||
|
| vulkan / moving | 8.123 → 8.103 | -0.24 % | 9.103 → 9.080 | -0.023 |
|
||||||
|
| opengl / mixed | 4.545 → 4.407 | -3.02 % | 7.080 → 6.733 | -0.347 |
|
||||||
|
| opengl / large | 4.879 → 5.110 | +4.73 % | 7.519 → 7.767 | +0.248 |
|
||||||
|
| opengl / moving | 6.817 → 6.338 | -7.02 % | 10.618 → 9.759 | -0.859 |
|
||||||
|
|
||||||
|
Les six comparaisons respectent les seuils : médiane ≤ +5 %, P95 ≤ +2 ms.
|
||||||
|
Le cas limitant est la grande surface OpenGL (+4,73 %, +0,248 ms).
|
||||||
|
Les compilations de sections sont mesurées sur le parcours en mouvement :
|
||||||
|
|
||||||
|
| Moteur | Sections OFF / ON | Médiane OFF → ON (ms) | P95 OFF → ON (ms) |
|
||||||
|
| --- | ---: | --- | --- |
|
||||||
|
| vulkan | 222 / 220 | 1.889 → 2.186 | 4.307 → 4.898 |
|
||||||
|
| opengl | 222 / 220 | 1.528 → 1.540 | 5.492 → 5.227 |
|
||||||
|
|
||||||
|
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é.
|
||||||
|
|
||||||
|
L’assemblage avec l’exclusion ci-dessus a réussi en 2 min 33 s (126 tâches).
|
||||||
|
`python3 scripts/pack.py check` réussit. Les cinq archives passent le contrôle
|
||||||
|
ZIP ; les deux MRpack embarquent exactement le même JAR Sanctuary et déclarent
|
||||||
|
Fabric API 0.160.5+26.3 avec l’empreinte du manifeste source. L’export MRpack est
|
||||||
|
exécuté depuis chaque dossier de staging packwiz. Les dépendances imbriquées
|
||||||
|
Demeure et JEI sont vérifiées par l’assemblage natif du dépôt.
|
||||||
|
|
||||||
|
[Release beta.144](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.144) publiée depuis
|
||||||
|
le commit source `83c377acc1b5a4d4137d7a93c397531f77915303` ; tag exact `beta.144`.
|
||||||
|
Canal packwiz stable : `c8e28d878a55703bdc28fc046242b42b2fcfa9f6`.
|
||||||
|
|
||||||
|
Deux synchronisations isolées puis deux synchronisations de l’instance
|
||||||
|
**Sanctuary Beta** ont réussi, jeu et lanceur fermés. Les secondes passes sont
|
||||||
|
idempotentes. Un seul JAR Sanctuary est actif, et son SHA-256 correspond
|
||||||
|
exactement à celui de la release et du manifeste stable.
|
||||||
|
Les **923 fichiers personnels** suivis (sauvegardes, configurations,
|
||||||
|
packs, captures et mods non gérés) ont conservé leurs empreintes. Aucun monde
|
||||||
|
personnel ouvert. Sauvegarde des fichiers remplacés :
|
||||||
|
`sanctuary-backups/before-beta.144/`.
|
||||||
|
|
||||||
|
Les neuf liens publics (cinq livrables, trois captures, manifeste SHA-256)
|
||||||
|
ont été retéléchargés anonymement et leurs empreintes comparées. Les reçus de
|
||||||
|
publication, de téléchargement et des quatre synchronisations restent dans
|
||||||
|
`build/water144-publish.log`, `build/water144-public-downloads.json`,
|
||||||
|
`build/water144-isolated.json` et `build/water144-prism.json`.
|
||||||
|
Le checkout initial beta.122 et ses modifications restent préservés.
|
||||||
|
Les sources sont sur `codex/water-beta144`, dans le worktree isolé.
|
||||||
|
Les archives vérifiées sont aussi copiées dans `sanctuary-beta/build/`.
|
||||||
|
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/`.
|
||||||
|
|
||||||
|
## Artefacts vérifiés
|
||||||
|
|
||||||
|
| Artefact | Octets | SHA-256 |
|
||||||
|
| --- | ---: | --- |
|
||||||
|
| [sanctuary-beta.144.jar](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.144/sanctuary-beta.144.jar) | 11343257 | `1ddbe941f3262da8a1497ea5df4a992e260b80ae95b341623dd68f8f9729ae88` |
|
||||||
|
| [Sanctuary-beta.144.mrpack](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.144/Sanctuary-beta.144.mrpack) | 10483196 | `d5ef5a781f5ee796117210d1f6afc96172461315faee34097323beff541ce6f0` |
|
||||||
|
| [Sanctuary-Test-beta.144.mrpack](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.144/Sanctuary-Test-beta.144.mrpack) | 10502119 | `e7cc32ac19ae98ebbe7a9aac3dc08decdf09396c8175b597f87051399ff3812e` |
|
||||||
|
| [Sanctuary-Template-beta.144.zip](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.144/Sanctuary-Template-beta.144.zip) | 4354678 | `e510c0040dc9053492715ac91381b7dbc5a6f7c7defbf81e7502d8e18dbb0ee6` |
|
||||||
|
| [Sanctuary-Prism-auto-update.zip](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.144/Sanctuary-Prism-auto-update.zip) | 92423 | `c0920823f40d709c14e074d8d949f2cc3681c24140bf86f21c3be345cfc3e1f6` |
|
||||||
|
|
||||||
|
## Captures comparatives
|
||||||
|
|
||||||
|
Pièces jointes de la release, issues du client Vulkan de validation :
|
||||||
|
[OFF](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.144/Sanctuary-beta.144-water-off.png),
|
||||||
|
[ON](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.144/Sanctuary-beta.144-water-on.png),
|
||||||
|
[profil étang en proximité](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.144/Sanctuary-beta.144-water-pond.png).
|
||||||
|
Les captures sont exportées à 854×480 ; la cible de présentation mesurée est
|
||||||
|
1708×960. Le [manifeste SHA-256](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.144/Sanctuary-beta.144-SHA256SUMS.txt) inclut les cinq
|
||||||
|
livrables et ces trois captures. Les journaux et échantillons détaillés restent
|
||||||
|
locaux, dans `build/water144-evidence/` et les reçus `build/water144-*.json`.
|
||||||
@@ -14,6 +14,20 @@ 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
|
dépendance téléchargée n'entre dans l'historique Git. Le canal Beta est distinct
|
||||||
de l'ancien pack 26.2.
|
de l'ancien pack 26.2.
|
||||||
|
|
||||||
|
## beta.144 — Eau naturelle pixélisée
|
||||||
|
|
||||||
|
Trois profils selon le fond, teinte olive d’étang, océans natifs clairs et
|
||||||
|
brume aquatique adaptée. [Contrat, captures et mesures](natural-water-beta144.md).
|
||||||
|
[Release beta.144](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.144)
|
||||||
|
publiée depuis `83c377acc1b5a4d4137d7a93c397531f77915303` ; canal
|
||||||
|
`c8e28d878a55703bdc28fc046242b42b2fcfa9f6`. Deux synchronisations isolées puis deux
|
||||||
|
dans l’instance Sanctuary Beta réussies et idempotentes. Un seul JAR Sanctuary
|
||||||
|
actif, identique à l’artefact public. Les 923 fichiers personnels suivis gardent
|
||||||
|
leurs empreintes ; aucun monde personnel ouvert. Sauvegarde préalable :
|
||||||
|
`sanctuary-backups/before-beta.144/`. Les cinq livrables et leurs SHA-256 sont
|
||||||
|
vérifiés dans `sanctuary-beta/build/`. Textures beta.121, Minecraft 26.3 et
|
||||||
|
dépendances exactes conservés.
|
||||||
|
|
||||||
## beta.143 — Neige saisonnière en temps réel
|
## 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,
|
Accumulation espacée par un facteur 12 et fonte par le facteur temps réel 72,
|
||||||
|
|||||||
@@ -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),
|
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
|
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.
|
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.
|
||||||
|
|||||||
+2
-2
@@ -9,8 +9,8 @@ loom_version=1.17.20
|
|||||||
fabric_api_version=0.160.5+26.3
|
fabric_api_version=0.160.5+26.3
|
||||||
|
|
||||||
# Release counter: beta.001, .002, .003, ... (see docs/versioning.md).
|
# Release counter: beta.001, .002, .003, ... (see docs/versioning.md).
|
||||||
mod_version=beta.143
|
mod_version=beta.144
|
||||||
pack_version=beta.143
|
pack_version=beta.144
|
||||||
resource_pack_version=beta.121
|
resource_pack_version=beta.121
|
||||||
maven_group=fr.koka.sanctuary
|
maven_group=fr.koka.sanctuary
|
||||||
jei_version=30.32.0-sanctuary.3
|
jei_version=30.32.0-sanctuary.3
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ if (clientRenderTests) {
|
|||||||
if (improvedTransparency.isPresent()) {
|
if (improvedTransparency.isPresent()) {
|
||||||
systemProperty('sanctuary.test.improvedTransparency', improvedTransparency.get())
|
systemProperty('sanctuary.test.improvedTransparency', improvedTransparency.get())
|
||||||
}
|
}
|
||||||
|
systemProperty('sanctuary.test.water144', providers.gradleProperty('sanctuaryWater144ClientTests').getOrElse('false'))
|
||||||
|
systemProperty('sanctuary.test.water144ExtrasOnly', providers.gradleProperty('sanctuaryWater144ExtrasOnly').getOrElse('false'))
|
||||||
|
systemProperty('sanctuary.test.water144PerfOnly', providers.gradleProperty('sanctuaryWater144PerfOnly').getOrElse('false'))
|
||||||
systemProperty('sanctuary.test.pbr142', providers.gradleProperty('sanctuaryPbr142ClientTests').getOrElse('false'))
|
systemProperty('sanctuary.test.pbr142', providers.gradleProperty('sanctuaryPbr142ClientTests').getOrElse('false'))
|
||||||
systemProperty('sanctuary.test.directional141Only', providers.gradleProperty('sanctuaryDirectional141Only').getOrElse('false'))
|
systemProperty('sanctuary.test.directional141Only', providers.gradleProperty('sanctuaryDirectional141Only').getOrElse('false'))
|
||||||
systemProperty('sanctuary.test.directional141', providers.gradleProperty('sanctuaryDirectional141ClientTests').getOrElse('false'))
|
systemProperty('sanctuary.test.directional141', providers.gradleProperty('sanctuaryDirectional141ClientTests').getOrElse('false'))
|
||||||
|
|||||||
+2
-1
@@ -151,6 +151,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
|||||||
c.runOnClient(m -> check(VanillaLight.active(), "Native Sanctuary shader is active without an external engine"));
|
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);
|
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(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(sunshaftOnly){Sunshafts137ClientChecks.run(c,server);return;}
|
||||||
if(closeupOnly){SsgiCloseup135ClientChecks.run(c,server);Pbr134ClientChecks.run(c,server);return;}
|
if(closeupOnly){SsgiCloseup135ClientChecks.run(c,server);Pbr134ClientChecks.run(c,server);return;}
|
||||||
@@ -568,7 +569,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static Frame capture(ClientGameTestContext c, String name) {
|
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>();
|
var result = new CompletableFuture<Frame>();
|
||||||
c.runOnClient(m -> Screenshot.takeScreenshot(m.gameRenderer.mainRenderTarget(), image -> {
|
c.runOnClient(m -> Screenshot.takeScreenshot(m.gameRenderer.mainRenderTarget(), image -> {
|
||||||
try (image) {
|
try (image) {
|
||||||
|
|||||||
+215
@@ -0,0 +1,215 @@
|
|||||||
|
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.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")){
|
||||||
|
resourceOverride(c);
|
||||||
|
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 unloaded=new BlockPos(1_000_016,96,1_000_016);
|
||||||
|
check(!NaturalWater.loaded(m.level,unloaded.getX()>>4,unloaded.getZ()>>4),"Fixture region is unloaded");
|
||||||
|
var missing=new NaturalWater.Region(m.level,62500,5,62500);
|
||||||
|
check(missing.corner(null,unloaded,0,0).equals(NaturalWater.CLEAR),"Unavailable biome snapshot forces clear without reading missing terrain");
|
||||||
|
check(!NaturalWater.loaded(m.level,unloaded.getX()>>4,unloaded.getZ()>>4),"Classification does not load chunks");
|
||||||
|
var boundary=NaturalWater.sample(m.level,80,99.1,8.5);check(boundary.tint()>.1&&boundary.tint()<.85,"Section boundary mixes profiles");
|
||||||
|
});
|
||||||
|
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");
|
||||||
|
boolean found=false;
|
||||||
|
for(int x=72;x<89;x++){
|
||||||
|
var p=new BlockPos(x,99,8);
|
||||||
|
if(!NaturalWater.ocean(m.level,p)&&NaturalWater.ocean(m.level,p.east())){
|
||||||
|
var left=new NaturalWater.Region(m.level,(x>>4)-1,5,-1);
|
||||||
|
var right=new NaturalWater.Region(m.level,((x+1)>>4)-1,5,-1);
|
||||||
|
check(left.corner(null,p,1,0).equals(right.corner(null,p.east(),0,0)),"Ocean edge emits identical shared vertices");
|
||||||
|
found=true;break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
check(found,"Native ocean boundary exercised");
|
||||||
|
});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");
|
||||||
|
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 net.minecraft.client.renderer.texture.SpriteContents waterSprite(net.minecraft.client.Minecraft m,String name){
|
||||||
|
return m.getAtlasManager().getAtlasOrThrow(net.minecraft.data.AtlasIds.BLOCKS)
|
||||||
|
.getSprite(net.minecraft.resources.Identifier.withDefaultNamespace("block/"+name)).contents();
|
||||||
|
}
|
||||||
|
private static void resourceOverride(ClientGameTestContext c){
|
||||||
|
int originalWidth=c.computeOnClient(m->waterSprite(m,"water_still").width());
|
||||||
|
int originalFrames=c.computeOnClient(m->waterSprite(m,"water_still").getUniqueFrames().size());
|
||||||
|
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);
|
||||||
|
var textures=directory.resolve("assets/minecraft/textures/block");Files.createDirectories(textures);
|
||||||
|
try(var pixels=new com.mojang.blaze3d.platform.NativeImage(32,64,false)){
|
||||||
|
for(int y=0;y<64;y++)for(int x=0;x<32;x++)pixels.setPixel(x,y,((x/4+y/4+y/32)&1)==0?0xd0ffffff:0x80404040);
|
||||||
|
for(String name:new String[]{"water_still","water_flow"}){
|
||||||
|
pixels.writeToFile(textures.resolve(name+".png"));
|
||||||
|
Files.writeString(textures.resolve(name+".png.mcmeta"),"{\"animation\":{\"frametime\":4,\"frames\":[0,1]}}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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");});
|
||||||
|
c.runOnClient(m->{for(String name:new String[]{"water_still","water_flow"}){
|
||||||
|
var sprite=waterSprite(m,name);check(sprite.width()==32&&sprite.height()==32&&sprite.getUniqueFrames().size()==2,"Custom pixel grid and animation loaded: "+name);
|
||||||
|
}});
|
||||||
|
var first=capture(c,"custom-materials");long animated=0;
|
||||||
|
for(int i=0;i<8;i++){c.waitTicks(1);animated=Math.max(animated,difference(first,capture(c,"custom-animation-"+i)));}
|
||||||
|
check(animated>100000,"Custom water animation remains visible");
|
||||||
|
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");
|
||||||
|
check(waterSprite(m,"water_still").width()==originalWidth&&waterSprite(m,"water_still").getUniqueFrames().size()==originalFrames,"Native water texture restored");
|
||||||
|
});
|
||||||
|
System.out.println("WATER144_TEXTURE_PASS custom 32px still/flow textures, alpha, two animation frames and restoration");
|
||||||
|
}
|
||||||
|
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);m.getFramerateLimitTracker().setFramerateLimit(net.minecraft.client.Options.UNLIMITED_FRAMERATE_CUTOFF);});
|
||||||
|
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);}
|
||||||
|
if(scene.equals("moving")){
|
||||||
|
// Test fixture only: prepare water beyond the initial client view so movement really compiles terrain.
|
||||||
|
s.runOnServer(server->{
|
||||||
|
command(server,"forceload add 112 -17 288 48");
|
||||||
|
for(int x=7;x<=18;x++)for(int z=-2;z<=3;z++)server.overworld().getChunk(x,z);
|
||||||
|
command(server,"fill 112 94 -17 288 100 -17 glass");command(server,"fill 112 94 48 288 100 48 glass");
|
||||||
|
command(server,"fill 288 94 -17 288 100 48 glass");
|
||||||
|
for(int x=112;x<288;x+=16){
|
||||||
|
command(server,"fill "+x+" 93 -16 "+(x+15)+" 93 47 quartz_block");
|
||||||
|
command(server,"fill "+x+" 94 -16 "+(x+15)+" 94 47 sand");
|
||||||
|
command(server,"fill "+x+" 95 -16 "+(x+15)+" 99 47 water");
|
||||||
|
}
|
||||||
|
});c.waitTicks(80);
|
||||||
|
}
|
||||||
|
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->{
|
||||||
|
check(m.getFramerateLimitTracker().getFramerateLimit()==260&&!m.options.enableVsync().get(),"Uncapped native limiter and VSync disabled");
|
||||||
|
System.out.println("WATER144_CONDITIONS limit="+m.getFramerateLimitTracker().getFramerateLimit()+" throttle="+m.getFramerateLimitTracker().getThrottleReason()+" renderLimit="+m.gameRenderer.gameRenderState().framerateLimit+" surface="+m.windowSurface().currentConfiguration()+" limiterBypass=true");
|
||||||
|
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.getFramerateLimitTracker().setFramerateLimit(oldLimit);m.options.inactivityFpsLimit().set(oldInactivity);});}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;}
|
||||||
|
}
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
package fr.koka.sanctuary.gametest.mixin;
|
||||||
|
import fr.koka.sanctuary.gametest.Water144Metrics;
|
||||||
|
import com.llamalad7.mixinextras.injector.wrapoperation.*;
|
||||||
|
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;
|
||||||
|
// Benchmark only: the extracted frame state can retain a 120 FPS limit despite the option/tracker.
|
||||||
|
@WrapOperation(method="renderFrame",at=@At(value="INVOKE",target="Lnet/minecraft/client/FramerateLimiter;limitDisplayFPS(I)V"))
|
||||||
|
private void sanctuary$uncapped(int fps,Operation<Void> original){if(!Water144Metrics.recording)original.call(fps);}
|
||||||
|
|
||||||
|
@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);}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,8 @@
|
|||||||
"Atlas026TextureProbeMixin",
|
"Atlas026TextureProbeMixin",
|
||||||
"Inventory013MerchantAccess",
|
"Inventory013MerchantAccess",
|
||||||
"Dynamic030TerrainProbeMixin",
|
"Dynamic030TerrainProbeMixin",
|
||||||
"Star040RenderProbeMixin"
|
"Star040RenderProbeMixin",
|
||||||
|
"Water144FrameProbeMixin",
|
||||||
|
"Water144SectionProbeMixin"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ public final class SanctuaryKnowledgeClient implements ClientModInitializer {
|
|||||||
|
|
||||||
public static void registerClient() {
|
public static void registerClient() {
|
||||||
SanctuaryResourcePack.register();
|
SanctuaryResourcePack.register();
|
||||||
|
fr.koka.sanctuary.client.shader.NaturalWater.register();
|
||||||
VortexCloudRenderer.register();
|
VortexCloudRenderer.register();
|
||||||
fr.koka.sanctuary.client.shader.PixelShadows.register();
|
fr.koka.sanctuary.client.shader.PixelShadows.register();
|
||||||
fr.koka.sanctuary.client.shader.EdgeHighlights.register();
|
fr.koka.sanctuary.client.shader.EdgeHighlights.register();
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
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 Profile fogFrom=CLEAR,fogTarget=CLEAR;
|
||||||
|
private static long fogChanged;
|
||||||
|
private static boolean wasUnderwater;
|
||||||
|
private NaturalWater(){}
|
||||||
|
public static void register(){
|
||||||
|
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){previousLevel=client.level;strength=next;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));}
|
||||||
|
// ClientLevel.hasChunk always returns true; inspect the cache without requesting a chunk.
|
||||||
|
public static boolean loaded(ClientLevel level,int x,int z){
|
||||||
|
return level.getChunkSource().getChunk(x,z,net.minecraft.world.level.chunk.status.ChunkStatus.FULL,false)!=null;
|
||||||
|
}
|
||||||
|
public static boolean ocean(ClientLevel level,BlockPos pos){return loaded(level,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(!loaded(l,x>>4,z>>4)||ocean(l,p))return CLEAR;
|
||||||
|
return PROFILES[classify(l::getBlockState,p,materials)];
|
||||||
|
}
|
||||||
|
/** Owned by a single native section compilation, with ocean data captured before worker submission. */
|
||||||
|
public static final class Region {
|
||||||
|
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>>(216);
|
||||||
|
int qx=(ox>>2)-1,qy=(oy>>2)-1,qz=(oz>>2)-1;
|
||||||
|
for(int y=0;y<6;y++)for(int z=0;z<6;z++)for(int x=0;x<6;x++)
|
||||||
|
copied.add(loaded(level,(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)*6+z-qz)*6+x-qx));
|
||||||
|
}
|
||||||
|
// A missing biome snapshot also forces clear water at unloaded region edges.
|
||||||
|
public boolean ocean(int x,int y,int z){
|
||||||
|
if(oceanKinds==null)oceanKinds=new byte[18*16*18];
|
||||||
|
int key=((y-oy)*18+z-oz+1)*18+x-ox+1;
|
||||||
|
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-1||x>ox+16||y<oy||y>oy+15||z<oz-1||z>oz+16)return CLEAR;
|
||||||
|
if(ocean(x,y,z))return CLEAR;
|
||||||
|
int key=((y-oy)*18+z-oz+1)*18+x-ox+1;
|
||||||
|
if(profiles==null)profiles=new byte[18*16*18];
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
// Both cells sharing an ocean edge must emit the same clear vertex.
|
||||||
|
// The one-block transition stays on the non-ocean side, preserving ocean priority.
|
||||||
|
if(ocean(x,y,z)||ocean(x+1,y,z)||ocean(x,y,z+1)||ocean(x+1,y,z+1))return CLEAR;
|
||||||
|
return profile(region,x,y,z).mix(profile(region,x+1,y,z),.5f).mix(profile(region,x,y,z+1).mix(profile(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;
|
||||||
|
renderer.setSectionRangeDirty((pos.getX()-1)>>4,pos.getY()>>4,(pos.getZ()-1)>>4,
|
||||||
|
(pos.getX()+1)>>4,(pos.getY()+DEPTH)>>4,(pos.getZ()+1)>>4);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,9 @@ public final class ShaderOptionsScreen extends OptionsSubScreen {
|
|||||||
list.addBig(OptionInstance.createBoolean("sanctuary.shaders.vanilla_light",OptionInstance.cachedConstantTooltip(
|
list.addBig(OptionInstance.createBoolean("sanctuary.shaders.vanilla_light",OptionInstance.cachedConstantTooltip(
|
||||||
Component.translatable(VanillaLight.externalEngine()?"sanctuary.shaders.external":"sanctuary.shaders.help")),
|
Component.translatable(VanillaLight.externalEngine()?"sanctuary.shaders.external":"sanctuary.shaders.help")),
|
||||||
VanillaLight.enabled(),VanillaLight::setEnabled));
|
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.saturation",OptionInstance.cachedConstantTooltip(
|
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),
|
Component.translatable("sanctuary.shaders.saturation_help")),(caption,value)->Component.translatable("sanctuary.shaders.saturation_value",value),
|
||||||
new OptionInstance.IntRange(0,200),VanillaLight.saturation(),VanillaLight::setSaturation));
|
new OptionInstance.IntRange(0,200),VanillaLight.saturation(),VanillaLight::setSaturation));
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ public final class VanillaLight {
|
|||||||
private VanillaLight() { }
|
private VanillaLight() { }
|
||||||
private static final class Preferences {
|
private static final class Preferences {
|
||||||
boolean enabled=true;
|
boolean enabled=true;
|
||||||
|
boolean naturalWater=true;
|
||||||
|
int waterIntensity=100;
|
||||||
int saturation=142;
|
int saturation=142;
|
||||||
boolean pixelShadows=true;
|
boolean pixelShadows=true;
|
||||||
boolean bloom=true;
|
boolean bloom=true;
|
||||||
@@ -58,6 +60,7 @@ public final class VanillaLight {
|
|||||||
preferences.oreIntensity=Math.clamp(preferences.oreIntensity,0,100);
|
preferences.oreIntensity=Math.clamp(preferences.oreIntensity,0,100);
|
||||||
preferences.edgeIntensity=Math.clamp(preferences.edgeIntensity,0,100);
|
preferences.edgeIntensity=Math.clamp(preferences.edgeIntensity,0,100);
|
||||||
preferences.edgeDistance=normalizeShadowDistance(preferences.edgeDistance);
|
preferences.edgeDistance=normalizeShadowDistance(preferences.edgeDistance);
|
||||||
|
preferences.waterIntensity=Math.clamp(preferences.waterIntensity,0,100);
|
||||||
preferences.saturation=Math.clamp(preferences.saturation,0,200);
|
preferences.saturation=Math.clamp(preferences.saturation,0,200);
|
||||||
preferences.shadowPixels=normalizePrecision(preferences.shadowPixels);
|
preferences.shadowPixels=normalizePrecision(preferences.shadowPixels);
|
||||||
preferences.shadowDistance=normalizeShadowDistance(preferences.shadowDistance);
|
preferences.shadowDistance=normalizeShadowDistance(preferences.shadowDistance);
|
||||||
@@ -66,6 +69,10 @@ public final class VanillaLight {
|
|||||||
}
|
}
|
||||||
public static boolean externalEngine(){return EXTERNAL_ENGINE;}
|
public static boolean externalEngine(){return EXTERNAL_ENGINE;}
|
||||||
public static boolean active(){return ProgressionClient.active()&&!EXTERNAL_ENGINE&&enabled();}
|
public static boolean active(){return ProgressionClient.active()&&!EXTERNAL_ENGINE&&enabled();}
|
||||||
|
public static boolean naturalWater(){enabled();return preferences.naturalWater;}
|
||||||
|
public static int waterIntensity(){enabled();return preferences.waterIntensity;}
|
||||||
|
public static void setNaturalWater(boolean value){enabled();preferences.naturalWater=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 int saturation(){enabled();return preferences.saturation;}
|
||||||
public static boolean edgeHighlights(){enabled();return preferences.edgeHighlights;}
|
public static boolean edgeHighlights(){enabled();return preferences.edgeHighlights;}
|
||||||
public static int edgeDistance(){enabled();return preferences.edgeDistance;}
|
public static int edgeDistance(){enabled();return preferences.edgeDistance;}
|
||||||
|
|||||||
+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();
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
|||||||
abstract class PixelShadowDirtyMixin {
|
abstract class PixelShadowDirtyMixin {
|
||||||
@Inject(method="blockChanged", at=@At("HEAD"))
|
@Inject(method="blockChanged", at=@At("HEAD"))
|
||||||
private void sanctuary$dirty(BlockPos pos, int flags, CallbackInfo ci) {
|
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);
|
PixelShadows.dirtyBlock(pos); ProceduralPbr.dirtyBlock(pos); Bloom.dirtyBlock(pos); ColoredLights.dirtyBlock(pos);
|
||||||
}
|
}
|
||||||
@Inject(method="allChanged", at=@At("HEAD"))
|
@Inject(method="allChanged", at=@At("HEAD"))
|
||||||
|
|||||||
@@ -2319,5 +2319,10 @@
|
|||||||
"sanctuary.shaders.colored_light_distance_value": "Colored lights: %s blocks",
|
"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.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": "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": "Biome colour, clarity from the bed. Clear oceans. Also changes underwater visibility.",
|
||||||
|
"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%%"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2319,5 +2319,10 @@
|
|||||||
"sanctuary.shaders.colored_light_distance_value": "Lumières colorées : %s blocs",
|
"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.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": "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": "Couleur du biome, clarté selon le fond. Océans clairs. Modifie aussi la visibilité sous l’eau.",
|
||||||
|
"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%%"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
}
|
||||||
@@ -278,7 +278,12 @@
|
|||||||
"client.PixelShadowRendererMixin",
|
"client.PixelShadowRendererMixin",
|
||||||
"client.PixelShadowLevelMixin",
|
"client.PixelShadowLevelMixin",
|
||||||
"client.PixelShadowDirtyMixin",
|
"client.PixelShadowDirtyMixin",
|
||||||
"client.PreferredGraphicsApiMixin"
|
"client.PreferredGraphicsApiMixin",
|
||||||
|
"client.NaturalWaterRegionMixin",
|
||||||
|
"client.NaturalWaterFluidMixin",
|
||||||
|
"client.NaturalWaterFogMixin",
|
||||||
|
"client.NaturalWaterFogColorMixin",
|
||||||
|
"client.NaturalWaterSectionCopyAccess"
|
||||||
],
|
],
|
||||||
"injectors": {
|
"injectors": {
|
||||||
"defaultRequire": 1
|
"defaultRequire": 1
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
name = "Sanctuary"
|
name = "Sanctuary"
|
||||||
author = "KOKA99CAB"
|
author = "KOKA99CAB"
|
||||||
version = "beta.143"
|
version = "beta.144"
|
||||||
pack-format = "packwiz:1.1.0"
|
pack-format = "packwiz:1.1.0"
|
||||||
|
|
||||||
[index]
|
[index]
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
#!/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))
|
||||||
|
assert log.count("WATER144_CONDITIONS limit=260 throttle=NONE")==24, "Measurements must be uncapped"
|
||||||
|
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)
|
||||||
|
if scene=="moving":
|
||||||
|
assert all(v[3]>0 for v in off+on), "Moving scene must compile terrain sections"
|
||||||
|
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,6 @@ ajoute les ombres portées pixélisées optionnelles, configurables depuis
|
|||||||
Ce dossier reste réservé aux futurs packs externes. Leur ajout au manifeste
|
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
|
packwiz nécessitera version, URL, hash, licence et compatibilité vérifiée pour
|
||||||
la version exacte du jeu et du moteur Iris.
|
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.
|
||||||
|
|||||||
Reference in New Issue
Block a user