Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9791d178ab | ||
|
|
32158e2e05 | ||
|
|
3bfb3cb251 | ||
|
|
0eb1a381d4 | ||
|
|
b97ec6c9e8 | ||
|
|
448e2febff | ||
|
|
1c23d0c17e | ||
|
|
6f871446bb | ||
|
|
96a36d99be | ||
|
|
cf9a670f44 | ||
|
|
ed2d088272 | ||
|
|
0cfd51eb4f | ||
|
|
38f4d6c9a9 | ||
|
|
580b2414da | ||
|
|
97ff2b54cb | ||
|
|
59346d7f0a | ||
|
|
ea151dd5df | ||
|
|
7ce01c47f2 | ||
|
|
93ed5574a4 | ||
|
|
c9f5c56e68 | ||
|
|
6e1b3b7108 | ||
|
|
c05c338e6b | ||
|
|
dc33ffa546 | ||
|
|
9d84a02ed9 | ||
|
|
91181865d5 | ||
|
|
daf36fdeac | ||
|
|
7f576d14d9 | ||
|
|
2d7072b8a8 | ||
|
|
0781cbd964 | ||
|
|
71296a9cfb | ||
|
|
07d09c8508 | ||
|
|
7fada570ef | ||
|
|
b7cf4bc294 | ||
|
|
fc4a0cb68e | ||
|
|
17172fd18b | ||
|
|
216d3f75b5 | ||
|
|
e48fc656c7 | ||
|
|
7e21781a9a | ||
|
|
d941b91265 | ||
|
|
078d3e6833 | ||
|
|
afa77f3d62 | ||
|
|
534dfec4f8 | ||
|
|
26e305d15f |
@@ -11,6 +11,7 @@ La vision est dans `docs/vision.md` ; elle décrit aussi des fonctionnalités fu
|
||||
- Ne pas modifier un monde existant, régénérer des chunks, changer un format de sauvegarde ou activer une expansion sans contrat de migration explicite.
|
||||
- Garder les identifiants `sanctuary:*` stables et documenter toute évolution de la génération avec graine et version.
|
||||
- Ajouter les libellés FR/EN des nouvelles interfaces. Vérifier les dépendances pour la version Minecraft exacte ; aucune compatibilité supposée à partir du nom d'un mod.
|
||||
- Les tests graphiques Sanctuary ciblent uniquement Vulkan. OpenGL est abandonné comme cible de validation depuis le 18 septembre 2026 : ne plus lancer de suite OpenGL ni revendiquer sa prise en charge à partir des essais historiques.
|
||||
- Lancer `./gradlew check build` pour livrer du code, et `./gradlew assemblePack` si la distribution change. Ajouter seulement les tests utiles au comportement touché.
|
||||
- Pour chaque nouvelle livraison du mod, incrémenter le compteur `beta.xxx` (départ `beta.001`) dans `mod_version` et `pack_version` de `gradle.properties`, synchroniser `packwiz/pack.toml` et documenter le changement. Le tag reprend cette version exacte, sans préfixe `v` ; voir `docs/versioning.md`. Une simple modification documentaire n'incrémente pas les binaires.
|
||||
- Le pack Beta suit un canal packwiz stable et une seule instance Prism. Pour une mise à jour demandée, suivre `docs/packwiz.md` : publier un artefact vérifié et immuable, avancer le canal, puis synchroniser l'instance existante en conservant ses sauvegardes et réglages.
|
||||
|
||||
@@ -1,5 +1,80 @@
|
||||
# Sanctuary
|
||||
|
||||
## beta.157 — photo obligatoire dans la Gazette (pack local)
|
||||
|
||||
Galerie de captures défilante, aperçu et assignation à un article. Publication
|
||||
avec photo obligatoire, stockage fichier ou MariaDB partagé avec le site.
|
||||
[Contrat de migration et vérifications](docs/gazette-photos-beta157.md).
|
||||
|
||||
## beta.156 — reprendre la partie sous la colonne communautaire (pack local)
|
||||
|
||||
Le bouton Reprendre la partie est aligné sous la Gazette et le tableau
|
||||
d’affichage, en bas à droite. [Vérifications](docs/pause-resume-beta156.md).
|
||||
|
||||
## beta.155 — nouvelle structure du menu pause (pack local)
|
||||
|
||||
Navigation Sanctuary à gauche, carte centrale, Gazette et demandes défilantes
|
||||
à droite. Date, intendance et heure en en-tête ; options et sortie en bas de la colonne
|
||||
gauche, reprise de la partie en bas au centre.
|
||||
[Contrat et vérifications](docs/pause-redesign-beta155.md).
|
||||
|
||||
## beta.154 — Gazette et tableau communautaire (pack local)
|
||||
|
||||
Panneaux gauche/droite du menu pause, intendance, publication, réponses,
|
||||
modification et modération. Stockage fichier par défaut ; MariaDB optionnelle
|
||||
commune avec le site. Shaders beta.151 conservés.
|
||||
[Contrat et configuration](docs/community-contract-v1.md) ·
|
||||
[Vérifications et limites](docs/community-beta154.md).
|
||||
|
||||
## beta.151 — Nether, reflets des blocs et SSR (pack local)
|
||||
|
||||
Synchronisation de l’inventaire au changement de dimension, retour de l’intensité
|
||||
spéculaire des blocs solides à son niveau antérieur et bois mat avec relief.
|
||||
SSR activé par défaut à 20 %, choix sauvegardés conservés.
|
||||
[Contrat et vérifications](docs/nether-pbr-beta151.md).
|
||||
|
||||
## beta.150 — Terre mate et reflets du coucher de soleil (pack local)
|
||||
|
||||
Terre mate avec relief conservé. Le soleil et son reflet se colorent ensemble
|
||||
en suivant la couleur et la transition du coucher de soleil natif de Minecraft.
|
||||
Les accents lumineux suivent les normales animées des vaguelettes et leur
|
||||
orientation vers le soleil et la caméra. [Contrat et vérifications](docs/matte-dirt-beta150.md).
|
||||
|
||||
## beta.149 — Distances PBR / SSR et feuillage (pack local)
|
||||
|
||||
Deux distances séparées, 64 blocs par défaut chacune, jusqu’à 256 blocs.
|
||||
Option renommée « SSR » et suppression du voile spéculaire sur le feuillage.
|
||||
[Contrat et vérifications](docs/independent-distances-beta149.md).
|
||||
|
||||
## beta.148 — PBR 50 % et soleil diffus (pack local)
|
||||
|
||||
PBR à 50 %, reflet solaire plus doux avec fusion par texel préservant
|
||||
la texture de l’eau. SSR à 20 %, portée commune jusqu’à 256 blocs.
|
||||
[Contrat et vérifications](docs/pbr-soft-sun-beta148.md).
|
||||
|
||||
## beta.147 — PBR proche, lune et portée 256 (pack local)
|
||||
|
||||
Correction du scintillement à très courte distance, reflet lunaire adouci
|
||||
et distance commune PBR/SSR jusqu’à 256 blocs.
|
||||
[Contrat et vérifications](docs/pbr-close-range-beta147.md).
|
||||
|
||||
## beta.146 — Reflets et transitions de l’eau (pack local)
|
||||
|
||||
Correction du masquage des personnages en F5 et sélection stricte des surfaces PBR.
|
||||
PBR à 80 % et SSR à 20 % par défaut, distance commune PBR/SSR
|
||||
(16/32/64/128 blocs), reflet solaire large et blanc avec léger bloom des matières.
|
||||
Eau naturelle selon le type de monde et mélange 1/3/5/9/17 blocs.
|
||||
[Contrat et vérifications](docs/reflections-water-beta146.md).
|
||||
|
||||
## beta.145 — PBR de l’eau et reflets pixélisés (livraison locale)
|
||||
|
||||
L’eau et le verre rejoignent le PBR natif. L’option expérimentale de reflets
|
||||
alimente les texels avec le paysage visible selon leurs normales.
|
||||
[Contrat, vérifications et limites](docs/pbr-ssr.md).
|
||||
Cette branche intègre un instantané des sources d’eau beta.144 ; elle ne
|
||||
publie pas le canal et ne synchronise pas Prism.
|
||||
|
||||
|
||||
Sanctuary est une extension gratuite de Minecraft qui transforme le jeu en un
|
||||
monde flottant d'exploration, de production et de progression collective.
|
||||
Les joueurs commencent ensemble sur Sanctuary Island et construisent les
|
||||
@@ -20,13 +95,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.129** ajoute les reflets CTM optionnels sur les contours du
|
||||
relief, avec une portée indépendante de 16 à 256 blocs. Les surfaces plates
|
||||
se raccordent entre tous les matériaux. Les ombres gagnent les portées
|
||||
128/256, gardent leur intensité à midi et ne réalignent plus leur grille à
|
||||
chaque mouvement du soleil. **Options → Shaders → Sanctuary · Vanilla Light**.
|
||||
Le [ticket de livraison](docs/shader-beta129.md) précise le résultat et ses
|
||||
vérifications. Publication et synchronisation en cours de préparation.
|
||||
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.
|
||||
@@ -37,11 +118,122 @@ 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
|
||||
option pour la chaleur des torches. Lumières colorées activées à 30 % par défaut.
|
||||
[Contrat et vérifications](docs/colored-dynamic-beta140.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.140/Sanctuary-beta.140.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.140/Sanctuary-Test-beta.140.mrpack).
|
||||
|
||||
## beta.139 — Lumières colorées
|
||||
|
||||
Teintes chaudes, sources des âmes bleues, redstone rouge, froglights et matériaux lumineux.
|
||||
Option désactivée initialement, intensité 35 % ; propagation locale avec obstacles.
|
||||
[Contrat et vérifications](docs/colored-lights-beta139.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.139/Sanctuary-beta.139.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.139/Sanctuary-Test-beta.139.mrpack).
|
||||
|
||||
## beta.138 — Rayons et brouillard
|
||||
|
||||
Densité automatique des rayons selon le brouillard et les précipitations.
|
||||
[Contrat et vérifications](docs/shader-beta138.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.138/Sanctuary-beta.138.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.138/Sanctuary-Test-beta.138.mrpack).
|
||||
|
||||
## beta.137 — Rayons solaires
|
||||
|
||||
Faisceaux volumétriques légers, intensité réglable et feuillage perméable.
|
||||
[Contrat et vérifications](docs/shader-beta137.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.137/Sanctuary-beta.137.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.137/Sanctuary-Test-beta.137.mrpack).
|
||||
|
||||
## beta.136 — Lens flare rectangulaire
|
||||
|
||||
Soleil et lune, reflets doux et intensité indépendante du bloom.
|
||||
[Contrat et vérifications](docs/shader-beta136.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.136/Sanctuary-beta.136.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.136/Sanctuary-Test-beta.136.mrpack).
|
||||
|
||||
## beta.135 — SSGI près des surfaces
|
||||
|
||||
Correction des coupures à courte distance, sans flouter les textures.
|
||||
[Contrat et vérifications](docs/shader-beta135.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.135/Sanctuary-beta.135.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.135/Sanctuary-Test-beta.135.mrpack).
|
||||
|
||||
## beta.134 — PBR optionnel et SSGI filtré
|
||||
|
||||
Relief et reflets des matériaux ; lissage de la lumière indirecte.
|
||||
[Contrat et vérifications](docs/shader-beta134.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.134/Sanctuary-beta.134.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.134/Sanctuary-Test-beta.134.mrpack).
|
||||
|
||||
## beta.133 — SSGI renforcé
|
||||
|
||||
Couleurs voisines plus visibles, même budget de rayons et de cibles GPU.
|
||||
[Contrat et vérifications](docs/shader-beta133.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.133/Sanctuary-beta.133.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.133/Sanctuary-Test-beta.133.mrpack).
|
||||
|
||||
## beta.132 — SSGI : diffusion locale des couleurs
|
||||
|
||||
Rebond diffus des couleurs visibles, intensité réglable et reconstruction
|
||||
préservant les silhouettes. Expérimental, initialement désactivé.
|
||||
[Contrat et vérifications](docs/shader-beta132.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.132/Sanctuary-beta.132.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.132/Sanctuary-Test-beta.132.mrpack).
|
||||
|
||||
## beta.131 — Émissifs indépendants et précision étendue
|
||||
|
||||
Bloom, émissifs et highlight activés initialement ; halo facultatif et cinq
|
||||
précisions communes de 8 à 128 pixels/bloc.
|
||||
[Contrat et vérifications](docs/shader-beta131.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.131/Sanctuary-beta.131.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.131/Sanctuary-Test-beta.131.mrpack).
|
||||
|
||||
## beta.130 — Bloom, minerais émissifs et précision commune
|
||||
|
||||
Halo réglable des sources lumineuses et inclusions des vingt textures de
|
||||
minerais ; réglages par défaut 32/128/16 et ombres activées.
|
||||
[Contrat et vérifications](docs/shader-beta130.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.130/Sanctuary-beta.130.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.130/Sanctuary-Test-beta.130.mrpack).
|
||||
|
||||
## beta.129 — Reflets CTM et ombres jusqu'à 256 blocs
|
||||
|
||||
Reflets connectés réglables, cinq portées indépendantes, soleil plein à midi
|
||||
et correction du réalignement cyclique de la grille d'ombres.
|
||||
[Contrat et vérifications](docs/shader-beta129.md).
|
||||
[Contrat et vérifications](docs/shader-beta129.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.129/Sanctuary-beta.129.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.129/Sanctuary-Test-beta.129.mrpack).
|
||||
|
||||
## beta.128 — Ombres stables, lune et feuillage
|
||||
|
||||
@@ -2202,7 +2394,7 @@ Depuis beta.090, la cible est **Minecraft 26.3 finale**, publiée le 15 septembr
|
||||
| Fabric API | 0.160.5+26.3 |
|
||||
| Fabric Loom | 1.17.20 |
|
||||
| Gradle Wrapper | 9.5.1, distribution vérifiée par SHA-256 |
|
||||
| Sanctuary / pack | beta.122 |
|
||||
| Sanctuary / pack | beta.151 |
|
||||
|
||||
Java 25 et Python 3.11 ou plus récent sont nécessaires. Le script pack utilise
|
||||
uniquement la bibliothèque standard et repère aussi une installation Python
|
||||
@@ -2231,7 +2423,7 @@ décrits dans [Validation](docs/testing.md).
|
||||
|
||||
Résultats :
|
||||
|
||||
- `mods/sanctuary/build/libs/sanctuary-beta.122.jar` : mod à installer avec
|
||||
- `mods/sanctuary/build/libs/sanctuary-beta.151.jar` : mod à installer avec
|
||||
Fabric API sur la version Minecraft indiquée.
|
||||
- `build/packwiz/` : pack de développement complet, avec le mod construit et
|
||||
l'index vérifié. Voir [Installation packwiz](packwiz/README.md).
|
||||
|
||||
@@ -136,3 +136,10 @@ intégré et le template personnel (SHA-256
|
||||
`a97570908db75caf8e6fc2f5ecabf12c54fd99bfea7be634df66c4310dca22af`).
|
||||
Le template regroupe les ressources client déjà distribuées par Sanctuary ;
|
||||
leurs crédits et conditions respectives restent applicables.
|
||||
|
||||
MariaDB Connector/J 3.5.10 (`org.mariadb.jdbc:mariadb-java-client`),
|
||||
MariaDB Corporation et contributeurs : LGPL-2.1-or-later. Le JAR officiel est
|
||||
embarqué comme dépendance imbriquée, sans modification, pour le stockage
|
||||
communautaire optionnel. Sa licence est fournie dans `licenses/mariadb-connector-j-LGPL-2.1.txt` du JAR Sanctuary.
|
||||
Sources correspondantes : https://github.com/mariadb-corporation/mariadb-connector-j/tree/3.5.10
|
||||
(distribution Maven Central 3.5.10).
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# Backlog Sanctuary
|
||||
|
||||
## COMM-154 — Communauté dans le menu pause et sur le site
|
||||
|
||||
Branches `codex/community-beta154` (mod) et `codex/community-contract` (site).
|
||||
Résultat : Gazette, tableau et intendance avec lecture, publication, réponses,
|
||||
droits serveur et comptes liés ; fichier autonome ou MariaDB partagée.
|
||||
[Contrat](community-contract-v1.md), [livraison locale](community-beta154.md).
|
||||
|
||||
## STAT-03 — Atelier d’argile et hotbar — beta.112
|
||||
|
||||
Branche `codex/clay-workshop-hotbar-beta112`. [Contrat et vérifications](clay-workshop-hotbar-beta112.md).
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
# LIGHT-140 — Lumières colorées portées et distance
|
||||
|
||||
Socle beta.139 `c05c338`, branche `codex/colored-dynamic-beta140`.
|
||||
Minecraft 26.3 / Java 25. Livraison beta.140.
|
||||
|
||||
## Contrat
|
||||
|
||||
Relier les couleurs aux vraies sources de lumière dynamique : mains, tête,
|
||||
cosmétiques, objets au sol et feu, selon le système existant. Renforcer la
|
||||
teinte et activer les lumières colorées par défaut à 30 %. Ajouter une distance
|
||||
d'affichage de 8, 16, 32 ou 64 blocs (16 par défaut), indépendante de la portée
|
||||
native de chaque source. Augmenter la distance coûte davantage en mémoire et
|
||||
calcul ; aucun chargement forcé de chunk. Les choix sauvegardés sont conservés.
|
||||
|
||||
Sources portées actualisées avec le système dynamique, sans reconstruire tout
|
||||
le champ statique à chaque mouvement. Aucun changement de monde ou de règle
|
||||
serveur. Vérifications natives OpenGL et Vulkan, puis construction et archives.
|
||||
|
||||
## Couleurs et fondus
|
||||
|
||||
Torches ordinaires : température native par défaut ; option « Torches chaudes »
|
||||
pour retrouver l'orangé. Glowstone jaune, lave jaune-rouge, redstone rouge saturé,
|
||||
cuivre vert (posé, en main ou sur la tête). Le cuivre est reconnu avant les
|
||||
familles génériques de torches et lanternes. Les palettes explicites restent
|
||||
indépendantes des packs ; le repli des autres matériaux suit leurs textures.
|
||||
|
||||
Le rendu s'atténue sur le dernier quart de la distance choisie. Le champ précédent
|
||||
reste disponible pendant un fondu de 300 ms vers le nouveau champ ; l'activation
|
||||
et les sources dynamiques ont aussi un fondu. Le changement de distance est
|
||||
interpolé. Les sources mobiles utilisent la même sélection bornée et les mêmes
|
||||
intensités que la lumière dynamique existante, avec interpolation visuelle des
|
||||
positions et couleurs. Elles héritent de sa propagation radiale, y compris de
|
||||
ses limites d'occultation ; les blocs posés conservent leur propagation avec murs.
|
||||
|
||||
Le volume varie réellement selon la distance (56, 72, 104 ou 168 blocs de côté,
|
||||
avec marge de propagation). Les grandes distances augmentent la mémoire et le
|
||||
délai de reconstruction ; la construction reste répartie entre les images.
|
||||
Deux champs coexistent brièvement pour le fondu. Seuls les chunks déjà chargés
|
||||
sont lus. La portée lumineuse native des torches n'est pas allongée.
|
||||
|
||||
## Frontières de saturation
|
||||
|
||||
Le stockage beta.139 tronquait les canaux faibles séparément en fin de portée :
|
||||
un bleu pouvait perdre son rouge avant son vert, puis devenir plus saturé sur
|
||||
une frontière visible. Le stockage logarithmique conserve maintenant une marge
|
||||
sous le niveau lumineux natif zéro. Le shader applique la coupure à l'énergie
|
||||
globale en conservant les rapports RGB. La réponse devient linéaire près du noir,
|
||||
sans amplification par racine quatrième des très faibles contributions.
|
||||
|
||||
La transition des champs interpole le résultat colorimétrique rendu, pas une
|
||||
énergie ensuite amplifiée. Les sources mobiles ont une atténuation de fondu
|
||||
compensée pour la même réponse. Le test contrôle les rapports rouge/bleu et
|
||||
vert/bleu d'une torche des âmes de 1 à 9 blocs, jusqu'au bord de sa portée.
|
||||
|
||||
## Vérifications natives
|
||||
|
||||
Minecraft 26.3 / Java 25 sur Apple M1, mondes de test neufs de graine 122 :
|
||||
|
||||
- OpenGL 4.1 Metal : **2 min 40 s**, `build/colored140-opengl.log`.
|
||||
- Vulkan / MoltenVK 1.4.2, profondeur 0–1 et transparence améliorée :
|
||||
**2 min 26 s**, `build/colored140-vulkan.log`.
|
||||
|
||||
Les deux exécutions passent `COLORED139_PASS` et `COLORED140_PASS` :
|
||||
propagation avec murs, sources hors champ, lampes allumées/éteintes, retrait,
|
||||
rechargement des ressources, cache, déplacement/retour de caméra, OFF/zéro,
|
||||
préférences et traductions FR/EN. Les nouveaux contrôles couvrent la teinte
|
||||
bleue jusqu'à neuf blocs, la torche native inchangée, l'option chaude, le cuivre
|
||||
vert et le rouge saturé, les deux mains et le cosmétique de tête, la position
|
||||
en troisième personne, les quatre distances et un fondu sans dépassement.
|
||||
|
||||
Dans la même scène contrôlée, la somme d'écarts OFF/torche chaude à 30 % vaut
|
||||
3 926 093 / 3 926 092 sur OpenGL/Vulkan, contre 2 025 577 en beta.139 à 35 %.
|
||||
Cette mesure vérifie l'accentuation demandée, pas la performance. Le test fige
|
||||
uniquement le scintillement de la lightmap native ; cette sonde et les tests
|
||||
client ne sont pas distribués. Les captures finales sont conservées sous
|
||||
`build/colored140-opengl-screenshots/` et `build/colored140-vulkan-screenshots/`.
|
||||
Comparatif autonome : `build/Shader-beta.140-comparaison.html`.
|
||||
|
||||
Pas de validation des pilotes Windows de l'utilisateur revendiquée. Les
|
||||
limites de beta.139 pour le rendu sous l'eau/lave et les panoramas restent
|
||||
ouvertes. Les sources dynamiques gardent l'occultation du système existant.
|
||||
Aucun monde personnel ouvert ni modifié.
|
||||
|
||||
## Construction et archives
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
réussit en **2 min 22 s**, 126 tâches. Le GameTest serveur dédié reste exclu
|
||||
conformément au refus EULA antérieur ; les tests clients ci-dessus sont exécutés
|
||||
séparément. Log : `build/shader140-build.log`.
|
||||
|
||||
La comparaison avec beta.139 trouve **18 entrées de production modifiées**,
|
||||
limitées aux lumières dynamiques/colorées, options et aides FR/EN. Les sources
|
||||
des JAR correspondent aux fichiers du dépôt. Les archives normale/Test contiennent
|
||||
le même JAR Sanctuary ; la version, les dépendances exactes, les textures de
|
||||
gemmes et de la clé de Steve, l'absence de classes de test et le template complet
|
||||
sont vérifiés. Reçu : `build/shader140-artifact.json`.
|
||||
|
||||
SHA-256 du JAR : `853284c1a61c7fce12edd2f7763717321eef56977290eb0e7c5db5e30bff442f`.
|
||||
|
||||
- Sanctuary-beta.140.mrpack : `1b366746e4d6bebfbe0115c849524bdeb81c14eee89a2510f9dbe18808974d6a`.
|
||||
- Sanctuary-Test-beta.140.mrpack : `50c3cdf759117bb5286d41b4535fde15d9d84deaac833b7f8f61c4a0d29b72e7`.
|
||||
|
||||
## Livraison
|
||||
|
||||
[Release beta.140](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.140)
|
||||
publiée sur le commit source `6e1b3b71088437fe57cf2b9b979b6d62711628d3` ; canal stable
|
||||
`2c360a0759d4a164f82c89ac2bef04ff69d50875` 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.140/`. Aucun monde personnel
|
||||
ouvert. Reçus : `build/shader140-isolated.json` et `build/shader140-prism.json`.
|
||||
Archives normale/Test/template et comparaison copiées dans `sanctuary-beta/build/`.
|
||||
@@ -0,0 +1,107 @@
|
||||
# LIGHT-139 — Lumières colorées
|
||||
|
||||
Socle beta.138 + vérification Vulkan `9d84a02`, Minecraft 26.3 / Java 25.
|
||||
Branche `codex/colored-lights-beta139`, livraison beta.139.
|
||||
|
||||
## Contrat
|
||||
|
||||
Option locale « Lumières colorées » avec intensité. Les vrais blocs émetteurs
|
||||
teintent discrètement leurs environs, y compris hors champ. Torches/lanternes,
|
||||
lave et feu chauds ; sources des âmes bleues ; redstone active rouge ;
|
||||
froglights par variante ; sources Sanctuary selon leur texture/matière.
|
||||
Les blocs éteints n'émettent pas. Le bloom et les inclusions émissives des
|
||||
minerais restent indépendants ; aucune lumière de gameplay n'est ajoutée.
|
||||
|
||||
Propagation RGB dans un volume local de blocs chargés, obstacles et formes
|
||||
natives, construction répartie entre les images, cache des couleurs de
|
||||
matériaux. Aucun chargement forcé de chunk, accès aux mondes personnels,
|
||||
changement de sauvegarde, réseau ou règles serveur.
|
||||
|
||||
## Réalisation
|
||||
|
||||
Dans les options du shader : « Lumières colorées », désactivées initialement,
|
||||
et une intensité de 0 à 100 %, réglée à 35 %. Zéro ou OFF libère les ressources.
|
||||
Les préférences existantes sont conservées. Libellés et aides FR/EN.
|
||||
|
||||
Les familles vanilla demandées ont une palette dédiée. Les autres émetteurs,
|
||||
y compris ceux de Sanctuary, utilisent la couleur des pixels lumineux de leurs
|
||||
textures de modèle, calculée puis mise en cache. Ce repli suit les packs de
|
||||
ressources ; les palettes dédiées vanilla restent constantes. Une texture
|
||||
illisible ou sans pixels exploitables utilise du blanc. Les blocs purement
|
||||
émissifs du bloom, sans émission lumineuse native, ne deviennent pas des lampes.
|
||||
|
||||
Le champ couvre un cube de 64 blocs de côté autour de la caméra. Une marge de
|
||||
14 blocs réserve la propagation aux sources présentes dans le volume ; le rendu
|
||||
s'atténue progressivement sur 4 blocs à sa périphérie. C'est donc un effet de
|
||||
proximité, pas une illumination de tous les chunks visibles. Les formes et
|
||||
l'opacité natives règlent le passage de la lumière. Les canaux utilisent des
|
||||
maxima indépendants, sans addition illimitée des sources superposées.
|
||||
|
||||
Le calcul CPU est réparti avec un budget visé de 2 ms par image, plus les
|
||||
allocations et chargements ponctuels de matériaux ; aucun chiffre de FPS n'est
|
||||
garanti. Un monde immobile réutilise son champ. Les changements rapides et les
|
||||
déplacements peuvent présenter un bref retard, le temps de publier un champ
|
||||
complet. Aucun chargement forcé de chunk. Une texture 512 × 512 contient le champ.
|
||||
La passe GPU teinte les surfaces visibles, atténue l'effet au soleil et dans le
|
||||
brouillard, puis laisse agir le SSGI et le bloom. Les sources tenues, entités,
|
||||
particules et le rendu sous l'eau/lave ne sont pas couverts par cette option.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Essais client natifs réussis dans des mondes neufs (graine 122) sur Apple M1 :
|
||||
|
||||
- OpenGL 4.1 Metal : **1 min 46 s**, `build/colored139-opengl.log`.
|
||||
- Vulkan / MoltenVK 1.4.2, profondeur 0–1 et transparence améliorée :
|
||||
**1 min 30 s**, `build/colored139-vulkan.log`.
|
||||
|
||||
Les deux journaux contiennent `COLORED139_PASS` et le backend réellement actif.
|
||||
Torche et torche des âmes hors champ, trois froglights, pierre lumineuse dont
|
||||
la couleur vient de sa texture, intensité 35/100, cache statique, déplacement
|
||||
et retour de caméra, mur fermé puis ouvert, lampe redstone allumée puis éteinte,
|
||||
retrait de source, rechargement des ressources et OFF/zéro vérifiés.
|
||||
Les préférences persistantes et les libellés FR/EN sont contrôlés ; une dernière
|
||||
capture active conjointement PBR, SSGI, bloom, ombres, highlights et rayons.
|
||||
|
||||
Les sommes d'écart entre OFF et 35 % sont presque identiques entre backends :
|
||||
torche 2 025 577 / 2 025 576 ; âmes 964 200 / 964 201. Il s'agit de mesures
|
||||
sur les pixels de cette scène, pas d'un indice de performance.
|
||||
Comparatif autonome : `build/Shader-beta.139-comparaison.html`.
|
||||
Pas de validation du pilote Vulkan Windows de l'utilisateur revendiquée.
|
||||
Le test fige uniquement le scintillement vanilla des torches afin de comparer
|
||||
les images ; cette sonde est exclue du JAR distribué. Aucun monde personnel ouvert.
|
||||
|
||||
## Diagnostic des essais
|
||||
|
||||
La première comparaison de stabilité détectait le scintillement natif de la
|
||||
lightmap des torches, même avec un champ coloré inchangé. La sonde de test
|
||||
stabilise ce seul facteur ; aucune désactivation de scintillement en production.
|
||||
La pièce d'essai est désormais construite après le chargement de ses chunks,
|
||||
et l'absence de lumière du ciel est vérifiée avant les captures.
|
||||
|
||||
## Construction et archives
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
réussit en **2 min 22 s**, 126 tâches. Le serveur GameTest dédié reste exclu
|
||||
conformément au refus EULA antérieur. Log : `build/shader139-build.log`.
|
||||
La comparaison à beta.138 trouve **13 entrées de production** modifiées,
|
||||
limitées au shader, à son menu et aux aides FR/EN. Les sondes de test sont
|
||||
absentes du JAR distribué. Sources, archives normale/Test et template complets
|
||||
vérifiés ; textures des gemmes et clé de Steve conservées à l'identique.
|
||||
Reçu local : `build/shader139-artifact.json`.
|
||||
|
||||
JAR SHA-256 : `7b958f6576adbabb2b1af589b866cb52b1a65600ea2fa505b05dd283f8d595df`.
|
||||
|
||||
- `Sanctuary-beta.139.mrpack` : 10450317 octets ; SHA-256 `13f3a283c260876d28ce0c6504615b4a8f4d575906badfbb2b39558f312e9fbb`.
|
||||
- `Sanctuary-Test-beta.139.mrpack` : 10469244 octets ; SHA-256 `c1e0b684dc2650950a0925c4625ef5eb4b78fb5f1c5680f5e302550424fa6d81`.
|
||||
|
||||
## Publication et instance
|
||||
|
||||
[Release beta.139](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.139)
|
||||
publiée depuis `dc33ffa54697f61cd8f579235e7cebdbf0765f7c`, tag exact `beta.139`.
|
||||
Canal packwiz : `9f10167b6acfece9c045573580e186aa72bc91dc`.
|
||||
Deux synchronisations isolées puis deux synchronisations de la même instance
|
||||
Prism réussissent. Un seul JAR Sanctuary beta.139 actif, empreinte identique
|
||||
à l'archive vérifiée ; les **923 fichiers personnels suivis** conservent leurs
|
||||
empreintes. Sauvegarde des fichiers gérés dans
|
||||
`sanctuary-backups/before-beta.139/`. Aucun monde personnel ouvert.
|
||||
Reçus : `build/shader139-isolated.json`, `build/shader139-prism.json`.
|
||||
@@ -0,0 +1,115 @@
|
||||
# COMM-154 — Communauté et conversations, beta.154
|
||||
|
||||
Livraison locale préparée sur `codex/community-beta154`, depuis beta.151
|
||||
`b97ec6c9e820bed7693e75f200a02b16af01cf89`. Le site est préparé sur
|
||||
`codex/community-contract`, depuis `4af93d1e42ba6084ecd654967ad6d8f317a5d3ef`.
|
||||
Contrat identique dans les deux dépôts : [communauté v1](community-contract-v1.md).
|
||||
|
||||
## Résultat
|
||||
|
||||
Gazette à gauche et tableau à droite du menu pause, intendance au-dessus ;
|
||||
accès compacts lorsque la largeur GUI ne permet pas trois colonnes.
|
||||
Lire, publier et répondre, modifier son texte, masquer et clôturer une annonce.
|
||||
Modération autorisée côté serveur et comptes Web liés par code confirmé en jeu.
|
||||
|
||||
Les discussions montrent les visages Minecraft, le pseudo, la date et chaque
|
||||
message dans un bloc de conversation. Les actions secondaires sont regroupées
|
||||
sous « ⋯ » ; la réponse se saisit directement sous le fil. Même présentation
|
||||
sur le site, avec rendu des visages par UUID via son prestataire existant.
|
||||
|
||||
Fichier JSON atomique par défaut ; MariaDB optionnelle partagée avec Laravel.
|
||||
Le serveur exécute la persistance hors du thread de jeu. Brouillons conservés
|
||||
en cas de refus ; aucun repli silencieux vers un fichier après une panne SQL.
|
||||
Aucun changement des shaders, de leurs réglages, de la génération ou des
|
||||
sauvegardes existantes. Le delta part exactement du shader beta.151.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Environnement : macOS Apple M1, Java Temurin 25, Minecraft 26.3,
|
||||
Fabric Loader 0.19.5, Fabric API 0.160.5+26.3, Vulkan/MoltenVK 1.4.2.
|
||||
Site : PHP 8.5.10, Laravel 13.32.0, PHPUnit 12.5.35. MariaDB locale 12.3.3,
|
||||
Connector/J 3.5.10 imbriqué dans le mod.
|
||||
|
||||
- Scénario de stockage commun réussi sur fichier et MariaDB : persistance,
|
||||
redémarrage, exact retry, conflits de révision, auteurs, opérateurs,
|
||||
fermeture/réouverture, masquage, pagination, texte Unicode et isolation.
|
||||
Codes expirés, consommés et tentative de réattribution d’identité refusés.
|
||||
`build/community-client-database-final.log`, marqueurs FILE_PASS et SQL_PASS.
|
||||
- Aller-retour réel Java → HTTP Laravel → Java réussi : article, création d’un
|
||||
code Web, liaison côté Java, modification Web et réponse relues par Java.
|
||||
`build/community-interop-{seed,claim,verify}.log` ; deux phases PHPUnit,
|
||||
3 puis 6 assertions. Migration Laravel sur les tables déjà créées par le SQL
|
||||
du mod réussie ; création depuis Laravel également testée par les tests HTTP.
|
||||
- Tests client Vulkan réussis en FR/EN, GUI 2/3 (panneaux et mode compact),
|
||||
publication, réponse intégrée, clôture, actions contextuelles, visages,
|
||||
brouillon refusé et réponse tardive après fermeture.
|
||||
Fichier : `build/community-conversation-client.log`.
|
||||
MariaDB : `build/community-conversation-database.log`, mode explicitement
|
||||
vérifié par le client. Captures dans `mods/sanctuary/build/run/clientGameTest/screenshots/`.
|
||||
- Panne SQL testée avec une adresse locale sans service : erreur visible,
|
||||
brouillon conservé, aucune création de fichier de repli.
|
||||
`build/community-client-outage.log`, marqueur COMMUNITY154_OUTAGE_PASS.
|
||||
- Web : **12 tests / 60 assertions** communautaires réussis, comprenant
|
||||
validation, identité non falsifiable, restrictions du staff, auteur,
|
||||
XSS/texte échappé, formulaires, pagination, compte lié et rejet CSRF réel.
|
||||
Pint réussi. Rendu des pages et visages vérifié dans le navigateur local,
|
||||
navigation accueil → discussion et FR/EN. Aucune erreur console applicative ;
|
||||
avertissement THREE.Clock hérité du panorama.
|
||||
- Suite Web complète : **55 réussites, 1 échec, 1 test optionnel ignoré** sur
|
||||
57. L’échec existant de PlayerModerationTest attend « Nommer admin », alors
|
||||
que ViewUser affiche déjà « Nommer staff » dans le commit de départ.
|
||||
- `./gradlew check build --continue` exécuté : **229/252 GameTests réussis**,
|
||||
**23 échecs strictement identiques à beta.151** (comparaison des identifiants).
|
||||
`build/community-check-build.log`, `build/community-server-failures.json`.
|
||||
Ce passage a aussi rencontré un `ClassNotFoundException` temporaire dans
|
||||
seasonal105Smoke pendant des compilations concurrentes de vérification.
|
||||
Le contrôle final est relancé seul, avec la tâche GameTests exclue (les 252 cas ont été exécutés lors du premier passage).
|
||||
|
||||
Le contrôle complet reste rouge : cette livraison ne prétend pas réparer les
|
||||
23 GameTests historiques ni l’ancienne assertion de libellé Web. Les parcours
|
||||
communautaires sont validés indépendamment.
|
||||
|
||||
## Rejouer les tests ciblés
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:community154Smoke
|
||||
./gradlew :sanctuary:runClientGameTest \
|
||||
-PsanctuaryClientTests=true -PsanctuaryQuickTests=true \
|
||||
-PsanctuaryCommunity154ClientTests=true -PsanctuaryClientGraphicsBackend=vulkan
|
||||
```
|
||||
|
||||
Pour le même scénario en base, préparer une MariaDB de développement vide
|
||||
avec `src/main/resources/community/schema-v1.sql`, puis définir
|
||||
`SANCTUARY_COMMUNITY_TEST_JDBC` (URL JDBC),
|
||||
`SANCTUARY_COMMUNITY_CLIENT_DATABASE=true` et
|
||||
`SANCTUARY_COMMUNITY_DB_PASSWORD`. Ces fixtures de développement utilisent
|
||||
`root` sans mot de passe, uniquement dans la MariaDB locale isolée de test.
|
||||
Ne jamais les lancer sur une base personnelle ou de production.
|
||||
|
||||
L’aller-retour utilise une autre base isolée, également migrée par Laravel :
|
||||
`community154Interop -PcommunityInteropPhase=seed`, puis CommunityInteropTest
|
||||
avec `SANCTUARY_COMMUNITY_INTEROP_PHASE=link`, Java `claim`, PHP `write`, Java
|
||||
`verify`. Les deux programmes reçoivent le même chemin absolu
|
||||
`SANCTUARY_COMMUNITY_INTEROP_FILE` vers un JSON dans un dossier ignoré.
|
||||
Le test PHP reçoit les paramètres DB_CONNECTION/DB_HOST/DB_PORT/DB_DATABASE
|
||||
pointant exclusivement vers cette base. Sans cette variable, il est ignoré.
|
||||
|
||||
## Limites de livraison
|
||||
|
||||
Code et artefacts locaux ; aucun déploiement Web, serveur personnel, canal
|
||||
packwiz ou instance Prism. Validation Windows et connexion Discord réelle sur
|
||||
le domaine de production à faire lors du déploiement. Le compte Web utilisé
|
||||
dans les essais est fictif, la liaison passe cependant par le vrai stockage.
|
||||
Pas de pièces jointes, de notification poussée ou d’import fichier → base.
|
||||
Les anciens JAR encore présents sont conservés.
|
||||
|
||||
## Artefacts vérifiés
|
||||
|
||||
Export packwiz réussi. ZIP, versions Minecraft/Fabric, JAR unique, pilote JDBC
|
||||
imbriqué, licence, absence des tests et égalité octet pour octet des 29 fichiers
|
||||
shader avec beta.151 vérifiés. Reçu : `build/community-artifacts.json`.
|
||||
|
||||
- `mods/sanctuary/build/libs/sanctuary-beta.154.jar` — SHA-256 `07e7419cb3e814d59a6b6f130a1b05b4f65c2407a0ffbd00b194ba97614923ad`.
|
||||
- `build/Sanctuary-beta.154.mrpack` — SHA-256 `fbc007fe59777b0c52b30b6e461b09fe0636fb0b86d254b9e98952856b7f6c8a`.
|
||||
|
||||
Site : commit local `32d804a` sur `codex/community-contract`.
|
||||
@@ -0,0 +1,172 @@
|
||||
# Communauté Sanctuary — contrat v1
|
||||
|
||||
> Depuis beta.157, le schéma actif est la v2 avec photo obligatoire pour les nouveaux
|
||||
> articles. Voir la section « Évolution v2 » en fin de document avant installation.
|
||||
|
||||
Ticket beta.154, branche `codex/community-beta154`, socle beta.151 `b97ec6c`.
|
||||
Ce contrat décrit le comportement cible et les échanges implémentés par ce ticket.
|
||||
La note de livraison indique séparément les vérifications effectivement réussies.
|
||||
Les shaders de beta.151 sont conservés sans modification.
|
||||
|
||||
## Périmètre
|
||||
|
||||
Gazette (`article`), tableau (`notice`), intendance (`bulletin`) et réponses
|
||||
(`reply`). Contenus texte brut : titre 120 caractères Unicode, corps 4 000,
|
||||
réponse 1 000. Aucune interprétation HTML, Markdown ou commande Minecraft.
|
||||
Catégories stables : `info`, `work`, `need`, `event`. Photos et pièces jointes
|
||||
sont hors de cette première livraison.
|
||||
|
||||
La pause présente la Gazette à gauche, les actions natives au centre, le tableau
|
||||
à droite et l'intendance au-dessus. À faible largeur GUI, accès compacts vers
|
||||
les mêmes écrans. Publication et réponses disponibles dès cette version.
|
||||
|
||||
## Autorité et identité
|
||||
|
||||
Le client passe exclusivement par le serveur Minecraft. L'auteur vient de la
|
||||
session serveur, jamais d'un UUID fourni par le client. Chaque installation
|
||||
possède un `serverId` UUID stable, également configuré sur le site. Chaque
|
||||
requête SQL inclut ce serveur. Les sauvegardes du monde restent indépendantes.
|
||||
|
||||
Tous les joueurs connectés peuvent publier et répondre. L'auteur peut modifier
|
||||
ou masquer son contenu et clôturer/réouvrir son annonce. Un opérateur Minecraft
|
||||
ou membre du staff Web disposant de `users.moderate` peut masquer des contenus, épingler les publications et
|
||||
publier l'intendance ; il ne réécrit pas le texte d'un autre auteur.
|
||||
Le site exige un compte connecté lié à l'UUID Minecraft : code aléatoire à usage
|
||||
unique créé sur le site, valable 10 minutes, confirmé en jeu. Seul le hash SHA-256
|
||||
est conservé. La confirmation nécessite un serveur authentifié (`online-mode`).
|
||||
Une identité déjà liée ne peut être réattribuée automatiquement.
|
||||
|
||||
## Stockage
|
||||
|
||||
`config/sanctuary-community.json` sélectionne `file` (défaut) ou `database`.
|
||||
Le fichier du monde `data/sanctuary-community.json` est remplacé atomiquement
|
||||
après synchronisation disque. Une corruption ou modification externe est une
|
||||
erreur : aucune réinitialisation silencieuse.
|
||||
|
||||
En mode base, MariaDB utilise les tables `sanctuary_community_*`, utf8mb4,
|
||||
InnoDB. Les nouvelles tables sont additives. Le schéma SQL v1 livré peut être
|
||||
installé par l'administrateur ; Laravel possède la migration équivalente.
|
||||
Le mod vérifie la version et ne lance jamais de migration au démarrage.
|
||||
Une seule application du schéma suffit avant de démarrer les deux applications.
|
||||
Les évolutions futures auront une migration explicite commune.
|
||||
|
||||
Aucun basculement automatique base → fichier lors d'une panne. Les lectures en
|
||||
cache restent signalées comme anciennes ; une écriture n'est confirmée qu'après
|
||||
commit. Les brouillons restent côté client durant la session. Les requêtes SQL
|
||||
s'exécutent hors du thread de jeu, avec délais et file d'attente bornés.
|
||||
Le passage fichier ↔ base nécessite un export/import explicite ; changer le mode
|
||||
ne migre ni ne fusionne les données. La v1 ne fournit pas cet importateur.
|
||||
|
||||
## Enregistrements et concurrence
|
||||
|
||||
Les entrées portent : serveur, UUID, parent éventuel, type, catégorie, UUID et nom
|
||||
d'auteur, titre, corps, état (`open`, `closed`, `hidden`), épingle, révision,
|
||||
date de création et date de modification (millisecondes Unix UTC).
|
||||
Une réponse a un parent article/annonce du même serveur ; pas de réponses imbriquées.
|
||||
Masquer un parent masque également sa discussion dans toutes les lectures.
|
||||
|
||||
Chaque création utilise un UUID d'opération stable. La répétition exacte rend le
|
||||
même résultat ; une réutilisation avec d'autres données est un conflit. Chaque
|
||||
modification exige la révision courante. La transaction verrouille le parent
|
||||
avant de répondre, empêchant la course avec une clôture ou un masquage.
|
||||
Les listes sont paginées (10 entrées), épingles puis date/id décroissants ; les
|
||||
réponses sont ordonnées par date/id croissants. L'intendance montre le bulletin
|
||||
visible le plus récent. Les textes et le nombre de résultats sont bornés.
|
||||
|
||||
## Vérification attendue
|
||||
|
||||
Même scénario sur fichier et vraie MariaDB : publication, réponses, redémarrage,
|
||||
idempotence, conflits, droits, masquage, fermeture et isolation de deux serveurs.
|
||||
Aller-retour Java → PHP → Java sur les mêmes tables. Essais client Vulkan FR/EN,
|
||||
GUI 2/3, mode compact, publication/refus/brouillon et fermeture avant réponse.
|
||||
Tests HTTP du site : identité, CSRF (middleware), validation, permissions,
|
||||
échappement HTML, erreurs, pagination. `./gradlew check build` et `assemblePack`
|
||||
sont consignés avec leurs éventuels échecs hérités.
|
||||
|
||||
## Mise en service
|
||||
|
||||
### Serveur autonome (fichier, défaut)
|
||||
|
||||
Installer le JAR ou le pack beta.154. La première ouverture d’un panneau crée
|
||||
`config/sanctuary-community.json` et son `serverId`. La première publication
|
||||
crée uniquement le fichier communautaire du monde. Aucun service externe requis.
|
||||
Les joueurs utilisent Échap → Gazette/Tableau → Publier, puis Répondre dans
|
||||
une discussion. Le mode fichier n’est pas partagé avec le site Web.
|
||||
|
||||
Conserver le `serverId` et sauvegarder le fichier communautaire avec son monde.
|
||||
Ne pas éditer ce fichier pendant que le serveur tourne. La limite de la v1 est
|
||||
64 Mio par fichier communautaire ; une écriture qui la dépasse est refusée.
|
||||
|
||||
### Même MariaDB pour Minecraft et Laravel
|
||||
|
||||
1. Sauvegarder la base existante. Déployer le code Web puis appliquer sa migration
|
||||
`php artisan migrate --force`. Elle ajoute quatre tables sans modifier les
|
||||
données de jeu. Un schéma déjà installé par `schema-v1.sql` est reconnu.
|
||||
À l’inverse, ne pas rejouer le SQL brut sur des tables déjà créées par Laravel.
|
||||
2. Dans le `.env` du site, garder ses paramètres MariaDB et définir
|
||||
`SANCTUARY_COMMUNITY_SERVER_ID` avec le même UUID que le mod. Recharger
|
||||
la configuration Laravel (`php artisan config:cache` en production).
|
||||
3. Arrêter Minecraft et éditer sa configuration :
|
||||
|
||||
```json
|
||||
{
|
||||
"mode": "database",
|
||||
"serverId": "11111111-1111-4111-8111-111111111111",
|
||||
"jdbcUrl": "jdbc:mariadb://db.example.net:3306/sanctuary?sslMode=verify-full",
|
||||
"user": "sanctuary_game",
|
||||
"passwordEnv": "SANCTUARY_COMMUNITY_DB_PASSWORD"
|
||||
}
|
||||
```
|
||||
|
||||
L’UUID ci-dessus est un exemple : reprendre celui de votre installation.
|
||||
Définir `SANCTUARY_COMMUNITY_DB_PASSWORD` dans l’environnement du service Minecraft,
|
||||
puis le redémarrer. Utiliser un certificat TLS valide pour l’hôte de la base.
|
||||
Le compte de jeu a besoin de SELECT sur `sanctuary_community_schema`, SELECT,
|
||||
INSERT et UPDATE sur `sanctuary_community_entries`, SELECT et INSERT sur
|
||||
`sanctuary_community_identities`, SELECT et DELETE sur `sanctuary_community_links`.
|
||||
Il n’a besoin ni des tables de comptes Web, ni de droits de création de tables.
|
||||
Le compte Laravel conserve ses permissions habituelles et celles des migrations.
|
||||
|
||||
4. Sur le site : se connecter avec Discord, ouvrir Gazette, demander un code de
|
||||
liaison. En jeu : Gazette → Lier mon compte → saisir le code. La liaison
|
||||
exige `online-mode=true`. Recharger le site après confirmation ; les boutons
|
||||
Publier et Répondre deviennent accessibles.
|
||||
5. Modération : opérateur Minecraft ou staff Web autorisé à `users.moderate`,
|
||||
avec compte lié. Les fonctions de lecture seule du staff ne donnent aucun
|
||||
droit de masquage, épinglage ou publication officielle.
|
||||
|
||||
L’accueil du site conserve sa version de téléchargement beta.151 tant que le
|
||||
pack public n’est pas mis à jour. Ce ticket ne publie pas le site, le canal
|
||||
packwiz ou une installation Prism.
|
||||
|
||||
### Synchronisation et limites de la v1
|
||||
|
||||
Le menu pause recharge ses aperçus toutes les 5 secondes. Les pages de lecture
|
||||
ont un bouton Actualiser. Le site charge les publications à chaque navigation.
|
||||
Les brouillons Minecraft restent en mémoire jusqu’à déconnexion ; le site garde
|
||||
ses brouillons dans le stockage local du navigateur, par compte et serveur.
|
||||
Aucune notification poussée ni import automatique d’archives ou de fichier vers
|
||||
MariaDB. Les anciennes publications d’exemple du site étaient du texte statique,
|
||||
pas des données à migrer. Les pièces jointes et la réattribution d’un compte lié
|
||||
restent hors périmètre.
|
||||
|
||||
### Présentation des discussions et visages
|
||||
|
||||
Les réponses se lisent en conversation : visage Minecraft, pseudo et date,
|
||||
texte dans un bloc distinct, actions compactes derrière « ⋯ ». Le formulaire
|
||||
reste sous la discussion. Les listes et les aperçus du menu pause affichent
|
||||
également les visages. En jeu, les profils sont résolus par les widgets natifs
|
||||
Minecraft, avec leur texture de repli tant que le skin n’est pas disponible.
|
||||
Sur le site, le rendu par UUID utilise le prestataire mc-api.io déjà employé
|
||||
par la page de compte ; chargement différé, sans référent, initiale locale en
|
||||
cas d’erreur. Aucune image ni URL arbitraire n’entre dans le contrat des posts.
|
||||
Documentation du rendu : https://mc-api.io/docs (GET `/render/{uuid}?size=64`).
|
||||
|
||||
|
||||
## Évolution v2 (beta.157)
|
||||
|
||||
Le champ photo et sa migration explicite, avec conservation des anciens articles,
|
||||
sont décrits dans [Photos de Gazette](gazette-photos-beta157.md).
|
||||
Pour une nouvelle base partagée avec le site, utiliser les migrations Laravel.
|
||||
Pour une base autonome administrée par SQL, appliquer schema-v1.sql puis
|
||||
schema-v1-to-v2.sql avant de connecter le mod beta.157.
|
||||
@@ -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,96 @@
|
||||
# beta.157 — photo obligatoire et galerie de captures
|
||||
|
||||
## Contrat de migration communautaire v2
|
||||
|
||||
La génération, les chunks et les sauvegardes Minecraft ne changent pas.
|
||||
Seul le document communautaire évolue : champ `photo`, JPEG encodé en base64,
|
||||
48 Kio binaires maximum, dimensions de 1 × 1 à 960 × 540 pixels.
|
||||
Les captures originales ne sont jamais modifiées. Les copies destinées à la Gazette
|
||||
sont redimensionnées et réencodées, sans métadonnées, avant transmission.
|
||||
|
||||
Les nouveaux articles exigent une photo valide. Les articles v1 restent lisibles ;
|
||||
leur prochaine modification exige une photo. Avis, intendance et réponses restent
|
||||
textuels. La modération ne nécessite pas d'ajouter une photo à un ancien article.
|
||||
La photo est enregistrée atomiquement avec le texte, auteur et révision ; pas de
|
||||
référence à un chemin local, d'URL distante ou de média orphelin persistant.
|
||||
|
||||
Fichier : v1 est lu sans modification. À la première écriture réussie, une copie
|
||||
exacte `sanctuary-community.json.v1.bak` est créée sans écrasement, puis le document
|
||||
v2 est écrit atomiquement. Un backup différent bloque la migration. La limite de
|
||||
64 Mio du document reste appliquée, photos comprises. Un retour à beta.156 exige
|
||||
de restaurer la copie v1 et perd les changements communautaires postérieurs.
|
||||
Ne pas restaurer pendant que le serveur tourne.
|
||||
|
||||
MariaDB : arrêter les écritures du site et du serveur, sauvegarder la base, exécuter
|
||||
explicitement `community/schema-v1-to-v2.sql` ou la migration Laravel équivalente,
|
||||
puis lancer les deux applications v2. Migration additive : colonne `photo` nullable,
|
||||
aucune suppression ni réécriture des articles existants. Le mod n'exécute aucune
|
||||
migration SQL au démarrage. Les applications v1 refusent le schéma v2. Pas de
|
||||
rollback automatique destructif ; restaurer la sauvegarde avec les deux logiciels
|
||||
v1 si nécessaire. Aucun changement n'est appliqué aux bases ou mondes personnels
|
||||
par la préparation de cette livraison.
|
||||
|
||||
Réseau : canaux v1 conservés, action `photo` par fragments de 8 000 caractères,
|
||||
au plus 9 fragments et 65 536 caractères, accusés un par un. Une seule photo
|
||||
transitoire par joueur, liée à l'UUID de publication, expire après deux minutes ;
|
||||
limite globale de 128 transferts. Les listes ne transportent pas les photos ;
|
||||
seul le détail d'un article les contient. Le plafond des réponses
|
||||
passe à 262 144 caractères pour une photo et une discussion complète ; les
|
||||
clients et le serveur doivent être mis à jour ensemble vers beta.157. Les
|
||||
anciens clients ne peuvent plus créer d'article sans photo.
|
||||
|
||||
Galerie : grille de 2 ou 3 colonnes selon la taille d'interface, molette et barre
|
||||
native, 24 captures par page parmi les 4 096 PNG les plus récents par nom, lecture
|
||||
en arrière-plan. Aperçu puis assignation explicite ; Annuler conserve le brouillon.
|
||||
Les captures illisibles restent visibles mais désactivées. Limites sources :
|
||||
32 Mio, 8 192 pixels par axe et 32 millions de pixels. Textures libérées à la
|
||||
fermeture. Le site accepte PNG/JPEG et nécessite PHP GD ; le mod propose les PNG
|
||||
du dossier `screenshots` de son instance.
|
||||
|
||||
## Vérifications
|
||||
|
||||
- Tests natifs Vulkan Minecraft 26.3 : galerie FR/EN aux échelles 2/3/4,
|
||||
molette, pagination, aperçu, annulation avec brouillon conservé, bouton Publier
|
||||
désactivé sans photo, envoi fragmenté, photo visible et relecture après
|
||||
réouverture de l'adaptateur. Parcours réussi en fichier et MariaDB.
|
||||
- Tests métier : photo manquante/invalide, révision, auteur, conservation après
|
||||
modification, reprise idempotente, redémarrage fichier/SQL, ancien article v1,
|
||||
copie de migration exacte, fragments hors ordre/incomplets/expirés.
|
||||
- Site : 17 tests HTTP, 97 assertions réussies ; photo obligatoire, faux fichier,
|
||||
dimensions refusées, réduction, lecture, isolement serveur, auteur, masquage,
|
||||
anciennes publications et conservation après modification.
|
||||
- Échange réel Java/PHP sur une MariaDB isolée : création en jeu lue par le site,
|
||||
modification et réponse Web relues en Java, article/photo du site décodés par
|
||||
Java. Migration Laravel et alternative SQL testées sur bases de développement.
|
||||
- `./gradlew check build assemblePack` : 252 GameTests serveur terminés en
|
||||
3,457 minutes, 229 réussis et les mêmes 23 échecs que la baseline beta.154/155
|
||||
(liste comparée intégralement, aucun nouvel identifiant en échec). La commande
|
||||
générale échoue donc après 6 min 59 s ; cette limite préexistante reste ouverte.
|
||||
- Régression native du menu pause : FR/EN, échelles 2/3/4, carte, défilements
|
||||
indépendants, pagination, article et avis longs, réponse, options et reprise :
|
||||
réussie avec les articles munis d'une photo.
|
||||
- Construction et assemblage séparés, sans relancer les 23 GameTests connus :
|
||||
`check build assemblePack -x :sanctuary:runGameTest` réussit (avec le parcours
|
||||
client natif du menu pause), 2 min 56 s.
|
||||
|
||||
Les captures d'essai sont des mires générées dans une instance de développement,
|
||||
sans accès aux captures ou sauvegardes personnelles. Pas de déploiement sur le
|
||||
site public, serveur personnel, canal packwiz ou Prism. Les JAR beta.154–156
|
||||
restent présents ; JAR et MRpack beta.156 gardent leurs empreintes antérieures.
|
||||
|
||||
|
||||
Le site correspondant est sur `codex/community-photos`, commits `e3ae3eb` et
|
||||
`9626e95`, sans push ni publication. Logs de développement :
|
||||
`build/photos157-client-file.log`, `build/photos157-client-database.log`,
|
||||
`build/photos157-interop-*.log`, `build/photos157-check-build.log` et
|
||||
`build/photos157-final-build.log`.
|
||||
|
||||
## Artefacts locaux
|
||||
|
||||
JAR `mods/sanctuary/build/libs/sanctuary-beta.157.jar` et pack
|
||||
`build/Sanctuary-beta.157.mrpack`, versions et JAR embarqué vérifiés.
|
||||
Les 29 ressources de shaders sont identiques à beta.156 (socle beta.151).
|
||||
Reçu complet : `build/photos157-validation.json`.
|
||||
|
||||
- SHA-256 mods/sanctuary/build/libs/sanctuary-beta.157.jar: `96ff6d22ab593eb113cde63239ff4839f6a75b9a165fbec93a0aaab3c09dee7c`.
|
||||
- SHA-256 build/Sanctuary-beta.157.mrpack: `b03da5f67a3dcf8051666641dccd379a52b8c3c09308e1654b00440a37693e96`.
|
||||
@@ -0,0 +1,68 @@
|
||||
# RENDER-149 — Distances PBR et SSR indépendantes
|
||||
|
||||
Branche `codex/separate-reflection-distances-beta149`, socle beta.148 `6f87144`.
|
||||
|
||||
- Deux réglages : Distance PBR et Distance SSR, chacun à 64 blocs par défaut.
|
||||
Choix 16, 32, 64, 128, 256 blocs, enregistrés séparément.
|
||||
- Le PBR conserve son propre fondu et sa propre limite de portée. Le SSR
|
||||
utilise sa distance pour les surfaces réfléchissantes et sa recherche de
|
||||
paysage. Le maillage et la sélection des surfaces couvrent le maximum des
|
||||
deux portées actives, afin que réduire l’une ne coupe pas l’autre.
|
||||
- Ancien défaut commun 32 migré une fois vers PBR 64 ; nouveau réglage SSR 64.
|
||||
Les anciennes valeurs PBR différentes de 32 restent conservées. Après la
|
||||
migration, un choix explicite 32 est conservé comme les autres valeurs.
|
||||
- Option nommée exactement « SSR » en français et en anglais, sans la mention
|
||||
« expérimental ». Les autres options expérimentales du jeu sont inchangées.
|
||||
- Intensités PBR 50 % et SSR 20 %, rendu solaire diffus de beta.148 conservés.
|
||||
SSR conserve sa dépendance au PBR et les limites du paysage visible/chargé
|
||||
et du budget de maillage.
|
||||
|
||||
- Les feuilles (et fibres de laine, même profil entièrement mat) ne reçoivent
|
||||
plus de large éclat spéculaire blanc. Leur relief normal reste présent.
|
||||
La pierre et le bois retrouvent exactement leur poids spéculaire précédent,
|
||||
comme les surfaces polies, les métaux, l’eau et le verre. La suppression
|
||||
concerne les profils non métalliques de rugosité 0,98, avec transition 0,90–0,98.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Contrôles de persistance indépendante, valeurs par défaut et libellés FR/EN.
|
||||
Scènes graphiques avec PBR variable/SSR 256 puis PBR 256/SSR variable.
|
||||
- Suite PBR/préférences Vulkan réussie : deux défauts 64 et persistance
|
||||
indépendante sur 16/32/64/128/256 ; libellés FR/EN et nom SSR vérifiés.
|
||||
Journal : `build/beta149-pbr-vulkan.log`.
|
||||
- Suites SSR Vulkan et OpenGL réussies avec les portées dissociées, F5,
|
||||
intersections, eau/verre/métal, soleil/lune, OFF/zéro et rechargement.
|
||||
Journaux : `build/beta149-ssr-vulkan.log`, `build/beta149-ssr-opengl.log`.
|
||||
Ces passes précèdent l’ajustement final du profil entièrement mat.
|
||||
- Suite PBR Vulkan finale avec feuillage réussie : 170 172 pixels de feuilles
|
||||
contrôlés, saturation relative 1,00007, relief visible sur 63 236 pixels.
|
||||
Journal : `build/beta149-foliage-vulkan.log` ; captures correspondantes
|
||||
dans `build/evidence/beta149-foliage-vulkan/`.
|
||||
- Bois et briques après recentrage sur le feuillage : aucun pixel modifié
|
||||
par rapport à la capture précédant l’ajustement, sur les zones de contrôle
|
||||
de 26 304 et 40 896 pixels respectivement.
|
||||
- `./gradlew check build` exécuté dans l’instantané isolé : 252 tests serveur,
|
||||
229 réussis et 23 échecs, mêmes identifiants qu’en beta.148. Aucun nouvel échec.
|
||||
Journal : `build/beta149-check-build-isolated.log` ; comparaison :
|
||||
`build/beta149-server-failures.json`. L’ajustement final du feuillage concerne
|
||||
seulement le shader et son test client. Le contrôle global reste en échec.
|
||||
|
||||
- Suite PBR OpenGL finale réussie également : 170 199 pixels de feuilles,
|
||||
saturation relative 1,00001, relief visible sur 63 263 pixels.
|
||||
Journal : `build/beta149-foliage-opengl.log` ; captures correspondantes
|
||||
dans `build/evidence/beta149-foliage-opengl/`.
|
||||
- Assemblage final réussi : `./gradlew build assemblePack -x :sanctuary:check`,
|
||||
sans répéter les contrôles purs/serveur exécutés dans l’instantané.
|
||||
Journal : `build/beta149-build-pack.log`. Cette exclusion ne transforme
|
||||
pas le contrôle complet en réussite.
|
||||
|
||||
## Artefact
|
||||
|
||||
[Sanctuary-beta.149.mrpack](../build/Sanctuary-beta.149.mrpack), copié aussi dans
|
||||
`sanctuary-beta/build/` avec les autres versions. Archive vérifiée, un seul JAR
|
||||
Sanctuary beta.149 identique au JAR construit, shaders identiques aux sources,
|
||||
libellés SSR et deux portées FR/EN vérifiés. Minecraft 26.3 / Loader 0.19.5.
|
||||
Taille : 10 520 465 octets. SHA-256 :
|
||||
`ef5519d9a0e2006b865b62371a6f8712db02ab138aa9698339d6fdb02fb4befa`.
|
||||
Reçu : `build/beta149-artifacts.json`. Aucune installation Prism ni publication.
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
# RENDER-150 — Terre mate et reflets solaires colorés
|
||||
|
||||
Branche `codex/matte-dirt-beta150`, socle beta.149 `1c23d0c`.
|
||||
|
||||
La terre sèche utilise un profil entièrement mat (rugosité 0,98, métal 0),
|
||||
comme le feuillage corrigé en beta.149. Cela enlève l’éclat blanc/gris diffus
|
||||
sans supprimer les normales ni le contraste de relief. Le SSR de ces matières
|
||||
est également atténué par leur rugosité, sans changer son réglage global.
|
||||
|
||||
Blocs concernés : dirt, coarse_dirt, rooted_dirt, grass_block, dirt_path,
|
||||
farmland, podzol et mycelium. Les faces terreuses suivent ainsi le même profil.
|
||||
Les autres profils de matière sont inchangés.
|
||||
|
||||
Le disque du soleil et son reflet utilisent la même couleur native
|
||||
`SkyRenderState.sunriseAndSunsetColor`, issue de l’attribut environnemental
|
||||
Minecraft `SUNRISE_SUNSET_COLOR`. Son alpha pilote la transition depuis le
|
||||
blanc : aucune palette horaire ou altitude artificielle. Le soleil reste
|
||||
neutre tant que le dégradé natif est absent et se colore avec le ciel.
|
||||
La teinte solaire est convertie en lumière linéaire pour le PBR. Le mixin
|
||||
modifie seulement la couleur du disque solaire, préserve l’alpha de pluie,
|
||||
la texture native et la lune, et respecte la désactivation globale de Vanilla Light.
|
||||
La fusion diffuse de l’eau et sa normale animée d’origine sont conservées.
|
||||
Les accents suivent les pentes des vaguelettes qui réfléchissent le soleil
|
||||
vers la caméra. Leur pulsation vient des normales animées de la texture,
|
||||
sans oscillateur indépendant ni inversion artificielle du relief.
|
||||
Une faible variation stable par texel évite des crêtes toutes identiques ;
|
||||
elle module seulement l’intensité, sans produire des points isolés aléatoires.
|
||||
Les détails s’atténuent lorsqu’ils deviennent plus petits qu’un pixel écran.
|
||||
Les accents restent limités au reflet solaire et conservent sa couleur.
|
||||
Les lampes et la lune gardent leurs
|
||||
couleurs propres.
|
||||
PBR 50 %, SSR 20 % et deux portées indépendantes de 64 blocs par défaut conservés.
|
||||
Minecraft 26.3, Java 25 et dépendances inchangés ; aucune sauvegarde modifiée.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Tests des profils de terre et scènes GPU comparant PBR OFF/ON sur terre et
|
||||
feuillage : conservation de la couleur native et présence du relief.
|
||||
- Suites PBR finales Vulkan/OpenGL réussies, dont profils des huit types de
|
||||
sol, proximité, intensités, préférences et feuillage.
|
||||
Journaux : `build/beta150-pbr-vulkan-final.log` et
|
||||
`build/beta150-pbr-opengl-final.log`.
|
||||
- Terre : 180 469 pixels contrôlés, saturation relative 0,99877 sur les deux
|
||||
moteurs ; relief visible sur 145 415 pixels. Captures conservées dans
|
||||
`build/evidence/beta150-pbr-vulkan-final/` et
|
||||
`build/evidence/beta150-pbr-opengl-final/`.
|
||||
|
||||
Les tests comparent la contribution PBR au rendu natif et capturent aussi
|
||||
le disque du soleil, avant puis pendant la transition native du ciel.
|
||||
Le contrôle serveur `./gradlew check build`, exécuté dans une copie isolée
|
||||
`build/server-validation` avant les derniers ajouts exclusivement client,
|
||||
compte 229 réussites et 23 échecs sur 252 tests. Les 23 identifiants d’échec
|
||||
sont identiques à beta.149 ; aucun nouvel identifiant. Le contrôle global
|
||||
n’est donc pas vert. Journaux : `build/beta150-check-build-isolated.log` et
|
||||
`build/beta150-server-failures.json`.
|
||||
|
||||
Les scènes solaires comparent le rendu PBR au natif, avant et pendant le
|
||||
coucher de soleil. À 2 000 ticks, la teinte native est blanche ; à 11 500 ticks
|
||||
sa transition commence légèrement (alpha 0,051) ; à 12 400 ticks l’alpha
|
||||
natif atteint 0,831 et le disque devient chaud (centre RGB 255/255/135).
|
||||
Les deux moteurs ont validé cette synchronisation native.
|
||||
|
||||
Un lancement sans sélection PBR a également rencontré l’assertion client
|
||||
Blocodex « The full snapshot must become visible together », avant les scènes
|
||||
GPU (`build/beta150-water-motion-vulkan.log`). Il ne constitue pas une
|
||||
validation de la suite client générale.
|
||||
|
||||
La suite finale Vulkan des éclats liés aux vaguelettes passe
|
||||
(`build/beta150-wave-glints-vulkan.log`) : animation native, masquage F5,
|
||||
distances indépendantes, reflets solaires et lunaires, matériaux transparents,
|
||||
rechargement des ressources et redimensionnement. Dans la scène solaire,
|
||||
150 563 pixels sont éclaircis, sans pixel blanc saturé.
|
||||
|
||||
La suite OpenGL finale passe également
|
||||
(`build/beta150-wave-glints-opengl.log`) : 148 180 pixels éclaircis, aucun
|
||||
pixel blanc saturé. Les captures finales sont conservées dans
|
||||
`build/evidence/beta150-wave-glints-vulkan/` et
|
||||
`build/evidence/beta150-wave-glints-opengl/`.
|
||||
Le ressenti des vaguelettes en mouvement reste à confirmer par le créateur
|
||||
sur sa configuration ; les tests ne constituent pas une validation esthétique.
|
||||
|
||||
## Pack local
|
||||
|
||||
Construction et assemblage réussis avec
|
||||
`./gradlew build assemblePack -x :sanctuary:check`, après le contrôle complet
|
||||
ci-dessus. L’exclusion ne transforme pas ses échecs connus en réussite.
|
||||
Export packwiz puis vérification ZIP, version Fabric, Minecraft 26.3,
|
||||
Loader 0.19.5, JAR unique identique au construit, shader final et mixin solaire.
|
||||
|
||||
- Pack : `/Users/koka/Documents/sanctuary/sanctuary-beta/build/Sanctuary-beta.150.mrpack`
|
||||
- Taille : 10522942 octets.
|
||||
- SHA-256 pack : `65a4a44948dcdcf0325b5e24c076a03f8832184f25f5c1ff16f69bd3ee641b93`.
|
||||
- SHA-256 JAR : `4962e6f8cafd6ef3e81e088bf9b0092a264441632cfa095d4fe7394b855f093d`.
|
||||
- Reçu : `build/beta150-artifacts.json`, également copié à côté du pack.
|
||||
|
||||
Aucun canal publié, aucune instance Prism ni sauvegarde personnelle modifiée.
|
||||
@@ -0,0 +1,121 @@
|
||||
# WATER-144 — Eau naturelle pixélisée
|
||||
|
||||
Branche `codex/water-beta144`, socle beta.143 publiée (`97ff2b5`) et suivi
|
||||
`580b241`. Minecraft 26.3, Java 25, dépendances conservées.
|
||||
|
||||
## Contrat
|
||||
|
||||
**Options → Shaders → Eau naturelle** est activée par défaut avec Sanctuary.
|
||||
L’intensité (0–100 %, défaut 100 %) agit sur la surface et l’immersion.
|
||||
L’interrupteur ou 0 % retrouve le rendu précédent. Les préférences existantes
|
||||
restent conservées ; un moteur externe suspend l’effet comme les autres effets
|
||||
natifs Sanctuary.
|
||||
|
||||
La couleur native du biome reste la base, y compris les variantes d’océans.
|
||||
Les océans (`minecraft:is_ocean`) imposent le profil clair.
|
||||
Hors océan, le premier support situé au maximum huit blocs sous la cellule
|
||||
observée choisit le profil :
|
||||
|
||||
| Profil | Fond | Mélange olive #788448 | Alpha natif | Distance de brume native |
|
||||
| --- | --- | ---: | ---: | ---: |
|
||||
| Claire | Pierre, matériaux construits, inconnus | 0 % | ×0,55 | ×1 |
|
||||
| Légèrement teintée | Sable, sable rouge, gravier, argile | 10 % | ×0,80 | ×0,70 |
|
||||
| Étang | Terre, terre stérile/enracinée, herbe, podzol, mycélium, boue, racines de palétuvier boueuses | 85 % | ×1 | ×0,40 |
|
||||
|
||||
L’eau et les plantes aquatiques sans collision sont traversées. Un support
|
||||
immergé reste un fond ; absence de support, air ou données manquantes donnent
|
||||
une eau claire. La référence visuelle fournie pendant l’implémentation demande un véritable
|
||||
vert olive légèrement jaunâtre : le profil étang passe donc de 45 % à 85 %
|
||||
de mélange, tout en laissant les eaux claires et océaniques natives.
|
||||
Les surfaces profondes sans fond dans la limite restent donc
|
||||
claires. Les profils se mélangent aux sommets sur un bloc, sans filtrer les
|
||||
textures. L’eau sous un toit suit son fond, pas l’éclairage du bâtiment.
|
||||
|
||||
La ressource client `assets/sanctuary/water_materials.json` accepte deux listes
|
||||
d’identifiants de blocs, `light` et `pond`, remplaçables par un resource pack.
|
||||
Les blocs absents des listes sont clairs. Une ressource invalide est signalée
|
||||
et reprend les familles embarquées. Le template personnel inclut cette ressource.
|
||||
|
||||
## Rendu et coût
|
||||
|
||||
Modification des seules couleurs/alpha des sommets de `FluidRenderer`, sans
|
||||
passe écran ni duplication de maillage. Textures, UV, animation, éclairage
|
||||
et tri des transparences restent natifs. Les calculs de profils sont différés
|
||||
jusqu’à l’émission d’un sommet visible, puis mutualisés dans la région compilée.
|
||||
Un test de palette élimine les sections sans eau avant toute copie de biomes.
|
||||
Les états de blocs viennent des copies natives ; les appartenances océaniques
|
||||
sont capturées avant l’envoi au worker. Aucun chargement de chunk n’est demandé.
|
||||
|
||||
Un changement de bloc invalide la zone dépendante, huit blocs au-dessus et
|
||||
un bloc horizontalement. Les reconstructions natives recopient leurs données ;
|
||||
l’effet ne déclenche aucune reconstruction pour une lumière mobile.
|
||||
Un changement d’option reconstruit seulement les sections chargées contenant de
|
||||
l’eau ; les caches des autres effets restent disponibles. Le cache temporaire
|
||||
disparaît avec sa compilation, sans historique de monde.
|
||||
|
||||
Sous l’eau, le profil est échantillonné au maximum une fois par tick, avec
|
||||
transition de 0,5 seconde. La couleur et les distances environnementales sont
|
||||
adaptées dans le rendu natif du brouillard. La teinte finale est appliquée après
|
||||
la normalisation de la vision aquatique, pour éviter de transformer l’olive en
|
||||
jaune fluorescent ; la luminosité issue des modificateurs natifs de vision
|
||||
et le bonus du compagnon calmar sont conservés. Lave et brume atmosphérique ne changent
|
||||
pas. Il n’y a pas de simulation d’absorption volumétrique ni de reflet ajouté.
|
||||
|
||||
## Validation native
|
||||
|
||||
Les essais utilisent un monde plat jetable, graine 122, avec bassins sur quartz,
|
||||
sable et boue, support du sable, plantes aquatiques, toit, frontière de sections
|
||||
et changement de biome océan. Une copie personnelle de ressource remplace
|
||||
réellement les familles de matériaux puis est désactivée.
|
||||
|
||||
Les suites graphiques OpenGL et Vulkan ont passé les contrôles de surface,
|
||||
d’immersion, de fond modifié, d’océan, de ressources, de préférences et de
|
||||
cascades pendant le développement. La dernière optimisation reconstruit
|
||||
seulement les sections contenant de l’eau lors du changement d’option.
|
||||
Sa série de validation finale reste à terminer avant publication.
|
||||
|
||||
### Protocole de performance
|
||||
|
||||
Apple M1, macOS, Java 25, 854×480, distance de rendu 6 chunks,
|
||||
ombres 32 blocs / 16 pixels, PBR 50 %, autres effets activés.
|
||||
VSync désactivée, limite 260 (illimitée), limitation AFK désactivée.
|
||||
Pour chaque scène : OFF/ON/ON/OFF/ON/OFF/OFF/ON, dix secondes par passage,
|
||||
avec stabilisation des sections puis quatre secondes de chauffe entre passages.
|
||||
Le déplacement avance de 120 blocs et laisse les sections se compiler
|
||||
naturellement, sans vidange artificielle des caches des autres effets.
|
||||
Les moyennes des quatre médianes et P95 par état sont comparées avec les seuils
|
||||
+5 % et +2 ms. Les temps de compilation de sections sont consignés également.
|
||||
|
||||
Les essais courts initiaux ont montré une forte variabilité. Une limitation AFK
|
||||
à 30 FPS puis des clients Minecraft concurrents dans le chantier PBR/SSR ont
|
||||
été identifiés. Ces essais restent dans les preuves de développement ; ils
|
||||
ne valident pas la livraison. La série finale attend la fin du chantier
|
||||
concurrent, conformément à la demande du créateur. Les résultats ne sont pas
|
||||
présentés comme acquis, et aucun gain de performance universel n’est promis.
|
||||
|
||||
Pour reproduire, avec Java 25 et un environnement graphique disponible :
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
|
||||
-PsanctuaryPixelShadows122ClientTests=true -PsanctuaryWater144ClientTests=true \
|
||||
-PsanctuaryClientNoVsync=true -PsanctuaryQuickTests=true \
|
||||
-PsanctuaryClientGraphicsBackend=vulkan > build/water144-vulkan.log 2>&1
|
||||
python3 scripts/check_water_performance.py build/water144-vulkan.log
|
||||
```
|
||||
|
||||
Répéter avec `opengl`, sans autre client graphique simultané. Le vérificateur
|
||||
refuse les séries incomplètes et les dépassements ; il écrit le reçu détaillé
|
||||
près du journal. Le marqueur `WATER144_PASS` atteste les contrôles fonctionnels.
|
||||
|
||||
### Assemblage et publication
|
||||
|
||||
Le GameTest serveur dédié reste exclu conformément au refus antérieur de son
|
||||
EULA. La commande de livraison est `./gradlew check build assemblePack
|
||||
assembleTestPack -x :sanctuary:runGameTest` ; la suite complète non exclue
|
||||
n’est pas déclarée réussie. Les contrôles natifs de l’eau utilisent le serveur
|
||||
intégré d’un monde jetable. Aucun essai Windows n’est revendiqué.
|
||||
|
||||
La release, le canal et l’instance ne sont pas encore mis à jour.
|
||||
Les preuves détaillées restent dans les dossiers ignorés :
|
||||
`build/water144-*.log`, `build/water144-*-performance.json`,
|
||||
`build/water144-*-isolation.json` et `build/water144-evidence/`.
|
||||
@@ -0,0 +1,93 @@
|
||||
# FIX-151 — Nether, PBR opaque et SSR
|
||||
|
||||
Branche `codex/nether-wood-beta151`, socle beta.150 `448e2fe`.
|
||||
|
||||
## Déconnexion Nether
|
||||
|
||||
Le rapport Windows beta.150 transmis le 18 septembre 2026 indique
|
||||
`IndexOutOfBoundsException: Index 46 out of bounds for length 46` lors de
|
||||
`ClientPacketListener.handleContainerContent`, paquet `container_set_content`.
|
||||
Le passage au Nether dans le test client local reproduit exactement cette trace
|
||||
(journal `build/beta151-client-first.log`). Ce défaut n’est pas un crash GPU.
|
||||
|
||||
Minecraft recrée le joueur client et son menu lors de `handleRespawn`, utilisé
|
||||
aussi pour les changements de dimension. La progression n’était réattachée
|
||||
qu’en fin de tick ; un paquet d’inventaire dans le même lot arrivait avant la
|
||||
création des 18 cases supplémentaires et des 3 accessoires. Le correctif lie la
|
||||
progression déjà reçue au nouveau joueur dès la fin du traitement du paquet de
|
||||
respawn, avant le paquet suivant. Aucun paquet ni objet n’est ignoré et aucun
|
||||
format de sauvegarde ne change. Sans progression Sanctuary active, le chemin
|
||||
vanilla reste inchangé.
|
||||
|
||||
## Rendu
|
||||
|
||||
La beta.146 avait porté le gain solaire des surfaces opaques de 3 à 20
|
||||
(4 × 5), soit environ 6,7 fois plus. Le renforcement prévu pour les reflets de
|
||||
l’eau avait ainsi créé un voile sur les matières sombres, dont l’obsidienne.
|
||||
Les surfaces opaques retrouvent le gain 3 et le budget spéculaire total 0,5,
|
||||
partagé entre soleil et lampes. La teinte native du coucher de soleil, les
|
||||
normales et le relief sont conservés. Eau et verre conservent leur traitement.
|
||||
Le bois utilise le profil mat 0,98 : planches, bûches et variantes de construction,
|
||||
bambou, tiges et hyphes du Nether. Ce classement ne transforme pas le nylium en bois.
|
||||
|
||||
SSR est activé par défaut à 20 %, avec portée 64 blocs. Un choix OFF déjà
|
||||
sauvegardé reste OFF. PBR reste à 50 % par défaut.
|
||||
|
||||
## Cible graphique
|
||||
|
||||
Décision du créateur pendant ce ticket : **Vulkan uniquement** pour les nouvelles
|
||||
validations. Le passage OpenGL de ce ticket s’était terminé avant cette demande ;
|
||||
il reste une trace historique, pas une cible à conserver ni à relancer.
|
||||
Cette décision est reportée dans `AGENTS.md` et `docs/testing.md`.
|
||||
|
||||
## Vérification
|
||||
|
||||
Les assertions GPU Vulkan et les quatre transferts successifs
|
||||
Overworld → Nether → Overworld → Nether → Overworld passent sur un nouveau monde
|
||||
plat de développement, graine 122. Après chaque transfert : menu de 67 cases,
|
||||
pile de 7 diamants en inventaire natif, 3 émeraudes dans la première case
|
||||
supplémentaire et 5 lingots d’or dans la dernière, toujours présents côté client.
|
||||
|
||||
Les scènes PBR conservent 99,878 % de la saturation relative du chêne et 99,968 %
|
||||
de celle de l’épicéa écorcé sur 185 776 pixels chacune. Le relief affecte
|
||||
respectivement 101 382 et 51 671 pixels. De face, l’obsidienne ne gagne en moyenne
|
||||
que 5,114 niveaux RGB sur 255 (102 480 pixels), avec relief visible sur 95 225
|
||||
pixels. Captures inspectées : bois et obsidienne PBR OFF/ON.
|
||||
|
||||
Les contrôles couvrent aussi les réglages SSR absents (ON/20 %), la persistance
|
||||
d’un choix OFF, les profils bois, les petits déplacements de caméra, les
|
||||
intensités, le rechargement des ressources et le redimensionnement.
|
||||
Le premier passage Vulkan corrigé valide toutes les assertions, mais se bloque
|
||||
ensuite à la fermeture dans les barrières de synchronisation du programme de
|
||||
test Fabric ; son processus est arrêté après capture des piles de threads.
|
||||
Premier journal : `build/beta151-client-vulkan-final.log` ; diagnostic :
|
||||
`build/beta151-client-shutdown-threads.txt`. Le second passage Vulkan réussit entièrement, fermeture comprise, en **3 min 9 s**
|
||||
(`build/beta151-client-vulkan-clean.log`). La suite OpenGL terminée avant l’abandon de
|
||||
cette cible a réussi en 2 min 42 s ; aucun nouveau test OpenGL n’est prévu.
|
||||
|
||||
Le contrôle complet `./gradlew check build` dans `build/server-validation`
|
||||
(3 068 fichiers de production identiques aux sources courantes) termine en
|
||||
11 min 13 s : **229 réussites, 23 échecs sur 252**. Les 23 identifiants sont
|
||||
strictement identiques à beta.150 : aucun nouveau, aucun résolu.
|
||||
Le contrôle global reste rouge. Journaux et liste :
|
||||
`build/beta151-check-build.log`, `build/beta151-server-failures.json`.
|
||||
Ces échecs restent ouverts et ne constituent pas la validation du correctif
|
||||
réseau ; celui-ci repose sur la reproduction puis le test client des transferts.
|
||||
|
||||
## Pack local
|
||||
|
||||
`./gradlew build assemblePack -x :sanctuary:check` réussit en 11 s,
|
||||
indépendamment du contrôle complet en échec ci-dessus. Export packwiz réussi ;
|
||||
ZIP, version Fabric, Minecraft 26.3, Loader 0.19.5, JAR Sanctuary unique,
|
||||
mixin de respawn et shader exacts, classes conformes aux sorties compilées et
|
||||
absence des tests dans le JAR vérifiés.
|
||||
|
||||
- Pack : `/Users/koka/Documents/sanctuary/sanctuary-beta/build/Sanctuary-beta.151.mrpack`.
|
||||
- SHA-256 pack : `1d87804d75edda0a95c318ea3feaf47ecadeeb29d86ca55483a42741d7a5fcab`.
|
||||
- SHA-256 JAR : `6676c67f8be4e650a9b6a8213c8b0fdf3cf7e62b030c3d1a6d4805d438a8e543`.
|
||||
- Reçu : `build/beta151-artifacts.json`.
|
||||
|
||||
Les essais sont effectués sur Apple M1 ; la validation sur le PC Windows du
|
||||
créateur reste à faire.
|
||||
|
||||
Aucune sauvegarde personnelle ouverte, aucun canal publié ni instance installée.
|
||||
+175
@@ -14,6 +14,181 @@ 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
|
||||
préservant la teinte. Torches natives par défaut, option chaude, cuivre vert
|
||||
et redstone rouge. [Contrat et vérifications](colored-dynamic-beta140.md).
|
||||
[Release beta.140](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.140)
|
||||
publiée ; canal `2c360a0759d4a164f82c89ac2bef04ff69d50875`. 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.140/`. Archives vérifiées dans `sanctuary-beta/build/`.
|
||||
|
||||
## beta.138 — Densité des rayons selon la météo
|
||||
|
||||
Brouillard plus dense et diffus, précipitations et atténuation de l'orage.
|
||||
Réglage manuel conservé, aucune nouvelle option ni modification de sauvegarde.
|
||||
[Contrat et vérifications](shader-beta138.md).
|
||||
[Release beta.138](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.138)
|
||||
publiée ; canal `c049423de1971079b9d61874d83adeb6f1ad49f7`. Deux synchronisations isolées,
|
||||
puis deux dans la même instance Sanctuary Beta réussies. Les 923 fichiers
|
||||
personnels suivis gardent leurs hashes. Aucun monde personnel ouvert ;
|
||||
sauvegarde préalable `sanctuary-backups/before-beta.138/`.
|
||||
Packs normal/Test/template vérifiés dans `sanctuary-beta/build/`.
|
||||
|
||||
## beta.137 — Rayons solaires volumétriques
|
||||
|
||||
Rayons à travers les ouvertures et le feuillage, activés à 35 %, indépendants
|
||||
du bloom et de l'affichage des ombres de surface. Carte solaire partagée.
|
||||
[Contrat et vérifications](shader-beta137.md).
|
||||
[Release beta.137](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.137)
|
||||
publiée ; canal `68cb39a5edbf5d34def02f670e2488ee852605b8`. Deux synchronisations isolées,
|
||||
puis deux dans la même instance Sanctuary Beta réussies. Les 923 fichiers
|
||||
personnels suivis gardent leurs hashes. Aucun monde personnel ouvert ;
|
||||
sauvegarde préalable `sanctuary-backups/before-beta.137/`.
|
||||
Packs normal/Test/template vérifiés dans `sanctuary-beta/build/`.
|
||||
|
||||
## beta.136 — Lens flare rectangulaire
|
||||
|
||||
Rectangles à dégradé doux pour le soleil et la lune, activés à 30 % et
|
||||
réglables indépendamment du bloom. Occultation par les blocs et phases lunaires.
|
||||
[Contrat et vérifications](shader-beta136.md).
|
||||
[Release beta.136](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.136)
|
||||
publiée ; canal `dd5b545a7cfbb957f2c70e9dc17034b44caef98f`. Deux synchronisations isolées,
|
||||
puis deux dans la même instance Sanctuary Beta réussies. Les 923 fichiers
|
||||
personnels suivis gardent leurs hashes. Aucun monde personnel ouvert ;
|
||||
sauvegarde préalable `sanctuary-backups/before-beta.136/`.
|
||||
Packs normal/Test/template vérifiés dans `sanctuary-beta/build/`.
|
||||
|
||||
## beta.135 — SSGI en proximité et nouveaux défauts
|
||||
|
||||
Normales stables près des surfaces, atténuation progressive du rebond au contact.
|
||||
Bloom à 20 %, PBR et SSGI activés par défaut, choix personnels existants conservés.
|
||||
[Contrat et vérifications](shader-beta135.md).
|
||||
[Release beta.135](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.135)
|
||||
publiée ; canal `5b0df577fcd036937c25be3a1f4580f9db3103a1`. Deux synchronisations isolées,
|
||||
puis deux dans la même instance Sanctuary Beta réussies. Les 923 fichiers
|
||||
personnels suivis gardent leurs hashes. Aucun monde personnel ouvert ;
|
||||
sauvegarde préalable `sanctuary-backups/before-beta.135/`.
|
||||
Packs normal/Test/template vérifiés dans `sanctuary-beta/build/`.
|
||||
|
||||
## beta.134 — PBR optionnel et SSGI filtré
|
||||
|
||||
Normales procédurales en cache, rugosité/métallicité par famille et reflets
|
||||
directionnels. Filtrage SSGI et suppression de sa forte réinjection neutre.
|
||||
[Contrat et vérifications](shader-beta134.md).
|
||||
[Release beta.134](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.134)
|
||||
publiée ; canal `35ec0f6e145002f6367944df5b75e2703958eaef`. Deux synchronisations isolées,
|
||||
puis deux dans la même instance Sanctuary Beta réussies. Les 923 fichiers
|
||||
personnels suivis gardent leurs hashes. Aucun monde personnel ouvert ;
|
||||
sauvegarde préalable `sanctuary-backups/before-beta.134/`.
|
||||
Packs normal/Test/template vérifiés dans `sanctuary-beta/build/`.
|
||||
|
||||
## beta.133 — SSGI renforcé
|
||||
|
||||
Diffusion des couleurs rendue visible au réglage 35, même budget de rayons
|
||||
et cibles GPU. [Contrat et vérifications](shader-beta133.md).
|
||||
[Release beta.133](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.133)
|
||||
publiée ; canal `9ceb5a5353c9d1b8bb7834a20e818fd65096ff98`. Deux synchronisations isolées,
|
||||
puis deux dans la même instance Sanctuary Beta réussies. Les 923 fichiers
|
||||
personnels suivis gardent leurs hashes. Aucun monde personnel ouvert ;
|
||||
sauvegarde préalable `sanctuary-backups/before-beta.133/`.
|
||||
Packs normal/Test/template vérifiés dans `sanctuary-beta/build/`.
|
||||
|
||||
## beta.132 — SSGI expérimental
|
||||
|
||||
Diffusion locale des couleurs visibles, rayon 2,5 blocs, intensité réglable,
|
||||
initialement OFF. [Contrat et vérifications](shader-beta132.md).
|
||||
[Release beta.132](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.132)
|
||||
publiée ; canal `9edaaedc0205c525ecce9de66b5ca7827c56c28f`. Deux synchronisations isolées,
|
||||
puis deux dans la même instance Sanctuary Beta réussies. Les 923 fichiers
|
||||
personnels suivis gardent leurs hashes. Aucun monde personnel ouvert ;
|
||||
sauvegarde préalable `sanctuary-backups/before-beta.132/`.
|
||||
Packs normal/Test/template vérifiés dans `sanctuary-beta/build/`.
|
||||
|
||||
## beta.131 — émissifs indépendants et précision étendue
|
||||
|
||||
Bloom, émissifs et highlight ON initialement. Couper le halo ou régler son
|
||||
intensité à zéro garde les inclusions lumineuses. Précisions 64/128 ajoutées,
|
||||
défaut 16 conservé. [Contrat et vérifications](shader-beta131.md).
|
||||
[Release beta.131](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.131)
|
||||
publiée ; canal `efb18c274d95d988a9566b8c52751469f002cdda`. Deux synchronisations isolées,
|
||||
puis deux dans la même instance Sanctuary Beta réussies. Les 923 fichiers
|
||||
personnels suivis sont conservés, préférences et sauvegardes comprises.
|
||||
Aucun monde personnel ouvert ; sauvegarde `sanctuary-backups/before-beta.131/`.
|
||||
Archives normal/Test/template vérifiées dans `sanctuary-beta/build/`.
|
||||
|
||||
## beta.130 — bloom et précision commune
|
||||
|
||||
Bloom réglable, masques sélectifs des vingt textures de minerais, précision
|
||||
commune aux ombres et au highlight. Réglages initiaux 32/128/16 et Pixel Shadows
|
||||
activé ; anciennes préférences conservées. [Contrat et vérifications](shader-beta130.md).
|
||||
[Release beta.130](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.130)
|
||||
publiée ; canal `68e06353bf880af04c1c72d4e3a159f11f94b619`. Deux synchronisations isolées
|
||||
puis deux dans la même instance **Sanctuary Beta** réussies. Les 923 fichiers
|
||||
personnels suivis, réglages de shaders compris, gardent leurs hashes. Aucun
|
||||
monde personnel ouvert ; sauvegarde préalable `sanctuary-backups/before-beta.130/`.
|
||||
Packs normal/Test et template vérifiés et copiés dans `sanctuary-beta/build/`.
|
||||
|
||||
## beta.129 — reflets CTM et portées 16–256 blocs
|
||||
|
||||
Reflets connectés entre tous les matériaux coplanaires, portée indépendante,
|
||||
ombres à 128/256 blocs, intensité rétablie à midi et correction du réalignement
|
||||
cyclique de la grille lumineuse. [Contrat et vérifications](shader-beta129.md).
|
||||
[Release beta.129](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.129)
|
||||
publiée ; canal `2eaa0130a4cd475c598beefdab40f48fc8681ab7`. Deux synchronisations isolées
|
||||
puis deux dans la même instance **Sanctuary Beta** réussies. Les 923 fichiers
|
||||
personnels suivis, réglages de shaders compris, sont conservés. Aucun monde
|
||||
personnel ouvert ; copie préalable dans `sanctuary-backups/before-beta.129/`.
|
||||
Packs normal/Test et template copiés et vérifiés dans `sanctuary-beta/build/`.
|
||||
|
||||
## beta.128 — stabilité des ombres, lune et feuillage
|
||||
|
||||
Ombres des blocs nettes et stabilisées, atténuation à midi, lune légère et
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
# PAUSE-155 — une entrée Sanctuary, trois espaces
|
||||
|
||||
Branche `codex/pause-redesign-beta155`, livraison locale `beta.155`.
|
||||
Suite de [COMMUNITY-154](community-beta154.md).
|
||||
|
||||
## Résultat attendu
|
||||
|
||||
Remplacer l'ancienne grille du menu pause par trois espaces sans fond ou
|
||||
séparateurs imposés à chaque espace :
|
||||
|
||||
- En-tête : date locale à gauche, dernier message d'intendance au centre,
|
||||
heure locale à droite. Le message ouvre directement sa publication.
|
||||
- Corps : navigation permanente Découverte, Monde, Progression, Habitant,
|
||||
Faction et Batailles à gauche, tous espacés de 4 pixels comme les boutons
|
||||
d’options. Carte personnelle au centre. Gazette au-dessus du tableau d'affichage à droite.
|
||||
- Bas de la colonne gauche : Options, Options du monde, puis Sauvegarder et
|
||||
quitter, empilés dans cet ordre et alignés avec les six boutons Sanctuary.
|
||||
Reprendre la partie se trouve seul tout en bas de la colonne centrale,
|
||||
sous la carte. Les actions natives Minecraft sont réutilisées, y compris
|
||||
Déconnexion en multijoueur. À grande échelle d'interface, les six boutons
|
||||
de navigation deviennent plus compacts pour préserver cette séparation.
|
||||
|
||||
La Gazette présente des boutons-titres avec la tête de l’auteur dans un carré
|
||||
de 20 pixels à gauche, sur la même ligne ; le nom reste disponible au survol. Les demandes prennent la forme de
|
||||
cartes encadrées avec aperçu, auteur, catégorie et état. Chaque zone possède
|
||||
son propre défilement à la molette et sa barre de défilement. Le bouton +
|
||||
ouvre la rédaction, l'en-tête ouvre la liste complète. « Afficher la suite »
|
||||
ajoute la page suivante sans quitter le menu ni remettre le défilement à zéro.
|
||||
La lecture des publications longues et des conversations utilise le
|
||||
conteneur défilant natif existant.
|
||||
|
||||
## Périmètre technique
|
||||
|
||||
La carte utilise exclusivement les relevés personnels de l'atlas existant,
|
||||
sans chargement ou génération de chunks. Elle respecte le déblocage de
|
||||
l'atlas et les restrictions de dimension. Un clic ouvre la carte détaillée.
|
||||
Le serveur solo transmet les relevés existants pendant la pause, par lots bornés,
|
||||
sans observer de nouveaux blocs ni avancer la simulation. La limite de fréquence
|
||||
accepte aussi le temps réel lorsque les ticks du monde sont arrêtés.
|
||||
La texture et le flux réseau sont libérés en quittant le menu ; les réponses
|
||||
tardives ne sont acceptées que par l'écran courant et son jeton.
|
||||
|
||||
Les publications utilisent les mêmes opérations paginées du contrat v1,
|
||||
en fichier comme en MariaDB. Aucune migration, table ou modification du site.
|
||||
L'actualisation des premières pages intervient lorsque leur défilement est en
|
||||
haut ; une liste étendue reste stable jusqu'à sa réouverture.
|
||||
Les textes d'interface sont disponibles en français et anglais.
|
||||
|
||||
La sélection d'un screenshot pour la Gazette reste l'étape suivante.
|
||||
Les visages des demandes du tableau restent différés ; ceux de la Gazette
|
||||
et des conversations sont présents. Les futures entrées
|
||||
économiques ne sont pas des boutons factices.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Environnement : macOS Apple M1, Java Temurin 25, Minecraft 26.3, Fabric Loader
|
||||
0.19.5, Fabric API 0.160.5+26.3, Vulkan/MoltenVK 1.4.2. Base de développement
|
||||
MariaDB 12.3.3, distincte des données personnelles et du site public.
|
||||
|
||||
- Essais natifs Vulkan réussis dans les deux modes : fichier et MariaDB.
|
||||
Français et anglais, GUI 2/3/4 à 1280 × 960 pixels (jusqu'à 320 × 240 unités
|
||||
d'interface), contrôles contenus dans l'écran.
|
||||
- Six vrais boutons Sanctuary ; espacement uniforme de 4 pixels en haut et
|
||||
en bas ; avatars d’articles alignés à gauche des titres.
|
||||
Défilements indépendants à la molette, ajout de la treizième publication par
|
||||
pagination sans retour en haut, lecture jusqu'au bas des longs articles et
|
||||
demandes, réponse publiée et relue.
|
||||
- Options et Options du monde natives, reprise du jeu, ouverture de la carte
|
||||
détaillée puis retour au menu. Carte personnelle reçue en plusieurs lots
|
||||
pendant la pause ; l'horloge de simulation du monde reste immobile.
|
||||
- Logs : `build/pause155-client-file.log`, `build/pause155-client-database.log`.
|
||||
Captures : `build/pause155-{file,database}-screenshots/`, aperçu
|
||||
`build/pause155-preview.png`. Les articles et le terrain de ces captures
|
||||
proviennent exclusivement d'un monde de test plat et de données fictives.
|
||||
|
||||
`./gradlew check build` a terminé ses 252 GameTests : 229 réussites et les
|
||||
23 échecs déjà recensés dans beta.154 (mêmes identifiants, aucun ajout ni
|
||||
retrait). La commande échoue donc ; ce n'est pas un contrôle global vert.
|
||||
Log : `build/pause155-check-build.log`, comparaison :
|
||||
`build/pause155-server-failures.json` / `build/community-server-failures.json`.
|
||||
`./gradlew check build assemblePack -x :sanctuary:runGameTest` réussit :
|
||||
compilation, contrôles hors GameTests et assemblage. Les derniers ajustements
|
||||
visuels de disposition sont ensuite vérifiés par les scénarios client natifs
|
||||
et un nouvel assemblage des artefacts, sans réexécuter les 252 GameTests.
|
||||
Log : `build/pause155-build-pack.log`.
|
||||
Aucun déploiement personnel ni publication du canal packwiz.
|
||||
|
||||
Commande des essais natifs :
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
|
||||
-PsanctuaryQuickTests=true -PsanctuaryPause155ClientTests=true \
|
||||
-PsanctuaryClientGraphicsBackend=vulkan
|
||||
```
|
||||
|
||||
Le scénario MariaDB utilise les variables de développement documentées dans
|
||||
[COMMUNITY-154](community-beta154.md). Le schéma reste le contrat v1.
|
||||
La sélection de screenshots et la validation Windows restent ouvertes.
|
||||
|
||||
|
||||
## Artefacts locaux vérifiés
|
||||
|
||||
Assemblage final réussi avec `./gradlew build assemblePack -x :sanctuary:check`,
|
||||
après réussite des essais natifs de la disposition finale dans les deux modes.
|
||||
Export packwiz réussi. ZIP, version Fabric/Minecraft, JAR unique dans le MRpack,
|
||||
pilote MariaDB embarqué et absence des classes de test vérifiés. Les 29 fichiers
|
||||
shader sont identiques octet pour octet à beta.151. Les anciens JAR et le pack
|
||||
beta.154 sont conservés, leurs empreintes vérifiées.
|
||||
|
||||
Reçu : `build/pause155-artifacts.json`. Logs : `build/pause155-final-assemble.log`
|
||||
et `build/pause155-export.log`.
|
||||
|
||||
- `mods/sanctuary/build/libs/sanctuary-beta.155.jar` — SHA-256 `e8ba34ddd2726eb48fcea2b56a6f5a2e0262354553378c5bc25ca6f5061d32dc`.
|
||||
- `build/Sanctuary-beta.155.mrpack` — SHA-256 `11be47c07edbb8180b4d6911a1eff7656ba2ce4168d35b96096519a2491e899f`.
|
||||
@@ -0,0 +1,33 @@
|
||||
# PAUSE-156 — reprendre la partie sous la colonne droite
|
||||
|
||||
Branche `codex/pause-resume-right-beta156`, livraison locale `beta.156`.
|
||||
Suite de [PAUSE-155](pause-redesign-beta155.md).
|
||||
|
||||
Le bouton natif Reprendre la partie se trouve en bas à droite, sous la colonne
|
||||
Gazette / tableau, avec le même alignement et la même largeur. Il conserve son
|
||||
action native et les libellés français / anglais de Minecraft.
|
||||
|
||||
La modification concerne uniquement sa géométrie. Les options restent en bas
|
||||
à gauche ; les publications, le stockage, la carte et les shaders conservent
|
||||
leur fonctionnement de beta.155. Aucune migration ni installation personnelle.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Le scénario client Vulkan existant passe en français et anglais, GUI 2/3/4 à
|
||||
1280 × 960 : alignement du bouton avec la colonne droite, largeur identique,
|
||||
absence de chevauchement avec le tableau, reprise effective du jeu. Les
|
||||
interactions communautaires du scénario passent également en mode fichier.
|
||||
Log : `build/pause156-client.log`, aperçu `build/pause156-preview.png`.
|
||||
|
||||
`./gradlew check build assemblePack -x :sanctuary:runGameTest` réussit.
|
||||
Export packwiz, intégrité ZIP, versions du mod / pack et contenu du JAR
|
||||
embarqué vérifiés. Les shaders sont inchangés et les artefacts beta.155
|
||||
conservés avec leurs empreintes. Logs : `build/pause156-build-pack.log`,
|
||||
`build/pause156-export.log`, reçu `build/pause156-artifacts.json`.
|
||||
La suite serveur complète de beta.155 compte 229 réussites / 252, avec les
|
||||
23 échecs historiques. Elle n’est pas relancée pour ce déplacement de bouton.
|
||||
|
||||
## Artefacts locaux
|
||||
|
||||
- `mods/sanctuary/build/libs/sanctuary-beta.156.jar` — SHA-256 `c325a757cff6063fe3702e755a57a3abc13ad86e071762354a3c449504d51c6f`.
|
||||
- `build/Sanctuary-beta.156.mrpack` — SHA-256 `0537273dbfecb071d481f3f22982fbbb9a8e8a75581e38128ded87926ee699d0`.
|
||||
@@ -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`.
|
||||
@@ -0,0 +1,68 @@
|
||||
# RENDER-148 — PBR 50 % et reflet solaire diffus
|
||||
|
||||
Branche `codex/pbr-soft-sun-beta148`, socle beta.147 `96a36d9`.
|
||||
Minecraft 26.3, Java 25, dépendances et sauvegardes inchangés.
|
||||
|
||||
## Résultat
|
||||
|
||||
- PBR à 50 % par défaut, SSR toujours à 20 % et portée commune jusqu’à 256.
|
||||
- Les configurations anciennes contenant exactement PBR 80 % passent à 50 %.
|
||||
Un marqueur de migration conserve ensuite tout choix explicite, y compris
|
||||
un nouveau choix 80 %. Les autres anciennes intensités restent conservées.
|
||||
- Le reflet solaire de l’eau avait un renforcement ×20 et une compression
|
||||
exponentielle qui effaçait les détails dans une large zone blanche.
|
||||
Le renforcement passe à ×6 et le lobe est plus diffus (rugosité minimale
|
||||
de 0,42 au lieu de 0,34 pour le lobe céleste de l’eau).
|
||||
- Pour le soleil sur l’eau, fusion par texel entre éclaircissement vers le
|
||||
blanc et lumière douce fondée sur la couleur native. La pondération emploie
|
||||
une compression rationnelle, sans atteindre brutalement un plateau blanc.
|
||||
La texture et ses animations restent natives ; aucun bruit ajouté.
|
||||
- La lune conserve son intensité atténuée, sans renforcement solaire.
|
||||
Les lampes et matières solides conservent leur fusion spéculaire précédente.
|
||||
Le bloom reste réglable et suit la lumière réellement ajoutée.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Scènes plates jetables, Apple M1, Vulkan et OpenGL. Le test solaire contrôle
|
||||
une lumière largement visible et limite les pixels quasi blancs à moins de
|
||||
0,5 % de l’image, au lieu d’imposer une grande tache blanche.
|
||||
|
||||
Premier contrôle Vulkan à PBR 50 % / SSR 20 % : 149 908 pixels éclaircis,
|
||||
zéro pixel quasi blanc sur 854 × 480. Captures vues du dessus et à soleil bas
|
||||
inspectées : la texture reste visible dans la traînée lumineuse.
|
||||
Preuves : `build/evidence/beta148-preview/`.
|
||||
Rendu de la traînée solaire approuvé par le créateur après inspection de la capture.
|
||||
|
||||
- Suites SSR complètes Vulkan et OpenGL réussies : F5, intersections, portées,
|
||||
eau/verre/métal, OFF/zéro, lune, bloom, rechargement et redimensionnement.
|
||||
Journaux : `build/beta148-ssr-vulkan.log`, `build/beta148-ssr-opengl.log`.
|
||||
Sous OpenGL, 146 680 pixels éclaircis et zéro pixel quasi blanc dans la scène
|
||||
solaire. La lune reste visible sans saturation (35 865 pixels modifiés).
|
||||
Preuves : `build/evidence/beta148-ssr-vulkan/` et
|
||||
`build/evidence/beta148-ssr-opengl/`.
|
||||
|
||||
- Suites PBR/préférences Vulkan et OpenGL réussies : défaut 50 %, migration
|
||||
de l’ancien 80 %, conservation d’un nouveau choix explicite 80 %, valeurs
|
||||
personnalisées, FR/EN, proximité, OFF/zéro, intensités et rechargement.
|
||||
Journaux : `build/beta148-pbr-vulkan.log`, `build/beta148-pbr-opengl.log`.
|
||||
- `./gradlew check build` dans l’instantané isolé : 252 tests serveur,
|
||||
229 réussis, 23 échecs. Exactement les mêmes identifiants que beta.147,
|
||||
aucun nouvel échec. Journal : `build/beta148-check-build-isolated.log` ;
|
||||
comparaison : `build/beta148-server-failures.json`. Le contrôle global
|
||||
demeure donc en échec.
|
||||
- Assemblage final réussi : `./gradlew build assemblePack -x :sanctuary:check`,
|
||||
sans répéter les contrôles purs et serveur déjà exécutés dans l’instantané.
|
||||
Journal : `build/beta148-build-pack.log`. L’exclusion ne constitue pas
|
||||
une réussite du contrôle complet.
|
||||
|
||||
## Artefact
|
||||
|
||||
[Sanctuary-beta.148.mrpack](../build/Sanctuary-beta.148.mrpack), également copié
|
||||
à côté des autres versions dans `sanctuary-beta/build/`. Archive ZIP vérifiée,
|
||||
un seul JAR Sanctuary beta.148 identique au JAR construit, shaders finaux
|
||||
identiques aux sources, textes FR/EN 50 % et dépendances exactes vérifiés.
|
||||
Taille : 10 520 050 octets. SHA-256 :
|
||||
`597cc2a96eabc3cf0ebf8ac59b4f94cf7cabf0c773e1dce13c10736ca6fcba38`.
|
||||
Reçu : `build/beta148-artifacts.json`.
|
||||
Aucune publication ni installation Prism effectuée.
|
||||
|
||||
+221
@@ -0,0 +1,221 @@
|
||||
# PBR-SSR — eau, verre et reflets pixélisés
|
||||
|
||||
Livraison locale **beta.145**, branche `codex/pbr-ssr`. Socle PBR beta.142 /
|
||||
suivi beta.143 et copie figée des sources du chantier d’eau beta.144. Minecraft 26.3, API graphique native.
|
||||
|
||||
## Contrat
|
||||
|
||||
Les reflets colorent les texels de matière sur une grille 16 × 16 liée aux UV,
|
||||
avec les normales procédurales existantes. Eau et verre sont diélectriques.
|
||||
L’eau reprend les faces du tessellateur natif (niveaux, pentes, écoulements,
|
||||
waterlogging), sans modification de géométrie ni simulation de vagues.
|
||||
Le verre réfléchit aussi sur les zones sans pigment de sa texture. Les pixels
|
||||
pigmentés conservent leur relief même avec un alpha faible ; l’alpha nul
|
||||
utilise la normale géométrique.
|
||||
|
||||
Les images des normales d’eau sont préparées au chargement du matériau ; leur
|
||||
séquence suit l’horloge effective des animations de l’atlas, avec interpolation.
|
||||
Seules les petites tuiles de normales changent sur le GPU, pas le maillage.
|
||||
Les normales des textures haute résolution sont rééchantillonnées à 16 × 16.
|
||||
Au-delà de 1 024 images par sprite, un avertissement signale le repli vers
|
||||
une normale fixe pour borner la mémoire.
|
||||
|
||||
L’option FR/EN « Reflets du paysage — expérimental » est OFF initialement,
|
||||
intensité 50 %, bornée entre 0 et 100. Elle exige le PBR actif. L’eau naturelle
|
||||
reste indépendante pour ses couleurs, sa transparence et sa brume.
|
||||
|
||||
## Pipeline et limites
|
||||
|
||||
Couleur et profondeur du paysage sont copiées après les opaques et avant les
|
||||
transparences, dans les deux chemins natifs. Le chemin classique ferme la passe
|
||||
opaque avant la copie, puis reprend la transparence native dans une nouvelle
|
||||
passe. Aucune copie d’attachement Vulkan encore ouvert.
|
||||
|
||||
Une profondeur propre sélectionne le récepteur PBR le plus proche devant le
|
||||
paysage opaque. Une vitre devant l’eau reçoit seule le SSR ; les couches
|
||||
suivantes conservent leur rendu natif. La profondeur du fond n’est pas utilisée
|
||||
comme profondeur de surface de l’eau.
|
||||
|
||||
8 à 32 pas déterministes adaptés à la longueur écran, puis quatre raffinements maximum, récepteurs jusqu’à
|
||||
32 blocs et rayons jusqu’à 128 blocs. Pas de bruit aléatoire, historique,
|
||||
réflexion récursive, réfraction nouvelle, lecture GPU vers CPU ou ajout de
|
||||
lumière de gameplay. Les objets hors écran ou masqués ne sont pas retrouvés. Les détails fins entre
|
||||
deux pas peuvent être manqués ; une interpolation finale affine le point
|
||||
d’intersection sans augmenter le nombre de recherches.
|
||||
Un échec revient progressivement au rendu existant ; les transparences et les
|
||||
effets Sanctuary postérieurs à la capture ne figurent pas dans le reflet.
|
||||
|
||||
Les reflets sont composés après le SSGI, avant contours, bloom et rayons.
|
||||
La main et le HUD restent en dehors du traitement. Le PBR conserve son
|
||||
périmètre Overworld hors immersion et s’efface avec les moteurs externes.
|
||||
|
||||
Budget de maillages : 24 Mio de solides, 8 Mio supplémentaires de transparences.
|
||||
Deux profondeurs plein écran servent aux surfaces PBR ; la couleur de paysage
|
||||
SSR est allouée seulement à ON/intensité positive. La copie de composition PBR
|
||||
existante est réutilisée. Aucun buffer de normales plein écran.
|
||||
La grille visuelle ne signifie pas qu’un seul fragment GPU est exécuté par texel.
|
||||
|
||||
## Intégration
|
||||
|
||||
Le commit `38f4d6c` apporte le SSR et les récepteurs transparents. Le commit
|
||||
`0cfd51e` importe une copie contrôlée des sources beta.144 depuis le chantier
|
||||
`codex/water-beta144`, fondé sur `580b241`. Les empreintes des fichiers ont été
|
||||
comparées avant et après la copie ; le reçu local est dans
|
||||
`build/water144-snapshot/manifest.json`. Cette intégration ne prétend pas que
|
||||
beta.144 a été publiée ou que son benchmark isolé est terminé. Le code de
|
||||
production de cet instantané est identique à celui du commit
|
||||
`7eef945` ensuite créé sur la branche d’eau. Les essais ci-dessous portent
|
||||
sur les sources réunies dans cette branche.
|
||||
|
||||
Points d’entrée : `PbrMaterials` et `PbrAnimation` pour les normales animées,
|
||||
`PbrFluidMesh` pour le tessellateur natif, `PbrTerrain` pour les deux budgets,
|
||||
`ReflectionSceneMixin` pour la capture opaque, `PixelReflections` et
|
||||
`pixel_reflections.fsh` pour la sélection et le rayon. Les horloges de sprites
|
||||
sont référencées faiblement des deux côtés afin de ne pas retenir les anciens
|
||||
atlas après rechargement.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Machine : Apple M1, Minecraft 26.3, Java 25, macOS. Mondes plats de test neufs,
|
||||
graine 122 ; aucune sauvegarde personnelle utilisée. Les captures conservent
|
||||
le préfixe historique `sanctuary-beta142-shader` du banc de test.
|
||||
|
||||
- Client OpenGL 4.1 Metal 90.5, transparence classique : suite SSR réussie avec les sources d’eau
|
||||
intégrées (`build/ssr145-opengl-final.log`).
|
||||
- Client Vulkan/MoltenVK : suite SSR avec transparence améliorée réussie
|
||||
(`build/ssr145-vulkan-final.log`).
|
||||
- Régression du PBR historique et nouvelles assertions de profils, normales
|
||||
transparentes, persistance, bornes et traductions SSR : réussies
|
||||
(`build/ssr145-pbr-regression-retry.log`). Une première tentative interrompue
|
||||
s’était arrêtée sur un timeout de sémaphore Vulkan ; elle reste un échec
|
||||
consigné dans `build/ssr145-pbr-regression.log`.
|
||||
- Suite fonctionnelle eau beta.144 : réussie, y compris profils, fond modifié,
|
||||
brume, océan, rechargement, préférences, cascades et pack de matériaux
|
||||
(`build/ssr145-water-functional.log`). Son long benchmark autonome a été
|
||||
explicitement sauté ; le coût SSR est mesuré séparément ci-dessous.
|
||||
- Le relief rouge puis bleu produit deux contributions de couleur distinctes
|
||||
dans le bassin ; caché par un mur opaque, ce changement ne traverse pas le mur.
|
||||
- Eau animée sans nouvelle analyse de texture, verre transparent devant l’eau,
|
||||
métal, déplacement minime de caméra, rotation, OFF/zéro, immersion,
|
||||
rechargement de ressources et redimensionnement sont exercés par assertions.
|
||||
L’horloge d’eau reste active après rechargement ; les profils sable et étang
|
||||
restent corrects avec PBR et SSR simultanément actifs.
|
||||
- Des captures couvrent également les effets combinés, la lune, les angles
|
||||
rasants, la translation de caméra, la sortie du paysage hors écran, la pierre
|
||||
rugueuse OFF/ON, des vitres colorées superposées, un écoulement, une cascade
|
||||
et une dalle gorgée d’eau. Elles ne remplacent pas une inspection
|
||||
artistique de tous les angles et de tous les packs.
|
||||
|
||||
Captures locales OpenGL : [OFF](../build/ssr-evidence/beta145-opengl-final/0000_sanctuary-beta142-shader-ssr-water-off.png),
|
||||
[ON](../build/ssr-evidence/beta145-opengl-final/0001_sanctuary-beta142-shader-ssr-water-on.png).
|
||||
Le dossier `build/ssr-evidence/beta145-opengl-final/` contient les autres captures.
|
||||
Les mêmes cas Vulkan sont conservés dans
|
||||
`build/ssr-evidence/beta145-vulkan-final/`. Les comparaisons eau, métal et
|
||||
verre empilé ont été inspectées visuellement. Ces pièces sont ignorées par Git.
|
||||
|
||||
## Coût mesuré
|
||||
|
||||
1920 × 1080, PBR à 80 %, SSR à 100 % pour accentuer le résultat, VSync OFF,
|
||||
limite de FPS et réduction AFK désactivées. Chaque scène chauffe 30 ticks par
|
||||
état avant 80 ticks de mesure, soit 324 à 483 images pour OpenGL et 317 à 477 pour Vulkan. Ordre OFF puis
|
||||
ON ; il s’agit d’un diagnostic local court, pas d’une campagne statistique.
|
||||
Le temps de frame mesure `Minecraft.renderFrame`, présentation comprise.
|
||||
|
||||
| OpenGL | Médiane OFF / ON (ms) | p95 OFF / ON (ms) | Écart médian |
|
||||
| --- | --- | --- | --- |
|
||||
| Petit étang | 7,57 / 9,42 | 10,35 / 12,50 | +1,85 ms |
|
||||
| Eau dominante | 7,76 / 11,67 | 10,67 / 13,96 | +3,91 ms |
|
||||
| Construction vitrée | 7,85 / 10,12 | 10,67 / 12,65 | +2,27 ms |
|
||||
|
||||
| Vulkan | Médiane OFF / ON (ms) | p95 OFF / ON (ms) | Écart médian |
|
||||
| --- | --- | --- | --- |
|
||||
| Petit étang | 7,97 / 10,36 | 8,97 / 11,49 | +2,40 ms |
|
||||
| Eau dominante | 10,10 / 12,23 | 10,97 / 12,97 | +2,13 ms |
|
||||
| Construction vitrée | 8,52 / 11,25 | 9,49 / 12,20 | +2,72 ms |
|
||||
|
||||
La mesure GPU Vulkan autour de `PixelReflections.render` donne, à ON,
|
||||
**2,07 / 2,18 ms** (médiane / p95) pour l’étang, **2,56 / 2,63 ms** pour
|
||||
l’eau dominante et **2,09 / 2,21 ms** pour le verre. À OFF, la sonde mesure
|
||||
0,03 ms environ. Ce périmètre couvre la copie de composition et le dessin
|
||||
SSR ; il exclut la capture opaque antérieure et la profondeur partagée PBR.
|
||||
La sonde utilise les valeurs rendues par la requête native, pas une mesure
|
||||
GPU du jeu complet.
|
||||
|
||||
La requête GPU native ne fournit pas de mesure exploitable sur ce pilote
|
||||
OpenGL ; ce champ est indisponible, pas nul. Les premiers essais contaminés
|
||||
par la réduction AFK à 30 FPS sont exclus des résultats retenus.
|
||||
**L’objectif de surcoût médian inférieur à 2 ms n’est pas respecté dans l’ensemble des
|
||||
scènes.** L’option reste expérimentale et désactivée par défaut.
|
||||
|
||||
À 1080p : profondeurs partagées par le PBR = 15,82 Mio ; copie couleur SSR
|
||||
supplémentaire = 7,91 Mio, libérée à OFF/zéro. Total de ces cibles à ON :
|
||||
23,73 Mio. Les maillages observés occupent environ 0,51–0,55 Mio de solides et
|
||||
1,09–1,21 Mio de transparences, en dessous des plafonds 24 / 8 Mio vérifiés
|
||||
par assertion. Ces compteurs n’incluent ni la mémoire du moteur, ni la copie
|
||||
PBR existante, ni les atlas de matériaux de 6,25 Mio chacun ; ce ne sont pas
|
||||
des mesures de mémoire totale du processus.
|
||||
|
||||
## Construction et contrôles généraux
|
||||
|
||||
`./gradlew check build` a été exécuté jusqu’au bout : **229 GameTests serveur
|
||||
réussis sur 252, 23 échecs**, en 8 min 45 s pour la commande complète.
|
||||
Les échecs concernent les tombes/familiers, le catalogue, deux invariants de
|
||||
génération, les placements de blocs/progression/construction et les menus
|
||||
natifs. Les tests concernés et le code de gameplay serveur ne sont pas
|
||||
modifiés par ce lot. Aucun contre-test de toute la branche de départ n’est
|
||||
revendiqué. Le journal complet est `build/ssr145-check-build.log` ; le relevé
|
||||
exhaustif est `build/ssr145-server-failures.json`.
|
||||
|
||||
L’assemblage est repris avec `./gradlew check build assemblePack assembleTestPack
|
||||
-x :sanctuary:runGameTest`, en excluant uniquement cette tâche déjà exécutée.
|
||||
Cette commande réussit en 2 min 56 s (126 tâches) ; vérification du pack,
|
||||
compilation, contrôles hors GameTests serveur et assemblages passent. Les
|
||||
exports MRpack sont produits depuis leurs dossiers packwiz de staging.
|
||||
L’intégrité ZIP, les métadonnées, les traductions et mixins SSR ainsi que
|
||||
l’identité du JAR embarqué dans les deux packs sont contrôlées.
|
||||
Cette exclusion ne transforme pas le contrôle complet en succès.
|
||||
|
||||
## Reproduire les essais client
|
||||
|
||||
Base de chaque commande :
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runClientGameTest \
|
||||
-PsanctuaryClientTests=true -PsanctuaryQuickTests=true \
|
||||
-PsanctuaryPbr134ClientTests=true -PsanctuaryClientNoVsync=true
|
||||
```
|
||||
|
||||
- SSR Vulkan : ajouter `-PsanctuarySsrClientTests=true
|
||||
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryClientImprovedTransparency=true`.
|
||||
- SSR OpenGL : ajouter `-PsanctuarySsrClientTests=true
|
||||
-PsanctuaryClientGraphicsBackend=opengl -PsanctuaryClientImprovedTransparency=false`.
|
||||
- Régression PBR : ajouter uniquement `-PsanctuaryClientGraphicsBackend=vulkan`.
|
||||
- Profils et rendu de l’eau : ajouter `-PsanctuaryWater144ClientTests=true
|
||||
-PsanctuaryWater144FunctionalOnly=true -PsanctuaryClientGraphicsBackend=vulkan`.
|
||||
|
||||
Le mode fonctionnel saute uniquement le long benchmark autonome beta.144.
|
||||
Les sondes de temps de frame/GPU et lectures d’images appartiennent au mod
|
||||
de test ; elles ne sont pas embarquées dans le JAR de production.
|
||||
|
||||
## Artefacts locaux
|
||||
|
||||
Les archives finales sont rafraîchies depuis le JAR testé, puis revérifiées
|
||||
(`build/ssr145-artifacts.json`). Aucun tag ni canal n’est publié.
|
||||
|
||||
| Artefact | Octets | SHA-256 |
|
||||
| --- | ---: | --- |
|
||||
| [sanctuary-beta.145.jar](../mods/sanctuary/build/libs/sanctuary-beta.145.jar) | 11369411 | `55341e2f5c8dcc352190fe96d15e86cd1ee6bb4ea2908441b70491d48f86f608` |
|
||||
| [Sanctuary-beta.145.mrpack](../build/Sanctuary-beta.145.mrpack) | 10508045 | `8bebe6227cbfe629c714a7c7547640ab5ccfbefaaf211484a93c042cfb42b5c5` |
|
||||
| [Sanctuary-Test-beta.145.mrpack](../build/Sanctuary-Test-beta.145.mrpack) | 10526966 | `54402ff8414ed1ae0d12b3b97af70c547c44dde2f3aa183712ae3769f99efbe7` |
|
||||
| [Sanctuary-Template-beta.145.zip](../build/Sanctuary-Template-beta.145.zip) | 4358316 | `226d92362a5ce922b8757b36f30c312081aca5cf73a299b82a221a12f384a95b` |
|
||||
|
||||
## Limites de validation
|
||||
|
||||
Le banc utilise un relief construit reproductible, pas une montagne générée
|
||||
représentative de tous les biomes. La validation artistique sur terrain généré,
|
||||
les textures personnalisées avec ordre/durées/interpolation atypiques et la
|
||||
stabilité sur un long trajet restent à compléter. Les surfaces peuvent perdre les petits détails entre
|
||||
les pas SSR et les zones hors écran ne sont jamais reconstruites. Windows,
|
||||
autres GPU et moteurs graphiques externes ne sont pas validés ici.
|
||||
|
||||
Aucune publication de canal ni synchronisation Prism dans cette livraison.
|
||||
@@ -0,0 +1,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.
|
||||
+11
-1
@@ -106,4 +106,14 @@ habituel `sanctuary-beta/build/` :
|
||||
- JAR Sanctuary :
|
||||
`ca3e2ff4e0717a3a54c1d57478d43aade39d001b71c4eec745d76f010f7def54`.
|
||||
|
||||
Publication et synchronisation à consigner après leur exécution.
|
||||
[Release beta.129](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.129)
|
||||
publiée depuis `35103a7ea8635dc4ed26293f00e86d99aaacfca8`. Canal packwiz
|
||||
`2eaa0130a4cd475c598beefdab40f48fc8681ab7`. Deux synchronisations isolées puis deux synchronisations
|
||||
dans l'instance **Sanctuary Beta** ont réussi. Les **923 fichiers personnels**
|
||||
suivis, dont les réglages de shader, sont inchangés. Aucun monde personnel ouvert.
|
||||
Sauvegarde préalable des fichiers gérés : `sanctuary-backups/before-beta.129/`.
|
||||
Le JAR installé a le hash vérifié ci-dessus. Reçus locaux :
|
||||
`build/shader129-artifact.json`, `build/shader129-isolated.json` et
|
||||
`build/shader129-prism.json`. Le checkout initial avec ses modifications en
|
||||
cours a été préservé ; seule la copie des archives versionnées rejoint son
|
||||
répertoire `build/`.
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
# SHADER-130 — bloom et précision commune
|
||||
|
||||
Demandes du 17 septembre 2026 : bloom réglable du soleil et des sources
|
||||
lumineuses ; inclusions émissives du fer, cuivre, redstone, lapis, diamant,
|
||||
émeraude, rubis, saphir, quartz et or ; réglage commun « Précision » pour
|
||||
les ombres et le highlight. Branche `codex/bloom-beta130`, socle beta.129,
|
||||
Minecraft 26.3 / Java 25.
|
||||
|
||||
## Résultat
|
||||
|
||||
Dans **Options → Shaders → Sanctuary · Vanilla Light**, le bloom possède
|
||||
un interrupteur, une intensité et une diffusion réglables de 0 à 100.
|
||||
Les minerais ont leur propre interrupteur et intensité. Le bloom reste
|
||||
optionnel, désactivé initialement, avec une intensité préparée à 35, une
|
||||
diffusion à 50 et une émission des minerais à 50. Une intensité globale
|
||||
nulle arrête les passes et libère les ressources de cet effet.
|
||||
|
||||
Les nouveaux réglages par défaut sont **Pixel Shadows activé**, distance
|
||||
des ombres **128 blocs**, highlight **32 blocs** et **Précision 16 pixels
|
||||
par bloc**. La précision unique propose 8, 16 et 32 et contrôle simultanément
|
||||
ombres et arêtes. La clé JSON historique `shadowPixels` est conservée :
|
||||
les préférences déjà personnalisées ne sont pas écrasées.
|
||||
|
||||
Les vingt textures de minerais sont analysées depuis les ressources actives.
|
||||
La palette de pierre, de deepslate ou de netherrack est comparée aux pixels
|
||||
colorés des inclusions, avec un traitement du quartz et de l'or du Nether.
|
||||
Les reflets clairs accolés aux inclusions sont conservés. Le masque est
|
||||
calculé une fois par texture et conservé en cache jusqu'au rechargement des
|
||||
ressources. Une texture optionnelle `<nom>_e.png` peut préciser ce masque
|
||||
pour une texture candidate ; sa luminance maximale multipliée par l'alpha
|
||||
remplace la détection automatique. Les couleurs affichées viennent toujours
|
||||
de l'atlas Minecraft actif, y compris son animation.
|
||||
|
||||
Les sources sont les faces visibles des modèles de blocs lumineux, la lave,
|
||||
le soleil et la lune. Les lampes éteintes ne produisent pas de halo. Le
|
||||
niveau lumineux natif pondère les sources ordinaires ; l'émission des minerais
|
||||
est purement graphique. La profondeur du terrain élimine les inclusions
|
||||
cachées et masque le soleil derrière les constructions. Le cœur émissif
|
||||
conserve ses pixels nets, tandis qu'un halo séparé est diffusé à résolution
|
||||
réduite puis composé sur l'image.
|
||||
|
||||
Aucun niveau de lumière serveur, spawn, règle de jeu, format de sauvegarde,
|
||||
identifiant ou paramètre de génération ne change. Aucun monde personnel
|
||||
n'est ouvert pour les essais.
|
||||
|
||||
## Coût et limites
|
||||
|
||||
La géométrie suit uniquement les sections natives visibles et déjà chargées.
|
||||
Leur palette permet d'écarter rapidement les sections sans source ; le cache
|
||||
invalide les sections touchées lors des modifications. Construction répartie
|
||||
sur les images, au plus quatre sections démarrées et un budget souple de
|
||||
3 ms, cache GPU plafonné à 32 Mio et 16 384 faces par section. Une section
|
||||
qui dépasse les limites est omise. Aucun chargement de chunk forcé.
|
||||
|
||||
Le masque GPU est plafonné à 256 textures, chacune échantillonnée au plus en
|
||||
64 × 64 ; les textures natives 16 × 16 conservent leurs texels. Les masques
|
||||
animés utilisent leur première image, tandis que la couleur reste animée.
|
||||
La carte d'émission est à la résolution de l'écran ; la diffusion séparable
|
||||
utilise deux cibles flottantes à 1/8 de largeur et de hauteur, sans retour
|
||||
GPU vers CPU en production. Les allocations sont libérées à l'arrêt, au
|
||||
rechargement et à la déconnexion, et recréées au changement de taille.
|
||||
|
||||
Cette première version ne reconstruit pas les sources hors écran, les
|
||||
particules, les entités lumineuses ni les modèles dessinés exclusivement par
|
||||
un renderer de block entity. Les packs très différents peuvent demander un
|
||||
masque explicite. Les silhouettes des liquides sont approximées puis
|
||||
masquées par la profondeur native. Aucune promesse de FPS ni validation
|
||||
Windows/Vulkan ; les essais graphiques portent sur macOS / Apple M1 / OpenGL.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Le test client ciblé réussit en **1 min 29 s**, dans un nouveau monde plat,
|
||||
graine 122. Il analyse les vingt textures, refuse les pixels gris de leur
|
||||
roche, mesure le gain nocturne et les variations réelles d'intensité/diffusion.
|
||||
Couvrir les minerais de pierre supprime toute source émissive. Retirer les
|
||||
blocs rétablit l'émission ; le rechargement des ressources fait de même.
|
||||
Torche, glowstone, lampe alimentée, lave et soleil sont vérifiés, ainsi que
|
||||
l'arrêt de la lampe et l'occultation du soleil par de la pierre. Les valeurs
|
||||
par défaut et la conservation d'anciens choix personnalisés passent.
|
||||
|
||||
Log ciblé : `build/shader130-client-bloom.log` ; masques inspectables dans
|
||||
`mods/sanctuary/build/run/clientGameTest/bloom130-ore-masks.png` et son CSV.
|
||||
La suite shader complète réussit en **3 min 16 s** : casters hors écran,
|
||||
retrait/repose, trois précisions, cinq portées, lune, zénith, feuillage,
|
||||
realtime, caméra et rechargement restent vérifiés. Les arêtes CTM restent
|
||||
raccordées entre matériaux ; 1 295 échantillons du contour sont éclaircis
|
||||
à précision 8, 596 à 16 et 120 à 32. Les captures françaises/anglaises et
|
||||
le rendu des minerais/du soleil sont inspectés.
|
||||
|
||||
Commandes natives : `./gradlew :sanctuary:runClientGameTest
|
||||
-PsanctuaryClientTests=true -PsanctuaryPixelShadows122ClientTests=true
|
||||
-PsanctuaryClientNoVsync=true -PsanctuaryQuickTests=true`. Le test ciblé utilise
|
||||
`-PsanctuaryBloom130ClientTests=true` à la place de la propriété PixelShadows.
|
||||
Log complet : `build/shader130-client-full.log` ; captures sous
|
||||
`mods/sanctuary/build/run/clientGameTest/screenshots/`.
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
réussit en **2 min 24 s**, 126 tâches. Le serveur GameTest dédié reste exclu
|
||||
conformément au refus antérieur de son EULA. La comparaison au binaire beta.129
|
||||
trouve **24 entrées de production** modifiées, toutes limitées au shader et à
|
||||
ses interfaces. Gameplay et textures des gemmes inchangés ; aucun test,
|
||||
monde ou modèle GLB embarqué. Les sources archivées correspondent aux sources
|
||||
Java, les packs normal/Test embarquent le même JAR et le template contient
|
||||
exactement les ressources prévues.
|
||||
|
||||
Archives vérifiées dans `build/` et recopiées dans le dossier habituel
|
||||
`sanctuary-beta/build/` :
|
||||
|
||||
|
||||
- `Sanctuary-beta.130.mrpack` : 10386021 octets, SHA-256
|
||||
`7a3fcd4e496fa136eb3e0896fa7b856ae91771123186fa8df13c8fb1e0cf3e9a`.
|
||||
|
||||
- `Sanctuary-Test-beta.130.mrpack` : 10404944 octets, SHA-256
|
||||
`8de094fcbc58a300cc24d64c8d1b7931fdb8db354c75defa6bf2a2aa4a123a23`.
|
||||
|
||||
JAR : `1ac4d1b1339361fb665f03ca3bd359163db5037ffd90e684c5a461759f6076e7`.
|
||||
|
||||
[Release beta.130](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.130)
|
||||
publiée depuis `534dfec4f88169a200702b50d1ba87d85c107bee`. Canal packwiz
|
||||
`68e06353bf880af04c1c72d4e3a159f11f94b619`. Les téléchargements publics sont vérifiés par SHA-256.
|
||||
Deux synchronisations isolées, puis deux dans la même instance **Sanctuary
|
||||
Beta**, réussissent. Un seul JAR Sanctuary beta.130 est actif. Les
|
||||
**923 fichiers personnels suivis** conservent leurs hashes, dont les mondes,
|
||||
packs et préférences de shaders. Sauvegarde préalable dans
|
||||
`sanctuary-backups/before-beta.130/` de l'instance. Aucun monde personnel ouvert.
|
||||
|
||||
Reçus locaux ignorés : `build/shader130-artifact.json`,
|
||||
`build/shader130-isolated.json`, `build/shader130-prism.json` et
|
||||
`build/shader130-publication.log`. Le checkout original conserve ses
|
||||
modifications préexistantes ; seuls les trois artefacts versionnés vérifiés
|
||||
sont recopiés dans son dossier `build/`.
|
||||
@@ -0,0 +1,91 @@
|
||||
# SHADER-131 — émissifs indépendants et réglages initiaux
|
||||
|
||||
Demande : activer par défaut bloom, minerais émissifs et highlight ; garder
|
||||
les émissifs avec halo coupé ou intensité du bloom à zéro, sans bouton ajouté ;
|
||||
ajouter 64 et 128 pixels/bloc en options. Précision initiale 16, distances
|
||||
highlight 32 et ombres 128 conservées. Préférences personnalisées préservées.
|
||||
Branche `codex/emissive-beta131`, socle beta.130, Minecraft 26.3 / Java 25.
|
||||
|
||||
Le futur effet demandé est une légère teinte diffusée depuis les blocs
|
||||
voisins, pas des reflets reconnaissables. Étude seulement pour ce ticket ;
|
||||
aucun SSR ou éclairage indirect expérimental activé.
|
||||
|
||||
## Comportement et validation
|
||||
|
||||
Le curseur à zéro et l'interrupteur Bloom coupent uniquement le halo.
|
||||
Le réglage existant des minerais contrôle leurs inclusions nettes séparément.
|
||||
Sans halo, les cibles de diffusion GPU sont libérées ; l'émission garde
|
||||
seulement les ressources nécessaires. Tout désactiver libère l'ensemble.
|
||||
La grille commune propose 8, 16, 32, 64 et 128, sans augmenter le plafond
|
||||
4096 de la carte d'ombres. Libellés FR/EN et essais clients natifs sur les
|
||||
inclusions, l'absence de halo, les transitions et les nouvelles précisions.
|
||||
Aucun changement de lumière serveur, de génération ou de sauvegardes.
|
||||
|
||||
Le cœur émissif et le halo utilisent maintenant deux chemins indépendants.
|
||||
Les textures et leur analyse restent celles de beta.130. Avec le halo coupé,
|
||||
la carte d'émission est effacée avant les faces, le cœur net est composé,
|
||||
et les passes de ciel, réduction, diffusion et application du halo sont
|
||||
omises. Le retour au bloom recrée ses cibles. Aucun bouton ajouté, aides
|
||||
FR/EN adaptées et choix explicitement désactivés toujours conservés.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Suite client native réussie en **3 min 46 s**, monde plat neuf de graine 122,
|
||||
macOS / Apple M1 / OpenGL. Cinq précisions sauvegardées et chargées, bornes
|
||||
et seuils de normalisation, carte d'ombres plafonnée et arêtes CTM rendues
|
||||
à 64/128. Le gain mesuré des traits diminue de 0,02778 à précision 32 à
|
||||
0,02094 à 64, puis 0,01013 à 128 dans la même scène.
|
||||
|
||||
Les inclusions restent nettes sans halo : le rendu Bloom OFF correspond
|
||||
au rendu intensité zéro. Aucun gain mesuré hors du masque source, ressources
|
||||
de diffusion absentes ; couper aussi les émissifs libère l'ensemble et
|
||||
restitue l'image initiale. Réactiver le bloom restitue son image précédente.
|
||||
Captures OFF/zéro inspectées. Valeurs initiales ON vérifiées ; un ancien
|
||||
choix explicitement OFF reste OFF. Les essais beta.130 (vingt masques,
|
||||
occultation, sources, rechargement), lune, feuillage et realtime passent.
|
||||
|
||||
Le premier essai rencontrait une assertion historique attendant le highlight
|
||||
OFF lorsqu'une clé était absente. Elle attend désormais la nouvelle valeur
|
||||
ON ; les cas de préférences explicitement désactivées restent testés.
|
||||
|
||||
Commande : `./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true
|
||||
-PsanctuaryPixelShadows122ClientTests=true -PsanctuaryClientNoVsync=true
|
||||
-PsanctuaryQuickTests=true`. Log : `build/shader131-client-full.log` ; captures
|
||||
sous `mods/sanctuary/build/run/clientGameTest/screenshots/`.
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
réussit en **2 min 30 s**, 126 tâches. Les archives normal/Test, leurs sources
|
||||
et le template sont vérifiés. Six entrées de production changent : moteur
|
||||
Bloom, préférences, interface et libellés. Gameplay, shaders GLSL et textures
|
||||
restent identiques à beta.130. Aucun résultat Windows/Vulkan
|
||||
revendiqué ; le serveur GameTest dédié reste exclu conformément au refus
|
||||
antérieur de son EULA.
|
||||
|
||||
## Piste suivante — diffusion des couleurs
|
||||
|
||||
Choix confirmé : légère teinte des surfaces voisines. Une estimation SSGI
|
||||
utilise la couleur et la profondeur visibles pour des rebonds diffus ; voir
|
||||
la [documentation primaire Unity](https://docs.unity3d.com/Packages/com.unity.render-pipelines.high-definition@17.0/manual/Override-Screen-Space-GI.html).
|
||||
Proposition à expérimenter : faible intensité, courte portée autour des
|
||||
surfaces, calcul réduit, rejet par profondeur pour ne pas teinter au travers
|
||||
des murs. Vérifier les bords de l'écran, les blocs hors champ, la stabilité
|
||||
en mouvement et les textures avant de fixer une qualité. Cette piste n'est
|
||||
pas implémentée dans beta.131.
|
||||
|
||||
## Livraison
|
||||
|
||||
- `Sanctuary-beta.131.mrpack` : 10386312 octets ; SHA-256 `e02067e424bf4f54512d5cfb1b5cde0fe232aa6bdadc0d38981cd15817a6387a`.
|
||||
- `Sanctuary-Test-beta.131.mrpack` : 10405238 octets ; SHA-256 `298387cce5a5b7cdc09b3a9d24859b674a736b25abcbafa7995b813757641265`.
|
||||
|
||||
JAR : `87210da23f7781efccdbdbf5d4e2d3c1ff1857a7f326027b93d6a301c7dfdb0d`.
|
||||
|
||||
Archives vérifiées recopiées dans `sanctuary-beta/build/`. [Release beta.131](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.131)
|
||||
publiée depuis `078d3e6833daa7de7f6c6d0e1a6c17d5dbefe075`. Canal packwiz
|
||||
`efb18c274d95d988a9566b8c52751469f002cdda`. Téléchargements publics vérifiés par hash.
|
||||
Deux synchronisations isolées, puis deux dans la même instance **Sanctuary Beta**
|
||||
réussissent. Un seul JAR Sanctuary beta.131 ; les **923 fichiers personnels
|
||||
suivis** conservent leurs hashes, dont les préférences de shaders et les mondes.
|
||||
Sauvegarde préalable `sanctuary-backups/before-beta.131/` ; aucun monde personnel
|
||||
ouvert. Reçus ignorés : `build/shader131-artifact.json`,
|
||||
`build/shader131-isolated.json`, `build/shader131-prism.json` et
|
||||
`build/shader131-publication.log`. Le checkout original conserve ses changements.
|
||||
@@ -0,0 +1,106 @@
|
||||
# SHADER-132 — diffusion locale SSGI expérimentale
|
||||
|
||||
Demande : légère teinte diffusée des surfaces voisines, sans reflets miroir
|
||||
ni shader d'eau. Branche `codex/ssgi-beta132`, socle beta.131, Minecraft 26.3 /
|
||||
Java 25. Aucun changement de génération, sauvegarde ou lumière serveur.
|
||||
|
||||
## Résultat
|
||||
|
||||
**Options → Shaders → SSGI · couleurs diffusées** active un rebond diffus
|
||||
local. L'effet expérimental est initialement OFF, avec une intensité préparée
|
||||
à **35 %** ; le curseur propose 0–100. À zéro ou OFF, ses cibles et uniformes
|
||||
GPU sont libérés. Les autres effets et leurs préférences sont conservés.
|
||||
|
||||
La couleur d'une surface visible peut teinter les surfaces proches qui lui
|
||||
font face. Le calcul porte à **2,5 blocs** du récepteur et s'estompe entre
|
||||
24 et 32 blocs de la caméra. Il utilise une copie de la couleur native après
|
||||
les ombres, avec la profondeur et des normales reconstruites. Le premier
|
||||
obstacle arrête chaque rayon, même si son épaisseur empêche de retenir une
|
||||
intersection. Les plans coplanaires ne s'éclairent pas eux-mêmes.
|
||||
|
||||
Douze directions hémisphériques fixes, au maximum dix étapes par rayon,
|
||||
sont évaluées à demi-résolution en largeur et hauteur. La reconstruction
|
||||
plein écran est guidée par la profondeur pour conserver les silhouettes et
|
||||
les détails des textures. Le cœur des émissifs, le highlight et le bloom
|
||||
sont composés ensuite : pas de halo réinjecté dans le rebond ni de boucle
|
||||
de rétroaction d'une image sur l'autre. Aucune lecture GPU vers CPU ni
|
||||
recherche de chunks en production. Pas de bruit temporel ou d'historique.
|
||||
|
||||
Deux cibles, RGBA8 plein écran et RGBA16 flottant à demi-résolution, soit
|
||||
environ six octets par pixel de l'écran, plus 176 octets d'uniformes par
|
||||
segment du tampon natif. Redimensionnement, arrêt, déconnexion et
|
||||
rechargement libèrent/recréent les ressources possédées. Ce budget n'est
|
||||
pas une garantie de FPS ; le SSGI coûte davantage qu'un simple réglage de
|
||||
colorimétrie.
|
||||
|
||||
## Limites de cette version
|
||||
|
||||
L'estimation utilise seulement la couleur et la profondeur visibles. Les
|
||||
blocs hors champ ou cachés n'apportent pas leur couleur. Le résultat peut
|
||||
changer lorsqu'ils entrent ou sortent de l'image ; une atténuation près
|
||||
des bords limite la coupure. Les surfaces transparentes absentes de la
|
||||
profondeur ne sont pas reconstruites. Petits objets et intersections plus
|
||||
minces que l'échantillonnage peuvent manquer. Il s'agit d'une approximation
|
||||
diffuse locale, pas d'une simulation complète d'éclairage global ni de
|
||||
reflets spéculaires. Aucune modification de l'eau et aucune source hors
|
||||
champ déduite ou générée.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Test ciblé natif réussi en **1 min** sur macOS / Apple M1 / OpenGL, nouveau
|
||||
monde plat de graine 122. À intensité 90, les échantillons du sol blanc
|
||||
montrent un gain rouge cumulé de 8 968 contre 20 bleu devant le mur rouge,
|
||||
puis 9 168 bleu contre 23 rouge devant le mur bleu. Le sol seul est inchangé.
|
||||
OFF/zéro restituent l'image de base et libèrent les cibles ; intensité 20
|
||||
réduit le gain. Changer la couleur du mur complètement occulté laisse
|
||||
l'image inchangée. Rechargement, caméra fixe, mouvement infinitésimal et
|
||||
retour à la caméra d'origine passent, sans accumulation de couleur.
|
||||
|
||||
Deux ajustements de fixture ont précédé ce résultat : inclure dans les
|
||||
mesures le blanc légèrement bleuté par l'éclairage natif, et formater les
|
||||
petits angles de `/tp` en décimal car la commande refuse la notation
|
||||
scientifique. Aucun comportement du jeu modifié pour ces corrections.
|
||||
|
||||
Log ciblé : `build/shader132-client-ssgi.log`. La suite complète réussit en
|
||||
**4 min 18 s** et vérifie aussi une fenêtre 961 × 541, le retour à sa taille
|
||||
initiale, le SSGI avec ombres/highlight/bloom/émissifs actifs, ainsi que
|
||||
l'arrêt global du shader. Les captures françaises et anglaises passent.
|
||||
Les contrôles précédents des cinq précisions, ombres, lune, feuillage, CTM,
|
||||
bloom et émissifs restent réussis. Log : `build/shader132-client-full.log`.
|
||||
|
||||
Commandes : `./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true
|
||||
-PsanctuaryPixelShadows122ClientTests=true -PsanctuaryClientNoVsync=true
|
||||
-PsanctuaryQuickTests=true`. Pour le test ciblé, remplacer la propriété
|
||||
PixelShadows par `-PsanctuarySsgi132ClientTests=true`.
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
réussit en **2 min 25 s**, 126 tâches. Les packs normal/Test contiennent le
|
||||
même JAR ; sources archivées et template de ressources correspondent aux
|
||||
sources. Comparaison avec beta.131 : **11 entrées de production** modifiées,
|
||||
limitées au SSGI, à ses options et à ses points d'appel. Gameplay et textures
|
||||
des gemmes inchangés, aucun monde ni test embarqué. Aucun résultat
|
||||
Windows/Vulkan revendiqué.
|
||||
Le serveur GameTest dédié reste exclu conformément au refus antérieur de
|
||||
son EULA. Les captures panoramiques et les milieux de brume sous l'eau/lave
|
||||
ou neige poudreuse suspendent cette passe, comme les autres effets de
|
||||
profondeur Sanctuary.
|
||||
|
||||
## Livraison
|
||||
|
||||
- `Sanctuary-beta.132.mrpack` : 10394026 octets, SHA-256 `a7c0823e00f36925919645e32e7792242ec1d01015b41bee5b4f6285b9fc533d`.
|
||||
- `Sanctuary-Test-beta.132.mrpack` : 10412949 octets, SHA-256 `9bc30a7d6d5834f9323f15f0c39d3f49d200e1a97d739266ab373c9e1ba4a8fd`.
|
||||
|
||||
JAR : `83eb2d091a7acd85eaa8b3b6a00a40ea483f9eeb2a30c033bdf68b4d717c5142`.
|
||||
|
||||
Archives vérifiées recopiées dans `sanctuary-beta/build/`. [Release beta.132](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.132)
|
||||
publiée depuis `7e21781a9a08c669c7d6b65653bd9d408887689c`. Canal packwiz
|
||||
`9edaaedc0205c525ecce9de66b5ca7827c56c28f` ; téléchargements publics vérifiés par SHA-256.
|
||||
Deux synchronisations isolées, puis deux dans la même instance **Sanctuary Beta**,
|
||||
réussissent. Un seul JAR beta.132 actif ; les **923 fichiers personnels suivis**,
|
||||
dont sauvegardes et préférences de shaders, gardent leurs hashes. Sauvegarde
|
||||
préalable `sanctuary-backups/before-beta.132/` ; aucun monde personnel ouvert.
|
||||
Le checkout original conserve ses changements préexistants.
|
||||
|
||||
Reçus locaux ignorés : `build/shader132-artifact.json`,
|
||||
`build/shader132-isolated.json`, `build/shader132-prism.json`,
|
||||
`build/shader132-publication.log`.
|
||||
@@ -0,0 +1,106 @@
|
||||
# SHADER-133 — SSGI visible au réglage courant
|
||||
|
||||
Demande : le rebond diffus de beta.132 est trop discret pour être distingué.
|
||||
Branche `codex/ssgi-strength-beta133`, socle beta.132, Minecraft 26.3 / Java 25.
|
||||
|
||||
## Changement
|
||||
|
||||
La collecte conserve douze directions, dix étapes, une demi-résolution et
|
||||
un rayon de 2,5 blocs. L'atténuation spatiale devient linéaire plutôt que
|
||||
quadratique. Le mélange est reconstruit en lumière approximativement linéaire,
|
||||
puis réencodé une seule fois. Un gain artistique plus fort rend la couleur
|
||||
lisible et une courbe progressive évite de couper brutalement les canaux
|
||||
à blanc. Les surfaces sans rebond restent intactes. La force demeure réglable
|
||||
de 0 à 100, avec le même réglage initial de 35 ; aucune préférence enregistrée
|
||||
n'est écrasée. SSGI reste optionnel, initialement désactivé.
|
||||
|
||||
Aucun rayon, cible GPU ou texture de matériau supplémentaire. La composition
|
||||
ajoute une exponentielle et une racine carrée par canal ; le coût exact en FPS
|
||||
reste à mesurer sur la machine du joueur. Les normales de géométrie orientent
|
||||
déjà le rebond. Pas de PBR ni de normales procédurales de textures ajoutés.
|
||||
|
||||
## Piste matériaux
|
||||
|
||||
Pour une étape distincte, des normales de faible amplitude calculées au
|
||||
chargement des textures, mises en cache, et une rugosité par famille de matériaux
|
||||
permettraient d'étudier du relief et des reflets orientés. Une simple conversion
|
||||
luminosité → hauteur ne connaît pas le relief réel : un motif peint peut devenir
|
||||
une bosse artificielle. Commencer sur pierre, briques et bois avec une amplitude
|
||||
réglable serait préférable à une génération aveugle pour tous les blocs.
|
||||
La passe actuelle ne possède ni atlas de normales, ni identifiants de matériaux,
|
||||
ni albédo non éclairé ; des images seules ne suffiraient pas à les exploiter.
|
||||
|
||||
## Limites conservées
|
||||
|
||||
Approximation diffuse à partir de la couleur déjà éclairée, pas un transport
|
||||
physique complet. Seules les surfaces visibles contribuent, avec variations
|
||||
possibles aux entrées/sorties d'écran. Premier obstacle bloquant, pas d'historique
|
||||
ni de bruit temporel ; aucune modification d'eau, monde ou lumière serveur.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Suite client native complète réussie en **4 min 29 s**, sur macOS / Apple M1 /
|
||||
OpenGL, dans un monde de test plat (graine 122). Les comparaisons ON/OFF à 35 %
|
||||
mesurent, sur les pixels neutres du sol :
|
||||
|
||||
| Mur | Pixels avec gain ≥ 8/255 | Pixels avec gain ≥ 16/255 | Gain cumulé du canal dominant |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| Rouge | 16 487 | 7 311 | 305 221 |
|
||||
| Bleu | 14 954 | 7 328 | 309 991 |
|
||||
|
||||
À 90 %, les gains dominants atteignent 654 683 et 655 867, contre 8 968 et
|
||||
9 168 au même réglage dans la fixture beta.132. Ce rapport concerne uniquement
|
||||
cette scène et ces pixels, pas une promesse de gain uniforme dans tous les mondes.
|
||||
Les captures confirment une bande colorée au pied du mur. Un premier réglage
|
||||
intermédiaire a été rejeté car aucun pixel du sol ne dépassait 8/255 à 35 % ;
|
||||
un second a passé les contrôles ciblés, puis la force a encore été augmentée
|
||||
après examen visuel. Les valeurs finales sont celles de la suite complète.
|
||||
|
||||
Le test impose maintenant plus de 5 000 pixels à 8/255 et plus de 500 à 16/255
|
||||
au réglage 35, pour les deux couleurs. Intensité, OFF/zéro, absence de rebond
|
||||
coplanaire, occultation d'un mur changeant de couleur, caméra fixe, déplacement
|
||||
infinitésimal, retour de caméra, redimensionnement et rechargement passent.
|
||||
Ombres, lune, feuillage, CTM, cinq précisions, bloom, émissifs indépendants et
|
||||
interfaces FR/EN restent validés, y compris avec le SSGI actif.
|
||||
|
||||
Commande : `./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true
|
||||
-PsanctuaryPixelShadows122ClientTests=true -PsanctuaryClientNoVsync=true
|
||||
-PsanctuaryQuickTests=true`. Log : `build/shader133-client-full.log`.
|
||||
Le nom de la propriété ciblée `sanctuarySsgi132ClientTests` est conservé.
|
||||
|
||||
Comparateur local autonome : `build/SSGI-beta.133-comparaison.html`, captures
|
||||
natives OFF/35/90 des murs rouge/bleu, sans retouche. Pas de validation
|
||||
Windows/Vulkan ni de mesure de coût FPS revendiquée.
|
||||
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
réussit en **2 min 21 s**, 126 tâches. Le serveur GameTest dédié reste exclu
|
||||
conformément au refus EULA antérieur. Log : `build/shader133-build.log`.
|
||||
Les archives normal/Test contiennent le même JAR ; sources archivées et template
|
||||
de ressources correspondent aux sources. Comparaison avec beta.132 : **5 entrées
|
||||
de production** changées (classe du compositeur, deux shaders, libellés FR/EN).
|
||||
Gameplay, textures, sauvegardes et dépendances inchangés.
|
||||
|
||||
## Artefacts
|
||||
|
||||
- `Sanctuary-beta.133.mrpack` : 10394282 octets, SHA-256 `0c90a070da7f969706abeeec1bdfb0830bb52244c20fccbca4fd876f3fbe0564`.
|
||||
- `Sanctuary-Test-beta.133.mrpack` : 10413204 octets, SHA-256 `e01f0355bf2cce18156fe94a069e142bedc498624ba0b6e2729ae0f23ea73e80`.
|
||||
|
||||
JAR : `a01cae4a98606007b533ca09ec9f072f19a1db2e9b8de466f4ad075d5c1fc185`.
|
||||
|
||||
|
||||
## Livraison
|
||||
|
||||
[Release beta.133](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.133)
|
||||
publiée depuis `216d3f75b576bd2ed531653012f9cc6c4801d87a`. Canal packwiz
|
||||
`9ceb5a5353c9d1b8bb7834a20e818fd65096ff98` ; téléchargements publics vérifiés par SHA-256.
|
||||
Deux synchronisations isolées puis deux synchronisations de la même instance
|
||||
**Sanctuary Beta** réussissent. Un seul JAR beta.133 actif ; les **923 fichiers
|
||||
personnels suivis**, dont sauvegardes et préférences, gardent leurs hashes.
|
||||
Sauvegarde préalable `sanctuary-backups/before-beta.133/`, aucun monde personnel
|
||||
ouvert. Le checkout original conserve ses changements préexistants.
|
||||
|
||||
Les archives normal/Test/template et le comparateur natif sont également dans
|
||||
`sanctuary-beta/build/`. Reçus locaux ignorés : `build/shader133-artifact.json`,
|
||||
`build/shader133-isolated.json`, `build/shader133-prism.json` et
|
||||
`build/shader133-publication.log`.
|
||||
@@ -0,0 +1,109 @@
|
||||
# SHADER-134 — PBR procédural optionnel et herbe SSGI
|
||||
|
||||
Demande : ajouter en option des normales calculées à partir des textures actives,
|
||||
avec rugosité des matériaux et reflets orientés. Retour : le SSGI renforcé
|
||||
éclaire artificiellement l'intérieur de l'herbe ; vérifier et corriger ce cas.
|
||||
Branche `codex/pbr-beta134`, socle beta.133, Minecraft 26.3 / Java 25.
|
||||
|
||||
## Contrat
|
||||
|
||||
PBR initialement OFF, intensité 50 réglable 0–100. Cartes de normales à partir
|
||||
des différences de luminance, amplitude limitée ; rugosité/métallicité par
|
||||
famille de blocs, repli rugueux pour les types inconnus. Analyse des ressources
|
||||
actives conservée en cache jusqu'à désactivation ou rechargement ; pas de fichiers
|
||||
écrits dans le pack personnel, pas de GPU readback en production.
|
||||
|
||||
Portée 32 blocs, sections visibles construites progressivement. Budget de maillage
|
||||
24 Mio, atlas de 4 096 matériaux en 16 × 16 avec bordures (1 280 × 1 280 RGBA8,
|
||||
6,25 Mio), copie RGBA8 de l'image courante et uniformes. Les textures de plus haute
|
||||
résolution sont échantillonnées à 16 × 16 ; première image des textures animées.
|
||||
La couleur et l'animation restent celles de l'atlas natif. Pas de parallax ni
|
||||
de géométrie ajoutée aux blocs ; les motifs clairs peuvent donner un faux relief.
|
||||
|
||||
La passe utilise une réponse spéculaire GGX (normales, rugosité, métallicité,
|
||||
Fresnel) et module l'éclairage diffus déjà rendu. Elle ne remplace pas tout le
|
||||
pipeline Minecraft par un moteur PBR physique. Soleil/lune, exposition au ciel et
|
||||
carte d'ombres active limitent les reflets ; pas de reflets miroir, d'environnement
|
||||
SSR, ni de direction locale déduite pour chaque torche. Aucun rendu PBR d'eau,
|
||||
d'entités, d'items ou de modèles spéciaux hors maillage de terrain.
|
||||
|
||||
## SSGI
|
||||
|
||||
Les normales des faces presque axiales sont stabilisées sur leur axe. Deux passes
|
||||
spatiales filtrent la lumière indirecte à demi-résolution, avec rejet des voisins
|
||||
hors du plan du récepteur et interpolation de l'échantillonnage. Les textures de
|
||||
l'image restent nettes. Un tampon RGBA16F supplémentaire à demi-résolution porte
|
||||
le budget des cibles SSGI à environ 8 octets par pixel plein écran (contre 6).
|
||||
Pas d'historique temporel ni de rémanence ajoutés.
|
||||
|
||||
Un parcours de 12 blocs a reproduit des bandes sur un mur et un éclaircissement
|
||||
artificiel d'herbe à 35 et 100. Le filtrage seul ne suffisait pas : la collecte
|
||||
réinjectait aussi trop fortement la lumière ambiante neutre. Elle conserve
|
||||
maintenant la composante chromatique de la source, en soustrayant son canal
|
||||
linéaire minimum. L'éclairage ambiant neutre reste celui de Minecraft. Ce réglage
|
||||
vise le transfert de couleur voulu, pas une simulation physique complète du
|
||||
rebond blanc. Les sources hors champ/cachées restent absentes du SSGI ; le résultat
|
||||
peut encore varier lors d'un déplacement important. PBR et SSGI restent indépendants.
|
||||
|
||||
## Vérification
|
||||
|
||||
Test PBR ciblé réussi en 1 min 13 s : relief éclairé/ombré, intensités 20/50/100,
|
||||
soleil matin/soir, cache stable, préférences, OFF/zéro, mouvement infinitésimal,
|
||||
redimensionnement, rechargement et combinaison des effets. Le premier parcours
|
||||
SSGI est conservé comme reproduction ; résultats finaux ci-dessous.
|
||||
|
||||
|
||||
Le parcours corrigé a réussi en **2 min 47 s** avec 13 positions réparties sur
|
||||
12 blocs, à 35 et 100, chaque fois comparées à OFF. Un mob immobile, des brins
|
||||
d'herbe et un mur figurent dans la scène. Retour au point initial sans rémanence.
|
||||
Sur les captures de ce parcours, le maximum du gain vert moyen mesuré dans
|
||||
l'herbe passe sous 9,73 niveaux à 35 et 24,51 à 100. Le test impose ensuite
|
||||
respectivement des limites de 12 et 30 à chaque position. L'essai reproduisant
|
||||
le défaut atteignait notamment 23,68 et 49,20 à l'extrémité droite.
|
||||
Ces mesures décrivent cette scène, pas une garantie d'absence d'artefacts
|
||||
dans tous les mondes ni sur toutes les cartes graphiques.
|
||||
|
||||
|
||||
La suite client complète réussit en **7 min** sur macOS / Apple M1 / OpenGL.
|
||||
Elle inclut le parcours latéral et ses seuils de gain, les deux couleurs de rebond,
|
||||
le PBR, les ombres, le feuillage, les cinq précisions, le CTM, les émissifs, le
|
||||
bloom, les préférences et les écrans FR/EN. Log : `build/shader134-client-full.log`.
|
||||
Le contrôle PBR final réussit en **1 min 19 s** : placement/retrait d'un toit,
|
||||
invalidations de lumière/maillage et rendu combiné avec les ombres actives. Les
|
||||
reflets utilisent maintenant la grille des ombres pour leur test de visibilité. Aucun résultat Windows/Vulkan ni
|
||||
mesure de FPS revendiqués.
|
||||
|
||||
|
||||
## Construction et artefacts
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
réussit en **2 min 22 s**, 126 tâches. Le serveur GameTest dédié reste exclu
|
||||
conformément au refus EULA antérieur. Log : `build/shader134-build.log`.
|
||||
Packs normal/Test : même JAR Sanctuary. Sources archivées et template identiques
|
||||
aux sources. Comparaison à beta.133 : **23 entrées de production** changées,
|
||||
limitées au rendu, à ses options et points d'appel. Gameplay et gemmes inchangés.
|
||||
|
||||
- `Sanctuary-beta.134.mrpack` : 10421971 octets, SHA-256 `d5bc79acb1d56e7dcf83c976156fdb2fcde80c98e443bde8a6ab312d56887b7f`.
|
||||
- `Sanctuary-Test-beta.134.mrpack` : 10440898 octets, SHA-256 `04308ebb9adaf76b30a3cbf4fa32ab897e76842803fc8c8411cfa32952d1bdd5`.
|
||||
|
||||
JAR : `a6e69622a5e6629f70dd19b7494ad3b00916bc02169b73635f0e2d563929b7fa`.
|
||||
|
||||
Comparateur local : `build/Shader-beta.134-comparaison.html`. Il contient les
|
||||
captures natives PBR OFF/50/100 et le parcours SSGI de 12 blocs OFF/35/100.
|
||||
Le parcours est une succession de positions enregistrées, pas une mesure des FPS.
|
||||
|
||||
|
||||
## Livraison
|
||||
|
||||
[Release beta.134](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.134)
|
||||
publiée depuis `fc4a0cb68ed2d89163c8e74773976df5bab4f3f1`. Canal packwiz
|
||||
`35ec0f6e145002f6367944df5b75e2703958eaef` ; téléchargements publics vérifiés par SHA-256.
|
||||
Deux synchronisations isolées puis deux dans la même instance **Sanctuary Beta**
|
||||
réussissent. Un seul JAR beta.134 actif ; les **923 fichiers personnels suivis**,
|
||||
dont sauvegardes et préférences, gardent leurs hashes. Copie préalable dans
|
||||
`sanctuary-backups/before-beta.134/`, aucun monde personnel ouvert.
|
||||
|
||||
Packs normal/Test/template et comparateur natif vérifiés également dans
|
||||
`sanctuary-beta/build/`. Le checkout original garde ses changements préexistants.
|
||||
Reçus locaux ignorés : `build/shader134-artifact.json`, `build/shader134-isolated.json`,
|
||||
`build/shader134-prism.json`, `build/shader134-publication.log`.
|
||||
@@ -0,0 +1,116 @@
|
||||
# SHADER-135 — SSGI en contre-plongée et près des surfaces
|
||||
|
||||
Demande : supprimer les déchirures horizontales sous une surface, notamment
|
||||
près de la lave, et atténuer proprement le rebond quand la caméra s'approche
|
||||
d'une source ou d'un mur. Branche `codex/shader-closeup-beta135`, socle beta.134.
|
||||
Minecraft 26.3 / Java 25 ; aucune modification de monde ou de gameplay.
|
||||
|
||||
## Diagnostic et correction
|
||||
|
||||
Le calcul des normales comparait l'aire du pixel en unités du monde à un seuil
|
||||
fixe. À courte distance ou à haute résolution, cette aire devient naturellement
|
||||
très petite : le rejet pouvait couper une surface continue. Les deux directions
|
||||
sont maintenant normalisées avant leur produit vectoriel. Les texels de bord
|
||||
sont bornés et les voisins sans profondeur ne créent plus de fausse surface.
|
||||
|
||||
La composition utilise le centre exact du texel de profondeur. Le rebond
|
||||
s'atténue entre 0,4 et 0,06 bloc de distance au **plan** receveur, et lorsque la
|
||||
vue devient presque tangente. Cette distance évite une découpe circulaire sur
|
||||
un mur plat. L'image native reste nette ; aucune texture du décor n'est floutée.
|
||||
|
||||
Les sondes sortant du volume de projection sont rejetées. Leur contribution
|
||||
s'atténue près du plan de caméra et de la limite d'épaisseur d'un impact, au lieu
|
||||
d'une contribution pleine jusqu'à cette limite. Trois ressources GLSL modifiées ;
|
||||
aucune passe, cible GPU, lecture CPU, histoire temporelle ou option ajoutée.
|
||||
Les 12 rayons × 10 pas et les deux passes de filtrage spatial restent inchangés.
|
||||
|
||||
## Réglages initiaux
|
||||
|
||||
Demande ajoutée pendant la livraison : **bloom 20 %**, **PBR ON** et **SSGI ON**.
|
||||
Les intensités PBR 50 et SSGI 35 restent inchangées. Les champs absents du fichier
|
||||
de préférences prennent ces nouvelles valeurs ; les choix explicites du joueur
|
||||
restent conservés. Aides françaises et anglaises mises à jour. Vérification
|
||||
native supplémentaire des préférences vierges, partielles et personnalisées.
|
||||
|
||||
## Reproduction native
|
||||
|
||||
Deux scènes isolées : stone au-dessus de la lave, puis coin à 0,5 bloc d'une
|
||||
paroi rouge. Caméra en contre-plongée, sept distances au plafond de 2 à 0,04 bloc,
|
||||
comparaisons OFF/35/100 avant/après. Dans la première scène, à 0,1 bloc et
|
||||
intensité 100, le saut maximal de gain rouge moyen entre lignes de pixels neutres
|
||||
passe de **14,88 à 0,052** niveaux sur 255. Dans le coin, à 0,4 bloc et intensité
|
||||
35, le rebond auparavant rejeté revient (gain moyen de **6,01** niveaux), puis
|
||||
s'atténue à l'approche du contact. Ces chiffres concernent ces scènes uniquement.
|
||||
|
||||
Les deux essais ciblés corrigés réussissent en 1 min 18 s et 1 min 17 s sur
|
||||
Apple M1 / OpenGL. Images avant/après conservées dans `build/closeup-*` ;
|
||||
comparateur autonome `build/Shader-beta.135-comparaison.html`.
|
||||
|
||||
## Régression et livraison
|
||||
|
||||
Les contrôles natifs de la suite générale réussissent jusqu'au parcours SSGI :
|
||||
ombres, lune, feuillage, temps réel, CTM, émissifs/bloom, couleurs de rebond,
|
||||
occlusion, réglages, rechargement et déplacement latéral de 12 blocs à 35/100.
|
||||
Log : `build/shader135-client-main.log`. La suite n'est pas présentée comme
|
||||
entièrement réussie : elle s'est arrêtée sur une attente incorrecte du nouveau
|
||||
test, qui demandait du rebond alors que sa source était sortie du champ.
|
||||
|
||||
Le scénario corrigé distingue la haute résolution avec source visible de la
|
||||
vue très relevée sans source visible. Le bassin possède un fond pour isoler la
|
||||
lave des essais suivants ; les particules du scénario précédent sont effacées
|
||||
avant la comparaison PBR statique. Une première tentative avait aussi rencontré
|
||||
le refus natif de remettre l'horloge à sa valeur actuelle ; la préparation de
|
||||
l'heure est désormais répétable. Ces changements concernent uniquement les tests.
|
||||
|
||||
Le contrôle final ciblé **réussit en 2 min 52 s** : les deux scènes, 35/100,
|
||||
sept distances, 1280 × 720 avec source visible, source hors champ, mouvement
|
||||
minuscule, rendu combiné, puis contrôles PBR complets (cache, intensités,
|
||||
matériaux, soleil, toit, OFF/zéro, redimensionnement et rechargement).
|
||||
Log : `build/shader135-client-final.log` ; marqueurs `SSGI135_CLOSEUP_PASS` et
|
||||
`PBR134_PASS`. Les seuils de proximité et PBR sont conservés.
|
||||
Le contrôle supplémentaire des nouveaux défauts et du PBR **réussit en
|
||||
1 min 16 s** : préférences vierges, champs absents, valeurs personnelles conservées,
|
||||
puis rendu PBR et combinaison des effets. Log : `build/shader135-defaults-client.log`.
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
réussit en **2 min 18 s**, 126 tâches. Le serveur GameTest dédié demeure exclu
|
||||
conformément au refus EULA antérieur. Les classes des scénarios clients compilent
|
||||
aussi après leur isolation explicite des effets activés par défaut.
|
||||
Log : `build/shader135-build.log` ; compilation : `build/shader135-test-compile.log`.
|
||||
|
||||
## Archives vérifiées
|
||||
|
||||
Comparaison à beta.134 : **6 entrées de production** modifiées, limitées aux
|
||||
trois ressources SSGI, aux valeurs initiales et aux aides FR/EN. Gameplay et
|
||||
textures des gemmes inchangés. Packs normal/Test : JAR Sanctuary identique ;
|
||||
archives de sources et template conformes aux sources. Reçu local ignoré :
|
||||
`build/shader135-artifact.json`.
|
||||
|
||||
|
||||
## Limites
|
||||
|
||||
Le SSGI utilise seulement les surfaces visibles : les sources hors champ restent
|
||||
absentes et un grand changement de vue peut modifier le rebond. L'atténuation
|
||||
proche est volontaire ; ce n'est pas une simulation volumétrique. Le brouillard
|
||||
natif de submersion continue de suspendre le SSGI, comme en beta.134. Aucun
|
||||
résultat Windows/Vulkan ni mesure de FPS revendiqués.
|
||||
|
||||
- `Sanctuary-beta.135.mrpack` : 10422391 octets ; SHA-256 `36ac278f9f5ecd2cefd69da16ea7d153ff44c5fc8e6bf0b3a3d6b71f5d767af6`.
|
||||
|
||||
- `Sanctuary-Test-beta.135.mrpack` : 10441314 octets ; SHA-256 `e096374f7ea0856b20392e5d277e94f6c3d411723d7ffc10ceccdd9f0bf5c4d4`.
|
||||
|
||||
JAR : `a68ccd04a92a962c46a76b26a80cb44a4faf7a13a562094dde51010ade594c10`.
|
||||
|
||||
## Livraison
|
||||
|
||||
[Release beta.135](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.135)
|
||||
publiée depuis `7fada570efb5ed579e6e4d7bc2b87079c7131b31`. Canal packwiz
|
||||
`5b0df577fcd036937c25be3a1f4580f9db3103a1` ; téléchargements publics vérifiés par SHA-256.
|
||||
Deux synchronisations isolées puis deux dans la même instance **Sanctuary Beta**
|
||||
réussissent. Un seul JAR beta.135 actif ; les **923 fichiers personnels suivis**,
|
||||
dont sauvegardes et préférences, gardent leurs hashes. Copie préalable dans
|
||||
`sanctuary-backups/before-beta.135/`, aucun monde personnel ouvert.
|
||||
|
||||
Packs normal/Test/template et comparateur natif également vérifiés dans
|
||||
`sanctuary-beta/build/`. Le checkout original garde ses changements préexistants.
|
||||
Reçus locaux ignorés : `build/shader135-artifact.json`, `build/shader135-isolated.json`,
|
||||
`build/shader135-prism.json`, `build/shader135-publication.log`.
|
||||
@@ -0,0 +1,85 @@
|
||||
# SHADER-136 — Lens flare rectangulaire doux
|
||||
|
||||
Demande : ajouter un lens flare avec des rectangles dégradés, exclusivement
|
||||
pour le soleil et la lune. Socle beta.135, branche `codex/lens-flare-beta136`.
|
||||
Minecraft 26.3 / Java 25. Aucun changement de gameplay ou de monde.
|
||||
|
||||
## Contrat
|
||||
|
||||
Effet activé initialement à **30 %**, intensité réglable 0–100, indépendant du halo de bloom.
|
||||
Rectangles translucides aux bords doux, suivant la position de l'astre à l'écran.
|
||||
La visibilité provient de l'image émissive céleste native, avant les blocs :
|
||||
les murs masquent l'effet, les sources de terrain ne le déclenchent pas.
|
||||
L'astre sortant du champ doit s'atténuer sans sauter sur le bord opposé.
|
||||
Pas d'historique, ni lecture GPU vers le CPU, ni texture générée sur disque.
|
||||
|
||||
## Rendu et budget
|
||||
|
||||
Trois rectangles à dégradé doux par astre : un halo horizontal près de sa
|
||||
position et deux reflets plus petits le long de l'axe passant par le centre de
|
||||
l'écran. Teinte solaire chaude, lunaire légèrement bleue. Intégration de la
|
||||
lumière réellement dessinée par le ciel natif ; phases de lune conservées.
|
||||
Atténuation aux bords de l'écran et à l'horizon, sans projection derrière la caméra.
|
||||
|
||||
Le masque céleste est mesuré avant l'ajout des émissifs de terrain. Réduction
|
||||
vers une texture 2 × 1 RGBA16F (512 échantillons au maximum au total), puis une
|
||||
passe de composition avec deux lectures de cette texture et des dégradés analytiques.
|
||||
La cible d'émission plein écran existante est réutilisée. Quand seul le flare
|
||||
fonctionne, aucun maillage d'émissif de terrain ni tampon de flou n'est construit.
|
||||
OFF et zéro libèrent la cible du flare ; les autres effets gardent leur autonomie.
|
||||
|
||||
## Vérification
|
||||
|
||||
Le scénario client natif réussit en **1 min 52 s** sur macOS / Apple M1 / OpenGL.
|
||||
Soleil OFF/30/80, lune et nouvelle lune, mur occultant, source hors champ,
|
||||
glowstone exclue, image statique, redimensionnement et rechargement des ressources.
|
||||
Le flare reste actif sans bloom ; OFF/zéro libèrent ses ressources. Préférences
|
||||
vierges, partielles, personnalisées, bornage et libellés FR/EN vérifiés.
|
||||
Les contrôles existants du bloom et des minerais émissifs passent également.
|
||||
Log : `build/shader136-client-final.log`, marqueurs `EMISSIVE131_PASS` et
|
||||
`LENS_FLARE136_PASS`. Les captures natives restent dans le dossier de tests ; comparatif autonome
|
||||
`build/Shader-beta.136-comparaison.html` (soleil OFF/30/80 et lune OFF/30).
|
||||
|
||||
Une première tentative s'est arrêtée sur l'attente d'un angle de caméra :
|
||||
Minecraft normalise 180° en −180°. Le scénario utilise maintenant un angle
|
||||
canonique ; ce changement concerne uniquement le test.
|
||||
|
||||
## Limites
|
||||
|
||||
Effet optique en espace écran : il s'atténue quand l'astre quitte le champ et
|
||||
à l'horizon. Le masque natif du ciel contrôle l'occultation ; la lune reste
|
||||
volontairement beaucoup plus discrète. Le brouillard de submersion suspend
|
||||
l'effet comme le bloom existant. Aucun essai Windows/Vulkan ni mesure de FPS
|
||||
revendiqués.
|
||||
|
||||
|
||||
## Construction et archives
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
réussit en **2 min 29 s**, 126 tâches. Le serveur GameTest dédié reste exclu
|
||||
conformément au refus EULA antérieur. Log : `build/shader136-build.log`.
|
||||
Comparaison à beta.135 : **9 entrées de production** modifiées, uniquement
|
||||
le rendu, ses préférences et ses libellés. Gameplay et textures des gemmes
|
||||
strictement conservés. JAR normal/Test identique, sources et template vérifiés.
|
||||
Reçu local : `build/shader136-artifact.json`.
|
||||
|
||||
- `Sanctuary-beta.136.mrpack` : 10424821 octets ; SHA-256 `808006bf2b0cb3fe63d3e16ae3a0003d27ad0b7e793a3a05f25b5d73ae88bc8d`.
|
||||
|
||||
- `Sanctuary-Test-beta.136.mrpack` : 10443744 octets ; SHA-256 `70f593ad16d1f4fd4305b276ce2727a63f87bcb84df45e55840c1427a314a1eb`.
|
||||
|
||||
JAR : `086afede22903f8988889f6085631451f65d6d55c2e4c81f249d2234698e68e5`.
|
||||
|
||||
## Livraison
|
||||
|
||||
[Release beta.136](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.136)
|
||||
publiée depuis `71296a9cfb0f6b1a6e6f0c48145f3cc432c8afd0`. Canal packwiz
|
||||
`dd5b545a7cfbb957f2c70e9dc17034b44caef98f` ; téléchargements publics vérifiés par SHA-256.
|
||||
Deux synchronisations isolées puis deux dans la même instance **Sanctuary Beta**
|
||||
réussissent. Un seul JAR beta.136 actif ; les **923 fichiers personnels suivis**,
|
||||
dont sauvegardes et préférences, gardent leurs hashes. Copie préalable dans
|
||||
`sanctuary-backups/before-beta.136/`, aucun monde personnel ouvert.
|
||||
|
||||
Packs normal/Test/template et comparatif natif également vérifiés dans
|
||||
`sanctuary-beta/build/`. Le checkout original garde ses changements préexistants.
|
||||
Reçus locaux ignorés : `build/shader136-artifact.json`, `build/shader136-isolated.json`,
|
||||
`build/shader136-prism.json`, `build/shader136-publication.log`.
|
||||
@@ -0,0 +1,96 @@
|
||||
# SHADER-137 — Rayons solaires volumétriques
|
||||
|
||||
Demande validée : faisceaux légers découpés par les blocs, carte d'ombres partagée,
|
||||
feuillage perméable, rendu natif préservé, option et intensité indépendantes du bloom.
|
||||
Socle beta.136, branche `codex/sunshafts-beta137`, Minecraft 26.3 / Java 25.
|
||||
Aucun changement de monde ni de gameplay.
|
||||
|
||||
## Contrat
|
||||
|
||||
Soleil uniquement ; activé initialement à 35 %, intensité 0–100.
|
||||
Intégration de 16 segments par pixel à un quart de chaque dimension.
|
||||
Reconstruction guidée par la profondeur, sans historique ni bruit animé.
|
||||
Réutilisation du masque de feuillage déjà stable dans les ombres.
|
||||
La carte solaire peut fonctionner sans afficher les ombres sur les surfaces.
|
||||
Portée bornée à 64 blocs et à la moitié de la portée des ombres.
|
||||
Atténuation aux limites de la carte, à l'horizon et avec la météo.
|
||||
|
||||
## Rendu et coût borné
|
||||
|
||||
Deux passes : intégration dans une cible RGBA16F de largeur et hauteur divisées
|
||||
par quatre, puis reconstruction guidée par la profondeur. Seize segments
|
||||
quadratiques privilégient les premiers mètres ; quatre lectures fixes de la
|
||||
carte d'ombres par segment adoucissent seulement la lumière dans l'air.
|
||||
La dernière portion du rayon rétrécit continûment à l'approche d'une surface.
|
||||
La lumière utilise une teinte solaire chaude mélangée au brouillard natif.
|
||||
|
||||
La carte d'ombres et ses maillages sont partagés, y compris les trous de
|
||||
transmission déterministes des feuilles. Aucune copie d'image plein écran,
|
||||
aucune duplication du terrain, aucune lecture GPU côté CPU ni accumulation
|
||||
entre images dans le rendu de production. OFF/zéro libèrent le volume ; la
|
||||
carte d'ombres reste seulement si les ombres de surface sont activées.
|
||||
Le soleil sous l'horizon et la submersion suspendent les rayons.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Scénario natif réussi en **1 min 41 s** sur macOS / Apple M1 / OpenGL : pièce
|
||||
entièrement fermée, toit ajouré OFF/35/100, feuilles puis ciel ouvert, déplacement
|
||||
de quatre blocs et retour, caméra proche d'un mur à 0,5 puis 0,125 bloc,
|
||||
soleil hors champ, nuit, OFF/zéro, carte d'ombres partagée, effet combiné,
|
||||
redimensionnement à 961 × 541 et rechargement des ressources. Préférences
|
||||
vierges/partielles/personnelles, bornage et libellés FR/EN également vérifiés.
|
||||
Log : `build/shader137-client-final.log`, marqueur `SUNSHAFT137_PASS`.
|
||||
Comparatif autonome : `build/Shader-beta.137-comparaison.html`.
|
||||
|
||||
La première tentative s'est arrêtée sur une comparaison de gain brut entre
|
||||
feuillage et ciel ouvert. La composition dispose de moins de marge sur un mur
|
||||
déjà clair : le test compare maintenant la contribution normalisée sur les mêmes
|
||||
surfaces, et les feuilles du scénario sont persistantes. Aucun seuil du rendu
|
||||
n'a été assoupli pour faire passer ce contrôle.
|
||||
|
||||
Les contrôles natifs existants du bloom, des vingt textures de minerais émissifs
|
||||
et du lens flare réussissent également en **1 min 32 s**, avec leurs seuils
|
||||
inchangés. Log : `build/shader137-regression.log`, marqueurs `EMISSIVE131_PASS`
|
||||
et `LENS_FLARE136_PASS`. Les autres effets sont explicitement isolés dans les
|
||||
scénarios qui nécessitent une image témoin sans rayons.
|
||||
|
||||
## Limites
|
||||
|
||||
Le volume utilise les formes présentes dans la carte d'ombres existante :
|
||||
il n'ajoute pas de projection d'ombre des entités ou des nuages. Le feuillage
|
||||
reste une approximation par transmission déterministe. Portée maximale de
|
||||
64 blocs, atténuée à son extrémité. Le faible nombre de segments et la résolution
|
||||
réduite peuvent lisser les ouvertures très fines. Aucun résultat Windows/Vulkan
|
||||
ni mesure de FPS revendiqués.
|
||||
|
||||
|
||||
## Construction et archives
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
réussit en **2 min 27 s**, 126 tâches. Le serveur GameTest dédié reste exclu
|
||||
conformément au refus EULA antérieur. Log : `build/shader137-build.log`.
|
||||
Comparaison à beta.136 : **11 entrées de production** modifiées, uniquement
|
||||
le rendu, ses préférences et ses libellés. Gameplay et textures des gemmes
|
||||
strictement conservés. JAR normal/Test identique, sources et template vérifiés.
|
||||
Reçu local : `build/shader137-artifact.json`.
|
||||
|
||||
- `Sanctuary-beta.137.mrpack` : 10431723 octets ; SHA-256 `6684d1dbe48cd3031b373a03d4380519fdbf5d841e1c7cb5e290dec63b5ba9f6`.
|
||||
|
||||
- `Sanctuary-Test-beta.137.mrpack` : 10450650 octets ; SHA-256 `32b0cee60152b6a5c8f31b248391c79a5fdf47b649de74902b066457ad4785ef`.
|
||||
|
||||
JAR : `f1fbc106b4b048753f9a669af37e9c12f92168af486d4072f514e86920c81faa`.
|
||||
|
||||
## Livraison
|
||||
|
||||
[Release beta.137](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.137)
|
||||
publiée depuis `2d7072b8a8febac53354e9d0063246ac1e843e37`. Canal packwiz
|
||||
`68cb39a5edbf5d34def02f670e2488ee852605b8` ; téléchargements publics vérifiés par SHA-256.
|
||||
Deux synchronisations isolées puis deux dans la même instance **Sanctuary Beta**
|
||||
réussissent. Un seul JAR beta.137 actif ; les **923 fichiers personnels suivis**,
|
||||
dont sauvegardes et préférences, gardent leurs hashes. Copie préalable dans
|
||||
`sanctuary-backups/before-beta.137/`, aucun monde personnel ouvert.
|
||||
|
||||
Packs normal/Test/template et comparatif natif également vérifiés dans
|
||||
`sanctuary-beta/build/`. Le checkout original garde ses changements préexistants.
|
||||
Reçus locaux ignorés : `build/shader137-artifact.json`, `build/shader137-isolated.json`,
|
||||
`build/shader137-prism.json`, `build/shader137-publication.log`.
|
||||
@@ -0,0 +1,101 @@
|
||||
# SHADER-138 — Épaisseur des rayons et météo
|
||||
|
||||
Demande : épaissir les sunshafts avec la météo, surtout le brouillard.
|
||||
Socle beta.137, branche `codex/sunshaft-weather-beta138`, Minecraft 26.3 / Java 25.
|
||||
Aucune modification des règles météo, des sauvegardes ou du gameplay.
|
||||
|
||||
## Contrat
|
||||
|
||||
Densité et diffusion liées au brouillard environnemental réellement rendu,
|
||||
indépendamment de la limite de distance de rendu. Pluie/neige natives :
|
||||
renforcement plus modéré ; orage : réduction supplémentaire du soleil.
|
||||
Transitions continues, mêmes deux passes et seize segments. Le réglage
|
||||
manuel conserve sa valeur, aucune nouvelle option.
|
||||
|
||||
## Rendu
|
||||
|
||||
Le brouillard utilise `FogData.environmentalEnd`, séparé de la distance de
|
||||
rendu native. Sa contribution croît en douceur entre 192 et 80 blocs de
|
||||
visibilité. La densité passe de ×1 à ×3,5 pour le brouillard seul ; la diffusion
|
||||
angulaire s'élargit et la lumière lointaine s'éteint plus vite. Les coefficients
|
||||
ne dépendent d'aucun bruit animé ni historique d'image.
|
||||
|
||||
Pluie et neige utilisent le niveau natif de précipitation : densité jusqu'à
|
||||
×1,8, diffusion plus légère. La lumière disponible tombe progressivement à
|
||||
25 % sous une pluie maximale et à 8,75 % sous un orage maximal. La carte solaire
|
||||
reste disponible pour le volume même lorsque Minecraft cache entièrement
|
||||
son disque solaire ; les ombres sur les surfaces conservent leur comportement.
|
||||
Le brouillard et la météo continuent leurs transitions natives.
|
||||
|
||||
Même cible au quart de chaque dimension, deux passes, seize segments et
|
||||
quatre lectures de la carte par segment. L'empreinte de l'ombre ne change pas :
|
||||
la météo ne crée pas d'ouverture artificielle à travers un mur.
|
||||
|
||||
## Vérification
|
||||
|
||||
Scénario client natif réussi en **3 min 29 s**, macOS / Apple M1 / OpenGL.
|
||||
Les contrôles des volumes beta.137 passent à nouveau : ouvertures, feuilles,
|
||||
pièce fermée, mouvement/retour, proximité, soleil hors champ, intensité,
|
||||
OFF/zéro/nuit, redimensionnement, rechargement, préférences et libellés FR/EN.
|
||||
|
||||
La suite utilise ensuite les vraies commandes serveur de météo : clair,
|
||||
`weather fog`, pluie, orage et retour au clair, dans le même décor avec le
|
||||
curseur maintenu à 35. Transition progressive du brouillard, rechargement
|
||||
sous brouillard, absence d'éclairage d'une pièce fermée et restitution de
|
||||
l'image claire initiale vérifiés. La contribution normalisée du volume dans
|
||||
cette scène passe d'environ 0,0034 au clair à 0,0236 sous brouillard ; ce n'est
|
||||
pas un multiplicateur universel de luminosité. L'orage reste sous la pluie.
|
||||
|
||||
Log : `build/shader138-client-final.log`, marqueurs `SUNSHAFT137_PASS` et
|
||||
`SUNSHAFT138_WEATHER_PASS`. Comparatif natif autonome :
|
||||
`build/Shader-beta.138-comparaison.html`.
|
||||
|
||||
## Diagnostic de la pluie
|
||||
|
||||
La première exécution native a validé le brouillard, puis s'est arrêtée en
|
||||
attendant des rayons sous une pluie maximale. Minecraft 26.3 fournit alors
|
||||
`rainBrightness = 1 - rainLevel`, soit zéro : beta.137 ne construisait plus la
|
||||
carte solaire. Le volume reçoit désormais son atténuation météo propre et
|
||||
conserve la carte nécessaire, sans réactiver les ombres de surface sous la
|
||||
pluie maximale. Log de diagnostic : `build/shader138-client-first.log`.
|
||||
|
||||
## Limites
|
||||
|
||||
Coefficients artistiques bornés, sans simulation physique de gouttes ni de
|
||||
nuages. Le brouillard environnemental est distinct de la simple distance
|
||||
d'affichage ; la pluie et la neige partagent le niveau natif de précipitation.
|
||||
Les limites des volumes beta.137 restent applicables : 64 blocs maximum,
|
||||
formes de la carte d'ombres et feuillage approximé. Pas de mesure de FPS ni
|
||||
de validation Windows/Vulkan revendiquée.
|
||||
|
||||
|
||||
## Construction et archives
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
réussit en **2 min 20 s**, 126 tâches. Le serveur GameTest dédié reste exclu
|
||||
conformément au refus EULA antérieur. Log : `build/shader138-build.log`.
|
||||
Comparaison à beta.137 : **6 entrées de production** modifiées, uniquement
|
||||
le rendu et les aides FR/EN. Gameplay et textures des gemmes strictement
|
||||
conservés. JAR normal/Test identique, sources et template vérifiés.
|
||||
Reçu local : `build/shader138-artifact.json`.
|
||||
|
||||
- `Sanctuary-beta.138.mrpack` : 10432860 octets ; SHA-256 `67093d3a53e3d90c31e8fb940d709ccf41ad730595e358346fa27ad744b44460`.
|
||||
|
||||
- `Sanctuary-Test-beta.138.mrpack` : 10451787 octets ; SHA-256 `1ff39158b47f44a831b6506c8c2636441b2bff02644d20b1698330097e5ff059`.
|
||||
|
||||
JAR : `44b6dd14f0e962fcb6f1efd61e0e7bb2c8dbb033cef81598582ec16c46a46518`.
|
||||
|
||||
## Livraison
|
||||
|
||||
[Release beta.138](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.138)
|
||||
publiée depuis `daf36fdeacca707930092363589c89984af78f3b`. Canal packwiz
|
||||
`c049423de1971079b9d61874d83adeb6f1ad49f7` ; téléchargements publics vérifiés par SHA-256.
|
||||
Deux synchronisations isolées puis deux dans la même instance **Sanctuary Beta**
|
||||
réussissent. Un seul JAR beta.138 actif ; les **923 fichiers personnels suivis**,
|
||||
dont sauvegardes et préférences, gardent leurs hashes. Copie préalable dans
|
||||
`sanctuary-backups/before-beta.138/`, aucun monde personnel ouvert.
|
||||
|
||||
Packs normal/Test/template et comparatif natif également vérifiés dans
|
||||
`sanctuary-beta/build/`. Le checkout original garde ses changements préexistants.
|
||||
Reçus locaux ignorés : `build/shader138-artifact.json`, `build/shader138-isolated.json`,
|
||||
`build/shader138-prism.json`, `build/shader138-publication.log`.
|
||||
@@ -0,0 +1,99 @@
|
||||
# SHADER-139 — Compatibilité Vulkan du shader beta.138
|
||||
|
||||
Socle **beta.138**, Minecraft **26.3**, Java **25**, branche
|
||||
`codex/vulkan-shaders-beta139`.
|
||||
|
||||
Le signalement initial concernait un bloom invisible même à 100 %. Le joueur
|
||||
indique ensuite que le rendu fonctionne de nouveau sur Windows et demande
|
||||
la compatibilité Vulkan. Les changements expérimentaux d'intensité ont été
|
||||
retirés : les shaders et les valeurs par défaut restent ceux de beta.138.
|
||||
|
||||
## Contrat
|
||||
|
||||
Utiliser les mêmes pipelines RenderPearl sur OpenGL et Vulkan. Vérifier le
|
||||
backend réellement utilisé, car Minecraft peut retomber sur OpenGL lorsque
|
||||
Vulkan ne démarre pas. Tester les effets dans de nouveaux mondes ignorés,
|
||||
sans ouvrir de sauvegarde personnelle ni modifier le backend de l'instance Prism.
|
||||
|
||||
Le lancement des tests accepte désormais `-PsanctuaryClientGraphicsBackend=vulkan`
|
||||
ou `opengl`. Les tests de shaders refusent tout repli silencieux et consignent
|
||||
le nom du backend, du GPU et du pilote. Cette option concerne uniquement
|
||||
le client de développement.
|
||||
|
||||
## Reproduction
|
||||
|
||||
```sh
|
||||
JAVA_HOME=/chemin/vers/jdk-25 ./gradlew :sanctuary:runClientGameTest \
|
||||
-PsanctuaryClientTests=true -PsanctuaryPixelShadows122ClientTests=true \
|
||||
-PsanctuaryClientGraphicsBackend=vulkan \
|
||||
-PsanctuaryClientNoVsync=true -PsanctuaryQuickTests=true
|
||||
```
|
||||
|
||||
Sur Windows, utiliser `gradlew.bat` avec les mêmes propriétés et Java 25.
|
||||
Ajouter `-PsanctuaryClientImprovedTransparency=true` pour tester la transparence
|
||||
améliorée ; le choix initial est restauré après le test.
|
||||
Pour les rayons et la météo, remplacer `sanctuaryPixelShadows122ClientTests`
|
||||
par `sanctuarySunshaft137ClientTests`. Pour isoler le bloom et le lens flare,
|
||||
utiliser `sanctuaryBloom130ClientTests`.
|
||||
|
||||
## Diagnostic initial
|
||||
|
||||
Le client Vulkan démarre ici sur **Apple M1 / MoltenVK 1.4.2**, API 1.2.350.
|
||||
Le test du bloom réussit en **2 min 19 s** : les masques des vingt minerais,
|
||||
les contrôles OFF/zéro, intensité/diffusion, l'occlusion, le rechargement,
|
||||
les torches, glowstone, lampes, lave, soleil et le lens flare sont vérifiés.
|
||||
Les mesures de référence du bloom sont identiques à celles d'OpenGL :
|
||||
14649 pixels de minerai, gain du halo faible 218131, fort 1970502,
|
||||
gain solaire 7708. Log : `build/shader139-vulkan-first.log`.
|
||||
|
||||
Cela confirme cette combinaison précise ; ce n'est pas une validation des
|
||||
pilotes NVIDIA/AMD/Intel sous Windows. En cas de retour du problème, le
|
||||
`logs/latest.log` de la session Windows concernée est nécessaire pour relever
|
||||
le backend effectif, le GPU, le pilote et les éventuelles erreurs de pipeline.
|
||||
|
||||
## Suite Vulkan
|
||||
|
||||
`sanctuaryPixelShadows122ClientTests` réussit en **8 min 50 s**. Les contrôles
|
||||
couvrent ombres diurnes/nocturnes, feuillage, stabilité en temps réel,
|
||||
highlights CTM, bloom et lens flare, SSGI en déplacement et près des surfaces,
|
||||
PBR directionnel, rechargement, redimensionnement et interfaces FR/EN.
|
||||
Log : `build/shader139-vulkan-suite2.log`.
|
||||
|
||||
Le bloom et le lens flare réussissent aussi avec
|
||||
`sanctuaryClientImprovedTransparency=true`, en **1 min 45 s**. Le backend
|
||||
est bien Vulkan, sa profondeur est en `[0, 1]` et l'option de transparence
|
||||
est active. Les mesures du bloom restent identiques.
|
||||
Log : `build/shader139-vulkan-transparency.log`.
|
||||
|
||||
La suite météo/rayons beta.138 n'a pas été rejouée sous Vulkan dans ce ticket ;
|
||||
les résultats ci-dessus portent sur les effets explicitement listés.
|
||||
Le joueur confirme que le retour à OpenGL rétablit l'effet sur Windows.
|
||||
Ce défaut particulier reste à reproduire à partir d'un journal récent.
|
||||
|
||||
Un premier lancement utilisait un nom de propriété incomplet et a exécuté
|
||||
la suite générale, échouant dans Blocodex avant les tests de shaders.
|
||||
Ce lancement n'est pas compté comme une validation Vulkan des shaders.
|
||||
La commande corrigée utilise `sanctuaryPixelShadows122ClientTests`.
|
||||
|
||||
## Construction
|
||||
|
||||
`./gradlew check build -x :sanctuary:runGameTest` réussit ; le serveur
|
||||
GameTest dédié reste exclu conformément au refus EULA antérieur.
|
||||
Log : `build/shader139-check-build.log`. Le JAR reconstruit est **strictement
|
||||
identique, octet pour octet**, au JAR Sanctuary de la release beta.138.
|
||||
La distribution ne change pas et ne nécessite donc pas de nouvel assemblage.
|
||||
|
||||
## Distribution
|
||||
|
||||
Ce ticket ajoute des options de lancement de tests et leur documentation.
|
||||
Les shaders de production et les réglages de beta.138 ne sont pas changés.
|
||||
Aucune nouvelle livraison binaire, aucun tag beta.139 ni mise à jour de
|
||||
l'instance personnelle n'est effectué pour une vérification seule.
|
||||
|
||||
## Référence du moteur
|
||||
|
||||
Minecraft 26.3 compile les shaders OpenGL et Vulkan via ShaderC et expose
|
||||
`RENDERPEARL_DEPTH_IS_ZERO_TO_ONE` pour la convention de profondeur.
|
||||
[Notes officielles Minecraft 26.3](https://feedback.minecraft.net/hc/en-us/articles/48913133328013-Minecraft-Java-Edition-26-3).
|
||||
Les effets Sanctuary transmettent déjà la convention du périphérique aux
|
||||
reconstructions de position ; aucun remplacement spéculatif n'est appliqué.
|
||||
@@ -107,3 +107,9 @@ Les contrôles ne revendiquent pas un essai avec Iris ou un autre moteur externe
|
||||
Les ombres portées pixélisées sont livrées dans [beta.127](shader-beta127.md),
|
||||
intégrées sur la beta.126. Leur préparation locale portait un numéro beta.122
|
||||
non publié ; la release beta.122 existante n'est pas remplacée.
|
||||
|
||||
## beta.144 — Eau naturelle
|
||||
|
||||
Le [ticket WATER-144](natural-water-beta144.md) ajoute les profils clair,
|
||||
légèrement teinté et étang au rendu natif des fluides et à leur brouillard.
|
||||
La couleur du biome et les textures pixélisées restent la base.
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# Vérifier Sanctuary
|
||||
|
||||
## Cible graphique — décision du 18 septembre 2026
|
||||
|
||||
Les nouvelles validations graphiques Sanctuary utilisent **Vulkan uniquement**.
|
||||
OpenGL est abandonné comme cible de validation à la demande du créateur.
|
||||
Les anciens résultats OpenGL ci-dessous sont historiques et ne constituent
|
||||
pas un engagement de compatibilité. Ne plus programmer de suite OpenGL.
|
||||
|
||||
|
||||
Utiliser Java 25 et les dépendances épinglées dans `gradle.properties`.
|
||||
|
||||
```sh
|
||||
|
||||
+2
-2
@@ -9,8 +9,8 @@ loom_version=1.17.20
|
||||
fabric_api_version=0.160.5+26.3
|
||||
|
||||
# Release counter: beta.001, .002, .003, ... (see docs/versioning.md).
|
||||
mod_version=beta.129
|
||||
pack_version=beta.129
|
||||
mod_version=beta.157
|
||||
pack_version=beta.157
|
||||
resource_pack_version=beta.121
|
||||
maven_group=fr.koka.sanctuary
|
||||
jei_version=30.32.0-sanctuary.3
|
||||
|
||||
@@ -25,6 +25,24 @@ if (clientRenderTests) {
|
||||
java.srcDir('src/clientGametest/java')
|
||||
}
|
||||
tasks.named('runClientGameTest') {
|
||||
def graphicsBackend = providers.gradleProperty('sanctuaryClientGraphicsBackend')
|
||||
if (graphicsBackend.isPresent()) {
|
||||
args('--graphicsBackend', graphicsBackend.get())
|
||||
systemProperty('sanctuary.test.graphicsBackend', graphicsBackend.get())
|
||||
}
|
||||
def improvedTransparency = providers.gradleProperty('sanctuaryClientImprovedTransparency')
|
||||
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'))
|
||||
systemProperty('sanctuary.test.introVisualOnly', providers.gradleProperty('sanctuaryIntroVisualOnly').getOrElse('false'))
|
||||
@@ -93,6 +111,27 @@ tasks.named('runGameTest') {
|
||||
}
|
||||
|
||||
tasks.named('processGametestResources') {
|
||||
def colored139Client = providers.gradleProperty('sanctuaryColored139ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryColored139ClientTests', colored139Client)
|
||||
if (colored139Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.shader.ColoredLights139ClientChecks') }
|
||||
|
||||
def pbr134Client = providers.gradleProperty('sanctuaryPbr134ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryPbr134ClientTests', pbr134Client)
|
||||
if (pbr134Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.shader.Pbr134ClientChecks') }
|
||||
def ssgi132Client = providers.gradleProperty('sanctuarySsgi132ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuarySsgi132ClientTests', ssgi132Client)
|
||||
if (ssgi132Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.shader.Ssgi132ClientChecks') }
|
||||
def closeup135Client = providers.gradleProperty('sanctuarySsgiCloseup135ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuarySsgiCloseup135ClientTests', closeup135Client)
|
||||
if (closeup135Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.shader.SsgiCloseup135ClientChecks') }
|
||||
|
||||
def sunshaft137Client = providers.gradleProperty('sanctuarySunshaft137ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuarySunshaft137ClientTests', sunshaft137Client)
|
||||
if (sunshaft137Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.shader.Sunshafts137ClientChecks') }
|
||||
def bloom130Client = providers.gradleProperty('sanctuaryBloom130ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryBloom130ClientTests', bloom130Client)
|
||||
if (bloom130Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.shader.Bloom130ClientChecks') }
|
||||
|
||||
def shadows122Client = providers.gradleProperty('sanctuaryPixelShadows122ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryPixelShadows122ClientTests', shadows122Client)
|
||||
if (shadows122Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.shader.PixelShadows122ClientChecks') }
|
||||
@@ -219,6 +258,15 @@ tasks.named('processGametestResources') {
|
||||
def multiblocks084Client = providers.gradleProperty('sanctuaryMultiblocks084ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryMultiblocks084ClientTests', multiblocks084Client)
|
||||
if (multiblocks084Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Multiblocks084ClientChecks') }
|
||||
def photos157Client = providers.gradleProperty('sanctuaryPhotos157ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryPhotos157ClientTests', photos157Client)
|
||||
if (photos157Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Photos157ClientChecks') }
|
||||
def pause155Client = providers.gradleProperty('sanctuaryPause155ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryPause155ClientTests', pause155Client)
|
||||
if (pause155Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Pause155ClientChecks') }
|
||||
def community154Client = providers.gradleProperty('sanctuaryCommunity154ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryCommunity154ClientTests', community154Client)
|
||||
if (community154Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Community154ClientChecks') }
|
||||
def menus083Client = providers.gradleProperty('sanctuaryMenus083ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryMenus083ClientTests', menus083Client)
|
||||
if (menus083Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Menus083ClientChecks') }
|
||||
@@ -448,6 +496,8 @@ tasks.named('processGametestResources') {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'org.mariadb.jdbc:mariadb-java-client:3.5.10'
|
||||
include('org.mariadb.jdbc:mariadb-java-client:3.5.10')
|
||||
implementation project(':jei')
|
||||
include project(':jei')
|
||||
implementation project(':demeure')
|
||||
@@ -1172,3 +1222,19 @@ tasks.register('seasonal105Smoke', JavaExec) {
|
||||
mainClass = 'fr.koka.sanctuary.weather.Seasonal105Smoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('seasonal105Smoke') }
|
||||
|
||||
tasks.register('community154Smoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.community.CommunitySmoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('community154Smoke') }
|
||||
|
||||
tasks.register('community154Interop', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.community.CommunityInterop'
|
||||
args(providers.gradleProperty('communityInteropPhase').getOrElse('seed'))
|
||||
}
|
||||
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import java.util.*;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
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;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.components.events.*;
|
||||
import net.minecraft.client.gui.screens.*;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
|
||||
/** Real screens and server packets on an isolated flat development world. */
|
||||
public final class Community154ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
boolean outage=Boolean.parseBoolean(System.getenv("SANCTUARY_COMMUNITY_CLIENT_OUTAGE"));
|
||||
boolean database=Boolean.parseBoolean(System.getenv("SANCTUARY_COMMUNITY_CLIENT_DATABASE"));
|
||||
if(database) try {
|
||||
var path=net.fabricmc.loader.api.FabricLoader.getInstance().getConfigDir().resolve("sanctuary-community.json");
|
||||
var config=new fr.koka.sanctuary.community.CommunityConfig("database",UUID.randomUUID().toString(),System.getenv("SANCTUARY_COMMUNITY_TEST_JDBC"),"root","SANCTUARY_COMMUNITY_DB_PASSWORD");
|
||||
java.nio.file.Files.createDirectories(path.getParent());
|
||||
java.nio.file.Files.writeString(path,new com.google.gson.Gson().toJson(config));
|
||||
} catch(Exception error) { throw new AssertionError(error); }
|
||||
var intro = new AtomicBoolean(true);
|
||||
c.runOnClient(m -> { m.options.renderDistance().set(5); m.options.simulationDistance().set(5); m.options.pauseOnLostFocus=false; IntroClient.setQuickTestIntro(true); });
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m -> { if (intro.get() && m.gui.screen() instanceof HelloWorldScreen screen) {
|
||||
Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b -> b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
press(screen,"sanctuary.progression.enter"); intro.set(false);
|
||||
}});
|
||||
try (var world = c.worldBuilder().adjustSettings(s -> { s.setName("Community154 "+UUID.randomUUID()); s.setSeed("154"); s.setAllowCommands(false); s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL); s.setWorldType(s.getNormalPresetList().stream().filter(e -> e.preset()!=null && e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow()); }).create()) {
|
||||
c.waitFor(m -> ProgressionClient.active() && m.player!=null && m.gui.screen()==null && !IntroSpawnFlash.active(),1200);
|
||||
world.getServer().runOnServer(s -> s.getPlayerList().getPlayers().getFirst().getAbilities().invulnerable=true);
|
||||
if(outage) {
|
||||
c.runOnClient(m -> m.gui.setScreen(new PauseScreen(true)));
|
||||
c.waitFor(m -> !CommunityClient.pending() && CommunityClient.overviewError.equals("unavailable"),300);
|
||||
c.takeScreenshot("community154-database-unavailable");
|
||||
c.runOnClient(m -> { CommunityClient.open(m.gui.screen(),"article","",true); edit(m.gui.screen()).setValue("Brouillon panne"); body(m.gui.screen()).setValue("Texte conservé"); press(m.gui.screen(),"sanctuary.community.publish"); });
|
||||
c.waitFor(m -> !CommunityClient.pending() && has(m.gui.screen(),"sanctuary.community.unavailable"),300);
|
||||
c.runOnClient(m -> { check(body(m.gui.screen()).getValue().equals("Texte conservé"),"Outage draft retained"); check(CommunityClient.overview.isEmpty(),"No false cached acknowledgement"); });
|
||||
world.getServer().runOnServer(s -> check(!java.nio.file.Files.exists(s.getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT).resolve("data/sanctuary-community.json")),"No file fallback"));
|
||||
System.out.println("COMMUNITY154_OUTAGE_PASS: visible error, draft preserved, no file fallback");
|
||||
c.runOnClient(m -> m.gui.setScreen(null));
|
||||
return;
|
||||
}
|
||||
for (String locale : List.of("fr_fr","en_us")) for (int scale : List.of(2,3)) {
|
||||
c.runOnClient(m -> { m.getWindow().setWindowed(1280,800); m.options.guiScale().set(scale); m.resizeGui(); m.getLanguageManager().setSelected(locale); m.getLanguageManager().onResourceManagerReload(m.getResourceManager()); m.gui.setScreen(new PauseScreen(true)); });
|
||||
c.waitFor(m -> !CommunityClient.pending() && !CommunityClient.overview.isEmpty(),300);
|
||||
c.runOnClient(m -> { check(CommunityClient.overview.get("mode").getAsString().equals(database?"database":"file"),"Expected storage adapter"); press(m.gui.screen(),"sanctuary.community.article"); });
|
||||
c.waitFor(m -> !CommunityClient.pending() && has(m.gui.screen(),"sanctuary.community.empty"),300);
|
||||
c.takeScreenshot("community154-list-"+locale+"-"+scale);
|
||||
c.runOnClient(m -> m.gui.setScreen(new PauseScreen(true))); c.waitTicks(5);
|
||||
c.takeScreenshot("community154-pause-"+locale+"-"+scale);
|
||||
}
|
||||
c.runOnClient(m -> { m.options.guiScale().set(2); m.resizeGui(); m.getLanguageManager().setSelected("fr_fr"); m.getLanguageManager().onResourceManagerReload(m.getResourceManager()); CommunityClient.open(new PauseScreen(true),"notice","",true); });
|
||||
c.waitFor(m -> !CommunityClient.pending(),250);
|
||||
c.runOnClient(m -> { edit(m.gui.screen()).setValue("La passerelle 🌳"); body(m.gui.screen()).setValue("Du bois et des bras.\nQui vient construire ?"); press(m.gui.screen(),"sanctuary.community.publish"); });
|
||||
c.waitFor(m -> !CommunityClient.pending() && has(m.gui.screen(),"sanctuary.community.reply"),300);
|
||||
c.takeScreenshot("community154-published");
|
||||
c.waitTicks(25);
|
||||
c.runOnClient(m -> { body(m.gui.screen()).setValue("Je viens ce soir !"); press(m.gui.screen(),"sanctuary.community.reply"); });
|
||||
c.waitFor(m -> !CommunityClient.pending() && text(m.gui.screen(),"Je viens ce soir !"),300);
|
||||
c.takeScreenshot("community154-reply");
|
||||
c.runOnClient(m -> check(widgets(m.gui.screen()).filter(PlayerFaceWidget.class::isInstance).count()==2,"Both conversation participants have faces"));
|
||||
c.waitTicks(25);
|
||||
c.runOnClient(m -> { press(m.gui.screen(),"sanctuary.community.actions"); press(m.gui.screen(),"sanctuary.community.close"); });
|
||||
c.waitFor(m -> !CommunityClient.pending() && has(m.gui.screen(),"sanctuary.community.reopen"),300);
|
||||
c.runOnClient(m -> { check(!has(m.gui.screen(),"sanctuary.community.reply"),"Closed announcement hides reply action"); CommunityClient.DRAFTS.computeIfAbsent("new:article",unused->new CommunityClient.Draft()).photo=testPhoto(); CommunityClient.open(new PauseScreen(true),"article","",true); edit(m.gui.screen()).setValue("Brouillon"); body(m.gui.screen()).setValue("§Texte refusé"); press(m.gui.screen(),"sanctuary.community.publish"); });
|
||||
c.waitFor(m -> !CommunityClient.pending() && (has(m.gui.screen(),"sanctuary.community.invalid") || has(m.gui.screen(),"sanctuary.community.busy")),300);
|
||||
c.runOnClient(m -> { check(body(m.gui.screen()).getValue().equals("§Texte refusé"),"Refused draft retained"); m.gui.screen().onClose(); CommunityClient.open(m.gui.screen(),"article","",true); check(edit(m.gui.screen()).getValue().equals("Brouillon"),"Draft survives closing screen"); m.gui.screen().onClose(); });
|
||||
c.runOnClient(m -> { CommunityClient.open(m.gui.screen(),"article","",false); }); c.waitTicks(4);
|
||||
c.runOnClient(m -> m.gui.screen().onClose()); c.waitTicks(30);
|
||||
c.runOnClient(m -> check(m.gui.screen() instanceof PauseScreen,"Late response does not reopen screen"));
|
||||
System.out.println("COMMUNITY154_CLIENT_MODE="+(database?"database":"file"));
|
||||
System.out.println("COMMUNITY154_CLIENT_PASS: pause FR/EN GUI 2/3, publication, reply, close, refused draft, late response");
|
||||
c.runOnClient(m -> m.gui.setScreen(null));
|
||||
} finally { intro.set(false); }
|
||||
}
|
||||
static String testPhoto() {try {return fr.koka.sanctuary.community.CommunityPhoto.encode(new java.awt.image.BufferedImage(16,9,java.awt.image.BufferedImage.TYPE_INT_RGB),960,540);}catch(Exception e){throw new AssertionError(e);}}
|
||||
static java.util.stream.Stream<GuiEventListener> widgets(GuiEventListener e) { return java.util.stream.Stream.concat(java.util.stream.Stream.of(e), e instanceof ContainerEventHandler h ? h.children().stream().flatMap(Community154ClientChecks::widgets) : java.util.stream.Stream.empty()); }
|
||||
static boolean has(Screen s,String key) { return s!=null && widgets(s).filter(AbstractWidget.class::isInstance).map(AbstractWidget.class::cast).anyMatch(w -> w.getMessage().getContents() instanceof TranslatableContents t && t.getKey().equals(key)); }
|
||||
static boolean text(Screen s,String value) { return widgets(s).filter(AbstractWidget.class::isInstance).map(AbstractWidget.class::cast).anyMatch(w -> w.getMessage().getString().equals(value)); }
|
||||
static void press(Screen s,String key) { widgets(s).filter(AbstractButton.class::isInstance).map(AbstractButton.class::cast).filter(b -> b.getMessage().getContents() instanceof TranslatableContents t && t.getKey().equals(key)).findFirst().orElseThrow(() -> new AssertionError("Missing "+key)).onPress(enter()); }
|
||||
static EditBox edit(Screen s) { return widgets(s).filter(EditBox.class::isInstance).map(EditBox.class::cast).findFirst().orElseThrow(); }
|
||||
static MultiLineEditBox body(Screen s) { return widgets(s).filter(MultiLineEditBox.class::isInstance).map(MultiLineEditBox.class::cast).findFirst().orElseThrow(); }
|
||||
static KeyEvent enter() { return new KeyEvent(InputConstants.KEY_RETURN,0,0); }
|
||||
static void check(boolean ok,String message) { if (!ok) throw new AssertionError(message); }
|
||||
}
|
||||
+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);}
|
||||
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import fr.koka.sanctuary.community.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
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;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.screens.*;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import static fr.koka.sanctuary.client.Community154ClientChecks.*;
|
||||
|
||||
/** Native pause regions, independently scrollable feeds, paginated posts and long conversations. */
|
||||
public final class Pause155ClientChecks implements FabricClientGameTest {
|
||||
static String photo() throws Exception {return CommunityPhoto.encode(new java.awt.image.BufferedImage(32,18,java.awt.image.BufferedImage.TYPE_INT_RGB),960,540);}
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
boolean database=Boolean.parseBoolean(System.getenv("SANCTUARY_COMMUNITY_CLIENT_DATABASE"));
|
||||
var intro=new AtomicBoolean(true);
|
||||
try {
|
||||
var path=net.fabricmc.loader.api.FabricLoader.getInstance().getConfigDir().resolve("sanctuary-community.json");
|
||||
var config=new CommunityConfig(database?"database":"file",UUID.randomUUID().toString(),database?System.getenv("SANCTUARY_COMMUNITY_TEST_JDBC"):"jdbc:mariadb://localhost/sanctuary","root","SANCTUARY_COMMUNITY_DB_PASSWORD");
|
||||
java.nio.file.Files.createDirectories(path.getParent());java.nio.file.Files.writeString(path,new com.google.gson.Gson().toJson(config));
|
||||
} catch(Exception e) {throw new AssertionError(e);}
|
||||
c.runOnClient(m->{m.options.renderDistance().set(5);m.options.simulationDistance().set(5);m.options.pauseOnLostFocus=false;IntroClient.setQuickTestIntro(true);});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get() && m.gui.screen() instanceof HelloWorldScreen s) {
|
||||
Starter039TestSupport.selectFirst(s);s.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());press(s,"sanctuary.progression.enter");intro.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Pause155 "+UUID.randomUUID());s.setSeed("155");s.setAllowCommands(false);s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()) {
|
||||
c.waitFor(m->ProgressionClient.active() && m.player!=null && m.gui.screen()==null && !IntroSpawnFlash.active(),1200);
|
||||
world.getServer().runOnServer(s->{
|
||||
var p=s.getPlayerList().getPlayers().getFirst();p.getAbilities().invulnerable=true;p.setExperienceLevels(20);
|
||||
check(ProgressionService.purchase(p,"atlas",ProgressEffects.get(p).revision()),"Atlas unlocked");ProgressionService.send(p,"");
|
||||
for(int dz=-2;dz<=2;dz++)for(int dx=-2;dx<=2;dx++) {
|
||||
var tile=fr.koka.sanctuary.atlas.AtlasSampler.sample(p.level(),p.chunkPosition().x()+dx,p.chunkPosition().z()+dz);
|
||||
check(tile!=null,"Loaded fixture tile");fr.koka.sanctuary.atlas.AtlasService.observations(p).observe(tile);
|
||||
}
|
||||
try {
|
||||
var config=CommunityConfig.load(net.fabricmc.loader.api.FabricLoader.getInstance().getConfigDir().resolve("sanctuary-community.json"));
|
||||
CommunityStore store=database?new SqlCommunityStore(config.jdbcUrl(),config.user(),System.getenv(config.passwordEnv()),config.serverId()):new FileCommunityStore(s.getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT).resolve("data/sanctuary-community.json"),config.serverId());
|
||||
var actor=new Community.Actor(p.getStringUUID(),"Koka",true);
|
||||
for(String kind:List.of("article","notice")) for(int i=0;i<13;i++)store.change(actor,new Community.Change("create",UUID.randomUUID().toString(),"",kind,"work",(kind.equals("article")?"Gazette — ":"La passerelle — ")+String.format("%02d",i),"Rendez-vous sur la place du village.\n".repeat(60)+"FIN DU MESSAGE",0,kind.equals("article")?photo():null));
|
||||
store.change(actor,new Community.Change("create",UUID.randomUUID().toString(),"","bulletin","info","Bienvenue au village","Le marché ouvrira ce soir à 20 h.",0));
|
||||
} catch(Exception e) {throw new AssertionError(e);}
|
||||
});
|
||||
c.waitFor(m->ProgressionClient.progress.atlas(),200);c.waitTicks(40);
|
||||
for(String locale:List.of("fr_fr","en_us"))for(int scale:List.of(2,3,4)) {
|
||||
c.runOnClient(m->{m.getWindow().setWindowed(1280,960);m.options.guiScale().set(scale);m.resizeGui();m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.gui.setScreen(new PauseScreen(true));});
|
||||
c.waitFor(m->widgets(m.gui.screen()).filter(AbstractWidget.class::isInstance).map(AbstractWidget.class::cast).filter(w->w.getMessage().getString().startsWith("Gazette — ")).count()==10 && widgets(m.gui.screen()).filter(AbstractWidget.class::isInstance).map(AbstractWidget.class::cast).filter(w->w.getMessage().getString().startsWith("La passerelle — ")).count()==10 && !CommunityClient.pending(),400);
|
||||
c.waitFor(m->((PauseAtlasAccess)m.gui.screen()).sanctuary$pauseMap().knownPixels()>4096,400);
|
||||
c.runOnClient(m->{
|
||||
var s=m.gui.screen();
|
||||
check(s instanceof PauseAtlasAccess,"Pause atlas access");
|
||||
for(String key:List.of("sanctuary.progression.blocks","sanctuary.progression.history","sanctuary.progression.progress","sanctuary.progression.identity","menu.options","options.worldOptions.button","menu.returnToMenu","menu.returnToGame"))check(has(s,key),"Destination "+key);
|
||||
var navigation=s.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getX()==8&&b.getY()>=40&&b.getY()<s.height-76).sorted(java.util.Comparator.comparingInt(Button::getY)).toList();
|
||||
check(navigation.size()==6,"Six real navigation buttons");
|
||||
for(int i=1;i<navigation.size();i++)check(navigation.get(i).getY()-navigation.get(i-1).getBottom()==4,"Uniform 4-pixel navigation spacing");
|
||||
var options=nativeButton(s,"menu.options");var worldOptions=nativeButton(s,"options.worldOptions.button");var quit=nativeButton(s,"menu.returnToMenu");var resume=nativeButton(s,"menu.returnToGame");
|
||||
check(options.getX()==8 && worldOptions.getX()==8 && quit.getX()==8 && worldOptions.getY()-options.getBottom()==4 && quit.getY()-worldOptions.getBottom()==4 && quit.getBottom()==s.height-8,"Options and quit stacked at bottom left");
|
||||
var feed=areas(s).getFirst();
|
||||
check(resume.getX()==feed.getX() && resume.getWidth()==feed.getWidth() && resume.getBottom()==s.height-8 && areas(s).getLast().getBottom()<resume.getY(),"Resume aligned below right community column");
|
||||
for(var e:s.children())if(e instanceof Button b && b.getX()==8 && b.getY()<options.getY())check(b.getBottom()<options.getY(),"Navigation does not overlap options");
|
||||
check(!has(s,"gui.advancements") && !has(s,"gui.stats") && !has(s,"menu.game"),"Old grid removed");
|
||||
for(var e:s.children())if(e instanceof AbstractWidget w)check(w.getX()>=0&&w.getRight()<=s.width&&w.getY()>=0&&w.getBottom()<=s.height,"Widget bounds "+w.getClass().getSimpleName()+" "+w.getX()+","+w.getY()+" "+w.getWidth()+"x"+w.getHeight()+" / "+s.width+"x"+s.height);
|
||||
var faces=widgets(s).filter(PlayerFaceWidget.class::isInstance).map(PlayerFaceWidget.class::cast).toList();
|
||||
check(faces.size()==10,"Each Gazette article has a face");
|
||||
var first=firstPost(s,"Gazette — ");check(faces.getFirst().getY()==first.getY() && faces.getFirst().getRight()+5==first.getX(),"Face is left of the title on the same row");
|
||||
var areas=areas(s);check(areas.size()==2,"Two independent feeds");
|
||||
double other=areas.get(1).scrollAmount();check(s.mouseScrolled(areas.get(0).getX()+5,areas.get(0).getY()+5,0,-5),"Gazette wheel handled");check(areas.get(0).scrollAmount()>0&&areas.get(1).scrollAmount()==other,"Gazette scroll isolated");
|
||||
check(s.mouseScrolled(areas.get(1).getX()+5,areas.get(1).getY()+5,0,-5),"Notice wheel handled");check(areas.get(1).scrollAmount()>0,"Notices scroll");
|
||||
areas.forEach(a->a.setScrollAmount(0));
|
||||
});
|
||||
if(locale.equals("fr_fr") && scale==2) {
|
||||
var time=new java.util.concurrent.atomic.AtomicLong();
|
||||
world.getServer().runOnServer(s->{check(((net.minecraft.client.server.IntegratedServer)s).isPaused(),"Solo remains paused");time.set(s.overworld().getGameTime());});
|
||||
c.waitTicks(30);world.getServer().runOnServer(s->check(s.overworld().getGameTime()==time.get(),"Reading atlas does not advance world"));
|
||||
}
|
||||
c.takeScreenshot("pause155-"+locale+"-"+scale+(database?"-database":"-file"));
|
||||
}
|
||||
c.runOnClient(m->{var areas=areas(m.gui.screen());areas.getFirst().setScrollAmount(areas.getFirst().maxScrollAmount());press(m.gui.screen(),"sanctuary.community.more");});
|
||||
c.waitFor(m->widgets(m.gui.screen()).filter(AbstractWidget.class::isInstance).map(AbstractWidget.class::cast).filter(w->w.getMessage().getString().startsWith("Gazette — ")).count()==13&&!CommunityClient.pending(),300);
|
||||
c.runOnClient(m->{check(areas(m.gui.screen()).getFirst().scrollAmount()>0,"Appending preserves scroll");firstPost(m.gui.screen(),"Gazette — ").onPress(enter());});
|
||||
c.waitFor(m->has(m.gui.screen(),"sanctuary.community.reply")&&!CommunityClient.pending(),300);
|
||||
c.runOnClient(m->{var a=areas(m.gui.screen()).getFirst();check(a.maxScrollAmount()>300,"Long article scrolls");m.gui.screen().mouseScrolled(a.getX()+5,a.getY()+5,0,-1000);check(a.scrollAmount()==a.maxScrollAmount(),"Reach article end and reply");});
|
||||
c.takeScreenshot("pause155-long-article");
|
||||
c.runOnClient(m->{body(m.gui.screen()).setValue("Présent pour le marché !");press(m.gui.screen(),"sanctuary.community.reply");});
|
||||
c.waitFor(m->text(m.gui.screen(),"Présent pour le marché !")&&!CommunityClient.pending(),300);
|
||||
c.runOnClient(m->m.gui.setScreen(new PauseScreen(true)));
|
||||
c.waitFor(m->widgets(m.gui.screen()).filter(AbstractWidget.class::isInstance).map(AbstractWidget.class::cast).anyMatch(w->w.getMessage().getString().startsWith("La passerelle — ")),300);
|
||||
c.runOnClient(m->firstPost(m.gui.screen(),"La passerelle — ").onPress(enter()));
|
||||
c.waitFor(m->has(m.gui.screen(),"sanctuary.community.reply")&&!CommunityClient.pending(),300);
|
||||
c.runOnClient(m->{var a=areas(m.gui.screen()).getFirst();check(a.maxScrollAmount()>300,"Long notice scrolls");m.gui.screen().mouseScrolled(a.getX()+5,a.getY()+5,0,-1000);check(a.scrollAmount()==a.maxScrollAmount(),"Reach notice end");m.gui.setScreen(new PauseScreen(true));});
|
||||
c.waitFor(m->((PauseAtlasAccess)m.gui.screen()).sanctuary$pauseMap().knownPixels()>4096,400);
|
||||
c.runOnClient(m->{check(((PauseAtlasAccess)m.gui.screen()).sanctuary$pauseMap().knownPixels()>0,"Paused map renders personal surveys");press(m.gui.screen(),"menu.options");check(m.gui.screen() instanceof net.minecraft.client.gui.screens.options.OptionsScreen,"Native options");m.gui.screen().onClose();press(m.gui.screen(),"options.worldOptions.button");check(m.gui.screen() instanceof WorldOptionsScreen,"Native world options");m.gui.screen().onClose();press(m.gui.screen(),"menu.returnToGame");check(m.gui.screen()==null,"Resume game");});
|
||||
c.runOnClient(m->m.gui.setScreen(new PauseScreen(true)));c.waitTicks(25);
|
||||
c.runOnClient(m->((PauseAtlasAccess)m.gui.screen()).sanctuary$pauseMap().onPress(enter()));
|
||||
c.waitFor(m->m.gui.screen() instanceof SanctuaryAtlasScreen,100);
|
||||
c.runOnClient(m->m.gui.screen().onClose());c.waitTicks(25);
|
||||
c.runOnClient(m->{check(m.gui.screen() instanceof PauseScreen,"Atlas returns to pause");m.gui.setScreen(null);});
|
||||
System.out.println("PAUSE155_CLIENT_PASS mode="+(database?"database":"file")+" FR/EN scales 2/3/4, independent wheel scrolling, pagination, long article/notice, reply, native footer, atlas return");
|
||||
} finally {intro.set(false);}
|
||||
}
|
||||
private static Button nativeButton(Screen s,String key) {return widgets(s).filter(Button.class::isInstance).map(Button.class::cast).filter(b->CommunityPausePanels.key(b).equals(key)).findFirst().orElseThrow();}
|
||||
private static List<AbstractScrollArea> areas(Screen s) {return widgets(s).filter(AbstractScrollArea.class::isInstance).map(AbstractScrollArea.class::cast).toList();}
|
||||
private static AbstractButton firstPost(Screen s,String prefix) {return widgets(s).filter(AbstractButton.class::isInstance).map(AbstractButton.class::cast).filter(b->b.getMessage().getString().startsWith(prefix)).findFirst().orElseThrow();}
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import fr.koka.sanctuary.community.*;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
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;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.screens.*;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import static fr.koka.sanctuary.client.Community154ClientChecks.*;
|
||||
|
||||
/** Screenshot selection and article round trip through actual client/server packets. */
|
||||
public final class Photos157ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
boolean database=Boolean.parseBoolean(System.getenv("SANCTUARY_COMMUNITY_CLIENT_DATABASE"));
|
||||
var intro=new AtomicBoolean(true);
|
||||
var folder=new java.util.concurrent.atomic.AtomicReference<Path>();
|
||||
try {
|
||||
var path=net.fabricmc.loader.api.FabricLoader.getInstance().getConfigDir().resolve("sanctuary-community.json");
|
||||
var config=new CommunityConfig(database?"database":"file",UUID.randomUUID().toString(),database?System.getenv("SANCTUARY_COMMUNITY_TEST_JDBC"):"jdbc:mariadb://localhost/sanctuary","root","SANCTUARY_COMMUNITY_DB_PASSWORD");
|
||||
Files.createDirectories(path.getParent());Files.writeString(path,new com.google.gson.Gson().toJson(config));
|
||||
} catch(Exception e){throw new AssertionError(e);}
|
||||
c.runOnClient(m->{m.options.renderDistance().set(5);m.options.simulationDistance().set(5);m.options.pauseOnLostFocus=false;IntroClient.setQuickTestIntro(true);folder.set(m.gameDirectory.toPath().resolve("screenshots"));});
|
||||
try {
|
||||
Files.createDirectories(folder.get());
|
||||
var image=new java.awt.image.BufferedImage(1200,675,java.awt.image.BufferedImage.TYPE_INT_RGB);
|
||||
var random=new Random(157);
|
||||
for(int y=0;y<675;y++)for(int x=0;x<1200;x++)image.setRGB(x,y,((x/50)%2==0 && (y/50)%2==0)?0x2a6b52:random.nextInt());
|
||||
for(int i=0;i<28;i++)javax.imageio.ImageIO.write(image,"png",folder.get().resolve(String.format("z-photo157-%02d.png",i)).toFile());
|
||||
Files.writeString(folder.get().resolve("z-photo157-29.png"),"not a PNG");
|
||||
}catch(Exception e){throw new AssertionError(e);}
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get() && m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
press(screen,"sanctuary.progression.enter");intro.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Photos157 "+UUID.randomUUID());s.setSeed("157");s.setAllowCommands(false);s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()) {
|
||||
c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);
|
||||
world.getServer().runOnServer(s->s.getPlayerList().getPlayers().getFirst().getAbilities().invulnerable=true);
|
||||
for(String locale:List.of("fr_fr","en_us"))for(int scale:List.of(2,3,4)) {
|
||||
c.runOnClient(m->{
|
||||
m.getWindow().setWindowed(1280,960);m.options.guiScale().set(scale);m.resizeGui();m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());
|
||||
CommunityClient.open(new PauseScreen(true),"article","",true);
|
||||
check(!button(m.gui.screen(),"publish").active,"Photo is mandatory in the composer");
|
||||
edit(m.gui.screen()).setValue("Une journée au village");body(m.gui.screen()).setValue("La photo rejoint la Gazette.");
|
||||
press(m.gui.screen(),"sanctuary.community.photo_choose");
|
||||
});
|
||||
c.waitFor(m->widgets(m.gui.screen()).anyMatch(CommunityPhotoWidget.class::isInstance),500);
|
||||
c.runOnClient(m->{
|
||||
var s=m.gui.screen();check(s instanceof CommunityScreenshotsScreen,"Dedicated screenshot view");
|
||||
var a=widgets(s).filter(AbstractScrollArea.class::isInstance).map(AbstractScrollArea.class::cast).findFirst().orElseThrow();
|
||||
check(a.maxScrollAmount()>0,"Gallery scrolls");s.mouseScrolled(a.getX()+5,a.getY()+5,0,-4);check(a.scrollAmount()>0,"Wheel moves screenshots");a.setScrollAmount(0);
|
||||
for(var child:s.children())if(child instanceof AbstractWidget w)check(w.getX()>=0&&w.getRight()<=s.width&&w.getY()>=0&&w.getBottom()<=s.height,"Gallery bounds "+w.getMessage());
|
||||
});
|
||||
c.takeScreenshot("photos157-gallery-"+locale+"-"+scale+(database?"-database":"-file"));
|
||||
c.runOnClient(m->m.gui.screen().onClose());
|
||||
c.runOnClient(m->{check(edit(m.gui.screen()).getValue().equals("Une journée au village"),"Cancel retains text");check(!button(m.gui.screen(),"publish").active,"Cancel leaves photo unassigned");});
|
||||
}
|
||||
c.runOnClient(m->{m.options.guiScale().set(2);m.resizeGui();m.getLanguageManager().setSelected("fr_fr");m.getLanguageManager().onResourceManagerReload(m.getResourceManager());press(m.gui.screen(),"sanctuary.community.photo_choose");});
|
||||
c.waitFor(m->widgets(m.gui.screen()).anyMatch(CommunityPhotoWidget.class::isInstance),500);
|
||||
c.runOnClient(m->press(m.gui.screen(),"sanctuary.community.next"));
|
||||
c.waitFor(m->widgets(m.gui.screen()).filter(CommunityPhotoWidget.class::isInstance).count()>0&&!button(m.gui.screen(),"next").active,500);
|
||||
c.runOnClient(m->widgets(m.gui.screen()).filter(CommunityPhotoWidget.class::isInstance).map(CommunityPhotoWidget.class::cast).findFirst().orElseThrow().onPress(enter()));
|
||||
c.takeScreenshot("photos157-preview");
|
||||
c.runOnClient(m->press(m.gui.screen(),"sanctuary.community.photo_assign"));
|
||||
c.runOnClient(m->{check(button(m.gui.screen(),"publish").active,"Photo enables publishing");check(edit(m.gui.screen()).getValue().equals("Une journée au village"),"Selection retains draft");press(m.gui.screen(),"sanctuary.community.publish");});
|
||||
c.waitFor(m->has(m.gui.screen(),"sanctuary.community.reply")&&!CommunityClient.pending(),600);
|
||||
c.runOnClient(m->check(widgets(m.gui.screen()).anyMatch(CommunityPhotoWidget.class::isInstance),"Published photo displayed"));
|
||||
c.takeScreenshot("photos157-article"+(database?"-database":"-file"));
|
||||
world.getServer().runOnServer(s->{try {
|
||||
var config=CommunityConfig.load(net.fabricmc.loader.api.FabricLoader.getInstance().getConfigDir().resolve("sanctuary-community.json"));
|
||||
CommunityStore store=database?new SqlCommunityStore(config.jdbcUrl(),config.user(),"",config.serverId()):new FileCommunityStore(s.getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT).resolve("data/sanctuary-community.json"),config.serverId());
|
||||
var posts=store.list("article","",0).entries();check(posts.size()==1,"Single publication");check(CommunityPhoto.validate(posts.getFirst().photo()).length()>8000,"Photo transported in multiple chunks and survives adapter restart");
|
||||
}catch(Exception e){throw new AssertionError(e);}});
|
||||
c.runOnClient(m->m.gui.setScreen(null));
|
||||
System.out.println("PHOTOS157_CLIENT_PASS mode="+(database?"database":"file"));
|
||||
} finally {intro.set(false);}
|
||||
}
|
||||
private static AbstractButton button(Screen screen,String key) {return widgets(screen).filter(AbstractButton.class::isInstance).map(AbstractButton.class::cast).filter(b->b.getMessage().getContents() instanceof net.minecraft.network.chat.contents.TranslatableContents t&&t.getKey().equals("sanctuary.community."+key)).findFirst().orElseThrow();}
|
||||
}
|
||||
+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);}
|
||||
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import static fr.koka.sanctuary.client.shader.PixelShadows122ClientChecks.*;
|
||||
import com.mojang.blaze3d.platform.NativeImage;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.TestServerContext;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.minecraft.client.Screenshot;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
|
||||
/** Actual source texels, native GPU masks, occlusion and user controls in an isolated world. */
|
||||
public final class Bloom130ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c){new PixelShadows122ClientChecks(true).runTest(c);}
|
||||
static final List<String> ORES=ores();
|
||||
private static List<String> ores(){var result=new ArrayList<String>();for(String name:List.of("iron","copper","redstone","lapis","diamond","emerald","ruby","sapphire","gold")){
|
||||
String ns=name.equals("ruby")||name.equals("sapphire")?"sanctuary:":"minecraft:";result.add(ns+name+"_ore");result.add(ns+"deepslate_"+name+"_ore");}
|
||||
result.add("minecraft:nether_quartz_ore");result.add("minecraft:nether_gold_ore");return List.copyOf(result);}
|
||||
static void run(ClientGameTestContext c,TestServerContext server){
|
||||
c.runOnClient(m->{preferences();VanillaLight.setSunshafts(false);VanillaLight.setLensFlare(false);VanillaLight.setPbr(false);VanillaLight.setSsgi(false);analyseTextures();VanillaLight.setSaturation(100);VanillaLight.setPixelShadows(false);VanillaLight.setEdgeHighlights(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);});
|
||||
server.runOnServer(s->{command(s,"time set 18000");
|
||||
for(int x=4;x<=20;x++)for(int z=4;z<=20;z++){
|
||||
s.overworld().setBlockAndUpdate(new BlockPos(x,64,z),Blocks.CONCRETE.black().defaultBlockState());
|
||||
for(int y=65;y<=70;y++)s.overworld().setBlockAndUpdate(new BlockPos(x,y,z),Blocks.AIR.defaultBlockState());
|
||||
}
|
||||
for(int i=0;i<ORES.size();i++){var state=BuiltInRegistries.BLOCK.getValue(Identifier.parse(ORES.get(i))).defaultBlockState();check(state.getLightEmission()==0,"Ores do not acquire server-side light: "+ORES.get(i));s.overworld().setBlockAndUpdate(orePos(i),state);}
|
||||
s.overworld().setBlockAndUpdate(new BlockPos(14,65,9),Blocks.REDSTONE_LAMP.defaultBlockState());
|
||||
});
|
||||
camera(c,server,10.5,71,9.5,60);c.waitTicks(40);c.waitFor(m->m.levelRenderer.hasRenderedAllSections(),1200);
|
||||
var off=capture(c,"bloom-ores-off");
|
||||
c.runOnClient(m->{VanillaLight.setOreEmission(false);VanillaLight.setBloom(true);});frames(c);
|
||||
check(luminous(emission(c))==0,"White/grey rock, ordinary surfaces and unlit lamps do not seed bloom");
|
||||
unchanged(off,capture(c,"bloom-ores-disabled"),"Disabling ore emission leaves the original scene intact");
|
||||
c.runOnClient(m->{VanillaLight.setOreEmission(true);VanillaLight.setOreIntensity(100);});frames(c);
|
||||
var source=emission(c);int orePixels=luminous(source);check(orePixels>100,"Actual exposed ore inclusions enter the GPU mask: "+orePixels);
|
||||
var on=capture(c,"bloom-ores-on");double oreGain=gain(off,on,null);check(oreGain>1000,"Ores visibly glow in the native night scene: "+oreGain);
|
||||
c.runOnClient(m->VanillaLight.setBloom(false));frames(c);
|
||||
var coreOnly=capture(c,"emissive-bloom-off");
|
||||
check(gain(off,coreOnly,null)>1000,"Emissive cores survive bloom OFF");
|
||||
check(gain(off,coreOnly,source)<1,"No halo outside source texels with bloom OFF");
|
||||
c.runOnClient(m->check(!Bloom.haloAllocated(),"Bloom OFF frees blur targets and uniforms"));
|
||||
c.runOnClient(m->{VanillaLight.setBloom(true);VanillaLight.setBloomIntensity(0);});frames(c);
|
||||
unchanged(coreOnly,capture(c,"emissive-bloom-zero"),"Zero bloom matches OFF while retaining emissive cores");
|
||||
c.runOnClient(m->check(!Bloom.haloAllocated(),"Zero bloom frees blur resources"));
|
||||
c.runOnClient(m->VanillaLight.setOreEmission(false));c.waitTicks(8);
|
||||
c.runOnClient(m->check(!Bloom.allocated()&&!Bloom.applied(),"Zero halo and disabled cores release everything"));
|
||||
unchanged(off,capture(c,null),"Disabling both restores the original scene");
|
||||
c.runOnClient(m->{VanillaLight.setOreEmission(true);VanillaLight.setBloomIntensity(35);});frames(c);
|
||||
unchanged(on,capture(c,null),"Re-enabling bloom restores its original image");
|
||||
c.runOnClient(m->VanillaLight.setBloomIntensity(10));frames(c);double low=gain(off,capture(c,null),source);
|
||||
c.runOnClient(m->VanillaLight.setBloomIntensity(90));frames(c);double high=gain(off,capture(c,"bloom-strength-90"),source);
|
||||
check(high>Math.max(30,low*2),"Bloom strength changes the halo outside source pixels: low="+low+" high="+high);
|
||||
c.runOnClient(m->VanillaLight.setBloomRadius(0));frames(c);var tight=capture(c,"bloom-spread-0");
|
||||
c.runOnClient(m->VanillaLight.setBloomRadius(100));frames(c);var wide=capture(c,"bloom-spread-100");
|
||||
check(difference(tight,wide)>100,"Spread changes the real GPU halo");
|
||||
c.runOnClient(m->VanillaLight.setOreIntensity(0));frames(c);check(luminous(emission(c))==0,"Zero ore strength suppresses both cores and halos");
|
||||
unchanged(off,capture(c,null),"Zero ore strength restores the source image");
|
||||
c.runOnClient(m->{VanillaLight.setOreIntensity(100);VanillaLight.setBloomIntensity(35);VanillaLight.setBloomRadius(50);});frames(c);
|
||||
server.runOnServer(s->{for(int i=0;i<ORES.size();i++)s.overworld().setBlockAndUpdate(orePos(i).above(),Blocks.STONE.defaultBlockState());});c.waitTicks(25);frames(c);
|
||||
check(luminous(emission(c))==0,"Covering veins with stone removes all emission: no through-wall ore glow");
|
||||
capture(c,"bloom-covered-ores");
|
||||
server.runOnServer(s->{for(int i=0;i<ORES.size();i++)s.overworld().setBlockAndUpdate(orePos(i).above(),Blocks.AIR.defaultBlockState());});c.waitTicks(25);frames(c);
|
||||
check(luminous(emission(c))>100,"Removing covers reveals the genuine visible inclusion masks again");
|
||||
var reload=c.computeOnClient(net.minecraft.client.Minecraft::reloadResourcePacks);c.waitFor(m->reload.isDone()&&m.gui.overlay()==null,1200);reload.join();frames(c);
|
||||
check(luminous(emission(c))>100,"Resource reload rebuilds emitter meshes and masks");
|
||||
c.runOnClient(m->VanillaLight.setOreEmission(false));
|
||||
server.runOnServer(s->s.overworld().setBlockAndUpdate(new BlockPos(14,65,9),Blocks.TORCH.defaultBlockState()));c.waitTicks(25);frames(c);
|
||||
int torch=luminous(emission(c));check(torch>3,"A native torch seeds bloom: "+torch);capture(c,"bloom-torch");
|
||||
server.runOnServer(s->s.overworld().setBlockAndUpdate(new BlockPos(14,65,9),Blocks.GLOWSTONE.defaultBlockState()));c.waitTicks(25);frames(c);
|
||||
check(luminous(emission(c))>torch,"A full luminous block contributes its real textured surface");capture(c,"bloom-glowstone");
|
||||
server.runOnServer(s->s.overworld().setBlockAndUpdate(new BlockPos(14,65,9),Blocks.STONE.defaultBlockState()));c.waitTicks(25);frames(c);
|
||||
check(luminous(emission(c))==0,"Replacing the light source removes its cached emission");
|
||||
server.runOnServer(s->{s.overworld().setBlockAndUpdate(new BlockPos(14,65,9),Blocks.REDSTONE_LAMP.defaultBlockState());s.overworld().setBlockAndUpdate(new BlockPos(14,64,9),Blocks.REDSTONE_BLOCK.defaultBlockState());});
|
||||
c.waitTicks(25);frames(c);check(luminous(emission(c))>10,"Powered lamp emits through its on-state texture");capture(c,"bloom-lamp-on");
|
||||
server.runOnServer(s->s.overworld().setBlockAndUpdate(new BlockPos(14,64,9),Blocks.CONCRETE.black().defaultBlockState()));
|
||||
c.waitTicks(25);frames(c);check(luminous(emission(c))==0,"Unpowered lamp loses its glow after its native state update");
|
||||
server.runOnServer(s->s.overworld().setBlockAndUpdate(new BlockPos(14,65,9),Blocks.LAVA.defaultBlockState()));
|
||||
c.waitTicks(25);frames(c);check(luminous(emission(c))>10,"Native visible lava seeds bloom");capture(c,"bloom-lava");
|
||||
server.runOnServer(s->command(s,"time set 3000"));camera(c,server,10.5,90,9.5,70);
|
||||
var direction=c.computeOnClient(m->PixelShadows.sunDirection(m.gameRenderer.gameRenderState().levelRenderState.skyRenderState.sunAngle));
|
||||
look(c,server,(float)Math.toDegrees(Math.atan2(-direction.x,direction.z)),-(float)Math.toDegrees(Math.asin(direction.y)));
|
||||
c.runOnClient(m->VanillaLight.setBloom(false));c.waitTicks(8);var sunOff=capture(c,"bloom-sun-off");
|
||||
c.runOnClient(m->VanillaLight.setBloom(true));frames(c);var sunMask=emission(c);
|
||||
check(luminous(sunMask)>40,"The actual sun disc seeds bloom");
|
||||
double sunlight=gain(sunOff,capture(c,"bloom-sun-on"),sunMask);check(sunlight>100,"Sun halo extends beyond its source disc: "+sunlight);
|
||||
server.runOnServer(s->{var eye=s.getPlayerList().getPlayers().getFirst().getEyePosition();var center=BlockPos.containing(eye.x+direction.x*8,eye.y+direction.y*8,eye.z+direction.z*8);
|
||||
for(int x=-4;x<=4;x++)for(int y=-4;y<=4;y++)for(int z=-4;z<=4;z++)s.overworld().setBlockAndUpdate(center.offset(x,y,z),Blocks.STONE.defaultBlockState());});
|
||||
c.waitTicks(30);frames(c);check(luminous(emission(c))==0,"Solid terrain in front of the sun suppresses its bloom source");capture(c,"bloom-sun-occluded");
|
||||
c.runOnClient(m->VanillaLight.setBloom(false));c.waitTicks(6);var coveredOff=capture(c,null);
|
||||
c.runOnClient(m->{VanillaLight.setBloom(true);VanillaLight.setBloomIntensity(0);});c.waitTicks(6);c.runOnClient(m->check(!Bloom.applied()&&!Bloom.allocated(),"Zero bloom releases GPU resources"));
|
||||
unchanged(coveredOff,capture(c,null),"Zero bloom leaves no residual glow");
|
||||
c.runOnClient(m->{VanillaLight.setBloomIntensity(35);VanillaLight.setBloom(false);VanillaLight.setOreEmission(true);VanillaLight.setOreIntensity(50);});
|
||||
System.out.println("EMISSIVE131_PASS independent cores with halo OFF/zero; blur resources released; defaults; masks for 20 ore textures; selective night glow; halo intensity/spread; no through-wall glow; live edits; torch/glowstone/lamp/lava/sun; resource reload; defaults/migration; orePixels="+orePixels+" oreGain="+oreGain+" haloLow="+low+" haloHigh="+high+" sunHalo="+sunlight);
|
||||
LensFlare136ClientChecks.run(c,server);
|
||||
}
|
||||
private static BlockPos orePos(int i){return new BlockPos(8+i%5,65,8+i/5);}
|
||||
private static void preferences(){try{
|
||||
var path=FabricLoader.getInstance().getConfigDir().resolve("sanctuary-shaders.json");Files.deleteIfExists(path);forgetPreferences();
|
||||
check(VanillaLight.pixelShadows()&&VanillaLight.shadowDistance()==128&&VanillaLight.shadowPixels()==16&&VanillaLight.edgeDistance()==32,"Fresh requested defaults: shadows ON, 128; highlights 32; shared precision 16");
|
||||
check(VanillaLight.bloom()&&VanillaLight.oreEmission()&&VanillaLight.edgeHighlights()&&VanillaLight.bloomIntensity()==20&&VanillaLight.bloomRadius()==50,"Bloom, ore emission and edge highlights default to ON");
|
||||
Files.writeString(path,"{\"bloom\":false,\"oreEmission\":false,\"edgeHighlights\":false,\"pixelShadows\":false,\"shadowDistance\":64,\"shadowPixels\":8,\"edgeDistance\":16}");forgetPreferences();
|
||||
check(!VanillaLight.bloom()&&!VanillaLight.oreEmission()&&!VanillaLight.edgeHighlights()&&!VanillaLight.pixelShadows()&&VanillaLight.shadowDistance()==64&&VanillaLight.shadowPixels()==8&&VanillaLight.edgeDistance()==16,"Existing user choices are retained");
|
||||
for(int n:new int[]{-10,0,35,100,400}){VanillaLight.setBloomIntensity(n);VanillaLight.setBloomRadius(n);VanillaLight.setOreIntensity(n);forgetPreferences();check(VanillaLight.bloomIntensity()==Math.clamp(n,0,100)&&VanillaLight.bloomRadius()==Math.clamp(n,0,100)&&VanillaLight.oreIntensity()==Math.clamp(n,0,100),"Persisted bloom controls are bounded");}
|
||||
VanillaLight.setBloomIntensity(35);VanillaLight.setBloomRadius(50);VanillaLight.setOreIntensity(50);VanillaLight.setShadowPixels(16);
|
||||
}catch(java.io.IOException e){throw new AssertionError(e);}}
|
||||
private static void analyseTextures(){
|
||||
var report=new StringBuilder("texture,selected,total,neutral_rock_selected\n");
|
||||
try(var materials=new BloomMaterials();var sheet=new NativeImage(5*16*6,4*32*6,false)){
|
||||
sheet.fillRect(0,0,sheet.getWidth(),sheet.getHeight(),0xFF16191E);
|
||||
for(int index=0;index<ORES.size();index++){
|
||||
var block=Identifier.parse(ORES.get(index));var id=block.withPath("block/"+block.getPath());
|
||||
try(var image=BloomMaterials.read(id)){
|
||||
int w=image.getWidth(),h=image.getHeight();int[] pixels=new int[w*h];for(int y=0;y<h;y++)for(int x=0;x<w;x++)pixels[y*w+x]=image.getPixel(x,y);
|
||||
var mask=materials.mask(id,pixels,w,h,true,false);int count=0,grey=0;
|
||||
for(int i=0;i<mask.length;i++){if(mask[i]>0){count++;int c=pixels[i],r=c>>16&255,g=c>>8&255,b=c&255;if(r==g&&g==b&&r<215)grey++;}}
|
||||
check(count>=4&&count<w*h*.7,"Texture has a selective inclusion mask: "+id+" "+count+"/"+mask.length);
|
||||
if(!id.getPath().contains("quartz"))check(grey==0,"Neutral host rock never emits: "+id);
|
||||
report.append(id).append(',').append(count).append(',').append(mask.length).append(',').append(grey).append('\n');
|
||||
for(int y=0;y<32*6;y++)for(int x=0;x<16*6;x++){
|
||||
int source=(y/6%16)*w/16*w+(x/6)*w/16;int color=pixels[source];if(y>=16*6&&mask[source]==0)color=0xFF16191E;
|
||||
sheet.setPixel((index%5)*16*6+x,(index/5)*32*6+y,color);
|
||||
}
|
||||
}
|
||||
}
|
||||
var root=FabricLoader.getInstance().getGameDir();sheet.writeToFile(root.resolve("bloom130-ore-masks.png"));Files.writeString(root.resolve("bloom130-ore-masks.csv"),report.toString());
|
||||
System.out.println("BLOOM130_MASKS\n"+report);
|
||||
}catch(java.io.IOException e){throw new AssertionError(e);}
|
||||
}
|
||||
private static void frames(ClientGameTestContext c){int before=c.computeOnClient(m->Bloom.renderedFrames());c.waitFor(m->Bloom.applied()&&Bloom.ready()&&Bloom.renderedFrames()>before+10,1200);c.waitTicks(8);}
|
||||
private static Frame emission(ClientGameTestContext c){var result=new CompletableFuture<Frame>();c.runOnClient(m->Screenshot.takeScreenshot(Bloom.emissionTarget(),image->{try(image){result.complete(new Frame(image.getWidth(),image.getHeight(),image.getPixels()));}}));c.waitFor(m->result.isDone(),200);return result.join();}
|
||||
private static int luminous(Frame frame){int count=0;for(int color:frame.pixels())if((color&0xFFFFFF)!=0)count++;return count;}
|
||||
private static double gain(Frame off,Frame on,Frame source){double result=0;for(int i=0;i<off.pixels().length;i++)if(source==null||(source.pixels()[i]&0xFFFFFF)==0)result+=Math.max(0,brightness(on.pixels()[i])-brightness(off.pixels()[i]));return result;}
|
||||
private static int difference(Frame a,Frame b){int n=0;for(int i=0;i<a.pixels().length;i++)if(channelDifference(a.pixels()[i],b.pixels()[i])>3)n++;return n;}
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import static fr.koka.sanctuary.client.shader.PixelShadows122ClientChecks.*;
|
||||
import java.nio.file.Files;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.TestServerContext;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.network.chat.Component;
|
||||
|
||||
/** Fresh native world: offscreen sources, true voxel occlusion and real GPU tint. */
|
||||
public final class ColoredLights139ClientChecks implements FabricClientGameTest {
|
||||
private static final BlockPos SOURCE=new BlockPos(98,101,102),PROBE=new BlockPos(99,101,106);
|
||||
public void runTest(ClientGameTestContext c){
|
||||
System.setProperty("sanctuary.test.freezeBlockFlicker","true");
|
||||
try{new PixelShadows122ClientChecks(false,false,false,false,false,true).runTest(c);}
|
||||
finally{System.clearProperty("sanctuary.test.freezeBlockFlicker");}
|
||||
}
|
||||
static void run(ClientGameTestContext c,TestServerContext server){
|
||||
c.runOnClient(m->{preferences();VanillaLight.setWarmTorches(true);m.options.particles().set(net.minecraft.server.level.ParticleStatus.MINIMAL);VanillaLight.setEnabled(true);VanillaLight.setSaturation(100);VanillaLight.setPbr(false);VanillaLight.setSsgi(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);VanillaLight.setLensFlare(false);VanillaLight.setEdgeHighlights(false);VanillaLight.setPixelShadows(false);VanillaLight.setSunshafts(false);VanillaLight.setColoredLights(false);VanillaLight.setColoredLightIntensity(35);});
|
||||
camera(c,server,100.5,101,104.5,70);c.waitTicks(30);
|
||||
server.runOnServer(s->{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||y==109||x==88||x==112||z==92||z==116)?Blocks.CONCRETE.white().defaultBlockState():Blocks.AIR.defaultBlockState());
|
||||
});
|
||||
look(c,server,0,35);c.waitTicks(30);
|
||||
c.waitFor(m->m.level.getBrightness(net.minecraft.world.level.LightLayer.SKY,PROBE)==0,1200);
|
||||
long warm=0;
|
||||
for(var block:new Block[]{Blocks.TORCH,Blocks.SOUL_TORCH,Blocks.PEARLESCENT_FROGLIGHT,Blocks.VERDANT_FROGLIGHT,Blocks.OCHRE_FROGLIGHT,Blocks.GLOWSTONE}){
|
||||
source(c,server,block);enable(c,false);var off=capture(c,"colored-"+name(block)+"-off");enable(c,true);var on=capture(c,"colored-"+name(block)+"-35");
|
||||
int color=c.computeOnClient(m->ColoredLights.sample(PROBE));long delta=difference(off,on);
|
||||
check(delta>250,"Offscreen source visibly tints native surfaces: "+name(block)+" "+delta);
|
||||
int r=color>>16&255,g=color>>8&255,b=color&255;
|
||||
if(block==Blocks.TORCH){check(r>g&&g>b,"Torch is warm");warm=delta;}
|
||||
if(block==Blocks.SOUL_TORCH)check(b>g&&g>r,"Soul source is blue");
|
||||
if(block==Blocks.SOUL_TORCH)for(int step=1;step<=9;step++){
|
||||
final int distance=step;int sample=c.computeOnClient(m->ColoredLights.sample(SOURCE.offset(distance,0,0)));
|
||||
int sr=sample>>16&255,sg=sample>>8&255,sb=sample&255;
|
||||
double rr=sr==0?0:Math.pow(.8,(sb-sr)/8.0),gg=sg==0?0:Math.pow(.8,(sb-sg)/8.0);
|
||||
check(Math.abs(rr-85/255.)<.02&&Math.abs(gg-189/255.)<.02,"Blue hue remains consistent at native distance "+step+" "+rr+" / "+gg);
|
||||
}
|
||||
if(block==Blocks.VERDANT_FROGLIGHT)check(g>r&&g>b,"Verdant froglight is green");
|
||||
if(block==Blocks.PEARLESCENT_FROGLIGHT)check(r>g&&b>g,"Pearlescent froglight is pink");
|
||||
if(block==Blocks.OCHRE_FROGLIGHT)check(r>g&&g>b,"Ochre froglight is warm");
|
||||
if(block==Blocks.GLOWSTONE)check(r>b,"Other emitters derive their colour from the native texture");
|
||||
System.out.println("COLORED139_SOURCE "+name(block)+" sample="+Integer.toHexString(color)+" delta="+delta);
|
||||
}
|
||||
source(c,server,Blocks.TORCH);enable(c,false);var base=capture(c,null);enable(c,true);var low=capture(c,null);
|
||||
c.runOnClient(m->VanillaLight.setColoredLightIntensity(100));settle(c);var strong=capture(c,"colored-torch-100");check(difference(base,strong)>difference(base,low)*1.5,"Intensity increases colour, without changing native light");
|
||||
int builds=c.computeOnClient(m->ColoredLights.builds());c.waitTicks(25);check(c.computeOnClient(m->ColoredLights.builds())==builds,"Stationary volume is cached");unchanged(strong,capture(c,null),"Static light is stable");
|
||||
camera(c,server,104.5,101,104.5,70);look(c,server,0,35);settle(c);capture(c,"colored-moved");
|
||||
camera(c,server,100.5,101,104.5,70);look(c,server,0,35);settle(c);unchanged(strong,capture(c,"colored-returned"),"Moving and returning preserves world-anchored light");
|
||||
// A complete divider closes the room; light may not take a path around its edges.
|
||||
server.runOnServer(s->{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(35);settle(c);
|
||||
check((c.computeOnClient(m->ColoredLights.sample(new BlockPos(100,101,106)))&0xffffff)==0,"Opaque wall blocks colour propagation");
|
||||
enable(c,false);var sealedOff=capture(c,null);enable(c,true);unchanged(sealedOff,capture(c,"colored-sealed"),"No coloured light crosses a sealed wall");
|
||||
server.runOnServer(s->{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());});c.waitTicks(35);settle(c);
|
||||
check((c.computeOnClient(m->ColoredLights.sample(PROBE))&0xffffff)!=0,"Opening the wall restores light");
|
||||
server.runOnServer(s->{s.overworld().setBlockAndUpdate(SOURCE,Blocks.REDSTONE_LAMP.defaultBlockState());s.overworld().setBlockAndUpdate(SOURCE.below(),Blocks.REDSTONE_BLOCK.defaultBlockState());});c.waitTicks(35);settle(c);
|
||||
int red=c.computeOnClient(m->ColoredLights.sample(PROBE));check((red>>16&255)>(red>>8&255)&& (red>>16&255)>(red&255),"Powered redstone source is red");capture(c,"colored-redstone-on");
|
||||
server.runOnServer(s->s.overworld().setBlockAndUpdate(SOURCE.below(),Blocks.CONCRETE.white().defaultBlockState()));c.waitTicks(35);settle(c);
|
||||
check((c.computeOnClient(m->ColoredLights.sample(PROBE))&0xffffff)==0,"Unpowered lamp removes cached coloured light");
|
||||
source(c,server,Blocks.SOUL_TORCH);var beforeReload=capture(c,null);
|
||||
var reload=c.computeOnClient(net.minecraft.client.Minecraft::reloadResourcePacks);c.waitFor(m->reload.isDone()&&m.gui.overlay()==null,1200);reload.join();settle(c);unchanged(beforeReload,capture(c,"colored-reloaded"),"Resource reload rebuilds materials and light field");
|
||||
source(c,server,Blocks.AIR);check((c.computeOnClient(m->ColoredLights.sample(PROBE))&0xffffff)==0,"Removing the source clears its light");
|
||||
source(c,server,Blocks.SOUL_TORCH);enable(c,false);var off=capture(c,null);c.runOnClient(m->{VanillaLight.setColoredLights(true);VanillaLight.setColoredLightIntensity(0);});c.waitTicks(10);
|
||||
c.runOnClient(m->check(!ColoredLights.allocated(),"Zero releases volume and GPU resources"));unchanged(off,capture(c,null),"Zero is identical to OFF");
|
||||
c.runOnClient(m->{for(String locale:new String[]{"fr_fr","en_us"}){m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());for(String key:new String[]{"colored_lights","colored_lights_help","colored_light_intensity","colored_light_intensity_help"}){String full="sanctuary.shaders."+key;check(!Component.translatable(full).getString().equals(full),"Translated "+locale+" "+full);}}});
|
||||
c.runOnClient(m->{VanillaLight.setColoredLightIntensity(35);VanillaLight.setPbr(true);VanillaLight.setSsgi(true);VanillaLight.setBloom(true);VanillaLight.setBloomIntensity(20);VanillaLight.setEdgeHighlights(true);VanillaLight.setPixelShadows(true);VanillaLight.setSunshafts(true);VanillaLight.setLensFlare(true);VanillaLight.setOreEmission(true);});
|
||||
settle(c);c.waitFor(m->ProceduralPbr.applied()&&Bloom.applied(),1200);c.waitTicks(30);capture(c,"colored-with-shader-effects");
|
||||
System.out.println("COLORED139_PASS warm="+warm+" offscreen sources, hues, native textured materials, intensity, cache, camera, walls, lamp updates, removal, reload, zero/OFF and FR/EN");
|
||||
}
|
||||
private static String name(Block b){return net.minecraft.core.registries.BuiltInRegistries.BLOCK.getKey(b).getPath();}
|
||||
private static void source(ClientGameTestContext c,TestServerContext s,Block b){s.runOnServer(server->server.overworld().setBlockAndUpdate(SOURCE,b.defaultBlockState()));c.waitTicks(35);if(c.computeOnClient(m->VanillaLight.coloredLights()))settle(c);}
|
||||
private static void enable(ClientGameTestContext c,boolean value){c.runOnClient(m->VanillaLight.setColoredLights(value));if(value)settle(c);else c.waitTicks(8);}
|
||||
private static void settle(ClientGameTestContext c){int before=c.computeOnClient(m->ColoredLights.renderedFrames());c.waitFor(m->ColoredLights.applied()&&ColoredLights.settled()&&ColoredLights.renderedFrames()>before+8,1600);c.waitTicks(8);}
|
||||
private static long difference(Frame a,Frame b){long sum=0;for(int i=0;i<a.pixels().length;i++)sum+=channelDifference(a.pixels()[i],b.pixels()[i]);return sum;}
|
||||
private static void preferences(){try{
|
||||
var path=FabricLoader.getInstance().getConfigDir().resolve("sanctuary-shaders.json");Files.deleteIfExists(path);forgetPreferences();check(VanillaLight.coloredLights()&&VanillaLight.coloredLightIntensity()==30,"New defaults are enabled at 30 percent");
|
||||
Files.writeString(path,"{\"bloomIntensity\":20,\"ssgi\":false}");forgetPreferences();check(VanillaLight.coloredLights()&&VanillaLight.coloredLightIntensity()==30&&!VanillaLight.ssgi()&&VanillaLight.bloomIntensity()==20,"Old preferences keep their choices");
|
||||
for(int n:new int[]{-1,0,35,100,400}){VanillaLight.setColoredLights(true);VanillaLight.setColoredLightIntensity(n);forgetPreferences();check(VanillaLight.coloredLights()&&VanillaLight.coloredLightIntensity()==Math.clamp(n,0,100),"Bounded persistent controls");}
|
||||
}catch(java.io.IOException e){throw new AssertionError(e);}}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import static fr.koka.sanctuary.client.shader.PixelShadows122ClientChecks.*;
|
||||
import fr.koka.sanctuary.client.DynamicLightClient;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import fr.koka.sanctuary.cosmetics.AccessoryService;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.*;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.entity.EquipmentSlot;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.*;
|
||||
import net.minecraft.network.chat.Component;
|
||||
|
||||
final class ColoredLights140ClientChecks {
|
||||
private static final BlockPos SOURCE=new BlockPos(98,101,102),PROBE=new BlockPos(99,101,106);
|
||||
static void run(ClientGameTestContext c,TestServerContext server){
|
||||
c.runOnClient(m->{VanillaLight.setPbr(false);VanillaLight.setSsgi(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);VanillaLight.setLensFlare(false);VanillaLight.setEdgeHighlights(false);VanillaLight.setPixelShadows(false);VanillaLight.setSunshafts(false);VanillaLight.setColoredLightIntensity(30);VanillaLight.setWarmTorches(false);VanillaLight.setColoredLightDistance(16);});
|
||||
source(c,server,Blocks.TORCH);enable(c,false);var nativeTorch=capture(c,"torch-native-off");enable(c,true);unchanged(nativeTorch,capture(c,"torch-native-on"),"Normal torch preserves vanilla colour");
|
||||
c.runOnClient(m->VanillaLight.setWarmTorches(true));settle(c);var warm=capture(c,"torch-warm-30");long accent=difference(nativeTorch,warm);check(accent>2_025_577,"30 percent exceeds beta139 warm tint at 35 percent: "+accent);
|
||||
c.runOnClient(m->VanillaLight.setWarmTorches(false));settle(c);unchanged(nativeTorch,capture(c,null),"Warm torch option can restore native tint");
|
||||
for(String name:new String[]{"copper_torch","copper_lantern"}){
|
||||
var copper=BuiltInRegistries.BLOCK.getValue(Identifier.parse("minecraft:"+name));check(copper!=Blocks.AIR&&copper.defaultBlockState().getLightEmission()>0,"Native copper source exists "+name);
|
||||
source(c,server,copper);int value=c.computeOnClient(m->ColoredLights.sample(PROBE));check((value>>8&255)>(value>>16&255)&&(value>>8&255)>(value&255),"Copper emits green");capture(c,name+"-green");
|
||||
}
|
||||
server.runOnServer(s->{s.overworld().setBlockAndUpdate(SOURCE,Blocks.REDSTONE_LAMP.defaultBlockState());s.overworld().setBlockAndUpdate(SOURCE.below(),Blocks.REDSTONE_BLOCK.defaultBlockState());});c.waitTicks(40);settle(c);
|
||||
int red=c.computeOnClient(m->ColoredLights.sample(PROBE));check(Math.pow(.8,((red>>16&255)-(red>>8&255))/8.)<.04,"Redstone uses saturated red");capture(c,"redstone-red-30");
|
||||
source(c,server,Blocks.AIR);server.runOnServer(s->s.overworld().setBlockAndUpdate(SOURCE.below(),Blocks.CONCRETE.white().defaultBlockState()));
|
||||
// Actual carried sources, synchronized through native equipment and accessory packets.
|
||||
server.runOnServer(s->{command(s,"gamemode creative @a");var p=s.getPlayerList().getPlayers().getFirst();p.setItemSlot(EquipmentSlot.MAINHAND,new ItemStack(Items.SOUL_TORCH));});
|
||||
c.waitFor(m->DynamicLightClient.snapshot(m.level).size()==1,200);c.waitTicks(45);enable(c,false);var carriedOff=capture(c,"held-soul-off");enable(c,true);c.waitTicks(25);var carriedOn=capture(c,"held-soul-blue");check(difference(carriedOff,carriedOn)>1000&&c.computeOnClient(m->ColoredLights.dynamicCount())==1,"Held soul torch is visibly blue");
|
||||
c.runOnClient(m->{var l=DynamicLightClient.snapshot(m.level).sources().iterator().next();check((l.color()&255)>(l.color()>>16&255),"Dynamic source has actual blue colour");});
|
||||
c.runOnClient(m->m.options.setCameraType(net.minecraft.client.CameraType.THIRD_PERSON_BACK));c.waitTicks(25);
|
||||
c.runOnClient(m->{var pos=ColoredLights.dynamicPosition(m.player.getId());check(pos!=null&&pos.distanceToSqr(m.player.getEyePosition())<1,"Third-person light follows player, not camera");});capture(c,"held-third-person");
|
||||
c.runOnClient(m->m.options.setCameraType(net.minecraft.client.CameraType.FIRST_PERSON));c.waitTicks(20);settle(c);
|
||||
int builds=c.computeOnClient(m->ColoredLights.builds());
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.setItemSlot(EquipmentSlot.MAINHAND,ItemStack.EMPTY);p.setItemSlot(EquipmentSlot.OFFHAND,new ItemStack(Items.SOUL_TORCH));});c.waitTicks(35);settle(c);check(c.computeOnClient(m->ColoredLights.builds())==builds,"Moving a light between hands does not rebuild static volume");capture(c,"offhand-soul-blue");
|
||||
var copperItem=BuiltInRegistries.ITEM.getValue(Identifier.parse("minecraft:copper_torch"));
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.setItemSlot(EquipmentSlot.OFFHAND,ItemStack.EMPTY);AccessoryAccess.of(p).setItem(AccessoryContainer.HEAD,new ItemStack(copperItem));AccessoryService.publish(p);});c.waitTicks(45);settle(c);
|
||||
c.runOnClient(m->{var l=DynamicLightClient.snapshot(m.level).sources().iterator().next();check((l.color()>>8&255)>(l.color()>>16&255)&&(l.color()>>8&255)>(l.color()&255),"Head cosmetic carries copper green");});capture(c,"head-copper-green");check(c.computeOnClient(m->ColoredLights.builds())==builds,"Dynamic colour updates preserve static cache");
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();AccessoryAccess.of(p).setItem(AccessoryContainer.HEAD,ItemStack.EMPTY);AccessoryService.publish(p);});c.waitFor(m->DynamicLightClient.snapshot(m.level).size()==0&&ColoredLights.dynamicCount()==0,200);c.waitTicks(25);
|
||||
server.runOnServer(s->command(s,"gamemode spectator @a"));
|
||||
camera(c,server,100.5,108,104.5,70);look(c,server,0,18);c.waitTicks(25);
|
||||
server.runOnServer(s->{for(int x=88;x<=112;x++)for(int z=117;z<=155;z++)s.overworld().setBlockAndUpdate(new BlockPos(x,100,z),Blocks.CONCRETE.white().defaultBlockState());
|
||||
for(int x=88;x<=112;x++)for(int y=101;y<=109;y++)s.overworld().setBlockAndUpdate(new BlockPos(x,y,116),Blocks.AIR.defaultBlockState());
|
||||
s.overworld().setBlockAndUpdate(new BlockPos(100,101,140),Blocks.VERDANT_FROGLIGHT.defaultBlockState());});c.waitTicks(50);
|
||||
c.runOnClient(m->VanillaLight.setColoredLightDistance(8));settle(c);var shortRange=capture(c,"distance-8");int small=c.computeOnClient(m->ColoredLights.volumeSide());check(c.computeOnClient(m->ColoredLights.sample(new BlockPos(100,101,141)))==0,"Far field omitted at short distance");
|
||||
int before=c.computeOnClient(m->ColoredLights.builds());c.runOnClient(m->VanillaLight.setColoredLightDistance(64));c.waitFor(m->ColoredLights.builds()>before&&ColoredLights.blend()>0&&ColoredLights.blend()<1,1600);var transition=capture(c,"distance-fade");settle(c);var longRange=capture(c,"distance-64");
|
||||
check(c.computeOnClient(m->ColoredLights.volumeSide())>small,"Distance changes actual field allocation");check((c.computeOnClient(m->ColoredLights.sample(new BlockPos(100,101,141)))&0xffffff)!=0,"Far light retained at long distance");check(difference(shortRange,longRange)>1000,"Far light is visibly rendered");check(difference(transition,longRange)>100&&difference(shortRange,transition)<difference(shortRange,longRange),"Transition is an intermediate rendered frame without overshoot");
|
||||
for(int distance:new int[]{8,16,32,64}){c.runOnClient(m->{VanillaLight.setColoredLightDistance(distance);forgetPreferences();check(VanillaLight.coloredLightDistance()==distance,"Persisted distance "+distance);});settle(c);}
|
||||
c.runOnClient(m->{for(String locale:new String[]{"fr_fr","en_us"}){m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());for(String key:new String[]{"warm_torches","warm_torches_help","colored_light_distance","colored_light_distance_help","colored_light_distance_value"}){String full="sanctuary.shaders."+key;check(!Component.translatable(full,16).getString().equals(full),"Translated "+locale+" "+key);}}});
|
||||
System.out.println("COLORED140_PASS accent="+accent+" native/warm torches, copper green, saturated red, main/offhand/head, dynamic cache, 8/16/32/64 distance, fade and FR/EN");
|
||||
}
|
||||
private static void source(ClientGameTestContext c,TestServerContext s,Block b){s.runOnServer(server->server.overworld().setBlockAndUpdate(SOURCE,b.defaultBlockState()));c.waitTicks(35);settle(c);}
|
||||
private static void enable(ClientGameTestContext c,boolean value){c.runOnClient(m->VanillaLight.setColoredLights(value));if(value)settle(c);else c.waitTicks(10);}
|
||||
private static void settle(ClientGameTestContext c){c.waitFor(m->ColoredLights.applied()&&ColoredLights.settled(),2000);c.waitTicks(12);}
|
||||
private static long difference(Frame a,Frame b){long total=0;for(int i=0;i<a.pixels().length;i++)total+=channelDifference(a.pixels()[i],b.pixels()[i]);return total;}
|
||||
}
|
||||
+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;}
|
||||
}
|
||||
+17
-1
@@ -35,6 +35,19 @@ final class EdgeHighlights129ClientChecks {
|
||||
check(low.brighter>100 && low.stable>low.samples*.55 && low.darker<low.samples*.005,
|
||||
"Highlight brightens local strips, preserves face interiors: "+low);
|
||||
grid(c,off,on);
|
||||
c.runOnClient(m -> VanillaLight.setShadowPixels(8));frames(c);
|
||||
var coarse=measure(off,capture(c,"edges-shared-precision-8"));
|
||||
c.runOnClient(m -> VanillaLight.setShadowPixels(32));frames(c);
|
||||
var fine=measure(off,capture(c,"edges-shared-precision-32"));
|
||||
check(coarse.brighter()>low.brighter()*1.4 && fine.brighter()<low.brighter()*.8,
|
||||
"One precision setting controls the visible highlight width: coarse="+coarse+" middle="+low+" fine="+fine);
|
||||
for(int pixels:new int[]{64,128}){
|
||||
c.runOnClient(m -> VanillaLight.setShadowPixels(pixels));frames(c);
|
||||
var finer=measure(off,capture(c,"edges-shared-precision-"+pixels));
|
||||
check(finer.meanGain()<fine.meanGain() && finer.darker()==0,"High precision refines CTM without darkening: "+pixels+" "+finer);
|
||||
}
|
||||
c.runOnClient(m -> VanillaLight.setShadowPixels(16));frames(c);
|
||||
|
||||
for (int distance : new int[]{16,32,64,128,256}) {
|
||||
c.runOnClient(m -> VanillaLight.setEdgeDistance(distance));
|
||||
frames(c);
|
||||
@@ -131,7 +144,7 @@ final class EdgeHighlights129ClientChecks {
|
||||
// Real prior-version JSON, without either new field. Preserve its existing settings.
|
||||
Files.writeString(path,"{\"enabled\":true,\"saturation\":100,\"pixelShadows\":true,\"shadowPixels\":16,\"shadowDistance\":32}");
|
||||
forgetPreferences();
|
||||
check(!VanillaLight.edgeHighlights() && VanillaLight.edgeIntensity()==30 && VanillaLight.edgeDistance()==32,"Old configuration defaults highlights OFF at intensity 30");
|
||||
check(VanillaLight.edgeHighlights() && VanillaLight.edgeIntensity()==30 && VanillaLight.edgeDistance()==32,"Missing preference uses the new highlight ON default at intensity 30");
|
||||
check(VanillaLight.pixelShadows() && VanillaLight.saturation()==100,"Migration retains existing shader choices");
|
||||
for(int value:new int[]{-100,0,30,100,1000}) {
|
||||
VanillaLight.setEdgeIntensity(value);forgetPreferences();
|
||||
@@ -146,6 +159,9 @@ final class EdgeHighlights129ClientChecks {
|
||||
VanillaLight.setEdgeHighlights(true);forgetPreferences();
|
||||
check(VanillaLight.edgeHighlights(),"Toggle persists independently");
|
||||
VanillaLight.setEdgeHighlights(false);
|
||||
VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);
|
||||
VanillaLight.setSunshafts(false);VanillaLight.setLensFlare(false);
|
||||
VanillaLight.setPbr(false);VanillaLight.setSsgi(false);
|
||||
} catch(java.io.IOException e) { throw new AssertionError(e); }
|
||||
}
|
||||
|
||||
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import static fr.koka.sanctuary.client.shader.PixelShadows122ClientChecks.*;
|
||||
import java.nio.file.Files;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.TestServerContext;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.network.chat.Component;
|
||||
|
||||
/** Native sky image, not a generated mockup; explicit independent effect controls. */
|
||||
final class LensFlare136ClientChecks {
|
||||
static void run(ClientGameTestContext c,TestServerContext server){
|
||||
c.runOnClient(m->{preferences();VanillaLight.setEnabled(true);VanillaLight.setSaturation(100);VanillaLight.setPbr(false);VanillaLight.setSsgi(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);VanillaLight.setPixelShadows(false);VanillaLight.setEdgeHighlights(false);VanillaLight.setSunshafts(false);VanillaLight.setLensFlare(false);});
|
||||
server.runOnServer(s->{command(s,"time add 1");command(s,"time set 3000");});
|
||||
camera(c,server,100.5,130,100.5,70);aim(c,server,false);
|
||||
var off=capture(c,"flare-sun-off");enable(c,true);var on=capture(c,"flare-sun-30");
|
||||
long low=gain(off,on);check(low>1000,"Default sun flare is visible: "+low);
|
||||
c.runOnClient(m->check(!Bloom.haloAllocated()&&Bloom.flareAllocated(),"Flare runs without bloom blur targets"));
|
||||
unchanged(on,capture(c,null),"Static rectangular flare has no animated noise");
|
||||
c.runOnClient(m->VanillaLight.setLensFlareIntensity(80));frames(c);var high=capture(c,"flare-sun-80");
|
||||
check(gain(off,high)>low*1.5,"Flare intensity changes native pixels");
|
||||
var reload=c.computeOnClient(net.minecraft.client.Minecraft::reloadResourcePacks);c.waitFor(m->reload.isDone()&&m.gui.overlay()==null,1200);reload.join();frames(c);
|
||||
unchanged(high,capture(c,"flare-reloaded"),"Flare recovers after resource reload");
|
||||
int width=c.computeOnClient(m->m.getWindow().getScreenWidth()),height=c.computeOnClient(m->m.getWindow().getScreenHeight());
|
||||
c.runOnClient(m->m.getWindow().setWindowed(961,541));c.waitFor(m->m.gameRenderer.mainRenderTarget().width!=off.width(),200);frames(c);
|
||||
enable(c,false);var smallOff=capture(c,null);enable(c,true);check(gain(smallOff,capture(c,"flare-resized"))>1000,"Flare survives odd-sized targets");
|
||||
c.runOnClient(m->m.getWindow().setWindowed(width,height));c.waitFor(m->m.gameRenderer.mainRenderTarget().width==off.width(),200);frames(c);
|
||||
var direction=c.computeOnClient(m->PixelShadows.sunDirection(m.gameRenderer.gameRenderState().levelRenderState.skyRenderState.sunAngle));
|
||||
var eye=c.computeOnClient(m->m.player.getEyePosition());var center=BlockPos.containing(eye.x+direction.x*8,eye.y+direction.y*8,eye.z+direction.z*8);
|
||||
cover(server,center,true);c.waitTicks(35);frames(c);var hiddenOn=capture(c,"flare-sun-covered");enable(c,false);
|
||||
unchanged(hiddenOn,capture(c,null),"Occluded sun produces no rectangular flare through a wall");
|
||||
cover(server,center,false);c.waitTicks(35);aim(c,server,false);
|
||||
c.runOnClient(m->{VanillaLight.setBloom(true);VanillaLight.setBloomIntensity(20);VanillaLight.setLensFlareIntensity(30);});enable(c,true);capture(c,"flare-bloom-combined");
|
||||
c.runOnClient(m->VanillaLight.setBloom(false));look(c,server,0,80);enable(c,false);var away=capture(c,null);enable(c,true);unchanged(away,capture(c,"flare-away"),"Behind-camera celestial sources produce no reflected screen edge");
|
||||
server.runOnServer(s->s.overworld().setBlockAndUpdate(new BlockPos(100,129,104),Blocks.GLOWSTONE.defaultBlockState()));
|
||||
look(c,server,0,35);c.waitTicks(25);enable(c,false);var lampOff=capture(c,null);enable(c,true);unchanged(lampOff,capture(c,"flare-glowstone-excluded"),"Visible glowstone does not create a celestial flare");
|
||||
server.runOnServer(s->command(s,"time set 15000"));c.waitTicks(20);aim(c,server,true);
|
||||
enable(c,false);var moonOff=capture(c,"flare-moon-off");enable(c,true);long moon=gain(moonOff,capture(c,"flare-moon-30"));
|
||||
check(moon>200,"Native visible moon creates its own subtle flare: "+moon);
|
||||
server.runOnServer(s->command(s,"time set 111000"));c.waitFor(m->m.gameRenderer.gameRenderState().levelRenderState.skyRenderState.moonPhase.index()==4,200);aim(c,server,true);
|
||||
enable(c,false);var newOff=capture(c,null);enable(c,true);check(gain(newOff,capture(c,"flare-new-moon"))<moon*.1,"Dark new moon does not retain full moon flare");
|
||||
c.runOnClient(m->VanillaLight.setLensFlareIntensity(0));c.waitTicks(8);c.runOnClient(m->check(!Bloom.flareAllocated()&&!Bloom.allocated(),"Zero frees all resources with other effects OFF"));
|
||||
unchanged(newOff,capture(c,null),"Zero flare restores source image");
|
||||
c.runOnClient(m->{VanillaLight.setLensFlareIntensity(30);VanillaLight.setSunshafts(false);VanillaLight.setLensFlare(false);});
|
||||
for(String locale:new String[]{"fr_fr","en_us"})c.runOnClient(m->{
|
||||
m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());
|
||||
for(String suffix:new String[]{"lens_flare","lens_flare_help","lens_flare_intensity","lens_flare_intensity_help"}){
|
||||
String key="sanctuary.shaders."+suffix;check(!Component.translatable(key).getString().equals(key),"Localized flare control: "+locale+" "+key);
|
||||
}
|
||||
});
|
||||
System.out.println("LENS_FLARE136_PASS sunGain="+low+" moonGain="+moon+" visibility, resize, reload, strength, independent bloom, zero, preferences, FR/EN");
|
||||
}
|
||||
private static void cover(TestServerContext server,BlockPos center,boolean on){server.runOnServer(s->{for(int x=-4;x<=4;x++)for(int y=-4;y<=4;y++)for(int z=-4;z<=4;z++)s.overworld().setBlockAndUpdate(center.offset(x,y,z),on?Blocks.STONE.defaultBlockState():Blocks.AIR.defaultBlockState());});}
|
||||
private static void aim(ClientGameTestContext c,TestServerContext server,boolean moon){
|
||||
var direction=c.computeOnClient(m->PixelShadows.sunDirection(moon?m.gameRenderer.gameRenderState().levelRenderState.skyRenderState.moonAngle:m.gameRenderer.gameRenderState().levelRenderState.skyRenderState.sunAngle));
|
||||
look(c,server,net.minecraft.util.Mth.wrapDegrees((float)Math.toDegrees(Math.atan2(-direction.x,direction.z))+12),-(float)Math.toDegrees(Math.asin(direction.y))+8);
|
||||
}
|
||||
private static void frames(ClientGameTestContext c){int before=c.computeOnClient(m->Bloom.renderedFrames());c.waitFor(m->Bloom.applied()&&Bloom.ready()&&Bloom.renderedFrames()>before+10,1200);c.waitTicks(8);}
|
||||
private static void enable(ClientGameTestContext c,boolean value){c.runOnClient(m->VanillaLight.setLensFlare(value));if(value)frames(c);else c.waitTicks(8);}
|
||||
private static long gain(Frame off,Frame on){long sum=0;for(int i=0;i<off.pixels().length;i++)for(int shift:new int[]{0,8,16})sum+=Math.max(0,(on.pixels()[i]>>shift&255)-(off.pixels()[i]>>shift&255));return sum;}
|
||||
private static void preferences(){try{
|
||||
var path=FabricLoader.getInstance().getConfigDir().resolve("sanctuary-shaders.json");Files.writeString(path,"{\"bloom\":false,\"bloomIntensity\":20}");forgetPreferences();
|
||||
check(VanillaLight.lensFlare()&&VanillaLight.lensFlareIntensity()==30&&!VanillaLight.bloom()&&VanillaLight.bloomIntensity()==20,"Flare defaults preserve bloom choices");
|
||||
VanillaLight.setSunshafts(false);VanillaLight.setLensFlare(false);VanillaLight.setLensFlareIntensity(73);forgetPreferences();check(!VanillaLight.lensFlare()&&VanillaLight.lensFlareIntensity()==73,"Saved custom flare retained");
|
||||
for(int n:new int[]{-10,0,30,100,150}){VanillaLight.setLensFlareIntensity(n);forgetPreferences();check(VanillaLight.lensFlareIntensity()==Math.clamp(n,0,100),"Flare intensity persists and clamps");}
|
||||
VanillaLight.setLensFlareIntensity(30);
|
||||
}catch(java.io.IOException e){throw new AssertionError(e);}}
|
||||
}
|
||||
+199
@@ -0,0 +1,199 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import static fr.koka.sanctuary.client.shader.PixelShadows122ClientChecks.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.TestServerContext;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import java.nio.file.Files;
|
||||
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.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());
|
||||
}});
|
||||
camera(c,server,10.5,69,5.5,65);look(c,server,0,40);settle(c);
|
||||
// The preceding lava/mob fixture may leave animated particles after its blocks are removed.
|
||||
c.runOnClient(m->m.particleEngine.clearParticles());c.waitTicks(2);
|
||||
var off=capture(c,"pbr-off");enable(c,true);var on=capture(c,"pbr-50");
|
||||
var delta=difference(off,on);System.out.println("PBR134_DIFF "+delta);check(delta.changed>500&&delta.brighter>100&&delta.darker>100,"Texture relief has lit and shaded pixels: "+delta);
|
||||
int analyses=c.computeOnClient(m->ProceduralPbr.analyses());c.waitTicks(20);
|
||||
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->check(!ProceduralPbr.allocated()&&ProceduralPbr.materialCount()==0,"Zero releases targets and material cache"));
|
||||
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);
|
||||
var reload=c.computeOnClient(net.minecraft.client.Minecraft::reloadResourcePacks);c.waitFor(m->reload.isDone()&&m.gui.overlay()==null,1200);reload.join();frames(c);unchanged(eveningOn,capture(c,"pbr-reloaded"),"Resource reload rebuilds consistent cached materials");
|
||||
// Live geometry and light invalidation: an enclosing roof removes exposure to the sky.
|
||||
server.runOnServer(s->{for(int x=-8;x<=28;x++)for(int z=-8;z<=28;z++)s.overworld().setBlockAndUpdate(new BlockPos(x,73,z),Blocks.STONE.defaultBlockState());});
|
||||
settle(c);frames(c);var roofOn=capture(c,"pbr-roof-on");enable(c,false);var roofOff=capture(c,"pbr-roof-off");
|
||||
unchanged(roofOff,roofOn,"Roof prevents outdoor highlights on enclosed floor");enable(c,true);
|
||||
server.runOnServer(s->{for(int x=-8;x<=28;x++)for(int z=-8;z<=28;z++)s.overworld().setBlockAndUpdate(new BlockPos(x,73,z),Blocks.AIR.defaultBlockState());});
|
||||
settle(c);frames(c);unchanged(eveningOn,capture(c,"pbr-roof-removed"),"Removing roof restores material light without stale meshes");
|
||||
int oldWidth=c.computeOnClient(m->m.getWindow().getScreenWidth()),oldHeight=c.computeOnClient(m->m.getWindow().getScreenHeight());
|
||||
c.runOnClient(m->m.getWindow().setWindowed(961,541));c.waitFor(m->m.gameRenderer.mainRenderTarget().width!=on.width(),200);frames(c);capture(c,"pbr-resized");
|
||||
c.runOnClient(m->m.getWindow().setWindowed(oldWidth,oldHeight));c.waitFor(m->m.gameRenderer.mainRenderTarget().width==on.width(),200);frames(c);
|
||||
c.runOnClient(m->{VanillaLight.setPixelShadows(true);VanillaLight.setSsgi(true);VanillaLight.setBloom(true);VanillaLight.setOreEmission(true);VanillaLight.setEdgeHighlights(true);});
|
||||
c.waitFor(m->PixelShadows.applied()&&PixelShadows.ready()&&ScreenSpaceGI.applied()&&Bloom.applied()&&EdgeHighlights.applied()&&ProceduralPbr.applied(),1200);frames(c);capture(c,"pbr-combined");
|
||||
matteGround(c,server,Blocks.OAK_LEAVES,"leaves",true);
|
||||
matteGround(c,server,Blocks.DIRT,"dirt",false);
|
||||
matteGround(c,server,Blocks.OAK_PLANKS,"oak-planks",false);
|
||||
matteGround(c,server,Blocks.STRIPPED_SPRUCE_LOG,"stripped-spruce",false);
|
||||
darkStone(c,server);
|
||||
netherRoundTrip(c,server);
|
||||
c.runOnClient(m->VanillaLight.setEnabled(false));c.waitTicks(8);c.runOnClient(m->check(!ProceduralPbr.allocated()&&ProceduralPbr.materialCount()==0,"Global OFF releases PBR"));
|
||||
c.runOnClient(m->{VanillaLight.setEnabled(true);VanillaLight.setPbr(false);VanillaLight.setSsgi(false);});
|
||||
System.out.println("PBR134_PASS normals; profiles; texture cache; directional lighting; intensity; OFF/zero; tiny camera; reload; resize; combined; default="+delta+" high="+high+" directional="+directions);
|
||||
}
|
||||
private static void darkStone(ClientGameTestContext c,TestServerContext server){
|
||||
server.runOnServer(s->{for(int x=-5;x<=24;x++)for(int z=-5;z<=24;z++)
|
||||
s.overworld().setBlockAndUpdate(new BlockPos(x,64,z),Blocks.OBSIDIAN.defaultBlockState());});
|
||||
camera(c,server,10.5,69,5.5,65);look(c,server,0,90);settle(c);
|
||||
enable(c,false);var off=capture(c,"pbr151-obsidian-face-off");enable(c,true);var on=capture(c,"pbr151-obsidian-face-on");
|
||||
double gain=0;int samples=0,relief=0;
|
||||
for(int y=off.height()/4;y<off.height()*3/4;y++)for(int x=off.width()/4;x<off.width()*3/4;x++){
|
||||
int i=y*off.width()+x,a=off.pixels()[i],b=on.pixels()[i];
|
||||
double before=((a>>16&255)+(a>>8&255)+(a&255))/3.0,after=((b>>16&255)+(b>>8&255)+(b&255))/3.0;
|
||||
gain+=Math.max(0,after-before);samples++;if(Math.abs(after-before)>1)relief++;
|
||||
}
|
||||
System.out.println("PBR151_OBSIDIAN samples="+samples+" meanBrightening="+gain/samples+" relief="+relief);
|
||||
check(samples>1000&&gain/samples<18,"Dark stone keeps its dark face without amplified white sheen");
|
||||
check(relief>100,"Obsidian PBR remains visible");
|
||||
look(c,server,0,20);settle(c);capture(c,"pbr151-obsidian-grazing");
|
||||
}
|
||||
private static void netherRoundTrip(ClientGameTestContext c,TestServerContext server){
|
||||
c.runOnClient(m->{VanillaLight.setEnabled(true);VanillaLight.setPbr(true);VanillaLight.setSsr(true);VanillaLight.setColoredLights(true);VanillaLight.setPixelShadows(true);});
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();
|
||||
p.getInventory().setItem(0,new net.minecraft.world.item.ItemStack(net.minecraft.world.item.Items.DIAMOND,7));
|
||||
var overflow=fr.koka.sanctuary.inventory.SanctuaryOverflowAccess.of(p);
|
||||
overflow.setItem(0,new net.minecraft.world.item.ItemStack(net.minecraft.world.item.Items.EMERALD,3));
|
||||
overflow.setItem(17,new net.minecraft.world.item.ItemStack(net.minecraft.world.item.Items.GOLD_INGOT,5));
|
||||
p.inventoryMenu.broadcastFullState();});
|
||||
for(String dimension:new String[]{"the_nether","overworld","the_nether","overworld"}){
|
||||
server.runOnServer(s->command(s,"execute in minecraft:"+dimension+" run tp @a 10.5 100 5.5"));
|
||||
c.waitFor(m->m.level!=null&&m.player!=null&&m.level.dimension().identifier().getPath().equals(dimension)&&m.gui.screen()==null,2400);
|
||||
c.waitTicks(40);
|
||||
c.runOnClient(m->{
|
||||
check(m.player.inventoryMenu.slots.size()==67,"Inventory has exactly 46 native, 18 extra and 3 accessory slots");
|
||||
check(m.player.getInventory().getItem(0).is(net.minecraft.world.item.Items.DIAMOND)&&m.player.getInventory().getItem(0).getCount()==7,"Native stack survives transfer");
|
||||
var overflow=fr.koka.sanctuary.inventory.SanctuaryOverflowAccess.of(m.player);
|
||||
check(overflow.getItem(0).is(net.minecraft.world.item.Items.EMERALD)&&overflow.getItem(0).getCount()==3,"First overflow stack survives transfer");
|
||||
check(overflow.getItem(17).is(net.minecraft.world.item.Items.GOLD_INGOT)&&overflow.getItem(17).getCount()==5,"Last overflow stack survives transfer");
|
||||
});
|
||||
capture(c,"nether151-"+dimension);
|
||||
server.runOnServer(s->check(s.getPlayerList().getPlayers().size()==1,"Player stays connected after dimension transfer"));
|
||||
System.out.println("NETHER151_TRANSFER_PASS "+dimension);
|
||||
}
|
||||
}
|
||||
private static void matteGround(ClientGameTestContext c,TestServerContext server,net.minecraft.world.level.block.Block block,String name,boolean leaves){
|
||||
c.runOnClient(m->{VanillaLight.setSsr(false);VanillaLight.setPbrIntensity(50);VanillaLight.setPixelShadows(false);VanillaLight.setSsgi(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);VanillaLight.setEdgeHighlights(false);});
|
||||
server.runOnServer(s->{if(leaves)command(s,"time set 1000");for(int x=-5;x<=24;x++)for(int z=-5;z<=24;z++)
|
||||
s.overworld().setBlockAndUpdate(new BlockPos(x,64,z),leaves?block.defaultBlockState().setValue(net.minecraft.world.level.block.LeavesBlock.PERSISTENT,true):block.defaultBlockState());});
|
||||
camera(c,server,10.5,69,5.5,65);look(c,server,0,40);settle(c);
|
||||
enable(c,false);var off=capture(c,"pbr150-"+name+"-off");enable(c,true);var on=capture(c,"pbr150-"+name+"-on");
|
||||
double nativeChroma=0,pbrChroma=0;int samples=0,relief=0;
|
||||
for(int y=off.height()/3;y<off.height()*9/10;y++)for(int x=off.width()/10;x<off.width()*9/10;x++){
|
||||
int i=y*off.width()+x,a=off.pixels()[i],b=on.pixels()[i],r=a>>16&255,g=a>>8&255,blue=a&255;
|
||||
if(leaves?(g<30||g<r*1.1||g<blue*1.1):(r<30||r<g*1.05||g<blue*1.1))continue;
|
||||
nativeChroma+=chroma(a);pbrChroma+=chroma(b);samples++;
|
||||
if(Math.abs((b>>16&255)-r)+Math.abs((b>>8&255)-g)+Math.abs((b&255)-blue)>6)relief++;
|
||||
}
|
||||
double retained=pbrChroma/nativeChroma;
|
||||
System.out.println("PBR150_MATTE material="+name+" samples="+samples+" chromaRetained="+retained+" relief="+relief);
|
||||
check(samples>1000&&retained>.985,name+" preserves native colour without specular veil: "+retained);
|
||||
check(relief>100,name+" normal relief remains visible");
|
||||
}
|
||||
private static double chroma(int pixel){
|
||||
int r=pixel>>16&255,g=pixel>>8&255,b=pixel&255,max=Math.max(r,Math.max(g,b)),min=Math.min(r,Math.min(g,b));
|
||||
return (max-min)/(double)Math.max(1,max);
|
||||
}
|
||||
private static void closeRange(ClientGameTestContext c,TestServerContext server){
|
||||
int failures=0;
|
||||
c.runOnClient(m->VanillaLight.setPbrIntensity(80));
|
||||
for(double distance:new double[]{.12,.25,.5,1.0}){
|
||||
double eye=c.computeOnClient(m->(double)m.player.getEyeHeight());
|
||||
camera(c,server,4.5,65-eye+distance,5.5,65);look(c,server,17,55);settle(c);frames(c);
|
||||
var before=capture(c,"pbr147-close-"+distance);
|
||||
look(c,server,17.00002f,55.00002f);frames(c);var after=capture(c,"pbr147-close-moved-"+distance);
|
||||
int changed=0,count=0;
|
||||
for(int y=before.height()/10;y<before.height()*9/10;y++)for(int x=before.width()/10;x<before.width()*9/10;x++){
|
||||
int i=y*before.width()+x,a=before.pixels()[i],b=after.pixels()[i];count++;
|
||||
if(Math.abs((a>>16&255)-(b>>16&255))+Math.abs((a>>8&255)-(b>>8&255))+Math.abs((a&255)-(b&255))>12)changed++;
|
||||
}
|
||||
System.out.println("PBR147_CLOSE distance="+distance+" changed="+changed+" samples="+count);
|
||||
if(changed>count*.001)failures++;
|
||||
}
|
||||
c.runOnClient(m->VanillaLight.setPbrIntensity(50));
|
||||
check(failures==0,"Close PBR stays stable under subpixel motion: failing distances="+failures);
|
||||
}
|
||||
private static void materials(){
|
||||
int[] flat=new int[16];Arrays.fill(flat,0xFF808080);var profile=PbrMaterials.profile("bricks");
|
||||
for(int p:PbrMaterials.generate(flat,4,4,profile))check((p>>8&0xFFFF)==0x8080,"Flat source has neutral normal");
|
||||
flat[5]=0xFFFFFFFF;var slope=PbrMaterials.generate(flat,4,4,profile);check((slope[4]>>16&255)<128&&(slope[6]>>16&255)>128,"Height gradients produce opposite normal slopes");
|
||||
check(PbrMaterials.profile("iron_block").metal()>.8&&PbrMaterials.profile("iron_ore").metal()==0,"Metal block profile does not turn ore host rock into metal");
|
||||
check(PbrMaterials.family(Blocks.WATER.defaultBlockState())==2&&PbrMaterials.family(Blocks.GLASS.defaultBlockState())==1&&PbrMaterials.family(Blocks.GLASS_PANE.defaultBlockState())==1,"Water and glass receiver families");
|
||||
check(PbrMaterials.profile("water").metal()==0&&PbrMaterials.profile("glass").metal()==0,"Water and glass stay dielectric");
|
||||
int[] glass={0x00FFFFFF,0x00000000,0x00000000,0x00FFFFFF};
|
||||
for(int p:PbrMaterials.generate(glass,2,2,PbrMaterials.profile("glass")))check((p>>8&0xFFFF)==0x8080,"Transparent glass texels fall back to geometric normal");
|
||||
for(String soil:new String[]{"dirt","coarse_dirt","rooted_dirt","grass_block","dirt_path","farmland","podzol","mycelium"})
|
||||
check(PbrMaterials.profile(soil).roughness()>=.98f&&PbrMaterials.profile(soil).metal()==0,"Dry soil has a fully matte profile: "+soil);
|
||||
for(String wood:new String[]{"oak_planks","spruce_stairs","dark_oak_slab","stripped_spruce_log","stripped_crimson_stem","warped_hyphae","bamboo_mosaic","pale_oak_trapdoor"})
|
||||
check(PbrMaterials.profile(wood).roughness()>=.98f&&PbrMaterials.profile(wood).metal()==0,"Wood remains fully matte: "+wood);
|
||||
check(PbrMaterials.profile("crimson_nylium").roughness()<.9f,"Nether ground is not classified as wood");
|
||||
check(PbrMaterials.profile("oak_planks").roughness()>PbrMaterials.profile("polished_andesite").roughness(),"Wood remains rougher than polished stone");
|
||||
}
|
||||
private static void preferences(){try{
|
||||
var p=FabricLoader.getInstance().getConfigDir().resolve("sanctuary-shaders.json");Files.writeString(p,"{\"ssgi\":true,\"ssgiIntensity\":35,\"bloom\":false}");forgetPreferences();
|
||||
check(VanillaLight.pbr()&&VanillaLight.pbrIntensity()==50&&VanillaLight.ssgi()&&!VanillaLight.bloom(),"Missing PBR setting defaults ON while preserving explicit old choices");
|
||||
Files.writeString(p,"{\"pbrIntensity\":80}");forgetPreferences();
|
||||
check(VanillaLight.pbrIntensity()==50,"Previous 80 percent baseline migrates to 50 once");
|
||||
VanillaLight.setPbrIntensity(80);forgetPreferences();check(VanillaLight.pbrIntensity()==80,"New explicit 80 percent choice persists after migration");
|
||||
for(int n:new int[]{-1,0,50,100,101}){VanillaLight.setPbr(true);VanillaLight.setPbrIntensity(n);forgetPreferences();check(VanillaLight.pbr()&&VanillaLight.pbrIntensity()==Math.clamp(n,0,100),"PBR preference persistence/clamp");}
|
||||
check(VanillaLight.pbrDistance()==64&&VanillaLight.ssrDistance()==64,"Independent distances default to 64");
|
||||
check(!VanillaLight.naturalWater(false)&&VanillaLight.naturalWater(true),"World-specific water defaults");
|
||||
for(int n:new int[]{16,32,64,128,256}){VanillaLight.setPbrDistance(n);forgetPreferences();check(VanillaLight.pbrDistance()==n&&VanillaLight.ssrDistance()==64,"PBR distance changes independently");}
|
||||
for(int n:new int[]{16,32,64,128,256}){VanillaLight.setSsrDistance(n);forgetPreferences();check(VanillaLight.ssrDistance()==n&&VanillaLight.pbrDistance()==256,"SSR distance changes independently");}
|
||||
VanillaLight.setSsrDistance(64);
|
||||
for(int n:new int[]{0,1,2,4,8}){VanillaLight.setWaterBlendRadius(n);forgetPreferences();check(VanillaLight.waterBlendRadius()==n,"Water blend persistence");}
|
||||
VanillaLight.setPbrDistance(32);VanillaLight.setWaterBlendRadius(4);
|
||||
check(VanillaLight.ssr()&&VanillaLight.ssrIntensity()==20,"Missing SSR defaults ON at 20 percent");
|
||||
for(int n:new int[]{-1,0,50,100,101}){VanillaLight.setSsr(true);VanillaLight.setSsrIntensity(n);forgetPreferences();check(VanillaLight.ssr()&&VanillaLight.ssrIntensity()==Math.clamp(n,0,100),"SSR preference persistence/clamp");}
|
||||
VanillaLight.setSsr(false);VanillaLight.setSsrIntensity(20);forgetPreferences();check(!VanillaLight.ssr(),"SSR OFF persists");
|
||||
var manager=net.minecraft.client.Minecraft.getInstance().getLanguageManager();var oldLanguage=manager.getSelected();
|
||||
for(String locale:new String[]{"fr_fr","en_us"}){
|
||||
manager.setSelected(locale);manager.onResourceManagerReload(net.minecraft.client.Minecraft.getInstance().getResourceManager());
|
||||
for(String key:new String[]{"ssr_distance","ssr_distance_help","ssr_distance_value","pbr_distance","pbr_distance_help","pbr_distance_value","water_blend","water_blend_help","water_blend_value","ssr","ssr_help","ssr_intensity","ssr_intensity_help","ssr_intensity_value"}){
|
||||
String full="sanctuary.shaders."+key;check(!net.minecraft.network.chat.Component.translatable(full,50).getString().equals(full),"SSR FR/EN "+full);
|
||||
}
|
||||
}
|
||||
check(net.minecraft.network.chat.Component.translatable("sanctuary.shaders.ssr").getString().equals("SSR"),"SSR label without experimental qualifier");
|
||||
manager.setSelected(oldLanguage);manager.onResourceManagerReload(net.minecraft.client.Minecraft.getInstance().getResourceManager());
|
||||
Files.writeString(p,"{\"naturalWater\":false,\"pbrIntensity\":37}");forgetPreferences();
|
||||
check(!VanillaLight.naturalWater(false)&&!VanillaLight.naturalWater(true)&&VanillaLight.pbrIntensity()==37,"Legacy water OFF and personal PBR intensity are preserved");
|
||||
Files.writeString(p,"{\"naturalWater\":true}");forgetPreferences();
|
||||
check(!VanillaLight.naturalWater(false)&&VanillaLight.naturalWater(true),"Legacy global ON adopts contextual defaults");
|
||||
VanillaLight.setPbrIntensity(50);
|
||||
}catch(Exception e){throw new AssertionError(e);}}
|
||||
private record Delta(int changed,int brighter,int darker,long total){}
|
||||
private static Delta difference(Frame off,Frame on){int changed=0,bright=0,dark=0;long total=0;for(int i=0;i<off.pixels().length;i++){
|
||||
int a=off.pixels()[i],b=on.pixels()[i],delta=(b>>16&255)-(a>>16&255);total+=Math.abs(delta);if(Math.abs(delta)>3)changed++;if(delta>3)bright++;if(delta< -3)dark++;
|
||||
}return new Delta(changed,bright,dark,total);}
|
||||
private static void settle(ClientGameTestContext c){c.waitTicks(35);c.waitFor(m->m.levelRenderer.hasRenderedAllSections(),1200);c.waitTicks(10);}
|
||||
private static void enable(ClientGameTestContext c,boolean on){c.runOnClient(m->VanillaLight.setPbr(on));if(on)frames(c);else c.waitTicks(8);}
|
||||
private static void frames(ClientGameTestContext c){int start=c.computeOnClient(m->ProceduralPbr.renderedFrames());c.waitFor(m->ProceduralPbr.applied()&&ProceduralPbr.ready()&&ProceduralPbr.renderedFrames()>start+10,1200);c.waitTicks(8);}
|
||||
}
|
||||
+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);}
|
||||
}
|
||||
+265
@@ -0,0 +1,265 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import static fr.koka.sanctuary.client.shader.PixelShadows122ClientChecks.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.TestServerContext;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
|
||||
/** Native screenshots on a new development world; source-colour and transparent receiver checks. */
|
||||
final class PixelReflectionsClientChecks {
|
||||
static void run(ClientGameTestContext c,TestServerContext server){
|
||||
c.runOnClient(m->{VanillaLight.setEnabled(true);VanillaLight.setSaturation(100);VanillaLight.setPbr(true);VanillaLight.setPbrIntensity(80);
|
||||
VanillaLight.setNaturalWater(true);VanillaLight.setWaterIntensity(100);VanillaLight.setWaterBlendRadius(0);VanillaLight.setSsr(false);VanillaLight.setSsrIntensity(100);VanillaLight.setColoredLights(false);VanillaLight.setSsgi(false);
|
||||
VanillaLight.setSunshafts(false);VanillaLight.setLensFlare(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);
|
||||
VanillaLight.setEdgeHighlights(false);VanillaLight.setPixelShadows(false);});
|
||||
server.runOnServer(s->{
|
||||
command(s,"time set 6000");
|
||||
for(int x=-18;x<=18;x++)for(int z=-4;z<=24;z++){
|
||||
for(int y=65;y<=85;y++)s.overworld().setBlockAndUpdate(new BlockPos(x,y,z),Blocks.AIR.defaultBlockState());
|
||||
s.overworld().setBlockAndUpdate(new BlockPos(x,64,z),Blocks.STONE.defaultBlockState());
|
||||
if(z>=3&&z<=18)s.overworld().setBlockAndUpdate(new BlockPos(x,65,z),Blocks.WATER.defaultBlockState());
|
||||
}
|
||||
for(int x=-12;x<=12;x++)for(int y=65;y<=77-Math.abs(x)/2;y++)
|
||||
s.overworld().setBlockAndUpdate(new BlockPos(x,y,19),Blocks.CONCRETE.red().defaultBlockState());
|
||||
});
|
||||
camera(c,server,.5,68,0,70);look(c,server,0,12);settle(c);
|
||||
int analysis=c.computeOnClient(m->ProceduralPbr.waterAnalyses()),updates=c.computeOnClient(m->ProceduralPbr.waterUpdates());
|
||||
c.waitTicks(20);c.runOnClient(m->check(ProceduralPbr.waterAnalyses()==analysis&&ProceduralPbr.waterUpdates()>updates,"Water normals follow native animation without reanalysis"));
|
||||
var off=capture(c,"ssr-water-off");
|
||||
c.runOnClient(m->VanillaLight.setSsr(true));frames(c);var on=capture(c,"ssr-water-on");
|
||||
dumpLandscape(c);
|
||||
long changed=changed(off,on);System.out.println("SSR_WATER_DIFF "+changed);
|
||||
check(changed>100,"SSR changes water receiver pixels: "+changed);
|
||||
double redSignal=colourSignal(off,on);
|
||||
server.runOnServer(s->{command(s,"gamemode creative @a");var player=s.getPlayerList().getPlayers().getFirst();player.getAbilities().flying=true;player.onUpdateAbilities();});
|
||||
c.waitTicks(10);
|
||||
c.runOnClient(m->m.options.setCameraType(net.minecraft.client.CameraType.THIRD_PERSON_BACK));c.waitTicks(20);
|
||||
c.runOnClient(m->VanillaLight.setSsr(false));c.waitTicks(8);var f5Off=capture(c,"ssr146-f5-off");
|
||||
c.runOnClient(m->VanillaLight.setSsr(true));frames(c);var f5On=capture(c,"ssr146-f5-on");
|
||||
check(changed(f5Off,f5On)>100,"SSR remains visible around third-person player");
|
||||
int torsoChange=0,torsoPixels=0;
|
||||
for(int y=f5Off.height()*55/100;y<f5Off.height()*71/100;y++)for(int x=f5Off.width()*485/1000;x<f5Off.width()*515/1000;x++){
|
||||
int index=y*f5Off.width()+x;torsoPixels++;if(f5Off.pixels()[index]!=f5On.pixels()[index])torsoChange++;
|
||||
}
|
||||
check(torsoChange<torsoPixels*.01,"SSR does not draw through the visible F5 torso: "+torsoChange+" / "+torsoPixels);
|
||||
c.runOnClient(m->m.options.setCameraType(net.minecraft.client.CameraType.FIRST_PERSON));c.waitTicks(20);
|
||||
server.runOnServer(s->command(s,"gamemode spectator @a"));c.waitTicks(10);
|
||||
for(int distance:new int[]{16,64,128,256,32}){
|
||||
c.runOnClient(m->{VanillaLight.setPbrDistance(distance);VanillaLight.setSsrDistance(256);});settle(c);frames(c);capture(c,"ssr146-distance-"+distance);
|
||||
c.runOnClient(m->check(ProceduralPbr.applied()&&PixelReflections.applied(),"Independent material distances render at PBR "+distance+" / SSR 256"));
|
||||
}
|
||||
|
||||
for(int distance:new int[]{16,64,256}){
|
||||
c.runOnClient(m->{VanillaLight.setPbrDistance(256);VanillaLight.setSsrDistance(distance);});settle(c);frames(c);
|
||||
capture(c,"ssr149-independent-"+distance);
|
||||
c.runOnClient(m->check(ProceduralPbr.applied()&&PixelReflections.applied(),"PBR 256 remains active with independent SSR "+distance));
|
||||
}
|
||||
c.runOnClient(m->VanillaLight.setPbrDistance(32));
|
||||
c.runOnClient(m->VanillaLight.setSsrDistance(32));
|
||||
c.runOnClient(m->{VanillaLight.setPbrIntensity(50);VanillaLight.setSsrIntensity(20);});
|
||||
camera(c,server,.5,68,10,70);settle(c);frames(c);
|
||||
c.runOnClient(m->VanillaLight.setPbr(false));c.waitTicks(8);var solarOff=capture(c,"ssr148-solar-water-off");
|
||||
c.runOnClient(m->VanillaLight.setPbr(true));frames(c);
|
||||
var solar=capture(c,"ssr146-solar-water");
|
||||
int white=0;for(int pixel:solar.pixels())if((pixel>>16&255)>235&&(pixel>>8&255)>235&&(pixel&255)>235)white++;
|
||||
int brightened=0;for(int i=0;i<solar.pixels().length;i++){
|
||||
int a=solarOff.pixels()[i],b=solar.pixels()[i];
|
||||
if((b>>16&255)+(b>>8&255)+(b&255)>(a>>16&255)+(a>>8&255)+(a&255)+24)brightened++;
|
||||
}
|
||||
check(brightened>5000,"Soft solar reflection remains broadly visible: "+brightened);
|
||||
check(white<solar.pixels().length*.005,"Soft solar reflection preserves texture instead of a white plateau: "+white);
|
||||
System.out.println("PBR148_SOLAR brightened="+brightened+" white="+white);
|
||||
System.out.println("SSR146_SOLAR_WHITE pixels="+white+" width="+solar.width()+" height="+solar.height());
|
||||
c.runOnClient(m->VanillaLight.setBloom(true));frames(c);capture(c,"ssr146-solar-water-bloom");
|
||||
c.runOnClient(m->check(MaterialBloom.contribution()!=null,"Material light reaches bloom"));
|
||||
c.runOnClient(m->VanillaLight.setBloom(false));frames(c);
|
||||
c.runOnClient(m->check(MaterialBloom.contribution()==null,"Bloom OFF releases material halo"));
|
||||
server.runOnServer(s->command(s,"time set 18000"));c.waitTicks(15);
|
||||
c.runOnClient(m->VanillaLight.setSsr(false));c.waitTicks(8);
|
||||
var moon=capture(c,"ssr147-moon-water");
|
||||
int moonWhite=0;for(int pixel:moon.pixels())if((pixel>>16&255)>235&&(pixel>>8&255)>235&&(pixel&255)>235)moonWhite++;
|
||||
check(moonWhite<Math.max(10,white*.05),"Moon reflection stays much softer than the sun: "+moonWhite+" / "+white);
|
||||
c.runOnClient(m->VanillaLight.setPbr(false));c.waitTicks(8);var moonOff=capture(c,"ssr147-moon-water-off");
|
||||
check(changed(moonOff,moon)>100,"Soft moon reflection remains visible");
|
||||
System.out.println("PBR147_MOON white="+moonWhite+" sunWhite="+white+" changed="+changed(moonOff,moon));
|
||||
c.runOnClient(m->{VanillaLight.setPbr(true);VanillaLight.setSsr(true);VanillaLight.setBloom(true);});frames(c);capture(c,"ssr147-moon-water-bloom");
|
||||
c.runOnClient(m->VanillaLight.setBloom(false));
|
||||
server.runOnServer(s->command(s,"time set 1000"));c.waitTicks(10);
|
||||
var sunLook=c.computeOnClient(m->{var sun=PixelShadows.sunDirection(m.gameRenderer.gameRenderState().levelRenderState.skyRenderState.sunAngle);return new float[]{(float)Math.toDegrees(Math.atan2(-sun.x,sun.z)),(float)Math.toDegrees(Math.asin(sun.y))};});
|
||||
look(c,server,sunLook[0],sunLook[1]);frames(c);capture(c,"ssr146-solar-path-low-sun");
|
||||
c.runOnClient(m->VanillaLight.setBloom(true));frames(c);capture(c,"ssr146-solar-path-low-sun-bloom");
|
||||
for(int time:new int[]{2000,11500,12400}){
|
||||
c.runOnClient(m->{VanillaLight.setBloom(false);VanillaLight.setSsr(false);});
|
||||
server.runOnServer(s->command(s,"time set "+time));c.waitTicks(15);
|
||||
var solarLook=c.computeOnClient(m->{var sun=PixelShadows.sunDirection(m.gameRenderer.gameRenderState().levelRenderState.skyRenderState.sunAngle);return new float[]{(float)Math.toDegrees(Math.atan2(-sun.x,sun.z)),(float)Math.toDegrees(Math.asin(sun.y))};});
|
||||
look(c,server,solarLook[0],solarLook[1]);pbrFrames(c);
|
||||
var warm=capture(c,"ssr150-solar-"+time);
|
||||
c.runOnClient(m->VanillaLight.setPbr(false));c.waitTicks(8);var nativeWater=capture(c,"ssr150-solar-native-"+time);
|
||||
long red=0,green=0,blue=0;int samples=0;
|
||||
for(int y=warm.height()/3;y<warm.height()*9/10;y++)for(int x=warm.width()/3;x<warm.width()*2/3;x++){
|
||||
int i=y*warm.width()+x,a=nativeWater.pixels()[i],b=warm.pixels()[i];
|
||||
int dr=(b>>16&255)-(a>>16&255),dg=(b>>8&255)-(a>>8&255),db=(b&255)-(a&255);
|
||||
if(dr>3){red+=dr;green+=dg;blue+=db;samples++;}
|
||||
}
|
||||
System.out.println("PBR150_SOLAR_COLOUR time="+time+" pitch="+solarLook[1]+" samples="+samples+" red="+red+" green="+green+" blue="+blue);
|
||||
var tint=c.computeOnClient(m->SolarColour.tint(m.gameRenderer.gameRenderState().levelRenderState.skyRenderState));
|
||||
float nativeFade=c.computeOnClient(m->m.gameRenderer.gameRenderState().levelRenderState.skyRenderState.sunriseAndSunsetColor.w());
|
||||
System.out.println("PBR150_NATIVE_SUNSET time="+time+" fade="+nativeFade+" tint="+tint);
|
||||
check(samples>500,"Solar reflection remains visible: "+time);
|
||||
if(nativeFade>.1)check(red>green*1.1&&red>blue*1.1,"Reflection follows warm native horizon colour: "+time);
|
||||
if(nativeFade==0)check(tint.equals(new org.joml.Vector3f(1)),"No early tint before native sunset");
|
||||
c.runOnClient(m->{VanillaLight.setPbr(true);VanillaLight.setSsr(true);VanillaLight.setBloom(true);});frames(c);capture(c,"ssr150-solar-bloom-"+time);
|
||||
c.runOnClient(m->VanillaLight.setBloom(false));
|
||||
look(c,server,solarLook[0],-solarLook[1]);frames(c);var sunDisc=capture(c,"ssr150-sun-disc-"+time);
|
||||
long sr=0,sg=0,sb=0;int sunSamples=0;
|
||||
for(int y=sunDisc.height()/2-5;y<sunDisc.height()/2+5;y++)for(int x=sunDisc.width()/2-5;x<sunDisc.width()/2+5;x++){
|
||||
int pixel=sunDisc.pixels()[y*sunDisc.width()+x];sr+=pixel>>16&255;sg+=pixel>>8&255;sb+=pixel&255;sunSamples++;
|
||||
}
|
||||
System.out.println("PBR150_SUN_DISC time="+time+" red="+sr/sunSamples+" green="+sg/sunSamples+" blue="+sb/sunSamples);
|
||||
if(nativeFade>.1)check(sr>sb*1.05,"Sun disc follows native warm tint: "+time);
|
||||
look(c,server,solarLook[0],solarLook[1]);
|
||||
}
|
||||
c.runOnClient(m->{VanillaLight.setBloom(false);VanillaLight.setSsrIntensity(100);});
|
||||
server.runOnServer(s->command(s,"time set 6000"));c.waitTicks(10);
|
||||
camera(c,server,.5,68,0,70);look(c,server,0,12);settle(c);
|
||||
server.runOnServer(s->{for(int x=-12;x<=12;x++)for(int y=65;y<=77-Math.abs(x)/2;y++)s.overworld().setBlockAndUpdate(new BlockPos(x,y,19),Blocks.CONCRETE.blue().defaultBlockState());});
|
||||
settle(c);c.runOnClient(m->VanillaLight.setSsr(false));c.waitTicks(8);var blueOff=capture(c,"ssr-blue-off");
|
||||
c.runOnClient(m->VanillaLight.setSsr(true));frames(c);var blueOn=capture(c,"ssr-blue-on");double blueSignal=colourSignal(blueOff,blueOn);
|
||||
System.out.println("SSR_SOURCE_COLOUR red="+redSignal+" blue="+blueSignal);
|
||||
check(redSignal>blueSignal+3,"Water reflection follows source colour rather than animation: "+redSignal+" / "+blueSignal);
|
||||
server.runOnServer(s->{for(int x=-12;x<=12;x++)for(int y=65;y<=77-Math.abs(x)/2;y++)s.overworld().setBlockAndUpdate(new BlockPos(x,y,19),Blocks.CONCRETE.red().defaultBlockState());});settle(c);
|
||||
c.runOnClient(m->VanillaLight.setSsrIntensity(0));c.waitTicks(8);
|
||||
check(!c.computeOnClient(m->PixelReflections.applied()||PixelReflections.allocated()),"Zero removes SSR contribution and colour allocation");
|
||||
c.runOnClient(m->VanillaLight.setSsrIntensity(100));frames(c);
|
||||
for(var bed:new net.minecraft.world.level.block.Block[]{Blocks.SAND,Blocks.MUD}){
|
||||
server.runOnServer(s->{for(int x=-18;x<=18;x++)for(int z=3;z<=18;z++){s.overworld().setBlockAndUpdate(new BlockPos(x,63,z),Blocks.STONE.defaultBlockState());s.overworld().setBlockAndUpdate(new BlockPos(x,64,z),bed.defaultBlockState());}});
|
||||
settle(c);frames(c);
|
||||
c.runOnClient(m->check(NaturalWater.sample(m.level,.5,65.5,8.5).equals(bed==Blocks.SAND?NaturalWater.LIGHT:NaturalWater.POND),"Natural water profile remains independent of PBR and SSR: "+NaturalWater.sample(m.level,.5,65.5,8.5)+" above "+m.level.getBlockState(new BlockPos(0,64,8))));
|
||||
capture(c,bed==Blocks.SAND?"ssr-sand-profile":"ssr-pond-profile");
|
||||
}
|
||||
server.runOnServer(s->{for(int x=-18;x<=18;x++)for(int z=3;z<=18;z++)s.overworld().setBlockAndUpdate(new BlockPos(x,64,z),Blocks.STONE.defaultBlockState());});settle(c);frames(c);
|
||||
for(float yaw:new float[]{-25,0,25}){look(c,server,yaw,12);frames(c);capture(c,"ssr-water-angle-"+(int)yaw);}
|
||||
look(c,server,0,2);frames(c);capture(c,"ssr-grazing");
|
||||
look(c,server,75,12);frames(c);capture(c,"ssr-landscape-offscreen");
|
||||
camera(c,server,2.5,68,0,70);look(c,server,0,12);settle(c);frames(c);capture(c,"ssr-translated");
|
||||
camera(c,server,.5,68,0,70);look(c,server,0,12);settle(c);
|
||||
// A hidden landscape must not leak through an opaque foreground wall.
|
||||
server.runOnServer(s->{for(int x=-12;x<=12;x++)for(int y=65;y<=78;y++)s.overworld().setBlockAndUpdate(new BlockPos(x,y,18),Blocks.CONCRETE.white().defaultBlockState());});
|
||||
settle(c);c.runOnClient(m->VanillaLight.setSsr(false));c.waitTicks(8);var coveredOff=capture(c,"ssr-covered-off");
|
||||
c.runOnClient(m->VanillaLight.setSsr(true));frames(c);var coveredOn=capture(c,"ssr-covered-on");
|
||||
double coveredSignal=colourSignal(coveredOff,coveredOn);
|
||||
server.runOnServer(s->{for(int x=-12;x<=12;x++)for(int y=65;y<=77-Math.abs(x)/2;y++)s.overworld().setBlockAndUpdate(new BlockPos(x,y,19),Blocks.CONCRETE.blue().defaultBlockState());});settle(c);
|
||||
c.runOnClient(m->VanillaLight.setSsr(false));c.waitTicks(8);var hiddenBlueOff=capture(c,"ssr-hidden-blue-off");
|
||||
c.runOnClient(m->VanillaLight.setSsr(true));frames(c);var hiddenBlueOn=capture(c,"ssr-hidden-blue-on");
|
||||
double hiddenBlueSignal=colourSignal(hiddenBlueOff,hiddenBlueOn);
|
||||
check(Math.abs(coveredSignal-hiddenBlueSignal)<1.5,"Hidden landscape colour cannot leak through opaque wall: "+coveredSignal+" / "+hiddenBlueSignal);
|
||||
server.runOnServer(s->{for(int x=-12;x<=12;x++){
|
||||
for(int y=65;y<=78;y++)s.overworld().setBlockAndUpdate(new BlockPos(x,y,18),Blocks.AIR.defaultBlockState());
|
||||
for(int y=65;y<=77-Math.abs(x)/2;y++)s.overworld().setBlockAndUpdate(new BlockPos(x,y,19),Blocks.CONCRETE.red().defaultBlockState());
|
||||
}});settle(c);frames(c);
|
||||
benchmark(c,"pond");
|
||||
look(c,server,0,35);frames(c);benchmark(c,"water-fullscreen");look(c,server,0,12);frames(c);
|
||||
// The nearest glass layer must replace the water receiver, including alpha-zero glass texels.
|
||||
server.runOnServer(s->{for(int x=-7;x<=7;x++)for(int z=4;z<=14;z++)s.overworld().setBlockAndUpdate(new BlockPos(x,66,z),Blocks.GLASS.defaultBlockState());});
|
||||
settle(c);capture(c,"ssr-glass-over-water");
|
||||
c.runOnClient(m->VanillaLight.setSsr(false));c.waitTicks(8);var glassOff=capture(c,"ssr-glass-off");
|
||||
c.runOnClient(m->VanillaLight.setSsr(true));frames(c);var glassOn=capture(c,"ssr-glass-on");
|
||||
check(changed(glassOff,glassOn)>100,"Transparent glass reflects landscape");
|
||||
benchmark(c,"glass");
|
||||
server.runOnServer(s->{for(int x=-7;x<=7;x++)for(int z=4;z<=14;z++)s.overworld().setBlockAndUpdate(new BlockPos(x,66,z),Blocks.IRON_BLOCK.defaultBlockState());});
|
||||
settle(c);c.runOnClient(m->VanillaLight.setSsr(false));c.waitTicks(8);var metalOff=capture(c,"ssr-metal-off");
|
||||
c.runOnClient(m->VanillaLight.setSsr(true));frames(c);var metalOn=capture(c,"ssr-metal-on");
|
||||
check(changed(metalOff,metalOn)>100,"Solid PBR reflects landscape");
|
||||
look(c,server,.00002f,12.00002f);frames(c);var tiny=capture(c,"ssr-metal-tiny-motion");
|
||||
check(stableMetal(metalOn,tiny),"Tiny camera movement keeps material texels stable");look(c,server,0,12);frames(c);
|
||||
server.runOnServer(s->{
|
||||
command(s,"fill -7 67 9 7 70 9 iron_block");
|
||||
for(int x=-6;x<=6;x+=2)command(s,"setblock "+x+" 67 8 polished_andesite_stairs[facing=south]");
|
||||
});settle(c);frames(c);var intersections=capture(c,"ssr146-intersections");
|
||||
look(c,server,.00002f,12.00002f);frames(c);var shifted=capture(c,"ssr146-intersections-tiny-motion");
|
||||
check(stableIntersections(intersections,shifted),"Block intersections remain stable during subpixel motion");
|
||||
look(c,server,0,12);frames(c);
|
||||
c.runOnClient(m->{VanillaLight.setPixelShadows(true);VanillaLight.setSsgi(true);VanillaLight.setColoredLights(true);VanillaLight.setBloom(true);VanillaLight.setSunshafts(true);});
|
||||
frames(c);capture(c,"ssr-combined");
|
||||
server.runOnServer(s->command(s,"time set 18000"));c.waitTicks(15);frames(c);capture(c,"ssr-moon");
|
||||
server.runOnServer(s->command(s,"time set 6000"));c.waitTicks(15);frames(c);
|
||||
c.runOnClient(m->{VanillaLight.setPixelShadows(false);VanillaLight.setSsgi(false);VanillaLight.setColoredLights(false);VanillaLight.setBloom(false);VanillaLight.setSunshafts(false);});
|
||||
server.runOnServer(s->{for(int x=-7;x<=7;x++)for(int z=4;z<=14;z++)s.overworld().setBlockAndUpdate(new BlockPos(x,66,z),Blocks.STONE.defaultBlockState());});
|
||||
settle(c);c.runOnClient(m->VanillaLight.setSsr(false));c.waitTicks(8);capture(c,"ssr-rough-stone-off");
|
||||
c.runOnClient(m->VanillaLight.setSsr(true));frames(c);capture(c,"ssr-rough-stone-on");
|
||||
// Native panes, sloped water, cascade sides and a waterlogged slab share the receiver pass.
|
||||
server.runOnServer(s->{
|
||||
for(int y=66;y<=69;y++)s.overworld().setBlockAndUpdate(new BlockPos(-8,y,8),Blocks.GLASS_PANE.defaultBlockState());
|
||||
s.overworld().setBlockAndUpdate(new BlockPos(8,69,8),Blocks.WATER.defaultBlockState());
|
||||
s.overworld().setBlockAndUpdate(new BlockPos(-8,65,10),Blocks.OAK_SLAB.defaultBlockState().setValue(net.minecraft.world.level.block.state.properties.BlockStateProperties.WATERLOGGED,true));
|
||||
});settle(c);frames(c);capture(c,"ssr-flow-pane-waterlogged");
|
||||
server.runOnServer(s->{command(s,"fill -3 66 6 3 69 6 glass");command(s,"fill -3 66 8 3 69 8 light_blue_stained_glass");
|
||||
command(s,"setblock -4 67 6 blue_stained_glass_pane");});
|
||||
settle(c);c.runOnClient(m->VanillaLight.setSsr(false));c.waitTicks(8);capture(c,"ssr-stacked-coloured-glass-off");
|
||||
c.runOnClient(m->VanillaLight.setSsr(true));frames(c);capture(c,"ssr-stacked-coloured-glass-on");
|
||||
camera(c,server,12,64,8,70);look(c,server,0,0);c.waitTicks(15);
|
||||
c.runOnClient(m->check(!PixelReflections.allocated(),"Immersion suspends and releases SSR"));
|
||||
camera(c,server,.5,68,0,70);look(c,server,0,12);settle(c);frames(c);
|
||||
var reload=c.computeOnClient(net.minecraft.client.Minecraft::reloadResourcePacks);c.waitFor(m->reload.isDone()&&m.gui.overlay()==null,1200);reload.join();frames(c);capture(c,"ssr-reloaded");
|
||||
int reloadedUpdates=c.computeOnClient(m->ProceduralPbr.waterUpdates());c.waitTicks(20);
|
||||
c.runOnClient(m->check(ProceduralPbr.waterUpdates()>reloadedUpdates,"Reloaded atlas supplies a live native water clock"));
|
||||
int w=c.computeOnClient(m->m.getWindow().getScreenWidth()),h=c.computeOnClient(m->m.getWindow().getScreenHeight());
|
||||
c.runOnClient(m->m.getWindow().setWindowed(961,541));c.waitTicks(15);frames(c);capture(c,"ssr-resized");
|
||||
c.runOnClient(m->m.getWindow().setWindowed(w,h));c.waitTicks(15);frames(c);
|
||||
c.runOnClient(m->VanillaLight.setPbr(false));c.waitTicks(8);c.runOnClient(m->check(!PixelReflections.allocated(),"PBR OFF releases reflection resources"));
|
||||
System.out.println("SSR_PASS water; transparent glass; solids; OFF/zero; motion; reload; resize");
|
||||
}
|
||||
private static boolean stableIntersections(Frame a,Frame b){
|
||||
// The surrounding animated water is not a stability oracle for static blocks.
|
||||
int changed=0,count=0;
|
||||
for(int y=a.height()*30/100;y<a.height()*80/100;y++)for(int x=a.width()/4;x<a.width()*3/4;x++){
|
||||
int i=y*a.width()+x,aa=a.pixels()[i],bb=b.pixels()[i];count++;
|
||||
if(Math.abs((aa>>16&255)-(bb>>16&255))+Math.abs((aa>>8&255)-(bb>>8&255))+Math.abs((aa&255)-(bb&255))>12)changed++;
|
||||
}
|
||||
System.out.println("SSR146_INTERSECTIONS changed="+changed+" samples="+count);
|
||||
return changed<count*.001;
|
||||
}
|
||||
private static boolean stableMetal(Frame a,Frame b){int changes=0,count=0;
|
||||
for(int y=a.height()*58/100;y<a.height()*68/100;y++)for(int x=a.width()*3/10;x<a.width()*7/10;x++){
|
||||
int i=y*a.width()+x,aa=a.pixels()[i],bb=b.pixels()[i];count++;
|
||||
if(Math.abs((aa>>16&255)-(bb>>16&255))+Math.abs((aa>>8&255)-(bb>>8&255))+Math.abs((aa&255)-(bb&255))>12)changes++;
|
||||
}return changes<count*.02;
|
||||
}
|
||||
private static double colourSignal(Frame a,Frame b){long total=0;int count=0;
|
||||
for(int y=a.height()/2;y<a.height();y++)for(int x=a.width()/4;x<a.width()*3/4;x++){
|
||||
int i=y*a.width()+x,aa=a.pixels()[i],bb=b.pixels()[i];total+=(bb>>16&255)-(aa>>16&255)-(bb&255)+(aa&255);count++;
|
||||
}return total/(double)count;
|
||||
}
|
||||
private static void benchmark(ClientGameTestContext c,String name){
|
||||
c.runOnClient(m->{m.options.enableVsync().set(false);m.options.framerateLimit().set(net.minecraft.client.Options.UNLIMITED_FRAMERATE_CUTOFF);
|
||||
m.options.inactivityFpsLimit().set(net.minecraft.client.InactivityFpsLimit.MINIMIZED);m.getWindow().setWindowed(1920,1080);});
|
||||
c.waitTicks(20);frames(c);
|
||||
c.runOnClient(m->check(m.gameRenderer.mainRenderTarget().width==1920&&m.gameRenderer.mainRenderTarget().height==1080,"Benchmark uses 1080p"));
|
||||
for(boolean on:new boolean[]{false,true}){
|
||||
c.runOnClient(m->VanillaLight.setSsr(on));c.waitTicks(30);
|
||||
c.runOnClient(m->fr.koka.sanctuary.gametest.ReflectionMetrics.start());c.waitTicks(80);
|
||||
var result=c.computeOnClient(m->fr.koka.sanctuary.gametest.ReflectionMetrics.stop());
|
||||
c.runOnClient(m->{check(ProceduralPbr.solidBytes()<=24L*1024*1024&&ProceduralPbr.transparentBytes()<=8L*1024*1024,"Mesh memory budgets");
|
||||
System.out.println("SSR_MEMORY "+name+" enabled="+on+" solid="+ProceduralPbr.solidBytes()+" transparent="+ProceduralPbr.transparentBytes()+" targets="+PixelReflections.targetBytes());});
|
||||
check(result[0]>=30,"Benchmark has enough frame samples");
|
||||
System.out.println("SSR_BENCH "+name+" enabled="+on+" count="+result[0]+" medianMs="+result[1]+" p95Ms="+result[2]+" gpuCount="+result[3]+" gpuMedianMs="+result[4]+" gpuP95Ms="+result[5]);
|
||||
}
|
||||
}
|
||||
private static void dumpLandscape(ClientGameTestContext c){
|
||||
var done=new java.util.concurrent.CompletableFuture<Void>();
|
||||
c.runOnClient(m->{
|
||||
var src=PixelReflections.landscape();
|
||||
var target=new com.mojang.blaze3d.pipeline.TextureTarget("SSR test source",src.getWidth(0),src.getHeight(0),com.mojang.renderpearl.api.GpuFormat.RGBA8_UNORM,null);
|
||||
com.mojang.blaze3d.systems.RenderSystem.getDevice().createCommandEncoder().copyTextureToTexture(src,target.getColorTexture(),0,0,0,0,0,target.width,target.height);
|
||||
net.minecraft.client.Screenshot.takeScreenshot(target,image->{try(image){image.writeToFile(java.nio.file.Path.of("screenshots/ssr-opaque-source.png"));done.complete(null);}catch(Exception e){done.completeExceptionally(e);}finally{target.destroyBuffers();}});
|
||||
});c.waitFor(m->done.isDone(),200);done.join();
|
||||
}
|
||||
private static long changed(Frame a,Frame b){long n=0;for(int y=a.height()/2;y<a.height();y++)for(int x=0;x<a.width();x++){
|
||||
int i=y*a.width()+x,aa=a.pixels()[i],bb=b.pixels()[i];
|
||||
if(Math.abs((aa>>16&255)-(bb>>16&255))+Math.abs((aa>>8&255)-(bb>>8&255))+Math.abs((aa&255)-(bb&255))>8)n++;
|
||||
}return n;}
|
||||
private static void pbrFrames(ClientGameTestContext c){int start=c.computeOnClient(m->ProceduralPbr.renderedFrames());c.waitFor(m->ProceduralPbr.applied()&&ProceduralPbr.renderedFrames()>start+8,1200);}
|
||||
private static void frames(ClientGameTestContext c){int start=c.computeOnClient(m->PixelReflections.frames());c.waitFor(m->PixelReflections.applied()&&ProceduralPbr.ready()&&PixelReflections.frames()>start+8,1200);}
|
||||
private static void settle(ClientGameTestContext c){c.waitTicks(45);c.waitFor(m->m.levelRenderer.hasRenderedAllSections()&&ProceduralPbr.ready(),1200);c.waitTicks(10);}
|
||||
}
|
||||
+61
-8
@@ -36,6 +36,14 @@ import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Real terrain/depth-map checks on a new flat seed 122 world; GPU readback is test-only. */
|
||||
public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
||||
private final boolean bloomOnly,ssgiOnly,pbrOnly,closeupOnly,sunshaftOnly,coloredOnly;
|
||||
public PixelShadows122ClientChecks(){this(false);}
|
||||
PixelShadows122ClientChecks(boolean bloomOnly){this(bloomOnly,false);}
|
||||
PixelShadows122ClientChecks(boolean bloomOnly,boolean ssgiOnly){this(bloomOnly,ssgiOnly,false);}
|
||||
PixelShadows122ClientChecks(boolean bloomOnly,boolean ssgiOnly,boolean pbrOnly){this(bloomOnly,ssgiOnly,pbrOnly,false);}
|
||||
PixelShadows122ClientChecks(boolean bloomOnly,boolean ssgiOnly,boolean pbrOnly,boolean closeupOnly){this(bloomOnly,ssgiOnly,pbrOnly,closeupOnly,false);}
|
||||
PixelShadows122ClientChecks(boolean bloomOnly,boolean ssgiOnly,boolean pbrOnly,boolean closeupOnly,boolean sunshaftOnly){this(bloomOnly,ssgiOnly,pbrOnly,closeupOnly,sunshaftOnly,false);}
|
||||
PixelShadows122ClientChecks(boolean bloomOnly,boolean ssgiOnly,boolean pbrOnly,boolean closeupOnly,boolean sunshaftOnly,boolean coloredOnly){this.coloredOnly=coloredOnly;this.sunshaftOnly=sunshaftOnly;this.bloomOnly=bloomOnly;this.ssgiOnly=ssgiOnly;this.pbrOnly=pbrOnly;this.closeupOnly=closeupOnly;}
|
||||
private static final int FLOOR = 64;
|
||||
private static final AABB PILLAR = new AABB(0, FLOOR + 1, 0, 2, FLOOR + 9, 2);
|
||||
private static final BlockPos SLAB = new BlockPos(5, FLOOR + 3, -5);
|
||||
@@ -44,6 +52,15 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
||||
|
||||
@Override
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
c.runOnClient(m -> {
|
||||
var info = com.mojang.blaze3d.systems.RenderSystem.getDevice().getDeviceInfo();
|
||||
String expected = System.getProperty("sanctuary.test.graphicsBackend", "vulkan");
|
||||
check(net.minecraft.client.PreferredGraphicsApi.DEFAULT.getBackendsToTry()[0] instanceof com.mojang.renderpearl.backend.vulkan.VulkanBackend, "Default tries native Vulkan first");
|
||||
check(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()
|
||||
+ " / " + info.driverInfo() + " / zeroToOne=" + info.isZZeroToOne());
|
||||
});
|
||||
var path = FabricLoader.getInstance().getConfigDir().resolve("sanctuary-shaders.json");
|
||||
final byte[] originalPreferences;
|
||||
try {
|
||||
@@ -75,6 +92,9 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
||||
m.options.bobView().set(false);
|
||||
m.options.pauseOnLostFocus = false;
|
||||
m.options.setCameraType(CameraType.FIRST_PERSON);
|
||||
String transparency = System.getProperty("sanctuary.test.improvedTransparency");
|
||||
if (transparency != null) m.options.improvedTransparency().set(Boolean.parseBoolean(transparency));
|
||||
System.out.println("SHADER139_TRANSPARENCY " + m.options.improvedTransparency().get());
|
||||
checkPreferences();
|
||||
});
|
||||
try (var world = c.worldBuilder().adjustSettings(s -> {
|
||||
@@ -130,6 +150,14 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
||||
c.runOnClient(m -> check(VanillaLight.active(), "Native Sanctuary shader is active without an external engine"));
|
||||
float morning = c.computeOnClient(m -> m.gameRenderer.gameRenderState().levelRenderState.skyRenderState.sunAngle);
|
||||
|
||||
if(Boolean.getBoolean("sanctuary.test.water144")){Water144ClientChecks.run(c,server);return;}
|
||||
if(coloredOnly){if(Boolean.getBoolean("sanctuary.test.directional141Only")){DirectionalLights141ClientChecks.run(c,server);return;}ColoredLights139ClientChecks.run(c,server);if(Boolean.getBoolean("sanctuary.test.colored140"))ColoredLights140ClientChecks.run(c,server);if(Boolean.getBoolean("sanctuary.test.directional141"))DirectionalLights141ClientChecks.run(c,server);return;}
|
||||
if(sunshaftOnly){Sunshafts137ClientChecks.run(c,server);return;}
|
||||
if(closeupOnly){SsgiCloseup135ClientChecks.run(c,server);Pbr134ClientChecks.run(c,server);return;}
|
||||
if(Boolean.getBoolean("sanctuary.test.ssr")){PixelReflectionsClientChecks.run(c,server);return;}
|
||||
if(pbrOnly){Pbr134ClientChecks.run(c,server);return;}
|
||||
if(ssgiOnly){Ssgi132ClientChecks.run(c,server);return;}
|
||||
if(bloomOnly){Bloom130ClientChecks.run(c,server);return;}
|
||||
// Keep VanillaLight enabled for both frames: changing its fog/grade would invalidate this oracle.
|
||||
shadows(c, false);
|
||||
var off = capture(c, "day-off");
|
||||
@@ -145,7 +173,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
||||
check(PixelShadows.shadowMap() != null, "Daylight allocates a real shadow target");
|
||||
check(PixelShadows.shadowMap().width == PixelShadows.mapSize(32, 16), "Shadow map follows selected quality");
|
||||
});
|
||||
for (int pixels : new int[]{8, 32, 16}) {
|
||||
for (int pixels : new int[]{8, 32, 64, 128, 16}) {
|
||||
c.runOnClient(m -> VanillaLight.setShadowPixels(pixels));
|
||||
activeFrames(c);
|
||||
c.runOnClient(m -> check(PixelShadows.shadowMap().width == PixelShadows.mapSize(32, pixels),
|
||||
@@ -236,12 +264,15 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
||||
unchanged(beforeReload, capture(c, "after-reload"), "Resource reload preserves the rendered ground shadow");
|
||||
RealtimeShadows129ClientChecks.run(c, server);
|
||||
EdgeHighlights129ClientChecks.run(c, server);
|
||||
Bloom130ClientChecks.run(c, server);
|
||||
Ssgi132ClientChecks.run(c, server);
|
||||
Pbr134ClientChecks.run(c, server);
|
||||
c.getInput().pressKey(InputConstants.KEY_F1);
|
||||
for (String locale : new String[]{"fr_fr", "en_us"}) {
|
||||
c.runOnClient(m -> {
|
||||
m.getLanguageManager().setSelected(locale);
|
||||
m.getLanguageManager().onResourceManagerReload(m.getResourceManager());
|
||||
for (String key : new String[]{"pixel_shadows", "shadow_pixels", "shadow_distance", "edge_highlights", "edge_intensity", "edge_distance"}) {
|
||||
for (String key : new String[]{"pixel_shadows", "shadow_pixels", "shadow_distance", "edge_highlights", "edge_intensity", "edge_distance", "precision", "ssgi", "ssgi_intensity", "bloom", "bloom_radius", "ore_emission"}) {
|
||||
String fullKey = "sanctuary.shaders." + key;
|
||||
check(!Component.translatable(fullKey).getString().equals(fullKey), "Translated " + locale + " " + key);
|
||||
}
|
||||
@@ -253,7 +284,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
||||
});
|
||||
});
|
||||
c.waitTicks(3);
|
||||
c.takeScreenshot("sanctuary-beta129-shadows-options-" + locale);
|
||||
c.takeScreenshot("sanctuary-beta142-shader-options-" + locale);
|
||||
}
|
||||
c.runOnClient(m -> m.gui.setScreen(null));
|
||||
}
|
||||
@@ -417,11 +448,32 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
||||
? java.util.stream.Stream.concat(java.util.stream.Stream.of(widget), widgets(child)) : java.util.stream.Stream.of(widget));
|
||||
}
|
||||
|
||||
private static void checkDefaults135() {
|
||||
try {
|
||||
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.ssrIntensity()==20&&VanillaLight.ssgiIntensity()==35,"PBR 50 and SSR 20 defaults, SSGI strength retained");
|
||||
Files.writeString(path,"{\"pbr\":false,\"ssgi\":false,\"bloomIntensity\":73}");forgetPreferences();
|
||||
check(!VanillaLight.pbr()&&!VanillaLight.ssgi()&&VanillaLight.bloomIntensity()==73,"Explicit personal toggles and intensity survive changed defaults");
|
||||
Files.writeString(path,"{\"bloom\":false}");forgetPreferences();
|
||||
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);}
|
||||
}
|
||||
|
||||
private static void checkPreferences() {
|
||||
checkDefaults135();
|
||||
VanillaLight.setSunshafts(false);VanillaLight.setLensFlare(false);
|
||||
VanillaLight.setPbr(false);
|
||||
VanillaLight.setSsgi(false);
|
||||
VanillaLight.setBloom(false);
|
||||
VanillaLight.setOreEmission(false);
|
||||
VanillaLight.setEnabled(true);
|
||||
VanillaLight.setEdgeHighlights(false);
|
||||
VanillaLight.setSaturation(100);
|
||||
for (int[] pair : new int[][]{{Integer.MIN_VALUE, 8}, {11, 8}, {12, 16}, {16, 16}, {23, 16}, {24, 32}, {Integer.MAX_VALUE, 32}}) {
|
||||
for (int[] pair : new int[][]{{Integer.MIN_VALUE, 8}, {11, 8}, {12, 16}, {16, 16}, {23, 16}, {24, 32}, {47,32}, {48,64}, {64,64}, {95,64}, {96,128}, {128,128}, {Integer.MAX_VALUE,128}}) {
|
||||
VanillaLight.setShadowPixels(pair[0]);
|
||||
forgetPreferences();
|
||||
check(VanillaLight.shadowPixels() == pair[1], "Persisted pixel density rounds/clamps " + pair[0]);
|
||||
@@ -472,7 +524,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
||||
static void look(ClientGameTestContext c, TestServerContext server, float yaw, float pitch) {
|
||||
server.runOnServer(s -> {
|
||||
var p = s.getPlayerList().getPlayers().getFirst();
|
||||
command(s, "tp @a " + p.getX() + " " + p.getY() + " " + p.getZ() + " " + yaw + " " + pitch);
|
||||
command(s, "tp @a " + p.getX() + " " + p.getY() + " " + p.getZ() + String.format(java.util.Locale.ROOT," %.8f %.8f",yaw,pitch));
|
||||
});
|
||||
c.waitFor(m -> Math.abs(m.player.getXRot() - pitch) < .00001
|
||||
&& Math.abs(m.player.getYRot() - yaw) < .00001, 200);
|
||||
@@ -517,7 +569,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
||||
}
|
||||
|
||||
static Frame capture(ClientGameTestContext c, String name) {
|
||||
if (name != null) c.takeScreenshot("sanctuary-beta129-shadows-" + 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) {
|
||||
@@ -618,11 +670,11 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
||||
}
|
||||
|
||||
private record Options(int distance, int simulationDistance, int fov, boolean bob,
|
||||
boolean pause, CameraType camera, String language) {
|
||||
boolean pause, CameraType camera, String language, boolean improvedTransparency) {
|
||||
static Options capture(Minecraft m) {
|
||||
return new Options(m.options.renderDistance().get(), m.options.simulationDistance().get(), m.options.fov().get(),
|
||||
m.options.bobView().get(), m.options.pauseOnLostFocus,
|
||||
m.options.getCameraType(), m.getLanguageManager().getSelected());
|
||||
m.options.getCameraType(), m.getLanguageManager().getSelected(), m.options.improvedTransparency().get());
|
||||
}
|
||||
|
||||
void restore(Minecraft m) {
|
||||
@@ -632,6 +684,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
||||
m.options.bobView().set(bob);
|
||||
m.options.pauseOnLostFocus = pause;
|
||||
m.options.setCameraType(camera);
|
||||
m.options.improvedTransparency().set(improvedTransparency);
|
||||
m.getLanguageManager().setSelected(language);
|
||||
m.getLanguageManager().onResourceManagerReload(m.getResourceManager());
|
||||
}
|
||||
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import static fr.koka.sanctuary.client.shader.PixelShadows122ClientChecks.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.TestServerContext;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
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.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());
|
||||
}});
|
||||
camera(c,server,10.5,69,5.5,65);look(c,server,0,40);settle(c);
|
||||
var flatOff=capture(c,"ssgi-flat-off");enable(c,true);unchanged(flatOff,capture(c,"ssgi-flat-on"),"Coplanar ground does not bounce light onto itself");
|
||||
wall(server,12,Blocks.CONCRETE.red().defaultBlockState());settle(c);enable(c,false);var redOff=capture(c,"ssgi-red-off");
|
||||
c.runOnClient(m->VanillaLight.setSsgiIntensity(35));
|
||||
enable(c,true);var defaultRed=measure(redOff,capture(c,"ssgi-red-default-35"));
|
||||
System.out.println("SSGI133_DEFAULT "+defaultRed);
|
||||
check(defaultRed.visible>5000&&defaultRed.strong>500,"Default intensity produces 8/255 gain over 5000 pixels and 16/255 over 500 pixels: "+defaultRed);
|
||||
c.runOnClient(m->VanillaLight.setSsgiIntensity(90));frames(c);
|
||||
var redOn=capture(c,"ssgi-red-on");var red=measure(redOff,redOn);System.out.println("SSGI133_RED "+red);
|
||||
check(red.changed>30&&red.red>red.blue*1.5+20,"Visible red wall tints neutral ground: "+red);
|
||||
unchanged(redOn,capture(c,null),"Stationary SSGI has no animated noise or accumulating feedback");
|
||||
look(c,server,0.00002f,40.00002f);frames(c);unchanged(redOn,capture(c,null),"Subpixel motion keeps actual red bounce stable");
|
||||
look(c,server,5,40);frames(c);capture(c,"ssgi-camera-turned");
|
||||
look(c,server,0,40);frames(c);unchanged(redOn,capture(c,null),"Returning the camera leaves no temporal color trails");
|
||||
c.runOnClient(m->VanillaLight.setSsgiIntensity(20));frames(c);var low=measure(redOff,capture(c,"ssgi-strength-20"));
|
||||
check(red.red>low.red*1.5,"Intensity adjusts native color bounce: "+red+" / "+low);
|
||||
c.runOnClient(m->VanillaLight.setSsgiIntensity(0));c.waitTicks(6);unchanged(redOff,capture(c,null),"Zero intensity restores source image");
|
||||
c.runOnClient(m->check(!ScreenSpaceGI.allocated()&&!ScreenSpaceGI.applied(),"Zero frees SSGI targets"));
|
||||
c.runOnClient(m->VanillaLight.setSsgiIntensity(90));frames(c);
|
||||
int oldWidth=c.computeOnClient(m->m.getWindow().getScreenWidth()),oldHeight=c.computeOnClient(m->m.getWindow().getScreenHeight());
|
||||
c.runOnClient(m->m.getWindow().setWindowed(961,541));
|
||||
c.waitFor(m->m.gameRenderer.mainRenderTarget().width!=redOn.width(),200);frames(c);
|
||||
enable(c,false);var resizedOff=capture(c,null);enable(c,true);var resized=measure(resizedOff,capture(c,"ssgi-resized"));
|
||||
check(resized.changed>30&&resized.red>resized.blue*1.5+20,"Resizing reallocates a working SSGI pass: "+resized);
|
||||
c.runOnClient(m->m.getWindow().setWindowed(oldWidth,oldHeight));
|
||||
c.waitFor(m->m.gameRenderer.mainRenderTarget().width==redOn.width(),200);frames(c);
|
||||
wall(server,12,Blocks.CONCRETE.blue().defaultBlockState());settle(c);enable(c,false);var blueOff=capture(c,"ssgi-blue-off");enable(c,true);var blue=measure(blueOff,capture(c,"ssgi-blue-on"));
|
||||
c.runOnClient(m->VanillaLight.setSsgiIntensity(35));frames(c);
|
||||
var defaultBlue=measure(blueOff,capture(c,"ssgi-blue-default-35"));
|
||||
check(defaultBlue.visible>5000&&defaultBlue.strong>500&&defaultBlue.blue>defaultBlue.red*1.5+20,"Default blue bounce is visible too: "+defaultBlue);
|
||||
c.runOnClient(m->VanillaLight.setSsgiIntensity(90));frames(c);
|
||||
System.out.println("SSGI133_BLUE "+blue+" default="+defaultBlue);check(blue.changed>30&&blue.blue>blue.red*1.5+20,"Live blue wall changes bounced hue: "+blue);
|
||||
wall(server,11,Blocks.CONCRETE.white().defaultBlockState());settle(c);enable(c,false);var coveredOff=capture(c,null);enable(c,true);var covered=capture(c,"ssgi-covered-blue");
|
||||
wall(server,12,Blocks.CONCRETE.red().defaultBlockState());settle(c);unchanged(covered,capture(c,"ssgi-covered-red"),"Hidden wall color cannot pass through foreground wall");
|
||||
var reload=c.computeOnClient(net.minecraft.client.Minecraft::reloadResourcePacks);c.waitFor(m->reload.isDone()&&m.gui.overlay()==null,1200);reload.join();frames(c);unchanged(covered,capture(c,"ssgi-reloaded"),"Resource reload restores SSGI");
|
||||
look(c,server,0.00002f,40.00002f);frames(c);unchanged(covered,capture(c,null),"Subpixel camera movement keeps bounce stable");
|
||||
look(c,server,0,40);frames(c);enable(c,false);unchanged(coveredOff,capture(c,null),"OFF leaves no residual tint");
|
||||
c.runOnClient(m->check(!ScreenSpaceGI.allocated(),"OFF frees SSGI resources"));
|
||||
c.runOnClient(m->{VanillaLight.setSsgiIntensity(35);VanillaLight.setPixelShadows(true);VanillaLight.setEdgeHighlights(true);VanillaLight.setBloom(true);VanillaLight.setOreEmission(true);});
|
||||
enable(c,true);c.waitFor(m->PixelShadows.applied()&&PixelShadows.ready()&&EdgeHighlights.applied()&&Bloom.applied(),1200);
|
||||
capture(c,"ssgi-combined-shader");
|
||||
c.runOnClient(m->VanillaLight.setEnabled(false));c.waitTicks(8);
|
||||
c.runOnClient(m->check(!ScreenSpaceGI.allocated(),"Main shader OFF releases SSGI too"));
|
||||
c.runOnClient(m->VanillaLight.setEnabled(true));frames(c);enable(c,false);
|
||||
motion(c,server);
|
||||
SsgiCloseup135ClientChecks.run(c,server);
|
||||
System.out.println("SSGI133_PASS red/blue diffuse bounce; intensity; flat rejection; foreground occlusion; static stability; tiny camera motion; resize; reload; combined shader; OFF/zero; preferences; red="+red+" blue="+blue);
|
||||
}
|
||||
private static void motion(ClientGameTestContext c,TestServerContext server){
|
||||
c.runOnClient(m->{VanillaLight.setPbr(false);VanillaLight.setSsgi(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);VanillaLight.setPixelShadows(false);VanillaLight.setEdgeHighlights(false);});
|
||||
wall(server,11,Blocks.AIR.defaultBlockState());
|
||||
server.runOnServer(s->{
|
||||
command(s,"summon minecraft:cow 8.5 65 9.5 {NoAI:1b,Silent:1b,NoGravity:1b,Invulnerable:1b,Tags:[\"ssgi134_test\"]}");
|
||||
for(int x=11;x<=13;x++)for(int z=8;z<=10;z++){
|
||||
s.overworld().setBlockAndUpdate(new BlockPos(x,64,z),Blocks.GRASS_BLOCK.defaultBlockState());
|
||||
s.overworld().setBlockAndUpdate(new BlockPos(x,65,z),Blocks.SHORT_GRASS.defaultBlockState());
|
||||
}
|
||||
});settle(c);
|
||||
for(int intensity:new int[]{35,100}){
|
||||
c.runOnClient(m->VanillaLight.setSsgiIntensity(intensity));
|
||||
Frame start=null;double worstGrass=0;
|
||||
for(int step=0;step<=12;step++){
|
||||
double x=4.5+step;camera(c,server,x,66.5,5.5,65);
|
||||
look(c,server,(float)Math.toDegrees(Math.atan2(x-10.5,6.5)),10);
|
||||
enable(c,false);var base=capture(c,"ssgi-motion-"+intensity+"-"+step+"-off");
|
||||
enable(c,true);var frame=capture(c,"ssgi-motion-"+intensity+"-"+step+"-on");
|
||||
double grass=grassGain(base,frame);worstGrass=Math.max(worstGrass,grass);
|
||||
check(grass<(intensity==35?12:30),"Lateral view avoids excessive grass brightening at "+intensity+" / "+step+": "+grass);
|
||||
if(step==0)start=frame;
|
||||
}
|
||||
System.out.println("SSGI134_GRASS intensity="+intensity+" maxMeanGreenGain="+worstGrass);
|
||||
camera(c,server,4.5,66.5,5.5,65);look(c,server,(float)Math.toDegrees(Math.atan2(-6,6.5)),10);frames(c);
|
||||
unchanged(start,capture(c,null),"Twelve-block lateral movement returns without stale SSGI history at "+intensity);
|
||||
}
|
||||
enable(c,false);c.runOnClient(m->VanillaLight.setSsgiIntensity(35));
|
||||
server.runOnServer(s->command(s,"kill @e[tag=ssgi134_test]"));
|
||||
System.out.println("SSGI134_MOTION_PASS 12-block lateral sweep, grass/cow/blocks, OFF/ON at35 and100, return without history");
|
||||
}
|
||||
private static double grassGain(Frame off,Frame on){
|
||||
int count=0;long gain=0;
|
||||
for(int i=0;i<off.pixels().length;i++){
|
||||
int a=off.pixels()[i],r=a>>16&255,g=a>>8&255,b=a&255;
|
||||
if(g>r*1.15&&g>b*1.3&&g>45){count++;gain+=Math.max(0,(on.pixels()[i]>>8&255)-g);}
|
||||
}
|
||||
check(count>500,"Motion fixture includes visible vegetation");return gain/(double)count;
|
||||
}
|
||||
private static void wall(TestServerContext server,int z,BlockState state){server.runOnServer(s->{for(int x=6;x<=15;x++)for(int y=65;y<=(z==11?72:69);y++)s.overworld().setBlockAndUpdate(new BlockPos(x,y,z),state);});}
|
||||
private static void settle(ClientGameTestContext c){c.waitTicks(30);c.waitFor(m->m.levelRenderer.hasRenderedAllSections(),1200);c.waitTicks(8);}
|
||||
private static void enable(ClientGameTestContext c,boolean enabled){c.runOnClient(m->VanillaLight.setSsgi(enabled));if(enabled)frames(c);else c.waitTicks(8);}
|
||||
private static void frames(ClientGameTestContext c){int start=c.computeOnClient(m->ScreenSpaceGI.renderedFrames());c.waitFor(m->ScreenSpaceGI.applied()&&ScreenSpaceGI.renderedFrames()>start+10,1200);c.waitTicks(6);}
|
||||
private static void preferences(){try{
|
||||
var p=FabricLoader.getInstance().getConfigDir().resolve("sanctuary-shaders.json");Files.writeString(p,"{\"bloom\":false,\"shadowPixels\":128}");forgetPreferences();
|
||||
check(VanillaLight.ssgi()&&VanillaLight.ssgiIntensity()==35&&!VanillaLight.bloom()&&VanillaLight.shadowPixels()==128,"Legacy choices preserved; missing SSGI setting defaults ON at 35");
|
||||
for(int i:new int[]{-10,0,35,90,140}){VanillaLight.setSsgiIntensity(i);VanillaLight.setSsgi(true);forgetPreferences();check(VanillaLight.ssgi()&&VanillaLight.ssgiIntensity()==Math.clamp(i,0,100),"SSGI persists and clamps");}
|
||||
VanillaLight.setSsgiIntensity(90);
|
||||
}catch(Exception e){throw new AssertionError(e);}}
|
||||
private record Gain(int changed,int visible,int strong,double red,double green,double blue){}
|
||||
private static Gain measure(Frame off,Frame on){int count=0,visible=0,strong=0;double r=0,g=0,b=0;for(int i=0;i<off.pixels().length;i++){
|
||||
int a=off.pixels()[i],ar=a>>16&255,ag=a>>8&255,ab=a&255;
|
||||
if(Math.max(ar,Math.max(ag,ab))-Math.min(ar,Math.min(ag,ab))>25||ar<60||ar>245)continue;
|
||||
int z=on.pixels()[i];int dr=(z>>16&255)-ar,dg=(z>>8&255)-ag,db=(z&255)-ab;
|
||||
if(Math.max(dr,Math.max(dg,db))>=16)strong++;
|
||||
if(Math.max(dr,Math.max(dg,db))>=8)visible++;
|
||||
if(Math.max(dr,Math.max(dg,db))>1)count++;r+=Math.max(0,dr);g+=Math.max(0,dg);b+=Math.max(0,db);
|
||||
}return new Gain(count,visible,strong,r,g,b);}
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
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 low-angle and near-plane regression fixture; never opens a personal world. */
|
||||
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.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");
|
||||
for(int x=-5;x<=24;x++)for(int z=-5;z<=24;z++){
|
||||
// Contain lava: no flow below the platform into the following PBR fixture.
|
||||
s.overworld().setBlockAndUpdate(new BlockPos(x,63,z),Blocks.STONE.defaultBlockState());
|
||||
for(int y=64;y<=102;y++)s.overworld().setBlockAndUpdate(new BlockPos(x,y,z),y==64?Blocks.STONE.defaultBlockState():Blocks.AIR.defaultBlockState());
|
||||
if(x>=5&&x<=16&&z>=4&&z<=14)s.overworld().setBlockAndUpdate(new BlockPos(x,64,z),Blocks.LAVA.defaultBlockState());
|
||||
if(x>=5&&x<=16&&z>=8&&z<=14)s.overworld().setBlockAndUpdate(new BlockPos(x,68,z),Blocks.STONE.defaultBlockState());
|
||||
if(x>=5&&x<=16&&z==12)for(int y=65;y<=67;y++)s.overworld().setBlockAndUpdate(new BlockPos(x,y,z),Blocks.CONCRETE.red().defaultBlockState());
|
||||
}
|
||||
});c.waitTicks(40);c.waitFor(m->m.levelRenderer.hasRenderedAllSections(),1200);
|
||||
for(int scene=0;scene<2;scene++)for(int intensity:new int[]{35,100}){
|
||||
c.runOnClient(m->VanillaLight.setSsgiIntensity(intensity));
|
||||
for(int step=0;step<7;step++){
|
||||
// Eye approaches the underside, crossing the old fixed normal-area threshold.
|
||||
double gap=new double[]{2.0,1.0,.65,.4,.2,.1,.04}[step];
|
||||
camera(c,server,10.5,68-gap-1.62,scene==0?9.5:11.5,65);look(c,server,0,-25);
|
||||
c.runOnClient(m->VanillaLight.setSsgi(false));c.waitTicks(8);
|
||||
var off=capture(c,"closeup-"+scene+"-"+intensity+"-"+step+"-off");
|
||||
c.runOnClient(m->VanillaLight.setSsgi(true));c.waitFor(m->ScreenSpaceGI.applied(),1200);c.waitTicks(12);
|
||||
var on=capture(c,"closeup-"+scene+"-"+intensity+"-"+step+"-on");
|
||||
var stats=rowJump(off,on);
|
||||
System.out.println("SSGI135_CLOSEUP scene="+scene+" intensity="+intensity+" gap="+gap+" "+stats);
|
||||
if(scene==0&&gap<=.1)check(stats.jump<2&&stats.gain<.1,"Near-plane bounce fades without a horizontal bright strip: "+stats);
|
||||
if(scene==1&&gap==.4)check(stats.rows>30&&stats.gain>1,"Close corner retains actual bounce instead of rejecting small world pixels: "+stats);
|
||||
if(gap==.04)check(stats.gain<.05,"At contact, indirect light fades to the source image: "+stats);
|
||||
}
|
||||
}
|
||||
int width=c.computeOnClient(m->m.getWindow().getScreenWidth()),height=c.computeOnClient(m->m.getWindow().getScreenHeight());
|
||||
camera(c,server,10.5,65.98,11.5,65);look(c,server,0,-25);
|
||||
c.runOnClient(m->{VanillaLight.setSsgiIntensity(100);m.getWindow().setWindowed(1280,720);});
|
||||
c.waitFor(m->m.gameRenderer.mainRenderTarget().width!=width,200);c.waitTicks(15);
|
||||
c.runOnClient(m->VanillaLight.setSsgi(false));c.waitTicks(8);var largeOff=capture(c,"closeup-large-off");
|
||||
c.runOnClient(m->VanillaLight.setSsgi(true));c.waitTicks(15);var large=capture(c,"closeup-large-on");
|
||||
var largeStats=rowJump(largeOff,large);check(largeStats.rows>30&&largeStats.gain>1,"High resolution with a visible source retains bounce: "+largeStats);
|
||||
look(c,server,.00002f,-25.00002f);c.waitTicks(10);unchanged(large,capture(c,null),"Closeup has no subpixel normal threshold flicker");
|
||||
look(c,server,0,-65);c.runOnClient(m->VanillaLight.setSsgi(false));c.waitTicks(8);var steepOff=capture(c,"closeup-offscreen-source-off");
|
||||
c.runOnClient(m->VanillaLight.setSsgi(true));c.waitTicks(15);unchanged(steepOff,capture(c,"closeup-offscreen-source-on"),"Looking away from the source does not invent hidden light");
|
||||
c.runOnClient(m->m.getWindow().setWindowed(width,height));c.waitFor(m->m.gameRenderer.mainRenderTarget().width==width,200);c.waitTicks(15);
|
||||
c.runOnClient(m->{VanillaLight.setPbr(true);VanillaLight.setBloom(true);VanillaLight.setOreEmission(true);VanillaLight.setPixelShadows(true);VanillaLight.setEdgeHighlights(true);});
|
||||
c.waitFor(m->ProceduralPbr.applied()&&ProceduralPbr.ready()&&Bloom.applied()&&PixelShadows.applied()&&PixelShadows.ready(),1200);
|
||||
c.waitTicks(15);capture(c,"closeup-combined");
|
||||
c.runOnClient(m->{VanillaLight.setSsgi(false);VanillaLight.setSsgiIntensity(35);VanillaLight.setPbr(false);});
|
||||
System.out.println("SSGI135_CLOSEUP_PASS low-angle lava/stone and seven camera distances at 35/100");
|
||||
}
|
||||
private record Rows(double jump,double gain,int rows){}
|
||||
private static Rows rowJump(Frame a,Frame b){
|
||||
double previous=0,max=0,gain=0;int rows=0;
|
||||
for(int y=a.height()/8;y<a.height()*7/8;y++){
|
||||
double sum=0;int count=0;
|
||||
for(int x=a.width()/4;x<a.width()*3/4;x++){
|
||||
int i=y*a.width()+x,pa=a.pixels()[i],pb=b.pixels()[i];
|
||||
int r=pa>>16&255,g=pa>>8&255,blue=pa&255;
|
||||
if(Math.max(r,Math.max(g,blue))-Math.min(r,Math.min(g,blue))<12&&g>15){
|
||||
sum+=Math.max(0,(pb>>16&255)-r);count++;
|
||||
}
|
||||
}
|
||||
if(count<a.width()/8)continue;
|
||||
double mean=sum/count;if(rows>0)max=Math.max(max,Math.abs(mean-previous));
|
||||
previous=mean;gain+=mean;rows++;
|
||||
}
|
||||
return new Rows(max,gain/Math.max(rows,1),rows);
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import static fr.koka.sanctuary.client.shader.PixelShadows122ClientChecks.*;
|
||||
import static fr.koka.sanctuary.client.shader.Sunshafts137ClientChecks.*;
|
||||
import fr.koka.sanctuary.client.WeatherClient;
|
||||
import fr.koka.sanctuary.weather.DailyWeather;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.TestServerContext;
|
||||
|
||||
/** Actual server weather -> native atmospheric fog -> shader scattering, same scene and manual strength. */
|
||||
final class SunshaftWeather138ClientChecks {
|
||||
static void run(ClientGameTestContext c,TestServerContext server){
|
||||
check(Sunshafts.fogDensity(1024)==0&&Sunshafts.fogDensity(192)==0&&Sunshafts.fogDensity(80)==1,"Clear and full fog endpoints");
|
||||
float previous=0;for(int end=192;end>=80;end--){float d=Sunshafts.fogDensity(end);check(d>=previous&&d-previous<.014,"Fog density is bounded, smooth and monotonic");previous=d;}
|
||||
check(Sunshafts.fogDensity(Float.POSITIVE_INFINITY)==0&&Sunshafts.fogDensity(Float.NaN)==0,"Invalid fog distance cannot create NaN shafts");
|
||||
c.runOnClient(m->{VanillaLight.setSunshafts(false);VanillaLight.setSunshaftIntensity(35);VanillaLight.setPixelShadows(false);VanillaLight.setBloom(false);VanillaLight.setLensFlare(false);VanillaLight.setOreEmission(false);VanillaLight.setPbr(false);VanillaLight.setSsgi(false);VanillaLight.setEdgeHighlights(false);});
|
||||
roof(server,1);camera(c,server,100.5,101,110.5,70);aim(c,server);
|
||||
profile(c,server,DailyWeather.Kind.CLEAR);enable(c,false);var clearOff=capture(c,"weather-shafts-clear-off");enable(c,true);var clear=capture(c,"weather-shafts-clear-35");double clearEnergy=energy(clearOff,clear);
|
||||
server.runOnServer(s->command(s,"weather fog"));c.waitFor(m->WeatherClient.state().kind()==DailyWeather.Kind.FOG,200);
|
||||
float[] transition=new float[12];for(int i=0;i<transition.length;i++){c.waitTicks(20);transition[i]=c.computeOnClient(m->Sunshafts.fogDensity(m.gameRenderer.gameRenderState().levelRenderState.cameraRenderState.fogData.environmentalEnd));if(i>0)check(transition[i]>=transition[i-1]-.002&&transition[i]-transition[i-1]<.4,"Live fog transition has no abrupt density jump");}
|
||||
c.runOnClient(m->{check(m.gameRenderer.gameRenderState().levelRenderState.cameraRenderState.fogData.environmentalEnd<=80.01,"Fog command reaches actual native FogData");check(Sunshafts.applied(),"Atmospheric fog does not disable rays");});
|
||||
enable(c,false);var fogOff=capture(c,"weather-shafts-fog-off");enable(c,true);var fog=capture(c,"weather-shafts-fog-35");double fogEnergy=energy(fogOff,fog);
|
||||
check(fogEnergy>clearEnergy*1.7,"Fog visibly thickens scattering at the same intensity: "+clearEnergy+" -> "+fogEnergy);unchanged(fog,capture(c,null),"Stable fog adds no animated noise");
|
||||
var reload=c.computeOnClient(net.minecraft.client.Minecraft::reloadResourcePacks);c.waitFor(m->reload.isDone()&&m.gui.overlay()==null,1200);reload.join();settle(c);unchanged(fog,capture(c,"weather-shafts-fog-reload"),"Weather coupling survives shader reload");
|
||||
roof(server,0);settle(c);enable(c,false);var sealedOff=capture(c,null);enable(c,true);unchanged(sealedOff,capture(c,"weather-shafts-fog-sealed"),"Dense fog cannot illuminate a sealed room through walls");roof(server,1);settle(c);
|
||||
profile(c,server,DailyWeather.Kind.RAIN);enable(c,false);var rainOff=capture(c,"weather-shafts-rain-off");enable(c,true);double rainEnergy=energy(rainOff,capture(c,"weather-shafts-rain-35"));check(rainEnergy>0&&rainEnergy<fogEnergy,"Rain stays visible but below thick fog: "+rainEnergy);
|
||||
profile(c,server,DailyWeather.Kind.THUNDER);enable(c,false);var stormOff=capture(c,"weather-shafts-storm-off");enable(c,true);double stormEnergy=energy(stormOff,capture(c,"weather-shafts-storm-35"));check(stormEnergy<rainEnergy*.8,"Storm clouds reduce available sunlight: "+stormEnergy+" / "+rainEnergy);
|
||||
profile(c,server,DailyWeather.Kind.CLEAR);enable(c,true);var restored=capture(c,"weather-shafts-restored-clear");unchanged(clear,restored,"Returning to clear restores the original rays without accumulated brightness");
|
||||
c.runOnClient(m->check(VanillaLight.sunshaftIntensity()==35,"Weather never changes the player's intensity setting"));
|
||||
c.runOnClient(m->VanillaLight.setSunshaftIntensity(0));c.waitTicks(10);c.runOnClient(m->check(!Sunshafts.allocated(),"Zero still frees atmospheric scattering"));
|
||||
System.out.println("SUNSHAFT138_WEATHER_PASS clear="+clearEnergy+" fog="+fogEnergy+" rain="+rainEnergy+" storm="+stormEnergy+" smooth transitions, native fog, closed room, reload, restored clear and manual setting");
|
||||
}
|
||||
private static void profile(ClientGameTestContext c,TestServerContext server,DailyWeather.Kind kind){
|
||||
server.runOnServer(s->command(s,"sanctuary weather "+kind.key()));
|
||||
c.waitFor(m->WeatherClient.state().active()&&WeatherClient.state().kind()==kind,200);
|
||||
c.waitFor(m->(kind.raining()?m.level.getRainLevel(1)>.999:m.level.getRainLevel(1)<.001)&&(kind.thundering()?m.level.getThunderLevel(1)>.999:m.level.getThunderLevel(1)<.001),600);
|
||||
c.waitTicks(240);
|
||||
}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import static fr.koka.sanctuary.client.shader.PixelShadows122ClientChecks.*;
|
||||
import java.nio.file.Files;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.TestServerContext;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.util.Mth;
|
||||
|
||||
/** Native solar volume in a sealed room, a slotted roof and a leaf canopy. */
|
||||
public final class Sunshafts137ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c){new PixelShadows122ClientChecks(false,false,false,false,true).runTest(c);}
|
||||
static void run(ClientGameTestContext c,TestServerContext server){
|
||||
c.runOnClient(m->{preferences();VanillaLight.setEnabled(true);VanillaLight.setSaturation(100);VanillaLight.setPbr(false);VanillaLight.setSsgi(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);VanillaLight.setLensFlare(false);VanillaLight.setEdgeHighlights(false);VanillaLight.setPixelShadows(false);VanillaLight.setSunshafts(false);VanillaLight.setShadowDistance(64);});
|
||||
server.runOnServer(s->{command(s,"time add 1");command(s,"time set 3000");
|
||||
for(int x=76;x<=124;x++)for(int z=76;z<=124;z++){
|
||||
s.overworld().setBlockAndUpdate(new BlockPos(x,96,z),Blocks.STONE_BRICKS.defaultBlockState());
|
||||
if(x==76||x==124||z==76||z==124)for(int y=97;y<=113;y++)s.overworld().setBlockAndUpdate(new BlockPos(x,y,z),Blocks.STONE_BRICKS.defaultBlockState());
|
||||
}
|
||||
});
|
||||
roof(server,0);camera(c,server,100.5,101,110.5,70);aim(c,server);c.waitTicks(50);
|
||||
var closedOff=capture(c,"shafts-closed-off");enable(c,true);var closed=capture(c,"shafts-closed-on");
|
||||
check(gain(closedOff,closed)<300,"Sealed room receives no sunlight through opaque blocks: "+gain(closedOff,closed));
|
||||
c.runOnClient(m->check(!PixelShadows.applied()&&PixelShadows.depthReady()&&!Bloom.allocated(),"Volume alone shares depth without surface shadows or bloom"));
|
||||
roof(server,1);settle(c);enable(c,false);var off=capture(c,"shafts-slots-off");enable(c,true);var on=capture(c,"shafts-slots-35");long low=gain(off,on);
|
||||
check(low>1000,"Roof openings form visible shafts: "+low);unchanged(on,capture(c,null),"Static shafts have no animated sampling noise");
|
||||
c.runOnClient(m->VanillaLight.setSunshaftIntensity(100));settle(c);var strong=capture(c,"shafts-slots-100");check(gain(off,strong)>low*1.5,"Intensity increases volume energy");
|
||||
camera(c,server,104.5,101,110.5,70);aim(c,server);settle(c);capture(c,"shafts-moved-four-blocks");
|
||||
camera(c,server,100.5,101,110.5,70);aim(c,server);settle(c);unchanged(strong,capture(c,"shafts-returned"),"Moving and returning leaves no history or trailing light");
|
||||
var reload=c.computeOnClient(net.minecraft.client.Minecraft::reloadResourcePacks);c.waitFor(m->reload.isDone()&&m.gui.overlay()==null,1200);reload.join();settle(c);unchanged(strong,capture(c,"shafts-reloaded"),"Volume recovers after resource reload");
|
||||
int w=c.computeOnClient(m->m.getWindow().getScreenWidth()),h=c.computeOnClient(m->m.getWindow().getScreenHeight());
|
||||
c.runOnClient(m->m.getWindow().setWindowed(961,541));c.waitFor(m->m.gameRenderer.mainRenderTarget().width!=off.width(),200);settle(c);
|
||||
enable(c,false);var small=capture(c,null);enable(c,true);check(gain(small,capture(c,"shafts-resized"))>1000,"Bilateral volume survives odd dimensions");
|
||||
c.runOnClient(m->m.getWindow().setWindowed(w,h));c.waitFor(m->m.gameRenderer.mainRenderTarget().width==off.width(),200);settle(c);
|
||||
roof(server,2);settle(c);enable(c,false);var leafOff=capture(c,null);enable(c,true);var leafOn=capture(c,"shafts-leaves");long leaf=gain(leafOff,leafOn);double leafEnergy=energy(leafOff,leafOn);check(leaf>1000,"Leaf canopy transmits light: "+leaf);
|
||||
roof(server,3);settle(c);enable(c,false);var openOff=capture(c,null);enable(c,true);var openOn=capture(c,"shafts-open");long open=gain(openOff,openOn);double openEnergy=energy(openOff,openOn);check(openEnergy>leafEnergy,"Leaf canopy attenuates normalized scattering on identical walls: "+leafEnergy+" / "+openEnergy);
|
||||
look(c,server,0,0);settle(c);enable(c,false);var sideOff=capture(c,null);enable(c,true);check(gain(sideOff,capture(c,"shafts-sun-offscreen"))>1000,"Volume persists with sun outside the viewport");
|
||||
long[] close=new long[2];int index=0;
|
||||
for(double z:new double[]{123.5,123.875}){camera(c,server,100.5,101,z,70);look(c,server,0,0);settle(c);enable(c,false);var nearOff=capture(c,null);enable(c,true);close[index++]=gain(nearOff,capture(c,"shafts-wall-"+z));}
|
||||
check(close[1]<=close[0]+100,"Approaching wall attenuates scattering instead of increasing it: "+close[0]+" / "+close[1]);
|
||||
c.runOnClient(m->VanillaLight.setSunshaftIntensity(0));c.waitTicks(10);c.runOnClient(m->check(!Sunshafts.allocated()&&PixelShadows.shadowMap()==null,"Zero frees volume and unneeded shared map"));
|
||||
c.runOnClient(m->VanillaLight.setSunshaftIntensity(35));enable(c,true);
|
||||
server.runOnServer(s->command(s,"time set 18000"));c.waitTicks(30);c.runOnClient(m->check(!Sunshafts.allocated()&&!Sunshafts.applied(),"Solar volume stops at night"));
|
||||
server.runOnServer(s->command(s,"time set 3000"));camera(c,server,100.5,101,110.5,70);aim(c,server);settle(c);
|
||||
c.runOnClient(m->{VanillaLight.setPixelShadows(true);VanillaLight.setPbr(true);VanillaLight.setSsgi(true);VanillaLight.setBloom(true);VanillaLight.setLensFlare(true);VanillaLight.setEdgeHighlights(true);});activeFrames(c);capture(c,"shafts-combined");
|
||||
c.runOnClient(m->{VanillaLight.setSunshafts(false);});c.waitTicks(10);c.runOnClient(m->check(!Sunshafts.allocated()&&PixelShadows.applied(),"OFF preserves independently enabled surface shadows"));
|
||||
for(String locale:new String[]{"fr_fr","en_us"})c.runOnClient(m->{m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());for(String suffix:new String[]{"sunshafts","sunshafts_help","sunshaft_intensity","sunshaft_intensity_help"}){String key="sanctuary.shaders."+suffix;check(!Component.translatable(key).getString().equals(key),"Localized volume controls: "+locale);}});
|
||||
System.out.println("SUNSHAFT137_PASS slotGain="+low+" leafGain="+leaf+" openGain="+open+" closed room, offscreen sun, camera return, proximity, reload/resize, shared map, zero/night, preferences, FR/EN");
|
||||
SunshaftWeather138ClientChecks.run(c,server);
|
||||
}
|
||||
static void roof(TestServerContext server,int mode){server.runOnServer(s->{for(int x=76;x<=124;x++)for(int z=76;z<=124;z++)s.overworld().setBlockAndUpdate(new BlockPos(x,113,z),(mode==3||mode==1&&Math.floorMod(z,8)<3?Blocks.AIR:mode==2?Blocks.OAK_LEAVES:Blocks.OAK_PLANKS).defaultBlockState().trySetValue(net.minecraft.world.level.block.state.properties.BlockStateProperties.PERSISTENT,true));});}
|
||||
static void aim(ClientGameTestContext c,TestServerContext server){var d=c.computeOnClient(m->PixelShadows.sunDirection(m.gameRenderer.gameRenderState().levelRenderState.skyRenderState.sunAngle));look(c,server,Mth.wrapDegrees((float)Math.toDegrees(Math.atan2(-d.x,d.z))+25),-12);}
|
||||
static void settle(ClientGameTestContext c){int before=c.computeOnClient(m->Sunshafts.renderedFrames());c.waitFor(m->Sunshafts.applied()&&PixelShadows.ready()&&Sunshafts.renderedFrames()>before+10,1200);c.waitTicks(30);}
|
||||
static void enable(ClientGameTestContext c,boolean enabled){c.runOnClient(m->VanillaLight.setSunshafts(enabled));if(enabled)settle(c);else c.waitTicks(10);}
|
||||
private static long gain(Frame off,Frame on){long sum=0;for(int i=0;i<off.pixels().length;i++)for(int shift:new int[]{0,8,16})sum+=Math.max(0,(on.pixels()[i]>>shift&255)-(off.pixels()[i]>>shift&255));return sum;}
|
||||
// Screen blending has less headroom on a brighter vanilla wall. Remove that factor
|
||||
// before comparing roofs, and exclude the roof itself where geometry changes.
|
||||
static double energy(Frame off,Frame on){double sum=0;int n=0;for(int y=off.height()/3;y<off.height();y++)for(int x=0;x<off.width();x++){int i=y*off.width()+x;for(int shift:new int[]{0,8,16}){int a=off.pixels()[i]>>shift&255,b=on.pixels()[i]>>shift&255;if(a<240){sum+=Math.max(0,b-a)/(255.0-a);n++;}}}return sum/Math.max(1,n);}
|
||||
private static void preferences(){try{var p=FabricLoader.getInstance().getConfigDir().resolve("sanctuary-shaders.json");Files.writeString(p,"{\"bloom\":false,\"pixelShadows\":false}");forgetPreferences();check(VanillaLight.sunshafts()&&VanillaLight.sunshaftIntensity()==35&&!VanillaLight.bloom()&&!VanillaLight.pixelShadows(),"Missing shaft settings preserve existing choices");VanillaLight.setSunshafts(false);VanillaLight.setSunshaftIntensity(73);forgetPreferences();check(!VanillaLight.sunshafts()&&VanillaLight.sunshaftIntensity()==73,"Custom shafts persist");for(int n:new int[]{-10,0,35,100,150}){VanillaLight.setSunshaftIntensity(n);forgetPreferences();check(VanillaLight.sunshaftIntensity()==Math.clamp(n,0,100),"Intensity clamps and persists");}VanillaLight.setSunshaftIntensity(35);}catch(java.io.IOException e){throw new AssertionError(e);}}
|
||||
}
|
||||
+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;}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package fr.koka.sanctuary.gametest.mixin;
|
||||
|
||||
import net.minecraft.client.renderer.Lightmap;
|
||||
import net.minecraft.client.renderer.state.LightmapRenderState;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
/** Freeze only vanilla torch flicker for image comparisons; never packaged in the mod. */
|
||||
@Mixin(Lightmap.class)
|
||||
public abstract class Colored139LightmapProbeMixin {
|
||||
@Inject(method="render",at=@At("HEAD"))
|
||||
private void stableBlockLight(LightmapRenderState state,CallbackInfo ci){
|
||||
if(Boolean.getBoolean("sanctuary.test.freezeBlockFlicker")){state.blockFactor=1;state.needsUpdate=true;}
|
||||
}
|
||||
}
|
||||
+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,9 @@
|
||||
"defaultRequire": 1
|
||||
},
|
||||
"client": [
|
||||
"ReflectionFrameProbeMixin",
|
||||
"ReflectionGpuProbeMixin",
|
||||
"Colored139LightmapProbeMixin",
|
||||
"Fourneau094ParticleProbeMixin",
|
||||
"Tooltip093FontProbeMixin",
|
||||
"ClientVsync046Mixin",
|
||||
@@ -18,6 +21,8 @@
|
||||
"Atlas026TextureProbeMixin",
|
||||
"Inventory013MerchantAccess",
|
||||
"Dynamic030TerrainProbeMixin",
|
||||
"Star040RenderProbeMixin"
|
||||
"Star040RenderProbeMixin",
|
||||
"Water144FrameProbeMixin",
|
||||
"Water144SectionProbeMixin"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ public final class SanctuaryMod implements ModInitializer {
|
||||
|
||||
@Override
|
||||
public void onInitialize() {
|
||||
fr.koka.sanctuary.community.CommunityNetwork.register();
|
||||
fr.koka.sanctuary.building.ColoredBricks.register();
|
||||
fr.koka.sanctuary.gems.Gems.register();
|
||||
fr.koka.sanctuary.sky.VortexClouds.register();
|
||||
@@ -71,6 +72,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();
|
||||
|
||||
@@ -19,7 +19,7 @@ public final class AtlasService {
|
||||
private static final Map<MinecraftServer,Map<UUID,State>> STATES=new WeakHashMap<>();
|
||||
private static final int[][] OFFSETS=offsets();
|
||||
private static final class State {
|
||||
AtlasStore store;boolean failed;int cursor,lastRequest=-100,lastSave,saveLayer;Job job;
|
||||
AtlasStore store;boolean failed;int cursor,lastRequest=-100,lastSave,saveLayer;long lastRequestNanos;Job job;
|
||||
final Map<Integer,AtlasStore> layers=new LinkedHashMap<>();
|
||||
}
|
||||
private record Job(AtlasPayloads.Request request,ArrayDeque<AtlasStore.Key> remaining) { }
|
||||
@@ -75,8 +75,9 @@ public final class AtlasService {
|
||||
if (request.token()==0) {state.job=null;return;}
|
||||
int tick=player.level().getServer().getTickCount();
|
||||
state.job=null;
|
||||
if (tick-state.lastRequest<10) {reply(player,request,true,true,"retry",List.of());return;}
|
||||
state.lastRequest=tick;
|
||||
long now=System.nanoTime();
|
||||
if (tick-state.lastRequest<10 && now-state.lastRequestNanos<500_000_000L) {reply(player,request,true,true,"retry",List.of());return;}
|
||||
state.lastRequest=tick;state.lastRequestNanos=now;
|
||||
if (state.failed) {reply(player,request,true,true,"storage_invalid",List.of());return;}
|
||||
if (!canRead(player,request.operator())) {
|
||||
reply(player,request,true,true,player.level().dimension()!=Level.OVERWORLD ? "dimension" : request.operator() ? "denied" : "locked",List.of());return;
|
||||
@@ -123,24 +124,35 @@ public final class AtlasService {
|
||||
catch (Exception error) {state.failed=true;state.job=null;SanctuaryMod.LOGGER.error("Could not save atlas layer (file preserved)",error);}
|
||||
state.lastSave=tick;saved=true;
|
||||
}
|
||||
var job=state.job;
|
||||
if (job==null) continue;
|
||||
if (!canRead(player,job.request.operator())) {
|
||||
reply(player,job.request,true,true,"denied",List.of());state.job=null;continue;
|
||||
}
|
||||
var tiles=new ArrayList<AtlasStore.Tile>();
|
||||
int budget=job.request.operator() ? 4 : AtlasPayloads.BATCH;
|
||||
while (budget-->0 && !job.remaining.isEmpty()) {
|
||||
var key=job.remaining.removeFirst();
|
||||
var tile=job.request.operator() ? AtlasSampler.sample(player.level(),key.x(),key.z(),job.request.altitude())
|
||||
: state.layers.get(job.request.altitude()).get(key.x(),key.z());
|
||||
if (tile!=null) tiles.add(tile);
|
||||
}
|
||||
boolean done=job.remaining.isEmpty();
|
||||
reply(player,job.request,false,done,state.layers.get(job.request.altitude()).full() ? "full" : "ready",tiles);
|
||||
if (done) state.job=null;
|
||||
stream(player,state);
|
||||
}
|
||||
}
|
||||
/** The paused integrated server may send stored personal tiles, never sample or advance the world. */
|
||||
public static void streamPaused(MinecraftServer server) {
|
||||
var states=STATES.get(server);if(states==null)return;
|
||||
for(var player:server.getPlayerList().getPlayers()) {
|
||||
var state=states.get(player.getUUID());
|
||||
if(state!=null && !state.failed && state.job!=null && !state.job.request.operator())stream(player,state);
|
||||
}
|
||||
}
|
||||
private static void stream(ServerPlayer player,State state) {
|
||||
var job=state.job;
|
||||
if (job==null) return;
|
||||
if (!canRead(player,job.request.operator())) {
|
||||
reply(player,job.request,true,true,"denied",List.of());state.job=null;return;
|
||||
}
|
||||
var tiles=new ArrayList<AtlasStore.Tile>();
|
||||
int budget=job.request.operator() ? 4 : AtlasPayloads.BATCH;
|
||||
while (budget-->0 && !job.remaining.isEmpty()) {
|
||||
var key=job.remaining.removeFirst();
|
||||
var tile=job.request.operator() ? AtlasSampler.sample(player.level(),key.x(),key.z(),job.request.altitude())
|
||||
: state.layers.get(job.request.altitude()).get(key.x(),key.z());
|
||||
if (tile!=null) tiles.add(tile);
|
||||
}
|
||||
boolean done=job.remaining.isEmpty();
|
||||
reply(player,job.request,false,done,state.layers.get(job.request.altitude()).full() ? "full" : "ready",tiles);
|
||||
if (done) state.job=null;
|
||||
}
|
||||
private static void reply(ServerPlayer player,AtlasPayloads.Request request,boolean reset,boolean done,String status,List<AtlasStore.Tile> tiles) {
|
||||
if (ServerPlayNetworking.canSend(player,AtlasPayloads.Response.TYPE)) ServerPlayNetworking.send(player,
|
||||
new AtlasPayloads.Response(request.token(),reset,done,canOperate(player),request.operator(),status,tiles));
|
||||
|
||||
@@ -16,6 +16,7 @@ public final class AtlasClient {
|
||||
}));
|
||||
ClientPlayNetworking.registerGlobalReceiver(AtlasPayloads.Response.TYPE,(p,c) -> c.client().execute(() -> {
|
||||
if (c.client().gui.screen() instanceof SanctuaryAtlasScreen screen) screen.accept(p);
|
||||
else if (c.client().gui.screen() instanceof PauseAtlasAccess pause && pause.sanctuary$pauseMap()!=null) pause.sanctuary$pauseMap().accept(p);
|
||||
}));
|
||||
}
|
||||
public static int nextToken() {sequence=sequence==Integer.MAX_VALUE ? 1 : sequence+1;return sequence;}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import java.util.List;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
import net.minecraft.client.gui.components.AbstractWidget;
|
||||
import net.minecraft.client.gui.narration.NarrationElementOutput;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.util.FormattedCharSequence;
|
||||
|
||||
/** One compact, wrapped message; ordinary text never becomes a clickable command. */
|
||||
final class CommunityBubble extends AbstractWidget {
|
||||
private final List<FormattedCharSequence> lines;
|
||||
private final boolean own;
|
||||
CommunityBubble(int width, String body, boolean own) {
|
||||
super(0,0,width,20,Component.literal(body));
|
||||
this.own=own;
|
||||
lines=Minecraft.getInstance().font.split(getMessage(),Math.max(20,width-16));
|
||||
height=12+lines.size()*11;
|
||||
active=false;
|
||||
}
|
||||
@Override protected void extractWidgetRenderState(GuiGraphicsExtractor g,int mx,int my,float delta) {
|
||||
int x=getX(),y=getY();
|
||||
g.fill(x,y,x+width,y+height,own?0xC42B3732:0xB51B2028);
|
||||
g.fill(x,y,x+2,y+height,own?0xFF819D87:0xFF5E6879);
|
||||
for(int i=0;i<lines.size();i++)g.text(Minecraft.getInstance().font,lines.get(i),x+8,y+6+i*11,0xFFF0F0F0,false);
|
||||
}
|
||||
@Override protected void updateWidgetNarration(NarrationElementOutput output) { defaultButtonNarrationText(output); }
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.google.gson.*;
|
||||
import fr.koka.sanctuary.community.CommunityNetwork;
|
||||
import java.util.*;
|
||||
import java.util.function.Consumer;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.networking.v1.*;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.network.chat.Component;
|
||||
|
||||
public final class CommunityClient {
|
||||
public static JsonObject overview = new JsonObject();
|
||||
public static boolean stale = true;
|
||||
public static String overviewError = "";
|
||||
private static int sequence, pending, ticks;
|
||||
private static Screen owner;
|
||||
private static Consumer<JsonObject> callback;
|
||||
private static boolean overviewRequest;
|
||||
static final Map<String,Draft> DRAFTS = new HashMap<>();
|
||||
static final class Draft { String operation = UUID.randomUUID().toString(), title = "", body = "", category = "info", photo = ""; }
|
||||
private CommunityClient() { }
|
||||
public static net.minecraft.network.chat.MutableComponent text(String key, Object... args) { return Component.translatable("sanctuary.community." + key,args); }
|
||||
public static boolean available() { return ClientPlayNetworking.canSend(CommunityNetwork.Request.TYPE); }
|
||||
public static boolean pending() { return pending != 0; }
|
||||
public static void register() {
|
||||
ClientPlayNetworking.registerGlobalReceiver(CommunityNetwork.Response.TYPE,(r,c) -> c.client().execute(() -> {
|
||||
if (r.sequence() != pending) return;
|
||||
var result = JsonParser.parseString(r.json()).getAsJsonObject(); finish(result);
|
||||
}));
|
||||
ClientPlayConnectionEvents.DISCONNECT.register((h,c) -> { pending = 0; owner = null; callback = null; overview = new JsonObject(); stale = true; overviewError = ""; DRAFTS.clear(); });
|
||||
ClientTickEvents.END_CLIENT_TICK.register(client -> {
|
||||
if (pending != 0 && ++ticks > 200) { var result = new JsonObject(); result.addProperty("error","unavailable"); finish(result); }
|
||||
});
|
||||
}
|
||||
private static void finish(JsonObject result) {
|
||||
pending = 0;
|
||||
if (overviewRequest) {
|
||||
if (!result.has("error")) { overview = result; stale = false; overviewError = ""; }
|
||||
else { stale = true; overviewError = str(result,"error"); }
|
||||
}
|
||||
var receiver = callback; var from = owner; callback = null; owner = null;
|
||||
if (receiver != null && Minecraft.getInstance().gui.screen() == from) receiver.accept(result);
|
||||
}
|
||||
public static boolean request(Screen screen,String action,String id,String parent,String kind,String category,
|
||||
String title,String body,int revision,int page,Consumer<JsonObject> receiver) {
|
||||
if (!available() || pending != 0) return false;
|
||||
owner = screen; callback = receiver; overviewRequest = action.equals("overview"); ticks = 0;
|
||||
pending = ++sequence;
|
||||
ClientPlayNetworking.send(new CommunityNetwork.Request(pending,action,id,parent,kind,category,title,body,revision,page));
|
||||
return true;
|
||||
}
|
||||
public static void overview(Screen screen) { request(screen,"overview","","","article","info","","",0,0,r -> { }); }
|
||||
public static void open(Screen parent,String kind,String id,boolean compose) {
|
||||
Minecraft.getInstance().gui.setScreen(new CommunityScreen(parent,kind,id,compose));
|
||||
}
|
||||
static String str(JsonObject value,String key) { return value.has(key) ? value.get(key).getAsString() : ""; }
|
||||
static int number(JsonObject value,String key) { return value.has(key) ? value.get(key).getAsInt() : 0; }
|
||||
static boolean flag(JsonObject value,String key) { return value.has(key) && value.get(key).getAsBoolean(); }
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.google.gson.*;
|
||||
import java.time.*;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
import java.util.function.Consumer;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import static fr.koka.sanctuary.client.CommunityClient.*;
|
||||
|
||||
/** Three spatial regions, with independent community feeds and native exit/options actions. */
|
||||
public final class CommunityPausePanels implements AutoCloseable {
|
||||
private final Screen screen;
|
||||
private final PauseCommunityFeed articles, notices;
|
||||
private final StringWidget date, clock;
|
||||
private final PauseTextButton bulletin;
|
||||
private final PauseAtlasWidget map;
|
||||
private int ticks;
|
||||
private boolean closed;
|
||||
public CommunityPausePanels(Screen screen,Consumer<AbstractWidget> add,List<Button> nativeButtons) {
|
||||
this.screen=screen;
|
||||
var font=Minecraft.getInstance().font;
|
||||
int margin=8,gap=10,top=40,bottom=screen.height-38;
|
||||
int left=Math.min(132,Math.max(76,screen.width/5)),right=Math.min(244,Math.max(104,screen.width*31/100));
|
||||
int centerX=margin+left+gap,rightX=screen.width-margin-right,centerWidth=rightX-gap-centerX;
|
||||
date=new StringWidget(margin,8,left,20,Component.empty(),font);
|
||||
clock=new StringWidget(rightX,8,right,20,Component.empty(),font);
|
||||
add.accept(date);add.accept(clock);
|
||||
int headerSide=Math.max(84,screen.width/5);
|
||||
bulletin=new PauseTextButton(headerSide,3,screen.width-2*headerSide,32,text("bulletin"),()->{
|
||||
var list=items("bulletins");open(screen,"bulletin",list.isEmpty()?"":str(list.get(0).getAsJsonObject(),"id"),false);
|
||||
});
|
||||
add.accept(bulletin);
|
||||
String[] sections={"blocks","history","progress","identity"};
|
||||
int optionsTop=screen.height-76;
|
||||
int navHeight=Math.min(20,Math.max(16,(optionsTop-top-28)/6));
|
||||
int step=navHeight+4;
|
||||
for(int i=0;i<sections.length;i++) {
|
||||
String target=sections[i];
|
||||
add.accept(Button.builder(fr.koka.sanctuary.progression.ProgressionService.label(target),b->ProgressionClient.open(screen,target))
|
||||
.bounds(margin,top+i*step,left,navHeight).build());
|
||||
}
|
||||
add.accept(Button.builder(fr.koka.sanctuary.cycle.CycleService.label("faction"),b->CycleClient.open(screen,"faction")).bounds(margin,top+4*step,left,navHeight).build());
|
||||
add.accept(Button.builder(fr.koka.sanctuary.arena.Arenas.text("title"),b->ArenaClient.open(screen)).bounds(margin,top+5*step,left,navHeight).build());
|
||||
map=new PauseAtlasWidget(screen,centerX,top,centerWidth,Math.max(30,bottom-top));add.accept(map);
|
||||
place(nativeButtons,add,"menu.returnToGame",rightX,screen.height-28,right);
|
||||
int feedHeight=(bottom-top-gap)/2;
|
||||
articles=new PauseCommunityFeed(screen,add,rightX,top,right,feedHeight,"article");
|
||||
notices=new PauseCommunityFeed(screen,add,rightX,top+feedHeight+gap,right,bottom-top-feedHeight-gap,"notice");
|
||||
place(nativeButtons,add,"menu.options",margin,optionsTop,left);
|
||||
place(nativeButtons,add,"options.worldOptions.button",margin,optionsTop+24,left);
|
||||
for(var button:nativeButtons) if(key(button).equals("menu.returnToMenu") || key(button).equals("menu.disconnect")) {
|
||||
button.setRectangle(left,20,margin,optionsTop+48);button.setTooltip(Tooltip.create(button.getMessage()));add.accept(button);
|
||||
}
|
||||
updateHeader();
|
||||
}
|
||||
public static String key(Button button) { return button.getMessage().getContents() instanceof TranslatableContents t?t.getKey():""; }
|
||||
private static void place(List<Button> buttons,Consumer<AbstractWidget> add,String key,int x,int y,int width) {
|
||||
buttons.stream().filter(b->key(b).equals(key)).findFirst().ifPresent(b->{b.setRectangle(width,20,x,y);b.setTooltip(Tooltip.create(b.getMessage()));add.accept(b);});
|
||||
}
|
||||
private static JsonArray items(String key) { return overview.has(key)?overview.getAsJsonArray(key):new JsonArray(); }
|
||||
private void updateHeader() {
|
||||
var locale=Locale.forLanguageTag(Minecraft.getInstance().getLanguageManager().getSelected().replace('_','-'));
|
||||
var now=ZonedDateTime.now();
|
||||
date.setMessage(Component.literal(DateTimeFormatter.ofPattern("d MMMM",locale).format(now)));
|
||||
clock.setMessage(Component.literal(DateTimeFormatter.ofPattern("HH:mm",locale).format(now)));
|
||||
clock.setX(screen.width-8-Minecraft.getInstance().font.width(clock.getMessage()));clock.setWidth(Minecraft.getInstance().font.width(clock.getMessage()));
|
||||
var list=items("bulletins");
|
||||
Component message=list.isEmpty()?text("bulletin_empty"):Component.literal(str(list.get(0).getAsJsonObject(),"title")+" · "+str(list.get(0).getAsJsonObject(),"body"));
|
||||
if(!overviewError.isEmpty())message=text(overviewError);
|
||||
bulletin.setMessage(message);
|
||||
bulletin.setTooltip(Tooltip.create(text("bulletin").append("\n").append(list.isEmpty()?message:Component.literal(str(list.get(0).getAsJsonObject(),"body")))));
|
||||
bulletin.active=available();
|
||||
}
|
||||
public void tick() {
|
||||
if(closed)return;
|
||||
map.tick(); updateHeader();
|
||||
if(++ticks%6!=0 || pending() || !available())return;
|
||||
if(ticks==6 || ticks%240==0){overview(screen);if(ticks>6){articles.refresh();notices.refresh();}}
|
||||
else if(!articles.load())notices.load();
|
||||
}
|
||||
public PauseAtlasWidget map() { return map; }
|
||||
@Override public void close() {closed=true;articles.close();notices.close();map.close();}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.NativeImage;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
import net.minecraft.client.gui.components.AbstractButton;
|
||||
import net.minecraft.client.gui.narration.NarrationElementOutput;
|
||||
import net.minecraft.client.input.InputWithModifiers;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.client.renderer.texture.DynamicTexture;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
/** A locally owned texture; never installed under a user-controlled resource identifier. */
|
||||
final class CommunityPhotoWidget extends AbstractButton implements AutoCloseable {
|
||||
private final Identifier texture=Identifier.fromNamespaceAndPath("sanctuary","community/"+UUID.randomUUID());
|
||||
private final int imageWidth,imageHeight;
|
||||
private final Runnable action;
|
||||
private final boolean caption;
|
||||
private boolean closed;
|
||||
CommunityPhotoWidget(int width,int height,BufferedImage image,Component label,boolean caption,Runnable action) {
|
||||
super(0,0,width,height,label);this.action=action;this.caption=caption;
|
||||
imageWidth=image.getWidth();imageHeight=image.getHeight();
|
||||
var nativeImage=new NativeImage(imageWidth,imageHeight,false);
|
||||
for(int y=0;y<imageHeight;y++)for(int x=0;x<imageWidth;x++)nativeImage.setPixel(x,y,0xff000000|image.getRGB(x,y));
|
||||
Minecraft.getInstance().getTextureManager().register(texture,new DynamicTexture(()->"Gazette photo",nativeImage));
|
||||
active=action!=null;
|
||||
}
|
||||
@Override public void onPress(InputWithModifiers input) {if(action!=null)action.run();}
|
||||
@Override protected void extractContents(GuiGraphicsExtractor g,int mx,int my,float delta) {
|
||||
if(closed)return;
|
||||
int x=getX(),y=getY(),available=height-(caption?18:0);
|
||||
g.fill(x,y,x+width,y+height,isHoveredOrFocused()?0xff536957:0xff1b2028);
|
||||
double scale=Math.min((double)(width-4)/imageWidth,(double)(available-4)/imageHeight);
|
||||
int w=(int)(imageWidth*scale),h=(int)(imageHeight*scale);
|
||||
g.blit(RenderPipelines.GUI_TEXTURED,texture,x+(width-w)/2,y+(available-h)/2,0,0,w,h,imageWidth,imageHeight,imageWidth,imageHeight);
|
||||
if(caption)g.text(Minecraft.getInstance().font,Minecraft.getInstance().font.plainSubstrByWidth(getMessage().getString(),width-10),x+5,y+height-13,0xffeeeeee,false);
|
||||
}
|
||||
@Override protected void updateWidgetNarration(NarrationElementOutput output) {defaultButtonNarrationText(output);}
|
||||
@Override public void close() {if(!closed){closed=true;Minecraft.getInstance().getTextureManager().release(texture);}}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.google.gson.*;
|
||||
import java.time.*;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.layouts.LinearLayout;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import static fr.koka.sanctuary.client.CommunityClient.*;
|
||||
|
||||
/** Native community pages: user content is always a literal Minecraft component. */
|
||||
public final class CommunityScreen extends BlocodexFrame {
|
||||
private final String kind;
|
||||
private String id, editingId = "", message = "", expandedId = "";
|
||||
private int page, revision;
|
||||
private boolean composing, needsLoad = true, moderator;
|
||||
private JsonObject data = new JsonObject();
|
||||
private Draft draft;
|
||||
private int ticks;
|
||||
private final java.util.List<CommunityPhotoWidget> photos=new java.util.ArrayList<>();
|
||||
private boolean publishing;
|
||||
private int photoPart, sendAfter;
|
||||
private String outgoingPhoto="";
|
||||
private Runnable commit;
|
||||
private void closePhotos() {photos.forEach(CommunityPhotoWidget::close);photos.clear();}
|
||||
@Override public void removed() {closePhotos();publishing=false;commit=null;}
|
||||
|
||||
public CommunityScreen(Screen parent,String kind,String id,boolean compose) {
|
||||
super(parent); this.kind = kind; this.id = id; this.composing = compose;
|
||||
moderator = flag(overview,"moderator");
|
||||
if (compose) useDraft("new:" + kind);
|
||||
}
|
||||
private void useDraft(String key) { draft = DRAFTS.computeIfAbsent(key, unused -> new Draft()); }
|
||||
@Override protected void init() {
|
||||
closePhotos();initFrame("history",text(kind)); contentWidth=Math.min(520,contentWidth); contentX=(width-contentWidth)/2; int w = contentWidth - 28;
|
||||
var layout = LinearLayout.vertical().spacing(8);
|
||||
if (!message.isEmpty()) line(layout,w,text(message));
|
||||
if (!available()) line(layout,w,text("unavailable"));
|
||||
if (composing) compose(layout,w);
|
||||
else if (data.has("thread")) detail(layout,w,data.getAsJsonObject("thread"));
|
||||
else listing(layout,w);
|
||||
var scroll = new ScrollableLayout(minecraft,layout,Math.max(24,contentBottom-contentTop),ScrollableLayout.ReserveStrategy.RIGHT);
|
||||
scroll.setPosition(contentX+8,contentTop); scroll.arrangeElements(); scroll.visitWidgets(this::addRenderableWidget);
|
||||
}
|
||||
private void listing(LinearLayout layout,int w) {
|
||||
var tabs = layout.addChild(LinearLayout.horizontal().spacing(4));
|
||||
for (String target : new String[]{"article","notice","bulletin"}) tabs.addChild(Button.builder(text(target), b -> open(parent,target,"",false)).width((w-8)/3).build());
|
||||
var toolbar=layout.addChild(LinearLayout.horizontal().spacing(4));
|
||||
if (!kind.equals("bulletin") || moderator) button(toolbar,(w-8)/3,"publish",() -> { composing = true; useDraft("new:"+kind); rebuildWidgets(); });
|
||||
button(toolbar,(w-8)/3,"refresh",() -> needsLoad = true);
|
||||
button(toolbar,(w-8)/3,"link",() -> minecraft.gui.setScreen(new LinkScreen(this)));
|
||||
if (!data.has("page")) { line(layout,w,text("loading")); return; }
|
||||
var listing = data.getAsJsonObject("page");
|
||||
if (listing.getAsJsonArray("entries").isEmpty()) line(layout,w,text("empty"));
|
||||
for (var item : listing.getAsJsonArray("entries")) {
|
||||
var entry = item.getAsJsonObject();
|
||||
layout.addChild(Button.builder(Component.literal((flag(entry,"pinned") ? "★ " : "") + str(entry,"title")), b -> { id=str(entry,"id"); page=0; needsLoad=true; }).width(w).build());
|
||||
author(layout,w,entry,false);
|
||||
if (kind.equals("notice")) line(layout,w,text("category."+str(entry,"category")).append(" · ").append(text(str(entry,"status"))));
|
||||
}
|
||||
pages(layout,w,listing);
|
||||
}
|
||||
private void detail(LinearLayout layout,int w,JsonObject thread) {
|
||||
var entry = thread.getAsJsonObject("entry");
|
||||
var toolbar=layout.addChild(LinearLayout.horizontal().spacing(5));
|
||||
button(toolbar,90,"back",() -> { id=""; page=0; data=new JsonObject(); needsLoad=true; rebuildWidgets(); });
|
||||
button(toolbar,90,"refresh",() -> needsLoad=true);
|
||||
line(layout,w,Component.literal(str(entry,"title")).withStyle(net.minecraft.ChatFormatting.BOLD));
|
||||
photo(layout,w,str(entry,"photo"));
|
||||
message(layout,w,entry);
|
||||
if (kind.equals("notice")) line(layout,w,text("category."+str(entry,"category")).append(" · ").append(text(str(entry,"status"))));
|
||||
if (!kind.equals("bulletin")) {
|
||||
line(layout,w,text("replies"));
|
||||
var replies = thread.getAsJsonObject("replies");
|
||||
for (var item : replies.getAsJsonArray("entries")) message(layout,w,item.getAsJsonObject());
|
||||
pages(layout,w,replies);
|
||||
if (str(entry,"status").equals("open")) {
|
||||
useDraft("reply:"+id);
|
||||
var replyDraft=draft;
|
||||
var box=layout.addChild(MultiLineEditBox.builder().setPlaceholder(text("reply_hint")).build(font,w,52,text("reply")));
|
||||
box.setCharacterLimit(1000); box.setValue(replyDraft.body); box.setValueListener(value -> replyDraft.body=value);
|
||||
button(layout,100,"reply",() -> request(this,"create",replyDraft.operation,id,"reply","info","",replyDraft.body,0,0,result -> {
|
||||
if (result.has("error")) { message=str(result,"error"); rebuildWidgets(); return; }
|
||||
DRAFTS.values().removeIf(value -> value==replyDraft); message="saved"; needsLoad=true; stale=true;
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
private boolean own(JsonObject entry) { return minecraft.player!=null && str(entry,"authorId").equals(minecraft.player.getStringUUID()); }
|
||||
private void author(LinearLayout layout,int w,JsonObject entry,boolean actions) {
|
||||
var row=layout.addChild(LinearLayout.horizontal().spacing(6));
|
||||
row.addChild(new PlayerFaceWidget(24,net.minecraft.world.item.component.ResolvableProfile.createUnresolved(java.util.UUID.fromString(str(entry,"authorId")))));
|
||||
var labels=row.addChild(LinearLayout.vertical().spacing(3));
|
||||
labels.addChild(new StringWidget(w-58,11,Component.literal(str(entry,"authorName")),font));
|
||||
labels.addChild(new StringWidget(w-58,10,Component.literal(date(entry)).withStyle(net.minecraft.ChatFormatting.GRAY),font));
|
||||
if(actions && (own(entry)||moderator)) row.addChild(Button.builder(text("actions"),b -> {
|
||||
expandedId=expandedId.equals(str(entry,"id"))?"":str(entry,"id"); rebuildWidgets();
|
||||
}).size(20,18).tooltip(Tooltip.create(text("actions_for",str(entry,"authorName")))).build());
|
||||
}
|
||||
private void message(LinearLayout layout,int w,JsonObject entry) {
|
||||
author(layout,w,entry,true);
|
||||
layout.addChild(new CommunityBubble(w,str(entry,"body"),own(entry)));
|
||||
if(expandedId.equals(str(entry,"id"))) controls(layout,w,entry);
|
||||
}
|
||||
private void controls(LinearLayout layout,int w,JsonObject entry) {
|
||||
boolean own = own(entry);
|
||||
var row=layout.addChild(LinearLayout.horizontal().spacing(4)); int size=Math.min(100,(w-12)/4);
|
||||
if (own) button(row,size,"edit",() -> {
|
||||
editingId=str(entry,"id"); revision=number(entry,"revision"); composing=true; useDraft("edit:"+editingId);
|
||||
if (draft.body.isEmpty()) { draft.title=str(entry,"title"); draft.body=str(entry,"body"); draft.category=str(entry,"category"); draft.photo=str(entry,"photo"); }
|
||||
rebuildWidgets();
|
||||
});
|
||||
if (own || moderator) button(row,size,"hide",() -> minecraft.gui.setScreen(new net.minecraft.client.gui.screens.ConfirmScreen(
|
||||
confirmed -> { minecraft.gui.setScreen(this); if (confirmed) mutate("hide",entry); },text("hide"),text("hide_confirm"))));
|
||||
if ((own || moderator) && str(entry,"kind").equals("notice")) {
|
||||
String action=str(entry,"status").equals("closed")?"reopen":"close"; button(row,size,action,() -> mutate(action,entry));
|
||||
}
|
||||
if (moderator && !str(entry,"kind").equals("reply")) button(row,size,flag(entry,"pinned")?"unpin":"pin",() -> mutate("pin",entry));
|
||||
}
|
||||
private void compose(LinearLayout layout,int w) {
|
||||
boolean reply = !id.isEmpty() && (editingId.isEmpty() || !editingId.equals(id));
|
||||
if (!reply) {
|
||||
line(layout,w,text("title_label")); var titleBox=layout.addChild(new EditBox(font,w,20,text("title_label")));
|
||||
titleBox.setMaxLength(120); titleBox.setValue(draft.title); titleBox.setResponder(value -> draft.title=value);
|
||||
if (kind.equals("notice") && editingId.isEmpty()) button(layout,w,"category."+draft.category,() -> {
|
||||
String[] categories={"info","work","need","event"}; int i=java.util.Arrays.asList(categories).indexOf(draft.category);
|
||||
draft.category=categories[(i+1)%categories.length]; rebuildWidgets();
|
||||
});
|
||||
}
|
||||
if(!reply && kind.equals("article")) {
|
||||
photo(layout,w,draft.photo);
|
||||
button(layout,w,draft.photo.isEmpty()?"photo_choose":"photo_change",()->minecraft.gui.setScreen(new CommunityScreenshotsScreen(this,value->draft.photo=value)));
|
||||
if(draft.photo.isEmpty())line(layout,w,text("photo_required"));
|
||||
}
|
||||
line(layout,w,text("body_label"));
|
||||
var bodyBox=layout.addChild(MultiLineEditBox.builder().setPlaceholder(text("body_hint")).build(font,w,Math.max(60,Math.min(150,height/3)),text("body_label")));
|
||||
bodyBox.setCharacterLimit(reply?1000:4000); bodyBox.setValue(draft.body); bodyBox.setValueListener(value -> draft.body=value);
|
||||
var publish=layout.addChild(Button.builder(text(publishing?"photo_uploading":editingId.isEmpty()?"publish":"save"),b -> {
|
||||
if(pending() || publishing)return;
|
||||
String action=editingId.isEmpty()?"create":"edit";
|
||||
commit=()->request(this,action,editingId.isEmpty()?draft.operation:editingId,reply?id:"",reply?"reply":kind,draft.category,
|
||||
reply?"":draft.title,draft.body,revision,0,result -> {
|
||||
publishing=false;commit=null;
|
||||
if (result.has("error")) { message=str(result,"error"); rebuildWidgets(); return; }
|
||||
String saved=str(result.getAsJsonObject("result"),"id");
|
||||
DRAFTS.values().removeIf(value -> value==draft); composing=false; editingId="";
|
||||
if (!reply) id=saved; page=0; needsLoad=true; message="saved"; stale=true; rebuildWidgets();
|
||||
});
|
||||
if(!reply && kind.equals("article")) {outgoingPhoto=draft.photo;photoPart=0;publishing=true;sendAfter=ticks+4;rebuildWidgets();}
|
||||
else {var actionToRun=commit;commit=null;actionToRun.run();}
|
||||
}).width(150).build());
|
||||
publish.active=!publishing && (reply || !kind.equals("article") || !draft.photo.isEmpty());
|
||||
button(layout,Math.min(w,220),"back_draft",() -> { composing=false; editingId=""; needsLoad=true; rebuildWidgets(); });
|
||||
}
|
||||
private void photo(LinearLayout layout,int w,String value) {
|
||||
if(value.isEmpty())return;
|
||||
try {
|
||||
var image=fr.koka.sanctuary.community.CommunityPhoto.decode(java.util.Base64.getDecoder().decode(value),960,540);
|
||||
var widget=new CommunityPhotoWidget(w,Math.max(24,Math.min(200,w*image.getHeight()/image.getWidth())),image,text("photo"),false,null);
|
||||
photos.add(widget);layout.addChild(widget);
|
||||
} catch(Exception e) {line(layout,w,text("photo_invalid"));}
|
||||
}
|
||||
private void mutate(String action,JsonObject entry) {
|
||||
request(this,action,str(entry,"id"),"",str(entry,"kind"),str(entry,"category"),"","",number(entry,"revision"),0,result -> {
|
||||
message=result.has("error")?str(result,"error"):"saved";
|
||||
if (!result.has("error") && action.equals("hide") && str(entry,"id").equals(id)) { id=""; page=0; }
|
||||
stale=true; needsLoad=true; rebuildWidgets();
|
||||
});
|
||||
}
|
||||
private void pages(LinearLayout layout,int w,JsonObject listing) {
|
||||
if(page==0 && !flag(listing,"more"))return;
|
||||
w=Math.min(w,280);
|
||||
var row=layout.addChild(LinearLayout.horizontal().spacing(4));
|
||||
row.addChild(Button.builder(text("previous"),b -> { page=Math.max(0,page-1); needsLoad=true; }).width((w-8)/3).build()).active=page>0;
|
||||
row.addChild(new StringWidget((w-8)/3,20,text("page",page+1),font));
|
||||
row.addChild(Button.builder(text("next"),b -> { page++; needsLoad=true; }).width((w-8)/3).build()).active=flag(listing,"more");
|
||||
}
|
||||
private void line(LinearLayout layout,int w,Component value) { layout.addChild(new MultiLineTextWidget(value,font).setMaxWidth(w)); }
|
||||
private void button(LinearLayout layout,int w,String key,Runnable action) {
|
||||
layout.addChild(Button.builder(text(key),b -> { if (!pending() && !publishing) action.run(); }).width(w).build());
|
||||
}
|
||||
private static String date(JsonObject e) { return DateTimeFormatter.ofPattern("dd/MM HH:mm 'UTC'").withZone(ZoneOffset.UTC).format(Instant.ofEpochMilli(e.get("createdAt").getAsLong())); }
|
||||
@Override public void tick() {
|
||||
super.tick(); ticks++;
|
||||
if(publishing && !pending() && ticks>=sendAfter) {
|
||||
int parts=(outgoingPhoto.length()+7999)/8000;
|
||||
if(photoPart<parts) {
|
||||
int part=photoPart;
|
||||
request(this,"photo",editingId.isEmpty()?draft.operation:editingId,"","article","info","",outgoingPhoto.substring(part*8000,Math.min(outgoingPhoto.length(),(part+1)*8000)),parts,part,result->{
|
||||
if(result.has("error")){publishing=false;commit=null;message=str(result,"error");rebuildWidgets();}
|
||||
else {photoPart++;sendAfter=ticks+4;}
|
||||
});
|
||||
} else if(commit!=null) {var actionToRun=commit;commit=null;actionToRun.run();}
|
||||
}
|
||||
if (!composing && needsLoad && ticks%4==0 && request(this,id.isEmpty()?"list":"thread",id,"",kind,"info","","",0,page,result -> {
|
||||
if (result.has("error")) message=str(result,"error");
|
||||
else { data=result; moderator=flag(result,"moderator"); }
|
||||
rebuildWidgets();
|
||||
})) needsLoad=false;
|
||||
}
|
||||
@Override public boolean isPauseScreen() { return false; }
|
||||
private static final class LinkScreen extends BlocodexFrame {
|
||||
String code="", message="link_help";
|
||||
LinkScreen(Screen parent) { super(parent); }
|
||||
@Override protected void init() {
|
||||
initFrame("history",text("link")); int w=contentWidth-28; var layout=LinearLayout.vertical().spacing(8);
|
||||
layout.addChild(new MultiLineTextWidget(text(message),font).setMaxWidth(w));
|
||||
var box=layout.addChild(new EditBox(font,w,20,text("code"))); box.setMaxLength(32); box.setValue(code); box.setResponder(value -> code=value);
|
||||
layout.addChild(Button.builder(text("link"),b -> request(this,"link","","","article","info","",code,0,0,r -> { message=r.has("error")?str(r,"error"):"linked"; rebuildWidgets(); })).width(w).build());
|
||||
layout.setPosition(contentX+8,contentTop); layout.arrangeElements(); layout.visitWidgets(this::addRenderableWidget);
|
||||
}
|
||||
@Override public boolean isPauseScreen() { return false; }
|
||||
}
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.community.CommunityPhoto;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.function.Consumer;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.layouts.LinearLayout;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import static fr.koka.sanctuary.client.CommunityClient.text;
|
||||
|
||||
/** Read-only screenshot library, paged and scrollable; no disk or image decoding on the render thread. */
|
||||
public final class CommunityScreenshotsScreen extends BlocodexFrame {
|
||||
private static final int PAGE_SIZE=24;
|
||||
private static final ExecutorService LOADER=Executors.newSingleThreadExecutor(r->{var t=new Thread(r,"Sanctuary screenshots");t.setDaemon(true);return t;});
|
||||
private record Shot(String name,String photo,BufferedImage thumbnail) { }
|
||||
private record Library(List<Path> paths,List<Shot> shots,String error) { }
|
||||
private final Consumer<String> assign;
|
||||
private final List<CommunityPhotoWidget> textures=new ArrayList<>();
|
||||
private List<Path> paths=List.of();
|
||||
private List<Shot> shots=List.of();
|
||||
private CompletableFuture<Library> loading;
|
||||
private int page;
|
||||
private boolean loaded;
|
||||
private String error="";
|
||||
private Shot selected;
|
||||
public CommunityScreenshotsScreen(Screen parent,Consumer<String> assign) {super(parent);this.assign=assign;}
|
||||
private void load(boolean refresh) {
|
||||
loaded=true;
|
||||
Path folder=minecraft.gameDirectory.toPath().resolve("screenshots");
|
||||
List<Path> known=paths;int offset=page*PAGE_SIZE;
|
||||
loading=CompletableFuture.supplyAsync(()->{
|
||||
try {
|
||||
List<Path> all=known;
|
||||
if(refresh) {
|
||||
if(!Files.isDirectory(folder))return new Library(List.of(),List.of(),"");
|
||||
try(var files=Files.list(folder)) {
|
||||
all=files.filter(p->p.getFileName().toString().toLowerCase(Locale.ROOT).endsWith(".png")&&Files.isRegularFile(p,LinkOption.NOFOLLOW_LINKS))
|
||||
.sorted(Comparator.comparing((Path p)->p.getFileName().toString()).reversed()).limit(4096).toList();
|
||||
}
|
||||
}
|
||||
var found=new ArrayList<Shot>();
|
||||
for(Path file:all.stream().skip(offset).limit(PAGE_SIZE).toList()) {
|
||||
try {
|
||||
if(!Files.isRegularFile(file,LinkOption.NOFOLLOW_LINKS) || Files.size(file)>32*1024*1024)throw new java.io.IOException("Screenshot size");
|
||||
byte[] bytes;
|
||||
try(var in=Files.newInputStream(file,LinkOption.NOFOLLOW_LINKS)) {bytes=in.readNBytes(32*1024*1024+1);}
|
||||
if(bytes.length>32*1024*1024)throw new java.io.IOException("Screenshot size");
|
||||
var source=CommunityPhoto.decode(bytes,8192,8192);
|
||||
String photo=CommunityPhoto.encode(source,CommunityPhoto.WIDTH,CommunityPhoto.HEIGHT);
|
||||
String thumb=CommunityPhoto.encode(source,240,135);
|
||||
found.add(new Shot(file.getFileName().toString(),photo,CommunityPhoto.decode(Base64.getDecoder().decode(thumb),240,135)));
|
||||
} catch(Exception e) {found.add(new Shot(file.getFileName().toString(),"",null));}
|
||||
}
|
||||
return new Library(all,found,"");
|
||||
} catch(Exception e) {return new Library(List.of(),List.of(),"screenshots_error");}
|
||||
},LOADER);
|
||||
}
|
||||
@Override protected void init() {
|
||||
closeTextures();initFrame("history",text("screenshots"));
|
||||
if(!loaded)load(true);
|
||||
if(selected!=null) {preview();return;}
|
||||
int w=contentWidth-24;
|
||||
var controls=LinearLayout.horizontal().spacing(4);
|
||||
controls.addChild(Button.builder(text("previous"),b->{page--;load(false);rebuildWidgets();}).width(72).build()).active=page>0 && loading==null;
|
||||
controls.addChild(new StringWidget(Math.max(30,w-228),20,text("page",page+1),font));
|
||||
controls.addChild(Button.builder(text("next"),b->{page++;load(false);rebuildWidgets();}).width(72).build()).active=(page+1)*PAGE_SIZE<paths.size() && loading==null;
|
||||
controls.addChild(Button.builder(text("refresh"),b->{page=0;load(true);rebuildWidgets();}).width(76).build()).active=loading==null;
|
||||
controls.setPosition(contentX+8,contentBottom-20);controls.arrangeElements();controls.visitWidgets(this::addRenderableWidget);
|
||||
var layout=LinearLayout.vertical().spacing(8);
|
||||
if(loading!=null)layout.addChild(new StringWidget(text("loading"),font));
|
||||
else if(!error.isEmpty() || shots.isEmpty())layout.addChild(new MultiLineTextWidget(text(error.isEmpty()?"screenshots_empty":error),font).setMaxWidth(w));
|
||||
else {
|
||||
int columns=w>=450?3:2,cell=(w-(columns-1)*8)/columns;
|
||||
for(int i=0;i<shots.size();i+=columns) {
|
||||
var row=layout.addChild(LinearLayout.horizontal().spacing(8));
|
||||
for(int j=i;j<Math.min(i+columns,shots.size());j++) {
|
||||
Shot shot=shots.get(j);
|
||||
if(shot.thumbnail()==null)row.addChild(Button.builder(Component.literal(shot.name()),b->{}).size(cell,cell*9/16+18).tooltip(Tooltip.create(text("photo_invalid"))).build()).active=false;
|
||||
else {var widget=new CommunityPhotoWidget(cell,cell*9/16+18,shot.thumbnail(),Component.literal(shot.name()),true,()->{selected=shot;rebuildWidgets();});textures.add(widget);row.addChild(widget);}
|
||||
}
|
||||
}
|
||||
}
|
||||
var scroll=new ScrollableLayout(minecraft,layout,Math.max(24,contentBottom-contentTop-28),ScrollableLayout.ReserveStrategy.RIGHT);
|
||||
scroll.setPosition(contentX+8,contentTop);scroll.arrangeElements();scroll.visitWidgets(this::addRenderableWidget);
|
||||
}
|
||||
private void preview() {
|
||||
int w=contentWidth-16,h=Math.max(24,contentBottom-contentTop-34);
|
||||
try {
|
||||
var image=CommunityPhoto.decode(Base64.getDecoder().decode(selected.photo()),CommunityPhoto.WIDTH,CommunityPhoto.HEIGHT);
|
||||
var widget=new CommunityPhotoWidget(w,h,image,Component.literal(selected.name()),false,null);
|
||||
textures.add(widget);widget.setPosition(contentX+8,contentTop);addRenderableWidget(widget);
|
||||
} catch(Exception e) {error="photo_invalid";selected=null;return;}
|
||||
addRenderableWidget(Button.builder(text("screenshots_back"),b->{selected=null;rebuildWidgets();}).bounds(contentX+8,contentBottom-24,(w-6)/2,20).build());
|
||||
addRenderableWidget(Button.builder(text("photo_assign"),b->{assign.accept(selected.photo());minecraft.gui.setScreen(parent);}).bounds(contentX+8+(w+6)/2,contentBottom-24,(w-6)/2,20).build());
|
||||
}
|
||||
@Override public void tick() {
|
||||
if(loading!=null && loading.isDone()) {
|
||||
var result=loading.join();loading=null;paths=result.paths();shots=result.shots();error=result.error();rebuildWidgets();
|
||||
}
|
||||
}
|
||||
private void closeTextures() {textures.forEach(CommunityPhotoWidget::close);textures.clear();}
|
||||
@Override public void removed() {closeTextures();}
|
||||
@Override public boolean isPauseScreen() {return false;}
|
||||
}
|
||||
@@ -27,6 +27,15 @@ public final class DynamicLightClient {
|
||||
private static DynamicLightConfig config;
|
||||
private static final Map<Item,Integer> overrides=new IdentityHashMap<>();
|
||||
private static int ticks;
|
||||
private static boolean invalidatingSections;
|
||||
private static final fr.koka.sanctuary.client.shader.ColoredLightMaterials colors=new fr.koka.sanctuary.client.shader.ColoredLightMaterials();
|
||||
private static Object models;
|
||||
public static boolean invalidatingSections(){return invalidatingSections;}
|
||||
private static final class ColorMix {
|
||||
double r,g,b;
|
||||
void add(int light,int color){if(light<=0)return;double energy=Math.pow(.8,15-light);r=Math.max(r,(color>>16&255)*energy);g=Math.max(g,(color>>8&255)*energy);b=Math.max(b,(color&255)*energy);}
|
||||
int result(){double m=Math.max(1e-8,Math.max(r,Math.max(g,b)));return (int)Math.round(r/m*255)<<16|(int)Math.round(g/m*255)<<8|(int)Math.round(b/m*255);}
|
||||
}
|
||||
private DynamicLightClient() {}
|
||||
public static void register(KeyMapping.Category category) {
|
||||
config=DynamicLightConfig.load();
|
||||
@@ -50,7 +59,7 @@ public final class DynamicLightClient {
|
||||
}
|
||||
public static int pendingSections() {return field.pendingCount();}
|
||||
private static void reset(ClientLevel level) {
|
||||
field=new DynamicLightField();view=new View(level,field.snapshot());ticks=0;
|
||||
field=new DynamicLightField();view=new View(level,field.snapshot());ticks=0;colors.clear();models=null;
|
||||
}
|
||||
private static int emission(ItemStack stack) {return overrides.getOrDefault(stack.getItem(),-1)>=0?overrides.get(stack.getItem()):ItemLightEmission.of(stack);}
|
||||
private static void tick(Minecraft m) {
|
||||
@@ -59,6 +68,8 @@ public final class DynamicLightClient {
|
||||
if(ticks++%config.updateIntervalTicks==0) {
|
||||
List<DynamicLightField.Source> sources=List.of();
|
||||
if(config.enabled) {
|
||||
var currentModels=m.getModelManager().getBlockStateModelSet();if(models!=currentModels){colors.clear();models=currentModels;}
|
||||
boolean tinted=fr.koka.sanctuary.client.shader.VanillaLight.coloredLights();
|
||||
var eye=m.getCameraEntity().getEyePosition();
|
||||
var collect=new DynamicLightField.Collector(config.dropClusterSize);
|
||||
double distance=config.range*config.range;
|
||||
@@ -66,23 +77,26 @@ public final class DynamicLightClient {
|
||||
if(entity.isRemoved() || entity.distanceToSqr(eye)>distance)continue;
|
||||
boolean drop=entity instanceof ItemEntity;
|
||||
int light=entity.isOnFire()?15:0;
|
||||
var mix=new ColorMix();if(light>0)mix.add(light,0xFFAB55);
|
||||
double y=entity.getY()+entity.getBbHeight()*.5;
|
||||
if(entity instanceof ItemEntity item)light=Math.max(light,emission(item.getItem()));
|
||||
if(entity instanceof ItemEntity item){int emitted=emission(item.getItem());light=Math.max(light,emitted);if(emitted>0&&tinted)mix.add(emitted,colors.color(item.getItem()));}
|
||||
else if(entity instanceof LivingEntity living) {
|
||||
if(living instanceof Player p && p.isSpectator())continue;
|
||||
light=Math.max(light,Math.max(emission(living.getMainHandItem()),emission(living.getOffhandItem())));
|
||||
light=Math.max(light,emission(living.getItemBySlot(EquipmentSlot.HEAD)));
|
||||
light=Math.max(light,emission(HeadCosmeticService.appearance(living)));
|
||||
for(var stack:new ItemStack[]{living.getMainHandItem(),living.getOffhandItem(),living.getItemBySlot(EquipmentSlot.HEAD),HeadCosmeticService.appearance(living)}){
|
||||
int emitted=emission(stack);light=Math.max(light,emitted);if(emitted>0&&tinted)mix.add(emitted,colors.color(stack));
|
||||
}
|
||||
y=entity.getEyeY();
|
||||
}
|
||||
if(light>0)collect.add(entity.getId(),drop,entity.getX(),y,entity.getZ(),light);
|
||||
if(light>0)collect.add(entity.getId(),drop,entity.getX(),y,entity.getZ(),light,tinted?mix.result():0xFFFFFF);
|
||||
}
|
||||
sources=collect.finish(config.maxSources,eye.x,eye.y,eye.z,m.player==null?-1:m.player.getId());
|
||||
}
|
||||
if(field.update(sources))view=new View(m.level,field.snapshot());
|
||||
}
|
||||
// Native renderer marks only resident sections; it neither loads chunks nor rebuilds the whole world.
|
||||
field.drain(config.sectionUpdatesPerTick,key->m.levelExtractor.setSectionDirty(
|
||||
DynamicLightField.sectionX(key),DynamicLightField.sectionY(key),DynamicLightField.sectionZ(key)));
|
||||
invalidatingSections=true;
|
||||
try{field.drain(config.sectionUpdatesPerTick,key->m.levelExtractor.setSectionDirty(
|
||||
DynamicLightField.sectionX(key),DynamicLightField.sectionY(key),DynamicLightField.sectionZ(key)));}
|
||||
finally{invalidatingSections=false;}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
/** Routes network responses to the current pause screen, never a static cross-server cache. */
|
||||
public interface PauseAtlasAccess {
|
||||
PauseAtlasWidget sanctuary$pauseMap();
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.atlas.AtlasPayloads;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
import net.minecraft.client.gui.components.AbstractButton;
|
||||
import net.minecraft.client.gui.components.Tooltip;
|
||||
import net.minecraft.client.gui.narration.NarrationElementOutput;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.input.InputWithModifiers;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.client.renderer.texture.DynamicTexture;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.level.material.MapColor;
|
||||
|
||||
/** Personal surveyed terrain only. A token and disposable texture belong to this pause instance. */
|
||||
public final class PauseAtlasWidget extends AbstractButton implements AutoCloseable {
|
||||
private static final int SIZE=AtlasPayloads.PIXELS;
|
||||
private static final Identifier TEXTURE=SanctuaryMod.id("pause_atlas_viewport");
|
||||
private final Screen owner;
|
||||
private final Minecraft client=Minecraft.getInstance();
|
||||
private DynamicTexture texture;
|
||||
private int token,originX,originZ,refresh;
|
||||
private double centerX,centerZ;
|
||||
private String dimension="",status="loading";
|
||||
private boolean dirty,readable;
|
||||
private int pixels;
|
||||
PauseAtlasWidget(Screen owner,int x,int y,int width,int height) {
|
||||
super(x,y,width,height,Component.translatable("sanctuary.pause.map"));this.owner=owner;
|
||||
setTooltip(Tooltip.create(Component.translatable("sanctuary.pause.map_hint")));
|
||||
texture=new DynamicTexture("Sanctuary pause map",SIZE,SIZE,false);
|
||||
client.getTextureManager().register(TEXTURE,texture);reset();request();
|
||||
}
|
||||
private void reset() {texture.getPixels().fillRect(0,0,SIZE,SIZE,0);dirty=true;readable=false;pixels=0;status="loading";}
|
||||
private void request() {
|
||||
if(client.player==null || client.level==null)return;
|
||||
centerX=client.player.getX();centerZ=client.player.getZ();
|
||||
int cx=Math.floorDiv((int)Math.floor(centerX),16),cz=Math.floorDiv((int)Math.floor(centerZ),16);
|
||||
int nx=(cx-AtlasPayloads.SIDE/2)*16,nz=(cz-AtlasPayloads.SIDE/2)*16;
|
||||
String next=client.level.dimension().identifier().toString();
|
||||
if(nx!=originX || nz!=originZ || !next.equals(dimension))reset();
|
||||
originX=nx;originZ=nz;dimension=next;token=AtlasClient.nextToken();refresh=1600;
|
||||
AtlasClient.send(new AtlasPayloads.Request(token,cx,cz,false));
|
||||
}
|
||||
public void accept(AtlasPayloads.Response response) {
|
||||
if(texture==null || response.token()!=token || client.level==null || !dimension.equals(client.level.dimension().identifier().toString()))return;
|
||||
if(response.status().equals("retry")){refresh=12;return;}
|
||||
status=response.status();readable=status.equals("ready") || status.equals("full");
|
||||
if(!readable) {reset();status=response.status();refresh=100;return;}
|
||||
for(var tile:response.tiles()) {
|
||||
int x=tile.x()*16-originX,z=tile.z()*16-originZ;if(x<0 || z<0 || x+16>SIZE || z+16>SIZE)continue;
|
||||
byte[] colors=tile.colors();
|
||||
for(int dz=0;dz<16;dz++)for(int dx=0;dx<16;dx++) {
|
||||
int packed=Byte.toUnsignedInt(colors[dz*16+dx]);
|
||||
int color=packed<4?0:MapColor.getColorFromPackedId(packed);
|
||||
int before=texture.getPixels().getPixel(x+dx,z+dz);
|
||||
if(before==0 && color!=0)pixels++;else if(before!=0 && color==0)pixels--;
|
||||
texture.getPixels().setPixel(x+dx,z+dz,color);
|
||||
}
|
||||
dirty=true;
|
||||
}
|
||||
if(response.done())refresh=100;
|
||||
}
|
||||
void tick() {
|
||||
if(texture==null)return;
|
||||
if(client.level!=null && !dimension.equals(client.level.dimension().identifier().toString())) {reset();request();}
|
||||
if(--refresh<=0)request();
|
||||
}
|
||||
public int knownPixels() {return pixels;}
|
||||
@Override public void onPress(InputWithModifiers input) {ProgressionClient.open(owner,"history");}
|
||||
@Override protected void extractContents(GuiGraphicsExtractor g,int mx,int my,float delta) {
|
||||
if(texture==null)return;
|
||||
if(dirty){texture.upload();dirty=false;}
|
||||
int x=getX(),y=getY();var font=client.font;
|
||||
g.fill(x,y,getRight(),getBottom(),0x90151B19);
|
||||
g.enableScissor(x,y,getRight(),getBottom());
|
||||
if(readable) {
|
||||
double zoom=Math.max(.75,Math.max(width,height)/992.);
|
||||
int left=x+width/2+(int)((originX-centerX)*zoom),top=y+height/2+(int)((originZ-centerZ)*zoom);
|
||||
g.blit(RenderPipelines.GUI_TEXTURED,TEXTURE,left,top,0,0,(int)(SIZE*zoom),(int)(SIZE*zoom),SIZE,SIZE,SIZE,SIZE);
|
||||
int px=x+width/2,pz=y+height/2;
|
||||
if(client.player!=null) {
|
||||
px+=(int)((client.player.getX()-centerX)*zoom);pz+=(int)((client.player.getZ()-centerZ)*zoom);
|
||||
double yaw=Math.toRadians(client.player.getYRot());
|
||||
for(int i=2;i<9;i++){int ax=px-(int)Math.round(Math.sin(yaw)*i),az=pz+(int)Math.round(Math.cos(yaw)*i);g.fill(ax-1,az-1,ax+2,az+2,0xFF141818);g.fill(ax,az,ax+1,az+1,0xFFFFFFFF);}
|
||||
}
|
||||
int color=ProgressionClient.identity==null?0xFFFFFFFF:0xFF000000|ProgressionClient.identity.color();
|
||||
g.fill(px-3,pz-3,px+4,pz+4,0xFF141818);g.fill(px-2,pz-2,px+3,pz+3,color);
|
||||
}
|
||||
g.text(font,"N ↑",x+7,y+7,0xFFFFFFFF);
|
||||
if(pixels==0)g.textWithWordWrap(font,SanctuaryAtlasScreen.label(readable?"empty":status),x+8,y+height/2+12,width-16,0xFFCCCCCC);
|
||||
g.text(font,font.plainSubstrByWidth(getMessage().getString(),width-14),x+7,getBottom()-16,0xFFF0EEE6);
|
||||
if(isHoveredOrFocused()) {g.fill(x,y,getRight(),y+1,0xFFD0CCAF);g.fill(x,getBottom()-1,getRight(),getBottom(),0xFFD0CCAF);}
|
||||
g.disableScissor();
|
||||
}
|
||||
@Override protected void updateWidgetNarration(NarrationElementOutput output) {defaultButtonNarrationText(output);}
|
||||
@Override public void close() {
|
||||
if(texture!=null){AtlasClient.send(new AtlasPayloads.Request(0,0,0,false));client.getTextureManager().release(TEXTURE);texture=null;}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.google.gson.*;
|
||||
import java.util.*;
|
||||
import java.util.function.Consumer;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.layouts.LinearLayout;
|
||||
import net.minecraft.client.gui.narration.NarrationElementOutput;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.input.InputWithModifiers;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import static fr.koka.sanctuary.client.CommunityClient.*;
|
||||
|
||||
/** Each column owns its scrolling and reads the existing paginated contract. */
|
||||
final class PauseCommunityFeed {
|
||||
private final Screen owner;
|
||||
private final String kind;
|
||||
private final int width;
|
||||
private final LinearLayout rows=LinearLayout.vertical().spacing(5);
|
||||
private final ScrollableLayout scroll;
|
||||
private final List<JsonObject> entries=new ArrayList<>();
|
||||
private boolean needsLoad=true,more,closed,loaded;
|
||||
private int page;
|
||||
private String error="";
|
||||
PauseCommunityFeed(Screen owner,Consumer<AbstractWidget> add,int x,int y,int w,int h,String kind) {
|
||||
this.owner=owner;this.kind=kind;width=w-10;
|
||||
var heading=new PauseTextButton(x,y,w-24,20,text(kind),()->open(owner,kind,"",false));
|
||||
heading.setTooltip(Tooltip.create(text("browse")));add.accept(heading);
|
||||
add.accept(Button.builder(Component.literal("+"),b->open(owner,kind,"",true)).bounds(x+w-20,y,20,20).tooltip(Tooltip.create(text("publish"))).build());
|
||||
rows.addChild(new StringWidget(width,20,text(available()?"loading":"unavailable"),Minecraft.getInstance().font));
|
||||
scroll=new ScrollableLayout(Minecraft.getInstance(),rows,Math.max(24,h-24),ScrollableLayout.ReserveStrategy.RIGHT);
|
||||
scroll.setMinWidth(width);scroll.setMinHeight(Math.max(24,h-24));scroll.setPosition(x,y+24);scroll.arrangeElements();scroll.visitWidgets(add);
|
||||
}
|
||||
boolean load() {
|
||||
if(closed || !needsLoad)return false;
|
||||
if(!request(owner,"list","","",kind,"info","","",0,page,result->{
|
||||
if(closed)return;
|
||||
if(result.has("error")) {showError(str(result,"error"));return;}
|
||||
var listing=result.getAsJsonObject("page");
|
||||
var next=page==0?new ArrayList<JsonObject>():new ArrayList<>(entries);
|
||||
for(var value:listing.getAsJsonArray("entries")) {
|
||||
var entry=value.getAsJsonObject();
|
||||
if(next.stream().noneMatch(e->str(e,"id").equals(str(entry,"id"))))next.add(entry);
|
||||
}
|
||||
boolean nextMore=flag(listing,"more");
|
||||
if(loaded && error.isEmpty() && entries.equals(next) && more==nextMore)return;
|
||||
loaded=true;error="";entries.clear();entries.addAll(next);more=nextMore;rebuild();
|
||||
}))return false;
|
||||
needsLoad=false;return true;
|
||||
}
|
||||
private void rebuild() {
|
||||
rows.removeChildren();
|
||||
var font=Minecraft.getInstance().font;
|
||||
if(entries.isEmpty() && error.isEmpty())rows.addChild(new MultiLineTextWidget(text("empty"),font).setMaxWidth(width));
|
||||
for(var entry:entries) {
|
||||
Runnable action=()->open(owner,kind,str(entry,"id"),false);
|
||||
if(kind.equals("article")) {
|
||||
var title=Component.literal((flag(entry,"pinned")?"★ ":"")+str(entry,"title"));
|
||||
var row=rows.addChild(LinearLayout.horizontal().spacing(5));
|
||||
var face=row.addChild(new PlayerFaceWidget(20,net.minecraft.world.item.component.ResolvableProfile.createUnresolved(UUID.fromString(str(entry,"authorId")))));
|
||||
face.setTooltip(Tooltip.create(Component.literal(str(entry,"authorName"))));
|
||||
row.addChild(Button.builder(title,b->action.run()).width(width-25).tooltip(Tooltip.create(title.copy().append("\n").append(str(entry,"authorName")))).build());
|
||||
} else rows.addChild(new NoticeCard(width,entry,action));
|
||||
}
|
||||
if(more && error.isEmpty())rows.addChild(Button.builder(text("more"),b->{page++;needsLoad=true;b.active=false;}).width(width).build());
|
||||
scroll.arrangeElements();
|
||||
}
|
||||
private void showError(String error) {
|
||||
this.error=error;rebuild();rows.addChild(new MultiLineTextWidget(text(error),Minecraft.getInstance().font).setMaxWidth(width));
|
||||
rows.addChild(Button.builder(text("refresh"),b->{needsLoad=true;b.active=false;}).width(width).build());scroll.arrangeElements();
|
||||
}
|
||||
void refresh() {if(page==0 && scroll.getScrollAmount()==0 && error.isEmpty())needsLoad=true;}
|
||||
void close() {closed=true;}
|
||||
private static final class NoticeCard extends AbstractButton {
|
||||
private final JsonObject entry;
|
||||
private final Runnable action;
|
||||
NoticeCard(int width,JsonObject entry,Runnable action) {super(0,0,width,84,Component.literal(str(entry,"title")+"\n"+str(entry,"body")));this.entry=entry;this.action=action;}
|
||||
@Override public void onPress(InputWithModifiers input) {action.run();}
|
||||
@Override protected void extractContents(GuiGraphicsExtractor g,int mx,int my,float delta) {
|
||||
var font=Minecraft.getInstance().font;int x=getX(),y=getY(),border=isHoveredOrFocused()?0xFFC8C0A6:0xFF727365;
|
||||
g.fill(x,y,getRight(),getBottom(),0xBA202720);
|
||||
g.fill(x,y,getRight(),y+1,border);g.fill(x,getBottom()-1,getRight(),getBottom(),border);
|
||||
g.fill(x,y,x+1,getBottom(),border);g.fill(getRight()-1,y,getRight(),getBottom(),border);
|
||||
g.text(font,font.plainSubstrByWidth(str(entry,"title"),width-14),x+7,y+7,0xFFF0EEE6);
|
||||
var lines=font.split(Component.literal(str(entry,"body")),width-14);
|
||||
for(int i=0;i<Math.min(3,lines.size());i++)g.text(font,lines.get(i),x+7,y+23+i*10,0xFFD2D6CF,false);
|
||||
g.text(font,font.plainSubstrByWidth(str(entry,"authorName"),width-14),x+7,y+height-23,0xFFAABAAA);
|
||||
g.text(font,font.plainSubstrByWidth(text("category."+str(entry,"category")).append(" · ").append(text(str(entry,"status"))).getString(),width-14),x+7,y+height-12,0xFFAABAAA);
|
||||
}
|
||||
@Override protected void updateWidgetNarration(NarrationElementOutput output) {defaultButtonNarrationText(output);}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
import net.minecraft.client.gui.components.AbstractButton;
|
||||
import net.minecraft.client.gui.narration.NarrationElementOutput;
|
||||
import net.minecraft.client.input.InputWithModifiers;
|
||||
import net.minecraft.network.chat.Component;
|
||||
|
||||
/** Quiet navigation: only the active target gains a background. */
|
||||
final class PauseTextButton extends AbstractButton {
|
||||
private final Runnable action;
|
||||
PauseTextButton(int x,int y,int width,int height,Component label,Runnable action) {super(x,y,width,height,label);this.action=action;}
|
||||
@Override public void onPress(InputWithModifiers input) {action.run();}
|
||||
@Override protected void extractContents(GuiGraphicsExtractor g,int mx,int my,float delta) {
|
||||
if(isHoveredOrFocused())g.fill(getX(),getY(),getRight(),getBottom(),0x80424A48);
|
||||
var font=Minecraft.getInstance().font;
|
||||
if(height>24) {
|
||||
var lines=font.split(getMessage(),width-10);int count=Math.min(2,lines.size());
|
||||
for(int i=0;i<count;i++)g.text(font,lines.get(i),getX()+(width-font.width(lines.get(i)))/2,getY()+(height-count*10)/2+i*10,active?0xFFF0EEE6:0xFF969696);
|
||||
return;
|
||||
}
|
||||
g.text(font,font.plainSubstrByWidth(getMessage().getString(),width-10),getX()+5,getY()+(height-9)/2,active?0xFFF0EEE6:0xFF969696);
|
||||
}
|
||||
@Override protected void updateWidgetNarration(NarrationElementOutput output) {defaultButtonNarrationText(output);}
|
||||
}
|
||||
@@ -17,7 +17,9 @@ public final class SanctuaryKnowledgeClient implements ClientModInitializer {
|
||||
}
|
||||
|
||||
public static void registerClient() {
|
||||
CommunityClient.register();
|
||||
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();
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import com.mojang.blaze3d.buffers.Std140Builder;
|
||||
import com.mojang.blaze3d.pipeline.TextureTarget;
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.renderpearl.api.GpuFormat;
|
||||
import com.mojang.renderpearl.api.buffers.GpuBuffer;
|
||||
import com.mojang.renderpearl.api.buffers.GpuBufferSlice;
|
||||
import com.mojang.renderpearl.api.pipeline.*;
|
||||
import com.mojang.renderpearl.api.textures.FilterMode;
|
||||
import com.mojang.renderpearl.api.textures.GpuTextureView;
|
||||
import fr.koka.sanctuary.client.panorama.PanoramaCapture;
|
||||
import java.util.*;
|
||||
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.TextureAtlas;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.level.dimension.DimensionType;
|
||||
import net.minecraft.world.level.material.FogType;
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Vector3f;
|
||||
import org.joml.Vector4f;
|
||||
|
||||
/** Selective emissive bloom. No brightness threshold on ordinary world blocks, no scene-copy or readback. */
|
||||
public final class Bloom {
|
||||
private static Identifier id(String path){return Identifier.parse("sanctuary:"+path);}
|
||||
private static final BindGroupLayout SCENE=BindGroupLayout.builder().withUniform("BloomScene",UniformType.UNIFORM_BUFFER)
|
||||
.withUniform("WorldDepth",UniformType.COMBINED_IMAGE_SAMPLER).build();
|
||||
private static final RenderPipeline EMITTERS=RenderPipelines.register(RenderPipeline.builder()
|
||||
.withLocation(id("pipeline/bloom_emitters")).withVertexShader(id("core/bloom_emitters")).withFragmentShader(id("core/bloom_emitters"))
|
||||
.withBindGroupLayout(BindGroupLayouts.DYNAMIC_TRANSFORMS).withBindGroupLayout(SCENE)
|
||||
.withBindGroupLayout(BindGroupLayout.builder().withUniform("BlockAtlas",UniformType.COMBINED_IMAGE_SAMPLER).withUniform("EmissionMasks",UniformType.COMBINED_IMAGE_SAMPLER).build())
|
||||
.withVertexBinding(0,BloomTerrain.FORMAT).withPrimitiveTopology(PrimitiveTopology.QUADS).withCull(false)
|
||||
.withColorTargetState(new ColorTargetState(Optional.empty(),GpuFormat.RGBA8_UNORM,ColorTargetState.WRITE_ALL))
|
||||
.withDepthStencilState(Optional.empty()).build());
|
||||
private static RenderPipeline screen(String name,BlendFunction blend,boolean scene,boolean blur){
|
||||
var layout=BindGroupLayout.builder().withUniform("InputTexture",UniformType.COMBINED_IMAGE_SAMPLER);
|
||||
if(blur)layout.withUniform("BlurParams",UniformType.UNIFORM_BUFFER);
|
||||
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_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);
|
||||
private static final RenderPipeline CORE=screen("bloom_core",new BlendFunction(BlendFactor.ONE,BlendFactor.ONE_MINUS_SRC_ALPHA,BlendFactor.ZERO,BlendFactor.ONE),false,false);
|
||||
private static final RenderPipeline APPLY=screen("bloom_apply",new BlendFunction(BlendFactor.ONE,BlendFactor.ONE_MINUS_SRC_COLOR,BlendFactor.ZERO,BlendFactor.ONE),true,false);
|
||||
private static final RenderPipeline FLARE_SOURCE=screen("lens_flare_source",null,true,false);
|
||||
private static final RenderPipeline FLARE=screen("lens_flare",new BlendFunction(BlendFactor.ONE,BlendFactor.ONE_MINUS_SRC_COLOR,BlendFactor.ZERO,BlendFactor.ONE),true,false);
|
||||
private static final BloomTerrain TERRAIN=new BloomTerrain();
|
||||
private static final Matrix4f projection=new Matrix4f();
|
||||
private static TextureTarget emission,small,ping,flareSource;
|
||||
private static MappableRingBuffer scene,horizontal,vertical;
|
||||
private static int frames;
|
||||
private static boolean applied;
|
||||
private Bloom(){}
|
||||
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 render(){
|
||||
applied=false;var mc=Minecraft.getInstance();
|
||||
boolean halo=VanillaLight.bloom()&&VanillaLight.bloomIntensity()>0;
|
||||
boolean flare=VanillaLight.lensFlare()&&VanillaLight.lensFlareIntensity()>0;
|
||||
boolean cores=VanillaLight.oreEmission()&&VanillaLight.oreIntensity()>0;
|
||||
if(!VanillaLight.active()||(!halo&&!cores&&!flare)||mc.level==null){release();return;}
|
||||
if(PanoramaCapture.rendering())return;
|
||||
var state=mc.gameRenderer.gameRenderState().levelRenderState;var camera=state.cameraRenderState;
|
||||
if(camera.fogType!=FogType.NONE)return;
|
||||
var main=mc.gameRenderer.mainRenderTarget();
|
||||
if(emission==null||emission.width!=main.width||emission.height!=main.height){
|
||||
targetsClose();emission=target("emission",main.width,main.height);
|
||||
}
|
||||
if(scene==null)scene=uniforms("bloom scene",192);
|
||||
if(halo){
|
||||
if(small==null){small=target("bloom low",Math.max(1,main.width/8),Math.max(1,main.height/8));ping=target("bloom ping",small.width,small.height);}
|
||||
if(horizontal==null){horizontal=uniforms("bloom horizontal",16);vertical=uniforms("bloom vertical",16);}
|
||||
}else haloClose();
|
||||
if(flare){if(flareSource==null)flareSource=target("lens flare sources",2,1);}else flareClose();
|
||||
// A celestial-only flare never needs to scan or mesh terrain emitters.
|
||||
var entries=(halo||cores)?TERRAIN.update():List.<BloomTerrain.Entry>of();
|
||||
if(!halo&&!cores)TERRAIN.close();
|
||||
var atlas=TERRAIN.materials.atlas();
|
||||
var vp=new Matrix4f(projection).mul(camera.viewRotationMatrix);var inverse=new Matrix4f(vp).invert();
|
||||
var sun=PixelShadows.sunDirection(state.skyRenderState.sunAngle);var moon=PixelShadows.sunDirection(state.skyRenderState.moonAngle);
|
||||
float celestial=state.skyRenderState.skybox==DimensionType.Skybox.OVERWORLD?state.skyRenderState.rainBrightness:0;
|
||||
var flareSun=projectFlare(vp,sun,celestial);
|
||||
var flareMoon=projectFlare(vp,moon,celestial*.35f);
|
||||
var fog=camera.fogData;scene.rotate();
|
||||
try(var mapped=scene.currentBuffer().map(false,true)){
|
||||
Std140Builder.intoBuffer(mapped.data()).putMat4f(inverse)
|
||||
.putVec4(VanillaLight.oreEmission()?VanillaLight.oreIntensity()/100f:0,RenderSystem.getDevice().getDeviceInfo().isZZeroToOne()?1:0,VanillaLight.bloomIntensity()/50f,0)
|
||||
.putVec4(sun.x,sun.y,sun.z,celestial).putVec4(moon.x,moon.y,moon.z,celestial*.35f)
|
||||
.putVec4(fog.renderDistanceStart,fog.renderDistanceEnd,fog.environmentalStart,fog.environmentalEnd).putVec4(fog.color.w,0,0,0)
|
||||
.putVec4(flareSun).putVec4(flareMoon).putVec4(flare?VanillaLight.lensFlareIntensity()/100f:0,(float)main.width/main.height,0,0);
|
||||
}
|
||||
if(halo){
|
||||
float radius=.25f+VanillaLight.bloomRadius()*.0075f;
|
||||
horizontal.rotate();try(var mapped=horizontal.currentBuffer().map(false,true)){Std140Builder.intoBuffer(mapped.data()).putVec4(radius/small.width,0,0,0);}
|
||||
vertical.rotate();try(var mapped=vertical.currentBuffer().map(false,true)){Std140Builder.intoBuffer(mapped.data()).putVec4(0,radius/small.height,0,0);}
|
||||
}
|
||||
// Gather transforms before opening any pass (the native dynamic ring may resize).
|
||||
var transforms=new ArrayList<GpuBufferSlice>(entries.size());int max=0;
|
||||
for(var entry:entries){var pos=entry.origin();var offset=new Vector3f((float)(pos.getX()-camera.pos.x),(float)(pos.getY()-camera.pos.y),(float)(pos.getZ()-camera.pos.z));
|
||||
transforms.add(RenderSystem.getDynamicUniforms().writeTransform(new Matrix4f(vp).translate(offset),new Vector4f(1,1,1,1),offset,new Matrix4f()));max=Math.max(max,entry.indices());}
|
||||
var indices=RenderSystem.getSequentialBuffer(PrimitiveTopology.QUADS);indices.requestIndexCount(max);
|
||||
var nearest=RenderSystem.getSamplerCache().getClampToEdge(FilterMode.NEAREST);
|
||||
if(halo||flare)screen(SKY,emission.getColorTextureView(),main.getColorTextureView(),true,null);
|
||||
else try(var clear=RenderSystem.getDevice().createCommandEncoder().createRenderPass(()->"Sanctuary clear emission",emission.getColorTextureView(),Optional.of(new Vector4f(0,0,0,0)))){ }
|
||||
|
||||
// Reduce the celestial-only mask before drawing any torch, ore or other terrain emitter.
|
||||
if(flare)screen(FLARE_SOURCE,flareSource.getColorTextureView(),emission.getColorTextureView(),true,null);
|
||||
if(atlas!=null&&!entries.isEmpty())try(var pass=RenderSystem.getDevice().createCommandEncoder().createRenderPass(()->"Sanctuary visible emitters",emission.getColorTextureView(),Optional.empty())){
|
||||
pass.setPipeline(RenderSystem.getCompiledPipeline(EMITTERS));pass.setUniform("BloomScene",scene.currentBuffer());pass.setUniform("WorldDepth",main.getDepthTextureView(),nearest);
|
||||
pass.setUniform("BlockAtlas",mc.getTextureManager().getTexture(TextureAtlas.LOCATION_BLOCKS).getTextureView(),nearest);pass.setUniform("EmissionMasks",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);}
|
||||
}
|
||||
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);
|
||||
}
|
||||
if(cores)screen(CORE,main.getColorTextureView(),emission.getColorTextureView(),false,null);
|
||||
if(halo)screen(APPLY,main.getColorTextureView(),small.getColorTextureView(),true,null);
|
||||
if(flare)screen(FLARE,main.getColorTextureView(),flareSource.getColorTextureView(),true,null);
|
||||
applied=true;frames++;
|
||||
}
|
||||
private static Vector4f projectFlare(Matrix4f vp,Vector3f direction,float strength){
|
||||
var clip=vp.transform(new Vector4f(direction,0));
|
||||
if(clip.w<=.05f||direction.y<=0||strength<=0)return new Vector4f(0,0,0,0);
|
||||
float x=clip.x/clip.w*.5f+.5f,y=clip.y/clip.w*.5f+.5f;
|
||||
// Actual sprite light is measured on GPU. This is only its projected sampling region.
|
||||
float radius=Math.min(.3f,Math.abs(projection.m11())*.08f/clip.w);
|
||||
float horizon=Math.clamp(direction.y/.08f,0,1);
|
||||
return new Vector4f(x,y,radius,strength*horizon*horizon*(3-2*horizon));
|
||||
}
|
||||
private static void flareClose(){if(flareSource!=null){flareSource.destroyBuffers();flareSource=null;}}
|
||||
private static void screen(RenderPipeline pipeline,GpuTextureView output,GpuTextureView input,boolean useScene,MappableRingBuffer blur){
|
||||
try(var pass=RenderSystem.getDevice().createCommandEncoder().createRenderPass(()->"Sanctuary bloom",output,Optional.empty())){
|
||||
pass.setPipeline(RenderSystem.getCompiledPipeline(pipeline));pass.setUniform("InputTexture",input,RenderSystem.getSamplerCache().getClampToEdge(pipeline==CORE||pipeline==SKY?FilterMode.NEAREST:FilterMode.LINEAR));
|
||||
if(useScene){pass.setUniform("BloomScene",scene.currentBuffer());pass.setUniform("WorldDepth",Minecraft.getInstance().gameRenderer.mainRenderTarget().getDepthTextureView(),RenderSystem.getSamplerCache().getClampToEdge(FilterMode.NEAREST));}
|
||||
if(blur!=null)pass.setUniform("BlurParams",blur.currentBuffer());pass.draw(3,1,0,0);
|
||||
}
|
||||
}
|
||||
private static TextureTarget target(String name,int w,int h){return new TextureTarget("Sanctuary "+name,w,h,name.equals("emission")?GpuFormat.RGBA8_UNORM:GpuFormat.RGBA16_FLOAT,null);}
|
||||
private static MappableRingBuffer uniforms(String name,int size){return new MappableRingBuffer(()->"Sanctuary "+name,GpuBuffer.USAGE_UNIFORM|GpuBuffer.USAGE_MAP_WRITE,size);}
|
||||
private static void haloClose(){
|
||||
if(small!=null){small.destroyBuffers();ping.destroyBuffers();small=ping=null;}
|
||||
if(horizontal!=null){horizontal.close();vertical.close();horizontal=vertical=null;}
|
||||
}
|
||||
private static void targetsClose(){haloClose();flareClose();if(emission!=null){emission.destroyBuffers();emission=null;}}
|
||||
public static void release(){TERRAIN.close();targetsClose();if(scene!=null){scene.close();scene=null;}applied=false;}
|
||||
static TextureTarget flareTarget(){return flareSource;}
|
||||
static boolean flareAllocated(){return flareSource!=null;}
|
||||
static boolean haloAllocated(){return small!=null||horizontal!=null;}
|
||||
static boolean applied(){return applied;}
|
||||
static boolean allocated(){return emission!=null;}
|
||||
static boolean ready(){return TERRAIN.settled();}
|
||||
static int renderedFrames(){return frames;}
|
||||
static TextureTarget emissionTarget(){return emission;}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import com.mojang.blaze3d.platform.NativeImage;
|
||||
import java.util.*;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.texture.DynamicTexture;
|
||||
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
/** Analyse source texels once per resource reload, never rendered frames or GPU pixels. */
|
||||
final class BloomMaterials implements AutoCloseable {
|
||||
static final int TILE=68, INNER=64, SIZE=TILE*16, MAX_SPRITES=256;
|
||||
record Slot(int x,int y,boolean empty) {
|
||||
float u(float local){return (x+2+Math.clamp(local,0,1)*INNER)/SIZE;}
|
||||
float v(float local){return (y+2+Math.clamp(local,0,1)*INNER)/SIZE;}
|
||||
}
|
||||
private final Map<String,Slot> slots=new HashMap<>();
|
||||
private final Map<BlockState,Boolean> ores=new IdentityHashMap<>();
|
||||
private final Map<Identifier,int[]> hosts=new HashMap<>();
|
||||
private DynamicTexture atlas;
|
||||
private boolean dirty;
|
||||
private static final Set<String> ORES=Set.of("iron","copper","redstone","lapis","diamond","emerald","ruby","sapphire","quartz","gold");
|
||||
|
||||
boolean ore(BlockState state){return ores.computeIfAbsent(state,s->{
|
||||
var id=BuiltInRegistries.BLOCK.getKey(s.getBlock());
|
||||
if(!id.getNamespace().equals("minecraft")&&!id.getNamespace().equals("sanctuary"))return false;
|
||||
String name=id.getPath().replaceFirst("^(deepslate_|nether_)","");
|
||||
return name.endsWith("_ore")&&ORES.contains(name.substring(0,name.length()-4));
|
||||
});}
|
||||
boolean candidate(BlockState state){return ore(state)||state.getLightEmission()>0||state.emissiveRendering();}
|
||||
|
||||
Slot slot(TextureAtlasSprite sprite,boolean ore,boolean fullEmission){
|
||||
String key=sprite.contents().name()+":"+ore+":"+fullEmission;
|
||||
var cached=slots.get(key);if(cached!=null)return cached;
|
||||
if(slots.size()>=MAX_SPRITES)return new Slot(0,0,true);
|
||||
int index=slots.size(),x=(index%16)*TILE,y=(index/16)*TILE;
|
||||
if(atlas==null){atlas=new DynamicTexture("Sanctuary emissive masks",SIZE,SIZE,false);atlas.getPixels().fillRect(0,0,SIZE,SIZE,0);}
|
||||
boolean any=false;
|
||||
var id=sprite.contents().name();
|
||||
try(var image=read(id)){
|
||||
int frameWidth=Math.min(image.getWidth(),sprite.contents().width()),frameHeight=Math.min(image.getHeight(),sprite.contents().height());
|
||||
int w=Math.min(INNER,frameWidth),h=Math.min(INNER,frameHeight);
|
||||
int[] colors=new int[w*h];for(int iy=0;iy<h;iy++)for(int ix=0;ix<w;ix++)colors[iy*w+ix]=image.getPixel(ix*frameWidth/w,iy*frameHeight/h);
|
||||
int[] mask=mask(id,colors,w,h,ore,fullEmission);
|
||||
// Standard optional _e texture takes precedence, including in personal resource packs.
|
||||
var override=Minecraft.getInstance().getResourceManager().getResource(texture(id.withSuffix("_e")));
|
||||
if(override.isPresent())try(var stream=override.get().open();var explicit=NativeImage.read(stream)){
|
||||
for(int iy=0;iy<h;iy++)for(int ix=0;ix<w;ix++){
|
||||
int c=explicit.getPixel(ix*explicit.getWidth()/w,iy*Math.min(explicit.getHeight(),frameHeight)/h);
|
||||
mask[iy*w+ix]=(c>>>24)*Math.max((c>>16)&255,Math.max((c>>8)&255,c&255))/255;
|
||||
}
|
||||
}
|
||||
for(int iy=0;iy<TILE;iy++)for(int ix=0;ix<TILE;ix++){
|
||||
int value=mask[Math.clamp((iy-2)*h/INNER,0,h-1)*w+Math.clamp((ix-2)*w/INNER,0,w-1)];
|
||||
any|=value>0;atlas.getPixels().setPixel(x+ix,y+iy,0xFF000000|value*0x010101);
|
||||
}
|
||||
}catch(java.io.IOException exception){
|
||||
fr.koka.sanctuary.SanctuaryMod.LOGGER.warn("Cannot analyse emissive texture {}",id,exception);
|
||||
}
|
||||
var result=new Slot(x,y,!any);slots.put(key,result);dirty=true;return result;
|
||||
}
|
||||
|
||||
int[] mask(Identifier id,int[] colors,int width,int height,boolean ore,boolean fullEmission){
|
||||
int[] result=new int[colors.length];String name=id.getPath();
|
||||
boolean quartz=name.contains("quartz"),nether=name.contains("nether_gold")||quartz;
|
||||
int[] host=ore?host(nether?"netherrack":name.contains("deepslate")?"deepslate":"stone"):new int[0];
|
||||
boolean structural=!ore&&!fullEmission&&(name.contains("furnace_side")||name.contains("furnace_top")
|
||||
||name.contains("smoker_side")||name.contains("smoker_top")||name.contains("smoker_bottom")
|
||||
||name.contains("campfire_log")&&!name.contains("lit")||name.contains("respawn_anchor_side")
|
||||
||name.contains("respawn_anchor_bottom"));
|
||||
for(int i=0;i<colors.length;i++){
|
||||
int c=colors[i],r=c>>16&255,g=c>>8&255,b=c&255,max=Math.max(r,Math.max(g,b)),min=Math.min(r,Math.min(g,b));
|
||||
if((c>>>24)<128||structural)continue;
|
||||
if(ore){
|
||||
int difference=255;
|
||||
for(int base:host){int br=base>>16&255,bg=base>>8&255,bb=base&255;
|
||||
difference=Math.min(difference,Math.max(Math.abs(r-br),Math.max(Math.abs(g-bg),Math.abs(b-bb))));}
|
||||
boolean inclusion=nether?(quartz?g>r*.65&&b>r*.5&&max>110:r>110&&g>75&&g>b*1.45):max-min>=10;
|
||||
if(inclusion&&difference>=10)result[i]=255;
|
||||
}else{
|
||||
// Luminosity is tied to the block state; cold/off variants never enter this path.
|
||||
// Dark casings/wood are suppressed; live atlas colours and animation stay native.
|
||||
float value=fullEmission?1:Math.clamp((max-100)/110f,0,1);
|
||||
if(name.contains("torch")||name.contains("furnace")||name.contains("smoker")||name.contains("campfire"))
|
||||
value*=Math.clamp((max-min-35)/65f,0,1);
|
||||
result[i]=Math.round(value*255);
|
||||
}
|
||||
}
|
||||
if(ore&&!nether){
|
||||
// Retain pale/white highlights touching coloured inclusions, without growing into grey rock.
|
||||
int[] seed=result.clone();
|
||||
for(int y=0;y<height;y++)for(int x=0;x<width;x++)if(seed[y*width+x]==0){
|
||||
int c=colors[y*width+x],r=c>>16&255,g=c>>8&255,b=c&255;
|
||||
if((c>>>24)<128||Math.min(r,Math.min(g,b))<215)continue;
|
||||
for(int dy=-1;dy<=1;dy++)for(int dx=-1;dx<=1;dx++)if(x+dx>=0&&x+dx<width&&y+dy>=0&&y+dy<height&&seed[(y+dy)*width+x+dx]>0)result[y*width+x]=255;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
private int[] host(String name){return hosts.computeIfAbsent(Identifier.withDefaultNamespace("block/"+name),id->{
|
||||
try(var image=read(id)){var values=new LinkedHashSet<Integer>();for(int y=0;y<Math.min(64,image.getHeight());y++)for(int x=0;x<Math.min(64,image.getWidth());x++)values.add(image.getPixel(x*image.getWidth()/Math.min(64,image.getWidth()),y*image.getHeight()/Math.min(64,image.getHeight())));return values.stream().mapToInt(Integer::intValue).toArray();}
|
||||
catch(java.io.IOException exception){return new int[0];}
|
||||
});}
|
||||
static Identifier texture(Identifier id){return Identifier.fromNamespaceAndPath(id.getNamespace(),"textures/"+id.getPath()+".png");}
|
||||
static NativeImage read(Identifier id)throws java.io.IOException{try(var stream=Minecraft.getInstance().getResourceManager().getResourceOrThrow(texture(id)).open()){return NativeImage.read(stream);}}
|
||||
DynamicTexture atlas(){if(dirty){atlas.upload();dirty=false;}return atlas;}
|
||||
int count(){return slots.size();}
|
||||
public void close(){if(atlas!=null){atlas.close();atlas=null;}slots.clear();ores.clear();hosts.clear();dirty=false;}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.blaze3d.vertex.BufferBuilder;
|
||||
import com.mojang.blaze3d.vertex.ByteBufferBuilder;
|
||||
import com.mojang.renderpearl.api.GpuFormat;
|
||||
import com.mojang.renderpearl.api.buffers.GpuBuffer;
|
||||
import com.mojang.renderpearl.api.pipeline.PrimitiveTopology;
|
||||
import com.mojang.renderpearl.api.vertex.VertexFormat;
|
||||
import java.util.*;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.model.geom.builders.UVPair;
|
||||
import net.minecraft.client.renderer.block.dispatch.BlockStateModelPart;
|
||||
import net.minecraft.client.renderer.texture.TextureAtlas;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.SectionPos;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.chunk.LevelChunkSection;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Native visible sections only, palette rejection and bounded incremental emitter meshing. */
|
||||
final class BloomTerrain 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=32L*1024*1024;
|
||||
private final Map<Long,Section> sections=new HashMap<>();
|
||||
final BloomMaterials materials=new BloomMaterials();
|
||||
private Build building;
|
||||
private long bytes;
|
||||
private Object models,level;
|
||||
private List<Section> visible=List.of();
|
||||
record Entry(BlockPos origin,GpuBuffer vertices,int indices){}
|
||||
List<Entry> update(){
|
||||
var mc=Minecraft.getInstance();var current=mc.getModelManager().getBlockStateModelSet();
|
||||
if(models!=current||level!=mc.level){close();models=current;level=mc.level;}
|
||||
long frame=mc.level.getGameTime();var active=new ArrayList<Section>();
|
||||
for(var render:mc.levelRenderer.visibleSections()){
|
||||
long key=render.getSectionNode();var pos=render.getRenderOrigin();
|
||||
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);}
|
||||
section.seen=frame;active.add(section);
|
||||
}
|
||||
visible=active;
|
||||
for(var iterator=sections.values().iterator();iterator.hasNext();){var section=iterator.next();if(frame-section.seen>40){remove(section);iterator.remove();}}
|
||||
Vec3 camera=mc.gameRenderer.gameRenderState().levelRenderState.cameraRenderState.pos;
|
||||
long deadline=System.nanoTime()+3_000_000L;int started=building==null?0:1;
|
||||
while(System.nanoTime()<deadline){
|
||||
if(building==null){
|
||||
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;}
|
||||
building=new Build(next);
|
||||
}
|
||||
if(!advance(building,deadline))break;
|
||||
var done=building;building=null;
|
||||
try(done){
|
||||
done.section.dirty=false;
|
||||
if(done.quads>0&&!done.overflow){
|
||||
try(var mesh=done.out.buildOrThrow()){
|
||||
long size=mesh.vertexBuffer().remaining();
|
||||
if(bytes+size<=MAX_BYTES){
|
||||
var buffer=RenderSystem.getDevice().createBuffer(()->"Sanctuary bloom emitters",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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return active.stream().map(s->s.entry).filter(Objects::nonNull).toList();
|
||||
}
|
||||
private boolean advance(Build build,long deadline){
|
||||
var mc=Minecraft.getInstance();var models=mc.getModelManager().getBlockStateModelSet();
|
||||
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)){
|
||||
var pos=build.section.origin.offset(x,y,z);boolean ore=materials.ore(state);
|
||||
parts.clear();random.setSeed(state.getSeed(pos));if(!state.liquid())models.get(state).collectParts(random,parts);
|
||||
var tintSources=mc.getBlockColors().getTintSources(state);
|
||||
var offset=state.getOffset(pos);
|
||||
for(var part:parts)for(int f=0;f<7;f++){
|
||||
Direction face=f==6?null:Direction.values()[f];
|
||||
if(face!=null&&mc.level.getBlockState(pos.relative(face)).isSolidRender())continue;
|
||||
for(var quad:part.getQuads(face)){
|
||||
var material=quad.materialInfo();var sprite=material.sprite();
|
||||
if(!sprite.atlasLocation().equals(TextureAtlas.LOCATION_BLOCKS))continue;
|
||||
var slot=materials.slot(sprite,ore,material.lightEmission()>0||state.emissiveRendering()||sprite.isAnimated());
|
||||
if(slot.empty())continue;
|
||||
if(build.quads>=16384){build.overflow=true;return true;}
|
||||
int tint=0xFFFFFF,index=material.tintIndex();
|
||||
if(index>=0&&index<tintSources.size())tint=tintSources.get(index).colorInWorld(state,mc.level,pos)&0xFFFFFF;
|
||||
// Alpha marks ore vs. native emitter; the mask alpha is not a gameplay light value.
|
||||
int light=state.emissiveRendering()?15:Math.max(state.getLightEmission(),material.lightEmission());
|
||||
int alpha=ore?Math.round(light/15f*120):128+Math.round(light/15f*127);
|
||||
int color=(alpha<<24)|tint;
|
||||
for(int v=0;v<4;v++){
|
||||
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));
|
||||
}
|
||||
build.quads++;
|
||||
}
|
||||
}
|
||||
if(state.liquid()&&state.getLightEmission()>0)fluid(build,pos,x,y,z,state.getFluidState().getHeight(mc.level,pos));
|
||||
}
|
||||
if((build.cursor&31)==0&&System.nanoTime()>=deadline)return build.cursor==4096;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
private void fluid(Build build,BlockPos pos,int x,int y,int z,float height){
|
||||
var mc=Minecraft.getInstance();var sprite=((TextureAtlas)mc.getTextureManager().getTexture(TextureAtlas.LOCATION_BLOCKS)).getSprite(net.minecraft.resources.Identifier.withDefaultNamespace("block/lava_still"));
|
||||
var slot=materials.slot(sprite,false,true);if(slot.empty())return;
|
||||
// Native world depth retains the actual fluid silhouette and occludes these candidate faces.
|
||||
float[][][] faces={{{0,height,0},{0,height,1},{1,height,1},{1,height,0}},{{0,0,0},{0,height,0},{1,height,0},{1,0,0}},
|
||||
{{0,0,1},{1,0,1},{1,height,1},{0,height,1}},{{0,0,0},{0,0,1},{0,height,1},{0,height,0}},{{1,0,0},{1,height,0},{1,height,1},{1,0,1}}};
|
||||
for(var face:faces){if(build.quads>=16384){build.overflow=true;return;}
|
||||
for(int i=0;i<4;i++){float u=i>=2?1:0,v=i==1||i==2?1:0;var p=face[i];
|
||||
build.out.addVertex(x+p[0],y+p[1],z+p[2]).setColor(0xFFFFFFFF).setUv(sprite.getU(u),sprite.getV(v)).setUv3(slot.u(u),slot.v(v));}
|
||||
build.quads++;
|
||||
}
|
||||
}
|
||||
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;}}
|
||||
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;
|
||||
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);
|
||||
int cursor,quads;boolean overflow;
|
||||
Build(Section section){this.section=section;}
|
||||
public void close(){buffer.close();}
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import fr.koka.sanctuary.client.DynamicLightClient;
|
||||
import fr.koka.sanctuary.lighting.DynamicLightField;
|
||||
import java.util.*;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Same bounded sources as native dynamic lighting, with visual interpolation only. */
|
||||
final class ColoredDynamicLights {
|
||||
static final int LIMIT=128;
|
||||
static final class Light {
|
||||
double x,y,z,r,g,b,visibility;int level;boolean seen;
|
||||
Light(DynamicLightField.Source s){x=s.x();y=s.y();z=s.z();level=s.light();r=(s.color()>>16&255)/255.;g=(s.color()>>8&255)/255.;b=(s.color()&255)/255.;}
|
||||
}
|
||||
private final Map<DynamicLightField.Id,Light> lights=new HashMap<>();
|
||||
private long last;
|
||||
List<Light> update(Vec3 camera){
|
||||
long now=System.nanoTime();double dt=last==0?0:Math.min(.1,(now-last)/1e9);last=now;
|
||||
double ease=1-Math.exp(-dt*14),fade=dt/.3;
|
||||
lights.values().forEach(l->l.seen=false);
|
||||
var mc=Minecraft.getInstance();
|
||||
for(var s:DynamicLightClient.snapshot(mc.level).sources()){
|
||||
Light l=lights.computeIfAbsent(s.id(),id->new Light(s));l.seen=true;l.level=s.light();
|
||||
boolean own=!s.id().drop()&&mc.player!=null&&s.id().value()==mc.player.getId()
|
||||
&&mc.getCameraEntity()==mc.player&&mc.options.getCameraType().isFirstPerson();
|
||||
double x=own?camera.x:s.x(),y=own?camera.y:s.y(),z=own?camera.z:s.z();
|
||||
double step=own?1:ease;l.x+=(x-l.x)*step;l.y+=(y-l.y)*step;l.z+=(z-l.z)*step;
|
||||
l.r+=((s.color()>>16&255)/255.-l.r)*ease;l.g+=((s.color()>>8&255)/255.-l.g)*ease;l.b+=((s.color()&255)/255.-l.b)*ease;
|
||||
}
|
||||
for(var it=lights.values().iterator();it.hasNext();){var l=it.next();l.visibility=Math.clamp(l.visibility+(l.seen?fade:-fade),0,1);if(!l.seen&&l.visibility<=0)it.remove();}
|
||||
return lights.values().stream().filter(l->camera.distanceToSqr(l.x,l.y,l.z)<Math.pow(VanillaLight.coloredLightDistance()+l.level,2))
|
||||
.sorted(Comparator.comparingDouble(l->camera.distanceToSqr(l.x,l.y,l.z))).limit(LIMIT).toList();
|
||||
}
|
||||
Vec3 position(long entity){var l=lights.get(new DynamicLightField.Id(false,entity));return l==null?null:new Vec3(l.x,l.y,l.z);}
|
||||
void clear(){lights.clear();last=0;}
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import java.util.*;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.client.renderer.block.dispatch.BlockStateModelPart;
|
||||
|
||||
/** Colours are analysed once per state/resource reload, never from rendered GPU pixels. */
|
||||
public final class ColoredLightMaterials {
|
||||
private final Map<BlockState,Integer> colors=new IdentityHashMap<>();
|
||||
private final Map<Identifier,Integer> sprites=new HashMap<>();
|
||||
private boolean warm;
|
||||
public int color(BlockState state){if(warm!=VanillaLight.warmTorches()){warm=VanillaLight.warmTorches();clear();}return colors.computeIfAbsent(state,this::analyse);}
|
||||
public int color(net.minecraft.world.item.ItemStack stack){
|
||||
var state=fr.koka.sanctuary.cosmetics.HeadCosmeticData.state(stack);
|
||||
if(state!=null)return color(state);
|
||||
var item=stack.getItem();
|
||||
if(item==net.minecraft.world.item.Items.LAVA_BUCKET)return 0xFF6020;
|
||||
if(item==net.minecraft.world.item.Items.BLAZE_ROD||item==net.minecraft.world.item.Items.BLAZE_POWDER)return 0xFFAB55;
|
||||
if(item==net.minecraft.world.item.Items.GLOWSTONE_DUST)return color(net.minecraft.world.level.block.Blocks.GLOWSTONE.defaultBlockState());
|
||||
if(item==net.minecraft.world.item.Items.GLOW_BERRIES)return 0xFFBD52;
|
||||
if(item==net.minecraft.world.item.Items.GLOW_INK_SAC||item==net.minecraft.world.item.Items.GLOW_ITEM_FRAME)return 0x66EADD;
|
||||
return 0xFFFFFF;
|
||||
}
|
||||
private int analyse(BlockState state){
|
||||
var id=BuiltInRegistries.BLOCK.getKey(state.getBlock());String name=id.getPath();
|
||||
if(id.getNamespace().equals("minecraft")){
|
||||
if(name.contains("copper"))return 0x62FFA0;
|
||||
if(name.startsWith("soul_"))return 0x55BDFF;
|
||||
if(name.contains("redstone"))return 0xFF0602;
|
||||
if(name.equals("torch")||name.equals("wall_torch"))return VanillaLight.warmTorches()?0xFFAB55:0xFFFFFF;
|
||||
if(name.equals("lava"))return 0xFF6020;
|
||||
if(name.equals("pearlescent_froglight"))return 0xFFC0EC;
|
||||
if(name.equals("verdant_froglight"))return 0xB4FF82;
|
||||
if(name.equals("ochre_froglight"))return 0xFFCD72;
|
||||
if(name.contains("torch")||name.equals("lantern")||name.equals("fire")||name.equals("campfire")||name.equals("lava"))return 0xFFAB55;
|
||||
}
|
||||
var parts=new ArrayList<BlockStateModelPart>();
|
||||
Minecraft.getInstance().getModelManager().getBlockStateModelSet().get(state).collectParts(RandomSource.create(0),parts);
|
||||
var used=new HashSet<Identifier>();long r=0,g=0,b=0;int count=0;
|
||||
for(var part:parts)for(int f=0;f<7;f++)for(var quad:part.getQuads(f==6?null:Direction.values()[f])){
|
||||
var sprite=quad.materialInfo().sprite().contents().name();if(!used.add(sprite))continue;
|
||||
int color=sprites.computeIfAbsent(sprite,this::sample);if(color<0)continue;
|
||||
r+=color>>16&255;g+=color>>8&255;b+=color&255;count++;
|
||||
}
|
||||
if(count==0)return 0xFFFFFF;
|
||||
return normalize((float)r/count,(float)g/count,(float)b/count);
|
||||
}
|
||||
private int sample(Identifier id){
|
||||
try(var image=BloomMaterials.read(id)){
|
||||
double r=0,g=0,b=0,weight=0;
|
||||
// First animation frame; bounded sampling also covers high-resolution packs.
|
||||
int w=image.getWidth(),h=Math.min(w,image.getHeight()),step=Math.max(1,Math.max(w,h)/32);
|
||||
for(int y=0;y<h;y+=step)for(int x=0;x<w;x+=step){
|
||||
int c=image.getPixel(x,y);if((c>>>24)<128)continue;
|
||||
int cr=c>>16&255,cg=c>>8&255,cb=c&255,max=Math.max(cr,Math.max(cg,cb)),min=Math.min(cr,Math.min(cg,cb));
|
||||
double k=Math.max(0,(max-96)/159.0);k*=k*(.2+(max-min)/255.0);
|
||||
r+=cr*k;g+=cg*k;b+=cb*k;weight+=k;
|
||||
}
|
||||
return weight<.001?-1:normalize((float)(r/weight),(float)(g/weight),(float)(b/weight));
|
||||
}catch(java.io.IOException e){return -1;}
|
||||
}
|
||||
private static int normalize(float r,float g,float b){
|
||||
float max=Math.max(1,Math.max(r,Math.max(g,b)));
|
||||
return Math.round(r/max*255)<<16|Math.round(g/max*255)<<8|Math.round(b/max*255);
|
||||
}
|
||||
public void clear(){colors.clear();sprites.clear();}
|
||||
int cachedStates(){return colors.size();}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import com.mojang.blaze3d.platform.NativeImage;
|
||||
import it.unimi.dsi.fastutil.ints.IntArrayFIFOQueue;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import net.minecraft.client.renderer.texture.DynamicTexture;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.world.level.LightLayer;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
|
||||
/** Client-only RGB propagation. Snapshot, flood and packing share a bounded per-frame work budget. */
|
||||
final class ColoredLightVolume implements AutoCloseable {
|
||||
static int sideFor(int distance){return (distance+20)*2;}
|
||||
static int columns(int side){return (int)Math.ceil(Math.sqrt(side));}
|
||||
private static final Direction[] DIRECTIONS=Direction.values();
|
||||
private final ColoredLightMaterials materials=new ColoredLightMaterials();
|
||||
private ClientLevel level;
|
||||
private Object models;
|
||||
private Build build;
|
||||
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;
|
||||
float blend(){return Math.clamp((System.nanoTime()-transitionStarted)/300_000_000f,0,1);}
|
||||
DynamicTexture previous(){return previous;}
|
||||
BlockPos previousOrigin(){return previousOrigin;}
|
||||
int previousSide(){return previousSide;}
|
||||
int side(){return side;}
|
||||
private BlockPos origin;
|
||||
private long revision,published=-1,lastStart;
|
||||
private int completed;
|
||||
private boolean warm;
|
||||
void update(){
|
||||
var mc=Minecraft.getInstance();var modelSet=mc.getModelManager().getBlockStateModelSet();
|
||||
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(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.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;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++;
|
||||
}
|
||||
}
|
||||
private static int anchor(double p,int side){return Math.floorDiv((int)Math.floor(p),4)*4-side/2;}
|
||||
private static boolean inside(BlockPos base,BlockPos p,int side){return p.getX()>=base.getX()&&p.getX()<base.getX()+side&&p.getY()>=base.getY()&&p.getY()<base.getY()+side&&p.getZ()>=base.getZ()&&p.getZ()<base.getZ()+side;}
|
||||
void dirty(BlockPos p){if(origin!=null&&inside(origin,p,side)||build!=null&&inside(build.origin,p,build.side))revision++;}
|
||||
void dirtyChunk(int x,int z){if(overlaps(origin,x,z,side)||build!=null&&overlaps(build.origin,x,z,build.side))revision++;}
|
||||
void dirtySection(int x,int y,int z){if(overlapsSection(origin,x,y,z,side)||build!=null&&overlapsSection(build.origin,x,y,z,build.side))revision++;}
|
||||
private static boolean overlapsSection(BlockPos o,int x,int y,int z,int side){return overlaps(o,x,z,side)&&y>=(o.getY()>>4)&&y<=((o.getY()+side-1)>>4);}
|
||||
private static boolean overlaps(BlockPos o,int x,int z,int side){return o!=null&&x>=(o.getX()>>4)&&x<=((o.getX()+side-1)>>4)&&z>=(o.getZ()>>4)&&z<=((o.getZ()+side-1)>>4);}
|
||||
DynamicTexture texture(){return texture;}
|
||||
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;}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;
|
||||
final ClientLevel level;final BlockPos origin;final long revision;final ColoredLightMaterials materials;
|
||||
final BlockState[] states;
|
||||
final byte[] red,green,blue,sky;
|
||||
final boolean[] queued;final IntArrayFIFOQueue queue=new IntArrayFIFOQueue();
|
||||
final LevelChunk[] chunks;final int cx,cz;
|
||||
final BlockPos.MutableBlockPos pos=new BlockPos.MutableBlockPos();
|
||||
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);
|
||||
}
|
||||
boolean advance(long deadline){
|
||||
while(scan<count){scan(scan++);if((scan&127)==0&&System.nanoTime()>=deadline)return false;}
|
||||
int work=0;
|
||||
while(!queue.isEmpty()){
|
||||
int i=queue.dequeueInt();queued[i]=false;spread(i);
|
||||
if((++work&127)==0&&System.nanoTime()>=deadline)return false;
|
||||
}
|
||||
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;
|
||||
}
|
||||
void scan(int i){
|
||||
int x=origin.getX()+(i%side),z=origin.getZ()+(i/side%side),y=origin.getY()+(i/(side*side));pos.set(x,y,z);
|
||||
var chunk=chunks[((x>>4)-cx)+((z>>4)-cz)*chunkSide];
|
||||
var state=chunk==null||level.isOutsideBuildHeight(y)?Blocks.BEDROCK.defaultBlockState():chunk.getBlockState(pos);states[i]=state;
|
||||
if(chunk!=null&&!level.isOutsideBuildHeight(y))sky[i]=(byte)(level.getBrightness(LightLayer.SKY,pos)*17);
|
||||
int emission=state.getLightEmission();if(emission<=0)return;
|
||||
int c=materials.color(state);red[i]=(byte)encode(emission,c>>16&255);green[i]=(byte)encode(emission,c>>8&255);blue[i]=(byte)encode(emission,c&255);enqueue(i);
|
||||
}
|
||||
void spread(int i){
|
||||
int r=red[i]&255,g=green[i]&255,b=blue[i]&255;if(Math.max(r,Math.max(g,b))<=136)return;
|
||||
int x=i%side,z=i/side%side,y=i/(side*side);
|
||||
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;
|
||||
if(r-8<=(red[n]&255)&&g-8<=(green[n]&255)&&b-8<=(blue[n]&255))continue;
|
||||
int loss=net.minecraft.world.level.lighting.LightEngine.getLightDampeningInto(states[i],states[n],d,Math.max(1,states[n].getLightDampening()))*8;
|
||||
int nr=Math.max(0,r-loss),ng=Math.max(0,g-loss),nb=Math.max(0,b-loss);if(Math.max(nr,Math.max(ng,nb))<=128)continue;boolean changed=false;
|
||||
if(nr>(red[n]&255)){red[n]=(byte)nr;changed=true;}if(ng>(green[n]&255)){green[n]=(byte)ng;changed=true;}if(nb>(blue[n]&255)){blue[n]=(byte)nb;changed=true;}
|
||||
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;}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.
|
||||
static int encode(int emission,int channel){return channel==0?0:Math.clamp((int)Math.round((emission+16+Math.log(channel/255.0)/Math.log(1.25))*8),0,248);}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import com.mojang.blaze3d.buffers.Std140Builder;
|
||||
import com.mojang.blaze3d.pipeline.TextureTarget;
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.renderpearl.api.GpuFormat;
|
||||
import com.mojang.renderpearl.api.buffers.GpuBuffer;
|
||||
import com.mojang.renderpearl.api.pipeline.*;
|
||||
import com.mojang.renderpearl.api.textures.FilterMode;
|
||||
import fr.koka.sanctuary.client.panorama.PanoramaCapture;
|
||||
import java.util.Optional;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.MappableRingBuffer;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.level.material.FogType;
|
||||
import org.joml.Matrix4f;
|
||||
|
||||
/** Visual local colour only: native block/sky light and gameplay remain unchanged. */
|
||||
public final class ColoredLights {
|
||||
private static final RenderPipeline APPLY=RenderPipelines.register(RenderPipeline.builder()
|
||||
.withLocation(Identifier.parse("sanctuary:pipeline/colored_lights"))
|
||||
.withVertexShader("core/screenquad").withFragmentShader(Identifier.parse("sanctuary:core/colored_lights"))
|
||||
.withBindGroupLayout(BindGroupLayout.builder().withUniform("ColoredLightScene",UniformType.UNIFORM_BUFFER)
|
||||
.withUniform("WorldDepth",UniformType.COMBINED_IMAGE_SAMPLER)
|
||||
.withUniform("SceneColor",UniformType.COMBINED_IMAGE_SAMPLER)
|
||||
.withUniform("LightVolume",UniformType.COMBINED_IMAGE_SAMPLER)
|
||||
.withUniform("PreviousVolume",UniformType.COMBINED_IMAGE_SAMPLER).build())
|
||||
.withPrimitiveTopology(PrimitiveTopology.TRIANGLES).withCull(false)
|
||||
.withColorTargetState(new ColorTargetState(Optional.of(new BlendFunction(BlendFactor.ONE,BlendFactor.ZERO,BlendFactor.ZERO,BlendFactor.ONE)),GpuFormat.RGBA8_UNORM,ColorTargetState.WRITE_COLOR))
|
||||
.withDepthStencilState(Optional.empty()).build());
|
||||
private static final ColoredLightVolume VOLUME=new ColoredLightVolume();
|
||||
private static final Matrix4f projection=new Matrix4f();
|
||||
private static TextureTarget source;
|
||||
private static net.minecraft.client.renderer.texture.DynamicTexture black;
|
||||
private static final ColoredDynamicLights DYNAMIC=new ColoredDynamicLights();
|
||||
private static long activated,lastRender;
|
||||
private static float displayedDistance;
|
||||
private static int dynamicCount;
|
||||
private static MappableRingBuffer uniforms;
|
||||
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 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;
|
||||
if(camera.fogType!=FogType.NONE)return;
|
||||
long now=System.nanoTime();if(activated==0){activated=now;displayedDistance=VanillaLight.coloredLightDistance();}
|
||||
double elapsed=lastRender==0?0:Math.min(.1,(now-lastRender)/1e9);lastRender=now;
|
||||
displayedDistance+=(VanillaLight.coloredLightDistance()-displayedDistance)*(float)(1-Math.exp(-elapsed*10));
|
||||
VOLUME.update();
|
||||
if(black==null){var image=new com.mojang.blaze3d.platform.NativeImage(1,1,false);image.setPixel(0,0,0);black=new net.minecraft.client.renderer.texture.DynamicTexture(()->"Empty coloured light",image);}
|
||||
var lights=DYNAMIC.update(camera.pos);dynamicCount=lights.size();
|
||||
var current=VOLUME.texture()==null?black:VOLUME.texture();
|
||||
var previous=VOLUME.previous()==null?black:VOLUME.previous();
|
||||
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 coloured light source",main.width,main.height,GpuFormat.RGBA8_UNORM,null);}
|
||||
if(uniforms==null)uniforms=new MappableRingBuffer(()->"Sanctuary coloured light scene",GpuBuffer.USAGE_UNIFORM|GpuBuffer.USAGE_MAP_WRITE,176+ColoredDynamicLights.LIMIT*32);
|
||||
var base=VOLUME.origin()==null?BlockPos.containing(camera.pos):VOLUME.origin();var fog=camera.fogData;
|
||||
var old=VOLUME.previousOrigin()==null?base:VOLUME.previousOrigin();
|
||||
int side=VOLUME.texture()==null?1:VOLUME.side(),oldSide=VOLUME.previous()==null?1:VOLUME.previousSide();
|
||||
uniforms.rotate();try(var mapped=uniforms.currentBuffer().map(false,true)){
|
||||
var data=Std140Builder.intoBuffer(mapped.data()).putMat4f(new Matrix4f(projection).mul(camera.viewRotationMatrix).invert())
|
||||
.putVec4((float)(camera.pos.x-base.getX()),(float)(camera.pos.y-base.getY()),(float)(camera.pos.z-base.getZ()),VanillaLight.coloredLightIntensity()/100f)
|
||||
.putVec4(RenderSystem.getDevice().getDeviceInfo().isZZeroToOne()?1:0,Math.clamp((15-mc.level.getSkyDarken())/15f,0,1),fog.color.w,0)
|
||||
.putVec4(fog.renderDistanceStart,fog.renderDistanceEnd,fog.environmentalStart,fog.environmentalEnd)
|
||||
.putVec4(displayedDistance,VOLUME.texture()==null?0:VOLUME.blend(),Math.clamp((now-activated)/300_000_000f,0,1),lights.size())
|
||||
.putVec4(side,ColoredLightVolume.columns(side),current.getPixels().getWidth(),current.getPixels().getHeight())
|
||||
.putVec4((float)(camera.pos.x-old.getX()),(float)(camera.pos.y-old.getY()),(float)(camera.pos.z-old.getZ()),VOLUME.previous()==null?0:1)
|
||||
.putVec4(oldSide,ColoredLightVolume.columns(oldSide),previous.getPixels().getWidth(),previous.getPixels().getHeight());
|
||||
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);
|
||||
try(var pass=encoder.createRenderPass(()->"Sanctuary coloured light",main.getColorTextureView(),Optional.empty())){
|
||||
pass.setPipeline(RenderSystem.getCompiledPipeline(APPLY));pass.setUniform("ColoredLightScene",uniforms.currentBuffer());
|
||||
pass.setUniform("WorldDepth",main.getDepthTextureView(),nearest);pass.setUniform("SceneColor",source.getColorTextureView(),nearest);pass.setUniform("LightVolume",current.getTextureView(),nearest);pass.setUniform("PreviousVolume",previous.getTextureView(),nearest);pass.draw(3,1,0,0);
|
||||
}
|
||||
applied=true;frames++;
|
||||
}
|
||||
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();}
|
||||
static int renderedFrames(){return frames;}
|
||||
static int builds(){return VOLUME.completed();}
|
||||
static int sample(BlockPos pos){return VOLUME.sample(pos);}
|
||||
static net.minecraft.world.phys.Vec3 dynamicPosition(long entity){return DYNAMIC.position(entity);}
|
||||
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();}
|
||||
}
|
||||
@@ -68,7 +68,7 @@ public final class EdgeHighlights {
|
||||
Std140Builder.intoBuffer(mapped.data()).putMat4f(inverse).putMat4f(viewProjection)
|
||||
.putVec4((float)(camera.pos.x - Math.floor(camera.pos.x)),
|
||||
(float)(camera.pos.y - Math.floor(camera.pos.y)),
|
||||
(float)(camera.pos.z - Math.floor(camera.pos.z)), 16)
|
||||
(float)(camera.pos.z - Math.floor(camera.pos.z)), VanillaLight.shadowPixels())
|
||||
.putVec4(VanillaLight.edgeIntensity() * .004f,
|
||||
RenderSystem.getDevice().getDeviceInfo().isZZeroToOne() ? 1 : 0,
|
||||
fog.renderDistanceStart, fog.renderDistanceEnd)
|
||||
|
||||
@@ -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;}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import com.mojang.blaze3d.platform.NativeImage;
|
||||
import java.util.*;
|
||||
import net.minecraft.client.renderer.texture.DynamicTexture;
|
||||
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
/** One source-texture analysis per cached material. No frame readback and no generated files on disk. */
|
||||
final class PbrMaterials implements AutoCloseable {
|
||||
static final int INNER=16,TILE=20,GRID=64,SIZE=TILE*GRID,MAX_SPRITES=GRID*GRID;
|
||||
record Profile(float roughness,float metal){}
|
||||
record Slot(int x,int y,boolean empty){
|
||||
float u(float f){return (x+2+Math.clamp(f,0,1)*INNER)/SIZE;}
|
||||
float v(float f){return (y+2+Math.clamp(f,0,1)*INNER)/SIZE;}
|
||||
}
|
||||
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;
|
||||
static int family(BlockState state){
|
||||
String name=BuiltInRegistries.BLOCK.getKey(state.getBlock()).getPath();
|
||||
return state.getFluidState().is(net.minecraft.tags.FluidTags.WATER)&&state.liquid()?2:
|
||||
(name.equals("glass")||name.endsWith("_glass")||name.equals("glass_pane")||name.endsWith("_glass_pane"))?1:0;
|
||||
}
|
||||
boolean candidate(BlockState state){return !state.isAir();}
|
||||
Profile profile(BlockState state){return profiles.computeIfAbsent(state,s->profile(BuiltInRegistries.BLOCK.getKey(s.getBlock()).getPath()));}
|
||||
static Profile profile(String name){
|
||||
if(name.equals("water"))return new Profile(.12f,0);
|
||||
if(name.contains("glass"))return new Profile(.16f,0);
|
||||
if(name.equals("iron_block")||name.equals("gold_block")||name.equals("netherite_block")||name.contains("copper")&&!name.contains("ore")&&!name.contains("raw"))
|
||||
return new Profile(name.contains("oxidized")?.65f:.32f,name.contains("oxidized")?.35f:.85f);
|
||||
if(name.contains("polished")||name.contains("glazed")||name.equals("diamond_block")||name.equals("emerald_block")||name.equals("ruby_block")||name.equals("sapphire_block"))return new Profile(.42f,0);
|
||||
// Dry soil keeps its normal relief without a broad white specular coating.
|
||||
if(java.util.Set.of("dirt","coarse_dirt","rooted_dirt","grass_block","dirt_path","farmland","podzol","mycelium").contains(name))return new Profile(.98f,0);
|
||||
if(name.contains("wool")||name.contains("leaves"))return new Profile(.98f,0);
|
||||
// All wooden shapes share a matte response, including Nether stems/hyphae.
|
||||
String woodName=name.startsWith("stripped_")?name.substring(9):name;
|
||||
for(String wood:new String[]{"oak","spruce","birch","jungle","acacia","dark_oak","mangrove","cherry","pale_oak","bamboo","crimson","warped"})
|
||||
if(woodName.startsWith(wood+"_")&&Set.of("planks","log","wood","stem","hyphae","slab","stairs","fence","fence_gate","door","trapdoor","button","pressure_plate","sign","wall_sign","hanging_sign","wall_hanging_sign","block","mosaic","mosaic_slab","mosaic_stairs").contains(woodName.substring(wood.length()+1)))return new Profile(.98f,0);
|
||||
if(name.contains("planks")||name.contains("log")||name.contains("wood"))return new Profile(.98f,0);
|
||||
return new Profile(.82f,0);
|
||||
}
|
||||
Slot slot(TextureAtlasSprite sprite,BlockState state){
|
||||
var profile=profile(state);String key=sprite.contents().name()+":"+profile;
|
||||
var existing=slots.get(key);if(existing!=null)return existing;
|
||||
if(slots.size()>=MAX_SPRITES)return new Slot(0,0,true);
|
||||
int i=slots.size(),x=i%GRID*TILE,y=i/GRID*TILE;
|
||||
if(atlas==null){atlas=new DynamicTexture("Sanctuary procedural PBR",SIZE,SIZE,false);atlas.getPixels().fillRect(0,0,SIZE,SIZE,0x008080FF);}
|
||||
boolean empty=false;
|
||||
try(var image=BloomMaterials.read(sprite.contents().name())){
|
||||
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;
|
||||
}
|
||||
static int[] generate(int[] colors,int w,int h,Profile profile){
|
||||
int[] result=new int[colors.length];
|
||||
for(int y=0;y<h;y++)for(int x=0;x<w;x++){
|
||||
int center=colors[y*w+x];float base=luma(center);
|
||||
float left=height(colors[y*w+Math.floorMod(x-1,w)],base),right=height(colors[y*w+(x+1)%w],base);
|
||||
float up=height(colors[Math.floorMod(y-1,h)*w+x],base),down=height(colors[(y+1)%h*w+x],base);
|
||||
float nx=Math.clamp((left-right)*2,-.45f,.45f),ny=Math.clamp((up-down)*2,-.45f,.45f);
|
||||
if((center>>>24)<128){nx=ny=0;}
|
||||
int red=Math.round((nx*.5f+.5f)*255),green=Math.round((ny*.5f+.5f)*255);
|
||||
result[y*w+x]=(Math.round(profile.metal*255)<<24)|(red<<16)|(green<<8)|Math.round(profile.roughness*255);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
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;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;}for(var animation:animations)animation.tile.close();animations.clear();slots.clear();profiles.clear();dirty=false;analyses=0;}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.blaze3d.vertex.BufferBuilder;
|
||||
import com.mojang.blaze3d.vertex.ByteBufferBuilder;
|
||||
import com.mojang.renderpearl.api.GpuFormat;
|
||||
import com.mojang.renderpearl.api.buffers.GpuBuffer;
|
||||
import com.mojang.renderpearl.api.pipeline.PrimitiveTopology;
|
||||
import com.mojang.renderpearl.api.vertex.VertexFormat;
|
||||
import java.util.*;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.model.geom.builders.UVPair;
|
||||
import net.minecraft.client.renderer.block.dispatch.BlockStateModelPart;
|
||||
import net.minecraft.client.renderer.texture.TextureAtlas;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.SectionPos;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.chunk.LevelChunkSection;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Nearby native sections, bounded incremental meshing; models and resource UV rotations remain native. */
|
||||
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 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){}
|
||||
List<Entry> update(){
|
||||
var mc=Minecraft.getInstance();var current=mc.getModelManager().getBlockStateModelSet();
|
||||
if(models!=current||level!=mc.level){close();models=current;level=mc.level;}
|
||||
Vec3 camera=mc.gameRenderer.gameRenderState().levelRenderState.cameraRenderState.pos;
|
||||
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)>Math.pow(VanillaLight.materialDistance()+16,2))continue;
|
||||
var chunk=mc.level.getChunkSource().getChunk(pos.getX()>>4,pos.getZ()>>4,ChunkStatus.FULL,false);
|
||||
if(chunk==null)continue;
|
||||
var storage=chunk.getSection(mc.level.getSectionIndex(pos.getY()));
|
||||
var section=sections.get(key);
|
||||
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;
|
||||
for(var iterator=sections.values().iterator();iterator.hasNext();){var section=iterator.next();if(frame-section.seen>40){remove(section);iterator.remove();}}
|
||||
long deadline=System.nanoTime()+3_000_000L;int started=building==null?0:1;
|
||||
while(System.nanoTime()<deadline){
|
||||
if(building==null){
|
||||
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(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=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();
|
||||
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);
|
||||
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();
|
||||
}
|
||||
private boolean advance(Build build,long deadline){
|
||||
var mc=Minecraft.getInstance();var models=mc.getModelManager().getBlockStateModelSet();
|
||||
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(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);
|
||||
var offset=state.getOffset(pos);
|
||||
for(var part:parts)for(int f=0;f<7;f++){
|
||||
Direction face=f==6?null:Direction.values()[f];
|
||||
if(face!=null&&mc.level.getBlockState(pos.relative(face)).isSolidRender())continue;
|
||||
for(var quad:part.getQuads(face)){
|
||||
var material=quad.materialInfo();var sprite=material.sprite();
|
||||
if(!sprite.atlasLocation().equals(TextureAtlas.LOCATION_BLOCKS))continue;
|
||||
var slot=materials.slot(sprite,state);
|
||||
if(slot.empty())continue;
|
||||
if(build.quads>=16384){build.overflow=true;return true;}
|
||||
int tint=0xFFFFFF,index=material.tintIndex();
|
||||
if(index>=0&&index<tintSources.size())tint=tintSources.get(index).colorInWorld(state,mc.level,pos)&0xFFFFFF;
|
||||
int sky=mc.level.getBrightness(net.minecraft.world.level.LightLayer.SKY,face==null?pos:pos.relative(face));
|
||||
int color=(Math.round(sky/15f*255)<<24)|tint;
|
||||
for(int v=0;v<4;v++){
|
||||
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)+2*PbrMaterials.family(state));
|
||||
}
|
||||
build.quads++;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
if(System.nanoTime()>=deadline)return build.cursor==4096;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
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){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,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 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;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.ssrDistance())
|
||||
.putVec4(Math.max(128,VanillaLight.ssrDistance()),32,VanillaLight.materialDistance(),0);
|
||||
}
|
||||
int max=java.util.stream.Stream.concat(solid.stream(),transparent.stream()).mapToInt(PbrTerrain.Entry::indices).max().orElse(0);
|
||||
RenderSystem.getSequentialBuffer(PrimitiveTopology.QUADS).requestIndexCount(max);
|
||||
var encoder=RenderSystem.getDevice().createCommandEncoder();
|
||||
try(var pass=encoder.createRenderPass(RenderPassDescriptor.builder(()->"Sanctuary nearest material")
|
||||
.withDepthAttachment(receiverView,OptionalDouble.of(0)).build())){
|
||||
pass.setPipeline(RenderSystem.getCompiledPipeline(DEPTH));
|
||||
bind(pass,Minecraft.getInstance().gameRenderer.mainRenderTarget().getColorTextureView(),false);
|
||||
draw(pass,solid,solidAtlas);draw(pass,transparent,transparentAtlas);
|
||||
}
|
||||
ready=true;
|
||||
}
|
||||
static GpuTextureView depth(){return ready?receiverView:null;}
|
||||
private static void bind(com.mojang.renderpearl.api.commands.RenderPass pass,GpuTextureView scene,boolean reflection){
|
||||
var nearest=RenderSystem.getSamplerCache().getClampToEdge(FilterMode.NEAREST);
|
||||
pass.setUniform("ReflectionScene",uniform.currentBuffer());
|
||||
pass.setUniform("OpaqueDepth",opaqueDepthView,nearest);
|
||||
pass.setUniform("OpaqueColor",opaqueColorView!=null?opaqueColorView:scene,nearest);
|
||||
// Never bind the receiver as a sampled texture while it is attached for depth writes.
|
||||
pass.setUniform("ReceiverDepth",reflection?Minecraft.getInstance().gameRenderer.mainRenderTarget().getDepthTextureView():opaqueDepthView,nearest);
|
||||
pass.setUniform("SceneColor",scene,nearest);
|
||||
pass.setUniform("BlockAtlas",Minecraft.getInstance().getTextureManager().getTexture(TextureAtlas.LOCATION_BLOCKS).getTextureView(),nearest);
|
||||
}
|
||||
private static void draw(com.mojang.renderpearl.api.commands.RenderPass pass,List<PbrTerrain.Entry> entries,DynamicTexture atlas){
|
||||
if(atlas==null||entries.isEmpty())return;
|
||||
pass.setUniform("MaterialAtlas",atlas.getTextureView(),RenderSystem.getSamplerCache().getClampToEdge(FilterMode.NEAREST));
|
||||
var indices=RenderSystem.getSequentialBuffer(PrimitiveTopology.QUADS);
|
||||
var camera=Minecraft.getInstance().gameRenderer.gameRenderState().levelRenderState.cameraRenderState;
|
||||
for(var e:entries){
|
||||
pass.setIndexBuffer(indices.getBuffer(),indices.type());
|
||||
var p=e.origin();var offset=new Vector3f((float)(p.getX()-camera.pos.x),(float)(p.getY()-camera.pos.y),(float)(p.getZ()-camera.pos.z));
|
||||
pass.setUniform("DynamicTransforms",RenderSystem.getDynamicUniforms().writeTransform(new Matrix4f(vp).translate(offset),new Vector4f(1,1,1,1),offset,new Matrix4f()));
|
||||
pass.setVertexBuffer(0,e.vertices().slice());pass.drawIndexed(e.indices(),1,0,0,0);
|
||||
}
|
||||
}
|
||||
public static void render(){
|
||||
if(!ready||!VanillaLight.ssr()||VanillaLight.ssrIntensity()==0)return;
|
||||
var main=Minecraft.getInstance().gameRenderer.mainRenderTarget();
|
||||
var scene=ProceduralPbr.copyScene();
|
||||
try(var pass=RenderSystem.getDevice().createCommandEncoder().createRenderPass(()->"Sanctuary pixel reflections",main.getColorTextureView(),Optional.empty(),receiverView,OptionalDouble.empty())){
|
||||
pass.setPipeline(RenderSystem.getCompiledPipeline(REFLECT));bind(pass,scene,true);
|
||||
draw(pass,solid,solidAtlas);draw(pass,transparent,transparentAtlas);
|
||||
}
|
||||
MaterialBloom.accumulate(scene);
|
||||
applied=true;frames++;
|
||||
}
|
||||
public static void release(){
|
||||
if(opaqueColorView!=null){opaqueColorView.close();opaqueColorView=null;}if(opaqueColor!=null){opaqueColor.close();opaqueColor=null;}
|
||||
if(opaqueDepthView!=null){opaqueDepthView.close();opaqueDepthView=null;}if(opaqueDepth!=null){opaqueDepth.close();opaqueDepth=null;}if(receiverView!=null){receiverView.close();receiverView=null;}
|
||||
if(receiver!=null){receiver.close();receiver=null;}if(uniform!=null){uniform.close();uniform=null;}
|
||||
solid=transparent=List.of();solidAtlas=transparentAtlas=null;captured=ready=applied=false;
|
||||
}
|
||||
static long targetBytes(){return (long)width*height*((opaqueDepth!=null?4:0)+(receiver!=null?4:0)+(opaqueColor!=null?4:0));}
|
||||
static GpuTexture landscape(){return opaqueColor;}
|
||||
static boolean applied(){return applied;}static int frames(){return frames;}
|
||||
static boolean allocated(){return opaqueColor!=null;}
|
||||
}
|
||||
@@ -55,7 +55,7 @@ public final class PixelShadows {
|
||||
private static TextureTarget shadowMap;
|
||||
private static MappableRingBuffer uniforms;
|
||||
private static int renderedFrames;
|
||||
private static boolean applied;
|
||||
private static boolean applied, depthReady;
|
||||
|
||||
private PixelShadows() { }
|
||||
public static void register() {
|
||||
@@ -73,8 +73,9 @@ public final class PixelShadows {
|
||||
/** Called after the world's depth is resolved and before the first-person hand and GUI. */
|
||||
public static void render() {
|
||||
applied = false;
|
||||
depthReady = false;
|
||||
var client = Minecraft.getInstance();
|
||||
if (!VanillaLight.active() || !VanillaLight.pixelShadows() || client.level == null) {
|
||||
if (!VanillaLight.active() || (!VanillaLight.pixelShadows() && !(VanillaLight.sunshafts() && VanillaLight.sunshaftIntensity()>0)) || client.level == null) {
|
||||
release();
|
||||
return;
|
||||
}
|
||||
@@ -84,12 +85,16 @@ public final class PixelShadows {
|
||||
CameraRenderState camera = state.cameraRenderState;
|
||||
var sun = sunDirection(sky.sunAngle);
|
||||
boolean lunar = sun.y <= .08f;
|
||||
if (lunar && !VanillaLight.pixelShadows()) { release(); return; }
|
||||
if (lunar) sun = sunDirection(sky.moonAngle);
|
||||
if (sky.skybox != DimensionType.Skybox.OVERWORLD || camera.fogType != FogType.NONE || sun.y <= .08f) return;
|
||||
float moon = sky.moonPhase == null ? 1 : (float)(.5 + .5 * Math.cos(sky.moonPhase.index() * Math.PI / 4));
|
||||
float strength = (lunar ? .10f * moon : .38f)
|
||||
* Math.clamp((sun.y - .08f) / .16f, 0, 1) * sky.rainBrightness;
|
||||
if (strength <= .001f) return;
|
||||
// Rain hides the native solar sprite completely. The atmospheric volume still
|
||||
// needs occluders for its weak scattered daylight; surface shadows stay off.
|
||||
boolean solarVolume=!lunar&&VanillaLight.sunshafts()&&VanillaLight.sunshaftIntensity()>0;
|
||||
if (strength <= .001f && !solarVolume) return;
|
||||
int distance = VanillaLight.shadowDistance(), pixels = VanillaLight.shadowPixels();
|
||||
TERRAIN.update(client.level, camera.pos, distance, sun);
|
||||
int size = mapSize(distance, pixels);
|
||||
@@ -139,6 +144,8 @@ public final class PixelShadows {
|
||||
pass.drawIndexed(entry.indexCount(), 1, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
depthReady = !lunar;
|
||||
if (!VanillaLight.pixelShadows() || strength <= .001f) { renderedFrames++; return; }
|
||||
var main = client.gameRenderer.mainRenderTarget();
|
||||
var nearest = RenderSystem.getSamplerCache().getClampToEdge(FilterMode.NEAREST);
|
||||
try (var pass = encoder.createRenderPass(() -> "Sanctuary pixel shadows", main.getColorTextureView(), Optional.empty())) {
|
||||
@@ -154,6 +161,8 @@ public final class PixelShadows {
|
||||
}
|
||||
|
||||
static double anchor(double coordinate) { return Math.floor(coordinate / 16.0 + .5) * 16.0; }
|
||||
static com.mojang.renderpearl.api.buffers.GpuBuffer materialUniforms(){return uniforms.currentBuffer();}
|
||||
static com.mojang.renderpearl.api.textures.GpuTextureView materialDepth(){return shadowMap.getDepthTextureView();}
|
||||
static Vector3f sunDirection(float angle) { return new Vector3f(-(float)Math.sin(angle), (float)Math.cos(angle), 0); }
|
||||
static int mapSize(int distance, int pixels) {
|
||||
int size = 512;
|
||||
@@ -166,7 +175,9 @@ public final class PixelShadows {
|
||||
if (shadowMap != null) { shadowMap.destroyBuffers(); shadowMap = null; }
|
||||
if (uniforms != null) { uniforms.close(); uniforms = null; }
|
||||
applied = false;
|
||||
depthReady = false;
|
||||
}
|
||||
static boolean depthReady() { return depthReady; }
|
||||
static int renderedFrames() { return renderedFrames; }
|
||||
static boolean applied() { return applied; }
|
||||
static boolean ready() { return TERRAIN.settled(); }
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import com.mojang.blaze3d.buffers.Std140Builder;
|
||||
import com.mojang.blaze3d.pipeline.TextureTarget;
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.renderpearl.api.GpuFormat;
|
||||
import com.mojang.renderpearl.api.buffers.GpuBuffer;
|
||||
import com.mojang.renderpearl.api.buffers.GpuBufferSlice;
|
||||
import com.mojang.renderpearl.api.pipeline.*;
|
||||
import com.mojang.renderpearl.api.textures.FilterMode;
|
||||
import fr.koka.sanctuary.client.panorama.PanoramaCapture;
|
||||
import java.util.*;
|
||||
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.TextureAtlas;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.level.dimension.DimensionType;
|
||||
import net.minecraft.world.level.material.FogType;
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Vector3f;
|
||||
import org.joml.Vector4f;
|
||||
|
||||
/** Optional local material pass: cached texture normals, GGX highlights and diffuse relief. */
|
||||
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(true)
|
||||
.withColorTargetState(new ColorTargetState(Optional.empty(),GpuFormat.RGBA8_UNORM,ColorTargetState.WRITE_COLOR))
|
||||
.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);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;}
|
||||
var state=mc.gameRenderer.gameRenderState().levelRenderState;var camera=state.cameraRenderState;var sky=state.skyRenderState;
|
||||
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);}
|
||||
// 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));
|
||||
float strength=(lunar?.12f*moon:1)*sky.rainBrightness*Math.clamp((sun.y-.04f)/.16f,0,1);
|
||||
var sunColour=lunar?new org.joml.Vector3f(1):SolarColour.tint(sky);
|
||||
boolean shadow=PixelShadows.applied();
|
||||
scene.rotate();try(var mapped=scene.currentBuffer().map(false,true)){
|
||||
Std140Builder.intoBuffer(mapped.data()).putMat4f(new Matrix4f(vp).invert())
|
||||
.putVec4(sun.x,sun.y,sun.z,strength)
|
||||
.putVec4(VanillaLight.pbrIntensity()/100f,RenderSystem.getDevice().getDeviceInfo().isZZeroToOne()?1:0,shadow?1:0,VanillaLight.pbrDistance())
|
||||
.putVec4(fog.renderDistanceStart,fog.renderDistanceEnd,fog.environmentalStart,fog.environmentalEnd).putVec4(fog.color.w,ColoredLights.directionalReady()?1:0,lunar?0:1,0)
|
||||
.putVec4(sunColour.x*sunColour.x,sunColour.y*sunColour.y,sunColour.z*sunColour.z,1)
|
||||
.putVec4((float)(camera.pos.x%256),(float)(camera.pos.y%256),(float)(camera.pos.z%256),0)
|
||||
.putVec4(0,0,0,0).putVec4(0,0,0,0);
|
||||
}
|
||||
var transforms=new ArrayList<GpuBufferSlice>(entries.size());int max=0;
|
||||
for(var entry:entries){var p=entry.origin();var offset=new Vector3f((float)(p.getX()-camera.pos.x),(float)(p.getY()-camera.pos.y),(float)(p.getZ()-camera.pos.z));
|
||||
transforms.add(RenderSystem.getDynamicUniforms().writeTransform(new Matrix4f(vp).translate(offset),new Vector4f(1,1,1,1),offset,new Matrix4f()));max=Math.max(max,entry.indices());}
|
||||
var indices=RenderSystem.getSequentialBuffer(PrimitiveTopology.QUADS);indices.requestIndexCount(max);
|
||||
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||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!=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++;
|
||||
}
|
||||
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();}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import com.mojang.blaze3d.buffers.Std140Builder;
|
||||
import com.mojang.blaze3d.pipeline.TextureTarget;
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.renderpearl.api.GpuFormat;
|
||||
import com.mojang.renderpearl.api.buffers.GpuBuffer;
|
||||
import com.mojang.renderpearl.api.pipeline.*;
|
||||
import com.mojang.renderpearl.api.textures.FilterMode;
|
||||
import fr.koka.sanctuary.client.panorama.PanoramaCapture;
|
||||
import java.util.Optional;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.MappableRingBuffer;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.level.material.FogType;
|
||||
import org.joml.Matrix4f;
|
||||
|
||||
/** Bounded, single-bounce diffuse lighting from this frame's visible surfaces. No GPU readback or history. */
|
||||
public final class ScreenSpaceGI {
|
||||
private static final BindGroupLayout LAYOUT=BindGroupLayout.builder()
|
||||
.withUniform("GIParams",UniformType.UNIFORM_BUFFER)
|
||||
.withUniform("WorldDepth",UniformType.COMBINED_IMAGE_SAMPLER)
|
||||
.withUniform("SceneColor",UniformType.COMBINED_IMAGE_SAMPLER)
|
||||
.withUniform("IndirectLight",UniformType.COMBINED_IMAGE_SAMPLER).build();
|
||||
private static RenderPipeline pipeline(String name,boolean composite){
|
||||
return RenderPipelines.register(RenderPipeline.builder().withLocation(Identifier.parse("sanctuary:pipeline/"+name))
|
||||
.withVertexShader("core/screenquad").withFragmentShader(Identifier.parse("sanctuary:core/"+name))
|
||||
.withBindGroupLayout(LAYOUT).withBindGroupLayout(BindGroupLayout.builder().withUniform("GIBlur",UniformType.UNIFORM_BUFFER).build()).withPrimitiveTopology(PrimitiveTopology.TRIANGLES).withCull(false)
|
||||
.withColorTargetState(new ColorTargetState(composite?Optional.of(new BlendFunction(BlendFactor.ONE,BlendFactor.ZERO,BlendFactor.ZERO,BlendFactor.ONE)):Optional.empty(),
|
||||
composite?GpuFormat.RGBA8_UNORM:GpuFormat.RGBA16_FLOAT,composite?ColorTargetState.WRITE_COLOR:ColorTargetState.WRITE_ALL))
|
||||
.withDepthStencilState(Optional.empty()).build());
|
||||
}
|
||||
private static final RenderPipeline GATHER=pipeline("ssgi_gather",false),APPLY=pipeline("ssgi_apply",true),FILTER=pipeline("ssgi_filter",false);
|
||||
private static final Matrix4f projection=new Matrix4f();
|
||||
private static TextureTarget source,indirect,filtered;
|
||||
private static MappableRingBuffer uniforms,horizontal,vertical;
|
||||
private static boolean applied;
|
||||
private static int frames;
|
||||
private ScreenSpaceGI(){}
|
||||
public static void captureProjection(Matrix4f matrix){projection.set(matrix);}
|
||||
public static void render(){
|
||||
applied=false;var mc=Minecraft.getInstance();
|
||||
if(!VanillaLight.active()||!VanillaLight.ssgi()||VanillaLight.ssgiIntensity()==0||mc.level==null){release();return;}
|
||||
if(PanoramaCapture.rendering())return;
|
||||
var camera=mc.gameRenderer.gameRenderState().levelRenderState.cameraRenderState;
|
||||
if(camera.fogType!=FogType.NONE)return;
|
||||
var main=mc.gameRenderer.mainRenderTarget();
|
||||
if(source==null||source.width!=main.width||source.height!=main.height){
|
||||
release();source=new TextureTarget("Sanctuary SSGI source",main.width,main.height,GpuFormat.RGBA8_UNORM,null);
|
||||
indirect=new TextureTarget("Sanctuary SSGI indirect",Math.max(1,(main.width+1)/2),Math.max(1,(main.height+1)/2),GpuFormat.RGBA16_FLOAT,null);
|
||||
filtered=new TextureTarget("Sanctuary SSGI filtered",indirect.width,indirect.height,GpuFormat.RGBA16_FLOAT,null);
|
||||
}
|
||||
if(uniforms==null){
|
||||
uniforms=new MappableRingBuffer(()->"Sanctuary SSGI",GpuBuffer.USAGE_UNIFORM|GpuBuffer.USAGE_MAP_WRITE,176);
|
||||
horizontal=new MappableRingBuffer(()->"Sanctuary SSGI filter X",GpuBuffer.USAGE_UNIFORM|GpuBuffer.USAGE_MAP_WRITE,16);
|
||||
vertical=new MappableRingBuffer(()->"Sanctuary SSGI filter Y",GpuBuffer.USAGE_UNIFORM|GpuBuffer.USAGE_MAP_WRITE,16);
|
||||
}
|
||||
horizontal.rotate();try(var m=horizontal.currentBuffer().map(false,true)){Std140Builder.intoBuffer(m.data()).putVec4(1,0,0,0);}
|
||||
vertical.rotate();try(var m=vertical.currentBuffer().map(false,true)){Std140Builder.intoBuffer(m.data()).putVec4(0,1,0,0);}
|
||||
|
||||
var vp=new Matrix4f(projection).mul(camera.viewRotationMatrix);var fog=camera.fogData;
|
||||
uniforms.rotate();try(var mapped=uniforms.currentBuffer().map(false,true)){
|
||||
Std140Builder.intoBuffer(mapped.data()).putMat4f(new Matrix4f(vp).invert()).putMat4f(vp)
|
||||
.putVec4(VanillaLight.ssgiIntensity()/100f,RenderSystem.getDevice().getDeviceInfo().isZZeroToOne()?1:0,2.5f,32)
|
||||
.putVec4(fog.renderDistanceStart,fog.renderDistanceEnd,fog.environmentalStart,fog.environmentalEnd)
|
||||
.putVec4(fog.color.w,0,0,0);
|
||||
}
|
||||
// Snapshot once, before adding indirect light: no feedback, no texture sampled while attached for writing.
|
||||
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);
|
||||
try(var pass=encoder.createRenderPass(()->"Sanctuary SSGI gather",indirect.getColorTextureView(),Optional.empty())){
|
||||
pass.setPipeline(RenderSystem.getCompiledPipeline(GATHER));pass.setUniform("GIBlur",horizontal.currentBuffer());pass.setUniform("GIParams",uniforms.currentBuffer());
|
||||
pass.setUniform("WorldDepth",main.getDepthTextureView(),nearest);pass.setUniform("SceneColor",source.getColorTextureView(),nearest);
|
||||
// Unused by gather; bind a separate texture, never its own output.
|
||||
pass.setUniform("IndirectLight",source.getColorTextureView(),nearest);pass.draw(3,1,0,0);
|
||||
}
|
||||
filter(encoder,indirect,filtered,horizontal);
|
||||
filter(encoder,filtered,indirect,vertical);
|
||||
try(var pass=encoder.createRenderPass(()->"Sanctuary SSGI composite",main.getColorTextureView(),Optional.empty())){
|
||||
pass.setPipeline(RenderSystem.getCompiledPipeline(APPLY));pass.setUniform("GIBlur",horizontal.currentBuffer());pass.setUniform("GIParams",uniforms.currentBuffer());
|
||||
pass.setUniform("WorldDepth",main.getDepthTextureView(),nearest);pass.setUniform("SceneColor",source.getColorTextureView(),nearest);
|
||||
pass.setUniform("IndirectLight",indirect.getColorTextureView(),nearest);pass.draw(3,1,0,0);
|
||||
}
|
||||
applied=true;frames++;
|
||||
}
|
||||
private static void filter(com.mojang.renderpearl.api.commands.CommandEncoder encoder,TextureTarget input,TextureTarget output,MappableRingBuffer direction){
|
||||
var main=Minecraft.getInstance().gameRenderer.mainRenderTarget();var nearest=RenderSystem.getSamplerCache().getClampToEdge(FilterMode.NEAREST);
|
||||
try(var pass=encoder.createRenderPass(()->"Sanctuary SSGI bilateral filter",output.getColorTextureView(),Optional.empty())){
|
||||
pass.setPipeline(RenderSystem.getCompiledPipeline(FILTER));pass.setUniform("GIParams",uniforms.currentBuffer());pass.setUniform("GIBlur",direction.currentBuffer());
|
||||
pass.setUniform("WorldDepth",main.getDepthTextureView(),nearest);pass.setUniform("SceneColor",source.getColorTextureView(),nearest);
|
||||
pass.setUniform("IndirectLight",input.getColorTextureView(),RenderSystem.getSamplerCache().getClampToEdge(FilterMode.LINEAR));pass.draw(3,1,0,0);
|
||||
}
|
||||
}
|
||||
public static void release(){if(source!=null){source.destroyBuffers();indirect.destroyBuffers();filtered.destroyBuffers();source=indirect=filtered=null;}if(uniforms!=null){uniforms.close();horizontal.close();vertical.close();uniforms=horizontal=vertical=null;}applied=false;}
|
||||
static boolean applied(){return applied;}
|
||||
static boolean allocated(){return source!=null;}
|
||||
static int renderedFrames(){return frames;}
|
||||
}
|
||||
+50
-3
@@ -14,9 +14,54 @@ 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));
|
||||
list.addBig(new OptionInstance<Integer>("sanctuary.shaders.precision",OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders.precision_help")),(caption,value)->Component.translatable("sanctuary.shaders.precision_value",value),
|
||||
new OptionInstance.Enum<>(List.of(8,16,32,64,128),Codec.INT),VanillaLight.shadowPixels(),VanillaLight::setShadowPixels));
|
||||
list.addBig(OptionInstance.createBoolean("sanctuary.shaders.pbr",OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders.pbr_help")),VanillaLight.pbr(),VanillaLight::setPbr));
|
||||
slider("pbr_intensity",VanillaLight.pbrIntensity(),VanillaLight::setPbrIntensity);
|
||||
list.addBig(new OptionInstance<Integer>("sanctuary.shaders.pbr_distance",OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders.pbr_distance_help")),(caption,value)->Component.translatable("sanctuary.shaders.pbr_distance_value",value),
|
||||
new OptionInstance.Enum<>(List.of(16,32,64,128,256),Codec.INT),VanillaLight.pbrDistance(),VanillaLight::setPbrDistance));
|
||||
list.addBig(OptionInstance.createBoolean("sanctuary.shaders.ssr",OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders.ssr_help")),VanillaLight.ssr(),VanillaLight::setSsr));
|
||||
slider("ssr_intensity",VanillaLight.ssrIntensity(),VanillaLight::setSsrIntensity);
|
||||
list.addBig(new OptionInstance<Integer>("sanctuary.shaders.ssr_distance",OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders.ssr_distance_help")),(caption,value)->Component.translatable("sanctuary.shaders.ssr_distance_value",value),
|
||||
new OptionInstance.Enum<>(List.of(16,32,64,128,256),Codec.INT),VanillaLight.ssrDistance(),VanillaLight::setSsrDistance));
|
||||
list.addBig(OptionInstance.createBoolean("sanctuary.shaders.colored_lights",OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders.colored_lights_help")),VanillaLight.coloredLights(),VanillaLight::setColoredLights));
|
||||
list.addBig(OptionInstance.createBoolean("sanctuary.shaders.warm_torches",OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders.warm_torches_help")),VanillaLight.warmTorches(),VanillaLight::setWarmTorches));
|
||||
slider("colored_light_intensity",VanillaLight.coloredLightIntensity(),VanillaLight::setColoredLightIntensity);
|
||||
list.addBig(new OptionInstance<Integer>("sanctuary.shaders.colored_light_distance",OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders.colored_light_distance_help")),(caption,value)->Component.translatable("sanctuary.shaders.colored_light_distance_value",value),
|
||||
new OptionInstance.Enum<>(List.of(8,16,32,64),Codec.INT),VanillaLight.coloredLightDistance(),VanillaLight::setColoredLightDistance));
|
||||
list.addBig(OptionInstance.createBoolean("sanctuary.shaders.ssgi",OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders.ssgi_help")),VanillaLight.ssgi(),VanillaLight::setSsgi));
|
||||
slider("ssgi_intensity",VanillaLight.ssgiIntensity(),VanillaLight::setSsgiIntensity);
|
||||
list.addBig(OptionInstance.createBoolean("sanctuary.shaders.bloom",OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders.bloom_help")),VanillaLight.bloom(),VanillaLight::setBloom));
|
||||
slider("bloom_intensity",VanillaLight.bloomIntensity(),VanillaLight::setBloomIntensity);
|
||||
slider("bloom_radius",VanillaLight.bloomRadius(),VanillaLight::setBloomRadius);
|
||||
list.addBig(OptionInstance.createBoolean("sanctuary.shaders.sunshafts",OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders.sunshafts_help")),VanillaLight.sunshafts(),VanillaLight::setSunshafts));
|
||||
slider("sunshaft_intensity",VanillaLight.sunshaftIntensity(),VanillaLight::setSunshaftIntensity);
|
||||
list.addBig(OptionInstance.createBoolean("sanctuary.shaders.lens_flare",OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders.lens_flare_help")),VanillaLight.lensFlare(),VanillaLight::setLensFlare));
|
||||
slider("lens_flare_intensity",VanillaLight.lensFlareIntensity(),VanillaLight::setLensFlareIntensity);
|
||||
list.addBig(OptionInstance.createBoolean("sanctuary.shaders.ore_emission",OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders.ore_emission_help")),VanillaLight.oreEmission(),VanillaLight::setOreEmission));
|
||||
slider("ore_intensity",VanillaLight.oreIntensity(),VanillaLight::setOreIntensity);
|
||||
list.addBig(OptionInstance.createBoolean("sanctuary.shaders.edge_highlights",OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders.edge_highlights_help")),
|
||||
VanillaLight.edgeHighlights(),VanillaLight::setEdgeHighlights));
|
||||
@@ -29,11 +74,13 @@ public final class ShaderOptionsScreen extends OptionsSubScreen {
|
||||
list.addBig(OptionInstance.createBoolean("sanctuary.shaders.pixel_shadows",OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders.pixel_shadows_help")),
|
||||
VanillaLight.pixelShadows(),VanillaLight::setPixelShadows));
|
||||
list.addBig(new OptionInstance<Integer>("sanctuary.shaders.shadow_pixels",OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders.shadow_pixels_help")),(caption,value)->Component.translatable("sanctuary.shaders.shadow_pixels_value",value),
|
||||
new OptionInstance.Enum<>(List.of(8,16,32),Codec.INT),VanillaLight.shadowPixels(),VanillaLight::setShadowPixels));
|
||||
list.addBig(new OptionInstance<Integer>("sanctuary.shaders.shadow_distance",OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders.shadow_distance_help")),(caption,value)->Component.translatable("sanctuary.shaders.shadow_distance_value",value),
|
||||
new OptionInstance.Enum<>(List.of(16,32,64,128,256),Codec.INT),VanillaLight.shadowDistance(),VanillaLight::setShadowDistance));
|
||||
}
|
||||
private void slider(String key,int value,java.util.function.Consumer<Integer> setter){
|
||||
list.addBig(new OptionInstance<Integer>("sanctuary.shaders."+key,OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders."+key+"_help")),(caption,v)->Component.translatable("sanctuary.shaders."+key+"_value",v),
|
||||
new OptionInstance.IntRange(0,100),value,setter::accept));
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user