Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf9a670f44 | ||
|
|
ed2d088272 | ||
|
|
0cfd51eb4f | ||
|
|
38f4d6c9a9 | ||
|
|
580b2414da | ||
|
|
97ff2b54cb | ||
|
|
59346d7f0a | ||
|
|
ea151dd5df | ||
|
|
7ce01c47f2 | ||
|
|
93ed5574a4 | ||
|
|
c9f5c56e68 | ||
|
|
6e1b3b7108 | ||
|
|
c05c338e6b | ||
|
|
dc33ffa546 | ||
|
|
9d84a02ed9 | ||
|
|
91181865d5 | ||
|
|
daf36fdeac | ||
|
|
7f576d14d9 |
@@ -1,5 +1,22 @@
|
||||
# Sanctuary
|
||||
|
||||
## 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,11 +37,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.137** ajoute des **rayons solaires volumétriques** qui traversent
|
||||
les ouvertures du décor et le feuillage. Activés à 35 %, réglables indépendamment
|
||||
du bloom dans **Options → Shaders**. Carte d'ombres partagée, portée de 64 blocs
|
||||
maximum. Le [ticket](docs/shader-beta137.md) détaille les vérifications.
|
||||
Livraison en cours de vérification.
|
||||
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.
|
||||
@@ -35,6 +60,56 @@ 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.
|
||||
@@ -2261,7 +2336,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.146 |
|
||||
|
||||
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
|
||||
@@ -2290,7 +2365,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.146.jar` : mod à installer avec
|
||||
Fabric API sur la version Minecraft indiquée.
|
||||
- `build/packwiz/` : pack de développement complet, avec le mod construit et
|
||||
l'index vérifié. Voir [Installation packwiz](packwiz/README.md).
|
||||
|
||||
@@ -0,0 +1,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,127 @@
|
||||
# LIGHT-141 — Direction locale pour les normales PBR
|
||||
|
||||
Socle beta.140 `c9f5c56`, branche `codex/directional-lights-beta141`.
|
||||
Minecraft 26.3 / Java 25. Livraison beta.141.
|
||||
|
||||
## Contrat
|
||||
|
||||
Estimer une direction d'arrivée depuis les six voisins du champ lumineux,
|
||||
avec intensité et confiance. Conserver cette estimation dans une texture
|
||||
compacte reconstruite avec le champ, uniquement quand le PBR est actif.
|
||||
Les sources portées utilisent leurs positions déjà disponibles. Brancher ces
|
||||
directions sur les normales et reflets PBR existants, sans nouveau bouton.
|
||||
Les sources opposées doivent réduire la directionnalité, sans bascule brutale.
|
||||
Les champs précédent/courant gardent leur fondu et les obstacles restent pris
|
||||
en compte pour les blocs posés. Aucun changement de lumière de gameplay,
|
||||
de sauvegarde ou de génération ; aucune promesse de lancer de rayons exact.
|
||||
|
||||
## Programme de vérification
|
||||
|
||||
Contrôles natifs OpenGL/Vulkan : direction, sources opposées, murs, rendu de
|
||||
relief en intérieur, déplacement des sources, transitions et PBR OFF.
|
||||
Régression des lumières beta.139/140 puis check/build et assemblage. GameTest
|
||||
serveur dédié exclu selon le refus EULA antérieur. Aucun monde personnel ouvert.
|
||||
|
||||
## Réalisation
|
||||
|
||||
Le champ RGB conserve une texture directionnelle RGBA8 supplémentaire :
|
||||
les trois composantes codent la direction pondérée, l'alpha sa réponse lumineuse.
|
||||
Les voisins plus lumineux et accessibles contribuent à l'estimation ; les
|
||||
arrivées opposées s'annulent dans le vecteur, sans supprimer l'éclairage natif.
|
||||
Le shader interpole les moments pondérés avant normalisation, y compris entre
|
||||
les champs précédent et courant. La confiance réduit le relief quand aucune
|
||||
direction ne domine.
|
||||
|
||||
Cette texture est préparée dans le budget de reconstruction existant, avec
|
||||
un maximum de six voisins par cellule éclairée et une table d'énergie réutilisée.
|
||||
Aucune recherche de sources ni rayons par pixel pour les blocs posés. La
|
||||
lecture GPU interpole huit cellules par champ ; les deux champs sont utilisés
|
||||
pendant le fondu. Les sources mobiles réutilisent les positions, intensités et
|
||||
transitions déjà collectées, dans la limite existante de 128 sources.
|
||||
|
||||
Une texture directionnelle occupe environ 1,5 Mo à 16 blocs de distance et
|
||||
19 Mo à 64 blocs, par copie CPU/GPU ; deux champs coexistent lors des transitions.
|
||||
Le champ directionnel n'est construit que si PBR est actif avec une intensité
|
||||
non nulle. Sa désactivation libère les directions après la transition.
|
||||
Le coût est borné par ces réglages, sans garantie de FPS.
|
||||
|
||||
La passe PBR applique la différence entre normale de base et normale perturbée,
|
||||
puis un reflet GGX local. Elle garde sa portée de 32 blocs et les réglages PBR
|
||||
existants ; Lumières colorées doit être actif. Le soleil et la lune continuent
|
||||
à fonctionner lorsque les lumières colorées sont coupées. Les aides FR/EN
|
||||
expliquent cette dépendance.
|
||||
|
||||
C'est une direction dominante approximative : deux lumières colorées opposées
|
||||
ne produisent pas deux reflets indépendants. Les sources mobiles gardent la
|
||||
propagation radiale et les limites d'occultation existantes. Les normales PBR
|
||||
concernent les surfaces de blocs prises en charge, pas tous les modèles de mobs.
|
||||
Les exclusions existantes des panoramas et de l'immersion restent inchangées.
|
||||
|
||||
## Premier essai ciblé
|
||||
|
||||
L'essai natif OpenGL ciblé réussit en **1 min 12 s** : direction gauche/droite,
|
||||
annulation des sources opposées avec énergie conservée, occultation par un mur,
|
||||
relief sur la pierre en intérieur et avec une torche en main, stabilité au repos,
|
||||
libération des directions avec PBR OFF et maintien du chemin solaire avec
|
||||
Lumières colorées OFF. La somme d'écarts PBR OFF/ON sur la pierre vaut 878 114
|
||||
au réglage PBR 100 % ; c'est une comparaison de pixels, pas une mesure de FPS.
|
||||
Log : `build/directional141-focused.log`.
|
||||
|
||||
Les essais de préparation ont révélé un conflit de nom de matrice GLSL, corrigé,
|
||||
puis des erreurs de fixture (angle 180° normalisé par Minecraft, suppression
|
||||
d'entités alors que la sélection était vide, commande d'heure déjà à minuit),
|
||||
corrigées sans changer le rendu.
|
||||
|
||||
## Validation complète OpenGL
|
||||
|
||||
La chaîne complète réussit en **3 min 1 s** sur OpenGL / Apple M1 :
|
||||
`COLORED139_PASS`, `COLORED140_PASS` et `DIRECTIONAL141_PASS`.
|
||||
Elle réunit les contrôles de couleurs, objets portés, caméra, occultation,
|
||||
rechargement des ressources, préférences, distances/fondus et directions PBR.
|
||||
Le test ciblé est reproductible avec `-PsanctuaryDirectional141Only=true` ;
|
||||
la chaîne complète utilise `-PsanctuaryDirectional141ClientTests=true` avec
|
||||
les deux suites de lumières existantes.
|
||||
Log : `build/directional141-opengl.log`. Captures natives conservées dans
|
||||
`build/directional141-opengl-screenshots/`. Comparaison autonome sans retraitement :
|
||||
`build/Shader-beta.141-comparaison.html`.
|
||||
|
||||
## Validation complète Vulkan
|
||||
|
||||
La même chaîne réussit en **2 min 56 s** sur Vulkan / Apple M1, MoltenVK 1.4.2,
|
||||
profondeur 0–1 et transparence améliorée. Les trois marqueurs de réussite sont
|
||||
présents dans `build/directional141-vulkan.log`. L'écart PBR local OFF/ON vaut
|
||||
878 107, contre 878 114 sous OpenGL dans cette scène. Les captures natives sont
|
||||
conservées dans `build/directional141-vulkan-screenshots/`.
|
||||
Les deux moteurs vérifient la compilation, les chemins actifs et désactivés,
|
||||
le cache, les murs, les sources opposées et portées, ainsi que les régressions
|
||||
beta.139/140. Pas de validation des pilotes Windows de l'utilisateur revendiquée.
|
||||
|
||||
## Construction et archives
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
réussit en **2 min 35 s**, 126 tâches. Le GameTest serveur dédié reste exclu
|
||||
selon le refus EULA antérieur. Les suites clientes ci-dessus ont été exécutées
|
||||
séparément. Log : `build/shader141-build.log`.
|
||||
|
||||
Comparaison à beta.140 : **8 entrées de production modifiées**, limitées au
|
||||
champ lumineux, à sa préparation avant le PBR, au shader PBR et aux aides FR/EN.
|
||||
Les archives normale/Test contiennent le même JAR Sanctuary, sans classes
|
||||
de test. Version, dépendances exactes, sources des JAR, textures de gemmes et
|
||||
de la clé de Steve, et template complet sont vérifiés.
|
||||
Reçu : `build/shader141-artifact.json`.
|
||||
|
||||
SHA-256 du JAR : `660f29c3fa77fee4de6a8db5673c61619985804494f70e961998da36f396d0f1`.
|
||||
|
||||
- Sanctuary-beta.141.mrpack : `08df23dac9e98fbfa2935f747345c38244bddc3a3d9371c6416f251e85a7eb19`.
|
||||
- Sanctuary-Test-beta.141.mrpack : `a300ef5bfff7b6990c9f5c2760e9c3a23d7a27aeb10b8f04d29db9a7409fa0e1`.
|
||||
|
||||
## Livraison
|
||||
|
||||
[Release beta.141](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.141)
|
||||
publiée sur le commit source `93ed5574a4f77bd3a083b254498f4994efe31179` ; canal stable
|
||||
`7e6c84400f80db24bf5204cd21d628194ae9f0f7` vérifié après publication.
|
||||
Deux synchronisations isolées puis deux dans la même instance Sanctuary Beta
|
||||
réussissent. Les **923 fichiers personnels** suivis gardent leurs hashes.
|
||||
Sauvegarde préalable : `sanctuary-backups/before-beta.141/`. Aucun monde personnel
|
||||
ouvert. Reçus : `build/shader141-isolated.json` et `build/shader141-prism.json`.
|
||||
Archives normale/Test/template et comparaison copiées dans `sanctuary-beta/build/`.
|
||||
@@ -0,0 +1,121 @@
|
||||
# WATER-144 — Eau naturelle pixélisée
|
||||
|
||||
Branche `codex/water-beta144`, socle beta.143 publiée (`97ff2b5`) et suivi
|
||||
`580b241`. Minecraft 26.3, Java 25, dépendances conservées.
|
||||
|
||||
## Contrat
|
||||
|
||||
**Options → Shaders → Eau naturelle** est activée par défaut avec Sanctuary.
|
||||
L’intensité (0–100 %, défaut 100 %) agit sur la surface et l’immersion.
|
||||
L’interrupteur ou 0 % retrouve le rendu précédent. Les préférences existantes
|
||||
restent conservées ; un moteur externe suspend l’effet comme les autres effets
|
||||
natifs Sanctuary.
|
||||
|
||||
La couleur native du biome reste la base, y compris les variantes d’océans.
|
||||
Les océans (`minecraft:is_ocean`) imposent le profil clair.
|
||||
Hors océan, le premier support situé au maximum huit blocs sous la cellule
|
||||
observée choisit le profil :
|
||||
|
||||
| Profil | Fond | Mélange olive #788448 | Alpha natif | Distance de brume native |
|
||||
| --- | --- | ---: | ---: | ---: |
|
||||
| Claire | Pierre, matériaux construits, inconnus | 0 % | ×0,55 | ×1 |
|
||||
| Légèrement teintée | Sable, sable rouge, gravier, argile | 10 % | ×0,80 | ×0,70 |
|
||||
| Étang | Terre, terre stérile/enracinée, herbe, podzol, mycélium, boue, racines de palétuvier boueuses | 85 % | ×1 | ×0,40 |
|
||||
|
||||
L’eau et les plantes aquatiques sans collision sont traversées. Un support
|
||||
immergé reste un fond ; absence de support, air ou données manquantes donnent
|
||||
une eau claire. La référence visuelle fournie pendant l’implémentation demande un véritable
|
||||
vert olive légèrement jaunâtre : le profil étang passe donc de 45 % à 85 %
|
||||
de mélange, tout en laissant les eaux claires et océaniques natives.
|
||||
Les surfaces profondes sans fond dans la limite restent donc
|
||||
claires. Les profils se mélangent aux sommets sur un bloc, sans filtrer les
|
||||
textures. L’eau sous un toit suit son fond, pas l’éclairage du bâtiment.
|
||||
|
||||
La ressource client `assets/sanctuary/water_materials.json` accepte deux listes
|
||||
d’identifiants de blocs, `light` et `pond`, remplaçables par un resource pack.
|
||||
Les blocs absents des listes sont clairs. Une ressource invalide est signalée
|
||||
et reprend les familles embarquées. Le template personnel inclut cette ressource.
|
||||
|
||||
## Rendu et coût
|
||||
|
||||
Modification des seules couleurs/alpha des sommets de `FluidRenderer`, sans
|
||||
passe écran ni duplication de maillage. Textures, UV, animation, éclairage
|
||||
et tri des transparences restent natifs. Les calculs de profils sont différés
|
||||
jusqu’à l’émission d’un sommet visible, puis mutualisés dans la région compilée.
|
||||
Un test de palette élimine les sections sans eau avant toute copie de biomes.
|
||||
Les états de blocs viennent des copies natives ; les appartenances océaniques
|
||||
sont capturées avant l’envoi au worker. Aucun chargement de chunk n’est demandé.
|
||||
|
||||
Un changement de bloc invalide la zone dépendante, huit blocs au-dessus et
|
||||
un bloc horizontalement. Les reconstructions natives recopient leurs données ;
|
||||
l’effet ne déclenche aucune reconstruction pour une lumière mobile.
|
||||
Un changement d’option reconstruit seulement les sections chargées contenant de
|
||||
l’eau ; les caches des autres effets restent disponibles. Le cache temporaire
|
||||
disparaît avec sa compilation, sans historique de monde.
|
||||
|
||||
Sous l’eau, le profil est échantillonné au maximum une fois par tick, avec
|
||||
transition de 0,5 seconde. La couleur et les distances environnementales sont
|
||||
adaptées dans le rendu natif du brouillard. La teinte finale est appliquée après
|
||||
la normalisation de la vision aquatique, pour éviter de transformer l’olive en
|
||||
jaune fluorescent ; la luminosité issue des modificateurs natifs de vision
|
||||
et le bonus du compagnon calmar sont conservés. Lave et brume atmosphérique ne changent
|
||||
pas. Il n’y a pas de simulation d’absorption volumétrique ni de reflet ajouté.
|
||||
|
||||
## Validation native
|
||||
|
||||
Les essais utilisent un monde plat jetable, graine 122, avec bassins sur quartz,
|
||||
sable et boue, support du sable, plantes aquatiques, toit, frontière de sections
|
||||
et changement de biome océan. Une copie personnelle de ressource remplace
|
||||
réellement les familles de matériaux puis est désactivée.
|
||||
|
||||
Les suites graphiques OpenGL et Vulkan ont passé les contrôles de surface,
|
||||
d’immersion, de fond modifié, d’océan, de ressources, de préférences et de
|
||||
cascades pendant le développement. La dernière optimisation reconstruit
|
||||
seulement les sections contenant de l’eau lors du changement d’option.
|
||||
Sa série de validation finale reste à terminer avant publication.
|
||||
|
||||
### Protocole de performance
|
||||
|
||||
Apple M1, macOS, Java 25, 854×480, distance de rendu 6 chunks,
|
||||
ombres 32 blocs / 16 pixels, PBR 50 %, autres effets activés.
|
||||
VSync désactivée, limite 260 (illimitée), limitation AFK désactivée.
|
||||
Pour chaque scène : OFF/ON/ON/OFF/ON/OFF/OFF/ON, dix secondes par passage,
|
||||
avec stabilisation des sections puis quatre secondes de chauffe entre passages.
|
||||
Le déplacement avance de 120 blocs et laisse les sections se compiler
|
||||
naturellement, sans vidange artificielle des caches des autres effets.
|
||||
Les moyennes des quatre médianes et P95 par état sont comparées avec les seuils
|
||||
+5 % et +2 ms. Les temps de compilation de sections sont consignés également.
|
||||
|
||||
Les essais courts initiaux ont montré une forte variabilité. Une limitation AFK
|
||||
à 30 FPS puis des clients Minecraft concurrents dans le chantier PBR/SSR ont
|
||||
été identifiés. Ces essais restent dans les preuves de développement ; ils
|
||||
ne valident pas la livraison. La série finale attend la fin du chantier
|
||||
concurrent, conformément à la demande du créateur. Les résultats ne sont pas
|
||||
présentés comme acquis, et aucun gain de performance universel n’est promis.
|
||||
|
||||
Pour reproduire, avec Java 25 et un environnement graphique disponible :
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
|
||||
-PsanctuaryPixelShadows122ClientTests=true -PsanctuaryWater144ClientTests=true \
|
||||
-PsanctuaryClientNoVsync=true -PsanctuaryQuickTests=true \
|
||||
-PsanctuaryClientGraphicsBackend=vulkan > build/water144-vulkan.log 2>&1
|
||||
python3 scripts/check_water_performance.py build/water144-vulkan.log
|
||||
```
|
||||
|
||||
Répéter avec `opengl`, sans autre client graphique simultané. Le vérificateur
|
||||
refuse les séries incomplètes et les dépassements ; il écrit le reçu détaillé
|
||||
près du journal. Le marqueur `WATER144_PASS` atteste les contrôles fonctionnels.
|
||||
|
||||
### Assemblage et publication
|
||||
|
||||
Le GameTest serveur dédié reste exclu conformément au refus antérieur de son
|
||||
EULA. La commande de livraison est `./gradlew check build assemblePack
|
||||
assembleTestPack -x :sanctuary:runGameTest` ; la suite complète non exclue
|
||||
n’est pas déclarée réussie. Les contrôles natifs de l’eau utilisent le serveur
|
||||
intégré d’un monde jetable. Aucun essai Windows n’est revendiqué.
|
||||
|
||||
La release, le canal et l’instance ne sont pas encore mis à jour.
|
||||
Les preuves détaillées restent dans les dossiers ignorés :
|
||||
`build/water144-*.log`, `build/water144-*-performance.json`,
|
||||
`build/water144-*-isolation.json` et `build/water144-evidence/`.
|
||||
@@ -14,6 +14,87 @@ 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
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
# PBR-142 — Stabilité du relief et Vulkan par défaut
|
||||
|
||||
Socle beta.141 `7ce01c4`, branche `codex/pbr-refresh-beta142`.
|
||||
Minecraft 26.3 / Java 25. Livraison beta.142.
|
||||
|
||||
## Contrat
|
||||
|
||||
Corriger le clignotement lors de la reconstruction des sections PBR : conserver
|
||||
le rendu précédent jusqu'à ce que le remplaçant soit disponible et éviter les
|
||||
reconstructions dues seulement aux lumières dynamiques. Une invalidation pendant
|
||||
une reconstruction doit être conservée, sans annuler indéfiniment le travail.
|
||||
Le PBR passe à 80 % par défaut, valeur confirmée par l'utilisateur ; les choix
|
||||
explicitement enregistrés restent conservés.
|
||||
|
||||
La capture `2026-09-18_04.19.34.png` montre des points très clairs, plus fins
|
||||
que les pixels des textures, sur des parois et un plafond de grotte. L'utilisateur
|
||||
confirme OpenGL et leur disparition quand seul le PBR est coupé. Examiner
|
||||
les lectures de textures et les calculs aux angles rasants séparément du cache.
|
||||
|
||||
Tests natifs prévus : recalcul de section sous observation, invalidations
|
||||
répétées, remplacement atomique, suppression réelle de blocs, lumière portée,
|
||||
cache et préférences ; OpenGL et Vulkan. Pas de monde personnel ouvert.
|
||||
|
||||
## Précision du signalement et implémentation
|
||||
|
||||
L'utilisateur précise ensuite que le bruit apparaît **en mouvement**, et demande
|
||||
le passage à Vulkan. Il ne faut donc pas attribuer les points de sa capture au
|
||||
seul clignotement de reconstruction : ce sont deux vérifications distinctes.
|
||||
|
||||
- Les sections PBR publiées restent affichées jusqu'au remplacement de leur
|
||||
maillage. Une nouvelle invalidation durant la construction est mémorisée ;
|
||||
elle n'annule plus continuellement le travail. Le budget permanent reste
|
||||
24 Mio, avec temporairement l'ancien et le nouveau maillage d'une section.
|
||||
- Les invalidations de lumière dynamique ne régénèrent plus la géométrie PBR.
|
||||
Les changements de blocs et de lumière du ciel restent pris en compte.
|
||||
- Lectures explicites des texels de profondeur/scène et du niveau de matériau,
|
||||
gradients de texture calculés avant les rejets de fragments. Les produits
|
||||
scalaires des reflets sont bornés, la puissance entière de Fresnel est
|
||||
explicite, les résultats non finis ne remplacent pas la scène native.
|
||||
Aucun flou ajouté aux textures.
|
||||
- Le choix graphique natif « Par défaut » devient « Vulkan (par défaut) »
|
||||
(FR/EN) et essaie Vulkan en premier. Minecraft conserve son repli OpenGL si
|
||||
Vulkan ne peut pas être initialisé. Un choix **OpenGL explicite** reste
|
||||
disponible ; pour une installation ainsi réglée, sélectionner Vulkan dans
|
||||
les réglages vidéo puis redémarrer. Aucun moteur externe ni VulkanMod ajouté.
|
||||
- 80 % est la nouvelle intensité PBR initiale. Les valeurs déjà enregistrées
|
||||
explicitement restent conservées.
|
||||
|
||||
Le code Minecraft 26.3 a été inspecté : `PreferredGraphicsApi.VULKAN` essaie
|
||||
Vulkan puis OpenGL ; le choix natif `DEFAULT` essayait auparavant OpenGL d'abord.
|
||||
Tous les effets Sanctuary utilisent déjà l'API graphique native. Le seul hook
|
||||
OpenGL direct est le correctif spécifique de lecture des panoramas dans
|
||||
`GlCommandEncoder` ; il ne remplace pas la voie Vulkan native.
|
||||
|
||||
Référence pour les lectures après contrôle divergent :
|
||||
[documentation Khronos des samplers](https://wikis.khronos.org/opengl/Sampler_%28GLSL%29).
|
||||
La correction rend ces lectures explicites ; elle ne constitue pas à elle seule
|
||||
une reproduction du défaut sur le pilote Windows de l'utilisateur.
|
||||
|
||||
## Observations de diagnostic
|
||||
|
||||
Le détecteur de points blancs isolés (minimum RGB 170, au moins cinq voisins
|
||||
sur huit sous 120) compte 669 points sur l'image utilisateur et zéro sur la
|
||||
première grotte de contrôle. Il s'agit de scènes, résolutions et pilotes différents,
|
||||
pas d'un avant/après reproduisant exactement le bug utilisateur. Les rendus
|
||||
natifs OpenGL de contrôle étaient déjà exempts de ce grain avant les nouvelles
|
||||
protections du shader ; le signalement Windows reste donc à reconfirmer là-bas.
|
||||
|
||||
Le test de rafraîchissement conserve les mêmes maillages publiés durant douze
|
||||
invalidations consécutives, compare l'image et vérifie que des remplacements
|
||||
aboutissent. Le test de torche alterne quatre fois les torches normales/des âmes
|
||||
sans reconstruction des normales. Le contrôle de mouvement presse réellement
|
||||
Avancer, maintient la torche et le PBR actifs et vérifie un déplacement d'au moins
|
||||
un bloc avant de vérifier les 24 captures. Les angles -30°, 0° et 35° sont aussi
|
||||
contrôlés, ainsi que les effets combinés et les préférences.
|
||||
|
||||
### Domaine invalide du reflet de la torche en main
|
||||
|
||||
Pour la lumière du joueur en vue subjective, la source est la caméra : le
|
||||
vecteur lumière et le vecteur de vue sont parallèles. L'ancien
|
||||
`pow(1 - max(dot(v,h),0),5)` n'avait aucune borne supérieure pour ce produit
|
||||
scalaire. Un calcul reproductible Float32 (100 000 positions, graine 142)
|
||||
donne 19 991 bases légèrement négatives, jusqu'à -2,3841858e-7, contre zéro
|
||||
après clamp. C'est une reproduction numérique d'un **domaine de calcul invalide**,
|
||||
pas une reproduction des pixels produits par le pilote utilisateur.
|
||||
|
||||
GLSL 3.30 ne définit pas `pow` pour une base négative, même avec un exposant
|
||||
entier : [spécification Khronos, fonctions exponentielles](https://registry.khronos.org/OpenGL/specs/gl/GLSLangSpec.3.30.pdf).
|
||||
Le correctif borne les cosinus à [0,1] et remplace cette puissance par des
|
||||
multiplications. La torche alignée sur la caméra ne dépend donc plus du traitement
|
||||
de cette entrée invalide par le pilote. Reçu numérique ignoré :
|
||||
`build/pbr142-fresnel-domain.json`.
|
||||
|
||||
### Mise à jour des fixtures antérieures
|
||||
|
||||
La suite Vulkan complète s'est arrêtée au contrôle SSGI de coin : l'oracle
|
||||
ne trouvait plus de pierre neutre (`rows=0`), car les lumières colorées ajoutées
|
||||
depuis beta.139/140 teintaient la pierre avec la lave. Le test de proximité SSGI
|
||||
et le test PBR solaire isolent désormais leur effet en coupant cette autre passe.
|
||||
Aucun seuil n'a été diminué et aucun changement de rendu SSGI n'est livré.
|
||||
Le sous-ensemble en échec est relancé séparément ; les étapes précédentes
|
||||
(ombres, CTM, bloom, lens flare, déplacement SSGI) avaient réussi.
|
||||
|
||||
## Vérifications natives
|
||||
|
||||
Sur Mac Apple M1, Java 25 et Minecraft 26.3 :
|
||||
|
||||
- OpenGL : suite couleurs/directions/rafraîchissement en 3 min 36 s, puis
|
||||
déplacement continu avec torche active en 1 min 36 s. Zéro point blanc isolé
|
||||
sur les 24 images de mouvement et les trois angles de grotte.
|
||||
- Vulkan/MoltenVK 1.4.2 : sélection automatique sans argument de moteur,
|
||||
couleurs/directions/rafraîchissement en 3 min 47 s. Dernier test continu avec
|
||||
torche active en 1 min 37 s, zéro point blanc isolé sur les 24 images.
|
||||
- Ombrages lune/midi/feuilles, distances et précisions, stabilité temps réel,
|
||||
CTM, bloom (minerais, torche, lampe, lave, soleil), émissifs indépendants et
|
||||
lens flare validés sous Vulkan dans la suite d'effets. Le déplacement SSGI
|
||||
sur douze blocs passe également. La suite s'arrête ensuite sur l'oracle
|
||||
ancien de couleur de pierre décrit ci-dessus : ne pas présenter ce lancement
|
||||
de 7 min 15 s comme une réussite complète.
|
||||
- Reprise ciblée après isolation de cet oracle : SSGI près des surfaces puis
|
||||
PBR extérieur réussis sous Vulkan en 2 min 55 s. Proximité 35/100, haute
|
||||
résolution, léger mouvement, toit ajouté/retiré, reload, resize, zéro/OFF et
|
||||
effets combinés. Gain SSGI au coin test : 6,01 sur 99 lignes à 35 %.
|
||||
- Les nouvelles valeurs initiales et les choix enregistrés sont testés, dont
|
||||
une intensité PBR explicitement enregistrée à 50 qui reste à 50.
|
||||
|
||||
Les premiers essais du nouveau contrôle de mouvement ont permis de corriger
|
||||
son entrée clavier : `pressKey` ne maintient pas une touche enfoncée. Le test
|
||||
final utilise `holdKey` puis `releaseKey`, vérifie le déplacement réel et refuse
|
||||
la disparition de la lumière portée ou du PBR pendant les captures.
|
||||
|
||||
Les jeux de test utilisent uniquement des mondes neufs et ignorés. La capture
|
||||
et le pilote Windows utilisateur n'ont pas été reproduits à l'identique ; leur
|
||||
retest reste ouvert malgré les corrections numériques et les contrôles natifs.
|
||||
|
||||
## Construction et archives
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
réussit en **2 min 38 s**, 126 tâches. Le GameTest serveur dédié reste exclu
|
||||
selon le refus EULA antérieur ; les suites clientes ci-dessus ont réellement
|
||||
utilisé les deux moteurs natifs. `git diff --check` passe.
|
||||
|
||||
Les archives normale/Test embarquent le même JAR, leurs versions/index/hashes
|
||||
sont vérifiés, leurs sources correspondent aux fichiers courants. Le template
|
||||
contient exactement les ressources attendues. Les onze entrées de production
|
||||
modifiées par rapport à beta.141 sont limitées au PBR, au choix graphique et
|
||||
aux traductions ; les ressources de jeu antérieures sont conservées.
|
||||
|
||||
SHA-256 du JAR : `0bc2da9df2ef004e6dc78932cfde7b31b7717c63c83ceed789d151177c41bc1a`.
|
||||
|
||||
- `Sanctuary-beta.142.mrpack` : 10464746 octets ; SHA-256 `dba4fd8e1e3b20136faa1c1de3494df3cb250c87de199c04e07883c786ca48ee`.
|
||||
- `Sanctuary-Test-beta.142.mrpack` : 10483672 octets ; SHA-256 `d56b81a4aea6649a40a3c49dedee904989943a518ad0ae7c520b808dd0f9b9a7`.
|
||||
|
||||
Reçu détaillé ignoré : `build/shader142-artifact.json`.
|
||||
|
||||
## Livraison réalisée
|
||||
|
||||
[Release beta.142](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.142)
|
||||
publiée depuis `ea151dd5df8c7c5abd0107656f40c723efbf2ebe`. Canal stable :
|
||||
`d661260e2bbabb0326c86d6cc543daf986979596`. Archives normale/Test, ZIP d'amorçage Prism et template
|
||||
publiés avec vérification de leurs téléchargements ; tag et artefacts immuables.
|
||||
|
||||
Deux synchronisations isolées puis deux synchronisations de l'instance
|
||||
**Sanctuary Beta** réussies. Les 923 fichiers personnels suivis ont conservé
|
||||
leurs hashes, notamment mondes, réglages et packs personnels. Aucun monde
|
||||
personnel ouvert. Sauvegarde préalable : `sanctuary-backups/before-beta.142/`.
|
||||
L'instance locale conserve son réglage graphique natif « default », qui démarre
|
||||
désormais avec Vulkan prioritaire. Un Windows explicitement réglé sur OpenGL
|
||||
doit sélectionner Vulkan dans les réglages vidéo et redémarrer.
|
||||
|
||||
Archives locales également dans `sanctuary-beta/build/`. Le checkout de travail
|
||||
historique modifié `sanctuary-beta` n'a pas été réinitialisé ; seules ces archives
|
||||
ignorées y ont été déposées. Sources sur `codex/pbr-refresh-beta142`.
|
||||
+221
@@ -0,0 +1,221 @@
|
||||
# PBR-SSR — eau, verre et reflets pixélisés
|
||||
|
||||
Livraison locale **beta.145**, branche `codex/pbr-ssr`. Socle PBR beta.142 /
|
||||
suivi beta.143 et copie figée des sources du chantier d’eau beta.144. Minecraft 26.3, API graphique native.
|
||||
|
||||
## Contrat
|
||||
|
||||
Les reflets colorent les texels de matière sur une grille 16 × 16 liée aux UV,
|
||||
avec les normales procédurales existantes. Eau et verre sont diélectriques.
|
||||
L’eau reprend les faces du tessellateur natif (niveaux, pentes, écoulements,
|
||||
waterlogging), sans modification de géométrie ni simulation de vagues.
|
||||
Le verre réfléchit aussi sur les zones sans pigment de sa texture. Les pixels
|
||||
pigmentés conservent leur relief même avec un alpha faible ; l’alpha nul
|
||||
utilise la normale géométrique.
|
||||
|
||||
Les images des normales d’eau sont préparées au chargement du matériau ; leur
|
||||
séquence suit l’horloge effective des animations de l’atlas, avec interpolation.
|
||||
Seules les petites tuiles de normales changent sur le GPU, pas le maillage.
|
||||
Les normales des textures haute résolution sont rééchantillonnées à 16 × 16.
|
||||
Au-delà de 1 024 images par sprite, un avertissement signale le repli vers
|
||||
une normale fixe pour borner la mémoire.
|
||||
|
||||
L’option FR/EN « Reflets du paysage — expérimental » est OFF initialement,
|
||||
intensité 50 %, bornée entre 0 et 100. Elle exige le PBR actif. L’eau naturelle
|
||||
reste indépendante pour ses couleurs, sa transparence et sa brume.
|
||||
|
||||
## Pipeline et limites
|
||||
|
||||
Couleur et profondeur du paysage sont copiées après les opaques et avant les
|
||||
transparences, dans les deux chemins natifs. Le chemin classique ferme la passe
|
||||
opaque avant la copie, puis reprend la transparence native dans une nouvelle
|
||||
passe. Aucune copie d’attachement Vulkan encore ouvert.
|
||||
|
||||
Une profondeur propre sélectionne le récepteur PBR le plus proche devant le
|
||||
paysage opaque. Une vitre devant l’eau reçoit seule le SSR ; les couches
|
||||
suivantes conservent leur rendu natif. La profondeur du fond n’est pas utilisée
|
||||
comme profondeur de surface de l’eau.
|
||||
|
||||
8 à 32 pas déterministes adaptés à la longueur écran, puis quatre raffinements maximum, récepteurs jusqu’à
|
||||
32 blocs et rayons jusqu’à 128 blocs. Pas de bruit aléatoire, historique,
|
||||
réflexion récursive, réfraction nouvelle, lecture GPU vers CPU ou ajout de
|
||||
lumière de gameplay. Les objets hors écran ou masqués ne sont pas retrouvés. Les détails fins entre
|
||||
deux pas peuvent être manqués ; une interpolation finale affine le point
|
||||
d’intersection sans augmenter le nombre de recherches.
|
||||
Un échec revient progressivement au rendu existant ; les transparences et les
|
||||
effets Sanctuary postérieurs à la capture ne figurent pas dans le reflet.
|
||||
|
||||
Les reflets sont composés après le SSGI, avant contours, bloom et rayons.
|
||||
La main et le HUD restent en dehors du traitement. Le PBR conserve son
|
||||
périmètre Overworld hors immersion et s’efface avec les moteurs externes.
|
||||
|
||||
Budget de maillages : 24 Mio de solides, 8 Mio supplémentaires de transparences.
|
||||
Deux profondeurs plein écran servent aux surfaces PBR ; la couleur de paysage
|
||||
SSR est allouée seulement à ON/intensité positive. La copie de composition PBR
|
||||
existante est réutilisée. Aucun buffer de normales plein écran.
|
||||
La grille visuelle ne signifie pas qu’un seul fragment GPU est exécuté par texel.
|
||||
|
||||
## Intégration
|
||||
|
||||
Le commit `38f4d6c` apporte le SSR et les récepteurs transparents. Le commit
|
||||
`0cfd51e` importe une copie contrôlée des sources beta.144 depuis le chantier
|
||||
`codex/water-beta144`, fondé sur `580b241`. Les empreintes des fichiers ont été
|
||||
comparées avant et après la copie ; le reçu local est dans
|
||||
`build/water144-snapshot/manifest.json`. Cette intégration ne prétend pas que
|
||||
beta.144 a été publiée ou que son benchmark isolé est terminé. Le code de
|
||||
production de cet instantané est identique à celui du commit
|
||||
`7eef945` ensuite créé sur la branche d’eau. Les essais ci-dessous portent
|
||||
sur les sources réunies dans cette branche.
|
||||
|
||||
Points d’entrée : `PbrMaterials` et `PbrAnimation` pour les normales animées,
|
||||
`PbrFluidMesh` pour le tessellateur natif, `PbrTerrain` pour les deux budgets,
|
||||
`ReflectionSceneMixin` pour la capture opaque, `PixelReflections` et
|
||||
`pixel_reflections.fsh` pour la sélection et le rayon. Les horloges de sprites
|
||||
sont référencées faiblement des deux côtés afin de ne pas retenir les anciens
|
||||
atlas après rechargement.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Machine : Apple M1, Minecraft 26.3, Java 25, macOS. Mondes plats de test neufs,
|
||||
graine 122 ; aucune sauvegarde personnelle utilisée. Les captures conservent
|
||||
le préfixe historique `sanctuary-beta142-shader` du banc de test.
|
||||
|
||||
- Client OpenGL 4.1 Metal 90.5, transparence classique : suite SSR réussie avec les sources d’eau
|
||||
intégrées (`build/ssr145-opengl-final.log`).
|
||||
- Client Vulkan/MoltenVK : suite SSR avec transparence améliorée réussie
|
||||
(`build/ssr145-vulkan-final.log`).
|
||||
- Régression du PBR historique et nouvelles assertions de profils, normales
|
||||
transparentes, persistance, bornes et traductions SSR : réussies
|
||||
(`build/ssr145-pbr-regression-retry.log`). Une première tentative interrompue
|
||||
s’était arrêtée sur un timeout de sémaphore Vulkan ; elle reste un échec
|
||||
consigné dans `build/ssr145-pbr-regression.log`.
|
||||
- Suite fonctionnelle eau beta.144 : réussie, y compris profils, fond modifié,
|
||||
brume, océan, rechargement, préférences, cascades et pack de matériaux
|
||||
(`build/ssr145-water-functional.log`). Son long benchmark autonome a été
|
||||
explicitement sauté ; le coût SSR est mesuré séparément ci-dessous.
|
||||
- Le relief rouge puis bleu produit deux contributions de couleur distinctes
|
||||
dans le bassin ; caché par un mur opaque, ce changement ne traverse pas le mur.
|
||||
- Eau animée sans nouvelle analyse de texture, verre transparent devant l’eau,
|
||||
métal, déplacement minime de caméra, rotation, OFF/zéro, immersion,
|
||||
rechargement de ressources et redimensionnement sont exercés par assertions.
|
||||
L’horloge d’eau reste active après rechargement ; les profils sable et étang
|
||||
restent corrects avec PBR et SSR simultanément actifs.
|
||||
- Des captures couvrent également les effets combinés, la lune, les angles
|
||||
rasants, la translation de caméra, la sortie du paysage hors écran, la pierre
|
||||
rugueuse OFF/ON, des vitres colorées superposées, un écoulement, une cascade
|
||||
et une dalle gorgée d’eau. Elles ne remplacent pas une inspection
|
||||
artistique de tous les angles et de tous les packs.
|
||||
|
||||
Captures locales OpenGL : [OFF](../build/ssr-evidence/beta145-opengl-final/0000_sanctuary-beta142-shader-ssr-water-off.png),
|
||||
[ON](../build/ssr-evidence/beta145-opengl-final/0001_sanctuary-beta142-shader-ssr-water-on.png).
|
||||
Le dossier `build/ssr-evidence/beta145-opengl-final/` contient les autres captures.
|
||||
Les mêmes cas Vulkan sont conservés dans
|
||||
`build/ssr-evidence/beta145-vulkan-final/`. Les comparaisons eau, métal et
|
||||
verre empilé ont été inspectées visuellement. Ces pièces sont ignorées par Git.
|
||||
|
||||
## Coût mesuré
|
||||
|
||||
1920 × 1080, PBR à 80 %, SSR à 100 % pour accentuer le résultat, VSync OFF,
|
||||
limite de FPS et réduction AFK désactivées. Chaque scène chauffe 30 ticks par
|
||||
état avant 80 ticks de mesure, soit 324 à 483 images pour OpenGL et 317 à 477 pour Vulkan. Ordre OFF puis
|
||||
ON ; il s’agit d’un diagnostic local court, pas d’une campagne statistique.
|
||||
Le temps de frame mesure `Minecraft.renderFrame`, présentation comprise.
|
||||
|
||||
| OpenGL | Médiane OFF / ON (ms) | p95 OFF / ON (ms) | Écart médian |
|
||||
| --- | --- | --- | --- |
|
||||
| Petit étang | 7,57 / 9,42 | 10,35 / 12,50 | +1,85 ms |
|
||||
| Eau dominante | 7,76 / 11,67 | 10,67 / 13,96 | +3,91 ms |
|
||||
| Construction vitrée | 7,85 / 10,12 | 10,67 / 12,65 | +2,27 ms |
|
||||
|
||||
| Vulkan | Médiane OFF / ON (ms) | p95 OFF / ON (ms) | Écart médian |
|
||||
| --- | --- | --- | --- |
|
||||
| Petit étang | 7,97 / 10,36 | 8,97 / 11,49 | +2,40 ms |
|
||||
| Eau dominante | 10,10 / 12,23 | 10,97 / 12,97 | +2,13 ms |
|
||||
| Construction vitrée | 8,52 / 11,25 | 9,49 / 12,20 | +2,72 ms |
|
||||
|
||||
La mesure GPU Vulkan autour de `PixelReflections.render` donne, à ON,
|
||||
**2,07 / 2,18 ms** (médiane / p95) pour l’étang, **2,56 / 2,63 ms** pour
|
||||
l’eau dominante et **2,09 / 2,21 ms** pour le verre. À OFF, la sonde mesure
|
||||
0,03 ms environ. Ce périmètre couvre la copie de composition et le dessin
|
||||
SSR ; il exclut la capture opaque antérieure et la profondeur partagée PBR.
|
||||
La sonde utilise les valeurs rendues par la requête native, pas une mesure
|
||||
GPU du jeu complet.
|
||||
|
||||
La requête GPU native ne fournit pas de mesure exploitable sur ce pilote
|
||||
OpenGL ; ce champ est indisponible, pas nul. Les premiers essais contaminés
|
||||
par la réduction AFK à 30 FPS sont exclus des résultats retenus.
|
||||
**L’objectif de surcoût médian inférieur à 2 ms n’est pas respecté dans l’ensemble des
|
||||
scènes.** L’option reste expérimentale et désactivée par défaut.
|
||||
|
||||
À 1080p : profondeurs partagées par le PBR = 15,82 Mio ; copie couleur SSR
|
||||
supplémentaire = 7,91 Mio, libérée à OFF/zéro. Total de ces cibles à ON :
|
||||
23,73 Mio. Les maillages observés occupent environ 0,51–0,55 Mio de solides et
|
||||
1,09–1,21 Mio de transparences, en dessous des plafonds 24 / 8 Mio vérifiés
|
||||
par assertion. Ces compteurs n’incluent ni la mémoire du moteur, ni la copie
|
||||
PBR existante, ni les atlas de matériaux de 6,25 Mio chacun ; ce ne sont pas
|
||||
des mesures de mémoire totale du processus.
|
||||
|
||||
## Construction et contrôles généraux
|
||||
|
||||
`./gradlew check build` a été exécuté jusqu’au bout : **229 GameTests serveur
|
||||
réussis sur 252, 23 échecs**, en 8 min 45 s pour la commande complète.
|
||||
Les échecs concernent les tombes/familiers, le catalogue, deux invariants de
|
||||
génération, les placements de blocs/progression/construction et les menus
|
||||
natifs. Les tests concernés et le code de gameplay serveur ne sont pas
|
||||
modifiés par ce lot. Aucun contre-test de toute la branche de départ n’est
|
||||
revendiqué. Le journal complet est `build/ssr145-check-build.log` ; le relevé
|
||||
exhaustif est `build/ssr145-server-failures.json`.
|
||||
|
||||
L’assemblage est repris avec `./gradlew check build assemblePack assembleTestPack
|
||||
-x :sanctuary:runGameTest`, en excluant uniquement cette tâche déjà exécutée.
|
||||
Cette commande réussit en 2 min 56 s (126 tâches) ; vérification du pack,
|
||||
compilation, contrôles hors GameTests serveur et assemblages passent. Les
|
||||
exports MRpack sont produits depuis leurs dossiers packwiz de staging.
|
||||
L’intégrité ZIP, les métadonnées, les traductions et mixins SSR ainsi que
|
||||
l’identité du JAR embarqué dans les deux packs sont contrôlées.
|
||||
Cette exclusion ne transforme pas le contrôle complet en succès.
|
||||
|
||||
## Reproduire les essais client
|
||||
|
||||
Base de chaque commande :
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runClientGameTest \
|
||||
-PsanctuaryClientTests=true -PsanctuaryQuickTests=true \
|
||||
-PsanctuaryPbr134ClientTests=true -PsanctuaryClientNoVsync=true
|
||||
```
|
||||
|
||||
- SSR Vulkan : ajouter `-PsanctuarySsrClientTests=true
|
||||
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryClientImprovedTransparency=true`.
|
||||
- SSR OpenGL : ajouter `-PsanctuarySsrClientTests=true
|
||||
-PsanctuaryClientGraphicsBackend=opengl -PsanctuaryClientImprovedTransparency=false`.
|
||||
- Régression PBR : ajouter uniquement `-PsanctuaryClientGraphicsBackend=vulkan`.
|
||||
- Profils et rendu de l’eau : ajouter `-PsanctuaryWater144ClientTests=true
|
||||
-PsanctuaryWater144FunctionalOnly=true -PsanctuaryClientGraphicsBackend=vulkan`.
|
||||
|
||||
Le mode fonctionnel saute uniquement le long benchmark autonome beta.144.
|
||||
Les sondes de temps de frame/GPU et lectures d’images appartiennent au mod
|
||||
de test ; elles ne sont pas embarquées dans le JAR de production.
|
||||
|
||||
## Artefacts locaux
|
||||
|
||||
Les archives finales sont rafraîchies depuis le JAR testé, puis revérifiées
|
||||
(`build/ssr145-artifacts.json`). Aucun tag ni canal n’est publié.
|
||||
|
||||
| Artefact | Octets | SHA-256 |
|
||||
| --- | ---: | --- |
|
||||
| [sanctuary-beta.145.jar](../mods/sanctuary/build/libs/sanctuary-beta.145.jar) | 11369411 | `55341e2f5c8dcc352190fe96d15e86cd1ee6bb4ea2908441b70491d48f86f608` |
|
||||
| [Sanctuary-beta.145.mrpack](../build/Sanctuary-beta.145.mrpack) | 10508045 | `8bebe6227cbfe629c714a7c7547640ab5ccfbefaaf211484a93c042cfb42b5c5` |
|
||||
| [Sanctuary-Test-beta.145.mrpack](../build/Sanctuary-Test-beta.145.mrpack) | 10526966 | `54402ff8414ed1ae0d12b3b97af70c547c44dde2f3aa183712ae3769f99efbe7` |
|
||||
| [Sanctuary-Template-beta.145.zip](../build/Sanctuary-Template-beta.145.zip) | 4358316 | `226d92362a5ce922b8757b36f30c312081aca5cf73a299b82a221a12f384a95b` |
|
||||
|
||||
## Limites de validation
|
||||
|
||||
Le banc utilise un relief construit reproductible, pas une montagne générée
|
||||
représentative de tous les biomes. La validation artistique sur terrain généré,
|
||||
les textures personnalisées avec ordre/durées/interpolation atypiques et la
|
||||
stabilité sur un long trajet restent à compléter. Les surfaces peuvent perdre les petits détails entre
|
||||
les pas SSR et les zones hors écran ne sont jamais reconstruites. Windows,
|
||||
autres GPU et moteurs graphiques externes ne sont pas validés ici.
|
||||
|
||||
Aucune publication de canal ni synchronisation Prism dans cette livraison.
|
||||
@@ -0,0 +1,68 @@
|
||||
# SNOW-143 — Neige saisonnière en temps réel
|
||||
|
||||
Branche `codex/realtime-snow-melt-beta143`, socle beta.142. Minecraft 26.3 /
|
||||
Java 25. Livraison beta.143.
|
||||
|
||||
## Comportement
|
||||
|
||||
L'accumulation et la fonte saisonnière conservent le sampler natif de
|
||||
précipitation `ServerLevel.tickPrecipitation`. Sanctuary ne scanne donc toujours
|
||||
pas les chunks et ne force aucun chargement de terrain. Le changement porte
|
||||
seulement sur la probabilité des mutations de neige.
|
||||
|
||||
Pendant une chute de neige, Sanctuary ajoute désormais une couche une fois sur
|
||||
12 échantillons natifs en moyenne. La neige reste visible pendant une session,
|
||||
mais les colonnes ne montent plus instantanément. À `randomTickSpeed = 3`, une
|
||||
couche apparaît en moyenne après environ 40 minutes de neige continue.
|
||||
|
||||
Pour la fonte, chaque tirage saisonnier est multiplié par `72`, le rapport
|
||||
entre une journée réelle de 24 heures et une journée Minecraft de 20 minutes.
|
||||
|
||||
Une couche fond désormais en moyenne en 4 h 05 en été,
|
||||
8 h 10 au printemps et 16 h 25 en automne. Avec le plafond actuel de deux blocs,
|
||||
une colonne météo suivie met environ 2,7 jours réels à disparaître en été,
|
||||
5,4 jours au printemps et 11 jours en automne. L'hiver ne fond toujours pas.
|
||||
|
||||
Les règles de protection restent celles de beta.109 : seule la neige déposée
|
||||
par la météo Sanctuary depuis l'ajout du suivi `sanctuary:natural_snow_v1`
|
||||
peut fondre. La neige construite, l'ancienne neige non suivie, les couches
|
||||
manuelles, les biomes froids, les profils neigeux et la météo Vanilla restent
|
||||
préservés. Aucun format de sauvegarde ne change.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Vérifié le 18 septembre 2026 sur macOS, Java 25.
|
||||
|
||||
- `./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true
|
||||
-PsanctuaryMelt109ClientTests=true -PsanctuaryClientNoVsync=true
|
||||
-PsanctuaryQuickTests=true` : réussi en 57 s, avec le marqueur
|
||||
`MELT109_NATIVE_PASS`. Le test couvre l'accumulation beta.108 ralentie,
|
||||
la fonte couche par couche, la provenance persistante, les saisons, la
|
||||
gamerule, la météo Vanilla, les profils neigeux, les biomes froids et le
|
||||
rechargement de monde.
|
||||
- `./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest` :
|
||||
réussi en 2 min 31 s, 126 tâches. Le GameTest serveur dédié reste exclu
|
||||
conformément au refus antérieur de son EULA ; les contrôles natifs utilisent
|
||||
le serveur intégré.
|
||||
- `python3 scripts/pack.py check` : versions, icône et index packwiz vérifiés.
|
||||
- Exports MRpack produits avec `packwiz modrinth export`, intégrité ZIP vérifiée.
|
||||
Les deux MRpack embarquent exactement le JAR Sanctuary construit.
|
||||
|
||||
| Artefact | Taille | SHA-256 |
|
||||
| --- | ---: | --- |
|
||||
| `sanctuary-beta.143.jar` | 11 323 665 octets | `fd4b663ac2b2a106624c42b09997db229cc14d6c39d44ae83079bc6aa9d39a08` |
|
||||
| `Sanctuary-beta.143.mrpack` | 10 464 931 octets | `e55caaa41c5a3a2666ff5415846e18f44d504ac34b698f3d974091cfb52364c5` |
|
||||
| `Sanctuary-Test-beta.143.mrpack` | 10 483 858 octets | `b14bca04c4a48771209d0f7e234ff9ef6b800eb236fb969a6f0dfcee5620dafc` |
|
||||
| `Sanctuary-Template-beta.143.zip` | 4 354 153 octets | `071fd8da67d1d0224b89889a5fcbc4cd269673dddb7a52f89aa78802f154605a` |
|
||||
|
||||
## Publication
|
||||
|
||||
[Release beta.143](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.143)
|
||||
publiée depuis le commit `97ff2b54cb63afe8fdd88bfa8f2ebb25dbe3b86d`.
|
||||
Le canal packwiz stable pointe sur
|
||||
`bafff5e9fcdd95c5dd184953a34ada805e455797`.
|
||||
|
||||
Deux synchronisations isolées, puis deux synchronisations dans l'instance
|
||||
Sanctuary Beta ont réussi. Les 923 fichiers personnels suivis gardent leurs
|
||||
hashes ; aucun monde personnel n'a été ouvert. Sauvegarde préalable :
|
||||
`sanctuary-backups/before-beta.143/`.
|
||||
@@ -0,0 +1,112 @@
|
||||
# RENDER-146 — Reflets, portée commune et transitions de l’eau
|
||||
|
||||
Branche `codex/reflections-water-beta146`, socle beta.145 `ed2d088`.
|
||||
Minecraft 26.3, Java 25 et dépendances inchangés. Préparation locale uniquement.
|
||||
|
||||
## Contrat
|
||||
|
||||
- Les passes PBR et SSR vérifient la profondeur finale du rendu natif pour
|
||||
masquer les personnages, notamment en F5. La profondeur du paysage capturée
|
||||
avant les transparences reste celle utilisée pour rechercher le reflet.
|
||||
- Le PBR utilise le test matériel EQUAL contre la profondeur du récepteur
|
||||
sélectionné. L’ancienne tolérance de 3,5 cm pouvait accepter des faces
|
||||
concurrentes près des coins ; elle est supprimée. La comparaison avec la
|
||||
profondeur native conserve seulement une marge de 5 mm pour les différences
|
||||
d’ordre de calcul des matrices, sans relâcher la sélection stricte du récepteur.
|
||||
La position du vertex est déclarée invariante entre les passes.
|
||||
- PBR à 80 % et SSR à 20 % par défaut, pour toutes les matières, eau comprise.
|
||||
Deux intensités d’effets distinctes, sans réglage séparé solide/liquide.
|
||||
Les préférences explicites déjà enregistrées restent conservées.
|
||||
- Distance PBR/SSR commune : 16, 32 (défaut), 64, 128 blocs. Maillages,
|
||||
sélection des récepteurs et fondu final emploient ce réglage. Le rayon SSR
|
||||
conserve sa recherche maximale de 128 blocs dans le paysage visible.
|
||||
Budgets de maillages inchangés : solides 24 Mio, transparences 8 Mio.
|
||||
Une forte densité à grande portée peut donc limiter les surfaces disponibles.
|
||||
- Le reflet du soleil adopte un lobe plus large sur l’eau, avec une irradiance
|
||||
renforcée : grande empreinte blanche vue du dessus et traînée étendue aux
|
||||
angles rasants. Les lampes conservent leur lobe local.
|
||||
Les éclats spéculaires ne sont plus plafonnés au gris moyen. Leur compression
|
||||
conserve une valeur finie pouvant approcher le blanc. Une contribution douce
|
||||
des seules passes PBR/SSR alimente le bloom existant, sans faire briller tous
|
||||
les blocs blancs. Le réglage Bloom et son intensité gardent autorité.
|
||||
- Eau naturelle OFF par défaut dans une dimension ordinaire, ON pour une
|
||||
dimension dont le générateur est Sanctuary / ExpansionChunkGenerator.
|
||||
Le serveur transmet ce contexte à la connexion, au changement de dimension
|
||||
et à la réapparition, y compris en multijoueur. Le défaut automatique nécessite
|
||||
ce contexte envoyé par un serveur beta.146 ; sans lui, le client utilise le
|
||||
défaut ordinaire OFF, toujours réglable. Aucune donnée de monde modifiée.
|
||||
Les choix ON/OFF sont mémorisés séparément pour ces deux catégories.
|
||||
Un ancien OFF explicite est conservé dans les deux ; un ancien ON global
|
||||
adopte les nouveaux défauts par contexte.
|
||||
- Mélange des profils sur 1×1, 3×3, 5×5, 9×9 (défaut) ou 17×17 blocs, puis
|
||||
interpolation aux sommets. Océans toujours clairs. La texture demeure native.
|
||||
Surface et immersion utilisent la même étendue. Les changements du fond
|
||||
invalident toute la largeur dépendante ; changer le réglage reconstruit les
|
||||
sections chargées contenant de l’eau.
|
||||
|
||||
Le filtre de surface est calculé dans la copie de région native, avec sommes
|
||||
préfixées par hauteur pour borner le coût des sommets. Aucun accès au monde
|
||||
vivant depuis le worker, chargement de chunk ou modification de sauvegarde.
|
||||
Les textes des nouvelles options sont fournis en français et en anglais.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Machine : Apple M1, macOS, Java 25, Minecraft 26.3. Mondes plats jetables,
|
||||
graine 122 ; aucune sauvegarde personnelle utilisée.
|
||||
|
||||
- Suite SSR/PBR Vulkan avec transparence améliorée : réussie
|
||||
(`build/beta146-final-settings-vulkan.log`). F5 avec personnage réel en créatif,
|
||||
portée 16/32/64/128, couleur du paysage, eau/verre/métal, OFF/zéro,
|
||||
immersion, rechargement de ressources et redimensionnement.
|
||||
- Suite SSR/PBR OpenGL finale réussie également
|
||||
(`build/beta146-final-settings-opengl.log`), avec 22 450 pixels quasi blancs
|
||||
dans la scène solaire et 99 / 518 400 pixels modifiés dans le contrôle
|
||||
des intersections. Captures dans `build/evidence/final-settings-opengl/`.
|
||||
- F5 : les 1 900 pixels du torse contrôlés sont strictement identiques entre
|
||||
SSR OFF et ON, tandis que le reflet reste présent autour du personnage.
|
||||
- Soleil : 22 111 pixels quasi blancs sur 854 × 480 à PBR 80 % / SSR 20 %,
|
||||
sans bloom, sur la scène de contrôle Vulkan. Les captures vues du dessus et
|
||||
à soleil bas montrent une empreinte large et une traînée allongée.
|
||||
Bloom OFF libère sa cible.
|
||||
- Mur, sol et escaliers en contact : 86 pixels sur 518 400 changent de plus
|
||||
de 12 niveaux cumulés lors d’une rotation de 0,00002 degré ; seuil du test
|
||||
0,1 %. Le cadre exclut l’eau animée, qui ne constitue pas une référence
|
||||
de stabilité pour les blocs statiques.
|
||||
|
||||
- Eau : suites fonctionnelles Vulkan et OpenGL réussies
|
||||
(`build/beta146-water-vulkan.log`, `build/beta146-water-opengl.log`).
|
||||
Profils, mélange 5/9/17, changement de contexte transmis par le serveur,
|
||||
préférences séparées, immersion, fond modifié, océans, eau courante,
|
||||
waterlogging et rechargement. OpenGL contrôle aussi l’égalité des couleurs
|
||||
aux frontières des copies de région et avec l’échantillonnage de brume.
|
||||
- `./gradlew check build` exécuté dans un instantané isolé des sources :
|
||||
252 tests serveur, 229 réussis et 23 échecs. Les identifiants des échecs
|
||||
correspondent exactement au reçu archivé beta.145 ; aucun nouvel identifiant.
|
||||
Il ne s’agit pas d’une nouvelle exécution comparative de beta.145.
|
||||
Journal : `build/beta146-check-build-isolated.log` ; comparaison :
|
||||
`build/beta146-server-failures.json`. Les derniers changements après cet
|
||||
instantané concernent le rendu client, ses réglages et ses tests.
|
||||
|
||||
Les preuves Vulkan finales sont dans `build/evidence/final-settings-vulkan/`.
|
||||
La suite de régression PBR et de préférences finale réussit
|
||||
(`build/beta146-final-pbr-preferences.log`) : PBR 80 %, SSR 20 %, persistance,
|
||||
migration des anciennes préférences, FR/EN, OFF/zéro et rechargement.
|
||||
La commande finale `./gradlew check build assemblePack -x :sanctuary:runGameTest`
|
||||
réussit (125 tâches), sans répéter la suite serveur isolée déjà exécutée.
|
||||
Journal : `build/beta146-final-build-pack.log`. Cette exclusion ne transforme
|
||||
pas les 23 échecs serveur en validation globale réussie.
|
||||
|
||||
## Artefact local
|
||||
|
||||
Le pack [Sanctuary-beta.146.mrpack](../build/Sanctuary-beta.146.mrpack) est exporté
|
||||
et copié dans le dossier `sanctuary-beta/build/`, à côté des autres versions.
|
||||
Intégrité ZIP, version beta.146, Minecraft 26.3 et identité du JAR embarqué
|
||||
avec le JAR construit contrôlées ; shaders finaux présents et identiques aux sources.
|
||||
Taille : 10 519 634 octets. SHA-256 :
|
||||
`9f3f7a840652d401741cbe3c85bbb5626326542179754cb599479b01a6b4c955`.
|
||||
Reçu : `build/beta146-artifacts.json`.
|
||||
|
||||
Aucun benchmark isolé de performance de beta.146 n’est revendiqué : des
|
||||
contrôles serveur peuvent tourner simultanément. Les limites SSR hors écran,
|
||||
de maillage à grande portée et de compatibilité avec d’autres GPU subsistent.
|
||||
Aucune publication ou synchronisation Prism.
|
||||
@@ -79,3 +79,18 @@ Reçu local : `build/shader137-artifact.json`.
|
||||
- `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.
|
||||
|
||||
+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.137
|
||||
pack_version=beta.137
|
||||
mod_version=beta.146
|
||||
pack_version=beta.146
|
||||
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,10 @@ 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') }
|
||||
|
||||
+19
-11
@@ -12,6 +12,7 @@ import java.time.Instant;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.BooleanSupplier;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
@@ -50,24 +51,29 @@ public final class Melt109ClientChecks implements FabricClientGameTest {
|
||||
for(int x=-15;x<=15;x++)for(int z=-15;z<=15;z++)l.setBlockAndUpdate(new BlockPos(x,79,z),Blocks.STONE.defaultBlockState());
|
||||
clock(l,"2026-07-10T12:00:00Z",SeasonalWeather.Season.SUMMER);
|
||||
check(l.getGameRules().get(SeasonalSnowMelt.ENABLED),"Melt enabled by default");
|
||||
check(SeasonalSnowMelt.REALTIME_SCALE==72,"Real-time snow melt scale is one real day per 72 native days");
|
||||
check(SeasonalSnowMelt.chance(SeasonalWeather.Season.SUMMER)==72
|
||||
&&SeasonalSnowMelt.chance(SeasonalWeather.Season.SPRING)==144
|
||||
&&SeasonalSnowMelt.chance(SeasonalWeather.Season.AUTUMN)==288
|
||||
&&SeasonalSnowMelt.chance(SeasonalWeather.Season.WINTER)==0,"Seasonal melt chances include the 72x real-time scale");
|
||||
deposit(l,AT,16);check(l.getBlockState(AT.above()).is(Blocks.SNOW_BLOCK),"Default two-block accumulation retained");
|
||||
clear(l);step(l,AT,1);
|
||||
check(layers(l,AT.above())==7&&layers(l,AT)==8,"Summer melts only one top layer");
|
||||
step(l,AT,7);check(l.getBlockState(AT.above()).isAir()&&layers(l,AT)==8,"Top block melts completely before the lower block");
|
||||
step(l,AT,8);check(l.getBlockState(AT).isAir()&&!tracked(l,AT),"Natural column vanishes without water; receipt removed");
|
||||
clear(l);
|
||||
until(l,AT,5000,()->layers(l,AT.above())==7&&layers(l,AT)==8,"Summer melts only one top layer");
|
||||
until(l,AT,20000,()->l.getBlockState(AT.above()).isAir()&&layers(l,AT)==8,"Top block melts completely before the lower block");
|
||||
until(l,AT,20000,()->l.getBlockState(AT).isAir()&&!tracked(l,AT),"Natural column vanishes without water; receipt removed");
|
||||
|
||||
var built=new BlockPos(7,80,4);l.setBlockAndUpdate(built,Blocks.SNOW_BLOCK.defaultBlockState());step(l,built,64);check(layers(l,built)==8,"Untracked snow block preserved");
|
||||
var old=new BlockPos(8,80,4);snow(l,old,5);step(l,old,64);check(layers(l,old)==5,"Untracked old layers preserved");
|
||||
var base=new BlockPos(9,80,4);snow(l,base,3);deposit(l,base,5);clear(l);step(l,base,16);check(layers(l,base)==3&&!tracked(l,base),"Weather above manual layers melts only back to its base");
|
||||
var atop=new BlockPos(9,80,6);l.setBlockAndUpdate(atop,Blocks.SNOW_BLOCK.defaultBlockState());deposit(l,atop,8);clear(l);step(l,atop,16);check(layers(l,atop)==8&&l.getBlockState(atop.above()).isAir(),"Weather on a snow construction melts; construction stays");
|
||||
var base=new BlockPos(9,80,4);snow(l,base,3);deposit(l,base,5);clear(l);until(l,base,20000,()->layers(l,base)==3&&!tracked(l,base),"Weather above manual layers melts only back to its base");
|
||||
var atop=new BlockPos(9,80,6);l.setBlockAndUpdate(atop,Blocks.SNOW_BLOCK.defaultBlockState());deposit(l,atop,8);clear(l);until(l,atop,20000,()->layers(l,atop)==8&&l.getBlockState(atop.above()).isAir(),"Weather on a snow construction melts; construction stays");
|
||||
var replaced=new BlockPos(10,80,4);deposit(l,replaced,8);l.setBlockAndUpdate(replaced,Blocks.AIR.defaultBlockState());l.setBlockAndUpdate(replaced,Blocks.SNOW_BLOCK.defaultBlockState());clear(l);step(l,replaced,32);check(layers(l,replaced)==8&&!tracked(l,replaced),"Break and replacement cannot inherit melt permission");
|
||||
var thickened=new BlockPos(10,80,6);deposit(l,thickened,2);snow(l,thickened,6);clear(l);step(l,thickened,32);check(layers(l,thickened)==6&&!tracked(l,thickened),"Manual layer edit relinquishes natural provenance");
|
||||
var capped=new BlockPos(12,80,6);deposit(l,capped,8);snow(l,capped.above(),1);clear(l);step(l,capped,32);check(layers(l,capped)==8&&layers(l,capped.above())==1,"Manual thin snow cap protects its natural support");
|
||||
var roof=new BlockPos(11,80,4);deposit(l,roof,8);l.setBlockAndUpdate(roof.above(3),Blocks.STONE.defaultBlockState());clear(l);step(l,roof,32);check(layers(l,roof)==8,"Covered snow remains untouched");l.setBlockAndUpdate(roof.above(3),Blocks.AIR.defaultBlockState());step(l,roof,1);check(layers(l,roof)==7,"Uncovered natural snow resumes melt");
|
||||
var roof=new BlockPos(11,80,4);deposit(l,roof,8);l.setBlockAndUpdate(roof.above(3),Blocks.STONE.defaultBlockState());clear(l);step(l,roof,32);check(layers(l,roof)==8,"Covered snow remains untouched");l.setBlockAndUpdate(roof.above(3),Blocks.AIR.defaultBlockState());until(l,roof,10000,()->layers(l,roof)==7,"Uncovered natural snow resumes melt");
|
||||
command(l,"gamerule sanctuary:seasonal_snow_melt false");step(l,roof,32);check(layers(l,roof)==7,"Operator disables melting");command(l,"gamerule sanctuary:seasonal_snow_melt true");
|
||||
clock(l,"2026-01-10T12:00:00Z",SeasonalWeather.Season.WINTER);clear(l);step(l,roof,64);check(layers(l,roof)==7,"Winter never melts even with clear weather");
|
||||
clock(l,"2026-04-10T12:00:00Z",SeasonalWeather.Season.SPRING);clear(l);step(l,roof,64);check(l.getBlockState(roof).isAir(),"Spring progressively melts");
|
||||
deposit(l,roof,8);clock(l,"2026-10-10T12:00:00Z",SeasonalWeather.Season.AUTUMN);clear(l);step(l,roof,128);check(l.getBlockState(roof).isAir(),"Autumn progressively melts");
|
||||
clock(l,"2026-04-10T12:00:00Z",SeasonalWeather.Season.SPRING);clear(l);until(l,roof,30000,()->l.getBlockState(roof).isAir(),"Spring progressively melts");
|
||||
deposit(l,roof,8);clock(l,"2026-10-10T12:00:00Z",SeasonalWeather.Season.AUTUMN);clear(l);until(l,roof,50000,()->l.getBlockState(roof).isAir(),"Autumn progressively melts");
|
||||
deposit(l,roof,8);clock(l,"2026-07-10T12:00:00Z",SeasonalWeather.Season.SUMMER);
|
||||
command(l,"sanctuary weather snow");l.getWeatherData().setRaining(false);l.setRainLevel(0);step(l,roof,64);check(layers(l,roof)==8,"Forced summer snowfall blocks melting");
|
||||
command(l,"sanctuary weather snow_showers");l.getWeatherData().setRaining(false);l.setRainLevel(0);step(l,roof,64);check(layers(l,roof)==8,"Pauses in snow showers also block melting");
|
||||
@@ -84,7 +90,7 @@ public final class Melt109ClientChecks implements FabricClientGameTest {
|
||||
var l=server.overworld();clock(l,"2026-07-10T12:00:00Z",SeasonalWeather.Season.SUMMER);clear(l);
|
||||
check(!l.getGameRules().get(SeasonalSnowMelt.ENABLED),"Melt rule persists across restart");
|
||||
check(layers(l,PERSIST)==8&&layers(l,PERSIST.above())==8&&tracked(l,PERSIST)&&tracked(l,PERSIST.above()),"Chunk attachment survives native save/reload");
|
||||
command(l,"gamerule sanctuary:seasonal_snow_melt true");step(l,PERSIST,16);
|
||||
command(l,"gamerule sanctuary:seasonal_snow_melt true");until(l,PERSIST,30000,()->l.getBlockState(PERSIST).isAir()&&l.getBlockState(PERSIST.above()).isAir()&&!tracked(l,PERSIST), "Loaded natural snow melts and clears provenance");
|
||||
check(l.getBlockState(PERSIST).isAir()&&l.getBlockState(PERSIST.above()).isAir()&&!tracked(l,PERSIST),"Loaded natural snow melts and clears provenance: base="+l.getBlockState(PERSIST)+", top="+l.getBlockState(PERSIST.above())+", sky="+l.canSeeSky(PERSIST.above(2))+", active="+WeatherService.active(l)+", biome="+l.getBiome(PERSIST)+", season="+WeatherService.season(server));
|
||||
check(layers(l,new BlockPos(7,80,4))==8&&layers(l,new BlockPos(9,80,4))==3,"Built and preexisting snow preserved across restart");
|
||||
});
|
||||
@@ -92,13 +98,15 @@ public final class Melt109ClientChecks implements FabricClientGameTest {
|
||||
System.out.println("MELT109_NATIVE_PASS: accumulation regression, top-down melt, seasons, profiles, cold biomes, constructions, edit invalidation, gamerule, native chunk save/reload");
|
||||
}finally{intro.set(false);}
|
||||
}
|
||||
private static void deposit(ServerLevel l,BlockPos pos,int count){l.getGameRules().set(GameRules.MAX_SNOW_ACCUMULATION_HEIGHT,1,l.getServer());command(l,"sanctuary weather snow");l.getWeatherData().setRaining(true);l.setRainLevel(1);step(l,pos,count);l.getGameRules().set(GameRules.MAX_SNOW_ACCUMULATION_HEIGHT,0,l.getServer());}
|
||||
private static void deposit(ServerLevel l,BlockPos pos,int count){l.getGameRules().set(GameRules.MAX_SNOW_ACCUMULATION_HEIGHT,1,l.getServer());command(l,"sanctuary weather snow");l.getWeatherData().setRaining(true);l.setRainLevel(1);int target=depth(l,pos)+count;until(l,pos,Math.max(1000,count*200),()->depth(l,pos)>=target,"Weather snow reaches depth "+target+" at "+pos);l.getGameRules().set(GameRules.MAX_SNOW_ACCUMULATION_HEIGHT,0,l.getServer());}
|
||||
private static void clear(ServerLevel l){command(l,"sanctuary weather clear");l.getWeatherData().setRaining(false);l.setRainLevel(0);}
|
||||
private static void clock(ServerLevel l,String time,SeasonalWeather.Season expected){try{var field=RealtimeService.Session.class.getDeclaredField("clock");field.setAccessible(true);field.set(RealtimeService.session(l.getServer()),Clock.fixed(Instant.parse(time),ZoneOffset.UTC));check(WeatherService.season(l.getServer())==expected,"Test calendar: "+expected);}catch(ReflectiveOperationException e){throw new AssertionError(e);}}
|
||||
private static boolean tracked(ServerLevel l,BlockPos p){var m=l.getChunkAt(p).getAttached(SeasonalSnowMelt.NATURAL);return m!=null&&m.containsKey(Long.toString(p.asLong()));}
|
||||
private static void snow(ServerLevel l,BlockPos p,int layers){l.setBlockAndUpdate(p,Blocks.SNOW.defaultBlockState().setValue(SnowLayerBlock.LAYERS,layers));}
|
||||
private static int layers(ServerLevel l,BlockPos p){return SeasonalSnowMelt.layers(l.getBlockState(p));}
|
||||
private static int depth(ServerLevel l,BlockPos p){int total=0;for(int i=0;i<32;i++){int layers=layers(l,p.above(i));if(layers==0)break;total+=layers;}return total;}
|
||||
private static void step(ServerLevel l,BlockPos p,int n){for(int i=0;i<n;i++)l.tickPrecipitation(p);}
|
||||
private static void until(ServerLevel l,BlockPos p,int max,BooleanSupplier condition,String message){for(int i=0;i<max&&!condition.getAsBoolean();i++)l.tickPrecipitation(p);check(condition.getAsBoolean(),message);}
|
||||
private static void command(ServerLevel l,String s){l.getServer().getCommands().performPrefixedCommand(l.getServer().createCommandSourceStack(),s);}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
|
||||
+19
-15
@@ -6,6 +6,7 @@ import fr.koka.sanctuary.progression.SanctuaryGameplay;
|
||||
import fr.koka.sanctuary.weather.SnowAccumulation;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.BooleanSupplier;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
@@ -45,42 +46,45 @@ public final class Snow108ClientChecks implements FabricClientGameTest {
|
||||
if(enabled) {
|
||||
command(l,"sanctuary weather snow");l.setRainLevel(1);
|
||||
check(l.getGameRules().get(SnowAccumulation.MAX_HEIGHT)==2,"Default two blocks");
|
||||
for(int i=1;i<=7;i++){l.tickPrecipitation(AT);check(layers(l,AT)==i,"One layer per precipitation event: "+i);}
|
||||
check(SnowAccumulation.REALTIME_SCALE==12,"Snow accumulation uses the 12x real-time arrival scale");
|
||||
for(int i=1;i<=7;i++){int expected=i;until(l,AT,1000,()->layers(l,AT)==expected,"Layer "+i+" arrives through the native sampler");}
|
||||
var pig=EntityTypes.PIG.create(l,EntitySpawnReason.COMMAND);pig.setNoAi(true);pig.setNoGravity(true);pig.setPos(4.5,80.75,4.5);l.addFreshEntity(pig);
|
||||
l.tickPrecipitation(AT);check(l.getBlockState(AT).is(Blocks.SNOW_BLOCK),"Eighth layer becomes solid snow block");
|
||||
until(l,AT,1000,()->l.getBlockState(AT).is(Blocks.SNOW_BLOCK),"Eighth layer becomes solid snow block");
|
||||
check(l.getBlockState(AT).getCollisionShape(l,AT,CollisionContext.empty()).bounds().maxY==1,"Full block collision");
|
||||
check(pig.getY()>=81,"Growth lifts a standing entity");pig.discard();
|
||||
l.tickPrecipitation(AT);check(layers(l,AT.above())==1,"New layer begins above solid snow");
|
||||
for(int i=0;i<30;i++)l.tickPrecipitation(AT);
|
||||
until(l,AT,1000,()->layers(l,AT.above())==1,"New layer begins above solid snow");
|
||||
until(l,AT,2000,()->l.getBlockState(AT.above()).is(Blocks.SNOW_BLOCK),"Default cap fills a second block");
|
||||
check(l.getBlockState(AT.above()).is(Blocks.SNOW_BLOCK)&&l.getBlockState(AT.above(2)).isAir(),"Default cap at two blocks");
|
||||
command(l,"gamerule sanctuary:snow_accumulation_height 4");
|
||||
for(int i=0;i<30;i++)l.tickPrecipitation(AT);
|
||||
until(l,AT,5000,()->l.getBlockState(AT.above(3)).is(Blocks.SNOW_BLOCK),"Raised cap fills the fourth block");
|
||||
check(l.getBlockState(AT.above(3)).is(Blocks.SNOW_BLOCK)&&l.getBlockState(AT.above(4)).isAir(),"Operator raises cap to four blocks");
|
||||
command(l,"gamerule sanctuary:snow_accumulation_height -1");
|
||||
for(int i=0;i<16;i++)l.tickPrecipitation(AT);
|
||||
until(l,AT,3000,()->l.getBlockState(AT.above(5)).is(Blocks.SNOW_BLOCK),"Unlimited mode fills beyond the cap");
|
||||
check(l.getBlockState(AT.above(5)).is(Blocks.SNOW_BLOCK),"Unlimited mode continues upward");
|
||||
var fresh=new BlockPos(-5,80,-5);command(l,"gamerule sanctuary:snow_accumulation_height 0");l.tickPrecipitation(fresh);check(l.getBlockState(fresh).isAir(),"Sanctuary zero stops deposits");
|
||||
command(l,"gamerule sanctuary:snow_accumulation_height 2");l.getGameRules().set(GameRules.MAX_SNOW_ACCUMULATION_HEIGHT,0,server);l.tickPrecipitation(fresh);check(l.getBlockState(fresh).isAir(),"Native zero remains authoritative");l.getGameRules().set(GameRules.MAX_SNOW_ACCUMULATION_HEIGHT,1,server);
|
||||
l.setRainLevel(0);l.getWeatherData().setRaining(false);l.tickPrecipitation(fresh);check(l.getBlockState(fresh).isAir(),"No accumulation between showers");
|
||||
command(l,"sanctuary weather snow_showers");l.setRainLevel(1);l.getWeatherData().setRaining(true);l.tickPrecipitation(fresh);check(layers(l,fresh)==1,"Snow showers accumulate while falling");
|
||||
var flower=new BlockPos(-6,80,-6);l.setBlockAndUpdate(flower.below(),Blocks.GRASS_BLOCK.defaultBlockState());l.setBlockAndUpdate(flower,Blocks.DANDELION.defaultBlockState());l.tickPrecipitation(flower);check(l.getBlockState(flower).is(Blocks.DANDELION),"Plant preserved");
|
||||
var water=new BlockPos(-7,79,-7);l.setBlockAndUpdate(water,Blocks.WATER.defaultBlockState());l.tickPrecipitation(water);check(l.getBlockState(water).is(Blocks.WATER)&&l.getBlockState(water.above()).isAir(),"Open water not filled with snow");
|
||||
var roof=new BlockPos(7,83,-7);l.setBlockAndUpdate(roof,Blocks.STONE.defaultBlockState());l.tickPrecipitation(roof);check(layers(l,roof.above())==1&&l.getBlockState(roof.below()).isAir(),"Snow on roof, not indoors");
|
||||
var fresh=new BlockPos(-5,80,-5);command(l,"gamerule sanctuary:snow_accumulation_height 0");step(l,fresh,200);check(l.getBlockState(fresh).isAir(),"Sanctuary zero stops deposits");
|
||||
command(l,"gamerule sanctuary:snow_accumulation_height 2");l.getGameRules().set(GameRules.MAX_SNOW_ACCUMULATION_HEIGHT,0,server);step(l,fresh,200);check(l.getBlockState(fresh).isAir(),"Native zero remains authoritative");l.getGameRules().set(GameRules.MAX_SNOW_ACCUMULATION_HEIGHT,1,server);
|
||||
l.setRainLevel(0);l.getWeatherData().setRaining(false);step(l,fresh,200);check(l.getBlockState(fresh).isAir(),"No accumulation between showers");
|
||||
command(l,"sanctuary weather snow_showers");l.setRainLevel(1);l.getWeatherData().setRaining(true);until(l,fresh,1000,()->layers(l,fresh)==1,"Snow showers accumulate while falling");
|
||||
var flower=new BlockPos(-6,80,-6);l.setBlockAndUpdate(flower.below(),Blocks.GRASS_BLOCK.defaultBlockState());l.setBlockAndUpdate(flower,Blocks.DANDELION.defaultBlockState());step(l,flower,200);check(l.getBlockState(flower).is(Blocks.DANDELION),"Plant preserved");
|
||||
var water=new BlockPos(-7,79,-7);l.setBlockAndUpdate(water,Blocks.WATER.defaultBlockState());step(l,water,200);check(l.getBlockState(water).is(Blocks.WATER)&&l.getBlockState(water.above()).isAir(),"Open water not filled with snow");
|
||||
var roof=new BlockPos(7,83,-7);l.setBlockAndUpdate(roof,Blocks.STONE.defaultBlockState());until(l,roof,1000,()->layers(l,roof.above())==1,"Snow on roof arrives");check(layers(l,roof.above())==1&&l.getBlockState(roof.below()).isAir(),"Snow on roof, not indoors");
|
||||
var cauldron=new BlockPos(8,80,-8);l.setBlockAndUpdate(cauldron,Blocks.CAULDRON.defaultBlockState());for(int i=0;i<200;i++)l.tickPrecipitation(cauldron);check(l.getBlockState(cauldron).is(Blocks.POWDER_SNOW_CAULDRON),"Native cauldron snow filling preserved");
|
||||
command(l,"sanctuary weather rain");l.setRainLevel(1);var rainy=new BlockPos(10,80,0);l.tickPrecipitation(rainy);check(l.getBlockState(rainy).isAir(),"Temperate rain is not snow");
|
||||
command(l,"sanctuary weather vanilla");cold(l);for(int i=0;i<8;i++)l.tickPrecipitation(new BlockPos(0,80,0));check(l.getBlockState(new BlockPos(0,80,0)).is(Blocks.SNOW_BLOCK),"Cold biome also accumulates with vanilla weather");
|
||||
command(l,"sanctuary weather vanilla");cold(l);until(l,new BlockPos(0,80,0),2000,()->l.getBlockState(new BlockPos(0,80,0)).is(Blocks.SNOW_BLOCK),"Cold biome also accumulates with vanilla weather");
|
||||
var light=new BlockPos(11,80,11);l.setBlockAndUpdate(light.below(),Blocks.GLOWSTONE.defaultBlockState());command(l,"sanctuary weather snow");l.setRainLevel(1);
|
||||
} else {
|
||||
cold(l);for(int i=0;i<40;i++)l.tickPrecipitation(AT);check(layers(l,AT)==1&&l.getBlockState(AT.above()).isAir(),"Non-Sanctuary world keeps native one-layer limit");
|
||||
}
|
||||
});
|
||||
if(enabled){c.waitTicks(8);world.getServer().runOnServer(s->{var l=s.overworld();var light=new BlockPos(11,80,11);check(l.getBrightness(LightLayer.BLOCK,light)>=10,"Test light propagated");for(int i=0;i<16;i++)l.tickPrecipitation(light);check(l.getBlockState(light).isAir(),"Bright surfaces do not accumulate");});}
|
||||
if(enabled){c.waitTicks(8);world.getServer().runOnServer(s->{var l=s.overworld();var light=new BlockPos(11,80,11);check(l.getBrightness(LightLayer.BLOCK,light)>=10,"Test light propagated");step(l,light,400);check(l.getBlockState(light).isAir(),"Bright surfaces do not accumulate");});}
|
||||
}
|
||||
System.out.println("SNOW108_NATIVE_PASS: layers, solid blocks, vertical stacking, entity lift, caps and commands, weather, light, supports, cauldron and vanilla world");
|
||||
}finally{intro.set(false);}
|
||||
}
|
||||
private static void cold(ServerLevel l){command(l,"fillbiome -8 76 -8 8 88 8 minecraft:snowy_plains");l.getWeatherData().setRaining(true);l.setRainLevel(1);}
|
||||
private static int layers(ServerLevel l,BlockPos p){var s=l.getBlockState(p);return s.is(Blocks.SNOW)?s.getValue(SnowLayerBlock.LAYERS):0;}
|
||||
private static void step(ServerLevel l,BlockPos p,int n){for(int i=0;i<n;i++)l.tickPrecipitation(p);}
|
||||
private static void until(ServerLevel l,BlockPos p,int max,BooleanSupplier condition,String message){for(int i=0;i<max&&!condition.getAsBoolean();i++)l.tickPrecipitation(p);check(condition.getAsBoolean(),message);}
|
||||
private static void command(ServerLevel l,String s){l.getServer().getCommands().performPrefixedCommand(l.getServer().createCommandSourceStack(),s);}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
|
||||
+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;}
|
||||
}
|
||||
+28
-4
@@ -13,7 +13,7 @@ import java.util.Arrays;
|
||||
public final class Pbr134ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c){new PixelShadows122ClientChecks(false,false,true).runTest(c);}
|
||||
static void run(ClientGameTestContext c,TestServerContext server){
|
||||
c.runOnClient(m->{preferences();VanillaLight.setSunshafts(false);VanillaLight.setLensFlare(false);materials();VanillaLight.setEnabled(true);VanillaLight.setSaturation(100);VanillaLight.setPbr(false);VanillaLight.setSsgi(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);VanillaLight.setEdgeHighlights(false);VanillaLight.setPixelShadows(false);});
|
||||
c.runOnClient(m->{preferences();VanillaLight.setSunshafts(false);VanillaLight.setLensFlare(false);materials();VanillaLight.setEnabled(true);VanillaLight.setSaturation(100);VanillaLight.setColoredLights(false);VanillaLight.setPbr(false);VanillaLight.setSsgi(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);VanillaLight.setEdgeHighlights(false);VanillaLight.setPixelShadows(false);});
|
||||
server.runOnServer(s->{command(s,"time set 1000");for(int x=-5;x<=24;x++)for(int z=-5;z<=24;z++){
|
||||
s.overworld().setBlockAndUpdate(new BlockPos(x,64,z),(x<10?Blocks.BRICKS:x<17?Blocks.OAK_PLANKS:Blocks.IRON_BLOCK).defaultBlockState());
|
||||
for(int y=65;y<=102;y++)s.overworld().setBlockAndUpdate(new BlockPos(x,y,z),Blocks.AIR.defaultBlockState());
|
||||
@@ -30,9 +30,9 @@ public final class Pbr134ClientChecks implements FabricClientGameTest {
|
||||
c.runOnClient(m->VanillaLight.setPbrIntensity(20));frames(c);var low=difference(off,capture(c,"pbr-20"));
|
||||
check(delta.total>low.total*1.3,"PBR strength affects actual GPU output");
|
||||
c.runOnClient(m->VanillaLight.setPbrIntensity(100));frames(c);var high=difference(off,capture(c,"pbr-100"));check(high.total>delta.total*1.2,"Strong PBR visibly increases relief");
|
||||
c.runOnClient(m->VanillaLight.setPbrIntensity(0));c.waitTicks(8);unchanged(off,capture(c,null),"PBR zero restores native image");
|
||||
c.runOnClient(m->{VanillaLight.setPbrIntensity(0);});c.waitTicks(8);unchanged(off,capture(c,null),"PBR zero restores native image");
|
||||
c.runOnClient(m->check(!ProceduralPbr.allocated()&&ProceduralPbr.materialCount()==0,"Zero releases targets and material cache"));
|
||||
c.runOnClient(m->VanillaLight.setPbrIntensity(50));frames(c);
|
||||
c.runOnClient(m->{VanillaLight.setPbrIntensity(50);});frames(c);
|
||||
server.runOnServer(s->command(s,"time set 11000"));settle(c);enable(c,false);var eveningOff=capture(c,null);enable(c,true);var eveningOn=capture(c,"pbr-evening");
|
||||
int directions=0;for(int i=0;i<on.pixels().length;i++){int am=(on.pixels()[i]>>16&255)-(off.pixels()[i]>>16&255),pm=(eveningOn.pixels()[i]>>16&255)-(eveningOff.pixels()[i]>>16&255);if(Math.abs(am-pm)>5)directions++;}
|
||||
check(directions>500,"Relief follows the moving sun: "+directions);
|
||||
@@ -57,12 +57,36 @@ public final class Pbr134ClientChecks implements FabricClientGameTest {
|
||||
for(int p:PbrMaterials.generate(flat,4,4,profile))check((p>>8&0xFFFF)==0x8080,"Flat source has neutral normal");
|
||||
flat[5]=0xFFFFFFFF;var slope=PbrMaterials.generate(flat,4,4,profile);check((slope[4]>>16&255)<128&&(slope[6]>>16&255)>128,"Height gradients produce opposite normal slopes");
|
||||
check(PbrMaterials.profile("iron_block").metal()>.8&&PbrMaterials.profile("iron_ore").metal()==0,"Metal block profile does not turn ore host rock into metal");
|
||||
check(PbrMaterials.family(Blocks.WATER.defaultBlockState())==2&&PbrMaterials.family(Blocks.GLASS.defaultBlockState())==1&&PbrMaterials.family(Blocks.GLASS_PANE.defaultBlockState())==1,"Water and glass receiver families");
|
||||
check(PbrMaterials.profile("water").metal()==0&&PbrMaterials.profile("glass").metal()==0,"Water and glass stay dielectric");
|
||||
int[] glass={0x00FFFFFF,0x00000000,0x00000000,0x00FFFFFF};
|
||||
for(int p:PbrMaterials.generate(glass,2,2,PbrMaterials.profile("glass")))check((p>>8&0xFFFF)==0x8080,"Transparent glass texels fall back to geometric normal");
|
||||
check(PbrMaterials.profile("oak_planks").roughness()>PbrMaterials.profile("polished_andesite").roughness(),"Wood remains rougher than polished stone");
|
||||
}
|
||||
private static void preferences(){try{
|
||||
var p=FabricLoader.getInstance().getConfigDir().resolve("sanctuary-shaders.json");Files.writeString(p,"{\"ssgi\":true,\"ssgiIntensity\":35,\"bloom\":false}");forgetPreferences();
|
||||
check(VanillaLight.pbr()&&VanillaLight.pbrIntensity()==50&&VanillaLight.ssgi()&&!VanillaLight.bloom(),"Missing PBR setting defaults ON while preserving explicit old choices");
|
||||
check(VanillaLight.pbr()&&VanillaLight.pbrIntensity()==80&&VanillaLight.ssgi()&&!VanillaLight.bloom(),"Missing PBR setting defaults ON while preserving explicit old choices");
|
||||
for(int n:new int[]{-1,0,50,100,101}){VanillaLight.setPbr(true);VanillaLight.setPbrIntensity(n);forgetPreferences();check(VanillaLight.pbr()&&VanillaLight.pbrIntensity()==Math.clamp(n,0,100),"PBR preference persistence/clamp");}
|
||||
check(VanillaLight.pbrDistance()==32,"Shared distance default");
|
||||
check(!VanillaLight.naturalWater(false)&&VanillaLight.naturalWater(true),"World-specific water defaults");
|
||||
for(int n:new int[]{16,32,64,128}){VanillaLight.setPbrDistance(n);forgetPreferences();check(VanillaLight.pbrDistance()==n,"Shared distance persistence");}
|
||||
for(int n:new int[]{0,1,2,4,8}){VanillaLight.setWaterBlendRadius(n);forgetPreferences();check(VanillaLight.waterBlendRadius()==n,"Water blend persistence");}
|
||||
VanillaLight.setPbrDistance(32);VanillaLight.setWaterBlendRadius(4);
|
||||
check(!VanillaLight.ssr()&&VanillaLight.ssrIntensity()==20,"Missing SSR defaults OFF at 20 percent");
|
||||
for(int n:new int[]{-1,0,50,100,101}){VanillaLight.setSsr(true);VanillaLight.setSsrIntensity(n);forgetPreferences();check(VanillaLight.ssr()&&VanillaLight.ssrIntensity()==Math.clamp(n,0,100),"SSR preference persistence/clamp");}
|
||||
VanillaLight.setSsr(false);VanillaLight.setSsrIntensity(20);forgetPreferences();check(!VanillaLight.ssr(),"SSR OFF persists");
|
||||
var manager=net.minecraft.client.Minecraft.getInstance().getLanguageManager();var oldLanguage=manager.getSelected();
|
||||
for(String locale:new String[]{"fr_fr","en_us"}){
|
||||
manager.setSelected(locale);manager.onResourceManagerReload(net.minecraft.client.Minecraft.getInstance().getResourceManager());
|
||||
for(String key:new String[]{"pbr_distance","pbr_distance_help","pbr_distance_value","water_blend","water_blend_help","water_blend_value","ssr","ssr_help","ssr_intensity","ssr_intensity_help","ssr_intensity_value"}){
|
||||
String full="sanctuary.shaders."+key;check(!net.minecraft.network.chat.Component.translatable(full,50).getString().equals(full),"SSR FR/EN "+full);
|
||||
}
|
||||
}
|
||||
manager.setSelected(oldLanguage);manager.onResourceManagerReload(net.minecraft.client.Minecraft.getInstance().getResourceManager());
|
||||
Files.writeString(p,"{\"naturalWater\":false,\"pbrIntensity\":37}");forgetPreferences();
|
||||
check(!VanillaLight.naturalWater(false)&&!VanillaLight.naturalWater(true)&&VanillaLight.pbrIntensity()==37,"Legacy water OFF and personal PBR intensity are preserved");
|
||||
Files.writeString(p,"{\"naturalWater\":true}");forgetPreferences();
|
||||
check(!VanillaLight.naturalWater(false)&&VanillaLight.naturalWater(true),"Legacy global ON adopts contextual defaults");
|
||||
VanillaLight.setPbrIntensity(50);
|
||||
}catch(Exception e){throw new AssertionError(e);}}
|
||||
private record Delta(int changed,int brighter,int darker,long total){}
|
||||
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import static fr.koka.sanctuary.client.shader.PixelShadows122ClientChecks.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.*;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.entity.EquipmentSlot;
|
||||
|
||||
final class PbrRefresh142ClientChecks {
|
||||
static void refresh(ClientGameTestContext c,TestServerContext s,Frame reference){
|
||||
int entries=c.computeOnClient(m->ProceduralPbr.terrainEntries()),builds=c.computeOnClient(m->ProceduralPbr.terrainBuilds());check(entries>0,"PBR fixture has visible sections");
|
||||
for(int i=0;i<12;i++){
|
||||
c.runOnClient(m->{for(var section:m.levelRenderer.visibleSections()){var p=section.getRenderOrigin();ProceduralPbr.dirtySection(p.getX()>>4,p.getY()>>4,p.getZ()>>4);}
|
||||
check(ProceduralPbr.terrainEntries()==entries,"Invalidation retains every published PBR section");});
|
||||
c.waitTicks(1);unchanged(reference,capture(c,null),"PBR remains visible throughout repeated refreshes");
|
||||
}
|
||||
ready(c);check(c.computeOnClient(m->ProceduralPbr.terrainBuilds())>builds,"Replacement meshes actually published");unchanged(reference,capture(c,"refresh-stable"),"Replacement preserves the rendered normals");
|
||||
System.out.println("PBR142_REFRESH_PASS entries="+entries+" repeated invalidation, retained rendering, completed replacements");
|
||||
}
|
||||
static void held(ClientGameTestContext c,TestServerContext s){
|
||||
ready(c);int builds=c.computeOnClient(m->ProceduralPbr.terrainBuilds());
|
||||
for(var item:new Item[]{Items.SOUL_TORCH,Items.TORCH,Items.SOUL_TORCH,Items.TORCH}){
|
||||
s.runOnServer(server->server.getPlayerList().getPlayers().getFirst().setItemSlot(EquipmentSlot.MAINHAND,new ItemStack(item)));c.waitTicks(25);
|
||||
}
|
||||
check(c.computeOnClient(m->ProceduralPbr.terrainBuilds())==builds,"Carried light updates do not rebuild the normal mesh");
|
||||
System.out.println("PBR142_HELD_PASS native dynamic relighting preserves the PBR geometry cache");
|
||||
}
|
||||
static void cave(ClientGameTestContext c,TestServerContext s){
|
||||
c.runOnClient(m->{VanillaLight.setColoredLights(true);VanillaLight.setPbr(false);VanillaLight.setPbrIntensity(80);VanillaLight.setColoredLightDistance(16);});
|
||||
camera(c,s,0.5,65,2.5,70);look(c,s,0,-12);c.waitTicks(30);
|
||||
s.runOnServer(server->{for(int x=-5;x<=5;x++)for(int z=-1;z<=18;z++)for(int y=64;y<=70;y++)
|
||||
server.overworld().setBlockAndUpdate(new BlockPos(x,y,z),(x==-5||x==5||y==64||y==70||z==-1||z==18)?Blocks.DEEPSLATE.defaultBlockState():Blocks.AIR.defaultBlockState());
|
||||
for(int y=65;y<=69;y++)server.overworld().setBlockAndUpdate(new BlockPos(-2,y,9),Blocks.DEEPSLATE.defaultBlockState());
|
||||
for(int z=5;z<14;z++)server.overworld().setBlockAndUpdate(new BlockPos(3,67,z),Blocks.DEEPSLATE.defaultBlockState());
|
||||
server.overworld().setBlockAndUpdate(new BlockPos(3,68,8),Blocks.WATER.defaultBlockState());
|
||||
server.getPlayerList().getPlayers().getFirst().setItemSlot(EquipmentSlot.MAINHAND,new ItemStack(Items.TORCH));
|
||||
});c.waitTicks(100);var off=capture(c,"cave-pbr-off");c.runOnClient(m->VanillaLight.setPbr(true));ready(c);var on=capture(c,"cave-pbr-80");
|
||||
check(specks(on)<=specks(off)+4,"PBR does not introduce isolated white points in the cave");
|
||||
System.out.println("PBR142_CAVE_PIXELS off="+specks(off)+" on="+specks(on));
|
||||
// Capture while the camera is actually travelling, before settling caches.
|
||||
s.runOnServer(server->{var player=server.getPlayerList().getPlayers().getFirst();player.getAbilities().flying=true;player.onUpdateAbilities();});c.waitTicks(10);
|
||||
var start=c.computeOnClient(m->m.player.position());
|
||||
c.getInput().holdKey(o->o.keyUp);
|
||||
int maximumSpecks=0;
|
||||
try {for(int frame=0;frame<24;frame++){
|
||||
c.waitTicks(1);c.runOnClient(m->check(ColoredLights.dynamicCount()>0&&ProceduralPbr.applied(),"Carried light and PBR remain active during motion"));var moving=capture(c,frame%8==0?"cave-moving-"+frame:null);
|
||||
maximumSpecks=Math.max(maximumSpecks,specks(moving));
|
||||
}} finally {c.getInput().releaseKey(o->o.keyUp);}
|
||||
check(c.computeOnClient(m->m.player.position().distanceToSqr(start))>1.0,"Camera really travels at least one block");
|
||||
check(maximumSpecks<=4,"Moving camera has no isolated white grain: "+maximumSpecks);
|
||||
System.out.println("PBR142_MOVEMENT_PASS frames=24 maxSpecks="+maximumSpecks);
|
||||
camera(c,s,0.5,65,2.5,70);
|
||||
for(float pitch:new float[]{-30,0,35}){look(c,s,0,pitch);ready(c);var frame=capture(c,"cave-angle-"+(int)pitch);check(specks(frame)<=4,"Grazing-angle PBR has no white grain");System.out.println("PBR142_CAVE_ANGLE "+pitch+" specks="+specks(frame));}
|
||||
c.runOnClient(m->{VanillaLight.setSsgi(true);VanillaLight.setBloom(true);VanillaLight.setPixelShadows(true);VanillaLight.setEdgeHighlights(true);VanillaLight.setSunshafts(true);});c.waitTicks(40);ready(c);capture(c,"cave-combined");
|
||||
System.out.println("PBR142_PASS refresh, dynamic cache, defaults, cave and grazing-angle captures");
|
||||
}
|
||||
private static int specks(Frame f){int count=0,w=f.width();for(int y=2;y<f.height()-2;y++)for(int x=2;x<w-2;x++){
|
||||
int i=y*w+x,p=f.pixels()[i];int value=Math.min(p>>16&255,Math.min(p>>8&255,p&255));if(value<170)continue;
|
||||
int dark=0;for(int dy=-1;dy<=1;dy++)for(int dx=-1;dx<=1;dx++){if(dx==0&&dy==0)continue;int q=f.pixels()[i+dy*w+dx];if(Math.max(q>>16&255,Math.max(q>>8&255,q&255))<120)dark++;}if(dark>=5)count++;
|
||||
}return count;}
|
||||
private static void ready(ClientGameTestContext c){c.waitTicks(14);c.waitFor(m->ProceduralPbr.ready(),2400);c.waitTicks(8);}
|
||||
}
|
||||
+208
@@ -0,0 +1,208 @@
|
||||
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,32}){
|
||||
c.runOnClient(m->VanillaLight.setPbrDistance(distance));settle(c);frames(c);capture(c,"ssr146-distance-"+distance);
|
||||
c.runOnClient(m->check(ProceduralPbr.applied()&&PixelReflections.applied(),"Both material passes share distance "+distance));
|
||||
}
|
||||
|
||||
c.runOnClient(m->{VanillaLight.setPbrIntensity(80);VanillaLight.setSsrIntensity(20);});
|
||||
camera(c,server,.5,68,10,70);settle(c);frames(c);
|
||||
var solar=capture(c,"ssr146-solar-water");
|
||||
int white=0;for(int pixel:solar.pixels())if((pixel>>16&255)>235&&(pixel>>8&255)>235&&(pixel&255)>235)white++;
|
||||
check(white>300,"Solar reflection has a broad near-white footprint at PBR 80 / SSR 20 percent: "+white);
|
||||
System.out.println("SSR146_SOLAR_WHITE pixels="+white+" width="+solar.width()+" height="+solar.height());
|
||||
c.runOnClient(m->VanillaLight.setBloom(true));frames(c);capture(c,"ssr146-solar-water-bloom");
|
||||
c.runOnClient(m->check(MaterialBloom.contribution()!=null,"Material light reaches bloom"));
|
||||
c.runOnClient(m->VanillaLight.setBloom(false));frames(c);
|
||||
c.runOnClient(m->check(MaterialBloom.contribution()==null,"Bloom OFF releases material halo"));
|
||||
server.runOnServer(s->command(s,"time set 1000"));c.waitTicks(10);
|
||||
var sunLook=c.computeOnClient(m->{var sun=PixelShadows.sunDirection(m.gameRenderer.gameRenderState().levelRenderState.skyRenderState.sunAngle);return new float[]{(float)Math.toDegrees(Math.atan2(-sun.x,sun.z)),(float)Math.toDegrees(Math.asin(sun.y))};});
|
||||
look(c,server,sunLook[0],sunLook[1]);frames(c);capture(c,"ssr146-solar-path-low-sun");
|
||||
c.runOnClient(m->VanillaLight.setBloom(true));frames(c);capture(c,"ssr146-solar-path-low-sun-bloom");
|
||||
c.runOnClient(m->{VanillaLight.setBloom(false);VanillaLight.setSsrIntensity(100);});
|
||||
server.runOnServer(s->command(s,"time set 6000"));c.waitTicks(10);
|
||||
camera(c,server,.5,68,0,70);look(c,server,0,12);settle(c);
|
||||
server.runOnServer(s->{for(int x=-12;x<=12;x++)for(int y=65;y<=77-Math.abs(x)/2;y++)s.overworld().setBlockAndUpdate(new BlockPos(x,y,19),Blocks.CONCRETE.blue().defaultBlockState());});
|
||||
settle(c);c.runOnClient(m->VanillaLight.setSsr(false));c.waitTicks(8);var blueOff=capture(c,"ssr-blue-off");
|
||||
c.runOnClient(m->VanillaLight.setSsr(true));frames(c);var blueOn=capture(c,"ssr-blue-on");double blueSignal=colourSignal(blueOff,blueOn);
|
||||
System.out.println("SSR_SOURCE_COLOUR red="+redSignal+" blue="+blueSignal);
|
||||
check(redSignal>blueSignal+3,"Water reflection follows source colour rather than animation: "+redSignal+" / "+blueSignal);
|
||||
server.runOnServer(s->{for(int x=-12;x<=12;x++)for(int y=65;y<=77-Math.abs(x)/2;y++)s.overworld().setBlockAndUpdate(new BlockPos(x,y,19),Blocks.CONCRETE.red().defaultBlockState());});settle(c);
|
||||
c.runOnClient(m->VanillaLight.setSsrIntensity(0));c.waitTicks(8);
|
||||
check(!c.computeOnClient(m->PixelReflections.applied()||PixelReflections.allocated()),"Zero removes SSR contribution and colour allocation");
|
||||
c.runOnClient(m->VanillaLight.setSsrIntensity(100));frames(c);
|
||||
for(var bed:new net.minecraft.world.level.block.Block[]{Blocks.SAND,Blocks.MUD}){
|
||||
server.runOnServer(s->{for(int x=-18;x<=18;x++)for(int z=3;z<=18;z++){s.overworld().setBlockAndUpdate(new BlockPos(x,63,z),Blocks.STONE.defaultBlockState());s.overworld().setBlockAndUpdate(new BlockPos(x,64,z),bed.defaultBlockState());}});
|
||||
settle(c);frames(c);
|
||||
c.runOnClient(m->check(NaturalWater.sample(m.level,.5,65.5,8.5).equals(bed==Blocks.SAND?NaturalWater.LIGHT:NaturalWater.POND),"Natural water profile remains independent of PBR and SSR: "+NaturalWater.sample(m.level,.5,65.5,8.5)+" above "+m.level.getBlockState(new BlockPos(0,64,8))));
|
||||
capture(c,bed==Blocks.SAND?"ssr-sand-profile":"ssr-pond-profile");
|
||||
}
|
||||
server.runOnServer(s->{for(int x=-18;x<=18;x++)for(int z=3;z<=18;z++)s.overworld().setBlockAndUpdate(new BlockPos(x,64,z),Blocks.STONE.defaultBlockState());});settle(c);frames(c);
|
||||
for(float yaw:new float[]{-25,0,25}){look(c,server,yaw,12);frames(c);capture(c,"ssr-water-angle-"+(int)yaw);}
|
||||
look(c,server,0,2);frames(c);capture(c,"ssr-grazing");
|
||||
look(c,server,75,12);frames(c);capture(c,"ssr-landscape-offscreen");
|
||||
camera(c,server,2.5,68,0,70);look(c,server,0,12);settle(c);frames(c);capture(c,"ssr-translated");
|
||||
camera(c,server,.5,68,0,70);look(c,server,0,12);settle(c);
|
||||
// A hidden landscape must not leak through an opaque foreground wall.
|
||||
server.runOnServer(s->{for(int x=-12;x<=12;x++)for(int y=65;y<=78;y++)s.overworld().setBlockAndUpdate(new BlockPos(x,y,18),Blocks.CONCRETE.white().defaultBlockState());});
|
||||
settle(c);c.runOnClient(m->VanillaLight.setSsr(false));c.waitTicks(8);var coveredOff=capture(c,"ssr-covered-off");
|
||||
c.runOnClient(m->VanillaLight.setSsr(true));frames(c);var coveredOn=capture(c,"ssr-covered-on");
|
||||
double coveredSignal=colourSignal(coveredOff,coveredOn);
|
||||
server.runOnServer(s->{for(int x=-12;x<=12;x++)for(int y=65;y<=77-Math.abs(x)/2;y++)s.overworld().setBlockAndUpdate(new BlockPos(x,y,19),Blocks.CONCRETE.blue().defaultBlockState());});settle(c);
|
||||
c.runOnClient(m->VanillaLight.setSsr(false));c.waitTicks(8);var hiddenBlueOff=capture(c,"ssr-hidden-blue-off");
|
||||
c.runOnClient(m->VanillaLight.setSsr(true));frames(c);var hiddenBlueOn=capture(c,"ssr-hidden-blue-on");
|
||||
double hiddenBlueSignal=colourSignal(hiddenBlueOff,hiddenBlueOn);
|
||||
check(Math.abs(coveredSignal-hiddenBlueSignal)<1.5,"Hidden landscape colour cannot leak through opaque wall: "+coveredSignal+" / "+hiddenBlueSignal);
|
||||
server.runOnServer(s->{for(int x=-12;x<=12;x++){
|
||||
for(int y=65;y<=78;y++)s.overworld().setBlockAndUpdate(new BlockPos(x,y,18),Blocks.AIR.defaultBlockState());
|
||||
for(int y=65;y<=77-Math.abs(x)/2;y++)s.overworld().setBlockAndUpdate(new BlockPos(x,y,19),Blocks.CONCRETE.red().defaultBlockState());
|
||||
}});settle(c);frames(c);
|
||||
benchmark(c,"pond");
|
||||
look(c,server,0,35);frames(c);benchmark(c,"water-fullscreen");look(c,server,0,12);frames(c);
|
||||
// The nearest glass layer must replace the water receiver, including alpha-zero glass texels.
|
||||
server.runOnServer(s->{for(int x=-7;x<=7;x++)for(int z=4;z<=14;z++)s.overworld().setBlockAndUpdate(new BlockPos(x,66,z),Blocks.GLASS.defaultBlockState());});
|
||||
settle(c);capture(c,"ssr-glass-over-water");
|
||||
c.runOnClient(m->VanillaLight.setSsr(false));c.waitTicks(8);var glassOff=capture(c,"ssr-glass-off");
|
||||
c.runOnClient(m->VanillaLight.setSsr(true));frames(c);var glassOn=capture(c,"ssr-glass-on");
|
||||
check(changed(glassOff,glassOn)>100,"Transparent glass reflects landscape");
|
||||
benchmark(c,"glass");
|
||||
server.runOnServer(s->{for(int x=-7;x<=7;x++)for(int z=4;z<=14;z++)s.overworld().setBlockAndUpdate(new BlockPos(x,66,z),Blocks.IRON_BLOCK.defaultBlockState());});
|
||||
settle(c);c.runOnClient(m->VanillaLight.setSsr(false));c.waitTicks(8);var metalOff=capture(c,"ssr-metal-off");
|
||||
c.runOnClient(m->VanillaLight.setSsr(true));frames(c);var metalOn=capture(c,"ssr-metal-on");
|
||||
check(changed(metalOff,metalOn)>100,"Solid PBR reflects landscape");
|
||||
look(c,server,.00002f,12.00002f);frames(c);var tiny=capture(c,"ssr-metal-tiny-motion");
|
||||
check(stableMetal(metalOn,tiny),"Tiny camera movement keeps material texels stable");look(c,server,0,12);frames(c);
|
||||
server.runOnServer(s->{
|
||||
command(s,"fill -7 67 9 7 70 9 iron_block");
|
||||
for(int x=-6;x<=6;x+=2)command(s,"setblock "+x+" 67 8 polished_andesite_stairs[facing=south]");
|
||||
});settle(c);frames(c);var intersections=capture(c,"ssr146-intersections");
|
||||
look(c,server,.00002f,12.00002f);frames(c);var shifted=capture(c,"ssr146-intersections-tiny-motion");
|
||||
check(stableIntersections(intersections,shifted),"Block intersections remain stable during subpixel motion");
|
||||
look(c,server,0,12);frames(c);
|
||||
c.runOnClient(m->{VanillaLight.setPixelShadows(true);VanillaLight.setSsgi(true);VanillaLight.setColoredLights(true);VanillaLight.setBloom(true);VanillaLight.setSunshafts(true);});
|
||||
frames(c);capture(c,"ssr-combined");
|
||||
server.runOnServer(s->command(s,"time set 18000"));c.waitTicks(15);frames(c);capture(c,"ssr-moon");
|
||||
server.runOnServer(s->command(s,"time set 6000"));c.waitTicks(15);frames(c);
|
||||
c.runOnClient(m->{VanillaLight.setPixelShadows(false);VanillaLight.setSsgi(false);VanillaLight.setColoredLights(false);VanillaLight.setBloom(false);VanillaLight.setSunshafts(false);});
|
||||
server.runOnServer(s->{for(int x=-7;x<=7;x++)for(int z=4;z<=14;z++)s.overworld().setBlockAndUpdate(new BlockPos(x,66,z),Blocks.STONE.defaultBlockState());});
|
||||
settle(c);c.runOnClient(m->VanillaLight.setSsr(false));c.waitTicks(8);capture(c,"ssr-rough-stone-off");
|
||||
c.runOnClient(m->VanillaLight.setSsr(true));frames(c);capture(c,"ssr-rough-stone-on");
|
||||
// Native panes, sloped water, cascade sides and a waterlogged slab share the receiver pass.
|
||||
server.runOnServer(s->{
|
||||
for(int y=66;y<=69;y++)s.overworld().setBlockAndUpdate(new BlockPos(-8,y,8),Blocks.GLASS_PANE.defaultBlockState());
|
||||
s.overworld().setBlockAndUpdate(new BlockPos(8,69,8),Blocks.WATER.defaultBlockState());
|
||||
s.overworld().setBlockAndUpdate(new BlockPos(-8,65,10),Blocks.OAK_SLAB.defaultBlockState().setValue(net.minecraft.world.level.block.state.properties.BlockStateProperties.WATERLOGGED,true));
|
||||
});settle(c);frames(c);capture(c,"ssr-flow-pane-waterlogged");
|
||||
server.runOnServer(s->{command(s,"fill -3 66 6 3 69 6 glass");command(s,"fill -3 66 8 3 69 8 light_blue_stained_glass");
|
||||
command(s,"setblock -4 67 6 blue_stained_glass_pane");});
|
||||
settle(c);c.runOnClient(m->VanillaLight.setSsr(false));c.waitTicks(8);capture(c,"ssr-stacked-coloured-glass-off");
|
||||
c.runOnClient(m->VanillaLight.setSsr(true));frames(c);capture(c,"ssr-stacked-coloured-glass-on");
|
||||
camera(c,server,12,64,8,70);look(c,server,0,0);c.waitTicks(15);
|
||||
c.runOnClient(m->check(!PixelReflections.allocated(),"Immersion suspends and releases SSR"));
|
||||
camera(c,server,.5,68,0,70);look(c,server,0,12);settle(c);frames(c);
|
||||
var reload=c.computeOnClient(net.minecraft.client.Minecraft::reloadResourcePacks);c.waitFor(m->reload.isDone()&&m.gui.overlay()==null,1200);reload.join();frames(c);capture(c,"ssr-reloaded");
|
||||
int reloadedUpdates=c.computeOnClient(m->ProceduralPbr.waterUpdates());c.waitTicks(20);
|
||||
c.runOnClient(m->check(ProceduralPbr.waterUpdates()>reloadedUpdates,"Reloaded atlas supplies a live native water clock"));
|
||||
int w=c.computeOnClient(m->m.getWindow().getScreenWidth()),h=c.computeOnClient(m->m.getWindow().getScreenHeight());
|
||||
c.runOnClient(m->m.getWindow().setWindowed(961,541));c.waitTicks(15);frames(c);capture(c,"ssr-resized");
|
||||
c.runOnClient(m->m.getWindow().setWindowed(w,h));c.waitTicks(15);frames(c);
|
||||
c.runOnClient(m->VanillaLight.setPbr(false));c.waitTicks(8);c.runOnClient(m->check(!PixelReflections.allocated(),"PBR OFF releases reflection resources"));
|
||||
System.out.println("SSR_PASS water; transparent glass; solids; OFF/zero; motion; reload; resize");
|
||||
}
|
||||
private static boolean stableIntersections(Frame a,Frame b){
|
||||
// The surrounding animated water is not a stability oracle for static blocks.
|
||||
int changed=0,count=0;
|
||||
for(int y=a.height()*30/100;y<a.height()*80/100;y++)for(int x=a.width()/4;x<a.width()*3/4;x++){
|
||||
int i=y*a.width()+x,aa=a.pixels()[i],bb=b.pixels()[i];count++;
|
||||
if(Math.abs((aa>>16&255)-(bb>>16&255))+Math.abs((aa>>8&255)-(bb>>8&255))+Math.abs((aa&255)-(bb&255))>12)changed++;
|
||||
}
|
||||
System.out.println("SSR146_INTERSECTIONS changed="+changed+" samples="+count);
|
||||
return changed<count*.001;
|
||||
}
|
||||
private static boolean stableMetal(Frame a,Frame b){int changes=0,count=0;
|
||||
for(int y=a.height()*58/100;y<a.height()*68/100;y++)for(int x=a.width()*3/10;x<a.width()*7/10;x++){
|
||||
int i=y*a.width()+x,aa=a.pixels()[i],bb=b.pixels()[i];count++;
|
||||
if(Math.abs((aa>>16&255)-(bb>>16&255))+Math.abs((aa>>8&255)-(bb>>8&255))+Math.abs((aa&255)-(bb&255))>12)changes++;
|
||||
}return changes<count*.02;
|
||||
}
|
||||
private static double colourSignal(Frame a,Frame b){long total=0;int count=0;
|
||||
for(int y=a.height()/2;y<a.height();y++)for(int x=a.width()/4;x<a.width()*3/4;x++){
|
||||
int i=y*a.width()+x,aa=a.pixels()[i],bb=b.pixels()[i];total+=(bb>>16&255)-(aa>>16&255)-(bb&255)+(aa&255);count++;
|
||||
}return total/(double)count;
|
||||
}
|
||||
private static void benchmark(ClientGameTestContext c,String name){
|
||||
c.runOnClient(m->{m.options.enableVsync().set(false);m.options.framerateLimit().set(net.minecraft.client.Options.UNLIMITED_FRAMERATE_CUTOFF);
|
||||
m.options.inactivityFpsLimit().set(net.minecraft.client.InactivityFpsLimit.MINIMIZED);m.getWindow().setWindowed(1920,1080);});
|
||||
c.waitTicks(20);frames(c);
|
||||
c.runOnClient(m->check(m.gameRenderer.mainRenderTarget().width==1920&&m.gameRenderer.mainRenderTarget().height==1080,"Benchmark uses 1080p"));
|
||||
for(boolean on:new boolean[]{false,true}){
|
||||
c.runOnClient(m->VanillaLight.setSsr(on));c.waitTicks(30);
|
||||
c.runOnClient(m->fr.koka.sanctuary.gametest.ReflectionMetrics.start());c.waitTicks(80);
|
||||
var result=c.computeOnClient(m->fr.koka.sanctuary.gametest.ReflectionMetrics.stop());
|
||||
c.runOnClient(m->{check(ProceduralPbr.solidBytes()<=24L*1024*1024&&ProceduralPbr.transparentBytes()<=8L*1024*1024,"Mesh memory budgets");
|
||||
System.out.println("SSR_MEMORY "+name+" enabled="+on+" solid="+ProceduralPbr.solidBytes()+" transparent="+ProceduralPbr.transparentBytes()+" targets="+PixelReflections.targetBytes());});
|
||||
check(result[0]>=30,"Benchmark has enough frame samples");
|
||||
System.out.println("SSR_BENCH "+name+" enabled="+on+" count="+result[0]+" medianMs="+result[1]+" p95Ms="+result[2]+" gpuCount="+result[3]+" gpuMedianMs="+result[4]+" gpuP95Ms="+result[5]);
|
||||
}
|
||||
}
|
||||
private static void dumpLandscape(ClientGameTestContext c){
|
||||
var done=new java.util.concurrent.CompletableFuture<Void>();
|
||||
c.runOnClient(m->{
|
||||
var src=PixelReflections.landscape();
|
||||
var target=new com.mojang.blaze3d.pipeline.TextureTarget("SSR test source",src.getWidth(0),src.getHeight(0),com.mojang.renderpearl.api.GpuFormat.RGBA8_UNORM,null);
|
||||
com.mojang.blaze3d.systems.RenderSystem.getDevice().createCommandEncoder().copyTextureToTexture(src,target.getColorTexture(),0,0,0,0,0,target.width,target.height);
|
||||
net.minecraft.client.Screenshot.takeScreenshot(target,image->{try(image){image.writeToFile(java.nio.file.Path.of("screenshots/ssr-opaque-source.png"));done.complete(null);}catch(Exception e){done.completeExceptionally(e);}finally{target.destroyBuffers();}});
|
||||
});c.waitFor(m->done.isDone(),200);done.join();
|
||||
}
|
||||
private static long changed(Frame a,Frame b){long n=0;for(int y=a.height()/2;y<a.height();y++)for(int x=0;x<a.width();x++){
|
||||
int i=y*a.width()+x,aa=a.pixels()[i],bb=b.pixels()[i];
|
||||
if(Math.abs((aa>>16&255)-(bb>>16&255))+Math.abs((aa>>8&255)-(bb>>8&255))+Math.abs((aa&255)-(bb&255))>8)n++;
|
||||
}return n;}
|
||||
private static void frames(ClientGameTestContext c){int start=c.computeOnClient(m->PixelReflections.frames());c.waitFor(m->PixelReflections.applied()&&ProceduralPbr.ready()&&PixelReflections.frames()>start+8,1200);}
|
||||
private static void settle(ClientGameTestContext c){c.waitTicks(45);c.waitFor(m->m.levelRenderer.hasRenderedAllSections()&&ProceduralPbr.ready(),1200);c.waitTicks(10);}
|
||||
}
|
||||
+26
-7
@@ -36,13 +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;
|
||||
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.sunshaftOnly=sunshaftOnly;this.bloomOnly=bloomOnly;this.ssgiOnly=ssgiOnly;this.pbrOnly=pbrOnly;this.closeupOnly=closeupOnly;}
|
||||
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);
|
||||
@@ -51,6 +52,16 @@ 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(net.minecraft.client.PreferredGraphicsApi.OPENGL.getBackendsToTry()[0] instanceof com.mojang.renderpearl.backend.opengl.GlBackend, "Explicit OpenGL remains available");
|
||||
check(expected.isEmpty() || info.backendName().equalsIgnoreCase(expected),
|
||||
"Requested graphics backend must not silently fall back: " + expected + " / " + info.backendName());
|
||||
System.out.println("SHADER139_BACKEND " + info.backendName() + " / " + info.name()
|
||||
+ " / " + info.driverInfo() + " / zeroToOne=" + info.isZZeroToOne());
|
||||
});
|
||||
var path = FabricLoader.getInstance().getConfigDir().resolve("sanctuary-shaders.json");
|
||||
final byte[] originalPreferences;
|
||||
try {
|
||||
@@ -82,6 +93,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 -> {
|
||||
@@ -137,8 +151,11 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
||||
c.runOnClient(m -> check(VanillaLight.active(), "Native Sanctuary shader is active without an external engine"));
|
||||
float morning = c.computeOnClient(m -> m.gameRenderer.gameRenderState().levelRenderState.skyRenderState.sunAngle);
|
||||
|
||||
if(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;}
|
||||
@@ -268,7 +285,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
||||
});
|
||||
});
|
||||
c.waitTicks(3);
|
||||
c.takeScreenshot("sanctuary-beta137-shader-options-" + locale);
|
||||
c.takeScreenshot("sanctuary-beta142-shader-options-" + locale);
|
||||
}
|
||||
c.runOnClient(m -> m.gui.setScreen(null));
|
||||
}
|
||||
@@ -437,11 +454,12 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
||||
var path=FabricLoader.getInstance().getConfigDir().resolve("sanctuary-shaders.json");
|
||||
Files.deleteIfExists(path);forgetPreferences();
|
||||
check(VanillaLight.bloom()&&VanillaLight.bloomIntensity()==20&&VanillaLight.pbr()&&VanillaLight.ssgi(),"Fresh defaults: bloom20, PBR and SSGI ON");
|
||||
check(VanillaLight.pbrIntensity()==50&&VanillaLight.ssgiIntensity()==35,"Existing effect strengths retained");
|
||||
check(VanillaLight.pbrIntensity()==80&&VanillaLight.ssrIntensity()==20&&VanillaLight.ssgiIntensity()==35,"PBR 80 and SSR 20 defaults, SSGI strength retained");
|
||||
Files.writeString(path,"{\"pbr\":false,\"ssgi\":false,\"bloomIntensity\":73}");forgetPreferences();
|
||||
check(!VanillaLight.pbr()&&!VanillaLight.ssgi()&&VanillaLight.bloomIntensity()==73,"Explicit personal toggles and intensity survive changed defaults");
|
||||
Files.writeString(path,"{\"bloom\":false}");forgetPreferences();
|
||||
check(!VanillaLight.bloom()&&VanillaLight.bloomIntensity()==20&&VanillaLight.pbr()&&VanillaLight.ssgi(),"Missing fields get new defaults without replacing explicit values");
|
||||
Files.writeString(path,"{\"pbrIntensity\":50}");forgetPreferences();check(VanillaLight.pbrIntensity()==50,"Explicit PBR 50 is preserved");
|
||||
System.out.println("SHADER135_DEFAULTS_PASS fresh, partial and personal preferences");
|
||||
} catch(IOException e){throw new AssertionError(e);}
|
||||
}
|
||||
@@ -552,7 +570,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
||||
}
|
||||
|
||||
static Frame capture(ClientGameTestContext c, String name) {
|
||||
if (name != null) c.takeScreenshot("sanctuary-beta137-shader-" + name);
|
||||
if (name != null) c.takeScreenshot((Boolean.getBoolean("sanctuary.test.water144")?"sanctuary-beta144-water-":"sanctuary-beta142-shader-") + name);
|
||||
var result = new CompletableFuture<Frame>();
|
||||
c.runOnClient(m -> Screenshot.takeScreenshot(m.gameRenderer.mainRenderTarget(), image -> {
|
||||
try (image) {
|
||||
@@ -653,11 +671,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) {
|
||||
@@ -667,6 +685,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());
|
||||
}
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ import java.nio.file.Files;
|
||||
public final class Ssgi132ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c){new PixelShadows122ClientChecks(false,true).runTest(c);}
|
||||
static void run(ClientGameTestContext c,TestServerContext server){
|
||||
c.runOnClient(m->{preferences();VanillaLight.setSunshafts(false);VanillaLight.setLensFlare(false);VanillaLight.setPbr(false);VanillaLight.setEnabled(true);VanillaLight.setSaturation(100);VanillaLight.setSsgi(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);VanillaLight.setPixelShadows(false);VanillaLight.setEdgeHighlights(false);});
|
||||
c.runOnClient(m->{preferences();VanillaLight.setSunshafts(false);VanillaLight.setLensFlare(false);VanillaLight.setPbr(false);VanillaLight.setEnabled(true);VanillaLight.setSaturation(100);VanillaLight.setColoredLights(false);VanillaLight.setSsgi(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);VanillaLight.setPixelShadows(false);VanillaLight.setEdgeHighlights(false);});
|
||||
server.runOnServer(s->{command(s,"time set 6000");for(int x=-5;x<=24;x++)for(int z=-5;z<=24;z++){
|
||||
s.overworld().setBlockAndUpdate(new BlockPos(x,64,z),Blocks.CONCRETE.white().defaultBlockState());
|
||||
for(int y=65;y<=102;y++)s.overworld().setBlockAndUpdate(new BlockPos(x,y,z),Blocks.AIR.defaultBlockState());
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ import net.minecraft.world.level.block.Blocks;
|
||||
public final class SsgiCloseup135ClientChecks implements net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c){new PixelShadows122ClientChecks(false,false,false,true).runTest(c);}
|
||||
static void run(ClientGameTestContext c,TestServerContext server){
|
||||
c.runOnClient(m->{VanillaLight.setEnabled(true);VanillaLight.setSaturation(100);VanillaLight.setSsgi(false);VanillaLight.setPbr(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);VanillaLight.setPixelShadows(false);VanillaLight.setEdgeHighlights(false);});
|
||||
c.runOnClient(m->{VanillaLight.setEnabled(true);VanillaLight.setSaturation(100);VanillaLight.setColoredLights(false);VanillaLight.setSsgi(false);VanillaLight.setPbr(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);VanillaLight.setPixelShadows(false);VanillaLight.setEdgeHighlights(false);});
|
||||
server.runOnServer(s->{
|
||||
// Minecraft 26.3 rejects setting a clock to its existing value.
|
||||
command(s,"time add 1");command(s,"time set 6000");
|
||||
|
||||
+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);
|
||||
}
|
||||
}
|
||||
+6
-5
@@ -50,14 +50,15 @@ public final class Sunshafts137ClientChecks implements FabricClientGameTest {
|
||||
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);
|
||||
}
|
||||
private 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));});}
|
||||
private 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);}
|
||||
private 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);}
|
||||
private static void enable(ClientGameTestContext c,boolean enabled){c.runOnClient(m->VanillaLight.setSunshafts(enabled));if(enabled)settle(c);else c.waitTicks(10);}
|
||||
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.
|
||||
private 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);}
|
||||
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"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@ public final class SanctuaryMod implements ModInitializer {
|
||||
fr.koka.sanctuary.notifications.DiscoveryNotifications.register();
|
||||
fr.koka.sanctuary.realtime.RealtimeService.register();
|
||||
fr.koka.sanctuary.weather.WeatherService.register();
|
||||
fr.koka.sanctuary.water.WaterWorldSettings.register();
|
||||
fr.koka.sanctuary.plans.PlanService.register();
|
||||
fr.koka.sanctuary.plans.Statuary.register();
|
||||
fr.koka.sanctuary.food.FoodSync.register();
|
||||
|
||||
@@ -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;}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ public final class SanctuaryKnowledgeClient implements ClientModInitializer {
|
||||
|
||||
public static void registerClient() {
|
||||
SanctuaryResourcePack.register();
|
||||
fr.koka.sanctuary.client.shader.NaturalWater.register();
|
||||
VortexCloudRenderer.register();
|
||||
fr.koka.sanctuary.client.shader.PixelShadows.register();
|
||||
fr.koka.sanctuary.client.shader.EdgeHighlights.register();
|
||||
|
||||
@@ -42,8 +42,9 @@ public final class Bloom {
|
||||
var builder=RenderPipeline.builder().withLocation(id("pipeline/"+name)).withVertexShader("core/screenquad").withFragmentShader(id("core/"+name)).withBindGroupLayout(layout.build());
|
||||
if(scene)builder.withBindGroupLayout(SCENE);
|
||||
return RenderPipelines.register(builder.withPrimitiveTopology(PrimitiveTopology.TRIANGLES).withCull(false)
|
||||
.withColorTargetState(new ColorTargetState(Optional.ofNullable(blend),name.equals("bloom_blur")||name.equals("bloom_downsample")||name.equals("lens_flare_source")?GpuFormat.RGBA16_FLOAT:GpuFormat.RGBA8_UNORM,blend==null?ColorTargetState.WRITE_ALL:ColorTargetState.WRITE_COLOR)).withDepthStencilState(Optional.empty()).build());
|
||||
.withColorTargetState(new ColorTargetState(Optional.ofNullable(blend),name.equals("bloom_material_add")||name.equals("bloom_blur")||name.equals("bloom_downsample")||name.equals("lens_flare_source")?GpuFormat.RGBA16_FLOAT:GpuFormat.RGBA8_UNORM,blend==null?ColorTargetState.WRITE_ALL:ColorTargetState.WRITE_COLOR)).withDepthStencilState(Optional.empty()).build());
|
||||
}
|
||||
private static final RenderPipeline MATERIAL=screen("bloom_material_add",new BlendFunction(BlendFactor.ONE,BlendFactor.ONE,BlendFactor.ZERO,BlendFactor.ONE),false,false);
|
||||
private static final RenderPipeline SKY=screen("bloom_sky",null,true,false);
|
||||
private static final RenderPipeline DOWNSAMPLE=screen("bloom_downsample",null,false,false);
|
||||
private static final RenderPipeline BLUR=screen("bloom_blur",null,false,true);
|
||||
@@ -121,6 +122,7 @@ public final class Bloom {
|
||||
}
|
||||
if(halo){
|
||||
screen(DOWNSAMPLE,small.getColorTextureView(),emission.getColorTextureView(),false,null);
|
||||
if(MaterialBloom.contribution()!=null)screen(MATERIAL,small.getColorTextureView(),MaterialBloom.contribution(),false,null);
|
||||
screen(BLUR,ping.getColorTextureView(),small.getColorTextureView(),false,horizontal);
|
||||
screen(BLUR,small.getColorTextureView(),ping.getColorTextureView(),false,vertical);
|
||||
}
|
||||
|
||||
+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();}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import com.mojang.blaze3d.pipeline.TextureTarget;
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.renderpearl.api.GpuFormat;
|
||||
import com.mojang.renderpearl.api.pipeline.*;
|
||||
import com.mojang.renderpearl.api.textures.*;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import org.joml.Vector4f;
|
||||
import java.util.Optional;
|
||||
|
||||
/** Extract only the positive contribution of each material pass, before other effects. */
|
||||
final class MaterialBloom {
|
||||
private static final RenderPipeline EXTRACT=RenderPipelines.register(RenderPipeline.builder()
|
||||
.withLocation(Identifier.parse("sanctuary:pipeline/material_bloom"))
|
||||
.withVertexShader("core/screenquad").withFragmentShader(Identifier.parse("sanctuary:core/material_bloom"))
|
||||
.withBindGroupLayout(BindGroupLayout.builder().withUniform("Before",UniformType.COMBINED_IMAGE_SAMPLER)
|
||||
.withUniform("After",UniformType.COMBINED_IMAGE_SAMPLER).build())
|
||||
.withPrimitiveTopology(PrimitiveTopology.TRIANGLES).withCull(false)
|
||||
.withColorTargetState(new ColorTargetState(Optional.of(new BlendFunction(BlendFactor.ONE,BlendFactor.ONE,BlendFactor.ZERO,BlendFactor.ONE)),GpuFormat.RGBA16_FLOAT,ColorTargetState.WRITE_COLOR))
|
||||
.withDepthStencilState(Optional.empty()).build());
|
||||
private static TextureTarget contribution;
|
||||
private static boolean ready;
|
||||
private MaterialBloom(){}
|
||||
static void beginFrame(){ready=false;if(!VanillaLight.bloom()||VanillaLight.bloomIntensity()==0)release();}
|
||||
static void accumulate(GpuTextureView before){
|
||||
if(!VanillaLight.bloom()||VanillaLight.bloomIntensity()==0)return;
|
||||
var main=Minecraft.getInstance().gameRenderer.mainRenderTarget();
|
||||
int w=Math.max(1,main.width/8),h=Math.max(1,main.height/8);
|
||||
if(contribution==null||contribution.width!=w||contribution.height!=h){release();contribution=new TextureTarget("Sanctuary material glow",w,h,GpuFormat.RGBA16_FLOAT,null);}
|
||||
try(var pass=RenderSystem.getDevice().createCommandEncoder().createRenderPass(()->"Sanctuary material glow",contribution.getColorTextureView(),ready?Optional.empty():Optional.of(new Vector4f()))){
|
||||
pass.setPipeline(RenderSystem.getCompiledPipeline(EXTRACT));
|
||||
var nearest=RenderSystem.getSamplerCache().getClampToEdge(FilterMode.NEAREST);
|
||||
pass.setUniform("Before",before,nearest);pass.setUniform("After",main.getColorTextureView(),nearest);pass.draw(3,1,0,0);
|
||||
}
|
||||
ready=true;
|
||||
}
|
||||
static GpuTextureView contribution(){return ready?contribution.getColorTextureView():null;}
|
||||
static void release(){if(contribution!=null){contribution.destroyBuffers();contribution=null;}ready=false;}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import com.google.gson.*;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import java.util.*;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.resource.v1.ResourceLoader;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import net.minecraft.client.renderer.block.BlockAndTintGetter;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.packs.PackType;
|
||||
import net.minecraft.server.packs.resources.ResourceManagerReloadListener;
|
||||
import net.minecraft.tags.BiomeTags;
|
||||
import net.minecraft.tags.FluidTags;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
/** Client-only water materials; no world writes, per-frame terrain scan or additional render pass. */
|
||||
public final class NaturalWater {
|
||||
public static final int DEPTH=8;
|
||||
public record Profile(float tint,float alpha,float fog) {
|
||||
public Profile mix(Profile b,float t){if(this==b||t==0)return this;if(t==1)return b;return new Profile(tint+(b.tint-tint)*t,alpha+(b.alpha-alpha)*t,fog+(b.fog-fog)*t);}
|
||||
}
|
||||
public static final Profile CLEAR=new Profile(0,.55f,1),LIGHT=new Profile(.10f,.80f,.70f),POND=new Profile(.85f,1,.40f);
|
||||
private static final Profile[] PROFILES={CLEAR,LIGHT,POND};
|
||||
private static volatile Map<Block,Integer> materials=Map.of();
|
||||
private static volatile float strength;
|
||||
private static ClientLevel previousLevel;
|
||||
private static boolean sanctuaryWorld;
|
||||
private static volatile int blendRadius=4;
|
||||
public static boolean sanctuaryWorld(){return sanctuaryWorld;}
|
||||
private static Profile fogFrom=CLEAR,fogTarget=CLEAR;
|
||||
private static long fogChanged;
|
||||
private static boolean wasUnderwater;
|
||||
private NaturalWater(){}
|
||||
public static void register(){
|
||||
net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking.registerGlobalReceiver(
|
||||
fr.koka.sanctuary.water.WaterWorldSettings.Context.TYPE,(payload,context)->context.client().execute(()->sanctuaryWorld=payload.sanctuary()));
|
||||
net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents.DISCONNECT.register((listener,client)->sanctuaryWorld=false);
|
||||
net.fabricmc.fabric.api.client.networking.v1.ClientConfigurationConnectionEvents.INIT.register((listener,client)->sanctuaryWorld=false);
|
||||
ResourceLoader.get(PackType.CLIENT_RESOURCES).registerReloadListener(Identifier.parse("sanctuary:water_materials"),
|
||||
(ResourceManagerReloadListener) manager->{
|
||||
try(var reader=manager.getResourceOrThrow(Identifier.parse("sanctuary:water_materials.json")).openAsReader()){
|
||||
var root=JsonParser.parseReader(reader).getAsJsonObject();var next=new HashMap<Block,Integer>();
|
||||
for(var entry:Map.of("light",1,"pond",2).entrySet()) for(var id:root.getAsJsonArray(entry.getKey())){
|
||||
var key=Identifier.parse(id.getAsString());
|
||||
if(!BuiltInRegistries.BLOCK.containsKey(key))throw new IllegalArgumentException("Unknown water material "+key);
|
||||
if(next.put(BuiltInRegistries.BLOCK.getValue(key),entry.getValue())!=null)throw new IllegalArgumentException("Duplicate water material "+key);
|
||||
}
|
||||
materials=Map.copyOf(next);
|
||||
}catch(Exception e){SanctuaryMod.LOGGER.warn("Invalid water materials; using bundled defaults",e);materials=defaults();}
|
||||
});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(client->{
|
||||
float next=VanillaLight.active()&&VanillaLight.naturalWater()?VanillaLight.waterIntensity()/100f:0;
|
||||
if(previousLevel!=client.level||strength!=next||blendRadius!=VanillaLight.waterBlendRadius()){previousLevel=client.level;strength=next;blendRadius=VanillaLight.waterBlendRadius();wasUnderwater=false;
|
||||
if(client.level!=null)invalidateWaterSections(client);}
|
||||
if(client.level==null||next==0){wasUnderwater=false;return;}
|
||||
var camera=client.gameRenderer.mainCamera();
|
||||
if(camera.getFluidInCamera()!=net.minecraft.world.level.material.FogType.WATER){wasUnderwater=false;return;}
|
||||
var pos=camera.position();var target=sample(client.level,pos.x,pos.y,pos.z);
|
||||
if(!wasUnderwater){fogFrom=fogTarget=target;fogChanged=System.nanoTime();wasUnderwater=true;}
|
||||
else if(!target.equals(fogTarget)){fogFrom=fogProfile();fogTarget=target;fogChanged=System.nanoTime();}
|
||||
});
|
||||
}
|
||||
/** Preference changes only rebuild loaded water meshes, retaining unrelated shader caches. */
|
||||
private static void invalidateWaterSections(Minecraft client){
|
||||
var level=client.level;
|
||||
var center=BlockPos.containing(client.gameRenderer.mainCamera().position());
|
||||
int radius=client.options.getEffectiveRenderDistance()+1;
|
||||
for(int z=(center.getZ()>>4)-radius;z<=(center.getZ()>>4)+radius;z++)
|
||||
for(int x=(center.getX()>>4)-radius;x<=(center.getX()>>4)+radius;x++){
|
||||
var chunk=level.getChunkSource().getChunk(x,z,net.minecraft.world.level.chunk.status.ChunkStatus.FULL,false);
|
||||
if(chunk==null)continue;
|
||||
var sections=chunk.getSections();
|
||||
for(int i=0;i<sections.length;i++)if(!sections[i].hasOnlyAir()
|
||||
&§ions[i].getStates().maybeHas(state->state.getFluidState().is(FluidTags.WATER)))
|
||||
client.levelExtractor.setSectionDirty(x,level.getSectionYFromSectionIndex(i),z);
|
||||
}
|
||||
}
|
||||
private static Map<Block,Integer> defaults(){
|
||||
var result=new HashMap<Block,Integer>();
|
||||
for(String name:new String[]{"sand","red_sand","gravel","clay"})result.put(BuiltInRegistries.BLOCK.getValue(Identifier.withDefaultNamespace(name)),1);
|
||||
for(String name:new String[]{"dirt","coarse_dirt","rooted_dirt","grass_block","podzol","mycelium","mud","muddy_mangrove_roots"})result.put(BuiltInRegistries.BLOCK.getValue(Identifier.withDefaultNamespace(name)),2);
|
||||
return Map.copyOf(result);
|
||||
}
|
||||
public static float strength(){return strength;}
|
||||
public static Profile fogProfile(){return fogFrom.mix(fogTarget,Math.clamp((System.nanoTime()-fogChanged)/500_000_000f,0,1));}
|
||||
public static boolean ocean(ClientLevel level,BlockPos pos){return level.hasChunk(pos.getX()>>4,pos.getZ()>>4)&&level.getBiome(pos).is(BiomeTags.IS_OCEAN);}
|
||||
private static int classify(java.util.function.Function<BlockPos,BlockState> read,BlockPos pos,Map<Block,Integer> table){
|
||||
var cursor=pos.mutable();
|
||||
for(int i=1;i<=DEPTH;i++){
|
||||
cursor.set(pos.getX(),pos.getY()-i,pos.getZ());var state=read.apply(cursor);
|
||||
if(state==null||state.isAir())return 0;
|
||||
if(state.isSolid()||!state.getCollisionShape(net.minecraft.world.level.EmptyBlockGetter.INSTANCE,cursor).isEmpty()){
|
||||
// Plants have no collision; a waterlogged solid remains a basin floor.
|
||||
return table.getOrDefault(state.getBlock(),0);
|
||||
}
|
||||
if(!state.getFluidState().is(FluidTags.WATER))return 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
public static Profile sample(ClientLevel level,double x,double y,double z){
|
||||
var pos=BlockPos.containing(x,y,z);if(ocean(level,pos))return CLEAR;
|
||||
int bx=(int)Math.floor(x-.5),bz=(int)Math.floor(z-.5),by=pos.getY();
|
||||
float dx=(float)(x-.5-bx),dz=(float)(z-.5-bz);
|
||||
return live(level,bx,by,bz).mix(live(level,bx+1,by,bz),dx).mix(live(level,bx,by,bz+1).mix(live(level,bx+1,by,bz+1),dx),dz);
|
||||
}
|
||||
private static Profile live(ClientLevel l,int x,int y,int z){
|
||||
var p=new BlockPos(x,y,z);if(!l.hasChunk(x>>4,z>>4)||ocean(l,p))return CLEAR;
|
||||
float tint=0,alpha=0,fog=0;int count=0;
|
||||
for(int dz=-blendRadius;dz<=blendRadius;dz++)for(int dx=-blendRadius;dx<=blendRadius;dx++){
|
||||
var q=p.offset(dx,0,dz);
|
||||
Profile v=!l.hasChunk(q.getX()>>4,q.getZ()>>4)||ocean(l,q)?CLEAR:PROFILES[classify(l::getBlockState,q,materials)];
|
||||
tint+=v.tint;alpha+=v.alpha;fog+=v.fog;count++;
|
||||
}
|
||||
return new Profile(tint/count,alpha/count,fog/count);
|
||||
}
|
||||
/** Owned by a single native section compilation, with ocean data captured before worker submission. */
|
||||
public static final class Region {
|
||||
private static final int PAD=9,SIDE=34;
|
||||
private final int radius=blendRadius;
|
||||
private final float[][][] sums=new float[16][][];
|
||||
private final int ox,oy,oz;
|
||||
private final net.minecraft.world.level.biome.BiomeManager biomes;
|
||||
private byte[] oceanKinds;
|
||||
private byte[] profiles;
|
||||
private final Map<Block,Integer> table=materials;
|
||||
public final float intensity=strength;
|
||||
public Region(ClientLevel level,int minX,int minY,int minZ){
|
||||
ox=(minX+1)*16;oy=(minY+1)*16;oz=(minZ+1)*16;
|
||||
// Copy quart holders, then keep the native seeded biome zoom on the immutable copy.
|
||||
// Sampling only quart centres would misclassify the jagged edges of ocean biomes.
|
||||
var copied=new java.util.ArrayList<net.minecraft.core.Holder<net.minecraft.world.level.biome.Biome>>(600);
|
||||
int qx=(ox>>2)-3,qy=(oy>>2)-1,qz=(oz>>2)-3;
|
||||
for(int y=0;y<6;y++)for(int z=0;z<10;z++)for(int x=0;x<10;x++)
|
||||
copied.add(level.hasChunk((qx+x)>>2,(qz+z)>>2)?level.getBiomeManager().getNoiseBiomeAtQuart(qx+x,qy+y,qz+z):null);
|
||||
biomes=level.getBiomeManager().withDifferentSource((x,y,z)->copied.get(((y-qy)*10+z-qz)*10+x-qx));
|
||||
}
|
||||
public boolean ocean(int x,int y,int z){
|
||||
if(oceanKinds==null)oceanKinds=new byte[SIDE*16*SIDE];
|
||||
int key=((y-oy)*SIDE+z-oz+PAD)*SIDE+x-ox+PAD;
|
||||
int code=oceanKinds[key];if(code==0){var biome=biomes.getBiome(x,y,z);code=biome!=null&&biome.is(BiomeTags.IS_OCEAN)?2:1;oceanKinds[key]=(byte)code;}
|
||||
return code==2;
|
||||
}
|
||||
private Profile profile(BlockAndTintGetter region,int x,int y,int z){
|
||||
if(x<ox-PAD||x>=ox-PAD+SIDE||y<oy||y>oy+15||z<oz-PAD||z>=oz-PAD+SIDE)return CLEAR;
|
||||
if(ocean(x,y,z))return CLEAR;
|
||||
int key=((y-oy)*SIDE+z-oz+PAD)*SIDE+x-ox+PAD;
|
||||
if(profiles==null)profiles=new byte[SIDE*16*SIDE];
|
||||
int code=profiles[key];if(code==0){code=1+classify(region::getBlockState,new BlockPos(x,y,z),table);profiles[key]=(byte)code;}
|
||||
return PROFILES[code-1];
|
||||
}
|
||||
private Profile blended(BlockAndTintGetter region,int x,int y,int z){
|
||||
if(radius==0)return profile(region,x,y,z);
|
||||
int layer=y-oy,stride=SIDE+1;
|
||||
if(sums[layer]==null){
|
||||
var sum=new float[3][stride*stride];
|
||||
for(int iz=0;iz<SIDE;iz++)for(int ix=0;ix<SIDE;ix++){
|
||||
var p=profile(region,ox-PAD+ix,y,oz-PAD+iz);int at=(iz+1)*stride+ix+1;
|
||||
float[] values={p.tint,p.alpha,p.fog};
|
||||
for(int c=0;c<3;c++)sum[c][at]=values[c]+sum[c][at-1]+sum[c][at-stride]-sum[c][at-stride-1];
|
||||
}
|
||||
sums[layer]=sum;
|
||||
}
|
||||
int left=x-ox+PAD-radius,top=z-oz+PAD-radius,right=left+radius*2+1,bottom=top+radius*2+1;
|
||||
float[] values=new float[3];float area=(radius*2+1)*(radius*2+1);
|
||||
for(int c=0;c<3;c++){var a=sums[layer][c];values[c]=(a[bottom*stride+right]-a[top*stride+right]-a[bottom*stride+left]+a[top*stride+left])/area;}
|
||||
return new Profile(values[0],values[1],values[2]);
|
||||
}
|
||||
public Profile corner(BlockAndTintGetter region,BlockPos p,int dx,int dz){
|
||||
if(ocean(p.getX(),p.getY(),p.getZ()))return CLEAR;
|
||||
int x=p.getX()+dx-1,y=p.getY(),z=p.getZ()+dz-1;
|
||||
return blended(region,x,y,z).mix(blended(region,x+1,y,z),.5f).mix(blended(region,x,y,z+1).mix(blended(region,x+1,y,z+1),.5f),.5f);
|
||||
}
|
||||
}
|
||||
public interface RegionAccess { Region sanctuary$waterRegion(); }
|
||||
public static int color(int color,int base,Profile profile,float intensity){
|
||||
float tint=profile.tint*intensity;
|
||||
int r=color>>16&255,g=color>>8&255,b=color&255;
|
||||
// Restore directional face shading after tinting, including custom biome/resource-pack colours.
|
||||
float shade=(r+g+b)/(float)Math.max(1,(base>>16&255)+(base>>8&255)+(base&255));
|
||||
int rr=Math.clamp(Math.round(r*(1-tint)+120*shade*tint),0,255);
|
||||
int gg=Math.clamp(Math.round(g*(1-tint)+132*shade*tint),0,255);
|
||||
int bb=Math.clamp(Math.round(b*(1-tint)+72*shade*tint),0,255);
|
||||
int a=Math.round((color>>>24)*(1+(profile.alpha-1)*intensity));
|
||||
return a<<24|rr<<16|gg<<8|bb;
|
||||
}
|
||||
public static void dirty(BlockPos pos){
|
||||
if(strength==0)return;
|
||||
var renderer=Minecraft.getInstance().levelExtractor;
|
||||
int margin=blendRadius+1;
|
||||
renderer.setSectionRangeDirty((pos.getX()-margin)>>4,pos.getY()>>4,(pos.getZ()-margin)>>4,
|
||||
(pos.getX()+margin)>>4,(pos.getY()+DEPTH)>>4,(pos.getZ()+margin)>>4);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Map;
|
||||
import java.util.WeakHashMap;
|
||||
import net.minecraft.client.renderer.texture.SpriteContents;
|
||||
|
||||
/** References the actual atlas clock, so pause, reload and custom frame durations stay aligned. */
|
||||
public final class PbrAnimation {
|
||||
public interface Clock { int sanctuary$frame(); int sanctuary$subFrame(); }
|
||||
// AnimationState retains its owning sprite; both sides must be weak across atlas reloads.
|
||||
private static final Map<SpriteContents,WeakReference<Clock>> clocks=new WeakHashMap<>();
|
||||
private PbrAnimation(){}
|
||||
public static void register(SpriteContents sprite,SpriteContents.AnimationState state){
|
||||
if(state!=null)clocks.put(sprite,new WeakReference<>((Clock)state));
|
||||
}
|
||||
static Clock clock(SpriteContents sprite){var clock=clocks.get(sprite);return clock==null?null:clock.get();}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import com.mojang.blaze3d.vertex.VertexConsumer;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.block.FluidRenderer;
|
||||
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
/** Replays the native fluid tessellator, including slope, flow UVs and waterlogged faces. */
|
||||
final class PbrFluidMesh implements VertexConsumer {
|
||||
private final VertexConsumer target;
|
||||
private final PbrMaterials materials;
|
||||
private final TextureAtlasSprite[] sprites;
|
||||
private final float[][] vertices=new float[4][5];
|
||||
private final int sky;
|
||||
private int cursor,quads;
|
||||
private PbrFluidMesh(VertexConsumer target,PbrMaterials materials,TextureAtlasSprite[] sprites,int sky){
|
||||
this.target=target;this.materials=materials;this.sprites=sprites;this.sky=sky;
|
||||
}
|
||||
static int emit(VertexConsumer target,PbrMaterials materials,net.minecraft.client.multiplayer.ClientLevel level,BlockPos pos,BlockState state){
|
||||
var models=Minecraft.getInstance().getModelManager().getFluidStateModelSet();
|
||||
var model=models.get(state.getFluidState());
|
||||
var sink=new PbrFluidMesh(target,materials,new TextureAtlasSprite[]{model.stillMaterial().sprite(),model.flowingMaterial().sprite(),model.overlayMaterial()==null?null:model.overlayMaterial().sprite()},
|
||||
level.getBrightness(net.minecraft.world.level.LightLayer.SKY,pos.above()));
|
||||
new FluidRenderer(models).tesselate(level,pos,layer->sink,state,state.getFluidState());
|
||||
return sink.quads;
|
||||
}
|
||||
@Override public void addVertex(float x,float y,float z,int color,float u,float v,int overlay,int light,float nx,float ny,float nz){
|
||||
vertices[cursor++]=new float[]{x,y,z,u,v};
|
||||
if(cursor!=4)return;
|
||||
cursor=0;float cu=0,cv=0;for(var p:vertices){cu+=p[3]*.25f;cv+=p[4]*.25f;}
|
||||
TextureAtlasSprite sprite=null;
|
||||
for(var candidate:sprites)if(candidate!=null&&cu>=candidate.getU0()&&cu<=candidate.getU1()&&cv>=candidate.getV0()&&cv<=candidate.getV1()){sprite=candidate;break;}
|
||||
if(sprite==null)return;
|
||||
var slot=materials.slot(sprite,net.minecraft.world.level.block.Blocks.WATER.defaultBlockState());
|
||||
if(slot.empty())return;
|
||||
for(var p:vertices)target.addVertex(p[0],p[1],p[2]).setColor((Math.round(sky/15f*255)<<24)|(color&0xFFFFFF))
|
||||
.setUv(p[3],p[4]).setUv3(slot.u((p[3]-sprite.getU0())/(sprite.getU1()-sprite.getU0())),slot.v((p[4]-sprite.getV0())/(sprite.getV1()-sprite.getV0()))+4);
|
||||
quads++;
|
||||
}
|
||||
// FluidRenderer uses the packed overload above. Fail visibly if that native contract changes.
|
||||
public VertexConsumer addVertex(float x,float y,float z){throw new UnsupportedOperationException("Unpacked fluid vertex");}
|
||||
public VertexConsumer setColor(int r,int g,int b,int a){return this;}
|
||||
public VertexConsumer setColor(int c){return this;}
|
||||
public VertexConsumer setUv(float u,float v){return this;}
|
||||
public VertexConsumer setUv1(int u,int v){return this;}
|
||||
public VertexConsumer setUv2(int u,int v){return this;}
|
||||
public VertexConsumer setUv3(float u,float v){return this;}
|
||||
public VertexConsumer setNormal(float x,float y,float z){return this;}
|
||||
public VertexConsumer setLineWidth(float w){return this;}
|
||||
}
|
||||
@@ -17,10 +17,18 @@ final class PbrMaterials implements AutoCloseable {
|
||||
}
|
||||
private final Map<String,Slot> slots=new HashMap<>();
|
||||
private final Map<BlockState,Profile> profiles=new IdentityHashMap<>();
|
||||
private final List<WaterAnimation> animations=new ArrayList<>();
|
||||
private DynamicTexture atlas;private boolean dirty;private int analyses;
|
||||
boolean candidate(BlockState state){return !state.isAir()&&!state.liquid();}
|
||||
static int family(BlockState state){
|
||||
String name=BuiltInRegistries.BLOCK.getKey(state.getBlock()).getPath();
|
||||
return state.getFluidState().is(net.minecraft.tags.FluidTags.WATER)&&state.liquid()?2:
|
||||
(name.equals("glass")||name.endsWith("_glass")||name.equals("glass_pane")||name.endsWith("_glass_pane"))?1:0;
|
||||
}
|
||||
boolean candidate(BlockState state){return !state.isAir();}
|
||||
Profile profile(BlockState state){return profiles.computeIfAbsent(state,s->profile(BuiltInRegistries.BLOCK.getKey(s.getBlock()).getPath()));}
|
||||
static Profile profile(String name){
|
||||
if(name.equals("water"))return new Profile(.12f,0);
|
||||
if(name.contains("glass"))return new Profile(.16f,0);
|
||||
if(name.equals("iron_block")||name.equals("gold_block")||name.equals("netherite_block")||name.contains("copper")&&!name.contains("ore")&&!name.contains("raw"))
|
||||
return new Profile(name.contains("oxidized")?.65f:.32f,name.contains("oxidized")?.35f:.85f);
|
||||
if(name.contains("polished")||name.contains("glazed")||name.equals("diamond_block")||name.equals("emerald_block")||name.equals("ruby_block")||name.equals("sapphire_block"))return new Profile(.42f,0);
|
||||
@@ -39,7 +47,13 @@ final class PbrMaterials implements AutoCloseable {
|
||||
int fw=Math.min(image.getWidth(),sprite.contents().width()),fh=Math.min(image.getHeight(),sprite.contents().height());
|
||||
int[] colors=new int[INNER*INNER];
|
||||
for(int py=0;py<INNER;py++)for(int px=0;px<INNER;px++)colors[py*INNER+px]=image.getPixel(px*fw/INNER,py*fh/INNER);
|
||||
int family=family(state);
|
||||
// Pigmented glass can have low alpha and still carry real texture relief.
|
||||
// Fully transparent glass texels keep the geometric-normal fallback.
|
||||
if(family==2||family==1)for(int j=0;j<colors.length;j++)
|
||||
if(family==2||(colors[j]>>>24)>0)colors[j]|=0xFF000000;
|
||||
int[] pixels=generate(colors,INNER,INNER,profile);analyses++;
|
||||
if(family(state)==2)animations.add(new WaterAnimation(sprite,image,profile,x,y));
|
||||
for(int py=0;py<TILE;py++)for(int px=0;px<TILE;px++)atlas.getPixels().setPixel(x+px,y+py,pixels[Math.clamp(py-2,0,INNER-1)*INNER+Math.clamp(px-2,0,INNER-1)]);
|
||||
}catch(java.io.IOException e){empty=true;fr.koka.sanctuary.SanctuaryMod.LOGGER.warn("Cannot analyse PBR texture {}",sprite.contents().name(),e);}
|
||||
var result=new Slot(x,y,empty);slots.put(key,result);dirty=true;return result;
|
||||
@@ -59,7 +73,52 @@ final class PbrMaterials implements AutoCloseable {
|
||||
}
|
||||
private static float height(int color,float fallback){return (color>>>24)<128?fallback:luma(color);}
|
||||
private static float luma(int c){return ((c>>16&255)*.2126f+(c>>8&255)*.7152f+(c&255)*.0722f)/255;}
|
||||
DynamicTexture atlas(){if(dirty){atlas.upload();dirty=false;}return atlas;}
|
||||
DynamicTexture atlas(){
|
||||
if(dirty){atlas.upload();dirty=false;for(var animation:animations)animation.lastFrame=-1;}
|
||||
if(atlas!=null)for(var animation:animations)animation.update(atlas);
|
||||
return atlas;
|
||||
}
|
||||
private static final class WaterAnimation {
|
||||
final TextureAtlasSprite sprite;final int x,y;final int[][] normals;final int[] frames,times;final boolean interpolate;
|
||||
final NativeImage tile=new NativeImage(TILE,TILE,false);int lastFrame=-1,lastSub=-1,updates;
|
||||
WaterAnimation(TextureAtlasSprite sprite,NativeImage image,Profile profile,int x,int y)throws java.io.IOException{
|
||||
this.sprite=sprite;this.x=x;this.y=y;
|
||||
int w=sprite.contents().width(),h=sprite.contents().height(),columns=image.getWidth()/w,count=columns*(image.getHeight()/h);
|
||||
// Bound custom resource-pack memory; unsupported huge animations keep their initial normal.
|
||||
boolean capped=count>1024;
|
||||
if(capped){fr.koka.sanctuary.SanctuaryMod.LOGGER.warn("PBR water animation {} exceeds 1024 frames; using a fixed normal",sprite.contents().name());count=1;}
|
||||
normals=new int[count][];
|
||||
for(int f=0;f<count;f++){
|
||||
int[] colors=new int[INNER*INNER];
|
||||
for(int py=0;py<INNER;py++)for(int px=0;px<INNER;px++)colors[py*INNER+px]=image.getPixel((f%columns)*w+px*w/INNER,(f/columns)*h+py*h/INNER)|0xFF000000;
|
||||
normals[f]=generate(colors,INNER,INNER,profile);
|
||||
}
|
||||
var metadata=net.minecraft.client.Minecraft.getInstance().getResourceManager().getResourceOrThrow(BloomMaterials.texture(sprite.contents().name()))
|
||||
.metadata().getSection(net.minecraft.client.resources.metadata.animation.AnimationMetadataSection.TYPE);
|
||||
var sequence=new ArrayList<Integer>();var durations=new ArrayList<Integer>();
|
||||
if(metadata.isPresent()&&!capped){
|
||||
var m=metadata.get();interpolate=m.interpolatedFrames();
|
||||
if(m.frames().isPresent())for(var frame:m.frames().get()){
|
||||
if(frame.index()>=0&&frame.index()<count&&frame.timeOr(m.defaultFrameTime())>0){sequence.add(frame.index());durations.add(frame.timeOr(m.defaultFrameTime()));}
|
||||
}else for(int f=0;f<count;f++){sequence.add(f);durations.add(m.defaultFrameTime());}
|
||||
}else interpolate=false;
|
||||
if(sequence.isEmpty()){sequence.add(0);durations.add(1);}
|
||||
frames=sequence.stream().mapToInt(Integer::intValue).toArray();times=durations.stream().mapToInt(Integer::intValue).toArray();
|
||||
}
|
||||
void update(DynamicTexture atlas){
|
||||
var clock=PbrAnimation.clock(sprite.contents());int f=clock==null?0:Math.floorMod(clock.sanctuary$frame(),frames.length),sub=clock==null||!interpolate?0:clock.sanctuary$subFrame();
|
||||
if(f==lastFrame&&sub==lastSub)return;lastFrame=f;lastSub=sub;updates++;
|
||||
float t=interpolate?Math.clamp(sub/(float)times[f],0,1):0;
|
||||
int[] a=normals[frames[f]],b=normals[frames[(f+1)%frames.length]];
|
||||
for(int py=0;py<TILE;py++)for(int px=0;px<TILE;px++){
|
||||
int i=Math.clamp(py-2,0,INNER-1)*INNER+Math.clamp(px-2,0,INNER-1);
|
||||
int r=Math.round((a[i]>>16&255)*(1-t)+(b[i]>>16&255)*t),g=Math.round((a[i]>>8&255)*(1-t)+(b[i]>>8&255)*t);
|
||||
tile.setPixel(px,py,(a[i]&0xFF0000FF)|(r<<16)|(g<<8));
|
||||
}
|
||||
com.mojang.blaze3d.systems.RenderSystem.getDevice().createCommandEncoder().writeToTexture(atlas.getTexture(),tile,0,0,x,y);
|
||||
}
|
||||
}
|
||||
int animationUpdates(){return animations.stream().mapToInt(a->a.updates).sum();}
|
||||
int count(){return slots.size();}int analyses(){return analyses;}
|
||||
public void close(){if(atlas!=null){atlas.close();atlas=null;}slots.clear();profiles.clear();dirty=false;analyses=0;}
|
||||
public void close(){if(atlas!=null){atlas.close();atlas=null;}for(var animation:animations)animation.tile.close();animations.clear();slots.clear();profiles.clear();dirty=false;analyses=0;}
|
||||
}
|
||||
|
||||
@@ -25,11 +25,19 @@ final class PbrTerrain implements AutoCloseable {
|
||||
static final VertexFormat FORMAT=VertexFormat.builder(0).addAttribute("Position",GpuFormat.RGB32_FLOAT)
|
||||
.addAttribute("Color",GpuFormat.RGBA8_UNORM).addAttribute("UV0",GpuFormat.RG32_FLOAT)
|
||||
.addAttribute("UV3",GpuFormat.RG32_FLOAT).build();
|
||||
private static final long MAX_BYTES=24L*1024*1024;
|
||||
private final boolean transparent;
|
||||
private final long maxBytes;
|
||||
PbrTerrain(){this(false);}
|
||||
PbrTerrain(boolean transparent){this.transparent=transparent;maxBytes=(transparent?8L:24L)*1024*1024;}
|
||||
private boolean candidate(net.minecraft.world.level.block.state.BlockState state){
|
||||
return transparent ? PbrMaterials.family(state)==1||state.getFluidState().is(net.minecraft.tags.FluidTags.WATER)
|
||||
: materials.candidate(state)&&!state.liquid()&&PbrMaterials.family(state)==0;
|
||||
}
|
||||
private final Map<Long,Section> sections=new HashMap<>();
|
||||
final PbrMaterials materials=new PbrMaterials();
|
||||
private Build building;
|
||||
private long bytes;
|
||||
private int completed;
|
||||
private Object models,level;
|
||||
private List<Section> visible=List.of();
|
||||
record Entry(BlockPos origin,GpuBuffer vertices,int indices){}
|
||||
@@ -40,12 +48,16 @@ final class PbrTerrain implements AutoCloseable {
|
||||
long frame=mc.level.getGameTime();var active=new ArrayList<Section>();
|
||||
for(var render:mc.levelRenderer.visibleSections()){
|
||||
long key=render.getSectionNode();var pos=render.getRenderOrigin();
|
||||
if(pos.offset(8,8,8).distToCenterSqr(camera)>48*48)continue;
|
||||
if(pos.offset(8,8,8).distToCenterSqr(camera)>Math.pow(VanillaLight.pbrDistance()+16,2))continue;
|
||||
var chunk=mc.level.getChunkSource().getChunk(pos.getX()>>4,pos.getZ()>>4,ChunkStatus.FULL,false);
|
||||
if(chunk==null)continue;
|
||||
var storage=chunk.getSection(mc.level.getSectionIndex(pos.getY()));
|
||||
var section=sections.get(key);
|
||||
if(section==null||section.storage!=storage){if(section!=null)remove(section);section=new Section(key,pos.immutable(),storage);sections.put(key,section);}
|
||||
if(section==null||section.storage!=storage){
|
||||
Entry retained=section==null?null:section.entry;
|
||||
if(section!=null){section.entry=null;remove(section);}
|
||||
section=new Section(key,pos.immutable(),storage);section.entry=retained;sections.put(key,section);
|
||||
}
|
||||
section.seen=frame;active.add(section);
|
||||
}
|
||||
visible=active;
|
||||
@@ -56,23 +68,30 @@ final class PbrTerrain implements AutoCloseable {
|
||||
if(started++>=4)break;
|
||||
Section next=active.stream().filter(s->s.dirty).min(Comparator.comparingDouble(s->s.origin.distToCenterSqr(camera))).orElse(null);
|
||||
if(next==null)break;
|
||||
if(!next.storage.maybeHas(materials::candidate)){next.dirty=false;continue;}
|
||||
if(!next.storage.maybeHas(this::candidate)){releaseEntry(next);next.dirty=false;continue;}
|
||||
building=new Build(next);
|
||||
}
|
||||
if(!advance(building,deadline))break;
|
||||
var done=building;building=null;
|
||||
try(done){
|
||||
done.section.dirty=false;
|
||||
done.section.dirty=done.section.revision!=done.revision;
|
||||
Entry replacement=null;
|
||||
if(done.quads>0&&!done.overflow){
|
||||
try(var mesh=done.out.buildOrThrow()){
|
||||
long size=mesh.vertexBuffer().remaining();
|
||||
if(bytes+size<=MAX_BYTES){
|
||||
long replaced=done.section.entry==null?0:done.section.entry.vertices.size();
|
||||
if(bytes-replaced+size<=maxBytes){
|
||||
var buffer=RenderSystem.getDevice().createBuffer(()->"Sanctuary PBR surfaces",GpuBuffer.USAGE_VERTEX|GpuBuffer.USAGE_COPY_DST,mesh.vertexBuffer());
|
||||
int count=mesh.drawState().indexCount();RenderSystem.getSequentialBuffer(PrimitiveTopology.QUADS).getBuffer(count);
|
||||
done.section.entry=new Entry(done.section.origin,buffer,count);bytes+=buffer.size();
|
||||
replacement=new Entry(done.section.origin,buffer,count);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Publish atomically. A dirty section stays visible while its new
|
||||
// geometry is built, including when light updates arrive mid-build.
|
||||
releaseEntry(done.section);done.section.entry=replacement;
|
||||
if(replacement!=null)bytes+=replacement.vertices.size();
|
||||
completed++;
|
||||
}
|
||||
}
|
||||
return active.stream().map(s->s.entry).filter(Objects::nonNull).toList();
|
||||
@@ -82,7 +101,13 @@ final class PbrTerrain implements AutoCloseable {
|
||||
var parts=new ArrayList<BlockStateModelPart>();var random=RandomSource.create();
|
||||
while(build.cursor<4096){
|
||||
int i=build.cursor++,x=i&15,z=i>>4&15,y=i>>8;var state=build.section.storage.getBlockState(x,y,z);
|
||||
if(materials.candidate(state)){
|
||||
if(candidate(state)){
|
||||
if(transparent&&state.getFluidState().is(net.minecraft.tags.FluidTags.WATER)){
|
||||
int emitted=PbrFluidMesh.emit(build.out,materials,mc.level,build.section.origin.offset(x,y,z),state);
|
||||
build.quads+=emitted;
|
||||
if(build.quads>=16384){build.overflow=true;return true;}
|
||||
}
|
||||
if(state.liquid() || (transparent&&PbrMaterials.family(state)!=1)){if(System.nanoTime()>=deadline)return build.cursor==4096;continue;}
|
||||
var pos=build.section.origin.offset(x,y,z);
|
||||
parts.clear();random.setSeed(state.getSeed(pos));if(!state.liquid())models.get(state).collectParts(random,parts);
|
||||
var tintSources=mc.getBlockColors().getTintSources(state);
|
||||
@@ -104,7 +129,7 @@ final class PbrTerrain implements AutoCloseable {
|
||||
var p=quad.position(v);float u=UVPair.unpackU(quad.packedUV(v)),tv=UVPair.unpackV(quad.packedUV(v));
|
||||
float localU=(u-sprite.getU0())/(sprite.getU1()-sprite.getU0()),localV=(tv-sprite.getV0())/(sprite.getV1()-sprite.getV0());
|
||||
build.out.addVertex(x+p.x()+(float)offset.x,y+p.y()+(float)offset.y,z+p.z()+(float)offset.z)
|
||||
.setColor(color).setUv(u,tv).setUv3(slot.u(localU),slot.v(localV));
|
||||
.setColor(color).setUv(u,tv).setUv3(slot.u(localU),slot.v(localV)+2*PbrMaterials.family(state));
|
||||
}
|
||||
build.quads++;
|
||||
}
|
||||
@@ -118,18 +143,23 @@ final class PbrTerrain implements AutoCloseable {
|
||||
void dirtySection(int x,int y,int z){dirty(sections.get(SectionPos.asLong(x,y,z)));}
|
||||
void dirty(BlockPos pos){for(int x=(pos.getX()-1)>>4;x<=(pos.getX()+1)>>4;x++)for(int y=(pos.getY()-1)>>4;y<=(pos.getY()+1)>>4;y++)for(int z=(pos.getZ()-1)>>4;z<=(pos.getZ()+1)>>4;z++)dirty(sections.get(SectionPos.asLong(x,y,z)));}
|
||||
void dirtyChunk(int x,int z){for(var section:sections.values())if(Math.abs((section.origin.getX()>>4)-x)<=1&&Math.abs((section.origin.getZ()>>4)-z)<=1)dirty(section);}
|
||||
private void dirty(Section section){if(section!=null){remove(section);section.dirty=true;}}
|
||||
private void remove(Section section){if(building!=null&&building.section==section){building.close();building=null;}if(section.entry!=null){bytes-=section.entry.vertices.size();section.entry.vertices.close();section.entry=null;}}
|
||||
private void dirty(Section section){if(section!=null){section.revision++;section.dirty=true;}}
|
||||
private void releaseEntry(Section section){if(section.entry!=null){bytes-=section.entry.vertices.size();section.entry.vertices.close();section.entry=null;}}
|
||||
private void remove(Section section){if(building!=null&&building.section==section){building.close();building=null;}releaseEntry(section);}
|
||||
long allocatedBytes(){return bytes;}
|
||||
int entryCount(){return (int)visible.stream().filter(s->s.entry!=null).count();}
|
||||
int completed(){return completed;}
|
||||
boolean building(){return building!=null;}
|
||||
boolean settled(){return building==null&&visible.stream().noneMatch(s->s.dirty);}
|
||||
public void close(){if(building!=null){building.close();building=null;}for(var section:sections.values())remove(section);sections.clear();visible=List.of();materials.close();models=level=null;bytes=0;}
|
||||
private static final class Section{
|
||||
final long key;final BlockPos origin;final LevelChunkSection storage;long seen;boolean dirty=true;Entry entry;
|
||||
final long key;final BlockPos origin;final LevelChunkSection storage;long seen,revision;boolean dirty=true;Entry entry;
|
||||
Section(long key,BlockPos origin,LevelChunkSection storage){this.key=key;this.origin=origin;this.storage=storage;}
|
||||
}
|
||||
private static final class Build implements AutoCloseable{
|
||||
final Section section;final ByteBufferBuilder buffer=new ByteBufferBuilder(65536);final BufferBuilder out=new BufferBuilder(buffer,PrimitiveTopology.QUADS,FORMAT);
|
||||
final Section section;final long revision;final ByteBufferBuilder buffer=new ByteBufferBuilder(65536);final BufferBuilder out=new BufferBuilder(buffer,PrimitiveTopology.QUADS,FORMAT);
|
||||
int cursor,quads;boolean overflow;
|
||||
Build(Section section){this.section=section;}
|
||||
Build(Section section){this.section=section;revision=section.revision;}
|
||||
public void close(){buffer.close();}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import com.mojang.blaze3d.buffers.Std140Builder;
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.renderpearl.api.GpuFormat;
|
||||
import com.mojang.renderpearl.api.buffers.GpuBuffer;
|
||||
import com.mojang.renderpearl.api.commands.RenderPassDescriptor;
|
||||
import com.mojang.renderpearl.api.pipeline.*;
|
||||
import com.mojang.renderpearl.api.textures.*;
|
||||
import fr.koka.sanctuary.client.panorama.PanoramaCapture;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.BindGroupLayouts;
|
||||
import net.minecraft.client.renderer.MappableRingBuffer;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.client.renderer.texture.DynamicTexture;
|
||||
import net.minecraft.client.renderer.texture.TextureAtlas;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.level.material.FogType;
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Vector3f;
|
||||
import org.joml.Vector4f;
|
||||
import java.util.*;
|
||||
|
||||
/** Bounded single-bounce landscape reflection. Never samples the target being written. */
|
||||
public final class PixelReflections {
|
||||
private static final BindGroupLayout LAYOUT=BindGroupLayout.builder()
|
||||
.withUniform("ReflectionScene",UniformType.UNIFORM_BUFFER)
|
||||
.withUniform("OpaqueDepth",UniformType.COMBINED_IMAGE_SAMPLER)
|
||||
.withUniform("OpaqueColor",UniformType.COMBINED_IMAGE_SAMPLER)
|
||||
.withUniform("ReceiverDepth",UniformType.COMBINED_IMAGE_SAMPLER)
|
||||
.withUniform("SceneColor",UniformType.COMBINED_IMAGE_SAMPLER)
|
||||
.withUniform("BlockAtlas",UniformType.COMBINED_IMAGE_SAMPLER)
|
||||
.withUniform("MaterialAtlas",UniformType.COMBINED_IMAGE_SAMPLER).build();
|
||||
private static RenderPipeline pipeline(String name,boolean depth){
|
||||
var b=RenderPipeline.builder().withLocation(Identifier.parse("sanctuary:pipeline/"+name))
|
||||
.withVertexShader(Identifier.parse("sanctuary:core/pbr"))
|
||||
.withFragmentShader(Identifier.parse("sanctuary:core/"+name))
|
||||
.withBindGroupLayout(BindGroupLayouts.DYNAMIC_TRANSFORMS).withBindGroupLayout(LAYOUT)
|
||||
.withVertexBinding(0,PbrTerrain.FORMAT).withPrimitiveTopology(PrimitiveTopology.QUADS).withCull(true);
|
||||
if(depth)b.withDepthStencilState(new DepthStencilState(CompareOp.GREATER_THAN,true));
|
||||
else b.withColorTargetState(new ColorTargetState(Optional.empty(),GpuFormat.RGBA8_UNORM,ColorTargetState.WRITE_COLOR)).withDepthStencilState(new DepthStencilState(CompareOp.EQUAL,false));
|
||||
return RenderPipelines.register(b.build());
|
||||
}
|
||||
private static final RenderPipeline DEPTH=pipeline("pbr_receiver",true),REFLECT=pipeline("pixel_reflections",false);
|
||||
private static GpuTexture opaqueDepth,opaqueColor;
|
||||
private static GpuTextureView opaqueDepthView,opaqueColorView;
|
||||
private static int width,height;
|
||||
private static GpuTexture receiver;
|
||||
private static GpuTextureView receiverView;
|
||||
private static MappableRingBuffer uniform;
|
||||
private static Matrix4f vp=new Matrix4f();
|
||||
private static List<PbrTerrain.Entry> solid=List.of(),transparent=List.of();
|
||||
private static DynamicTexture solidAtlas,transparentAtlas;
|
||||
private static boolean captured,ready,applied;
|
||||
private static int frames;
|
||||
private PixelReflections(){}
|
||||
public static boolean eligible(){
|
||||
var mc=Minecraft.getInstance();
|
||||
return mc.level!=null&&VanillaLight.active()&&VanillaLight.pbr()&&VanillaLight.pbrIntensity()>0&&!PanoramaCapture.rendering()
|
||||
&&mc.gameRenderer.gameRenderState().levelRenderState.cameraRenderState.fogType==FogType.NONE
|
||||
&&mc.gameRenderer.gameRenderState().levelRenderState.skyRenderState.skybox==net.minecraft.world.level.dimension.DimensionType.Skybox.OVERWORLD;
|
||||
}
|
||||
public static void beginFrame(){MaterialBloom.beginFrame();captured=ready=applied=false;if(!eligible())release();}
|
||||
/** Called only with the opaque render pass closed, on both native transparency paths. */
|
||||
public static void captureOpaque(){
|
||||
if(!eligible())return;
|
||||
var mc=Minecraft.getInstance();var main=mc.gameRenderer.mainRenderTarget();
|
||||
var device=RenderSystem.getDevice();
|
||||
int usage=GpuTexture.USAGE_RENDER_ATTACHMENT|GpuTexture.USAGE_TEXTURE_BINDING|GpuTexture.USAGE_COPY_DST|GpuTexture.USAGE_COPY_SRC;
|
||||
if(opaqueDepth==null||width!=main.width||height!=main.height){
|
||||
release();width=main.width;height=main.height;
|
||||
opaqueDepth=device.createTexture(()->"Sanctuary opaque depth",usage,main.getDepthTexture().getFormat(),width,height,1,1);
|
||||
opaqueDepthView=device.createTextureView(opaqueDepth);
|
||||
receiver=device.createTexture(()->"Sanctuary nearest PBR surface",usage,main.getDepthTexture().getFormat(),width,height,1,1);
|
||||
receiverView=device.createTextureView(receiver);
|
||||
}
|
||||
boolean enabled=VanillaLight.ssr()&&VanillaLight.ssrIntensity()>0;
|
||||
if(enabled&&opaqueColor==null){
|
||||
opaqueColor=device.createTexture(()->"Sanctuary reflection landscape",usage,GpuFormat.RGBA8_UNORM,width,height,1,1);
|
||||
opaqueColorView=device.createTextureView(opaqueColor);
|
||||
}else if(!enabled&&opaqueColor!=null){opaqueColorView.close();opaqueColor.close();opaqueColorView=null;opaqueColor=null;}
|
||||
var encoder=device.createCommandEncoder();
|
||||
encoder.copyTextureToTexture(main.getDepthTexture(),opaqueDepth,0,0,0,0,0,width,height);
|
||||
if(enabled)encoder.copyTextureToTexture(main.getColorTexture(),opaqueColor,0,0,0,0,0,width,height);
|
||||
captured=true;
|
||||
}
|
||||
static void prepare(Matrix4f matrix,List<PbrTerrain.Entry> solids,DynamicTexture sa,List<PbrTerrain.Entry> liquids,DynamicTexture ta){
|
||||
if(!captured)return;
|
||||
vp.set(matrix);solid=solids;transparent=liquids;solidAtlas=sa;transparentAtlas=ta;
|
||||
if(uniform==null)uniform=new MappableRingBuffer(()->"Sanctuary reflection parameters",GpuBuffer.USAGE_UNIFORM|GpuBuffer.USAGE_MAP_WRITE,160);
|
||||
uniform.rotate();try(var map=uniform.currentBuffer().map(false,true)){
|
||||
Std140Builder.intoBuffer(map.data()).putMat4f(vp).putMat4f(new Matrix4f(vp).invert())
|
||||
.putVec4(RenderSystem.getDevice().getDeviceInfo().isZZeroToOne()?1:0,VanillaLight.ssrIntensity()/100f,VanillaLight.pbrIntensity()/100f,VanillaLight.pbrDistance())
|
||||
.putVec4(128,32,0,0);
|
||||
}
|
||||
int max=java.util.stream.Stream.concat(solid.stream(),transparent.stream()).mapToInt(PbrTerrain.Entry::indices).max().orElse(0);
|
||||
RenderSystem.getSequentialBuffer(PrimitiveTopology.QUADS).requestIndexCount(max);
|
||||
var encoder=RenderSystem.getDevice().createCommandEncoder();
|
||||
try(var pass=encoder.createRenderPass(RenderPassDescriptor.builder(()->"Sanctuary nearest material")
|
||||
.withDepthAttachment(receiverView,OptionalDouble.of(0)).build())){
|
||||
pass.setPipeline(RenderSystem.getCompiledPipeline(DEPTH));
|
||||
bind(pass,Minecraft.getInstance().gameRenderer.mainRenderTarget().getColorTextureView(),false);
|
||||
draw(pass,solid,solidAtlas);draw(pass,transparent,transparentAtlas);
|
||||
}
|
||||
ready=true;
|
||||
}
|
||||
static GpuTextureView depth(){return ready?receiverView:null;}
|
||||
private static void bind(com.mojang.renderpearl.api.commands.RenderPass pass,GpuTextureView scene,boolean reflection){
|
||||
var nearest=RenderSystem.getSamplerCache().getClampToEdge(FilterMode.NEAREST);
|
||||
pass.setUniform("ReflectionScene",uniform.currentBuffer());
|
||||
pass.setUniform("OpaqueDepth",opaqueDepthView,nearest);
|
||||
pass.setUniform("OpaqueColor",opaqueColorView!=null?opaqueColorView:scene,nearest);
|
||||
// Never bind the receiver as a sampled texture while it is attached for depth writes.
|
||||
pass.setUniform("ReceiverDepth",reflection?Minecraft.getInstance().gameRenderer.mainRenderTarget().getDepthTextureView():opaqueDepthView,nearest);
|
||||
pass.setUniform("SceneColor",scene,nearest);
|
||||
pass.setUniform("BlockAtlas",Minecraft.getInstance().getTextureManager().getTexture(TextureAtlas.LOCATION_BLOCKS).getTextureView(),nearest);
|
||||
}
|
||||
private static void draw(com.mojang.renderpearl.api.commands.RenderPass pass,List<PbrTerrain.Entry> entries,DynamicTexture atlas){
|
||||
if(atlas==null||entries.isEmpty())return;
|
||||
pass.setUniform("MaterialAtlas",atlas.getTextureView(),RenderSystem.getSamplerCache().getClampToEdge(FilterMode.NEAREST));
|
||||
var indices=RenderSystem.getSequentialBuffer(PrimitiveTopology.QUADS);
|
||||
var camera=Minecraft.getInstance().gameRenderer.gameRenderState().levelRenderState.cameraRenderState;
|
||||
for(var e:entries){
|
||||
pass.setIndexBuffer(indices.getBuffer(),indices.type());
|
||||
var p=e.origin();var offset=new Vector3f((float)(p.getX()-camera.pos.x),(float)(p.getY()-camera.pos.y),(float)(p.getZ()-camera.pos.z));
|
||||
pass.setUniform("DynamicTransforms",RenderSystem.getDynamicUniforms().writeTransform(new Matrix4f(vp).translate(offset),new Vector4f(1,1,1,1),offset,new Matrix4f()));
|
||||
pass.setVertexBuffer(0,e.vertices().slice());pass.drawIndexed(e.indices(),1,0,0,0);
|
||||
}
|
||||
}
|
||||
public static void render(){
|
||||
if(!ready||!VanillaLight.ssr()||VanillaLight.ssrIntensity()==0)return;
|
||||
var main=Minecraft.getInstance().gameRenderer.mainRenderTarget();
|
||||
var scene=ProceduralPbr.copyScene();
|
||||
try(var pass=RenderSystem.getDevice().createCommandEncoder().createRenderPass(()->"Sanctuary pixel reflections",main.getColorTextureView(),Optional.empty(),receiverView,OptionalDouble.empty())){
|
||||
pass.setPipeline(RenderSystem.getCompiledPipeline(REFLECT));bind(pass,scene,true);
|
||||
draw(pass,solid,solidAtlas);draw(pass,transparent,transparentAtlas);
|
||||
}
|
||||
MaterialBloom.accumulate(scene);
|
||||
applied=true;frames++;
|
||||
}
|
||||
public static void release(){
|
||||
if(opaqueColorView!=null){opaqueColorView.close();opaqueColorView=null;}if(opaqueColor!=null){opaqueColor.close();opaqueColor=null;}
|
||||
if(opaqueDepthView!=null){opaqueDepthView.close();opaqueDepthView=null;}if(opaqueDepth!=null){opaqueDepth.close();opaqueDepth=null;}if(receiverView!=null){receiverView.close();receiverView=null;}
|
||||
if(receiver!=null){receiver.close();receiver=null;}if(uniform!=null){uniform.close();uniform=null;}
|
||||
solid=transparent=List.of();solidAtlas=transparentAtlas=null;captured=ready=applied=false;
|
||||
}
|
||||
static long targetBytes(){return (long)width*height*((opaqueDepth!=null?4:0)+(receiver!=null?4:0)+(opaqueColor!=null?4:0));}
|
||||
static GpuTexture landscape(){return opaqueColor;}
|
||||
static boolean applied(){return applied;}static int frames(){return frames;}
|
||||
static boolean allocated(){return opaqueColor!=null;}
|
||||
}
|
||||
@@ -91,7 +91,10 @@ public final class PixelShadows {
|
||||
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);
|
||||
@@ -142,7 +145,7 @@ public final class PixelShadows {
|
||||
}
|
||||
}
|
||||
depthReady = !lunar;
|
||||
if (!VanillaLight.pixelShadows()) { renderedFrames++; return; }
|
||||
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())) {
|
||||
|
||||
@@ -28,23 +28,25 @@ public final class ProceduralPbr {
|
||||
private static final BindGroupLayout SCENE=BindGroupLayout.builder().withUniform("PbrScene",UniformType.UNIFORM_BUFFER)
|
||||
.withUniform("PixelShadows",UniformType.UNIFORM_BUFFER).withUniform("SunDepth",UniformType.COMBINED_IMAGE_SAMPLER)
|
||||
.withUniform("WorldDepth",UniformType.COMBINED_IMAGE_SAMPLER).withUniform("SceneColor",UniformType.COMBINED_IMAGE_SAMPLER)
|
||||
.withUniform("ColoredLightScene",UniformType.UNIFORM_BUFFER).withUniform("LightDirection",UniformType.COMBINED_IMAGE_SAMPLER).withUniform("PreviousDirection",UniformType.COMBINED_IMAGE_SAMPLER)
|
||||
.withUniform("BlockAtlas",UniformType.COMBINED_IMAGE_SAMPLER).withUniform("MaterialAtlas",UniformType.COMBINED_IMAGE_SAMPLER).build();
|
||||
private static final RenderPipeline PIPELINE=RenderPipelines.register(RenderPipeline.builder()
|
||||
.withLocation(Identifier.parse("sanctuary:pipeline/pbr")).withVertexShader(Identifier.parse("sanctuary:core/pbr"))
|
||||
.withFragmentShader(Identifier.parse("sanctuary:core/pbr")).withBindGroupLayout(BindGroupLayouts.DYNAMIC_TRANSFORMS).withBindGroupLayout(SCENE)
|
||||
.withVertexBinding(0,PbrTerrain.FORMAT).withPrimitiveTopology(PrimitiveTopology.QUADS).withCull(false)
|
||||
.withVertexBinding(0,PbrTerrain.FORMAT).withPrimitiveTopology(PrimitiveTopology.QUADS).withCull(true)
|
||||
.withColorTargetState(new ColorTargetState(Optional.empty(),GpuFormat.RGBA8_UNORM,ColorTargetState.WRITE_COLOR))
|
||||
.withDepthStencilState(Optional.empty()).build());
|
||||
.withDepthStencilState(new DepthStencilState(CompareOp.EQUAL,false)).build());
|
||||
private static final PbrTerrain TERRAIN=new PbrTerrain();
|
||||
private static final PbrTerrain TRANSPARENT=new PbrTerrain(true);
|
||||
private static final Matrix4f projection=new Matrix4f();
|
||||
private static TextureTarget source;
|
||||
private static MappableRingBuffer scene;
|
||||
private static boolean applied;private static int frames;
|
||||
private ProceduralPbr(){}
|
||||
public static void captureProjection(Matrix4f matrix){projection.set(matrix);}
|
||||
public static void dirtyBlock(BlockPos pos){TERRAIN.dirty(pos);}
|
||||
public static void dirtyChunk(int x,int z){TERRAIN.dirtyChunk(x,z);}
|
||||
public static void dirtySection(int x,int y,int z){TERRAIN.dirtySection(x,y,z);}
|
||||
public static void dirtyBlock(BlockPos pos){TERRAIN.dirty(pos);TRANSPARENT.dirty(pos);}
|
||||
public static void dirtyChunk(int x,int z){TERRAIN.dirtyChunk(x,z);TRANSPARENT.dirtyChunk(x,z);}
|
||||
public static void dirtySection(int x,int y,int z){if(!fr.koka.sanctuary.client.DynamicLightClient.invalidatingSections()){TERRAIN.dirtySection(x,y,z);TRANSPARENT.dirtySection(x,y,z);}}
|
||||
public static void render(){
|
||||
applied=false;var mc=Minecraft.getInstance();
|
||||
if(!VanillaLight.active()||!VanillaLight.pbr()||VanillaLight.pbrIntensity()==0||mc.level==null){release();return;}
|
||||
@@ -52,10 +54,14 @@ public final class ProceduralPbr {
|
||||
if(PanoramaCapture.rendering()||camera.fogType!=FogType.NONE||sky.skybox!=DimensionType.Skybox.OVERWORLD)return;
|
||||
var main=mc.gameRenderer.mainRenderTarget();
|
||||
if(source==null||source.width!=main.width||source.height!=main.height){if(source!=null)source.destroyBuffers();source=new TextureTarget("Sanctuary PBR source",main.width,main.height,GpuFormat.RGBA8_UNORM,null);}
|
||||
// The extra padding also supplies a valid inactive PixelShadows binding (192 bytes).
|
||||
if(scene==null)scene=new MappableRingBuffer(()->"Sanctuary PBR scene",GpuBuffer.USAGE_UNIFORM|GpuBuffer.USAGE_MAP_WRITE,192);
|
||||
var entries=TERRAIN.update();var atlas=TERRAIN.materials.atlas();
|
||||
// Padding supplies valid inactive PixelShadows and local-light uniform bindings.
|
||||
if(scene==null)scene=new MappableRingBuffer(()->"Sanctuary PBR scene",GpuBuffer.USAGE_UNIFORM|GpuBuffer.USAGE_MAP_WRITE,176+ColoredDynamicLights.LIMIT*32);
|
||||
var solidEntries=TERRAIN.update();var atlas=TERRAIN.materials.atlas();
|
||||
var transparentEntries=TRANSPARENT.update();var transparentAtlas=TRANSPARENT.materials.atlas();
|
||||
var entries=new ArrayList<PbrTerrain.Entry>(solidEntries);entries.addAll(transparentEntries);
|
||||
var vp=new Matrix4f(projection).mul(camera.viewRotationMatrix);var fog=camera.fogData;
|
||||
PixelReflections.prepare(vp,solidEntries,atlas,transparentEntries,transparentAtlas);
|
||||
if(PixelReflections.depth()==null)return;
|
||||
var sun=PixelShadows.sunDirection(sky.sunAngle);boolean lunar=sun.y<=.08f;
|
||||
if(lunar)sun=PixelShadows.sunDirection(sky.moonAngle);
|
||||
float moon=sky.moonPhase==null?1:(float)(.5+.5*Math.cos(sky.moonPhase.index()*Math.PI/4));
|
||||
@@ -64,8 +70,8 @@ public final class ProceduralPbr {
|
||||
scene.rotate();try(var mapped=scene.currentBuffer().map(false,true)){
|
||||
Std140Builder.intoBuffer(mapped.data()).putMat4f(new Matrix4f(vp).invert())
|
||||
.putVec4(sun.x,sun.y,sun.z,strength)
|
||||
.putVec4(VanillaLight.pbrIntensity()/100f,RenderSystem.getDevice().getDeviceInfo().isZZeroToOne()?1:0,shadow?1:0,32)
|
||||
.putVec4(fog.renderDistanceStart,fog.renderDistanceEnd,fog.environmentalStart,fog.environmentalEnd).putVec4(fog.color.w,0,0,0)
|
||||
.putVec4(VanillaLight.pbrIntensity()/100f,RenderSystem.getDevice().getDeviceInfo().isZZeroToOne()?1:0,shadow?1:0,VanillaLight.pbrDistance())
|
||||
.putVec4(fog.renderDistanceStart,fog.renderDistanceEnd,fog.environmentalStart,fog.environmentalEnd).putVec4(fog.color.w,ColoredLights.directionalReady()?1:0,0,0)
|
||||
.putVec4(0,0,0,0).putVec4(0,0,0,0).putVec4(0,0,0,0).putVec4(0,0,0,0);
|
||||
}
|
||||
var transforms=new ArrayList<GpuBufferSlice>(entries.size());int max=0;
|
||||
@@ -75,19 +81,36 @@ public final class ProceduralPbr {
|
||||
var encoder=RenderSystem.getDevice().createCommandEncoder();
|
||||
encoder.copyTextureToTexture(main.getColorTexture(),source.getColorTexture(),0,0,0,0,0,main.width,main.height);
|
||||
var nearest=RenderSystem.getSamplerCache().getClampToEdge(FilterMode.NEAREST);
|
||||
if(atlas!=null&&!entries.isEmpty())try(var pass=encoder.createRenderPass(()->"Sanctuary PBR materials",main.getColorTextureView(),Optional.empty())){
|
||||
if((atlas!=null||transparentAtlas!=null)&&!entries.isEmpty())try(var pass=encoder.createRenderPass(()->"Sanctuary PBR materials",main.getColorTextureView(),Optional.empty(),PixelReflections.depth(),OptionalDouble.empty())){
|
||||
pass.setPipeline(RenderSystem.getCompiledPipeline(PIPELINE));pass.setUniform("PbrScene",scene.currentBuffer());
|
||||
pass.setUniform("PixelShadows",shadow?PixelShadows.materialUniforms():scene.currentBuffer());
|
||||
pass.setUniform("SunDepth",shadow?PixelShadows.materialDepth():main.getDepthTextureView(),nearest);
|
||||
pass.setUniform("WorldDepth",main.getDepthTextureView(),nearest);pass.setUniform("SceneColor",source.getColorTextureView(),nearest);
|
||||
pass.setUniform("BlockAtlas",mc.getTextureManager().getTexture(TextureAtlas.LOCATION_BLOCKS).getTextureView(),nearest);
|
||||
pass.setUniform("MaterialAtlas",atlas.getTextureView(),nearest);pass.setIndexBuffer(indices.getBuffer(),indices.type());
|
||||
for(int i=0;i<entries.size();i++){var entry=entries.get(i);pass.setUniform("DynamicTransforms",transforms.get(i));pass.setVertexBuffer(0,entry.vertices().slice());pass.drawIndexed(entry.indices(),1,0,0,0);}
|
||||
pass.setUniform("MaterialAtlas",(atlas!=null?atlas:transparentAtlas).getTextureView(),nearest);
|
||||
if(ColoredLights.directionalReady())ColoredLights.bindDirections(pass);
|
||||
else{pass.setUniform("ColoredLightScene",scene.currentBuffer());pass.setUniform("LightDirection",source.getColorTextureView(),nearest);pass.setUniform("PreviousDirection",source.getColorTextureView(),nearest);}
|
||||
pass.setIndexBuffer(indices.getBuffer(),indices.type());
|
||||
for(int i=0;i<entries.size();i++){var entry=entries.get(i);
|
||||
pass.setUniform("MaterialAtlas",(i<solidEntries.size()?atlas:transparentAtlas).getTextureView(),nearest);pass.setUniform("DynamicTransforms",transforms.get(i));pass.setVertexBuffer(0,entry.vertices().slice());pass.drawIndexed(entry.indices(),1,0,0,0);}
|
||||
}
|
||||
MaterialBloom.accumulate(source.getColorTextureView());
|
||||
applied=true;frames++;
|
||||
}
|
||||
public static void release(){TERRAIN.close();if(source!=null){source.destroyBuffers();source=null;}if(scene!=null){scene.close();scene=null;}applied=false;}
|
||||
static boolean applied(){return applied;}static boolean ready(){return TERRAIN.settled();}
|
||||
static com.mojang.renderpearl.api.textures.GpuTextureView copyScene(){
|
||||
var main=Minecraft.getInstance().gameRenderer.mainRenderTarget();
|
||||
RenderSystem.getDevice().createCommandEncoder().copyTextureToTexture(main.getColorTexture(),source.getColorTexture(),0,0,0,0,0,main.width,main.height);
|
||||
return source.getColorTextureView();
|
||||
}
|
||||
public static void release(){MaterialBloom.release();TERRAIN.close();TRANSPARENT.close();PixelReflections.release();if(source!=null){source.destroyBuffers();source=null;}if(scene!=null){scene.close();scene=null;}applied=false;}
|
||||
static long solidBytes(){return TERRAIN.allocatedBytes();}
|
||||
static long transparentBytes(){return TRANSPARENT.allocatedBytes();}
|
||||
static int waterUpdates(){return TRANSPARENT.materials.animationUpdates();}
|
||||
static int waterAnalyses(){return TRANSPARENT.materials.analyses();}
|
||||
static boolean applied(){return applied;}static boolean ready(){return TERRAIN.settled()&&TRANSPARENT.settled();}
|
||||
static boolean allocated(){return source!=null;}static int renderedFrames(){return frames;}
|
||||
static int terrainEntries(){return TERRAIN.entryCount();}
|
||||
static int terrainBuilds(){return TERRAIN.completed();}
|
||||
static boolean terrainBuilding(){return TERRAIN.building();}
|
||||
static int materialCount(){return TERRAIN.materials.count();}static int analyses(){return TERRAIN.materials.analyses();}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,12 @@ public final class ShaderOptionsScreen extends OptionsSubScreen {
|
||||
list.addBig(OptionInstance.createBoolean("sanctuary.shaders.vanilla_light",OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable(VanillaLight.externalEngine()?"sanctuary.shaders.external":"sanctuary.shaders.help")),
|
||||
VanillaLight.enabled(),VanillaLight::setEnabled));
|
||||
list.addBig(OptionInstance.createBoolean("sanctuary.shaders.natural_water",OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders.natural_water_help")),VanillaLight.naturalWater(),VanillaLight::setNaturalWater));
|
||||
slider("water_intensity",VanillaLight.waterIntensity(),VanillaLight::setWaterIntensity);
|
||||
list.addBig(new OptionInstance<Integer>("sanctuary.shaders.water_blend",OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders.water_blend_help")),(caption,value)->Component.translatable("sanctuary.shaders.water_blend_value",value*2+1,value*2+1),
|
||||
new OptionInstance.Enum<>(List.of(0,1,2,4,8),Codec.INT),VanillaLight.waterBlendRadius(),VanillaLight::setWaterBlendRadius));
|
||||
list.addBig(new OptionInstance<Integer>("sanctuary.shaders.saturation",OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders.saturation_help")),(caption,value)->Component.translatable("sanctuary.shaders.saturation_value",value),
|
||||
new OptionInstance.IntRange(0,200),VanillaLight.saturation(),VanillaLight::setSaturation));
|
||||
@@ -23,6 +29,20 @@ public final class ShaderOptionsScreen extends OptionsSubScreen {
|
||||
list.addBig(OptionInstance.createBoolean("sanctuary.shaders.pbr",OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders.pbr_help")),VanillaLight.pbr(),VanillaLight::setPbr));
|
||||
slider("pbr_intensity",VanillaLight.pbrIntensity(),VanillaLight::setPbrIntensity);
|
||||
list.addBig(OptionInstance.createBoolean("sanctuary.shaders.ssr",OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders.ssr_help")),VanillaLight.ssr(),VanillaLight::setSsr));
|
||||
slider("ssr_intensity",VanillaLight.ssrIntensity(),VanillaLight::setSsrIntensity);
|
||||
list.addBig(new OptionInstance<Integer>("sanctuary.shaders.pbr_distance",OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders.pbr_distance_help")),(caption,value)->Component.translatable("sanctuary.shaders.pbr_distance_value",value),
|
||||
new OptionInstance.Enum<>(List.of(16,32,64,128),Codec.INT),VanillaLight.pbrDistance(),VanillaLight::setPbrDistance));
|
||||
list.addBig(OptionInstance.createBoolean("sanctuary.shaders.colored_lights",OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders.colored_lights_help")),VanillaLight.coloredLights(),VanillaLight::setColoredLights));
|
||||
list.addBig(OptionInstance.createBoolean("sanctuary.shaders.warm_torches",OptionInstance.cachedConstantTooltip(
|
||||
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);
|
||||
|
||||
@@ -45,11 +45,21 @@ public final class Sunshafts {
|
||||
if(!PixelShadows.depthReady()){release();return;}
|
||||
var main=mc.gameRenderer.mainRenderTarget();int width=Math.max(1,(main.width+3)/4),height=Math.max(1,(main.height+3)/4);
|
||||
if(light==null||light.width!=width||light.height!=height){release();light=new TextureTarget("Sanctuary sunshafts",width,height,GpuFormat.RGBA16_FLOAT,null);}
|
||||
if(settings==null)settings=new MappableRingBuffer(()->"Sanctuary sunshafts",GpuBuffer.USAGE_UNIFORM|GpuBuffer.USAGE_MAP_WRITE,32);
|
||||
var fog=mc.gameRenderer.gameRenderState().levelRenderState.cameraRenderState.fogData;
|
||||
if(settings==null)settings=new MappableRingBuffer(()->"Sanctuary sunshafts",GpuBuffer.USAGE_UNIFORM|GpuBuffer.USAGE_MAP_WRITE,48);
|
||||
var camera=mc.gameRenderer.gameRenderState().levelRenderState.cameraRenderState;
|
||||
var fog=camera.fogData;
|
||||
float mist=fogDensity(fog.environmentalEnd);
|
||||
float rain=mc.level.getRainLevel(camera.cameraEntityPartialTicks);
|
||||
float thunder=mc.level.getThunderLevel(camera.cameraEntityPartialTicks);
|
||||
var sun=PixelShadows.sunDirection(mc.gameRenderer.gameRenderState().levelRenderState.skyRenderState.sunAngle);
|
||||
float daylight=Math.clamp((sun.y-.08f)/.16f,0,1)*(1-.75f*rain)*(1-.65f*thunder);
|
||||
float density=1+2.5f*mist+.8f*rain;
|
||||
float spread=Math.max(mist,.35f*rain);
|
||||
float extinction=1+.7f*mist+.25f*rain;
|
||||
settings.rotate();try(var mapped=settings.currentBuffer().map(false,true)){
|
||||
Std140Builder.intoBuffer(mapped.data()).putVec4(.85f+.15f*fog.color.x,.78f+.15f*fog.color.y,.66f+.15f*fog.color.z,0)
|
||||
.putVec4(VanillaLight.sunshaftIntensity()/100f,Math.min(64,VanillaLight.shadowDistance()*.5f),0,0);
|
||||
.putVec4(VanillaLight.sunshaftIntensity()/100f,Math.min(64,VanillaLight.shadowDistance()*.5f),0,0)
|
||||
.putVec4(density,spread,extinction,daylight);
|
||||
}
|
||||
var encoder=RenderSystem.getDevice().createCommandEncoder();var nearest=RenderSystem.getSamplerCache().getClampToEdge(FilterMode.NEAREST);
|
||||
try(var pass=encoder.createRenderPass(()->"Sanctuary solar volume",light.getColorTextureView(),Optional.empty())){
|
||||
@@ -62,6 +72,12 @@ public final class Sunshafts {
|
||||
}
|
||||
applied=true;frames++;
|
||||
}
|
||||
/** Environmental fog has its own end distance; changing render distance is not weather. */
|
||||
static float fogDensity(float environmentalEnd){
|
||||
if(!Float.isFinite(environmentalEnd))return 0;
|
||||
float t=Math.clamp((192-environmentalEnd)/112,0,1);
|
||||
return t*t*(3-2*t);
|
||||
}
|
||||
public static void release(){if(light!=null){light.destroyBuffers();light=null;}if(settings!=null){settings.close();settings=null;}applied=false;}
|
||||
static boolean applied(){return applied;}
|
||||
static boolean allocated(){return light!=null;}
|
||||
|
||||
@@ -16,6 +16,11 @@ public final class VanillaLight {
|
||||
private VanillaLight() { }
|
||||
private static final class Preferences {
|
||||
boolean enabled=true;
|
||||
Boolean naturalWater; // Legacy OFF is an explicit opt-out in both world types.
|
||||
Boolean sanctuaryNaturalWater;
|
||||
Boolean ordinaryNaturalWater;
|
||||
int waterBlendRadius=4;
|
||||
int waterIntensity=100;
|
||||
int saturation=142;
|
||||
boolean pixelShadows=true;
|
||||
boolean bloom=true;
|
||||
@@ -24,8 +29,15 @@ public final class VanillaLight {
|
||||
boolean lensFlare=true;
|
||||
int lensFlareIntensity=30;
|
||||
boolean pbr=true;
|
||||
int pbrIntensity=50;
|
||||
int pbrIntensity=80;
|
||||
int pbrDistance=32;
|
||||
boolean ssr=false;
|
||||
int ssrIntensity=20;
|
||||
boolean ssgi=true;
|
||||
boolean coloredLights=true;
|
||||
boolean warmTorches=false;
|
||||
int coloredLightIntensity=30;
|
||||
int coloredLightDistance=16;
|
||||
int ssgiIntensity=35;
|
||||
int bloomIntensity=20;
|
||||
int bloomRadius=50;
|
||||
@@ -43,15 +55,26 @@ public final class VanillaLight {
|
||||
try{if(Files.isRegularFile(path()))preferences=new GsonBuilder().create().fromJson(Files.readString(path()),Preferences.class);}
|
||||
catch(Exception e){SanctuaryMod.LOGGER.warn("Cannot read Sanctuary shader preference",e);}
|
||||
if(preferences==null)preferences=new Preferences();
|
||||
if(Boolean.FALSE.equals(preferences.naturalWater)){
|
||||
if(preferences.sanctuaryNaturalWater==null)preferences.sanctuaryNaturalWater=false;
|
||||
if(preferences.ordinaryNaturalWater==null)preferences.ordinaryNaturalWater=false;
|
||||
}
|
||||
preferences.naturalWater=null;
|
||||
preferences.sunshaftIntensity=Math.clamp(preferences.sunshaftIntensity,0,100);
|
||||
preferences.lensFlareIntensity=Math.clamp(preferences.lensFlareIntensity,0,100);
|
||||
preferences.ssrIntensity=Math.clamp(preferences.ssrIntensity,0,100);
|
||||
preferences.pbrDistance=normalizePbrDistance(preferences.pbrDistance);
|
||||
preferences.pbrIntensity=Math.clamp(preferences.pbrIntensity,0,100);
|
||||
preferences.coloredLightIntensity=Math.clamp(preferences.coloredLightIntensity,0,100);
|
||||
preferences.coloredLightDistance=normalizeColoredLightDistance(preferences.coloredLightDistance);
|
||||
preferences.ssgiIntensity=Math.clamp(preferences.ssgiIntensity,0,100);
|
||||
preferences.bloomIntensity=Math.clamp(preferences.bloomIntensity,0,100);
|
||||
preferences.bloomRadius=Math.clamp(preferences.bloomRadius,0,100);
|
||||
preferences.oreIntensity=Math.clamp(preferences.oreIntensity,0,100);
|
||||
preferences.edgeIntensity=Math.clamp(preferences.edgeIntensity,0,100);
|
||||
preferences.edgeDistance=normalizeShadowDistance(preferences.edgeDistance);
|
||||
preferences.waterBlendRadius=normalizeWaterBlendRadius(preferences.waterBlendRadius);
|
||||
preferences.waterIntensity=Math.clamp(preferences.waterIntensity,0,100);
|
||||
preferences.saturation=Math.clamp(preferences.saturation,0,200);
|
||||
preferences.shadowPixels=normalizePrecision(preferences.shadowPixels);
|
||||
preferences.shadowDistance=normalizeShadowDistance(preferences.shadowDistance);
|
||||
@@ -60,6 +83,14 @@ public final class VanillaLight {
|
||||
}
|
||||
public static boolean externalEngine(){return EXTERNAL_ENGINE;}
|
||||
public static boolean active(){return ProgressionClient.active()&&!EXTERNAL_ENGINE&&enabled();}
|
||||
public static boolean naturalWater(){return naturalWater(NaturalWater.sanctuaryWorld());}
|
||||
static boolean naturalWater(boolean sanctuary){enabled();Boolean value=sanctuary?preferences.sanctuaryNaturalWater:preferences.ordinaryNaturalWater;return value!=null?value:sanctuary;}
|
||||
public static int waterBlendRadius(){enabled();return preferences.waterBlendRadius;}
|
||||
public static void setWaterBlendRadius(int value){enabled();preferences.waterBlendRadius=normalizeWaterBlendRadius(value);save();}
|
||||
static int normalizeWaterBlendRadius(int value){return value<1?0:value<2?1:value<3?2:value<6?4:8;}
|
||||
public static int waterIntensity(){enabled();return preferences.waterIntensity;}
|
||||
public static void setNaturalWater(boolean value){enabled();if(NaturalWater.sanctuaryWorld())preferences.sanctuaryNaturalWater=value;else preferences.ordinaryNaturalWater=value;save();}
|
||||
public static void setWaterIntensity(int value){enabled();preferences.waterIntensity=Math.clamp(value,0,100);save();}
|
||||
public static int saturation(){enabled();return preferences.saturation;}
|
||||
public static boolean edgeHighlights(){enabled();return preferences.edgeHighlights;}
|
||||
public static int edgeDistance(){enabled();return preferences.edgeDistance;}
|
||||
@@ -75,10 +106,26 @@ public final class VanillaLight {
|
||||
public static int lensFlareIntensity(){enabled();return preferences.lensFlareIntensity;}
|
||||
public static void setLensFlare(boolean value){enabled();preferences.lensFlare=value;save();}
|
||||
public static void setLensFlareIntensity(int value){enabled();preferences.lensFlareIntensity=Math.clamp(value,0,100);save();}
|
||||
public static boolean ssr(){enabled();return preferences.ssr;}
|
||||
public static int ssrIntensity(){enabled();return preferences.ssrIntensity;}
|
||||
public static void setSsr(boolean value){enabled();preferences.ssr=value;save();}
|
||||
public static void setSsrIntensity(int value){enabled();preferences.ssrIntensity=Math.clamp(value,0,100);save();}
|
||||
public static int pbrDistance(){enabled();return preferences.pbrDistance;}
|
||||
public static void setPbrDistance(int value){enabled();preferences.pbrDistance=normalizePbrDistance(value);save();}
|
||||
static int normalizePbrDistance(int value){return value<24?16:value<48?32:value<96?64:128;}
|
||||
public static boolean pbr(){enabled();return preferences.pbr;}
|
||||
public static int pbrIntensity(){enabled();return preferences.pbrIntensity;}
|
||||
public static void setPbr(boolean value){enabled();preferences.pbr=value;save();}
|
||||
public static void setPbrIntensity(int value){enabled();preferences.pbrIntensity=Math.clamp(value,0,100);save();}
|
||||
public static boolean warmTorches(){enabled();return preferences.warmTorches;}
|
||||
public static void setWarmTorches(boolean value){enabled();preferences.warmTorches=value;save();}
|
||||
public static boolean coloredLights(){enabled();return preferences.coloredLights;}
|
||||
public static int coloredLightIntensity(){enabled();return preferences.coloredLightIntensity;}
|
||||
public static void setColoredLights(boolean value){enabled();preferences.coloredLights=value;save();}
|
||||
public static void setColoredLightIntensity(int value){enabled();preferences.coloredLightIntensity=Math.clamp(value,0,100);save();}
|
||||
public static int coloredLightDistance(){enabled();return preferences.coloredLightDistance;}
|
||||
public static void setColoredLightDistance(int value){enabled();preferences.coloredLightDistance=normalizeColoredLightDistance(value);save();}
|
||||
static int normalizeColoredLightDistance(int value){return value<12?8:value<24?16:value<48?32:64;}
|
||||
public static boolean ssgi(){enabled();return preferences.ssgi;}
|
||||
public static int ssgiIntensity(){enabled();return preferences.ssgiIntensity;}
|
||||
public static void setSsgi(boolean value){enabled();preferences.ssgi=value;save();}
|
||||
|
||||
@@ -7,7 +7,8 @@ import java.util.function.LongConsumer;
|
||||
/** Immutable render snapshots; entity collection and the dirty queue belong to the client tick. */
|
||||
public final class DynamicLightField {
|
||||
public record Id(boolean drop, long value) {}
|
||||
public record Source(Id id, double x, double y, double z, int light) {
|
||||
public record Source(Id id, double x, double y, double z, int light, int color) {
|
||||
public Source(Id id,double x,double y,double z,int light){this(id,x,y,z,light,0xFFFFFF);}
|
||||
public Source {
|
||||
if (!Double.isFinite(x+y+z) || light<1 || light>15) throw new IllegalArgumentException("light source");
|
||||
}
|
||||
@@ -20,9 +21,12 @@ public final class DynamicLightField {
|
||||
private final int cellSize;
|
||||
public Collector(int cellSize) {this.cellSize=Math.max(1,cellSize);}
|
||||
public void add(long entity,boolean drop,double x,double y,double z,int light) {
|
||||
add(entity,drop,x,y,z,light,0xFFFFFF);
|
||||
}
|
||||
public void add(long entity,boolean drop,double x,double y,double z,int light,int color) {
|
||||
if(light<=0)return;
|
||||
Id id=new Id(drop,drop ? sectionKey(floor(x/cellSize),floor(y/cellSize),floor(z/cellSize)) : entity);
|
||||
Source candidate=new Source(id,snap(x),snap(y),snap(z),Math.min(15,light));
|
||||
Source candidate=new Source(id,snap(x),snap(y),snap(z),Math.min(15,light),color);
|
||||
sources.merge(id,candidate,(a,b)->compareRepresentative(a,b)<=0?a:b);
|
||||
}
|
||||
public List<Source> finish(int limit,double x,double y,double z,long priorityEntity) {
|
||||
@@ -53,6 +57,7 @@ public final class DynamicLightField {
|
||||
lists.long2ObjectEntrySet().forEach(e->bins.put(e.getLongKey(),e.getValue().toArray(Source[]::new)));
|
||||
}
|
||||
public int size() {return sources.size();}
|
||||
public Collection<Source> sources(){return sources.values();}
|
||||
public int sectionCount() {return bins.size();}
|
||||
public int sampleCount(double x,double y,double z) {
|
||||
Source[] near=bins.get(sectionKey(floor(x)>>4,floor(y)>>4,floor(z)>>4));return near==null?0:near.length;
|
||||
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package fr.koka.sanctuary.mixin.client;
|
||||
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.*;
|
||||
import com.mojang.blaze3d.vertex.VertexConsumer;
|
||||
import fr.koka.sanctuary.client.shader.NaturalWater;
|
||||
import net.minecraft.client.color.block.BlockTintSource;
|
||||
import net.minecraft.client.renderer.block.*;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.tags.FluidTags;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.material.FluidState;
|
||||
import org.spongepowered.asm.mixin.*;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
/** Native renderer instances belong to one section compiler. Context is restored even on failure. */
|
||||
@Mixin(FluidRenderer.class)
|
||||
abstract class NaturalWaterFluidMixin {
|
||||
@Unique private NaturalWater.Profile[] sanctuary$corners;
|
||||
@Unique private NaturalWater.Region sanctuary$snapshot;
|
||||
@Unique private BlockAndTintGetter sanctuary$region;
|
||||
@Unique private BlockPos sanctuary$pos;
|
||||
@Unique private float sanctuary$strength;
|
||||
@Unique private int sanctuary$x,sanctuary$z,sanctuary$base;
|
||||
@WrapMethod(method="tesselate")
|
||||
private void sanctuary$water(BlockAndTintGetter region,BlockPos pos,FluidRenderer.Output output,BlockState state,FluidState fluid,Operation<Void> original){
|
||||
var previous=sanctuary$snapshot;
|
||||
try{
|
||||
sanctuary$corners=null;sanctuary$snapshot=null;sanctuary$base=-1;
|
||||
if(fluid.is(FluidTags.WATER)&®ion instanceof NaturalWater.RegionAccess access){
|
||||
var snapshot=access.sanctuary$waterRegion();
|
||||
if(snapshot!=null){
|
||||
sanctuary$strength=snapshot.intensity;sanctuary$x=pos.getX()&15;sanctuary$z=pos.getZ()&15;
|
||||
sanctuary$snapshot=snapshot;sanctuary$region=region;sanctuary$pos=pos;
|
||||
}
|
||||
}
|
||||
original.call(region,pos,output,state,fluid);
|
||||
}finally{sanctuary$snapshot=previous;sanctuary$corners=null;}
|
||||
}
|
||||
@WrapOperation(method="tesselate",at=@At(value="INVOKE",target="Lnet/minecraft/client/color/block/BlockTintSource;colorInWorld(Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/client/renderer/block/BlockAndTintGetter;Lnet/minecraft/core/BlockPos;)I"))
|
||||
private int sanctuary$base(BlockTintSource source,BlockState state,BlockAndTintGetter region,BlockPos pos,Operation<Integer> original){return sanctuary$base=original.call(source,state,region,pos);}
|
||||
@WrapOperation(method="vertex",at=@At(value="INVOKE",target="Lcom/mojang/blaze3d/vertex/VertexConsumer;addVertex(FFFIFFIIFFF)V"))
|
||||
private void sanctuary$vertex(VertexConsumer consumer,float x,float y,float z,int color,float u,float v,int overlay,int light,float nx,float ny,float nz,Operation<Void> original){
|
||||
if(sanctuary$snapshot!=null){
|
||||
if(sanctuary$corners==null){var s=sanctuary$snapshot;var r=sanctuary$region;var p=sanctuary$pos;
|
||||
sanctuary$corners=new NaturalWater.Profile[]{s.corner(r,p,0,0),s.corner(r,p,1,0),s.corner(r,p,0,1),s.corner(r,p,1,1)};}
|
||||
int corner=(x-sanctuary$x>.5f?1:0)+(z-sanctuary$z>.5f?2:0);color=NaturalWater.color(color,sanctuary$base,sanctuary$corners[corner],sanctuary$strength);}
|
||||
original.call(consumer,x,y,z,color,u,v,overlay,light,nx,ny,nz);
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package fr.koka.sanctuary.mixin.client;
|
||||
import fr.koka.sanctuary.client.shader.NaturalWater;
|
||||
import net.minecraft.client.Camera;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import net.minecraft.client.renderer.fog.FogRenderer;
|
||||
import net.minecraft.world.level.material.FogType;
|
||||
import org.joml.Vector4f;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.*;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
@Mixin(FogRenderer.class)
|
||||
abstract class NaturalWaterFogColorMixin {
|
||||
@Inject(method="computeFogColor",at=@At("RETURN"))
|
||||
private void sanctuary$waterColor(Camera camera,float ticks,ClientLevel level,int distance,float darken,Vector4f color,CallbackInfo ci){
|
||||
if(NaturalWater.strength()==0||camera.getFluidInCamera()!=FogType.WATER)return;
|
||||
float amount=NaturalWater.strength()*NaturalWater.fogProfile().tint();if(amount==0)return;
|
||||
// Apply after native water-vision normalization: normalizing olive to max=1 turns it neon yellow.
|
||||
// Retain native darkness/vision brightness, including a fully black blinded environment.
|
||||
float brightness=Math.max(color.x,Math.max(color.y,color.z));
|
||||
color.x+=(120/255f*brightness-color.x)*amount;
|
||||
color.y+=(132/255f*brightness-color.y)*amount;
|
||||
color.z+=(72/255f*brightness-color.z)*amount;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package fr.koka.sanctuary.mixin.client;
|
||||
import fr.koka.sanctuary.client.shader.NaturalWater;
|
||||
import net.minecraft.client.Camera;
|
||||
import net.minecraft.client.DeltaTracker;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import net.minecraft.client.renderer.fog.FogData;
|
||||
import net.minecraft.client.renderer.fog.environment.WaterFogEnvironment;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.*;
|
||||
import org.spongepowered.asm.mixin.injection.callback.*;
|
||||
@Mixin(WaterFogEnvironment.class)
|
||||
abstract class NaturalWaterFogMixin {
|
||||
@Inject(method="setupFog",at=@At("TAIL"))
|
||||
private void sanctuary$fog(FogData fog,Camera camera,ClientLevel level,float distance,DeltaTracker delta,CallbackInfo ci){
|
||||
float amount=NaturalWater.strength();if(amount==0)return;
|
||||
float factor=1+(NaturalWater.fogProfile().fog()-1)*amount;
|
||||
fog.environmentalStart*=factor;fog.environmentalEnd*=factor;fog.skyEnd*=factor;fog.cloudEnd*=factor;
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package fr.koka.sanctuary.mixin.client;
|
||||
import fr.koka.sanctuary.client.shader.NaturalWater;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import net.minecraft.client.renderer.chunk.RenderSectionRegion;
|
||||
import org.spongepowered.asm.mixin.*;
|
||||
import org.spongepowered.asm.mixin.injection.*;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
@Mixin(RenderSectionRegion.class)
|
||||
abstract class NaturalWaterRegionMixin implements NaturalWater.RegionAccess {
|
||||
@Shadow @Final private ClientLevel level;
|
||||
@Shadow @Final private net.minecraft.client.renderer.chunk.SectionCopy[] sections;
|
||||
@Shadow @Final private int minSectionX,minSectionY,minSectionZ;
|
||||
@Unique private NaturalWater.Region sanctuary$water;
|
||||
@Inject(method="<init>",at=@At("RETURN"))
|
||||
private void sanctuary$snapshot(CallbackInfo ci){
|
||||
if(NaturalWater.strength()==0)return;
|
||||
// Native 3×3×3 region, centre index 13. Palette rejection avoids 216 biome reads for dry terrain.
|
||||
var states=((NaturalWaterSectionCopyAccess)sections[13]).sanctuary$states();
|
||||
if(states!=null&&states.maybeHas(state->state.getFluidState().is(net.minecraft.tags.FluidTags.WATER)))
|
||||
sanctuary$water=new NaturalWater.Region(level,minSectionX,minSectionY,minSectionZ);
|
||||
}
|
||||
public NaturalWater.Region sanctuary$waterRegion(){return sanctuary$water;}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package fr.koka.sanctuary.mixin.client;
|
||||
import net.minecraft.client.renderer.chunk.SectionCopy;
|
||||
import net.minecraft.world.level.chunk.PalettedContainer;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.gen.Accessor;
|
||||
@Mixin(SectionCopy.class)
|
||||
interface NaturalWaterSectionCopyAccess {
|
||||
@Accessor("section") PalettedContainer<BlockState> sanctuary$states();
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package fr.koka.sanctuary.mixin.client;
|
||||
import fr.koka.sanctuary.client.shader.PbrAnimation;
|
||||
import net.minecraft.client.renderer.texture.SpriteContents;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.gen.Accessor;
|
||||
@Mixin(SpriteContents.AnimationState.class)
|
||||
public interface PbrAnimationClockMixin extends PbrAnimation.Clock {
|
||||
@Accessor("frame") int sanctuary$frame();
|
||||
@Accessor("subFrame") int sanctuary$subFrame();
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package fr.koka.sanctuary.mixin.client;
|
||||
import fr.koka.sanctuary.client.shader.PbrAnimation;
|
||||
import net.minecraft.client.renderer.texture.SpriteContents;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
@Mixin(SpriteContents.class)
|
||||
abstract class PbrAnimationSpriteMixin {
|
||||
@Inject(method="createAnimationState",at=@At("RETURN"))
|
||||
private void sanctuary$clock(CallbackInfoReturnable<SpriteContents.AnimationState> ci){
|
||||
PbrAnimation.register((SpriteContents)(Object)this,ci.getReturnValue());
|
||||
}
|
||||
}
|
||||
+5
-3
@@ -1,6 +1,7 @@
|
||||
package fr.koka.sanctuary.mixin.client;
|
||||
|
||||
import fr.koka.sanctuary.client.shader.PixelShadows;
|
||||
import fr.koka.sanctuary.client.shader.ColoredLights;
|
||||
import fr.koka.sanctuary.client.shader.ProceduralPbr;
|
||||
import fr.koka.sanctuary.client.shader.Bloom;
|
||||
import fr.koka.sanctuary.client.shader.ScreenSpaceGI;
|
||||
@@ -16,10 +17,11 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
abstract class PixelShadowDirtyMixin {
|
||||
@Inject(method="blockChanged", at=@At("HEAD"))
|
||||
private void sanctuary$dirty(BlockPos pos, int flags, CallbackInfo ci) {
|
||||
PixelShadows.dirtyBlock(pos); ProceduralPbr.dirtyBlock(pos); Bloom.dirtyBlock(pos);
|
||||
fr.koka.sanctuary.client.shader.NaturalWater.dirty(pos);
|
||||
PixelShadows.dirtyBlock(pos); ProceduralPbr.dirtyBlock(pos); Bloom.dirtyBlock(pos); ColoredLights.dirtyBlock(pos);
|
||||
}
|
||||
@Inject(method="allChanged", at=@At("HEAD"))
|
||||
private void sanctuary$reload(CallbackInfo ci) { PixelShadows.release(); ProceduralPbr.release(); Bloom.release(); ScreenSpaceGI.release(); }
|
||||
private void sanctuary$reload(CallbackInfo ci) { PixelShadows.release(); ProceduralPbr.release(); Bloom.release(); ColoredLights.release(); ScreenSpaceGI.release(); }
|
||||
@Inject(method="setSectionDirty", at=@At("HEAD"))
|
||||
private void sanctuary$pbrLight(int x,int y,int z,CallbackInfo ci){ProceduralPbr.dirtySection(x,y,z);}
|
||||
private void sanctuary$pbrLight(int x,int y,int z,CallbackInfo ci){ProceduralPbr.dirtySection(x,y,z); ColoredLights.dirtySection(x,y,z);}
|
||||
}
|
||||
|
||||
+4
-3
@@ -1,6 +1,7 @@
|
||||
package fr.koka.sanctuary.mixin.client;
|
||||
|
||||
import fr.koka.sanctuary.client.shader.PixelShadows;
|
||||
import fr.koka.sanctuary.client.shader.ColoredLights;
|
||||
import fr.koka.sanctuary.client.shader.ProceduralPbr;
|
||||
import fr.koka.sanctuary.client.shader.Bloom;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
@@ -17,11 +18,11 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
abstract class PixelShadowLevelMixin {
|
||||
@Inject(method="setBlocksDirty", at=@At("HEAD"))
|
||||
private void sanctuary$block(BlockPos pos, BlockState before, BlockState after, CallbackInfo ci) {
|
||||
if (before != after) {PixelShadows.dirtyBlock(pos); ProceduralPbr.dirtyBlock(pos);Bloom.dirtyBlock(pos);}
|
||||
if (before != after) {PixelShadows.dirtyBlock(pos); ProceduralPbr.dirtyBlock(pos);Bloom.dirtyBlock(pos); ColoredLights.dirtyBlock(pos);}
|
||||
}
|
||||
|
||||
@Inject(method="onChunkLoaded", at=@At("TAIL"))
|
||||
private void sanctuary$loaded(ChunkPos pos, CallbackInfo ci) { PixelShadows.dirtyChunk(pos.x(), pos.z()); ProceduralPbr.dirtyChunk(pos.x(), pos.z()); Bloom.dirtyChunk(pos.x(), pos.z()); }
|
||||
private void sanctuary$loaded(ChunkPos pos, CallbackInfo ci) { PixelShadows.dirtyChunk(pos.x(), pos.z()); ProceduralPbr.dirtyChunk(pos.x(), pos.z()); Bloom.dirtyChunk(pos.x(), pos.z()); ColoredLights.dirtyChunk(pos.x(),pos.z()); }
|
||||
@Inject(method="unload", at=@At("HEAD"))
|
||||
private void sanctuary$unload(LevelChunk chunk, CallbackInfo ci) { PixelShadows.dirtyChunk(chunk.getPos().x(), chunk.getPos().z()); ProceduralPbr.dirtyChunk(chunk.getPos().x(), chunk.getPos().z()); Bloom.dirtyChunk(chunk.getPos().x(), chunk.getPos().z()); }
|
||||
private void sanctuary$unload(LevelChunk chunk, CallbackInfo ci) { PixelShadows.dirtyChunk(chunk.getPos().x(), chunk.getPos().z()); ProceduralPbr.dirtyChunk(chunk.getPos().x(), chunk.getPos().z()); Bloom.dirtyChunk(chunk.getPos().x(), chunk.getPos().z()); ColoredLights.dirtyChunk(chunk.getPos().x(),chunk.getPos().z()); }
|
||||
}
|
||||
|
||||
+4
-2
@@ -4,6 +4,7 @@ import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
|
||||
import com.mojang.renderpearl.api.buffers.GpuBufferSlice;
|
||||
import fr.koka.sanctuary.client.shader.PixelShadows;
|
||||
import fr.koka.sanctuary.client.shader.ColoredLights;
|
||||
import fr.koka.sanctuary.client.shader.ProceduralPbr;
|
||||
import fr.koka.sanctuary.client.shader.EdgeHighlights;
|
||||
import fr.koka.sanctuary.client.shader.Bloom;
|
||||
@@ -25,11 +26,12 @@ abstract class PixelShadowRendererMixin {
|
||||
ProceduralPbr.captureProjection(matrix);
|
||||
EdgeHighlights.captureProjection(matrix);
|
||||
Bloom.captureProjection(matrix);
|
||||
ColoredLights.captureProjection(matrix);
|
||||
ScreenSpaceGI.captureProjection(matrix);
|
||||
return original.call(buffer, matrix);
|
||||
}
|
||||
@Inject(method="renderLevel", at=@At(value="INVOKE", target="Lnet/minecraft/client/renderer/GameRenderer;render3dHud(Lnet/minecraft/client/renderer/state/level/CameraRenderState;Lnet/minecraft/client/renderer/state/level/PlayerRenderState;Lnet/minecraft/client/renderer/state/OptionsRenderState;Z)V"))
|
||||
private void sanctuary$shadows(CallbackInfo ci) { PixelShadows.render(); ProceduralPbr.render(); ScreenSpaceGI.render(); EdgeHighlights.render(); Bloom.render(); Sunshafts.render(); }
|
||||
private void sanctuary$shadows(CallbackInfo ci) { ColoredLights.prepare(); PixelShadows.render(); ProceduralPbr.render(); ColoredLights.render(); ScreenSpaceGI.render(); fr.koka.sanctuary.client.shader.PixelReflections.render(); EdgeHighlights.render(); Bloom.render(); Sunshafts.render(); }
|
||||
@Inject(method={"close", "setLevel"}, at=@At("HEAD"))
|
||||
private void sanctuary$close(CallbackInfo ci) { PixelShadows.release(); ProceduralPbr.release(); EdgeHighlights.release(); Bloom.release(); ScreenSpaceGI.release(); Sunshafts.release(); }
|
||||
private void sanctuary$close(CallbackInfo ci) { PixelShadows.release(); ProceduralPbr.release(); EdgeHighlights.release(); Bloom.release(); ColoredLights.release(); ScreenSpaceGI.release(); Sunshafts.release(); }
|
||||
}
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package fr.koka.sanctuary.mixin.client;
|
||||
|
||||
import com.mojang.renderpearl.api.device.GpuBackend;
|
||||
import net.minecraft.client.PreferredGraphicsApi;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
/** Sanctuary's default uses native Vulkan with Minecraft's OpenGL fallback. */
|
||||
@Mixin(PreferredGraphicsApi.class)
|
||||
abstract class PreferredGraphicsApiMixin {
|
||||
@Inject(method="getBackendsToTry", at=@At("HEAD"), cancellable=true)
|
||||
private void sanctuary$vulkanDefault(CallbackInfoReturnable<GpuBackend[]> ci) {
|
||||
if ((Object)this == PreferredGraphicsApi.DEFAULT)
|
||||
ci.setReturnValue(PreferredGraphicsApi.VULKAN.getBackendsToTry());
|
||||
}
|
||||
@Inject(method="caption", at=@At("HEAD"), cancellable=true)
|
||||
private void sanctuary$defaultCaption(CallbackInfoReturnable<Component> ci) {
|
||||
if ((Object)this == PreferredGraphicsApi.DEFAULT)
|
||||
ci.setReturnValue(Component.translatable("sanctuary.graphics.default_vulkan"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package fr.koka.sanctuary.mixin.client;
|
||||
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.renderpearl.api.commands.RenderPass;
|
||||
import fr.koka.sanctuary.client.shader.PixelReflections;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.LevelRenderer;
|
||||
import net.minecraft.client.renderer.chunk.ChunkSectionsToRender;
|
||||
import net.minecraft.client.renderer.feature.FeatureRenderDispatcher;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalDouble;
|
||||
|
||||
/** Splits only the classic pass; copying an active Vulkan attachment is not legal. */
|
||||
@Mixin(LevelRenderer.class)
|
||||
abstract class ReflectionSceneMixin {
|
||||
@Unique private java.util.function.Consumer<RenderPass> sanctuary$transparency;
|
||||
@Inject(method="render",at=@At("HEAD"))
|
||||
private void sanctuary$begin(CallbackInfo ci){sanctuary$transparency=null;PixelReflections.beginFrame();}
|
||||
@WrapOperation(method="lambda$addMainPass$0",at=@At(value="INVOKE",target="Lnet/minecraft/client/renderer/LevelRenderer;executeClassicTransparency(Lnet/minecraft/client/renderer/chunk/ChunkSectionsToRender;Lnet/minecraft/client/renderer/feature/FeatureRenderDispatcher$PreparedFrame;Lcom/mojang/renderpearl/api/commands/RenderPass;)V"))
|
||||
private void sanctuary$defer(LevelRenderer self,ChunkSectionsToRender chunks,FeatureRenderDispatcher.PreparedFrame features,RenderPass pass,Operation<Void> original){
|
||||
if(!PixelReflections.eligible()){original.call(self,chunks,features,pass);return;}
|
||||
sanctuary$transparency=next->original.call(self,chunks,features,next);
|
||||
}
|
||||
@Inject(method="executeOit",at=@At("HEAD"))
|
||||
private void sanctuary$oit(CallbackInfo ci){PixelReflections.captureOpaque();}
|
||||
@Inject(method="executeOutline",at=@At("HEAD"))
|
||||
private void sanctuary$classic(CallbackInfo ci){
|
||||
var action=sanctuary$transparency;sanctuary$transparency=null;if(action==null)return;
|
||||
PixelReflections.captureOpaque();var main=Minecraft.getInstance().gameRenderer.mainRenderTarget();
|
||||
try(var pass=RenderSystem.getDevice().createCommandEncoder().createRenderPass(()->"Sanctuary native transparency",main.getColorTextureView(),Optional.empty(),main.getDepthTextureView(),OptionalDouble.empty())){
|
||||
RenderSystem.bindDefaultUniforms(pass);action.accept(pass);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package fr.koka.sanctuary.water;
|
||||
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.expansion.ExpansionChunkGenerator;
|
||||
import net.fabricmc.fabric.api.networking.v1.*;
|
||||
import net.fabricmc.fabric.api.entity.event.v1.ServerEntityLevelChangeEvents;
|
||||
import net.fabricmc.fabric.api.entity.event.v1.ServerPlayerEvents;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
|
||||
/** Read-only dimension context; no persistent world or generation changes. */
|
||||
public final class WaterWorldSettings {
|
||||
private WaterWorldSettings() {}
|
||||
public record Context(boolean sanctuary) implements CustomPacketPayload {
|
||||
public static final Type<Context> TYPE=new Type<>(SanctuaryMod.id("water_world_v1"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf,Context> CODEC=CustomPacketPayload.codec(
|
||||
(p,b)->b.writeBoolean(p.sanctuary),b->new Context(b.readBoolean()));
|
||||
public Type<Context> type(){return TYPE;}
|
||||
}
|
||||
public static void register(){
|
||||
PayloadTypeRegistry.clientboundPlay().register(Context.TYPE,Context.CODEC);
|
||||
ServerPlayConnectionEvents.JOIN.register((handler,sender,server)->send(handler.player));
|
||||
ServerEntityLevelChangeEvents.AFTER_PLAYER_CHANGE_LEVEL.register((player,origin,destination)->send(player));
|
||||
ServerPlayerEvents.AFTER_RESPAWN.register((oldPlayer,player,alive)->send(player));
|
||||
}
|
||||
private static void send(ServerPlayer player){
|
||||
if(ServerPlayNetworking.canSend(player,Context.TYPE))
|
||||
ServerPlayNetworking.send(player,new Context(player.level().getChunkSource().getGenerator() instanceof ExpansionChunkGenerator));
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,9 @@ import net.minecraft.world.level.levelgen.Heightmap;
|
||||
|
||||
/** Melt only known weather deposits, using the existing precipitation column sampler. */
|
||||
public final class SeasonalSnowMelt {
|
||||
/** One real day lasts 72 native Minecraft days. Keep the native sampler, slow only its melt chance. */
|
||||
public static final int REALTIME_SCALE = 72;
|
||||
|
||||
public static final GameRule<Boolean> ENABLED = GameRuleBuilder.forBoolean(true)
|
||||
.category(GameRuleCategory.UPDATES).buildAndRegister(SanctuaryMod.id("seasonal_snow_melt"));
|
||||
|
||||
@@ -77,12 +80,7 @@ public final class SeasonalSnowMelt {
|
||||
var chunk = level.getChunkAt(column);
|
||||
var entries = chunk.getAttached(NATURAL);
|
||||
if (entries == null || entries.isEmpty()) return;
|
||||
int chance = switch (WeatherService.season(level.getServer())) {
|
||||
case WINTER -> 0;
|
||||
case SPRING -> 2;
|
||||
case SUMMER -> 1;
|
||||
case AUTUMN -> 4;
|
||||
};
|
||||
int chance = chance(WeatherService.season(level.getServer()));
|
||||
if (chance == 0 || level.getRandom().nextInt(chance) != 0) return;
|
||||
var pos = level.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING, column);
|
||||
// Thin layers are above the heightmap; a full snow block is just below it.
|
||||
@@ -103,5 +101,15 @@ public final class SeasonalSnowMelt {
|
||||
if (level.setBlockAndUpdate(pos, after)) remember(level, pos, entry.base, after);
|
||||
}
|
||||
|
||||
public static int chance(SeasonalWeather.Season season) {
|
||||
int seasonal = switch (season) {
|
||||
case WINTER -> 0;
|
||||
case SPRING -> 2;
|
||||
case SUMMER -> 1;
|
||||
case AUTUMN -> 4;
|
||||
};
|
||||
return seasonal == 0 ? 0 : seasonal * REALTIME_SCALE;
|
||||
}
|
||||
|
||||
private static String key(BlockPos pos) { return Long.toString(pos.asLong()); }
|
||||
}
|
||||
|
||||
@@ -16,8 +16,11 @@ import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.gamerules.GameRule;
|
||||
import net.minecraft.world.level.gamerules.GameRuleCategory;
|
||||
|
||||
/** One real layer per native precipitation tick; no additional chunk or entity scan. */
|
||||
/** Weather snow accumulation, using the native precipitation sampler without chunk scans. */
|
||||
public final class SnowAccumulation {
|
||||
/** Snowfall should remain visible during a session, but no longer stack instantly. */
|
||||
public static final int REALTIME_SCALE = 12;
|
||||
|
||||
public static final GameRule<Integer> MAX_HEIGHT = GameRuleBuilder.forInteger(2)
|
||||
.range(-1, 128).category(GameRuleCategory.UPDATES)
|
||||
.buildAndRegister(SanctuaryMod.id("snow_accumulation_height"));
|
||||
@@ -38,7 +41,7 @@ public final class SnowAccumulation {
|
||||
/** Return whether vanilla should perform its own deposit. */
|
||||
public static boolean replaceNative(ServerLevel level, BlockPos pos, boolean canSnow) {
|
||||
if (!canSnow || !ENABLED.contains(level.getServer())) return canSnow;
|
||||
if (canSnow) deposit(level, pos);
|
||||
if (level.getRandom().nextInt(REALTIME_SCALE) == 0) deposit(level, pos);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
{
|
||||
"sanctuary.shaders.ssr_intensity_value": "Reflection strength: %s%%",
|
||||
"sanctuary.shaders.ssr_intensity_help": "Strength of visible landscape reflections on solids and water. Default: 20%.",
|
||||
"sanctuary.shaders.ssr": "Landscape reflections — experimental",
|
||||
"sanctuary.shaders.ssr_help": "Colours material texels with the visible landscape. Requires PBR; off-screen objects cannot be reflected.",
|
||||
"sanctuary.shaders.ssr_intensity": "Reflection strength",
|
||||
"sanctuary.graphics.default_vulkan": "Vulkan (default)",
|
||||
"sanctuary.statuary.clay_ready": "Clay: original model colors.",
|
||||
"sanctuary.weather.selected": "Real Time weather: %s for %s (%s). The daily forecast resumes at midnight.",
|
||||
"sanctuary.weather.overcast": "Overcast",
|
||||
@@ -2295,10 +2301,10 @@
|
||||
"sanctuary.shaders.ssgi_intensity_help": "Strength of diffuse color bounce. Zero stops rendering and frees its resources. Does not change world light.",
|
||||
"sanctuary.shaders.ssgi_intensity_value": "SSGI: %s%%",
|
||||
"sanctuary.shaders.pbr": "PBR · material relief",
|
||||
"sanctuary.shaders.pbr_help": "Experimental: procedural block normals and roughness, sun and moon highlights. Range 32 blocks. ON by default.",
|
||||
"sanctuary.shaders.pbr_intensity": "PBR strength",
|
||||
"sanctuary.shaders.pbr_intensity_help": "Material relief and highlights. Zero stops rendering and frees the cache. Original textures remain unchanged.",
|
||||
"sanctuary.shaders.pbr_intensity_value": "PBR: %s%%",
|
||||
"sanctuary.shaders.pbr_help": "Experimental: procedural block normals and roughness, sun, moon and local light highlights. Enable Colored lights for local direction. Range 32 blocks. ON at 80% by default.",
|
||||
"sanctuary.shaders.pbr_intensity": "PBR intensity",
|
||||
"sanctuary.shaders.pbr_intensity_help": "Material relief and highlights, including water. Default: 80%.",
|
||||
"sanctuary.shaders.pbr_intensity_value": "PBR intensity: %s %%",
|
||||
"sanctuary.shaders.lens_flare": "Lens flare · soft rectangles",
|
||||
"sanctuary.shaders.lens_flare_help": "Soft rectangular sun and moon reflections. Obstacles hide the sources. Independent of the bloom halo.",
|
||||
"sanctuary.shaders.lens_flare_intensity": "Lens flare intensity",
|
||||
@@ -2307,6 +2313,27 @@
|
||||
"sanctuary.shaders.sunshafts": "Sunshafts",
|
||||
"sanctuary.shaders.sunshafts_help": "Light beams through openings in the scenery. Independent of bloom; range follows shadows (up to 64 blocks).",
|
||||
"sanctuary.shaders.sunshaft_intensity": "Sunshaft intensity",
|
||||
"sanctuary.shaders.sunshaft_intensity_help": "Strength of light scattered in the air. Zero disables the effect.",
|
||||
"sanctuary.shaders.sunshaft_intensity_value": "Sunshafts: %s %%"
|
||||
"sanctuary.shaders.sunshaft_intensity_help": "Strength of light scattered in the air. Beams automatically thicken with fog and precipitation. Zero disables the effect.",
|
||||
"sanctuary.shaders.sunshaft_intensity_value": "Sunshafts: %s %%",
|
||||
"sanctuary.shaders.colored_lights": "Colored lights",
|
||||
"sanctuary.shaders.colored_lights_help": "Light-emitting blocks and carried items tint their surroundings, even off screen. Smooth appearance without changing gameplay light levels.",
|
||||
"sanctuary.shaders.colored_light_intensity": "Colored light intensity",
|
||||
"sanctuary.shaders.colored_light_intensity_value": "Colored lights: %s %%",
|
||||
"sanctuary.shaders.colored_light_intensity_help": "Enabled at 30% by default. Zero disables computation. Changes fade in smoothly.",
|
||||
"sanctuary.shaders.colored_light_distance": "Colored light distance",
|
||||
"sanctuary.shaders.colored_light_distance_value": "Colored lights: %s blocks",
|
||||
"sanctuary.shaders.colored_light_distance_help": "View distance: 8 to 64 blocks. Longer distances use more memory and computation. Smooth range fade; each source keeps its native reach.",
|
||||
"sanctuary.shaders.warm_torches": "Warm torches",
|
||||
"sanctuary.shaders.warm_torches_help": "Off: ordinary torches keep the native color temperature. On: orange tint. Other sources retain their own colors.",
|
||||
"sanctuary.shaders.natural_water": "Natural water",
|
||||
"sanctuary.shaders.natural_water_help": "Bed-dependent tint, transparency and fog. Defaults OFF in ordinary worlds and ON on Sanctuary islands. Preferences are stored separately for each world type.",
|
||||
"sanctuary.shaders.water_intensity": "Water intensity",
|
||||
"sanctuary.shaders.water_intensity_help": "At 0%, keeps native rendering. At 100%, applies all three profiles.",
|
||||
"sanctuary.shaders.water_intensity_value": "Water intensity: %s%%",
|
||||
"sanctuary.shaders.pbr_distance": "PBR and SSR distance",
|
||||
"sanctuary.shaders.pbr_distance_value": "PBR and SSR distance: %s blocks",
|
||||
"sanctuary.shaders.pbr_distance_help": "Shared range for relief and reflecting surfaces. 16, 32, 64 or 128 blocks. Longer distances cost more; mesh memory is bounded.",
|
||||
"sanctuary.shaders.water_blend": "Water blending",
|
||||
"sanctuary.shaders.water_blend_value": "Water blending: %s × %s blocks",
|
||||
"sanctuary.shaders.water_blend_help": "Transition width between bed-dependent water colours: 1, 3, 5, 9 or 17 blocks. Default: 9 × 9."
|
||||
}
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
{
|
||||
"sanctuary.shaders.ssr_intensity_value": "Intensité des reflets : %s %%",
|
||||
"sanctuary.shaders.ssr_intensity_help": "Intensité des reflets du paysage visible, sur les solides et l’eau. Défaut : 20 %.",
|
||||
"sanctuary.shaders.ssr": "Reflets du paysage — expérimental",
|
||||
"sanctuary.shaders.ssr_help": "Colore les pixels de matière avec le paysage visible. PBR requis ; les objets hors écran ne sont pas réfléchis.",
|
||||
"sanctuary.shaders.ssr_intensity": "Intensité des reflets",
|
||||
"sanctuary.graphics.default_vulkan": "Vulkan (par défaut)",
|
||||
"sanctuary.statuary.clay_ready": "Argile : couleurs originales du modèle.",
|
||||
"sanctuary.weather.selected": "Météo Real Time : %s pour la journée du %s (%s). Le tirage quotidien reprend à minuit.",
|
||||
"sanctuary.weather.overcast": "Ciel gris",
|
||||
@@ -2295,10 +2301,10 @@
|
||||
"sanctuary.shaders.ssgi_intensity_help": "Force de la teinte diffusée. Zéro coupe le calcul et libère ses ressources. Aucun changement de lumière du monde.",
|
||||
"sanctuary.shaders.ssgi_intensity_value": "SSGI : %s %%",
|
||||
"sanctuary.shaders.pbr": "PBR · relief des matériaux",
|
||||
"sanctuary.shaders.pbr_help": "Expérimental : normales procédurales et rugosité des blocs, reflets du soleil et de la lune. Portée 32 blocs. Activé par défaut.",
|
||||
"sanctuary.shaders.pbr_intensity": "Intensité du PBR",
|
||||
"sanctuary.shaders.pbr_intensity_help": "Relief et reflets des matériaux. Zéro coupe le calcul et libère le cache. Ne modifie pas les textures originales.",
|
||||
"sanctuary.shaders.pbr_intensity_value": "PBR : %s %%",
|
||||
"sanctuary.shaders.pbr_help": "Expérimental : normales procédurales et rugosité des blocs, reflets du soleil, de la lune et des lumières locales. Activer Lumières colorées pour la direction locale. Portée 32 blocs. Activé par défaut à 80 %.",
|
||||
"sanctuary.shaders.pbr_intensity": "Intensité PBR",
|
||||
"sanctuary.shaders.pbr_intensity_help": "Relief et éclats des matières, eau comprise. Défaut : 80 %.",
|
||||
"sanctuary.shaders.pbr_intensity_value": "Intensité PBR : %s %%",
|
||||
"sanctuary.shaders.lens_flare": "Lens flare · rectangles doux",
|
||||
"sanctuary.shaders.lens_flare_help": "Reflets rectangulaires du soleil et de la lune, aux bords dégradés. Les obstacles masquent les sources. Indépendant du halo de bloom.",
|
||||
"sanctuary.shaders.lens_flare_intensity": "Intensité du lens flare",
|
||||
@@ -2307,6 +2313,27 @@
|
||||
"sanctuary.shaders.sunshafts": "Rayons solaires",
|
||||
"sanctuary.shaders.sunshafts_help": "Faisceaux de lumière dans les ouvertures du décor. Indépendants du bloom, portée liée aux ombres (64 blocs maximum).",
|
||||
"sanctuary.shaders.sunshaft_intensity": "Intensité des rayons",
|
||||
"sanctuary.shaders.sunshaft_intensity_help": "Force de la lumière dans l’air. Zéro désactive le calcul.",
|
||||
"sanctuary.shaders.sunshaft_intensity_value": "Rayons : %s %%"
|
||||
"sanctuary.shaders.sunshaft_intensity_help": "Force de la lumière dans l’air. Les rayons s’épaississent automatiquement avec le brouillard et les précipitations. Zéro désactive le calcul.",
|
||||
"sanctuary.shaders.sunshaft_intensity_value": "Rayons : %s %%",
|
||||
"sanctuary.shaders.colored_lights": "Lumières colorées",
|
||||
"sanctuary.shaders.colored_lights_help": "Les blocs lumineux et les objets portés teintent leurs environs, même hors écran. Apparition progressive, sans changer les règles de lumière du jeu.",
|
||||
"sanctuary.shaders.colored_light_intensity": "Intensité des lumières colorées",
|
||||
"sanctuary.shaders.colored_light_intensity_value": "Lumières colorées : %s %%",
|
||||
"sanctuary.shaders.colored_light_intensity_help": "Activé par défaut à 30 %. Zéro désactive le calcul. Les changements apparaissent progressivement.",
|
||||
"sanctuary.shaders.colored_light_distance": "Distance des lumières colorées",
|
||||
"sanctuary.shaders.colored_light_distance_value": "Lumières colorées : %s blocs",
|
||||
"sanctuary.shaders.colored_light_distance_help": "Distance d'affichage : 8 à 64 blocs. Les grandes distances utilisent plus de mémoire et de calcul. Fondu aux limites ; la portée de chaque source reste native.",
|
||||
"sanctuary.shaders.warm_torches": "Torches chaudes",
|
||||
"sanctuary.shaders.warm_torches_help": "Désactivé : les torches ordinaires conservent la température de couleur native. Activé : teinte orangée. Les autres sources gardent leurs propres couleurs.",
|
||||
"sanctuary.shaders.natural_water": "Eau naturelle",
|
||||
"sanctuary.shaders.natural_water_help": "Teintes, transparence et brume selon le fond. OFF par défaut dans les mondes ordinaires, ON sur les îles Sanctuary. Choix mémorisé séparément pour chaque type de monde.",
|
||||
"sanctuary.shaders.water_intensity": "Intensité de l’eau",
|
||||
"sanctuary.shaders.water_intensity_help": "À 0 %, conserve le rendu natif. À 100 %, applique les trois profils.",
|
||||
"sanctuary.shaders.water_intensity_value": "Intensité de l’eau: %s%%",
|
||||
"sanctuary.shaders.pbr_distance": "Distance PBR et SSR",
|
||||
"sanctuary.shaders.pbr_distance_value": "Distance PBR et SSR : %s blocs",
|
||||
"sanctuary.shaders.pbr_distance_help": "Portée commune du relief et des surfaces réfléchissantes. 16, 32, 64 ou 128 blocs. Les distances élevées coûtent davantage ; budget de maillage limité.",
|
||||
"sanctuary.shaders.water_blend": "Mélange des eaux",
|
||||
"sanctuary.shaders.water_blend_value": "Mélange des eaux : %s × %s blocs",
|
||||
"sanctuary.shaders.water_blend_help": "Étendue de la transition des teintes selon le fond : 1, 3, 5, 9 ou 17 blocs. Défaut : 9 × 9."
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
#version 330
|
||||
#extension GL_ARB_separate_shader_objects : require
|
||||
uniform sampler2D InputTexture;
|
||||
layout(location=0) in vec2 texCoord;
|
||||
layout(location=0) out vec4 fragColor;
|
||||
void main(){fragColor=vec4(texture(InputTexture,texCoord).rgb,0);}
|
||||
@@ -0,0 +1,91 @@
|
||||
#version 330
|
||||
#extension GL_ARB_separate_shader_objects : require
|
||||
uniform sampler2D WorldDepth;
|
||||
uniform sampler2D SceneColor;
|
||||
uniform sampler2D LightVolume;
|
||||
uniform sampler2D PreviousVolume;
|
||||
layout(std140) uniform ColoredLightScene {
|
||||
mat4 InverseViewProjection;
|
||||
vec4 CameraStrength;
|
||||
vec4 ClipSkyFog;
|
||||
vec4 FogDistance;
|
||||
vec4 VolumeSize;
|
||||
vec4 Atlas;
|
||||
vec4 PreviousCamera;
|
||||
vec4 PreviousAtlas;
|
||||
vec4 DynamicPosition[128];
|
||||
vec4 DynamicColor[128];
|
||||
};
|
||||
layout(location=0) in vec2 texCoord;
|
||||
layout(location=0) out vec4 fragColor;
|
||||
vec3 positionAt(vec2 uv,float depth){
|
||||
vec4 p=InverseViewProjection*vec4(uv*2.0-1.0,ClipSkyFog.x>.5?depth:depth*2.0-1.0,1);
|
||||
return p.xyz/p.w;
|
||||
}
|
||||
vec3 neighbor(vec2 uv,vec3 center){float d=texture(WorldDepth,uv).r;return d>0.0?positionAt(uv,d):center;}
|
||||
vec3 normalAt(vec3 p){
|
||||
vec2 pixel=1.0/vec2(textureSize(WorldDepth,0));
|
||||
vec3 l=p-neighbor(texCoord-vec2(pixel.x,0),p),r=neighbor(texCoord+vec2(pixel.x,0),p)-p;
|
||||
vec3 d=p-neighbor(texCoord-vec2(0,pixel.y),p),u=neighbor(texCoord+vec2(0,pixel.y),p)-p;
|
||||
vec3 dx=dot(l,l)>0.0&&(dot(l,l)<dot(r,r)||dot(r,r)==0.0)?l:r;
|
||||
vec3 dy=dot(d,d)>0.0&&(dot(d,d)<dot(u,u)||dot(u,u)==0.0)?d:u;
|
||||
float xx=dot(dx,dx),yy=dot(dy,dy);if(xx<1e-20||yy<1e-20)return vec3(0);
|
||||
vec3 n=cross(dx*inversesqrt(xx),dy*inversesqrt(yy));float nn=dot(n,n);if(nn<1e-12)return vec3(0);
|
||||
n*=inversesqrt(nn);return dot(n,-p)<0.0?-n:n;
|
||||
}
|
||||
vec4 cell(sampler2D volume,ivec3 p,vec4 atlas){
|
||||
int side=int(atlas.x),cols=int(atlas.y);
|
||||
p=clamp(p,ivec3(0),ivec3(side-1));
|
||||
vec4 v=texelFetch(volume,ivec2(p.x+(p.y%cols)*side,p.z+(p.y/cols)*side),0);
|
||||
vec3 spectrum=pow(vec3(.8),vec3(31)-v.rgb*(255.0/8.0))*step(vec3(.5/255.0),v.rgb);
|
||||
float peak=max(spectrum.r,max(spectrum.g,spectrum.b));
|
||||
float energy=max(0.0,(peak-pow(.8,15.0))/(1.0-pow(.8,15.0)));
|
||||
v.rgb=spectrum*(energy/max(peak,1e-12));
|
||||
return v;
|
||||
}
|
||||
vec4 field(sampler2D volume,vec3 p,vec4 atlas){
|
||||
vec3 edges=min(p,vec3(atlas.x)-p);
|
||||
float margin=smoothstep(14.0,18.0,min(edges.x,min(edges.y,edges.z)));if(margin<=0.0)return vec4(0);
|
||||
vec3 at=p-.5;ivec3 base=ivec3(floor(at));vec3 f=fract(at);vec4 result=vec4(0);
|
||||
for(int y=0;y<2;y++)for(int z=0;z<2;z++)for(int x=0;x<2;x++){
|
||||
float w=(x==0?1.0-f.x:f.x)*(y==0?1.0-f.y:f.y)*(z==0?1.0-f.z:f.z);
|
||||
result+=cell(volume,base+ivec3(x,y,z),atlas)*w;
|
||||
}
|
||||
result.rgb*=margin;return result;
|
||||
}
|
||||
vec3 tintGain(vec4 light,float amount){
|
||||
float energy=max(light.r,max(light.g,light.b));if(energy<1e-12)return vec3(1);
|
||||
vec3 tint=clamp(light.rgb/max(dot(light.rgb,vec3(.2126,.7152,.0722)),1e-12),vec3(.02),vec3(3));
|
||||
// Linear close to darkness: no fourth-root halo at the edge of a light pool.
|
||||
float response=energy/(energy+.08);
|
||||
return pow(tint,vec3(clamp(amount*response*(1.0-.8*light.a*ClipSkyFog.y),0.0,.95)));
|
||||
}
|
||||
void main(){
|
||||
vec3 scene=texture(SceneColor,texCoord).rgb;fragColor=vec4(scene,0);
|
||||
float depth=texture(WorldDepth,texCoord).r;if(depth<=0.0)return;
|
||||
vec3 p=positionAt(texCoord,depth),n=normalAt(p);if(dot(n,n)<.5)return;
|
||||
vec3 a=abs(n);float axis=max(a.x,max(a.y,a.z));
|
||||
// Sample outside opaque block faces. Bilinear atlas filtering would leak
|
||||
// across unrelated slices, so the eight neighbours are explicitly addressed.
|
||||
vec3 offset=n*(axis>.995?.51:.06);
|
||||
float border=1.0-smoothstep(VolumeSize.x*.75,VolumeSize.x,length(p));if(border<=0.0)return;
|
||||
vec4 light=field(LightVolume,p+CameraStrength.xyz+offset,Atlas);
|
||||
vec4 before=VolumeSize.y<1.0&&PreviousCamera.w>.5?field(PreviousVolume,p+PreviousCamera.xyz+offset,PreviousAtlas):vec4(0);
|
||||
vec3 dynamic=vec3(0);
|
||||
for(int i=0;i<int(VolumeSize.w);i++){
|
||||
vec3 delta=p-DynamicPosition[i].xyz;float square=dot(delta,delta),radius=DynamicPosition[i].w;
|
||||
if(square>=radius*radius)continue;
|
||||
float remaining=radius-sqrt(square);
|
||||
float power=max(0.0,(pow(.8,15.0-remaining)-pow(.8,15.0))/(1.0-pow(.8,15.0)));
|
||||
float response=power/(power+.08)*smoothstep(0.0,1.0,DynamicColor[i].a);
|
||||
float fadedPower=.08*response/max(1e-6,1.0-response);
|
||||
dynamic=max(dynamic,DynamicColor[i].rgb*fadedPower);
|
||||
}
|
||||
light.rgb=max(light.rgb,dynamic);before.rgb=max(before.rgb,dynamic);
|
||||
float fog=max(clamp((max(length(p.xz),abs(p.y))-FogDistance.x)/max(.001,FogDistance.y-FogDistance.x),0,1),clamp((length(p)-FogDistance.z)/max(.001,FogDistance.w-FogDistance.z),0,1));
|
||||
float amount=CameraStrength.w*1.35*border*(1.0-fog*ClipSkyFog.z)*smoothstep(0.0,1.0,VolumeSize.z);
|
||||
vec3 gain=tintGain(light,amount);
|
||||
// Fade the rendered response, preserving colour and avoiding a bright first frame.
|
||||
if(VolumeSize.y<1.0)gain=mix(tintGain(before,amount),gain,smoothstep(0.0,1.0,VolumeSize.y));
|
||||
fragColor=vec4(scene*gain,0);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
#version 330
|
||||
#extension GL_ARB_separate_shader_objects : require
|
||||
uniform sampler2D Before;
|
||||
uniform sampler2D After;
|
||||
layout(location=0) in vec2 texCoord;
|
||||
layout(location=0) out vec4 fragColor;
|
||||
void main(){
|
||||
vec2 pixel=1.0/vec2(textureSize(After,0));vec3 glow=vec3(0);
|
||||
for(int y=0;y<8;y++)for(int x=0;x<8;x++){
|
||||
vec2 uv=texCoord+(vec2(x,y)-3.5)*pixel;
|
||||
vec3 a=texture(After,uv).rgb,b=texture(Before,uv).rgb;
|
||||
// A restrained halo for material light, stronger for near-white solar glints.
|
||||
glow+=max(a*a-b*b,vec3(0))*(.12+.28*smoothstep(.75,1.0,max(a.r,max(a.g,a.b))));
|
||||
}
|
||||
fragColor=vec4(glow/64.0,0);
|
||||
}
|
||||
@@ -5,6 +5,20 @@ uniform sampler2D SceneColor;
|
||||
uniform sampler2D BlockAtlas;
|
||||
uniform sampler2D MaterialAtlas;
|
||||
uniform sampler2D SunDepth;
|
||||
uniform sampler2D LightDirection;
|
||||
uniform sampler2D PreviousDirection;
|
||||
layout(std140) uniform ColoredLightScene {
|
||||
mat4 LocalInverseViewProjection;
|
||||
vec4 CameraStrength;
|
||||
vec4 ClipSkyFog;
|
||||
vec4 FogDistance;
|
||||
vec4 VolumeSize;
|
||||
vec4 Atlas;
|
||||
vec4 PreviousCamera;
|
||||
vec4 PreviousAtlas;
|
||||
vec4 DynamicPosition[128];
|
||||
vec4 DynamicColor[128];
|
||||
};
|
||||
layout(std140) uniform PbrScene {
|
||||
mat4 PbrInverseVP;
|
||||
vec4 PbrSun;
|
||||
@@ -34,7 +48,55 @@ float visibility(vec3 p,vec3 n){
|
||||
vec3 q=light.xyz/light.w;q.xy=q.xy*0.5+0.5;
|
||||
if(DistanceClipFog.y<0.5)q.z=q.z*0.5+0.5;
|
||||
if(any(lessThan(q,vec3(0)))||any(greaterThan(q,vec3(1))))return 1.0;
|
||||
return q.z>texture(SunDepth,q.xy).r+0.00015?0.0:1.0;
|
||||
return q.z>textureLod(SunDepth,q.xy,0.0).r+0.00015?0.0:1.0;
|
||||
}
|
||||
// Cached direction moments; filtering moments before normalization avoids unstable
|
||||
// switches between nearby/opposed sources. Alpha stores the light response.
|
||||
vec4 directionField(sampler2D field,vec3 p,vec4 atlas){
|
||||
if(textureSize(field,0).x<int(atlas.z))return vec4(0);
|
||||
vec3 edges=min(p,vec3(atlas.x)-p);float margin=smoothstep(14.0,18.0,min(edges.x,min(edges.y,edges.z)));
|
||||
if(margin<=0.0)return vec4(0);
|
||||
int side=int(atlas.x),cols=int(atlas.y);vec3 at=p-.5;ivec3 base=ivec3(floor(at));vec3 f=fract(at);vec4 sum=vec4(0);
|
||||
for(int y=0;y<2;y++)for(int z=0;z<2;z++)for(int x=0;x<2;x++){
|
||||
ivec3 q=clamp(base+ivec3(x,y,z),ivec3(0),ivec3(side-1));
|
||||
vec4 cell=texelFetch(field,ivec2(q.x+(q.y%cols)*side,q.z+(q.y/cols)*side),0);
|
||||
float w=(x==0?1.0-f.x:f.x)*(y==0?1.0-f.y:f.y)*(z==0?1.0-f.z:f.z);
|
||||
sum+=vec4((cell.rgb*255.0-128.0)/127.0*cell.a,cell.a)*w;
|
||||
}
|
||||
return sum*margin;
|
||||
}
|
||||
vec4 localArrival(vec3 p,vec3 normal){
|
||||
if(PbrEnvironment.y<.5)return vec4(0);
|
||||
vec3 offset=normal*.51;
|
||||
vec4 field=directionField(LightDirection,p+CameraStrength.xyz+offset,Atlas);
|
||||
if(VolumeSize.y<1.0){
|
||||
vec4 before=PreviousCamera.w>.5?directionField(PreviousDirection,p+PreviousCamera.xyz+offset,PreviousAtlas):vec4(0);
|
||||
field=mix(before,field,smoothstep(0.0,1.0,VolumeSize.y));
|
||||
}
|
||||
for(int i=0;i<int(VolumeSize.w);i++){
|
||||
vec3 delta=DynamicPosition[i].xyz-p;float distance=length(delta),radius=DynamicPosition[i].w;
|
||||
if(distance>=radius||distance<.001)continue;
|
||||
float power=max(0.0,(pow(.8,15.0-radius+distance)-pow(.8,15.0))/(1.0-pow(.8,15.0)));
|
||||
float weight=power/(power+.08)*smoothstep(0.0,1.0,DynamicColor[i].a);
|
||||
field+=vec4(delta/distance*weight,weight);
|
||||
}
|
||||
float magnitude=length(field.xyz);if(magnitude<.00001||field.a<.00001)return vec4(0);
|
||||
float confidence=smoothstep(.02,1.0,magnitude/field.a);
|
||||
float fade=(1.0-smoothstep(VolumeSize.x*.75,VolumeSize.x,length(p)))*smoothstep(0.0,1.0,VolumeSize.z);
|
||||
return vec4(field.xyz/magnitude,confidence*min(field.a,1.0)*fade*clamp(CameraStrength.w/0.3,0.0,1.0));
|
||||
}
|
||||
vec3 localShine(vec3 n,vec3 v,vec3 l,vec4 material,vec3 albedo){
|
||||
vec3 halfVector=l+v;float size=dot(halfVector,halfVector);if(size<.00001)return vec3(0);
|
||||
vec3 h=halfVector*inversesqrt(size);
|
||||
float nl=clamp(dot(n,l),0.0,1.0),nv=clamp(dot(n,v),.001,1.0),nh=clamp(dot(n,h),0.0,1.0),vh=clamp(dot(v,h),0.0,1.0);
|
||||
float roughness=max(material.b,.16),alpha=roughness*roughness,a2=alpha*alpha;
|
||||
float denom=nh*nh*(a2-1.0)+1.0,distribution=a2/max(3.14159265*denom*denom,.00000001);
|
||||
float k=(roughness+1.0)*(roughness+1.0)/8.0;
|
||||
float geometry=nv/(nv*(1.0-k)+k)*nl/max(nl*(1.0-k)+k,.00001);
|
||||
// Explicit integer power stays defined even at round-off limits on OpenGL drivers.
|
||||
float grazing=1.0-vh,grazing2=grazing*grazing;
|
||||
vec3 f0=mix(vec3(maskUv.y>=4.0?.02:.04),albedo,material.a),fresnel=f0+(1.0-f0)*(grazing2*grazing2*grazing);
|
||||
return distribution*geometry*fresnel/max(4.0*nv*nl,.0001)*nl;
|
||||
}
|
||||
void main(){
|
||||
// Derivatives precede divergent discard; UV-space tangents preserve rotated/mirrored block textures.
|
||||
@@ -42,37 +104,51 @@ void main(){
|
||||
vec2 tx=dFdx(blockUv),ty=dFdy(blockUv);
|
||||
float det=tx.x*ty.y-tx.y*ty.x;
|
||||
vec3 baseNormal=normalize(cross(dx,dy));if(dot(baseNormal,-relativePosition)<0.0)baseNormal=-baseNormal;
|
||||
vec2 uv=gl_FragCoord.xy/vec2(textureSize(WorldDepth,0));float depth=texture(WorldDepth,uv).r;
|
||||
vec4 projected=PbrInverseVP*vec4(uv*2.0-1.0,PbrParams.y>0.5?depth:depth*2.0-1.0,1.0);
|
||||
vec3 visible=projected.xyz/projected.w;
|
||||
if(depth<=0.0||abs(length(visible)-length(relativePosition))>0.035||length(relativePosition)>=PbrParams.w||abs(det)<1e-12)discard;
|
||||
vec4 texel=texture(BlockAtlas,blockUv);if(texel.a<0.99)discard;
|
||||
// Exact receiver depth is tested by the GPU; final scene depth also masks entities.
|
||||
float visibleDepth=texelFetch(WorldDepth,ivec2(gl_FragCoord.xy),0).r;
|
||||
vec2 screenUv=gl_FragCoord.xy/vec2(textureSize(WorldDepth,0));
|
||||
vec4 visiblePoint=PbrInverseVP*vec4(screenUv*2.0-1.0,PbrParams.y>.5?visibleDepth:visibleDepth*2.0-1.0,1);
|
||||
// Native terrain and material matrices have different evaluation order. Use a
|
||||
// bounded 5 mm tolerance only for native occlusion; receiver selection stays exact.
|
||||
if((visibleDepth>0.0&&length(visiblePoint.xyz/visiblePoint.w)+.005<length(relativePosition))||length(relativePosition)>=PbrParams.w||abs(det)<1e-12)discard;
|
||||
// Keep atlas derivatives valid after per-fragment depth/alpha rejection.
|
||||
float family=floor(maskUv.y*.5);
|
||||
vec4 texel=textureGrad(BlockAtlas,blockUv,tx,ty);if(family<.5&&texel.a<0.99)discard;
|
||||
vec3 tangent=normalize((dx*ty.y-dy*tx.y)/det),bitangent=normalize((dy*tx.x-dx*ty.x)/det);
|
||||
vec4 material=texture(MaterialAtlas,maskUv);
|
||||
vec4 material=textureLod(MaterialAtlas,vec2(maskUv.x,maskUv.y-family*2.0),0.0);
|
||||
// RG stores signed height derivatives; B roughness, A metalness, not transparency.
|
||||
vec2 slope=(material.rg*255.0-128.0)/127.0*PbrParams.x*2.0;
|
||||
float intensity=PbrParams.x;
|
||||
vec2 slope=(material.rg*255.0-128.0)/127.0*intensity*(family>1.5?.24:2.0);
|
||||
vec3 n=normalize(baseNormal+tangent*slope.x+bitangent*slope.y);
|
||||
vec3 l=normalize(PbrSun.xyz),v=normalize(-relativePosition),h=normalize(l+v);
|
||||
float nl=max(dot(n,l),0.0),nv=max(dot(n,v),0.001),nh=max(dot(n,h),0.0),vh=max(dot(v,h),0.0);
|
||||
float roughness=max(material.b,0.25),alpha=roughness*roughness,a2=alpha*alpha;
|
||||
float denom=nh*nh*(a2-1.0)+1.0;
|
||||
float distribution=a2/max(3.14159265*denom*denom,0.00001);
|
||||
float k=(roughness+1.0)*(roughness+1.0)/8.0;
|
||||
float geometry=nv/(nv*(1.0-k)+k)*nl/max(nl*(1.0-k)+k,0.00001);
|
||||
// Reject degenerate/interpolated geometry without replacing the native scene with NaNs.
|
||||
if(any(isnan(n))||any(isinf(n))||dot(relativePosition,relativePosition)<.000001)discard;
|
||||
vec3 l=normalize(PbrSun.xyz),v=normalize(-relativePosition);
|
||||
float nl=clamp(dot(n,l),0.0,1.0);
|
||||
vec3 albedo=texel.rgb*tint.rgb;albedo*=albedo;
|
||||
vec3 f0=mix(vec3(0.04),albedo,material.a);
|
||||
vec3 fresnel=f0+(1.0-f0)*pow(1.0-vh,5.0);
|
||||
vec3 specular=distribution*geometry*fresnel/max(4.0*nv*nl,0.0001)*nl;
|
||||
// A broad solar lobe gives water an extended glitter path at grazing angles.
|
||||
// Local lamps retain the material's original narrow lobe.
|
||||
vec4 solarMaterial=material;
|
||||
solarMaterial.b=max(material.b,family>1.5?.34:.24);
|
||||
vec3 specular=localShine(n,v,l,solarMaterial,albedo);
|
||||
float solarRadiance=family>1.5?20.0:4.0;
|
||||
float fog=max(clamp((max(length(relativePosition.xz),abs(relativePosition.y))-PbrFog.x)/max(PbrFog.y-PbrFog.x,.001),0.0,1.0),
|
||||
clamp((length(relativePosition)-PbrFog.z)/max(PbrFog.w-PbrFog.z,.001),0.0,1.0));
|
||||
float amount=PbrSun.w*pow(tint.a,8.0)*visibility(relativePosition,baseNormal)*(1.0-fog*PbrEnvironment.x)
|
||||
*(1.0-smoothstep(PbrParams.w*.75,PbrParams.w,length(relativePosition)));
|
||||
if(amount<=0.0001)discard;
|
||||
vec3 scene=texture(SceneColor,uv).rgb;vec3 linear=scene*scene;
|
||||
vec4 arrival=localArrival(relativePosition,baseNormal);
|
||||
float localAmount=arrival.w*(1.0-fog*PbrEnvironment.x)*(1.0-smoothstep(PbrParams.w*.75,PbrParams.w,length(relativePosition)));
|
||||
if(amount<=0.0001&&localAmount<=0.0001)discard;
|
||||
vec3 scene=texelFetch(SceneColor,ivec2(gl_FragCoord.xy),0).rgb;vec3 linear=scene*scene;
|
||||
// Native diffuse light already exists. Apply only the perturbed-normal difference, then add GGX.
|
||||
float relief=(nl-max(dot(baseNormal,l),0.0))*.7;
|
||||
float relief=family<.5?(nl-max(dot(baseNormal,l),0.0))*.7:0.0;
|
||||
vec3 lit=clamp(linear*(1.0+relief*amount),0.0,1.0);
|
||||
vec3 shine=min(specular*PbrParams.x*amount*3.0,vec3(0.5));
|
||||
if(localAmount>.0001){
|
||||
float localRelief=family<.5?(max(dot(n,arrival.xyz),0.0)-max(dot(baseNormal,arrival.xyz),0.0))*.7:0.0;
|
||||
lit=clamp(lit*(1.0+localRelief*localAmount),0.0,1.0);
|
||||
}
|
||||
// Preserve bright solar highlights instead of capping them at a medium grey.
|
||||
vec3 shine=min((specular*amount*solarRadiance+localShine(n,v,arrival.xyz,material,albedo)*localAmount)*intensity*5.0,vec3(8.0));
|
||||
lit+=(1.0-lit)*(1.0-exp(-shine));
|
||||
if(any(isnan(lit))||any(isinf(lit)))discard;
|
||||
fragColor=vec4(sqrt(clamp(lit,0.0,1.0)),1.0);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#version 330
|
||||
#extension GL_ARB_separate_shader_objects : require
|
||||
#include <minecraft:dynamictransforms.glsl>
|
||||
// Receiver and colour passes must produce bit-identical depth on every backend.
|
||||
invariant gl_Position;
|
||||
layout(location=0) in vec3 Position;
|
||||
layout(location=1) in vec4 Color;
|
||||
layout(location=2) in vec2 UV0;
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
#version 330
|
||||
#extension GL_ARB_separate_shader_objects : require
|
||||
uniform sampler2D OpaqueDepth;
|
||||
layout(std140) uniform ReflectionScene { mat4 ReflectionVP; mat4 ReflectionInverseVP; vec4 ReflectionParams; vec4 ReflectionLimits; };
|
||||
uniform sampler2D BlockAtlas;
|
||||
layout(location=0) in vec3 relativePosition;
|
||||
layout(location=1) in vec2 blockUv;
|
||||
layout(location=2) in vec2 maskUv;
|
||||
layout(location=3) in vec4 tint;
|
||||
void main(){
|
||||
vec2 dx=dFdx(blockUv),dy=dFdy(blockUv);
|
||||
if(length(relativePosition)>=ReflectionParams.w)discard;
|
||||
// Minecraft's world projection uses reverse depth: zero is the far plane.
|
||||
float opaque=texelFetch(OpaqueDepth,ivec2(gl_FragCoord.xy),0).r;
|
||||
if(gl_FragCoord.z+0.0000001<opaque)discard;
|
||||
if(maskUv.y<2.0&&textureGrad(BlockAtlas,blockUv,dx,dy).a<.99)discard;
|
||||
}
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
#version 330
|
||||
#extension GL_ARB_separate_shader_objects : require
|
||||
uniform sampler2D OpaqueDepth;
|
||||
uniform sampler2D OpaqueColor;
|
||||
uniform sampler2D ReceiverDepth;
|
||||
uniform sampler2D SceneColor;
|
||||
uniform sampler2D BlockAtlas;
|
||||
uniform sampler2D MaterialAtlas;
|
||||
layout(std140) uniform ReflectionScene {
|
||||
mat4 ReflectionVP;
|
||||
mat4 ReflectionInverseVP;
|
||||
vec4 ReflectionParams; // zero-to-one, strength, normal strength, receiver distance
|
||||
vec4 ReflectionLimits; // ray distance, steps, reserved
|
||||
};
|
||||
layout(location=0) in vec3 relativePosition;
|
||||
layout(location=1) in vec2 blockUv;
|
||||
layout(location=2) in vec2 maskUv;
|
||||
layout(location=3) in vec4 tint;
|
||||
layout(location=0) out vec4 fragColor;
|
||||
vec3 reconstruct(vec2 uv,float d){
|
||||
vec4 p=ReflectionInverseVP*vec4(uv*2.0-1.0,ReflectionParams.x>.5?d:d*2.0-1.0,1);
|
||||
return p.xyz/p.w;
|
||||
}
|
||||
bool sampleRay(vec3 origin,vec3 direction,float t,out vec2 uv,out float gap){
|
||||
vec3 p=origin+direction*t;vec4 clip=ReflectionVP*vec4(p,1);
|
||||
if(clip.w<=.001)return false;
|
||||
uv=clip.xy/clip.w*.5+.5;
|
||||
if(any(lessThanEqual(uv,vec2(0)))||any(greaterThanEqual(uv,vec2(1))))return false;
|
||||
float depth=textureLod(OpaqueDepth,uv,0).r;
|
||||
if(depth<=0.0){gap=-1e6;return true;}
|
||||
float rayDepth=ReflectionParams.x>.5?clip.z/clip.w:clip.z/clip.w*.5+.5;
|
||||
gap=depth-rayDepth;return true;
|
||||
}
|
||||
vec4 trace(vec3 p,vec3 n,vec3 geometricNormal){
|
||||
vec3 direction=reflect(normalize(p),n),origin=p+n*.035;
|
||||
// A perturbed normal must not send the ray back through its own geometric face.
|
||||
if(dot(direction,geometricNormal)<=.0001)return vec4(0);
|
||||
vec4 a=ReflectionVP*vec4(origin,1),b=ReflectionVP*vec4(origin+direction*ReflectionLimits.x,1);
|
||||
if(a.w<=.001)return vec4(0);
|
||||
float distance=ReflectionLimits.x;
|
||||
if(b.w<=.01){distance*=clamp((a.w-.01)/(a.w-b.w),0.0,1.0);b=ReflectionVP*vec4(origin+direction*distance,1);}
|
||||
if(distance<.1)return vec4(0);
|
||||
// Perspective-correct interpolation spaces the bounded march in screen coordinates.
|
||||
vec2 start=a.xy/a.w*.5+.5,end=b.xy/b.w*.5+.5,delta=end-start;
|
||||
float screenLimit=1.0;
|
||||
if(delta.x>0.0)screenLimit=min(screenLimit,(.9999-start.x)/delta.x);
|
||||
if(delta.x<0.0)screenLimit=min(screenLimit,(.0001-start.x)/delta.x);
|
||||
if(delta.y>0.0)screenLimit=min(screenLimit,(.9999-start.y)/delta.y);
|
||||
if(delta.y<0.0)screenLimit=min(screenLimit,(.0001-start.y)/delta.y);
|
||||
if(screenLimit<=0.0)return vec4(0);
|
||||
// Adaptive coarse march: large landscape silhouettes need fewer probes than screen pixels.
|
||||
// The four refinements and final intersection interpolation preserve a sharp hit colour.
|
||||
int steps=int(clamp(ceil(length(delta*screenLimit*vec2(textureSize(OpaqueDepth,0)))/128.0),8.0,32.0));
|
||||
float previous=0.0,oldGap=-1e6;vec2 hit=vec2(0);float gap;
|
||||
for(int i=1;i<=32;i++){
|
||||
if(i>steps)break;
|
||||
float f=float(i)/float(steps)*screenLimit;
|
||||
float t=(f*distance/b.w)/mix(1.0/a.w,1.0/b.w,f);
|
||||
vec2 uv;if(!sampleRay(origin,direction,t,uv,gap))return vec4(0);
|
||||
if(gap>=0.0&&oldGap<0.0){
|
||||
float lo=previous,hi=t,lowGap=oldGap,highGap=gap;
|
||||
for(int j=0;j<4;j++){
|
||||
float mid=(lo+hi)*.5;vec2 q;float delta;
|
||||
if(!sampleRay(origin,direction,mid,q,delta))return vec4(0);
|
||||
if(delta>=0.0){hi=mid;highGap=delta;}else{lo=mid;lowGap=delta;}
|
||||
}
|
||||
hi=mix(lo,hi,clamp(-lowGap/max(highGap-lowGap,1e-12),0.0,1.0));
|
||||
if(!sampleRay(origin,direction,hi,hit,gap))return vec4(0);
|
||||
// A bounded thickness rejects a silhouette jump into unrelated distant geometry.
|
||||
float thickness=.15+.002*length(origin+direction*hi);
|
||||
gap=length(origin+direction*hi)-length(reconstruct(hit,textureLod(OpaqueDepth,hit,0).r));
|
||||
if(abs(gap)>thickness||hi<.08)return vec4(0);
|
||||
float edge=min(min(hit.x,hit.y),min(1.0-hit.x,1.0-hit.y));
|
||||
float confidence=smoothstep(0.0,.04,edge)*(1.0-smoothstep(distance*.85,distance,hi))
|
||||
*(1.0-smoothstep(thickness*.65,thickness,abs(gap)))*smoothstep(.08,.2,hi);
|
||||
return vec4(hit,hi,confidence);
|
||||
}
|
||||
previous=t;oldGap=gap;
|
||||
}
|
||||
return vec4(0);
|
||||
}
|
||||
void main(){
|
||||
vec3 dx=dFdx(relativePosition),dy=dFdy(relativePosition);
|
||||
vec2 mx=dFdx(maskUv),my=dFdy(maskUv),tx=dFdx(blockUv),ty=dFdy(blockUv);
|
||||
float det=mx.x*my.y-mx.y*my.x;
|
||||
float family=floor(maskUv.y*.5);
|
||||
vec2 materialUv=vec2(maskUv.x,maskUv.y-family*2.0);
|
||||
vec3 base=normalize(cross(dx,dy));if(dot(base,-relativePosition)<0.0)base=-base;
|
||||
if(abs(det)<1e-14||length(relativePosition)>=ReflectionParams.w)discard;
|
||||
// The final native depth includes entities rendered after the landscape capture.
|
||||
float finalDepth=texelFetch(ReceiverDepth,ivec2(gl_FragCoord.xy),0).r;
|
||||
vec2 screenUv=gl_FragCoord.xy/vec2(textureSize(ReceiverDepth,0));
|
||||
if(finalDepth>0.0&&length(reconstruct(screenUv,finalDepth))+.005<length(relativePosition))discard;
|
||||
float intensity=ReflectionParams.z;
|
||||
if(intensity<=0.0)discard;
|
||||
vec4 texel=textureGrad(BlockAtlas,blockUv,tx,ty);if(family<.5&&texel.a<.99)discard;
|
||||
vec2 center=(floor(materialUv*1280.0)+.5)/1280.0;
|
||||
vec3 u=(dx*my.y-dy*mx.y)/det,v=(dy*mx.x-dx*my.x)/det;
|
||||
vec3 point=relativePosition+u*(center.x-materialUv.x)+v*(center.y-materialUv.y);
|
||||
vec4 material=textureLod(MaterialAtlas,center,0);
|
||||
vec2 slope=(material.rg*255.0-128.0)/127.0*intensity*(family>1.5?.24:2.0);
|
||||
vec3 normal=normalize(base+normalize(u)*slope.x+normalize(v)*slope.y);
|
||||
if(any(isnan(normal))||any(isinf(normal)))discard;
|
||||
vec4 hit=trace(point,normal,base);if(hit.w<=0.0)discard;
|
||||
vec3 reflected=textureLod(OpaqueColor,hit.xy,0).rgb;
|
||||
if(material.b>.3){
|
||||
vec2 stepUv=vec2(material.b*3.0)/vec2(textureSize(OpaqueColor,0));
|
||||
float reference=length(reconstruct(hit.xy,textureLod(OpaqueDepth,hit.xy,0).r));
|
||||
vec3 sum=vec3(0);float weight=0;
|
||||
for(int i=0;i<4;i++){
|
||||
vec2 uv=hit.xy+vec2(i%2==0?-1:1,i<2?-1:1)*stepUv;
|
||||
float d=textureLod(OpaqueDepth,uv,0).r;
|
||||
if(d>0.0&&abs(length(reconstruct(uv,d))-reference)<.25+reference*.002){sum+=textureLod(OpaqueColor,uv,0).rgb;weight+=1.0;}
|
||||
}
|
||||
if(weight>0.0)reflected=sum/weight;
|
||||
}
|
||||
float nv=clamp(dot(normal,normalize(-point)),0.0,1.0),grazing=1.0-nv,g2=grazing*grazing;
|
||||
// Metal tint is sampled at the same texel centre, never at the individual screen fragment.
|
||||
|
||||
vec2 localDelta=center-materialUv;
|
||||
vec2 centerBlock=blockUv+(tx*my.y-ty*mx.y)/det*localDelta.x+(ty*mx.x-tx*my.x)/det*localDelta.y;
|
||||
vec3 albedo=textureLod(BlockAtlas,centerBlock,0).rgb*tint.rgb;
|
||||
vec3 f0=mix(vec3(family>1.5?.02:.04),albedo*albedo,material.a);
|
||||
vec3 fresnel=f0+(1.0-f0)*(g2*g2*grazing);
|
||||
float fade=(1.0-smoothstep(ReflectionParams.w*.75,ReflectionParams.w,length(point)))*hit.w;
|
||||
vec3 weight=clamp(fresnel*ReflectionParams.y*(1.0-material.b*material.b)*fade,0.0,.9);
|
||||
vec3 scene=texelFetch(SceneColor,ivec2(gl_FragCoord.xy),0).rgb;
|
||||
vec3 result=mix(scene*scene,reflected*reflected,weight);
|
||||
if(any(isnan(result))||any(isinf(result)))discard;
|
||||
fragColor=vec4(sqrt(clamp(result,0.0,1.0)),1);
|
||||
}
|
||||
@@ -33,12 +33,15 @@ void main(){
|
||||
float span=max(0.0,stop-start);if(span<=0.0)break;
|
||||
float t=(start+stop)*.5;
|
||||
float distanceFade=1.0-smoothstep(range*.7,range,t);
|
||||
total+=sunlight(ray*t)*span*exp(-t/24.0)*distanceFade;
|
||||
total+=sunlight(ray*t)*span*exp(-t/24.0*ShaftAtmosphere.z)*distanceFade;
|
||||
}
|
||||
float phase=.12+.88*pow(max(0.0,dot(ray,SunStrength.xyz)),4.0);
|
||||
float weather=clamp(SunStrength.w/.38,0.0,1.0);
|
||||
// Thick air scatters into wider viewing angles, while extinguishing distant light.
|
||||
// Keep the shadow footprint unchanged: weather cannot open a hole in a solid wall.
|
||||
float base=mix(.12,.30,ShaftAtmosphere.y);
|
||||
float phase=base+(1.0-base)*pow(max(0.0,dot(ray,SunStrength.xyz)),mix(4.0,2.0,ShaftAtmosphere.y));
|
||||
float weather=ShaftAtmosphere.w;
|
||||
float fogDistance=max(abs(ray.x),max(abs(ray.y),abs(ray.z)))*end;
|
||||
float nativeFog=1.0-smoothstep(DistanceClipFog.z,max(DistanceClipFog.z+.01,DistanceClipFog.w),fogDistance);
|
||||
float amount=total*.018*phase*weather*ShaftSettings.x*nativeFog;
|
||||
float amount=total*.018*phase*weather*ShaftSettings.x*nativeFog*ShaftAtmosphere.x;
|
||||
fragColor=vec4(ShaftColor.rgb*amount,end);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,10 @@ layout(std140) uniform PixelShadows {
|
||||
vec4 DistanceClipFog;
|
||||
vec4 EnvironmentFog;
|
||||
};
|
||||
layout(std140) uniform ShaftParams { vec4 ShaftColor; vec4 ShaftSettings; };
|
||||
layout(std140) uniform ShaftParams {
|
||||
vec4 ShaftColor; vec4 ShaftSettings;
|
||||
vec4 ShaftAtmosphere; // density, angular diffusion, extinction, available daylight
|
||||
};
|
||||
uniform sampler2D WorldDepth;
|
||||
vec2 shaftTexel(vec2 uv){
|
||||
vec2 size=vec2(textureSize(WorldDepth,0));
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"light": ["minecraft:sand", "minecraft:red_sand", "minecraft:gravel", "minecraft:clay"],
|
||||
"pond": ["minecraft:dirt", "minecraft:coarse_dirt", "minecraft:rooted_dirt", "minecraft:grass_block", "minecraft:podzol", "minecraft:mycelium", "minecraft:mud", "minecraft:muddy_mangrove_roots"]
|
||||
}
|
||||
@@ -184,6 +184,9 @@
|
||||
"NaturalSnowEditMixin"
|
||||
],
|
||||
"client": [
|
||||
"client.ReflectionSceneMixin",
|
||||
"client.PbrAnimationClockMixin",
|
||||
"client.PbrAnimationSpriteMixin",
|
||||
"client.PackSelectionScreenMixin",
|
||||
"client.FourneauFireMixin",
|
||||
"client.BoatPassengerPickMixin",
|
||||
@@ -277,7 +280,13 @@
|
||||
"client.SculptureParticlesMixin",
|
||||
"client.PixelShadowRendererMixin",
|
||||
"client.PixelShadowLevelMixin",
|
||||
"client.PixelShadowDirtyMixin"
|
||||
"client.PixelShadowDirtyMixin",
|
||||
"client.PreferredGraphicsApiMixin",
|
||||
"client.NaturalWaterRegionMixin",
|
||||
"client.NaturalWaterFluidMixin",
|
||||
"client.NaturalWaterFogMixin",
|
||||
"client.NaturalWaterFogColorMixin",
|
||||
"client.NaturalWaterSectionCopyAccess"
|
||||
],
|
||||
"injectors": {
|
||||
"defaultRequire": 1
|
||||
|
||||
@@ -52,6 +52,13 @@ public final class Dynamic030Smoke {
|
||||
for(var light:lights)expected=Math.max(expected,(int)Math.max(0,(light.light()-Math.sqrt(light.distanceSquared(x,y,z)))*16));
|
||||
require(indexed.apply(nativeSky,x,y,z)==(nativeSky|expected),"Indexed samples agree with brute-force oracle");
|
||||
}
|
||||
var colored=new DynamicLightField.Collector(4);colored.add(42,false,2,3,4,10,0x55BDFF);
|
||||
var blue=colored.finish(32,0,0,0,42);var blueField=new DynamicLightField.Snapshot(blue);
|
||||
require(blueField.sources().iterator().next().color()==0x55BDFF,"Snapshot retains source colour");
|
||||
require((blueField.apply(nativeSky,2,3,4)&255)==160,"RGB metadata preserves native intensity");
|
||||
var changed=new DynamicLightField.Collector(4);changed.add(42,false,2,3,4,10,0x62FFA0);
|
||||
field.update(blue);require(field.update(changed.finish(32,0,0,0,42)),"Same intensity can change colour");
|
||||
require(blueField.sources().iterator().next().color()==0x55BDFF,"Older coloured snapshot remains immutable");
|
||||
long begin=System.nanoTime();long sum=0;
|
||||
for(int i=0;i<1000000;i++)sum+=stress.apply(0,i%80,2,(i/80)%80);
|
||||
System.out.printf("LIGHT-01 passed: 10,000-drop clustering, 100,000 oracle samples; 1M cached queries %.1f ms (checksum %d)%n",(System.nanoTime()-begin)/1e6,sum);
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
name = "Sanctuary"
|
||||
author = "KOKA99CAB"
|
||||
version = "beta.137"
|
||||
version = "beta.146"
|
||||
pack-format = "packwiz:1.1.0"
|
||||
|
||||
[index]
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check the eight alternating water benchmark passages per scene.
|
||||
|
||||
Usage: python3 scripts/check_water_performance.py build/water144-vulkan.log
|
||||
Writes the detailed receipt beside the log, and rejects incomplete/over-budget runs.
|
||||
The caller must also verify that no other graphics test ran during measurement.
|
||||
"""
|
||||
import json,re,sys
|
||||
from pathlib import Path
|
||||
log_path=Path(sys.argv[1])
|
||||
backend=log_path.stem
|
||||
log=log_path.read_text()
|
||||
rows={key:json.loads(data) for key,data in re.findall(r"WATER144_PERF (\S+) (\[[^\n]+\])",log)}
|
||||
assert len(rows)==24, ("Incomplete measurements",len(rows))
|
||||
result={"backend":backend,"samples":rows,"scenes":{}}
|
||||
for scene in ("mixed","large","moving"):
|
||||
off=[v for k,v in rows.items() if k.startswith(scene+"-false-")]
|
||||
on=[v for k,v in rows.items() if k.startswith(scene+"-true-")]
|
||||
assert len(off)==len(on)==4 and all(v[0]>50 for v in off+on)
|
||||
median_off=sum(v[1] for v in off)/len(off);median_on=sum(v[1] for v in on)/len(on)
|
||||
p95_off=sum(v[2] for v in off)/len(off);p95_on=sum(v[2] for v in on)/len(on)
|
||||
value={"offMedianMs":median_off,"onMedianMs":median_on,"medianIncreasePercent":100*(median_on/median_off-1),"offP95Ms":p95_off,"onP95Ms":p95_on,"p95IncreaseMs":p95_on-p95_off}
|
||||
value["passed"]=value["medianIncreasePercent"]<=5 and value["p95IncreaseMs"]<=2
|
||||
result["scenes"][scene]=value
|
||||
log_path.with_name(log_path.stem+"-performance.json").write_text(json.dumps(result,indent=2)+"\n")
|
||||
print(json.dumps(result["scenes"],indent=2))
|
||||
assert all(v["passed"] for v in result["scenes"].values()), "Performance acceptance not met"
|
||||
@@ -8,3 +8,10 @@ ajoute les ombres portées pixélisées optionnelles, configurables depuis
|
||||
Ce dossier reste réservé aux futurs packs externes. Leur ajout au manifeste
|
||||
packwiz nécessitera version, URL, hash, licence et compatibilité vérifiée pour
|
||||
la version exacte du jeu et du moteur Iris.
|
||||
|
||||
La beta.144 ajoute l’[eau naturelle](../docs/natural-water-beta144.md) dans
|
||||
le rendu natif : profils selon le fond, océans clairs et brume sous-marine.
|
||||
|
||||
La livraison locale beta.145 ajoute le [PBR de l’eau et du verre ainsi que les
|
||||
reflets pixélisés](../docs/pbr-ssr.md). SSR désactivé par défaut ; aucune
|
||||
dépendance graphique externe.
|
||||
|
||||
Reference in New Issue
Block a user