Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96a36d99be | ||
|
|
cf9a670f44 | ||
|
|
ed2d088272 | ||
|
|
0cfd51eb4f | ||
|
|
38f4d6c9a9 | ||
|
|
580b2414da | ||
|
|
97ff2b54cb | ||
|
|
59346d7f0a | ||
|
|
ea151dd5df | ||
|
|
7ce01c47f2 | ||
|
|
93ed5574a4 |
@@ -1,5 +1,28 @@
|
||||
# Sanctuary
|
||||
|
||||
## 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,14 +43,19 @@ La [vision complète](docs/vision.md) conserve les intentions ; le
|
||||
L’économie, les claims et les dimensions décrits dans la vision restent
|
||||
à implémenter.
|
||||
|
||||
La version **beta.140** relie les lumières colorées aux objets en main et sur
|
||||
la tête, renforce leur teinte et ajoute une distance réglable de 8 à 64 blocs
|
||||
avec apparition progressive. Activation par défaut à 30 % ; torches natives,
|
||||
option « Torches chaudes » désactivée, cuivre vert et redstone rouge saturé.
|
||||
Les frontières de saturation des lumières bleues s’atténuent progressivement.
|
||||
Le [ticket](docs/colored-dynamic-beta140.md) décrit le rendu et les vérifications.
|
||||
La beta.140 est publiée et synchronisée dans l’instance Sanctuary Beta, avec
|
||||
les 923 fichiers personnels suivis conservés.
|
||||
La version **beta.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
|
||||
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
|
||||
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à
|
||||
l’atelier, les œufs et la neige saisonnière dans l’instance **Sanctuary Beta**.
|
||||
Les [synchronisations antérieures](docs/git-sync-beta092.md) restent documentées.
|
||||
@@ -38,6 +66,33 @@ La [carte native beta.005](docs/atlas-beta005.md) prend le relais après
|
||||
l’étude de Xaero et des alternatives libres.
|
||||
|
||||
|
||||
## beta.143 — Neige saisonnière en temps réel
|
||||
|
||||
Accumulation ralentie par un facteur 12 et fonte saisonnière ralentie par le
|
||||
facteur naturel 72, tout en conservant le sampler natif des colonnes de
|
||||
précipitation. Deux blocs de neige fondent en environ 2,7 jours réels en été,
|
||||
5,4 au printemps et 11 en automne.
|
||||
[Contrat et vérifications](docs/realtime-snow-melt-beta143.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.143/Sanctuary-beta.143.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.143/Sanctuary-Test-beta.143.mrpack).
|
||||
|
||||
## beta.142 — Vulkan par défaut et stabilité PBR
|
||||
|
||||
Moteur natif Vulkan prioritaire, relief conservé pendant les reconstructions,
|
||||
calculs de reflets bornés et intensité initiale 80 %. Choix personnels conservés.
|
||||
[Contrat et vérifications](docs/pbr-refresh-beta142.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.142/Sanctuary-beta.142.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.142/Sanctuary-Test-beta.142.mrpack).
|
||||
|
||||
## beta.141 — Direction locale et normales PBR
|
||||
|
||||
Relief et reflets orientés par les lumières locales. Cache compact, confiance
|
||||
réduite pour les sources opposées, objets portés et fondus. Activer PBR et
|
||||
Lumières colorées ; portée PBR 32 blocs.
|
||||
[Contrat et vérifications](docs/directional-lights-beta141.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.141/Sanctuary-beta.141.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.141/Sanctuary-Test-beta.141.mrpack).
|
||||
|
||||
## beta.140 — Lumières portées et fondus
|
||||
|
||||
Mains et tête, distances 8/16/32/64, fondus, cuivre vert, redstone rouge et
|
||||
@@ -2287,7 +2342,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.147 |
|
||||
|
||||
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
|
||||
@@ -2316,7 +2371,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.147.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).
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
# LIGHT-141 — Direction locale pour les normales PBR
|
||||
|
||||
Socle beta.140 `c9f5c56`, branche `codex/directional-lights-beta141`.
|
||||
Minecraft 26.3 / Java 25. Livraison beta.141.
|
||||
|
||||
## Contrat
|
||||
|
||||
Estimer une direction d'arrivée depuis les six voisins du champ lumineux,
|
||||
avec intensité et confiance. Conserver cette estimation dans une texture
|
||||
compacte reconstruite avec le champ, uniquement quand le PBR est actif.
|
||||
Les sources portées utilisent leurs positions déjà disponibles. Brancher ces
|
||||
directions sur les normales et reflets PBR existants, sans nouveau bouton.
|
||||
Les sources opposées doivent réduire la directionnalité, sans bascule brutale.
|
||||
Les champs précédent/courant gardent leur fondu et les obstacles restent pris
|
||||
en compte pour les blocs posés. Aucun changement de lumière de gameplay,
|
||||
de sauvegarde ou de génération ; aucune promesse de lancer de rayons exact.
|
||||
|
||||
## Programme de vérification
|
||||
|
||||
Contrôles natifs OpenGL/Vulkan : direction, sources opposées, murs, rendu de
|
||||
relief en intérieur, déplacement des sources, transitions et PBR OFF.
|
||||
Régression des lumières beta.139/140 puis check/build et assemblage. GameTest
|
||||
serveur dédié exclu selon le refus EULA antérieur. Aucun monde personnel ouvert.
|
||||
|
||||
## Réalisation
|
||||
|
||||
Le champ RGB conserve une texture directionnelle RGBA8 supplémentaire :
|
||||
les trois composantes codent la direction pondérée, l'alpha sa réponse lumineuse.
|
||||
Les voisins plus lumineux et accessibles contribuent à l'estimation ; les
|
||||
arrivées opposées s'annulent dans le vecteur, sans supprimer l'éclairage natif.
|
||||
Le shader interpole les moments pondérés avant normalisation, y compris entre
|
||||
les champs précédent et courant. La confiance réduit le relief quand aucune
|
||||
direction ne domine.
|
||||
|
||||
Cette texture est préparée dans le budget de reconstruction existant, avec
|
||||
un maximum de six voisins par cellule éclairée et une table d'énergie réutilisée.
|
||||
Aucune recherche de sources ni rayons par pixel pour les blocs posés. La
|
||||
lecture GPU interpole huit cellules par champ ; les deux champs sont utilisés
|
||||
pendant le fondu. Les sources mobiles réutilisent les positions, intensités et
|
||||
transitions déjà collectées, dans la limite existante de 128 sources.
|
||||
|
||||
Une texture directionnelle occupe environ 1,5 Mo à 16 blocs de distance et
|
||||
19 Mo à 64 blocs, par copie CPU/GPU ; deux champs coexistent lors des transitions.
|
||||
Le champ directionnel n'est construit que si PBR est actif avec une intensité
|
||||
non nulle. Sa désactivation libère les directions après la transition.
|
||||
Le coût est borné par ces réglages, sans garantie de FPS.
|
||||
|
||||
La passe PBR applique la différence entre normale de base et normale perturbée,
|
||||
puis un reflet GGX local. Elle garde sa portée de 32 blocs et les réglages PBR
|
||||
existants ; Lumières colorées doit être actif. Le soleil et la lune continuent
|
||||
à fonctionner lorsque les lumières colorées sont coupées. Les aides FR/EN
|
||||
expliquent cette dépendance.
|
||||
|
||||
C'est une direction dominante approximative : deux lumières colorées opposées
|
||||
ne produisent pas deux reflets indépendants. Les sources mobiles gardent la
|
||||
propagation radiale et les limites d'occultation existantes. Les normales PBR
|
||||
concernent les surfaces de blocs prises en charge, pas tous les modèles de mobs.
|
||||
Les exclusions existantes des panoramas et de l'immersion restent inchangées.
|
||||
|
||||
## Premier essai ciblé
|
||||
|
||||
L'essai natif OpenGL ciblé réussit en **1 min 12 s** : direction gauche/droite,
|
||||
annulation des sources opposées avec énergie conservée, occultation par un mur,
|
||||
relief sur la pierre en intérieur et avec une torche en main, stabilité au repos,
|
||||
libération des directions avec PBR OFF et maintien du chemin solaire avec
|
||||
Lumières colorées OFF. La somme d'écarts PBR OFF/ON sur la pierre vaut 878 114
|
||||
au réglage PBR 100 % ; c'est une comparaison de pixels, pas une mesure de FPS.
|
||||
Log : `build/directional141-focused.log`.
|
||||
|
||||
Les essais de préparation ont révélé un conflit de nom de matrice GLSL, corrigé,
|
||||
puis des erreurs de fixture (angle 180° normalisé par Minecraft, suppression
|
||||
d'entités alors que la sélection était vide, commande d'heure déjà à minuit),
|
||||
corrigées sans changer le rendu.
|
||||
|
||||
## Validation complète OpenGL
|
||||
|
||||
La chaîne complète réussit en **3 min 1 s** sur OpenGL / Apple M1 :
|
||||
`COLORED139_PASS`, `COLORED140_PASS` et `DIRECTIONAL141_PASS`.
|
||||
Elle réunit les contrôles de couleurs, objets portés, caméra, occultation,
|
||||
rechargement des ressources, préférences, distances/fondus et directions PBR.
|
||||
Le test ciblé est reproductible avec `-PsanctuaryDirectional141Only=true` ;
|
||||
la chaîne complète utilise `-PsanctuaryDirectional141ClientTests=true` avec
|
||||
les deux suites de lumières existantes.
|
||||
Log : `build/directional141-opengl.log`. Captures natives conservées dans
|
||||
`build/directional141-opengl-screenshots/`. Comparaison autonome sans retraitement :
|
||||
`build/Shader-beta.141-comparaison.html`.
|
||||
|
||||
## Validation complète Vulkan
|
||||
|
||||
La même chaîne réussit en **2 min 56 s** sur Vulkan / Apple M1, MoltenVK 1.4.2,
|
||||
profondeur 0–1 et transparence améliorée. Les trois marqueurs de réussite sont
|
||||
présents dans `build/directional141-vulkan.log`. L'écart PBR local OFF/ON vaut
|
||||
878 107, contre 878 114 sous OpenGL dans cette scène. Les captures natives sont
|
||||
conservées dans `build/directional141-vulkan-screenshots/`.
|
||||
Les deux moteurs vérifient la compilation, les chemins actifs et désactivés,
|
||||
le cache, les murs, les sources opposées et portées, ainsi que les régressions
|
||||
beta.139/140. Pas de validation des pilotes Windows de l'utilisateur revendiquée.
|
||||
|
||||
## Construction et archives
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
réussit en **2 min 35 s**, 126 tâches. Le GameTest serveur dédié reste exclu
|
||||
selon le refus EULA antérieur. Les suites clientes ci-dessus ont été exécutées
|
||||
séparément. Log : `build/shader141-build.log`.
|
||||
|
||||
Comparaison à beta.140 : **8 entrées de production modifiées**, limitées au
|
||||
champ lumineux, à sa préparation avant le PBR, au shader PBR et aux aides FR/EN.
|
||||
Les archives normale/Test contiennent le même JAR Sanctuary, sans classes
|
||||
de test. Version, dépendances exactes, sources des JAR, textures de gemmes et
|
||||
de la clé de Steve, et template complet sont vérifiés.
|
||||
Reçu : `build/shader141-artifact.json`.
|
||||
|
||||
SHA-256 du JAR : `660f29c3fa77fee4de6a8db5673c61619985804494f70e961998da36f396d0f1`.
|
||||
|
||||
- Sanctuary-beta.141.mrpack : `08df23dac9e98fbfa2935f747345c38244bddc3a3d9371c6416f251e85a7eb19`.
|
||||
- Sanctuary-Test-beta.141.mrpack : `a300ef5bfff7b6990c9f5c2760e9c3a23d7a27aeb10b8f04d29db9a7409fa0e1`.
|
||||
|
||||
## Livraison
|
||||
|
||||
[Release beta.141](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.141)
|
||||
publiée sur le commit source `93ed5574a4f77bd3a083b254498f4994efe31179` ; canal stable
|
||||
`7e6c84400f80db24bf5204cd21d628194ae9f0f7` vérifié après publication.
|
||||
Deux synchronisations isolées puis deux dans la même instance Sanctuary Beta
|
||||
réussissent. Les **923 fichiers personnels** suivis gardent leurs hashes.
|
||||
Sauvegarde préalable : `sanctuary-backups/before-beta.141/`. Aucun monde personnel
|
||||
ouvert. Reçus : `build/shader141-isolated.json` et `build/shader141-prism.json`.
|
||||
Archives normale/Test/template et comparaison copiées dans `sanctuary-beta/build/`.
|
||||
@@ -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/`.
|
||||
@@ -14,6 +14,52 @@ 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,
|
||||
avec le sampler natif de précipitation conservé. Protections inchangées pour
|
||||
la neige construite, les biomes froids, l'hiver, les profils neigeux et la
|
||||
météo Vanilla. [Contrat et vérifications](realtime-snow-melt-beta143.md).
|
||||
[Release beta.143](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.143)
|
||||
publiée ; canal `bafff5e9fcdd95c5dd184953a34ada805e455797`. Deux synchronisations
|
||||
isolées puis deux dans l'instance Sanctuary Beta réussies. Les 923 fichiers
|
||||
personnels suivis gardent leurs hashes. Aucun monde personnel ouvert ;
|
||||
sauvegarde préalable `sanctuary-backups/before-beta.143/`. Archives vérifiées
|
||||
dans `sanctuary-beta/build/`.
|
||||
|
||||
## beta.142 — Vulkan par défaut et stabilité PBR
|
||||
|
||||
Vulkan natif prioritaire, rafraîchissements PBR stables et intensité initiale
|
||||
80 %, calculs de reflets bornés. [Contrat et vérifications](pbr-refresh-beta142.md).
|
||||
[Release beta.142](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.142)
|
||||
publiée ; canal `d661260e2bbabb0326c86d6cc543daf986979596`. Deux synchronisations isolées
|
||||
puis deux dans l'instance Sanctuary Beta réussies. Les 923 fichiers personnels
|
||||
suivis gardent leurs hashes. Aucun monde personnel ouvert ; sauvegarde préalable
|
||||
`sanctuary-backups/before-beta.142/`. Archives vérifiées dans `sanctuary-beta/build/`.
|
||||
Un choix OpenGL explicite reste conservé : sélectionner Vulkan dans les réglages
|
||||
vidéo et redémarrer pour changer ce choix. La capture Windows de bruit en
|
||||
mouvement reste à reconfirmer sur son pilote ; les contrôles OpenGL/Vulkan Mac
|
||||
et la correction du domaine numérique du reflet sont documentés dans le ticket.
|
||||
|
||||
## beta.141 — Direction des lumières pour les normales PBR
|
||||
|
||||
Directions locales mises en cache, sources portées, confiance réduite lorsque
|
||||
les sources s'opposent et fondus. Activer PBR et Lumières colorées ; portée PBR
|
||||
de 32 blocs. [Contrat et vérifications](directional-lights-beta141.md).
|
||||
[Release beta.141](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.141)
|
||||
publiée ; canal `7e6c84400f80db24bf5204cd21d628194ae9f0f7`. Deux synchronisations isolées
|
||||
puis deux dans l'instance Sanctuary Beta réussies. Les 923 fichiers personnels
|
||||
suivis gardent leurs hashes. Aucun monde personnel ouvert ; sauvegarde préalable
|
||||
`sanctuary-backups/before-beta.141/`. Archives vérifiées dans `sanctuary-beta/build/`.
|
||||
|
||||
## beta.140 — Lumières portées et fondus
|
||||
|
||||
Couleurs des objets en main et sur la tête, distances 8/16/32/64 et fondus
|
||||
|
||||
@@ -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,173 @@
|
||||
# PBR-142 — Stabilité du relief et Vulkan par défaut
|
||||
|
||||
Socle beta.141 `7ce01c4`, branche `codex/pbr-refresh-beta142`.
|
||||
Minecraft 26.3 / Java 25. Livraison beta.142.
|
||||
|
||||
## Contrat
|
||||
|
||||
Corriger le clignotement lors de la reconstruction des sections PBR : conserver
|
||||
le rendu précédent jusqu'à ce que le remplaçant soit disponible et éviter les
|
||||
reconstructions dues seulement aux lumières dynamiques. Une invalidation pendant
|
||||
une reconstruction doit être conservée, sans annuler indéfiniment le travail.
|
||||
Le PBR passe à 80 % par défaut, valeur confirmée par l'utilisateur ; les choix
|
||||
explicitement enregistrés restent conservés.
|
||||
|
||||
La capture `2026-09-18_04.19.34.png` montre des points très clairs, plus fins
|
||||
que les pixels des textures, sur des parois et un plafond de grotte. L'utilisateur
|
||||
confirme OpenGL et leur disparition quand seul le PBR est coupé. Examiner
|
||||
les lectures de textures et les calculs aux angles rasants séparément du cache.
|
||||
|
||||
Tests natifs prévus : recalcul de section sous observation, invalidations
|
||||
répétées, remplacement atomique, suppression réelle de blocs, lumière portée,
|
||||
cache et préférences ; OpenGL et Vulkan. Pas de monde personnel ouvert.
|
||||
|
||||
## Précision du signalement et implémentation
|
||||
|
||||
L'utilisateur précise ensuite que le bruit apparaît **en mouvement**, et demande
|
||||
le passage à Vulkan. Il ne faut donc pas attribuer les points de sa capture au
|
||||
seul clignotement de reconstruction : ce sont deux vérifications distinctes.
|
||||
|
||||
- Les sections PBR publiées restent affichées jusqu'au remplacement de leur
|
||||
maillage. Une nouvelle invalidation durant la construction est mémorisée ;
|
||||
elle n'annule plus continuellement le travail. Le budget permanent reste
|
||||
24 Mio, avec temporairement l'ancien et le nouveau maillage d'une section.
|
||||
- Les invalidations de lumière dynamique ne régénèrent plus la géométrie PBR.
|
||||
Les changements de blocs et de lumière du ciel restent pris en compte.
|
||||
- Lectures explicites des texels de profondeur/scène et du niveau de matériau,
|
||||
gradients de texture calculés avant les rejets de fragments. Les produits
|
||||
scalaires des reflets sont bornés, la puissance entière de Fresnel est
|
||||
explicite, les résultats non finis ne remplacent pas la scène native.
|
||||
Aucun flou ajouté aux textures.
|
||||
- Le choix graphique natif « Par défaut » devient « Vulkan (par défaut) »
|
||||
(FR/EN) et essaie Vulkan en premier. Minecraft conserve son repli OpenGL si
|
||||
Vulkan ne peut pas être initialisé. Un choix **OpenGL explicite** reste
|
||||
disponible ; pour une installation ainsi réglée, sélectionner Vulkan dans
|
||||
les réglages vidéo puis redémarrer. Aucun moteur externe ni VulkanMod ajouté.
|
||||
- 80 % est la nouvelle intensité PBR initiale. Les valeurs déjà enregistrées
|
||||
explicitement restent conservées.
|
||||
|
||||
Le code Minecraft 26.3 a été inspecté : `PreferredGraphicsApi.VULKAN` essaie
|
||||
Vulkan puis OpenGL ; le choix natif `DEFAULT` essayait auparavant OpenGL d'abord.
|
||||
Tous les effets Sanctuary utilisent déjà l'API graphique native. Le seul hook
|
||||
OpenGL direct est le correctif spécifique de lecture des panoramas dans
|
||||
`GlCommandEncoder` ; il ne remplace pas la voie Vulkan native.
|
||||
|
||||
Référence pour les lectures après contrôle divergent :
|
||||
[documentation Khronos des samplers](https://wikis.khronos.org/opengl/Sampler_%28GLSL%29).
|
||||
La correction rend ces lectures explicites ; elle ne constitue pas à elle seule
|
||||
une reproduction du défaut sur le pilote Windows de l'utilisateur.
|
||||
|
||||
## Observations de diagnostic
|
||||
|
||||
Le détecteur de points blancs isolés (minimum RGB 170, au moins cinq voisins
|
||||
sur huit sous 120) compte 669 points sur l'image utilisateur et zéro sur la
|
||||
première grotte de contrôle. Il s'agit de scènes, résolutions et pilotes différents,
|
||||
pas d'un avant/après reproduisant exactement le bug utilisateur. Les rendus
|
||||
natifs OpenGL de contrôle étaient déjà exempts de ce grain avant les nouvelles
|
||||
protections du shader ; le signalement Windows reste donc à reconfirmer là-bas.
|
||||
|
||||
Le test de rafraîchissement conserve les mêmes maillages publiés durant douze
|
||||
invalidations consécutives, compare l'image et vérifie que des remplacements
|
||||
aboutissent. Le test de torche alterne quatre fois les torches normales/des âmes
|
||||
sans reconstruction des normales. Le contrôle de mouvement presse réellement
|
||||
Avancer, maintient la torche et le PBR actifs et vérifie un déplacement d'au moins
|
||||
un bloc avant de vérifier les 24 captures. Les angles -30°, 0° et 35° sont aussi
|
||||
contrôlés, ainsi que les effets combinés et les préférences.
|
||||
|
||||
### Domaine invalide du reflet de la torche en main
|
||||
|
||||
Pour la lumière du joueur en vue subjective, la source est la caméra : le
|
||||
vecteur lumière et le vecteur de vue sont parallèles. L'ancien
|
||||
`pow(1 - max(dot(v,h),0),5)` n'avait aucune borne supérieure pour ce produit
|
||||
scalaire. Un calcul reproductible Float32 (100 000 positions, graine 142)
|
||||
donne 19 991 bases légèrement négatives, jusqu'à -2,3841858e-7, contre zéro
|
||||
après clamp. C'est une reproduction numérique d'un **domaine de calcul invalide**,
|
||||
pas une reproduction des pixels produits par le pilote utilisateur.
|
||||
|
||||
GLSL 3.30 ne définit pas `pow` pour une base négative, même avec un exposant
|
||||
entier : [spécification Khronos, fonctions exponentielles](https://registry.khronos.org/OpenGL/specs/gl/GLSLangSpec.3.30.pdf).
|
||||
Le correctif borne les cosinus à [0,1] et remplace cette puissance par des
|
||||
multiplications. La torche alignée sur la caméra ne dépend donc plus du traitement
|
||||
de cette entrée invalide par le pilote. Reçu numérique ignoré :
|
||||
`build/pbr142-fresnel-domain.json`.
|
||||
|
||||
### Mise à jour des fixtures antérieures
|
||||
|
||||
La suite Vulkan complète s'est arrêtée au contrôle SSGI de coin : l'oracle
|
||||
ne trouvait plus de pierre neutre (`rows=0`), car les lumières colorées ajoutées
|
||||
depuis beta.139/140 teintaient la pierre avec la lave. Le test de proximité SSGI
|
||||
et le test PBR solaire isolent désormais leur effet en coupant cette autre passe.
|
||||
Aucun seuil n'a été diminué et aucun changement de rendu SSGI n'est livré.
|
||||
Le sous-ensemble en échec est relancé séparément ; les étapes précédentes
|
||||
(ombres, CTM, bloom, lens flare, déplacement SSGI) avaient réussi.
|
||||
|
||||
## Vérifications natives
|
||||
|
||||
Sur Mac Apple M1, Java 25 et Minecraft 26.3 :
|
||||
|
||||
- OpenGL : suite couleurs/directions/rafraîchissement en 3 min 36 s, puis
|
||||
déplacement continu avec torche active en 1 min 36 s. Zéro point blanc isolé
|
||||
sur les 24 images de mouvement et les trois angles de grotte.
|
||||
- Vulkan/MoltenVK 1.4.2 : sélection automatique sans argument de moteur,
|
||||
couleurs/directions/rafraîchissement en 3 min 47 s. Dernier test continu avec
|
||||
torche active en 1 min 37 s, zéro point blanc isolé sur les 24 images.
|
||||
- Ombrages lune/midi/feuilles, distances et précisions, stabilité temps réel,
|
||||
CTM, bloom (minerais, torche, lampe, lave, soleil), émissifs indépendants et
|
||||
lens flare validés sous Vulkan dans la suite d'effets. Le déplacement SSGI
|
||||
sur douze blocs passe également. La suite s'arrête ensuite sur l'oracle
|
||||
ancien de couleur de pierre décrit ci-dessus : ne pas présenter ce lancement
|
||||
de 7 min 15 s comme une réussite complète.
|
||||
- Reprise ciblée après isolation de cet oracle : SSGI près des surfaces puis
|
||||
PBR extérieur réussis sous Vulkan en 2 min 55 s. Proximité 35/100, haute
|
||||
résolution, léger mouvement, toit ajouté/retiré, reload, resize, zéro/OFF et
|
||||
effets combinés. Gain SSGI au coin test : 6,01 sur 99 lignes à 35 %.
|
||||
- Les nouvelles valeurs initiales et les choix enregistrés sont testés, dont
|
||||
une intensité PBR explicitement enregistrée à 50 qui reste à 50.
|
||||
|
||||
Les premiers essais du nouveau contrôle de mouvement ont permis de corriger
|
||||
son entrée clavier : `pressKey` ne maintient pas une touche enfoncée. Le test
|
||||
final utilise `holdKey` puis `releaseKey`, vérifie le déplacement réel et refuse
|
||||
la disparition de la lumière portée ou du PBR pendant les captures.
|
||||
|
||||
Les jeux de test utilisent uniquement des mondes neufs et ignorés. La capture
|
||||
et le pilote Windows utilisateur n'ont pas été reproduits à l'identique ; leur
|
||||
retest reste ouvert malgré les corrections numériques et les contrôles natifs.
|
||||
|
||||
## Construction et archives
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
réussit en **2 min 38 s**, 126 tâches. Le GameTest serveur dédié reste exclu
|
||||
selon le refus EULA antérieur ; les suites clientes ci-dessus ont réellement
|
||||
utilisé les deux moteurs natifs. `git diff --check` passe.
|
||||
|
||||
Les archives normale/Test embarquent le même JAR, leurs versions/index/hashes
|
||||
sont vérifiés, leurs sources correspondent aux fichiers courants. Le template
|
||||
contient exactement les ressources attendues. Les onze entrées de production
|
||||
modifiées par rapport à beta.141 sont limitées au PBR, au choix graphique et
|
||||
aux traductions ; les ressources de jeu antérieures sont conservées.
|
||||
|
||||
SHA-256 du JAR : `0bc2da9df2ef004e6dc78932cfde7b31b7717c63c83ceed789d151177c41bc1a`.
|
||||
|
||||
- `Sanctuary-beta.142.mrpack` : 10464746 octets ; SHA-256 `dba4fd8e1e3b20136faa1c1de3494df3cb250c87de199c04e07883c786ca48ee`.
|
||||
- `Sanctuary-Test-beta.142.mrpack` : 10483672 octets ; SHA-256 `d56b81a4aea6649a40a3c49dedee904989943a518ad0ae7c520b808dd0f9b9a7`.
|
||||
|
||||
Reçu détaillé ignoré : `build/shader142-artifact.json`.
|
||||
|
||||
## Livraison réalisée
|
||||
|
||||
[Release beta.142](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.142)
|
||||
publiée depuis `ea151dd5df8c7c5abd0107656f40c723efbf2ebe`. Canal stable :
|
||||
`d661260e2bbabb0326c86d6cc543daf986979596`. Archives normale/Test, ZIP d'amorçage Prism et template
|
||||
publiés avec vérification de leurs téléchargements ; tag et artefacts immuables.
|
||||
|
||||
Deux synchronisations isolées puis deux synchronisations de l'instance
|
||||
**Sanctuary Beta** réussies. Les 923 fichiers personnels suivis ont conservé
|
||||
leurs hashes, notamment mondes, réglages et packs personnels. Aucun monde
|
||||
personnel ouvert. Sauvegarde préalable : `sanctuary-backups/before-beta.142/`.
|
||||
L'instance locale conserve son réglage graphique natif « default », qui démarre
|
||||
désormais avec Vulkan prioritaire. Un Windows explicitement réglé sur OpenGL
|
||||
doit sélectionner Vulkan dans les réglages vidéo et redémarrer.
|
||||
|
||||
Archives locales également dans `sanctuary-beta/build/`. Le checkout de travail
|
||||
historique modifié `sanctuary-beta` n'a pas été réinitialisé ; seules ces archives
|
||||
ignorées y ont été déposées. Sources sur `codex/pbr-refresh-beta142`.
|
||||
+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,68 @@
|
||||
# SNOW-143 — Neige saisonnière en temps réel
|
||||
|
||||
Branche `codex/realtime-snow-melt-beta143`, socle beta.142. Minecraft 26.3 /
|
||||
Java 25. Livraison beta.143.
|
||||
|
||||
## Comportement
|
||||
|
||||
L'accumulation et la fonte saisonnière conservent le sampler natif de
|
||||
précipitation `ServerLevel.tickPrecipitation`. Sanctuary ne scanne donc toujours
|
||||
pas les chunks et ne force aucun chargement de terrain. Le changement porte
|
||||
seulement sur la probabilité des mutations de neige.
|
||||
|
||||
Pendant une chute de neige, Sanctuary ajoute désormais une couche une fois sur
|
||||
12 échantillons natifs en moyenne. La neige reste visible pendant une session,
|
||||
mais les colonnes ne montent plus instantanément. À `randomTickSpeed = 3`, une
|
||||
couche apparaît en moyenne après environ 40 minutes de neige continue.
|
||||
|
||||
Pour la fonte, chaque tirage saisonnier est multiplié par `72`, le rapport
|
||||
entre une journée réelle de 24 heures et une journée Minecraft de 20 minutes.
|
||||
|
||||
Une couche fond désormais en moyenne en 4 h 05 en été,
|
||||
8 h 10 au printemps et 16 h 25 en automne. Avec le plafond actuel de deux blocs,
|
||||
une colonne météo suivie met environ 2,7 jours réels à disparaître en été,
|
||||
5,4 jours au printemps et 11 jours en automne. L'hiver ne fond toujours pas.
|
||||
|
||||
Les règles de protection restent celles de beta.109 : seule la neige déposée
|
||||
par la météo Sanctuary depuis l'ajout du suivi `sanctuary:natural_snow_v1`
|
||||
peut fondre. La neige construite, l'ancienne neige non suivie, les couches
|
||||
manuelles, les biomes froids, les profils neigeux et la météo Vanilla restent
|
||||
préservés. Aucun format de sauvegarde ne change.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Vérifié le 18 septembre 2026 sur macOS, Java 25.
|
||||
|
||||
- `./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true
|
||||
-PsanctuaryMelt109ClientTests=true -PsanctuaryClientNoVsync=true
|
||||
-PsanctuaryQuickTests=true` : réussi en 57 s, avec le marqueur
|
||||
`MELT109_NATIVE_PASS`. Le test couvre l'accumulation beta.108 ralentie,
|
||||
la fonte couche par couche, la provenance persistante, les saisons, la
|
||||
gamerule, la météo Vanilla, les profils neigeux, les biomes froids et le
|
||||
rechargement de monde.
|
||||
- `./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest` :
|
||||
réussi en 2 min 31 s, 126 tâches. Le GameTest serveur dédié reste exclu
|
||||
conformément au refus antérieur de son EULA ; les contrôles natifs utilisent
|
||||
le serveur intégré.
|
||||
- `python3 scripts/pack.py check` : versions, icône et index packwiz vérifiés.
|
||||
- Exports MRpack produits avec `packwiz modrinth export`, intégrité ZIP vérifiée.
|
||||
Les deux MRpack embarquent exactement le JAR Sanctuary construit.
|
||||
|
||||
| Artefact | Taille | SHA-256 |
|
||||
| --- | ---: | --- |
|
||||
| `sanctuary-beta.143.jar` | 11 323 665 octets | `fd4b663ac2b2a106624c42b09997db229cc14d6c39d44ae83079bc6aa9d39a08` |
|
||||
| `Sanctuary-beta.143.mrpack` | 10 464 931 octets | `e55caaa41c5a3a2666ff5415846e18f44d504ac34b698f3d974091cfb52364c5` |
|
||||
| `Sanctuary-Test-beta.143.mrpack` | 10 483 858 octets | `b14bca04c4a48771209d0f7e234ff9ef6b800eb236fb969a6f0dfcee5620dafc` |
|
||||
| `Sanctuary-Template-beta.143.zip` | 4 354 153 octets | `071fd8da67d1d0224b89889a5fcbc4cd269673dddb7a52f89aa78802f154605a` |
|
||||
|
||||
## Publication
|
||||
|
||||
[Release beta.143](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.143)
|
||||
publiée depuis le commit `97ff2b54cb63afe8fdd88bfa8f2ebb25dbe3b86d`.
|
||||
Le canal packwiz stable pointe sur
|
||||
`bafff5e9fcdd95c5dd184953a34ada805e455797`.
|
||||
|
||||
Deux synchronisations isolées, puis deux synchronisations dans l'instance
|
||||
Sanctuary Beta ont réussi. Les 923 fichiers personnels suivis gardent leurs
|
||||
hashes ; aucun monde personnel n'a été ouvert. Sauvegarde préalable :
|
||||
`sanctuary-backups/before-beta.143/`.
|
||||
@@ -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.
|
||||
|
||||
+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.140
|
||||
pack_version=beta.140
|
||||
mod_version=beta.147
|
||||
pack_version=beta.147
|
||||
resource_pack_version=beta.121
|
||||
maven_group=fr.koka.sanctuary
|
||||
jei_version=30.32.0-sanctuary.3
|
||||
|
||||
@@ -34,6 +34,14 @@ 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'))
|
||||
systemProperty('sanctuary.test.colored140', providers.gradleProperty('sanctuaryColored140ClientTests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.noVsync', providers.gradleProperty('sanctuaryClientNoVsync').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.quick', providers.gradleProperty('sanctuaryQuickTests').getOrElse('false'))
|
||||
|
||||
+19
-11
@@ -12,6 +12,7 @@ import java.time.Instant;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.BooleanSupplier;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
@@ -50,24 +51,29 @@ public final class Melt109ClientChecks implements FabricClientGameTest {
|
||||
for(int x=-15;x<=15;x++)for(int z=-15;z<=15;z++)l.setBlockAndUpdate(new BlockPos(x,79,z),Blocks.STONE.defaultBlockState());
|
||||
clock(l,"2026-07-10T12:00:00Z",SeasonalWeather.Season.SUMMER);
|
||||
check(l.getGameRules().get(SeasonalSnowMelt.ENABLED),"Melt enabled by default");
|
||||
check(SeasonalSnowMelt.REALTIME_SCALE==72,"Real-time snow melt scale is one real day per 72 native days");
|
||||
check(SeasonalSnowMelt.chance(SeasonalWeather.Season.SUMMER)==72
|
||||
&&SeasonalSnowMelt.chance(SeasonalWeather.Season.SPRING)==144
|
||||
&&SeasonalSnowMelt.chance(SeasonalWeather.Season.AUTUMN)==288
|
||||
&&SeasonalSnowMelt.chance(SeasonalWeather.Season.WINTER)==0,"Seasonal melt chances include the 72x real-time scale");
|
||||
deposit(l,AT,16);check(l.getBlockState(AT.above()).is(Blocks.SNOW_BLOCK),"Default two-block accumulation retained");
|
||||
clear(l);step(l,AT,1);
|
||||
check(layers(l,AT.above())==7&&layers(l,AT)==8,"Summer melts only one top layer");
|
||||
step(l,AT,7);check(l.getBlockState(AT.above()).isAir()&&layers(l,AT)==8,"Top block melts completely before the lower block");
|
||||
step(l,AT,8);check(l.getBlockState(AT).isAir()&&!tracked(l,AT),"Natural column vanishes without water; receipt removed");
|
||||
clear(l);
|
||||
until(l,AT,5000,()->layers(l,AT.above())==7&&layers(l,AT)==8,"Summer melts only one top layer");
|
||||
until(l,AT,20000,()->l.getBlockState(AT.above()).isAir()&&layers(l,AT)==8,"Top block melts completely before the lower block");
|
||||
until(l,AT,20000,()->l.getBlockState(AT).isAir()&&!tracked(l,AT),"Natural column vanishes without water; receipt removed");
|
||||
|
||||
var built=new BlockPos(7,80,4);l.setBlockAndUpdate(built,Blocks.SNOW_BLOCK.defaultBlockState());step(l,built,64);check(layers(l,built)==8,"Untracked snow block preserved");
|
||||
var old=new BlockPos(8,80,4);snow(l,old,5);step(l,old,64);check(layers(l,old)==5,"Untracked old layers preserved");
|
||||
var base=new BlockPos(9,80,4);snow(l,base,3);deposit(l,base,5);clear(l);step(l,base,16);check(layers(l,base)==3&&!tracked(l,base),"Weather above manual layers melts only back to its base");
|
||||
var atop=new BlockPos(9,80,6);l.setBlockAndUpdate(atop,Blocks.SNOW_BLOCK.defaultBlockState());deposit(l,atop,8);clear(l);step(l,atop,16);check(layers(l,atop)==8&&l.getBlockState(atop.above()).isAir(),"Weather on a snow construction melts; construction stays");
|
||||
var base=new BlockPos(9,80,4);snow(l,base,3);deposit(l,base,5);clear(l);until(l,base,20000,()->layers(l,base)==3&&!tracked(l,base),"Weather above manual layers melts only back to its base");
|
||||
var atop=new BlockPos(9,80,6);l.setBlockAndUpdate(atop,Blocks.SNOW_BLOCK.defaultBlockState());deposit(l,atop,8);clear(l);until(l,atop,20000,()->layers(l,atop)==8&&l.getBlockState(atop.above()).isAir(),"Weather on a snow construction melts; construction stays");
|
||||
var replaced=new BlockPos(10,80,4);deposit(l,replaced,8);l.setBlockAndUpdate(replaced,Blocks.AIR.defaultBlockState());l.setBlockAndUpdate(replaced,Blocks.SNOW_BLOCK.defaultBlockState());clear(l);step(l,replaced,32);check(layers(l,replaced)==8&&!tracked(l,replaced),"Break and replacement cannot inherit melt permission");
|
||||
var thickened=new BlockPos(10,80,6);deposit(l,thickened,2);snow(l,thickened,6);clear(l);step(l,thickened,32);check(layers(l,thickened)==6&&!tracked(l,thickened),"Manual layer edit relinquishes natural provenance");
|
||||
var capped=new BlockPos(12,80,6);deposit(l,capped,8);snow(l,capped.above(),1);clear(l);step(l,capped,32);check(layers(l,capped)==8&&layers(l,capped.above())==1,"Manual thin snow cap protects its natural support");
|
||||
var roof=new BlockPos(11,80,4);deposit(l,roof,8);l.setBlockAndUpdate(roof.above(3),Blocks.STONE.defaultBlockState());clear(l);step(l,roof,32);check(layers(l,roof)==8,"Covered snow remains untouched");l.setBlockAndUpdate(roof.above(3),Blocks.AIR.defaultBlockState());step(l,roof,1);check(layers(l,roof)==7,"Uncovered natural snow resumes melt");
|
||||
var roof=new BlockPos(11,80,4);deposit(l,roof,8);l.setBlockAndUpdate(roof.above(3),Blocks.STONE.defaultBlockState());clear(l);step(l,roof,32);check(layers(l,roof)==8,"Covered snow remains untouched");l.setBlockAndUpdate(roof.above(3),Blocks.AIR.defaultBlockState());until(l,roof,10000,()->layers(l,roof)==7,"Uncovered natural snow resumes melt");
|
||||
command(l,"gamerule sanctuary:seasonal_snow_melt false");step(l,roof,32);check(layers(l,roof)==7,"Operator disables melting");command(l,"gamerule sanctuary:seasonal_snow_melt true");
|
||||
clock(l,"2026-01-10T12:00:00Z",SeasonalWeather.Season.WINTER);clear(l);step(l,roof,64);check(layers(l,roof)==7,"Winter never melts even with clear weather");
|
||||
clock(l,"2026-04-10T12:00:00Z",SeasonalWeather.Season.SPRING);clear(l);step(l,roof,64);check(l.getBlockState(roof).isAir(),"Spring progressively melts");
|
||||
deposit(l,roof,8);clock(l,"2026-10-10T12:00:00Z",SeasonalWeather.Season.AUTUMN);clear(l);step(l,roof,128);check(l.getBlockState(roof).isAir(),"Autumn progressively melts");
|
||||
clock(l,"2026-04-10T12:00:00Z",SeasonalWeather.Season.SPRING);clear(l);until(l,roof,30000,()->l.getBlockState(roof).isAir(),"Spring progressively melts");
|
||||
deposit(l,roof,8);clock(l,"2026-10-10T12:00:00Z",SeasonalWeather.Season.AUTUMN);clear(l);until(l,roof,50000,()->l.getBlockState(roof).isAir(),"Autumn progressively melts");
|
||||
deposit(l,roof,8);clock(l,"2026-07-10T12:00:00Z",SeasonalWeather.Season.SUMMER);
|
||||
command(l,"sanctuary weather snow");l.getWeatherData().setRaining(false);l.setRainLevel(0);step(l,roof,64);check(layers(l,roof)==8,"Forced summer snowfall blocks melting");
|
||||
command(l,"sanctuary weather snow_showers");l.getWeatherData().setRaining(false);l.setRainLevel(0);step(l,roof,64);check(layers(l,roof)==8,"Pauses in snow showers also block melting");
|
||||
@@ -84,7 +90,7 @@ public final class Melt109ClientChecks implements FabricClientGameTest {
|
||||
var l=server.overworld();clock(l,"2026-07-10T12:00:00Z",SeasonalWeather.Season.SUMMER);clear(l);
|
||||
check(!l.getGameRules().get(SeasonalSnowMelt.ENABLED),"Melt rule persists across restart");
|
||||
check(layers(l,PERSIST)==8&&layers(l,PERSIST.above())==8&&tracked(l,PERSIST)&&tracked(l,PERSIST.above()),"Chunk attachment survives native save/reload");
|
||||
command(l,"gamerule sanctuary:seasonal_snow_melt true");step(l,PERSIST,16);
|
||||
command(l,"gamerule sanctuary:seasonal_snow_melt true");until(l,PERSIST,30000,()->l.getBlockState(PERSIST).isAir()&&l.getBlockState(PERSIST.above()).isAir()&&!tracked(l,PERSIST), "Loaded natural snow melts and clears provenance");
|
||||
check(l.getBlockState(PERSIST).isAir()&&l.getBlockState(PERSIST.above()).isAir()&&!tracked(l,PERSIST),"Loaded natural snow melts and clears provenance: base="+l.getBlockState(PERSIST)+", top="+l.getBlockState(PERSIST.above())+", sky="+l.canSeeSky(PERSIST.above(2))+", active="+WeatherService.active(l)+", biome="+l.getBiome(PERSIST)+", season="+WeatherService.season(server));
|
||||
check(layers(l,new BlockPos(7,80,4))==8&&layers(l,new BlockPos(9,80,4))==3,"Built and preexisting snow preserved across restart");
|
||||
});
|
||||
@@ -92,13 +98,15 @@ public final class Melt109ClientChecks implements FabricClientGameTest {
|
||||
System.out.println("MELT109_NATIVE_PASS: accumulation regression, top-down melt, seasons, profiles, cold biomes, constructions, edit invalidation, gamerule, native chunk save/reload");
|
||||
}finally{intro.set(false);}
|
||||
}
|
||||
private static void deposit(ServerLevel l,BlockPos pos,int count){l.getGameRules().set(GameRules.MAX_SNOW_ACCUMULATION_HEIGHT,1,l.getServer());command(l,"sanctuary weather snow");l.getWeatherData().setRaining(true);l.setRainLevel(1);step(l,pos,count);l.getGameRules().set(GameRules.MAX_SNOW_ACCUMULATION_HEIGHT,0,l.getServer());}
|
||||
private static void deposit(ServerLevel l,BlockPos pos,int count){l.getGameRules().set(GameRules.MAX_SNOW_ACCUMULATION_HEIGHT,1,l.getServer());command(l,"sanctuary weather snow");l.getWeatherData().setRaining(true);l.setRainLevel(1);int target=depth(l,pos)+count;until(l,pos,Math.max(1000,count*200),()->depth(l,pos)>=target,"Weather snow reaches depth "+target+" at "+pos);l.getGameRules().set(GameRules.MAX_SNOW_ACCUMULATION_HEIGHT,0,l.getServer());}
|
||||
private static void clear(ServerLevel l){command(l,"sanctuary weather clear");l.getWeatherData().setRaining(false);l.setRainLevel(0);}
|
||||
private static void clock(ServerLevel l,String time,SeasonalWeather.Season expected){try{var field=RealtimeService.Session.class.getDeclaredField("clock");field.setAccessible(true);field.set(RealtimeService.session(l.getServer()),Clock.fixed(Instant.parse(time),ZoneOffset.UTC));check(WeatherService.season(l.getServer())==expected,"Test calendar: "+expected);}catch(ReflectiveOperationException e){throw new AssertionError(e);}}
|
||||
private static boolean tracked(ServerLevel l,BlockPos p){var m=l.getChunkAt(p).getAttached(SeasonalSnowMelt.NATURAL);return m!=null&&m.containsKey(Long.toString(p.asLong()));}
|
||||
private static void snow(ServerLevel l,BlockPos p,int layers){l.setBlockAndUpdate(p,Blocks.SNOW.defaultBlockState().setValue(SnowLayerBlock.LAYERS,layers));}
|
||||
private static int layers(ServerLevel l,BlockPos p){return SeasonalSnowMelt.layers(l.getBlockState(p));}
|
||||
private static int depth(ServerLevel l,BlockPos p){int total=0;for(int i=0;i<32;i++){int layers=layers(l,p.above(i));if(layers==0)break;total+=layers;}return total;}
|
||||
private static void step(ServerLevel l,BlockPos p,int n){for(int i=0;i<n;i++)l.tickPrecipitation(p);}
|
||||
private static void until(ServerLevel l,BlockPos p,int max,BooleanSupplier condition,String message){for(int i=0;i<max&&!condition.getAsBoolean();i++)l.tickPrecipitation(p);check(condition.getAsBoolean(),message);}
|
||||
private static void command(ServerLevel l,String s){l.getServer().getCommands().performPrefixedCommand(l.getServer().createCommandSourceStack(),s);}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
|
||||
+19
-15
@@ -6,6 +6,7 @@ import fr.koka.sanctuary.progression.SanctuaryGameplay;
|
||||
import fr.koka.sanctuary.weather.SnowAccumulation;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.BooleanSupplier;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
@@ -45,42 +46,45 @@ public final class Snow108ClientChecks implements FabricClientGameTest {
|
||||
if(enabled) {
|
||||
command(l,"sanctuary weather snow");l.setRainLevel(1);
|
||||
check(l.getGameRules().get(SnowAccumulation.MAX_HEIGHT)==2,"Default two blocks");
|
||||
for(int i=1;i<=7;i++){l.tickPrecipitation(AT);check(layers(l,AT)==i,"One layer per precipitation event: "+i);}
|
||||
check(SnowAccumulation.REALTIME_SCALE==12,"Snow accumulation uses the 12x real-time arrival scale");
|
||||
for(int i=1;i<=7;i++){int expected=i;until(l,AT,1000,()->layers(l,AT)==expected,"Layer "+i+" arrives through the native sampler");}
|
||||
var pig=EntityTypes.PIG.create(l,EntitySpawnReason.COMMAND);pig.setNoAi(true);pig.setNoGravity(true);pig.setPos(4.5,80.75,4.5);l.addFreshEntity(pig);
|
||||
l.tickPrecipitation(AT);check(l.getBlockState(AT).is(Blocks.SNOW_BLOCK),"Eighth layer becomes solid snow block");
|
||||
until(l,AT,1000,()->l.getBlockState(AT).is(Blocks.SNOW_BLOCK),"Eighth layer becomes solid snow block");
|
||||
check(l.getBlockState(AT).getCollisionShape(l,AT,CollisionContext.empty()).bounds().maxY==1,"Full block collision");
|
||||
check(pig.getY()>=81,"Growth lifts a standing entity");pig.discard();
|
||||
l.tickPrecipitation(AT);check(layers(l,AT.above())==1,"New layer begins above solid snow");
|
||||
for(int i=0;i<30;i++)l.tickPrecipitation(AT);
|
||||
until(l,AT,1000,()->layers(l,AT.above())==1,"New layer begins above solid snow");
|
||||
until(l,AT,2000,()->l.getBlockState(AT.above()).is(Blocks.SNOW_BLOCK),"Default cap fills a second block");
|
||||
check(l.getBlockState(AT.above()).is(Blocks.SNOW_BLOCK)&&l.getBlockState(AT.above(2)).isAir(),"Default cap at two blocks");
|
||||
command(l,"gamerule sanctuary:snow_accumulation_height 4");
|
||||
for(int i=0;i<30;i++)l.tickPrecipitation(AT);
|
||||
until(l,AT,5000,()->l.getBlockState(AT.above(3)).is(Blocks.SNOW_BLOCK),"Raised cap fills the fourth block");
|
||||
check(l.getBlockState(AT.above(3)).is(Blocks.SNOW_BLOCK)&&l.getBlockState(AT.above(4)).isAir(),"Operator raises cap to four blocks");
|
||||
command(l,"gamerule sanctuary:snow_accumulation_height -1");
|
||||
for(int i=0;i<16;i++)l.tickPrecipitation(AT);
|
||||
until(l,AT,3000,()->l.getBlockState(AT.above(5)).is(Blocks.SNOW_BLOCK),"Unlimited mode fills beyond the cap");
|
||||
check(l.getBlockState(AT.above(5)).is(Blocks.SNOW_BLOCK),"Unlimited mode continues upward");
|
||||
var fresh=new BlockPos(-5,80,-5);command(l,"gamerule sanctuary:snow_accumulation_height 0");l.tickPrecipitation(fresh);check(l.getBlockState(fresh).isAir(),"Sanctuary zero stops deposits");
|
||||
command(l,"gamerule sanctuary:snow_accumulation_height 2");l.getGameRules().set(GameRules.MAX_SNOW_ACCUMULATION_HEIGHT,0,server);l.tickPrecipitation(fresh);check(l.getBlockState(fresh).isAir(),"Native zero remains authoritative");l.getGameRules().set(GameRules.MAX_SNOW_ACCUMULATION_HEIGHT,1,server);
|
||||
l.setRainLevel(0);l.getWeatherData().setRaining(false);l.tickPrecipitation(fresh);check(l.getBlockState(fresh).isAir(),"No accumulation between showers");
|
||||
command(l,"sanctuary weather snow_showers");l.setRainLevel(1);l.getWeatherData().setRaining(true);l.tickPrecipitation(fresh);check(layers(l,fresh)==1,"Snow showers accumulate while falling");
|
||||
var flower=new BlockPos(-6,80,-6);l.setBlockAndUpdate(flower.below(),Blocks.GRASS_BLOCK.defaultBlockState());l.setBlockAndUpdate(flower,Blocks.DANDELION.defaultBlockState());l.tickPrecipitation(flower);check(l.getBlockState(flower).is(Blocks.DANDELION),"Plant preserved");
|
||||
var water=new BlockPos(-7,79,-7);l.setBlockAndUpdate(water,Blocks.WATER.defaultBlockState());l.tickPrecipitation(water);check(l.getBlockState(water).is(Blocks.WATER)&&l.getBlockState(water.above()).isAir(),"Open water not filled with snow");
|
||||
var roof=new BlockPos(7,83,-7);l.setBlockAndUpdate(roof,Blocks.STONE.defaultBlockState());l.tickPrecipitation(roof);check(layers(l,roof.above())==1&&l.getBlockState(roof.below()).isAir(),"Snow on roof, not indoors");
|
||||
var fresh=new BlockPos(-5,80,-5);command(l,"gamerule sanctuary:snow_accumulation_height 0");step(l,fresh,200);check(l.getBlockState(fresh).isAir(),"Sanctuary zero stops deposits");
|
||||
command(l,"gamerule sanctuary:snow_accumulation_height 2");l.getGameRules().set(GameRules.MAX_SNOW_ACCUMULATION_HEIGHT,0,server);step(l,fresh,200);check(l.getBlockState(fresh).isAir(),"Native zero remains authoritative");l.getGameRules().set(GameRules.MAX_SNOW_ACCUMULATION_HEIGHT,1,server);
|
||||
l.setRainLevel(0);l.getWeatherData().setRaining(false);step(l,fresh,200);check(l.getBlockState(fresh).isAir(),"No accumulation between showers");
|
||||
command(l,"sanctuary weather snow_showers");l.setRainLevel(1);l.getWeatherData().setRaining(true);until(l,fresh,1000,()->layers(l,fresh)==1,"Snow showers accumulate while falling");
|
||||
var flower=new BlockPos(-6,80,-6);l.setBlockAndUpdate(flower.below(),Blocks.GRASS_BLOCK.defaultBlockState());l.setBlockAndUpdate(flower,Blocks.DANDELION.defaultBlockState());step(l,flower,200);check(l.getBlockState(flower).is(Blocks.DANDELION),"Plant preserved");
|
||||
var water=new BlockPos(-7,79,-7);l.setBlockAndUpdate(water,Blocks.WATER.defaultBlockState());step(l,water,200);check(l.getBlockState(water).is(Blocks.WATER)&&l.getBlockState(water.above()).isAir(),"Open water not filled with snow");
|
||||
var roof=new BlockPos(7,83,-7);l.setBlockAndUpdate(roof,Blocks.STONE.defaultBlockState());until(l,roof,1000,()->layers(l,roof.above())==1,"Snow on roof arrives");check(layers(l,roof.above())==1&&l.getBlockState(roof.below()).isAir(),"Snow on roof, not indoors");
|
||||
var cauldron=new BlockPos(8,80,-8);l.setBlockAndUpdate(cauldron,Blocks.CAULDRON.defaultBlockState());for(int i=0;i<200;i++)l.tickPrecipitation(cauldron);check(l.getBlockState(cauldron).is(Blocks.POWDER_SNOW_CAULDRON),"Native cauldron snow filling preserved");
|
||||
command(l,"sanctuary weather rain");l.setRainLevel(1);var rainy=new BlockPos(10,80,0);l.tickPrecipitation(rainy);check(l.getBlockState(rainy).isAir(),"Temperate rain is not snow");
|
||||
command(l,"sanctuary weather vanilla");cold(l);for(int i=0;i<8;i++)l.tickPrecipitation(new BlockPos(0,80,0));check(l.getBlockState(new BlockPos(0,80,0)).is(Blocks.SNOW_BLOCK),"Cold biome also accumulates with vanilla weather");
|
||||
command(l,"sanctuary weather vanilla");cold(l);until(l,new BlockPos(0,80,0),2000,()->l.getBlockState(new BlockPos(0,80,0)).is(Blocks.SNOW_BLOCK),"Cold biome also accumulates with vanilla weather");
|
||||
var light=new BlockPos(11,80,11);l.setBlockAndUpdate(light.below(),Blocks.GLOWSTONE.defaultBlockState());command(l,"sanctuary weather snow");l.setRainLevel(1);
|
||||
} else {
|
||||
cold(l);for(int i=0;i<40;i++)l.tickPrecipitation(AT);check(layers(l,AT)==1&&l.getBlockState(AT.above()).isAir(),"Non-Sanctuary world keeps native one-layer limit");
|
||||
}
|
||||
});
|
||||
if(enabled){c.waitTicks(8);world.getServer().runOnServer(s->{var l=s.overworld();var light=new BlockPos(11,80,11);check(l.getBrightness(LightLayer.BLOCK,light)>=10,"Test light propagated");for(int i=0;i<16;i++)l.tickPrecipitation(light);check(l.getBlockState(light).isAir(),"Bright surfaces do not accumulate");});}
|
||||
if(enabled){c.waitTicks(8);world.getServer().runOnServer(s->{var l=s.overworld();var light=new BlockPos(11,80,11);check(l.getBrightness(LightLayer.BLOCK,light)>=10,"Test light propagated");step(l,light,400);check(l.getBlockState(light).isAir(),"Bright surfaces do not accumulate");});}
|
||||
}
|
||||
System.out.println("SNOW108_NATIVE_PASS: layers, solid blocks, vertical stacking, entity lift, caps and commands, weather, light, supports, cauldron and vanilla world");
|
||||
}finally{intro.set(false);}
|
||||
}
|
||||
private static void cold(ServerLevel l){command(l,"fillbiome -8 76 -8 8 88 8 minecraft:snowy_plains");l.getWeatherData().setRaining(true);l.setRainLevel(1);}
|
||||
private static int layers(ServerLevel l,BlockPos p){var s=l.getBlockState(p);return s.is(Blocks.SNOW)?s.getValue(SnowLayerBlock.LAYERS):0;}
|
||||
private static void step(ServerLevel l,BlockPos p,int n){for(int i=0;i<n;i++)l.tickPrecipitation(p);}
|
||||
private static void until(ServerLevel l,BlockPos p,int max,BooleanSupplier condition,String message){for(int i=0;i<max&&!condition.getAsBoolean();i++)l.tickPrecipitation(p);check(condition.getAsBoolean(),message);}
|
||||
private static void command(ServerLevel l,String s){l.getServer().getCommands().performPrefixedCommand(l.getServer().createCommandSourceStack(),s);}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import static fr.koka.sanctuary.client.shader.PixelShadows122ClientChecks.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.*;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
|
||||
/** Real enclosed stone surfaces: local normal response without sunlight. */
|
||||
final class DirectionalLights141ClientChecks {
|
||||
private static final BlockPos LEFT=new BlockPos(96,101,102),RIGHT=new BlockPos(104,101,102),PROBE=new BlockPos(100,101,102);
|
||||
static void run(ClientGameTestContext c,TestServerContext server){
|
||||
c.runOnClient(m->{VanillaLight.setEnabled(true);VanillaLight.setSaturation(100);VanillaLight.setSsgi(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);VanillaLight.setLensFlare(false);VanillaLight.setEdgeHighlights(false);VanillaLight.setPixelShadows(false);VanillaLight.setSunshafts(false);m.options.particles().set(net.minecraft.server.level.ParticleStatus.MINIMAL);VanillaLight.setColoredLights(true);VanillaLight.setColoredLightIntensity(30);VanillaLight.setColoredLightDistance(16);VanillaLight.setWarmTorches(false);VanillaLight.setPbr(false);VanillaLight.setPbrIntensity(100);});
|
||||
camera(c,server,100.5,101,98.5,70);look(c,server,0,35);c.waitTicks(30);
|
||||
server.runOnServer(s->{if(s.clockManager().getInstance(s.overworld().dimensionType().defaultClock().orElseThrow()).totalTicks()!=18000)command(s,"time set 18000");
|
||||
for(int x=88;x<=112;x++)for(int z=92;z<=116;z++)for(int y=100;y<=109;y++)
|
||||
s.overworld().setBlockAndUpdate(new BlockPos(x,y,z),(y==100)?Blocks.STONE.defaultBlockState():(y==109||x==88||x==112||z==92||z==116)?Blocks.CONCRETE.white().defaultBlockState():Blocks.AIR.defaultBlockState());
|
||||
s.overworld().setBlockAndUpdate(LEFT,Blocks.TORCH.defaultBlockState());
|
||||
});c.waitTicks(40);settle(c);var flat=capture(c,"direction-left-pbr-off");
|
||||
c.runOnClient(m->VanillaLight.setPbr(true));c.waitFor(m->ColoredLights.directionSample(PROBE)!=0&&ProceduralPbr.ready(),2000);settle(c);
|
||||
int left=c.computeOnClient(m->ColoredLights.directionSample(PROBE));check(dx(left)<-.9&&Math.abs(dy(left))<.02&&Math.abs(dz(left))<.02,"Cached arrival points toward the left source");
|
||||
var relief=capture(c,"direction-left-pbr-on");long delta=difference(flat,relief);check(delta>1000,"Local light visibly affects stone normals without skylight: "+delta);
|
||||
int builds=c.computeOnClient(m->ColoredLights.builds());c.waitTicks(25);check(c.computeOnClient(m->ColoredLights.builds())==builds,"Static directions reuse the cache");unchanged(relief,capture(c,null),"Static local PBR is stable");
|
||||
if(Boolean.getBoolean("sanctuary.test.pbr142"))PbrRefresh142ClientChecks.refresh(c,server,relief);
|
||||
server.runOnServer(s->{s.overworld().setBlockAndUpdate(LEFT,Blocks.AIR.defaultBlockState());s.overworld().setBlockAndUpdate(RIGHT,Blocks.TORCH.defaultBlockState());});c.waitTicks(40);settle(c);
|
||||
int right=c.computeOnClient(m->ColoredLights.directionSample(PROBE));check(dx(right)>.9,"Moving the source reverses arrival direction");capture(c,"direction-right-pbr-on");
|
||||
server.runOnServer(s->s.overworld().setBlockAndUpdate(LEFT,Blocks.TORCH.defaultBlockState()));c.waitTicks(40);settle(c);
|
||||
int opposed=c.computeOnClient(m->ColoredLights.directionSample(PROBE));check(Math.abs(dx(opposed))<.02&&Math.abs(dy(opposed))<.02&&Math.abs(dz(opposed))<.02&&(opposed>>>24)>0,"Equal opposing lights cancel direction, retain illumination");capture(c,"direction-opposed");
|
||||
// A sealed barrier must prevent both intensity and direction reaching the probe.
|
||||
server.runOnServer(s->{s.overworld().setBlockAndUpdate(RIGHT,Blocks.AIR.defaultBlockState());for(int y=101;y<109;y++)for(int z=93;z<116;z++)s.overworld().setBlockAndUpdate(new BlockPos(99,y,z),Blocks.STONE.defaultBlockState());});c.waitTicks(40);settle(c);
|
||||
check(c.computeOnClient(m->ColoredLights.directionSample(PROBE))==0,"Closed wall blocks local direction and energy");capture(c,"direction-blocked");
|
||||
server.runOnServer(s->{s.overworld().setBlockAndUpdate(LEFT,Blocks.AIR.defaultBlockState());
|
||||
for(int y=101;y<109;y++)for(int z=93;z<116;z++)s.overworld().setBlockAndUpdate(new BlockPos(99,y,z),Blocks.AIR.defaultBlockState());
|
||||
command(s,"gamemode creative @a");s.getPlayerList().getPlayers().getFirst().setItemSlot(net.minecraft.world.entity.EquipmentSlot.MAINHAND,new net.minecraft.world.item.ItemStack(net.minecraft.world.item.Items.TORCH));});
|
||||
c.waitTicks(45);settle(c);capture(c,"direction-held-pbr-on");
|
||||
c.runOnClient(m->VanillaLight.setPbr(false));c.waitTicks(35);settle(c);var heldFlat=capture(c,"direction-held-pbr-off");
|
||||
c.runOnClient(m->VanillaLight.setPbr(true));c.waitTicks(35);settle(c);var heldRelief=capture(c,"direction-held-pbr-on-final");
|
||||
if(Boolean.getBoolean("sanctuary.test.pbr142"))PbrRefresh142ClientChecks.held(c,server);
|
||||
long heldDelta=difference(heldFlat,heldRelief);check(heldDelta>1000&&c.computeOnClient(m->ColoredLights.dynamicCount())==1,"Carried light drives local PBR: "+heldDelta);
|
||||
server.runOnServer(s->s.getPlayerList().getPlayers().getFirst().setItemSlot(net.minecraft.world.entity.EquipmentSlot.MAINHAND,net.minecraft.world.item.ItemStack.EMPTY));c.waitTicks(35);
|
||||
c.runOnClient(m->VanillaLight.setPbr(false));c.waitTicks(35);settle(c);check(c.computeOnClient(m->ColoredLights.directionSample(LEFT))==0,"PBR OFF releases directional field");
|
||||
c.runOnClient(m->{VanillaLight.setColoredLights(false);VanillaLight.setPbr(true);});c.waitTicks(25);capture(c,"direction-colored-off");
|
||||
check(!c.computeOnClient(m->ColoredLights.allocated()),"Color OFF releases local field while PBR keeps its solar path");
|
||||
if(Boolean.getBoolean("sanctuary.test.pbr142"))PbrRefresh142ClientChecks.cave(c,server);
|
||||
System.out.println("DIRECTIONAL141_PASS localPbrDelta="+delta+" native normals, left/right, opposing sources, wall, held light, stable cache, OFF");
|
||||
}
|
||||
private static double dx(int c){return ((c>>16&255)-128)/127.;}
|
||||
private static double dy(int c){return ((c>>8&255)-128)/127.;}
|
||||
private static double dz(int c){return ((c&255)-128)/127.;}
|
||||
private static void settle(ClientGameTestContext c){c.waitFor(m->ColoredLights.applied()&&ColoredLights.settled(),2400);c.waitTicks(14);}
|
||||
private static long difference(Frame a,Frame b){long n=0;for(int i=0;i<a.pixels().length;i++)n+=channelDifference(a.pixels()[i],b.pixels()[i]);return n;}
|
||||
}
|
||||
+49
-4
@@ -13,7 +13,7 @@ import java.util.Arrays;
|
||||
public final class Pbr134ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c){new PixelShadows122ClientChecks(false,false,true).runTest(c);}
|
||||
static void run(ClientGameTestContext c,TestServerContext server){
|
||||
c.runOnClient(m->{preferences();VanillaLight.setSunshafts(false);VanillaLight.setLensFlare(false);materials();VanillaLight.setEnabled(true);VanillaLight.setSaturation(100);VanillaLight.setPbr(false);VanillaLight.setSsgi(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);VanillaLight.setEdgeHighlights(false);VanillaLight.setPixelShadows(false);});
|
||||
c.runOnClient(m->{preferences();VanillaLight.setSunshafts(false);VanillaLight.setLensFlare(false);materials();VanillaLight.setEnabled(true);VanillaLight.setSaturation(100);VanillaLight.setColoredLights(false);VanillaLight.setPbr(false);VanillaLight.setSsgi(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);VanillaLight.setEdgeHighlights(false);VanillaLight.setPixelShadows(false);});
|
||||
server.runOnServer(s->{command(s,"time set 1000");for(int x=-5;x<=24;x++)for(int z=-5;z<=24;z++){
|
||||
s.overworld().setBlockAndUpdate(new BlockPos(x,64,z),(x<10?Blocks.BRICKS:x<17?Blocks.OAK_PLANKS:Blocks.IRON_BLOCK).defaultBlockState());
|
||||
for(int y=65;y<=102;y++)s.overworld().setBlockAndUpdate(new BlockPos(x,y,z),Blocks.AIR.defaultBlockState());
|
||||
@@ -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);
|
||||
@@ -52,17 +54,60 @@ public final class Pbr134ClientChecks implements FabricClientGameTest {
|
||||
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 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");
|
||||
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()==50&&VanillaLight.ssgi()&&!VanillaLight.bloom(),"Missing PBR setting defaults ON while preserving explicit old choices");
|
||||
check(VanillaLight.pbr()&&VanillaLight.pbrIntensity()==80&&VanillaLight.ssgi()&&!VanillaLight.bloom(),"Missing PBR setting defaults ON while preserving explicit old choices");
|
||||
for(int n:new int[]{-1,0,50,100,101}){VanillaLight.setPbr(true);VanillaLight.setPbrIntensity(n);forgetPreferences();check(VanillaLight.pbr()&&VanillaLight.pbrIntensity()==Math.clamp(n,0,100),"PBR preference persistence/clamp");}
|
||||
check(VanillaLight.pbrDistance()==32,"Shared distance default");
|
||||
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,"Shared distance persistence");}
|
||||
for(int n:new int[]{0,1,2,4,8}){VanillaLight.setWaterBlendRadius(n);forgetPreferences();check(VanillaLight.waterBlendRadius()==n,"Water blend persistence");}
|
||||
VanillaLight.setPbrDistance(32);VanillaLight.setWaterBlendRadius(4);
|
||||
check(!VanillaLight.ssr()&&VanillaLight.ssrIntensity()==20,"Missing SSR defaults OFF at 20 percent");
|
||||
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[]{"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);
|
||||
}
|
||||
}
|
||||
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){}
|
||||
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import static fr.koka.sanctuary.client.shader.PixelShadows122ClientChecks.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.*;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.entity.EquipmentSlot;
|
||||
|
||||
final class PbrRefresh142ClientChecks {
|
||||
static void refresh(ClientGameTestContext c,TestServerContext s,Frame reference){
|
||||
int entries=c.computeOnClient(m->ProceduralPbr.terrainEntries()),builds=c.computeOnClient(m->ProceduralPbr.terrainBuilds());check(entries>0,"PBR fixture has visible sections");
|
||||
for(int i=0;i<12;i++){
|
||||
c.runOnClient(m->{for(var section:m.levelRenderer.visibleSections()){var p=section.getRenderOrigin();ProceduralPbr.dirtySection(p.getX()>>4,p.getY()>>4,p.getZ()>>4);}
|
||||
check(ProceduralPbr.terrainEntries()==entries,"Invalidation retains every published PBR section");});
|
||||
c.waitTicks(1);unchanged(reference,capture(c,null),"PBR remains visible throughout repeated refreshes");
|
||||
}
|
||||
ready(c);check(c.computeOnClient(m->ProceduralPbr.terrainBuilds())>builds,"Replacement meshes actually published");unchanged(reference,capture(c,"refresh-stable"),"Replacement preserves the rendered normals");
|
||||
System.out.println("PBR142_REFRESH_PASS entries="+entries+" repeated invalidation, retained rendering, completed replacements");
|
||||
}
|
||||
static void held(ClientGameTestContext c,TestServerContext s){
|
||||
ready(c);int builds=c.computeOnClient(m->ProceduralPbr.terrainBuilds());
|
||||
for(var item:new Item[]{Items.SOUL_TORCH,Items.TORCH,Items.SOUL_TORCH,Items.TORCH}){
|
||||
s.runOnServer(server->server.getPlayerList().getPlayers().getFirst().setItemSlot(EquipmentSlot.MAINHAND,new ItemStack(item)));c.waitTicks(25);
|
||||
}
|
||||
check(c.computeOnClient(m->ProceduralPbr.terrainBuilds())==builds,"Carried light updates do not rebuild the normal mesh");
|
||||
System.out.println("PBR142_HELD_PASS native dynamic relighting preserves the PBR geometry cache");
|
||||
}
|
||||
static void cave(ClientGameTestContext c,TestServerContext s){
|
||||
c.runOnClient(m->{VanillaLight.setColoredLights(true);VanillaLight.setPbr(false);VanillaLight.setPbrIntensity(80);VanillaLight.setColoredLightDistance(16);});
|
||||
camera(c,s,0.5,65,2.5,70);look(c,s,0,-12);c.waitTicks(30);
|
||||
s.runOnServer(server->{for(int x=-5;x<=5;x++)for(int z=-1;z<=18;z++)for(int y=64;y<=70;y++)
|
||||
server.overworld().setBlockAndUpdate(new BlockPos(x,y,z),(x==-5||x==5||y==64||y==70||z==-1||z==18)?Blocks.DEEPSLATE.defaultBlockState():Blocks.AIR.defaultBlockState());
|
||||
for(int y=65;y<=69;y++)server.overworld().setBlockAndUpdate(new BlockPos(-2,y,9),Blocks.DEEPSLATE.defaultBlockState());
|
||||
for(int z=5;z<14;z++)server.overworld().setBlockAndUpdate(new BlockPos(3,67,z),Blocks.DEEPSLATE.defaultBlockState());
|
||||
server.overworld().setBlockAndUpdate(new BlockPos(3,68,8),Blocks.WATER.defaultBlockState());
|
||||
server.getPlayerList().getPlayers().getFirst().setItemSlot(EquipmentSlot.MAINHAND,new ItemStack(Items.TORCH));
|
||||
});c.waitTicks(100);var off=capture(c,"cave-pbr-off");c.runOnClient(m->VanillaLight.setPbr(true));ready(c);var on=capture(c,"cave-pbr-80");
|
||||
check(specks(on)<=specks(off)+4,"PBR does not introduce isolated white points in the cave");
|
||||
System.out.println("PBR142_CAVE_PIXELS off="+specks(off)+" on="+specks(on));
|
||||
// Capture while the camera is actually travelling, before settling caches.
|
||||
s.runOnServer(server->{var player=server.getPlayerList().getPlayers().getFirst();player.getAbilities().flying=true;player.onUpdateAbilities();});c.waitTicks(10);
|
||||
var start=c.computeOnClient(m->m.player.position());
|
||||
c.getInput().holdKey(o->o.keyUp);
|
||||
int maximumSpecks=0;
|
||||
try {for(int frame=0;frame<24;frame++){
|
||||
c.waitTicks(1);c.runOnClient(m->check(ColoredLights.dynamicCount()>0&&ProceduralPbr.applied(),"Carried light and PBR remain active during motion"));var moving=capture(c,frame%8==0?"cave-moving-"+frame:null);
|
||||
maximumSpecks=Math.max(maximumSpecks,specks(moving));
|
||||
}} finally {c.getInput().releaseKey(o->o.keyUp);}
|
||||
check(c.computeOnClient(m->m.player.position().distanceToSqr(start))>1.0,"Camera really travels at least one block");
|
||||
check(maximumSpecks<=4,"Moving camera has no isolated white grain: "+maximumSpecks);
|
||||
System.out.println("PBR142_MOVEMENT_PASS frames=24 maxSpecks="+maximumSpecks);
|
||||
camera(c,s,0.5,65,2.5,70);
|
||||
for(float pitch:new float[]{-30,0,35}){look(c,s,0,pitch);ready(c);var frame=capture(c,"cave-angle-"+(int)pitch);check(specks(frame)<=4,"Grazing-angle PBR has no white grain");System.out.println("PBR142_CAVE_ANGLE "+pitch+" specks="+specks(frame));}
|
||||
c.runOnClient(m->{VanillaLight.setSsgi(true);VanillaLight.setBloom(true);VanillaLight.setPixelShadows(true);VanillaLight.setEdgeHighlights(true);VanillaLight.setSunshafts(true);});c.waitTicks(40);ready(c);capture(c,"cave-combined");
|
||||
System.out.println("PBR142_PASS refresh, dynamic cache, defaults, cave and grazing-angle captures");
|
||||
}
|
||||
private static int specks(Frame f){int count=0,w=f.width();for(int y=2;y<f.height()-2;y++)for(int x=2;x<w-2;x++){
|
||||
int i=y*w+x,p=f.pixels()[i];int value=Math.min(p>>16&255,Math.min(p>>8&255,p&255));if(value<170)continue;
|
||||
int dark=0;for(int dy=-1;dy<=1;dy++)for(int dx=-1;dx<=1;dx++){if(dx==0&&dy==0)continue;int q=f.pixels()[i+dy*w+dx];if(Math.max(q>>16&255,Math.max(q>>8&255,q&255))<120)dark++;}if(dark>=5)count++;
|
||||
}return count;}
|
||||
private static void ready(ClientGameTestContext c){c.waitTicks(14);c.waitFor(m->ProceduralPbr.ready(),2400);c.waitTicks(8);}
|
||||
}
|
||||
+218
@@ -0,0 +1,218 @@
|
||||
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));settle(c);frames(c);capture(c,"ssr146-distance-"+distance);
|
||||
c.runOnClient(m->check(ProceduralPbr.applied()&&PixelReflections.applied(),"Both material passes share distance "+distance));
|
||||
}
|
||||
|
||||
c.runOnClient(m->{VanillaLight.setPbrIntensity(80);VanillaLight.setSsrIntensity(20);});
|
||||
camera(c,server,.5,68,10,70);settle(c);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++;
|
||||
check(white>300,"Solar reflection has a broad near-white footprint at PBR 80 / SSR 20 percent: "+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<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");
|
||||
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 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);}
|
||||
}
|
||||
+10
-5
@@ -54,7 +54,9 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
c.runOnClient(m -> {
|
||||
var info = com.mojang.blaze3d.systems.RenderSystem.getDevice().getDeviceInfo();
|
||||
String expected = System.getProperty("sanctuary.test.graphicsBackend", "");
|
||||
String expected = System.getProperty("sanctuary.test.graphicsBackend", "vulkan");
|
||||
check(net.minecraft.client.PreferredGraphicsApi.DEFAULT.getBackendsToTry()[0] instanceof com.mojang.renderpearl.backend.vulkan.VulkanBackend, "Default tries native Vulkan first");
|
||||
check(net.minecraft.client.PreferredGraphicsApi.OPENGL.getBackendsToTry()[0] instanceof com.mojang.renderpearl.backend.opengl.GlBackend, "Explicit OpenGL remains available");
|
||||
check(expected.isEmpty() || info.backendName().equalsIgnoreCase(expected),
|
||||
"Requested graphics backend must not silently fall back: " + expected + " / " + info.backendName());
|
||||
System.out.println("SHADER139_BACKEND " + info.backendName() + " / " + info.name()
|
||||
@@ -149,9 +151,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(coloredOnly){ColoredLights139ClientChecks.run(c,server);if(Boolean.getBoolean("sanctuary.test.colored140"))ColoredLights140ClientChecks.run(c,server);return;}
|
||||
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;}
|
||||
@@ -281,7 +285,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
||||
});
|
||||
});
|
||||
c.waitTicks(3);
|
||||
c.takeScreenshot("sanctuary-beta140-shader-options-" + locale);
|
||||
c.takeScreenshot("sanctuary-beta142-shader-options-" + locale);
|
||||
}
|
||||
c.runOnClient(m -> m.gui.setScreen(null));
|
||||
}
|
||||
@@ -450,11 +454,12 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
||||
var path=FabricLoader.getInstance().getConfigDir().resolve("sanctuary-shaders.json");
|
||||
Files.deleteIfExists(path);forgetPreferences();
|
||||
check(VanillaLight.bloom()&&VanillaLight.bloomIntensity()==20&&VanillaLight.pbr()&&VanillaLight.ssgi(),"Fresh defaults: bloom20, PBR and SSGI ON");
|
||||
check(VanillaLight.pbrIntensity()==50&&VanillaLight.ssgiIntensity()==35,"Existing effect strengths retained");
|
||||
check(VanillaLight.pbrIntensity()==80&&VanillaLight.ssrIntensity()==20&&VanillaLight.ssgiIntensity()==35,"PBR 80 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();
|
||||
check(!VanillaLight.bloom()&&VanillaLight.bloomIntensity()==20&&VanillaLight.pbr()&&VanillaLight.ssgi(),"Missing fields get new defaults without replacing explicit values");
|
||||
Files.writeString(path,"{\"pbrIntensity\":50}");forgetPreferences();check(VanillaLight.pbrIntensity()==50,"Explicit PBR 50 is preserved");
|
||||
System.out.println("SHADER135_DEFAULTS_PASS fresh, partial and personal preferences");
|
||||
} catch(IOException e){throw new AssertionError(e);}
|
||||
}
|
||||
@@ -565,7 +570,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
||||
}
|
||||
|
||||
static Frame capture(ClientGameTestContext c, String name) {
|
||||
if (name != null) c.takeScreenshot("sanctuary-beta140-shader-" + name);
|
||||
if (name != null) c.takeScreenshot((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) {
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ import java.nio.file.Files;
|
||||
public final class Ssgi132ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c){new PixelShadows122ClientChecks(false,true).runTest(c);}
|
||||
static void run(ClientGameTestContext c,TestServerContext server){
|
||||
c.runOnClient(m->{preferences();VanillaLight.setSunshafts(false);VanillaLight.setLensFlare(false);VanillaLight.setPbr(false);VanillaLight.setEnabled(true);VanillaLight.setSaturation(100);VanillaLight.setSsgi(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);VanillaLight.setPixelShadows(false);VanillaLight.setEdgeHighlights(false);});
|
||||
c.runOnClient(m->{preferences();VanillaLight.setSunshafts(false);VanillaLight.setLensFlare(false);VanillaLight.setPbr(false);VanillaLight.setEnabled(true);VanillaLight.setSaturation(100);VanillaLight.setColoredLights(false);VanillaLight.setSsgi(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);VanillaLight.setPixelShadows(false);VanillaLight.setEdgeHighlights(false);});
|
||||
server.runOnServer(s->{command(s,"time set 6000");for(int x=-5;x<=24;x++)for(int z=-5;z<=24;z++){
|
||||
s.overworld().setBlockAndUpdate(new BlockPos(x,64,z),Blocks.CONCRETE.white().defaultBlockState());
|
||||
for(int y=65;y<=102;y++)s.overworld().setBlockAndUpdate(new BlockPos(x,y,z),Blocks.AIR.defaultBlockState());
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ import net.minecraft.world.level.block.Blocks;
|
||||
public final class SsgiCloseup135ClientChecks implements net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c){new PixelShadows122ClientChecks(false,false,false,true).runTest(c);}
|
||||
static void run(ClientGameTestContext c,TestServerContext server){
|
||||
c.runOnClient(m->{VanillaLight.setEnabled(true);VanillaLight.setSaturation(100);VanillaLight.setSsgi(false);VanillaLight.setPbr(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);VanillaLight.setPixelShadows(false);VanillaLight.setEdgeHighlights(false);});
|
||||
c.runOnClient(m->{VanillaLight.setEnabled(true);VanillaLight.setSaturation(100);VanillaLight.setColoredLights(false);VanillaLight.setSsgi(false);VanillaLight.setPbr(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);VanillaLight.setPixelShadows(false);VanillaLight.setEdgeHighlights(false);});
|
||||
server.runOnServer(s->{
|
||||
// Minecraft 26.3 rejects setting a clock to its existing value.
|
||||
command(s,"time add 1");command(s,"time set 6000");
|
||||
|
||||
+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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
+38
-11
@@ -22,7 +22,10 @@ final class ColoredLightVolume implements AutoCloseable {
|
||||
private ClientLevel level;
|
||||
private Object models;
|
||||
private Build build;
|
||||
private DynamicTexture texture,previous;
|
||||
private DynamicTexture texture,previous,direction,previousDirection;
|
||||
private boolean directional;
|
||||
DynamicTexture direction(){return direction;}
|
||||
DynamicTexture previousDirection(){return previousDirection;}
|
||||
private BlockPos previousOrigin;
|
||||
private int side,previousSide;
|
||||
private long transitionStarted;
|
||||
@@ -40,18 +43,20 @@ final class ColoredLightVolume implements AutoCloseable {
|
||||
if(level!=mc.level||models!=modelSet){close();level=mc.level;models=modelSet;}
|
||||
if(warm!=VanillaLight.warmTorches()){warm=VanillaLight.warmTorches();materials.clear();revision++;if(build!=null){build.close();build=null;}}
|
||||
var p=mc.gameRenderer.gameRenderState().levelRenderState.cameraRenderState.pos;
|
||||
boolean wantedDirection=VanillaLight.pbr()&&VanillaLight.pbrIntensity()>0;
|
||||
int wantedSide=sideFor(VanillaLight.coloredLightDistance());
|
||||
var wanted=new BlockPos(anchor(p.x,wantedSide),anchor(p.y,wantedSide),anchor(p.z,wantedSide));
|
||||
long now=System.nanoTime();
|
||||
if(previous!=null&&blend()>=1){previous.close();previous=null;previousOrigin=null;}
|
||||
if(previous!=null&&blend()>=1){previous.close();previous=null;previousOrigin=null;if(previousDirection!=null){previousDirection.close();previousDirection=null;}}
|
||||
// Complete a snapshot even if the world keeps changing, then catch up; moving
|
||||
// beyond it cancels immediately. Static worlds do no further scanning or flooding.
|
||||
if(build!=null&&(build.side!=wantedSide||!inside(build.origin,BlockPos.containing(p),build.side))){build.close();build=null;}
|
||||
if(build==null&&previous==null&&(texture==null||side!=wantedSide||!wanted.equals(origin)||published!=revision)&&now-lastStart>=100_000_000L){
|
||||
build=new Build(level,wanted,wantedSide,revision,materials);lastStart=now;
|
||||
if(build!=null&&(build.directional!=wantedDirection||build.side!=wantedSide||!inside(build.origin,BlockPos.containing(p),build.side))){build.close();build=null;}
|
||||
if(build==null&&previous==null&&(texture==null||directional!=wantedDirection||side!=wantedSide||!wanted.equals(origin)||published!=revision)&&now-lastStart>=100_000_000L){
|
||||
build=new Build(level,wanted,wantedSide,revision,materials,wantedDirection);lastStart=now;
|
||||
}
|
||||
if(build!=null&&build.advance(now+2_000_000L)){
|
||||
previous=texture;previousOrigin=origin;previousSide=side;
|
||||
previous=texture;previousOrigin=origin;previousSide=side;previousDirection=direction;
|
||||
direction=build.directionImage==null?null:new DynamicTexture(()->"Sanctuary light direction",build.takeDirectionImage());directional=build.directional;
|
||||
texture=new DynamicTexture(()->"Sanctuary coloured light volume",build.takeImage());
|
||||
side=build.side;transitionStarted=System.nanoTime();
|
||||
origin=build.origin;published=build.revision;build.close();build=null;completed++;
|
||||
@@ -68,9 +73,10 @@ final class ColoredLightVolume implements AutoCloseable {
|
||||
BlockPos origin(){return origin;}
|
||||
boolean settled(){return texture!=null&&build==null&&revision==published&&blend()>=1;}
|
||||
int completed(){return completed;}
|
||||
int directionSample(BlockPos p){if(direction==null||!inside(origin,p,side))return 0;int x=p.getX()-origin.getX(),y=p.getY()-origin.getY(),z=p.getZ()-origin.getZ(),cols=columns(side);return direction.getPixels().getPixel(x+(y%cols)*side,z+(y/cols)*side);}
|
||||
int cachedStates(){return materials.cachedStates();}
|
||||
int sample(BlockPos p){if(texture==null||!inside(origin,p,side))return 0;int x=p.getX()-origin.getX(),y=p.getY()-origin.getY(),z=p.getZ()-origin.getZ(),cols=columns(side);return texture.getPixels().getPixel(x+(y%cols)*side,z+(y/cols)*side);}
|
||||
public void close(){if(build!=null){build.close();build=null;}if(texture!=null){texture.close();texture=null;}if(previous!=null){previous.close();previous=null;}materials.clear();level=null;models=null;origin=null;previousOrigin=null;side=previousSide=0;published=-1;revision=0;lastStart=0;}
|
||||
public void close(){if(build!=null){build.close();build=null;}if(texture!=null){texture.close();texture=null;}if(previous!=null){previous.close();previous=null;}if(direction!=null){direction.close();direction=null;}if(previousDirection!=null){previousDirection.close();previousDirection=null;}materials.clear();level=null;models=null;origin=null;previousOrigin=null;side=previousSide=0;published=-1;revision=0;lastStart=0;}
|
||||
|
||||
private static final class Build implements AutoCloseable {
|
||||
final int side,count,cols,chunkSide;
|
||||
@@ -80,11 +86,14 @@ final class ColoredLightVolume implements AutoCloseable {
|
||||
final boolean[] queued;final IntArrayFIFOQueue queue=new IntArrayFIFOQueue();
|
||||
final LevelChunk[] chunks;final int cx,cz;
|
||||
final BlockPos.MutableBlockPos pos=new BlockPos.MutableBlockPos();
|
||||
NativeImage image;int scan,packed;
|
||||
Build(ClientLevel level,BlockPos origin,int side,long revision,ColoredLightMaterials materials){
|
||||
this.side=side;count=side*side*side;cols=columns(side);chunkSide=(side+15)/16+1;
|
||||
NativeImage image,directionImage;final boolean directional;int scan,packed;
|
||||
private static final double[] ENERGY=new double[256];
|
||||
static{for(int i=129;i<256;i++)ENERGY[i]=Math.max(0,(Math.pow(.8,31-i/8.)-Math.pow(.8,15))/(1-Math.pow(.8,15)));}
|
||||
Build(ClientLevel level,BlockPos origin,int side,long revision,ColoredLightMaterials materials,boolean directional){
|
||||
this.directional=directional;this.side=side;count=side*side*side;cols=columns(side);chunkSide=(side+15)/16+1;
|
||||
states=new BlockState[count];red=new byte[count];green=new byte[count];blue=new byte[count];sky=new byte[count];queued=new boolean[count];chunks=new LevelChunk[chunkSide*chunkSide];
|
||||
image=new NativeImage(cols*side,((side+cols-1)/cols)*side,false);
|
||||
if(directional)directionImage=new NativeImage(image.getWidth(),image.getHeight(),false);
|
||||
this.level=level;this.origin=origin;this.revision=revision;this.materials=materials;
|
||||
cx=origin.getX()>>4;cz=origin.getZ()>>4;
|
||||
for(int z=0;z<chunkSide;z++)for(int x=0;x<chunkSide;x++)chunks[x+z*chunkSide]=level.getChunkSource().getChunk(cx+x,cz+z,ChunkStatus.FULL,false);
|
||||
@@ -99,6 +108,7 @@ final class ColoredLightVolume implements AutoCloseable {
|
||||
while(packed<count){
|
||||
int i=packed++,x=i%side,z=i/side%side,y=i/(side*side);
|
||||
image.setPixel(x+(y%cols)*side,z+(y/cols)*side,(sky[i]&255)<<24|(red[i]&255)<<16|(green[i]&255)<<8|(blue[i]&255));
|
||||
if(directional)directionImage.setPixel(x+(y%cols)*side,z+(y/cols)*side,directionAt(i,x,y,z));
|
||||
if((packed&511)==0&&System.nanoTime()>=deadline)return false;
|
||||
}
|
||||
return true;
|
||||
@@ -124,9 +134,26 @@ final class ColoredLightVolume implements AutoCloseable {
|
||||
if(changed)enqueue(n);
|
||||
}
|
||||
}
|
||||
double energy(int i){return ENERGY[Math.max(red[i]&255,Math.max(green[i]&255,blue[i]&255))];}
|
||||
int directionAt(int i,int x,int y,int z){
|
||||
double energy=energy(i);if(energy<=0)return 0;
|
||||
double vx=0,vy=0,vz=0,total=0;
|
||||
for(var d:DIRECTIONS){
|
||||
int nx=x+d.getStepX(),ny=y+d.getStepY(),nz=z+d.getStepZ();
|
||||
if(nx<0||nx>=side||ny<0||ny>=side||nz<0||nz>=side)continue;
|
||||
int n=nx+nz*side+ny*side*side;double w=Math.max(0,energy(n)-energy);
|
||||
if(w<=0||net.minecraft.world.level.lighting.LightEngine.getLightDampeningInto(states[n],states[i],d.getOpposite(),Math.max(1,states[i].getLightDampening()))>=15)continue;
|
||||
vx+=d.getStepX()*w;vy+=d.getStepY()*w;vz+=d.getStepZ()*w;total+=w;
|
||||
}
|
||||
// The vector length is confidence: opposing arrivals cancel instead of flipping.
|
||||
double scale=total>1e-12?127/total:0;
|
||||
int a=(int)Math.round(255*energy/(energy+.08));
|
||||
return a<<24|(int)Math.round(128+vx*scale)<<16|(int)Math.round(128+vy*scale)<<8|(int)Math.round(128+vz*scale);
|
||||
}
|
||||
NativeImage takeDirectionImage(){var result=directionImage;directionImage=null;return result;}
|
||||
void enqueue(int i){if(!queued[i]){queued[i]=true;queue.enqueue(i);}}
|
||||
NativeImage takeImage(){var result=image;image=null;return result;}
|
||||
public void close(){if(image!=null){image.close();image=null;}}
|
||||
public void close(){if(image!=null){image.close();image=null;}if(directionImage!=null){directionImage.close();directionImage=null;}}
|
||||
}
|
||||
// An offset preserves dim colour channels below native level zero. The shader
|
||||
// applies the native cutoff to overall energy, never to each channel separately.
|
||||
|
||||
@@ -39,15 +39,15 @@ public final class ColoredLights {
|
||||
private static float displayedDistance;
|
||||
private static int dynamicCount;
|
||||
private static MappableRingBuffer uniforms;
|
||||
private static boolean applied;
|
||||
private static boolean applied,prepared;
|
||||
private static int frames;
|
||||
private ColoredLights(){}
|
||||
public static void captureProjection(Matrix4f matrix){projection.set(matrix);}
|
||||
public static void dirtyBlock(BlockPos pos){VOLUME.dirty(pos);}
|
||||
public static void dirtyChunk(int x,int z){VOLUME.dirtyChunk(x,z);}
|
||||
public static void dirtySection(int x,int y,int z){if(!fr.koka.sanctuary.client.DynamicLightClient.invalidatingSections())VOLUME.dirtySection(x,y,z);}
|
||||
public static void render(){
|
||||
applied=false;var mc=Minecraft.getInstance();
|
||||
public static void prepare(){
|
||||
applied=false;prepared=false;var mc=Minecraft.getInstance();
|
||||
if(!VanillaLight.active()||!VanillaLight.coloredLights()||VanillaLight.coloredLightIntensity()==0||mc.level==null){release();return;}
|
||||
if(PanoramaCapture.rendering())return;
|
||||
var camera=mc.gameRenderer.gameRenderState().levelRenderState.cameraRenderState;
|
||||
@@ -78,6 +78,20 @@ public final class ColoredLights {
|
||||
for(int i=0;i<ColoredDynamicLights.LIMIT;i++){if(i<lights.size()){var l=lights.get(i);data.putVec4((float)(l.x-camera.pos.x),(float)(l.y-camera.pos.y),(float)(l.z-camera.pos.z),l.level);}else data.putVec4(0,0,0,0);}
|
||||
for(int i=0;i<ColoredDynamicLights.LIMIT;i++){if(i<lights.size()){var l=lights.get(i);data.putVec4((float)l.r,(float)l.g,(float)l.b,(float)l.visibility);}else data.putVec4(0,0,0,0);}
|
||||
}
|
||||
prepared=true;
|
||||
}
|
||||
static void bindDirections(com.mojang.renderpearl.api.commands.RenderPass pass){
|
||||
var nearest=RenderSystem.getSamplerCache().getClampToEdge(FilterMode.NEAREST);
|
||||
pass.setUniform("ColoredLightScene",uniforms.currentBuffer());
|
||||
pass.setUniform("LightDirection",(VOLUME.direction()==null?black:VOLUME.direction()).getTextureView(),nearest);
|
||||
pass.setUniform("PreviousDirection",(VOLUME.previousDirection()==null?black:VOLUME.previousDirection()).getTextureView(),nearest);
|
||||
}
|
||||
static boolean directionalReady(){return prepared&&VanillaLight.pbr();}
|
||||
public static void render(){
|
||||
if(!prepared)return;
|
||||
var main=Minecraft.getInstance().gameRenderer.mainRenderTarget();
|
||||
var current=VOLUME.texture()==null?black:VOLUME.texture();
|
||||
var previous=VOLUME.previous()==null?black:VOLUME.previous();
|
||||
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);
|
||||
@@ -87,7 +101,7 @@ public final class ColoredLights {
|
||||
}
|
||||
applied=true;frames++;
|
||||
}
|
||||
public static void release(){VOLUME.close();DYNAMIC.clear();activated=lastRender=0;dynamicCount=0;if(black!=null){black.close();black=null;}if(source!=null){source.destroyBuffers();source=null;}if(uniforms!=null){uniforms.close();uniforms=null;}applied=false;}
|
||||
public static void release(){prepared=false;VOLUME.close();DYNAMIC.clear();activated=lastRender=0;dynamicCount=0;if(black!=null){black.close();black=null;}if(source!=null){source.destroyBuffers();source=null;}if(uniforms!=null){uniforms.close();uniforms=null;}applied=false;}
|
||||
static boolean applied(){return applied;}
|
||||
static boolean allocated(){return source!=null||VOLUME.texture()!=null;}
|
||||
static boolean settled(){return VOLUME.settled();}
|
||||
@@ -98,5 +112,6 @@ public final class ColoredLights {
|
||||
static int dynamicCount(){return dynamicCount;}
|
||||
static float blend(){return VOLUME.blend();}
|
||||
static int volumeSide(){return VOLUME.side();}
|
||||
static int directionSample(BlockPos pos){return VOLUME.directionSample(pos);}
|
||||
static int materials(){return VOLUME.cachedStates();}
|
||||
}
|
||||
|
||||
@@ -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,10 +17,18 @@ 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);
|
||||
@@ -39,7 +47,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 +73,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,11 +25,19 @@ 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;
|
||||
private long bytes;
|
||||
private int completed;
|
||||
private Object models,level;
|
||||
private List<Section> visible=List.of();
|
||||
record Entry(BlockPos origin,GpuBuffer vertices,int indices){}
|
||||
@@ -40,12 +48,16 @@ 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.pbrDistance()+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()));
|
||||
var section=sections.get(key);
|
||||
if(section==null||section.storage!=storage){if(section!=null)remove(section);section=new Section(key,pos.immutable(),storage);sections.put(key,section);}
|
||||
if(section==null||section.storage!=storage){
|
||||
Entry retained=section==null?null:section.entry;
|
||||
if(section!=null){section.entry=null;remove(section);}
|
||||
section=new Section(key,pos.immutable(),storage);section.entry=retained;sections.put(key,section);
|
||||
}
|
||||
section.seen=frame;active.add(section);
|
||||
}
|
||||
visible=active;
|
||||
@@ -56,23 +68,30 @@ final class PbrTerrain implements AutoCloseable {
|
||||
if(started++>=4)break;
|
||||
Section next=active.stream().filter(s->s.dirty).min(Comparator.comparingDouble(s->s.origin.distToCenterSqr(camera))).orElse(null);
|
||||
if(next==null)break;
|
||||
if(!next.storage.maybeHas(materials::candidate)){next.dirty=false;continue;}
|
||||
if(!next.storage.maybeHas(this::candidate)){releaseEntry(next);next.dirty=false;continue;}
|
||||
building=new Build(next);
|
||||
}
|
||||
if(!advance(building,deadline))break;
|
||||
var done=building;building=null;
|
||||
try(done){
|
||||
done.section.dirty=false;
|
||||
done.section.dirty=done.section.revision!=done.revision;
|
||||
Entry replacement=null;
|
||||
if(done.quads>0&&!done.overflow){
|
||||
try(var mesh=done.out.buildOrThrow()){
|
||||
long size=mesh.vertexBuffer().remaining();
|
||||
if(bytes+size<=MAX_BYTES){
|
||||
long replaced=done.section.entry==null?0:done.section.entry.vertices.size();
|
||||
if(bytes-replaced+size<=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);
|
||||
done.section.entry=new Entry(done.section.origin,buffer,count);bytes+=buffer.size();
|
||||
replacement=new Entry(done.section.origin,buffer,count);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Publish atomically. A dirty section stays visible while its new
|
||||
// geometry is built, including when light updates arrive mid-build.
|
||||
releaseEntry(done.section);done.section.entry=replacement;
|
||||
if(replacement!=null)bytes+=replacement.vertices.size();
|
||||
completed++;
|
||||
}
|
||||
}
|
||||
return active.stream().map(s->s.entry).filter(Objects::nonNull).toList();
|
||||
@@ -82,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);
|
||||
@@ -104,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++;
|
||||
}
|
||||
@@ -118,18 +143,23 @@ final class PbrTerrain implements AutoCloseable {
|
||||
void dirtySection(int x,int y,int z){dirty(sections.get(SectionPos.asLong(x,y,z)));}
|
||||
void dirty(BlockPos pos){for(int x=(pos.getX()-1)>>4;x<=(pos.getX()+1)>>4;x++)for(int y=(pos.getY()-1)>>4;y<=(pos.getY()+1)>>4;y++)for(int z=(pos.getZ()-1)>>4;z<=(pos.getZ()+1)>>4;z++)dirty(sections.get(SectionPos.asLong(x,y,z)));}
|
||||
void dirtyChunk(int x,int z){for(var section:sections.values())if(Math.abs((section.origin.getX()>>4)-x)<=1&&Math.abs((section.origin.getZ()>>4)-z)<=1)dirty(section);}
|
||||
private void dirty(Section section){if(section!=null){remove(section);section.dirty=true;}}
|
||||
private void remove(Section section){if(building!=null&&building.section==section){building.close();building=null;}if(section.entry!=null){bytes-=section.entry.vertices.size();section.entry.vertices.close();section.entry=null;}}
|
||||
private void dirty(Section section){if(section!=null){section.revision++;section.dirty=true;}}
|
||||
private void releaseEntry(Section section){if(section.entry!=null){bytes-=section.entry.vertices.size();section.entry.vertices.close();section.entry=null;}}
|
||||
private void remove(Section section){if(building!=null&&building.section==section){building.close();building=null;}releaseEntry(section);}
|
||||
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;}
|
||||
boolean settled(){return building==null&&visible.stream().noneMatch(s->s.dirty);}
|
||||
public void close(){if(building!=null){building.close();building=null;}for(var section:sections.values())remove(section);sections.clear();visible=List.of();materials.close();models=level=null;bytes=0;}
|
||||
private static final class Section{
|
||||
final long key;final BlockPos origin;final LevelChunkSection storage;long seen;boolean dirty=true;Entry entry;
|
||||
final long key;final BlockPos origin;final LevelChunkSection storage;long seen,revision;boolean dirty=true;Entry entry;
|
||||
Section(long key,BlockPos origin,LevelChunkSection storage){this.key=key;this.origin=origin;this.storage=storage;}
|
||||
}
|
||||
private static final class Build implements AutoCloseable{
|
||||
final Section section;final ByteBufferBuilder buffer=new ByteBufferBuilder(65536);final BufferBuilder out=new BufferBuilder(buffer,PrimitiveTopology.QUADS,FORMAT);
|
||||
final Section section;final long revision;final ByteBufferBuilder buffer=new ByteBufferBuilder(65536);final BufferBuilder out=new BufferBuilder(buffer,PrimitiveTopology.QUADS,FORMAT);
|
||||
int cursor,quads;boolean overflow;
|
||||
Build(Section section){this.section=section;}
|
||||
Build(Section section){this.section=section;revision=section.revision;}
|
||||
public void close(){buffer.close();}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.pbrDistance())
|
||||
.putVec4(Math.max(128,VanillaLight.pbrDistance()),32,0,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;}
|
||||
}
|
||||
@@ -28,23 +28,25 @@ public final class ProceduralPbr {
|
||||
private static final BindGroupLayout SCENE=BindGroupLayout.builder().withUniform("PbrScene",UniformType.UNIFORM_BUFFER)
|
||||
.withUniform("PixelShadows",UniformType.UNIFORM_BUFFER).withUniform("SunDepth",UniformType.COMBINED_IMAGE_SAMPLER)
|
||||
.withUniform("WorldDepth",UniformType.COMBINED_IMAGE_SAMPLER).withUniform("SceneColor",UniformType.COMBINED_IMAGE_SAMPLER)
|
||||
.withUniform("ColoredLightScene",UniformType.UNIFORM_BUFFER).withUniform("LightDirection",UniformType.COMBINED_IMAGE_SAMPLER).withUniform("PreviousDirection",UniformType.COMBINED_IMAGE_SAMPLER)
|
||||
.withUniform("BlockAtlas",UniformType.COMBINED_IMAGE_SAMPLER).withUniform("MaterialAtlas",UniformType.COMBINED_IMAGE_SAMPLER).build();
|
||||
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){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;}
|
||||
@@ -52,10 +54,14 @@ public final class ProceduralPbr {
|
||||
if(PanoramaCapture.rendering()||camera.fogType!=FogType.NONE||sky.skybox!=DimensionType.Skybox.OVERWORLD)return;
|
||||
var main=mc.gameRenderer.mainRenderTarget();
|
||||
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);}
|
||||
// The extra padding also supplies a valid inactive PixelShadows binding (192 bytes).
|
||||
if(scene==null)scene=new MappableRingBuffer(()->"Sanctuary PBR scene",GpuBuffer.USAGE_UNIFORM|GpuBuffer.USAGE_MAP_WRITE,192);
|
||||
var entries=TERRAIN.update();var atlas=TERRAIN.materials.atlas();
|
||||
// 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 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));
|
||||
@@ -64,8 +70,8 @@ public final class ProceduralPbr {
|
||||
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,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(0,0,0,0).putVec4(0,0,0,0).putVec4(0,0,0,0).putVec4(0,0,0,0);
|
||||
}
|
||||
var transforms=new ArrayList<GpuBufferSlice>(entries.size());int max=0;
|
||||
@@ -75,19 +81,36 @@ 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.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);}
|
||||
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("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();}
|
||||
static boolean terrainBuilding(){return TERRAIN.building();}
|
||||
static int materialCount(){return TERRAIN.materials.count();}static int analyses(){return TERRAIN.materials.analyses();}
|
||||
}
|
||||
|
||||
@@ -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,12 @@ 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(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.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.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(
|
||||
|
||||
@@ -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,10 @@ public final class VanillaLight {
|
||||
boolean lensFlare=true;
|
||||
int lensFlareIntensity=30;
|
||||
boolean pbr=true;
|
||||
int pbrIntensity=50;
|
||||
int pbrIntensity=80;
|
||||
int pbrDistance=32;
|
||||
boolean ssr=false;
|
||||
int ssrIntensity=20;
|
||||
boolean ssgi=true;
|
||||
boolean coloredLights=true;
|
||||
boolean warmTorches=false;
|
||||
@@ -47,8 +55,15 @@ 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);
|
||||
preferences.pbrDistance=normalizePbrDistance(preferences.pbrDistance);
|
||||
preferences.pbrIntensity=Math.clamp(preferences.pbrIntensity,0,100);
|
||||
preferences.coloredLightIntensity=Math.clamp(preferences.coloredLightIntensity,0,100);
|
||||
preferences.coloredLightDistance=normalizeColoredLightDistance(preferences.coloredLightDistance);
|
||||
@@ -58,6 +73,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 +83,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 +106,13 @@ 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 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();}
|
||||
|
||||
+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) { 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(); }
|
||||
}
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package fr.koka.sanctuary.mixin.client;
|
||||
|
||||
import com.mojang.renderpearl.api.device.GpuBackend;
|
||||
import net.minecraft.client.PreferredGraphicsApi;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
/** Sanctuary's default uses native Vulkan with Minecraft's OpenGL fallback. */
|
||||
@Mixin(PreferredGraphicsApi.class)
|
||||
abstract class PreferredGraphicsApiMixin {
|
||||
@Inject(method="getBackendsToTry", at=@At("HEAD"), cancellable=true)
|
||||
private void sanctuary$vulkanDefault(CallbackInfoReturnable<GpuBackend[]> ci) {
|
||||
if ((Object)this == PreferredGraphicsApi.DEFAULT)
|
||||
ci.setReturnValue(PreferredGraphicsApi.VULKAN.getBackendsToTry());
|
||||
}
|
||||
@Inject(method="caption", at=@At("HEAD"), cancellable=true)
|
||||
private void sanctuary$defaultCaption(CallbackInfoReturnable<Component> ci) {
|
||||
if ((Object)this == PreferredGraphicsApi.DEFAULT)
|
||||
ci.setReturnValue(Component.translatable("sanctuary.graphics.default_vulkan"));
|
||||
}
|
||||
}
|
||||
@@ -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,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));
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,9 @@ import net.minecraft.world.level.levelgen.Heightmap;
|
||||
|
||||
/** Melt only known weather deposits, using the existing precipitation column sampler. */
|
||||
public final class SeasonalSnowMelt {
|
||||
/** One real day lasts 72 native Minecraft days. Keep the native sampler, slow only its melt chance. */
|
||||
public static final int REALTIME_SCALE = 72;
|
||||
|
||||
public static final GameRule<Boolean> ENABLED = GameRuleBuilder.forBoolean(true)
|
||||
.category(GameRuleCategory.UPDATES).buildAndRegister(SanctuaryMod.id("seasonal_snow_melt"));
|
||||
|
||||
@@ -77,12 +80,7 @@ public final class SeasonalSnowMelt {
|
||||
var chunk = level.getChunkAt(column);
|
||||
var entries = chunk.getAttached(NATURAL);
|
||||
if (entries == null || entries.isEmpty()) return;
|
||||
int chance = switch (WeatherService.season(level.getServer())) {
|
||||
case WINTER -> 0;
|
||||
case SPRING -> 2;
|
||||
case SUMMER -> 1;
|
||||
case AUTUMN -> 4;
|
||||
};
|
||||
int chance = chance(WeatherService.season(level.getServer()));
|
||||
if (chance == 0 || level.getRandom().nextInt(chance) != 0) return;
|
||||
var pos = level.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING, column);
|
||||
// Thin layers are above the heightmap; a full snow block is just below it.
|
||||
@@ -103,5 +101,15 @@ public final class SeasonalSnowMelt {
|
||||
if (level.setBlockAndUpdate(pos, after)) remember(level, pos, entry.base, after);
|
||||
}
|
||||
|
||||
public static int chance(SeasonalWeather.Season season) {
|
||||
int seasonal = switch (season) {
|
||||
case WINTER -> 0;
|
||||
case SPRING -> 2;
|
||||
case SUMMER -> 1;
|
||||
case AUTUMN -> 4;
|
||||
};
|
||||
return seasonal == 0 ? 0 : seasonal * REALTIME_SCALE;
|
||||
}
|
||||
|
||||
private static String key(BlockPos pos) { return Long.toString(pos.asLong()); }
|
||||
}
|
||||
|
||||
@@ -16,8 +16,11 @@ import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.gamerules.GameRule;
|
||||
import net.minecraft.world.level.gamerules.GameRuleCategory;
|
||||
|
||||
/** One real layer per native precipitation tick; no additional chunk or entity scan. */
|
||||
/** Weather snow accumulation, using the native precipitation sampler without chunk scans. */
|
||||
public final class SnowAccumulation {
|
||||
/** Snowfall should remain visible during a session, but no longer stack instantly. */
|
||||
public static final int REALTIME_SCALE = 12;
|
||||
|
||||
public static final GameRule<Integer> MAX_HEIGHT = GameRuleBuilder.forInteger(2)
|
||||
.range(-1, 128).category(GameRuleCategory.UPDATES)
|
||||
.buildAndRegister(SanctuaryMod.id("snow_accumulation_height"));
|
||||
@@ -38,7 +41,7 @@ public final class SnowAccumulation {
|
||||
/** Return whether vanilla should perform its own deposit. */
|
||||
public static boolean replaceNative(ServerLevel level, BlockPos pos, boolean canSnow) {
|
||||
if (!canSnow || !ENABLED.contains(level.getServer())) return canSnow;
|
||||
if (canSnow) deposit(level, pos);
|
||||
if (level.getRandom().nextInt(REALTIME_SCALE) == 0) deposit(level, pos);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
{
|
||||
"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": "Landscape reflections — experimental",
|
||||
"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.",
|
||||
"sanctuary.weather.overcast": "Overcast",
|
||||
@@ -2295,10 +2301,10 @@
|
||||
"sanctuary.shaders.ssgi_intensity_help": "Strength of diffuse color bounce. Zero stops rendering and frees its resources. Does not change world light.",
|
||||
"sanctuary.shaders.ssgi_intensity_value": "SSGI: %s%%",
|
||||
"sanctuary.shaders.pbr": "PBR · material relief",
|
||||
"sanctuary.shaders.pbr_help": "Experimental: procedural block normals and roughness, sun and moon highlights. Range 32 blocks. ON 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_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 intensity",
|
||||
"sanctuary.shaders.pbr_intensity_help": "Material relief and highlights, including water. Default: 80%.",
|
||||
"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",
|
||||
@@ -2318,5 +2324,16 @@
|
||||
"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 and SSR distance",
|
||||
"sanctuary.shaders.pbr_distance_value": "PBR and SSR distance: %s blocks",
|
||||
"sanctuary.shaders.pbr_distance_help": "Shared range for relief and reflecting surfaces. 16, 32, 64, 128 or 256 blocks. Longer distances cost more; mesh memory is bounded.",
|
||||
"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."
|
||||
}
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
{
|
||||
"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": "Reflets du paysage — expérimental",
|
||||
"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.",
|
||||
"sanctuary.weather.overcast": "Ciel gris",
|
||||
@@ -2295,10 +2301,10 @@
|
||||
"sanctuary.shaders.ssgi_intensity_help": "Force de la teinte diffusée. Zéro coupe le calcul et libère ses ressources. Aucun changement de lumière du monde.",
|
||||
"sanctuary.shaders.ssgi_intensity_value": "SSGI : %s %%",
|
||||
"sanctuary.shaders.pbr": "PBR · relief des matériaux",
|
||||
"sanctuary.shaders.pbr_help": "Expérimental : normales procédurales et rugosité des blocs, reflets du soleil et de la lune. Portée 32 blocs. Activé par défaut.",
|
||||
"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_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é PBR",
|
||||
"sanctuary.shaders.pbr_intensity_help": "Relief et éclats des matières, eau comprise. Défaut : 80 %.",
|
||||
"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",
|
||||
@@ -2318,5 +2324,16 @@
|
||||
"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 et SSR",
|
||||
"sanctuary.shaders.pbr_distance_value": "Distance PBR et SSR : %s blocs",
|
||||
"sanctuary.shaders.pbr_distance_help": "Portée commune du relief et des surfaces réfléchissantes. 16, 32, 64, 128 ou 256 blocs. Les distances élevées coûtent davantage ; budget de maillage limité.",
|
||||
"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."
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -5,6 +5,20 @@ uniform sampler2D SceneColor;
|
||||
uniform sampler2D BlockAtlas;
|
||||
uniform sampler2D MaterialAtlas;
|
||||
uniform sampler2D SunDepth;
|
||||
uniform sampler2D LightDirection;
|
||||
uniform sampler2D PreviousDirection;
|
||||
layout(std140) uniform ColoredLightScene {
|
||||
mat4 LocalInverseViewProjection;
|
||||
vec4 CameraStrength;
|
||||
vec4 ClipSkyFog;
|
||||
vec4 FogDistance;
|
||||
vec4 VolumeSize;
|
||||
vec4 Atlas;
|
||||
vec4 PreviousCamera;
|
||||
vec4 PreviousAtlas;
|
||||
vec4 DynamicPosition[128];
|
||||
vec4 DynamicColor[128];
|
||||
};
|
||||
layout(std140) uniform PbrScene {
|
||||
mat4 PbrInverseVP;
|
||||
vec4 PbrSun;
|
||||
@@ -34,7 +48,55 @@ float visibility(vec3 p,vec3 n){
|
||||
vec3 q=light.xyz/light.w;q.xy=q.xy*0.5+0.5;
|
||||
if(DistanceClipFog.y<0.5)q.z=q.z*0.5+0.5;
|
||||
if(any(lessThan(q,vec3(0)))||any(greaterThan(q,vec3(1))))return 1.0;
|
||||
return q.z>texture(SunDepth,q.xy).r+0.00015?0.0:1.0;
|
||||
return q.z>textureLod(SunDepth,q.xy,0.0).r+0.00015?0.0:1.0;
|
||||
}
|
||||
// Cached direction moments; filtering moments before normalization avoids unstable
|
||||
// switches between nearby/opposed sources. Alpha stores the light response.
|
||||
vec4 directionField(sampler2D field,vec3 p,vec4 atlas){
|
||||
if(textureSize(field,0).x<int(atlas.z))return vec4(0);
|
||||
vec3 edges=min(p,vec3(atlas.x)-p);float margin=smoothstep(14.0,18.0,min(edges.x,min(edges.y,edges.z)));
|
||||
if(margin<=0.0)return vec4(0);
|
||||
int side=int(atlas.x),cols=int(atlas.y);vec3 at=p-.5;ivec3 base=ivec3(floor(at));vec3 f=fract(at);vec4 sum=vec4(0);
|
||||
for(int y=0;y<2;y++)for(int z=0;z<2;z++)for(int x=0;x<2;x++){
|
||||
ivec3 q=clamp(base+ivec3(x,y,z),ivec3(0),ivec3(side-1));
|
||||
vec4 cell=texelFetch(field,ivec2(q.x+(q.y%cols)*side,q.z+(q.y/cols)*side),0);
|
||||
float w=(x==0?1.0-f.x:f.x)*(y==0?1.0-f.y:f.y)*(z==0?1.0-f.z:f.z);
|
||||
sum+=vec4((cell.rgb*255.0-128.0)/127.0*cell.a,cell.a)*w;
|
||||
}
|
||||
return sum*margin;
|
||||
}
|
||||
vec4 localArrival(vec3 p,vec3 normal){
|
||||
if(PbrEnvironment.y<.5)return vec4(0);
|
||||
vec3 offset=normal*.51;
|
||||
vec4 field=directionField(LightDirection,p+CameraStrength.xyz+offset,Atlas);
|
||||
if(VolumeSize.y<1.0){
|
||||
vec4 before=PreviousCamera.w>.5?directionField(PreviousDirection,p+PreviousCamera.xyz+offset,PreviousAtlas):vec4(0);
|
||||
field=mix(before,field,smoothstep(0.0,1.0,VolumeSize.y));
|
||||
}
|
||||
for(int i=0;i<int(VolumeSize.w);i++){
|
||||
vec3 delta=DynamicPosition[i].xyz-p;float distance=length(delta),radius=DynamicPosition[i].w;
|
||||
if(distance>=radius||distance<.001)continue;
|
||||
float power=max(0.0,(pow(.8,15.0-radius+distance)-pow(.8,15.0))/(1.0-pow(.8,15.0)));
|
||||
float weight=power/(power+.08)*smoothstep(0.0,1.0,DynamicColor[i].a);
|
||||
field+=vec4(delta/distance*weight,weight);
|
||||
}
|
||||
float magnitude=length(field.xyz);if(magnitude<.00001||field.a<.00001)return vec4(0);
|
||||
float confidence=smoothstep(.02,1.0,magnitude/field.a);
|
||||
float fade=(1.0-smoothstep(VolumeSize.x*.75,VolumeSize.x,length(p)))*smoothstep(0.0,1.0,VolumeSize.z);
|
||||
return vec4(field.xyz/magnitude,confidence*min(field.a,1.0)*fade*clamp(CameraStrength.w/0.3,0.0,1.0));
|
||||
}
|
||||
vec3 localShine(vec3 n,vec3 v,vec3 l,vec4 material,vec3 albedo){
|
||||
vec3 halfVector=l+v;float size=dot(halfVector,halfVector);if(size<.00001)return vec3(0);
|
||||
vec3 h=halfVector*inversesqrt(size);
|
||||
float nl=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,.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(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.
|
||||
@@ -42,37 +104,52 @@ void main(){
|
||||
vec2 tx=dFdx(blockUv),ty=dFdy(blockUv);
|
||||
float det=tx.x*ty.y-tx.y*ty.x;
|
||||
vec3 baseNormal=normalize(cross(dx,dy));if(dot(baseNormal,-relativePosition)<0.0)baseNormal=-baseNormal;
|
||||
vec2 uv=gl_FragCoord.xy/vec2(textureSize(WorldDepth,0));float depth=texture(WorldDepth,uv).r;
|
||||
vec4 projected=PbrInverseVP*vec4(uv*2.0-1.0,PbrParams.y>0.5?depth:depth*2.0-1.0,1.0);
|
||||
vec3 visible=projected.xyz/projected.w;
|
||||
if(depth<=0.0||abs(length(visible)-length(relativePosition))>0.035||length(relativePosition)>=PbrParams.w||abs(det)<1e-12)discard;
|
||||
vec4 texel=texture(BlockAtlas,blockUv);if(texel.a<0.99)discard;
|
||||
// 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.
|
||||
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=texture(MaterialAtlas,maskUv);
|
||||
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);
|
||||
vec3 n=normalize(baseNormal+tangent*slope.x+bitangent*slope.y);
|
||||
vec3 l=normalize(PbrSun.xyz),v=normalize(-relativePosition),h=normalize(l+v);
|
||||
float nl=max(dot(n,l),0.0),nv=max(dot(n,v),0.001),nh=max(dot(n,h),0.0),vh=max(dot(v,h),0.0);
|
||||
float roughness=max(material.b,0.25),alpha=roughness*roughness,a2=alpha*alpha;
|
||||
float denom=nh*nh*(a2-1.0)+1.0;
|
||||
float distribution=a2/max(3.14159265*denom*denom,0.00001);
|
||||
float k=(roughness+1.0)*(roughness+1.0)/8.0;
|
||||
float geometry=nv/(nv*(1.0-k)+k)*nl/max(nl*(1.0-k)+k,0.00001);
|
||||
// Reject degenerate/interpolated geometry without replacing the native scene with NaNs.
|
||||
if(any(isnan(n))||any(isinf(n))||dot(relativePosition,relativePosition)<.000001)discard;
|
||||
vec3 l=normalize(PbrSun.xyz),v=normalize(-relativePosition);
|
||||
float nl=clamp(dot(n,l),0.0,1.0);
|
||||
vec3 albedo=texel.rgb*tint.rgb;albedo*=albedo;
|
||||
vec3 f0=mix(vec3(0.04),albedo,material.a);
|
||||
vec3 fresnel=f0+(1.0-f0)*pow(1.0-vh,5.0);
|
||||
vec3 specular=distribution*geometry*fresnel/max(4.0*nv*nl,0.0001)*nl;
|
||||
// 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?.34:.24);
|
||||
vec3 specular=localShine(n,v,l,solarMaterial,albedo);
|
||||
// PbrEnvironment.z identifies sunlight: moonlight keeps its modest base radiance.
|
||||
float solarRadiance=mix(1.0,family>1.5?20.0:4.0,PbrEnvironment.z);
|
||||
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)
|
||||
*(1.0-smoothstep(PbrParams.w*.75,PbrParams.w,length(relativePosition)));
|
||||
if(amount<=0.0001)discard;
|
||||
vec3 scene=texture(SceneColor,uv).rgb;vec3 linear=scene*scene;
|
||||
vec4 arrival=localArrival(relativePosition,baseNormal);
|
||||
float localAmount=arrival.w*(1.0-fog*PbrEnvironment.x)*(1.0-smoothstep(PbrParams.w*.75,PbrParams.w,length(relativePosition)));
|
||||
if(amount<=0.0001&&localAmount<=0.0001)discard;
|
||||
vec3 scene=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);
|
||||
vec3 shine=min(specular*PbrParams.x*amount*3.0,vec3(0.5));
|
||||
if(localAmount>.0001){
|
||||
float localRelief=family<.5?(max(dot(n,arrival.xyz),0.0)-max(dot(baseNormal,arrival.xyz),0.0))*.7:0.0;
|
||||
lit=clamp(lit*(1.0+localRelief*localAmount),0.0,1.0);
|
||||
}
|
||||
// Preserve bright solar highlights instead of capping them at a medium grey.
|
||||
vec3 shine=min((specular*amount*solarRadiance+localShine(n,v,arrival.xyz,material,albedo)*localAmount)*intensity*5.0,vec3(8.0));
|
||||
lit+=(1.0-lit)*(1.0-exp(-shine));
|
||||
if(any(isnan(lit))||any(isinf(lit)))discard;
|
||||
fragColor=vec4(sqrt(clamp(lit,0.0,1.0)),1.0);
|
||||
}
|
||||
|
||||
@@ -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)>=ReflectionParams.w)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",
|
||||
@@ -277,7 +280,13 @@
|
||||
"client.SculptureParticlesMixin",
|
||||
"client.PixelShadowRendererMixin",
|
||||
"client.PixelShadowLevelMixin",
|
||||
"client.PixelShadowDirtyMixin"
|
||||
"client.PixelShadowDirtyMixin",
|
||||
"client.PreferredGraphicsApiMixin",
|
||||
"client.NaturalWaterRegionMixin",
|
||||
"client.NaturalWaterFluidMixin",
|
||||
"client.NaturalWaterFogMixin",
|
||||
"client.NaturalWaterFogColorMixin",
|
||||
"client.NaturalWaterSectionCopyAccess"
|
||||
],
|
||||
"injectors": {
|
||||
"defaultRequire": 1
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
name = "Sanctuary"
|
||||
author = "KOKA99CAB"
|
||||
version = "beta.140"
|
||||
version = "beta.147"
|
||||
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