Compare commits
20
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b3eb791fe | ||
|
|
83c377acc1 | ||
|
|
7eef9454d4 | ||
|
|
580b2414da | ||
|
|
97ff2b54cb | ||
|
|
59346d7f0a | ||
|
|
ea151dd5df | ||
|
|
7ce01c47f2 | ||
|
|
93ed5574a4 | ||
|
|
c9f5c56e68 | ||
|
|
6e1b3b7108 | ||
|
|
c05c338e6b | ||
|
|
dc33ffa546 | ||
|
|
9d84a02ed9 | ||
|
|
91181865d5 | ||
|
|
daf36fdeac | ||
|
|
7f576d14d9 | ||
|
|
2d7072b8a8 | ||
|
|
0781cbd964 | ||
|
|
71296a9cfb |
@@ -20,13 +20,20 @@ La [vision complète](docs/vision.md) conserve les intentions ; le
|
||||
L’économie, les claims et les dimensions décrits dans la vision restent
|
||||
à implémenter.
|
||||
|
||||
La version **beta.135** corrige le SSGI en contre-plongée et près des surfaces :
|
||||
normales indépendantes de la taille des pixels et atténuation progressive au
|
||||
contact. Bloom initial à **20 %**, PBR et SSGI **activés par défaut**.
|
||||
**Options → Shaders** ; réglages existants conservés.
|
||||
Le [ticket de livraison](docs/shader-beta135.md) décrit le rendu et ses limites.
|
||||
Release publiée et même instance **Sanctuary Beta** synchronisée, sauvegardes
|
||||
et réglages conservés.
|
||||
La version **beta.144** ajoute une option d’**eau naturelle pixélisée** :
|
||||
couleur du biome, trois profils selon le fond, vert olive d’étang et visibilité
|
||||
adaptée en immersion.
|
||||
Les océans gardent leur couleur native et le profil clair.
|
||||
[Contrat et vérifications](docs/natural-water-beta144.md).
|
||||
La beta.144 est publiée et synchronisée dans **Sanctuary Beta** ;
|
||||
les 923 fichiers personnels suivis sont conservés.
|
||||
|
||||
La version **beta.143** garde le sampler natif de précipitation pour la neige
|
||||
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 [livraison précédente](docs/clay-workshop-integration-beta110.md) réunit déjà
|
||||
l’atelier, les œufs et la neige saisonnière dans l’instance **Sanctuary Beta**.
|
||||
Les [synchronisations antérieures](docs/git-sync-beta092.md) restent documentées.
|
||||
@@ -37,6 +44,80 @@ La [carte native beta.005](docs/atlas-beta005.md) prend le relais après
|
||||
l’étude de Xaero et des alternatives libres.
|
||||
|
||||
|
||||
## beta.144 — Eau naturelle pixélisée
|
||||
|
||||
Eau claire, légèrement teintée ou vert olive d’étang selon le fond, avec
|
||||
textures et animation natives. Océans clairs, visibilité sous-marine adaptée,
|
||||
interrupteur et intensité dans les options Shaders.
|
||||
[Contrat et mesures](docs/natural-water-beta144.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.144/Sanctuary-beta.144.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.144/Sanctuary-Test-beta.144.mrpack) ·
|
||||
[Tous les livrables](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.144).
|
||||
|
||||
## beta.143 — Neige saisonnière en temps réel
|
||||
|
||||
Accumulation ralentie par un facteur 12 et fonte saisonnière ralentie par le
|
||||
facteur naturel 72, tout en conservant le sampler natif des colonnes de
|
||||
précipitation. Deux blocs de neige fondent en environ 2,7 jours réels en été,
|
||||
5,4 au printemps et 11 en automne.
|
||||
[Contrat et vérifications](docs/realtime-snow-melt-beta143.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.143/Sanctuary-beta.143.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.143/Sanctuary-Test-beta.143.mrpack).
|
||||
|
||||
## beta.142 — Vulkan par défaut et stabilité PBR
|
||||
|
||||
Moteur natif Vulkan prioritaire, relief conservé pendant les reconstructions,
|
||||
calculs de reflets bornés et intensité initiale 80 %. Choix personnels conservés.
|
||||
[Contrat et vérifications](docs/pbr-refresh-beta142.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.142/Sanctuary-beta.142.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.142/Sanctuary-Test-beta.142.mrpack).
|
||||
|
||||
## beta.141 — Direction locale et normales PBR
|
||||
|
||||
Relief et reflets orientés par les lumières locales. Cache compact, confiance
|
||||
réduite pour les sources opposées, objets portés et fondus. Activer PBR et
|
||||
Lumières colorées ; portée PBR 32 blocs.
|
||||
[Contrat et vérifications](docs/directional-lights-beta141.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.141/Sanctuary-beta.141.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.141/Sanctuary-Test-beta.141.mrpack).
|
||||
|
||||
## beta.140 — Lumières portées et fondus
|
||||
|
||||
Mains et tête, distances 8/16/32/64, fondus, cuivre vert, redstone rouge et
|
||||
option pour la chaleur des torches. Lumières colorées activées à 30 % par défaut.
|
||||
[Contrat et vérifications](docs/colored-dynamic-beta140.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.140/Sanctuary-beta.140.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.140/Sanctuary-Test-beta.140.mrpack).
|
||||
|
||||
## beta.139 — Lumières colorées
|
||||
|
||||
Teintes chaudes, sources des âmes bleues, redstone rouge, froglights et matériaux lumineux.
|
||||
Option désactivée initialement, intensité 35 % ; propagation locale avec obstacles.
|
||||
[Contrat et vérifications](docs/colored-lights-beta139.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.139/Sanctuary-beta.139.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.139/Sanctuary-Test-beta.139.mrpack).
|
||||
|
||||
## beta.138 — Rayons et brouillard
|
||||
|
||||
Densité automatique des rayons selon le brouillard et les précipitations.
|
||||
[Contrat et vérifications](docs/shader-beta138.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.138/Sanctuary-beta.138.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.138/Sanctuary-Test-beta.138.mrpack).
|
||||
|
||||
## beta.137 — Rayons solaires
|
||||
|
||||
Faisceaux volumétriques légers, intensité réglable et feuillage perméable.
|
||||
[Contrat et vérifications](docs/shader-beta137.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.137/Sanctuary-beta.137.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.137/Sanctuary-Test-beta.137.mrpack).
|
||||
|
||||
## beta.136 — Lens flare rectangulaire
|
||||
|
||||
Soleil et lune, reflets doux et intensité indépendante du bloom.
|
||||
[Contrat et vérifications](docs/shader-beta136.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.136/Sanctuary-beta.136.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.136/Sanctuary-Test-beta.136.mrpack).
|
||||
|
||||
## beta.135 — SSGI près des surfaces
|
||||
|
||||
Correction des coupures à courte distance, sans flouter les textures.
|
||||
|
||||
@@ -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,209 @@
|
||||
# WATER-144 — Eau naturelle pixélisée
|
||||
|
||||
Branche `codex/water-beta144`, socle beta.143 publiée (`97ff2b5`) et suivi
|
||||
`580b241`. Minecraft 26.3, Java 25, dépendances conservées.
|
||||
|
||||
## Contrat
|
||||
|
||||
**Options → Shaders → Eau naturelle** est activée par défaut avec Sanctuary.
|
||||
L’intensité (0–100 %, défaut 100 %) agit sur la surface et l’immersion.
|
||||
L’interrupteur ou 0 % retrouve le rendu précédent. Les préférences existantes
|
||||
restent conservées ; un moteur externe suspend l’effet comme les autres effets
|
||||
natifs Sanctuary.
|
||||
|
||||
La couleur native du biome reste la base, y compris les variantes d’océans.
|
||||
Les océans (`minecraft:is_ocean`) imposent le profil clair.
|
||||
Hors océan, le premier support situé au maximum huit blocs sous la cellule
|
||||
observée choisit le profil :
|
||||
|
||||
| Profil | Fond | Mélange olive #788448 | Alpha natif | Distance de brume native |
|
||||
| --- | --- | ---: | ---: | ---: |
|
||||
| Claire | Pierre, matériaux construits, inconnus | 0 % | ×0,55 | ×1 |
|
||||
| Légèrement teintée | Sable, sable rouge, gravier, argile | 10 % | ×0,80 | ×0,70 |
|
||||
| Étang | Terre, terre stérile/enracinée, herbe, podzol, mycélium, boue, racines de palétuvier boueuses | 85 % | ×1 | ×0,40 |
|
||||
|
||||
L’eau et les plantes aquatiques sans collision sont traversées. Un support
|
||||
immergé reste un fond ; absence de support, air ou données manquantes donnent
|
||||
une eau claire. La référence visuelle fournie pendant l’implémentation demande un véritable
|
||||
vert olive légèrement jaunâtre : le profil étang passe donc de 45 % à 85 %
|
||||
de mélange, tout en laissant les eaux claires et océaniques natives.
|
||||
Les surfaces profondes sans fond dans la limite restent donc
|
||||
claires. Les profils se mélangent aux sommets sur un bloc, sans filtrer les
|
||||
textures. Aux bords océaniques, les sommets partagés restent clairs et la
|
||||
transition occupe le bloc voisin hors océan. L’eau sous un toit suit son fond,
|
||||
pas l’éclairage du bâtiment.
|
||||
|
||||
La ressource client `assets/sanctuary/water_materials.json` accepte deux listes
|
||||
d’identifiants de blocs, `light` et `pond`, remplaçables par un resource pack.
|
||||
Les blocs absents des listes sont clairs. Une ressource invalide est signalée
|
||||
et reprend les familles embarquées. Le template personnel inclut cette ressource.
|
||||
|
||||
## Rendu et coût
|
||||
|
||||
Modification des seules couleurs/alpha des sommets de `FluidRenderer`, sans
|
||||
passe écran ni duplication de maillage. Textures, UV, animation, éclairage
|
||||
et tri des transparences restent natifs. Les calculs de profils sont différés
|
||||
jusqu’à l’émission d’un sommet visible, puis mutualisés dans la région compilée.
|
||||
Un test de palette élimine les sections sans eau avant toute copie de biomes.
|
||||
Les états de blocs viennent des copies natives ; les appartenances océaniques
|
||||
sont capturées avant l’envoi au worker. La présence du terrain est vérifiée dans le cache client avec chargement
|
||||
désactivé, sans utiliser `ClientLevel.hasChunk` (toujours vrai). Aucun
|
||||
chargement de chunk n’est demandé par le classificateur.
|
||||
|
||||
Un changement de bloc invalide la zone dépendante, huit blocs au-dessus et
|
||||
un bloc horizontalement. Les reconstructions natives recopient leurs données ;
|
||||
l’effet ne déclenche aucune reconstruction pour une lumière mobile.
|
||||
Un changement d’option reconstruit seulement les sections chargées contenant de
|
||||
l’eau ; les caches des autres effets restent disponibles. Le cache temporaire
|
||||
disparaît avec sa compilation, sans historique de monde.
|
||||
|
||||
Sous l’eau, le profil est échantillonné au maximum une fois par tick, avec
|
||||
transition de 0,5 seconde. La couleur et les distances environnementales sont
|
||||
adaptées dans le rendu natif du brouillard. La teinte finale est appliquée après
|
||||
la normalisation de la vision aquatique, pour éviter de transformer l’olive en
|
||||
jaune fluorescent ; la luminosité issue des modificateurs natifs de vision
|
||||
et le bonus du compagnon calmar sont conservés. Lave et brume atmosphérique ne changent
|
||||
pas. Il n’y a pas de simulation d’absorption volumétrique ni de reflet ajouté.
|
||||
|
||||
## Validation native
|
||||
|
||||
Les essais utilisent un monde plat jetable, graine 122, avec bassins sur quartz,
|
||||
sable et boue, support du sable, plantes aquatiques, toit, frontière de sections
|
||||
et changement de biome océan. Une copie personnelle de ressource remplace
|
||||
réellement les familles de matériaux puis est désactivée.
|
||||
|
||||
Les suites graphiques finales OpenGL et Vulkan sont réussies le 18 septembre
|
||||
2026 (`WATER144_PASS` et `WATER144_EXTRAS_PASS`). Elles couvrent surface,
|
||||
immersion, fond modifié, priorité océan, toit, plantes, cascades, lave inchangée,
|
||||
blocs gorgés d’eau, préférences FR/EN et rechargement des ressources/chunks.
|
||||
Les sommets partagés aux limites océaniques et le repli clair lorsque le cache
|
||||
client ne contient pas le terrain font l’objet d’assertions supplémentaires.
|
||||
Aucun autre client de test concurrent n’a été détecté pendant ces séries.
|
||||
Deux passages ciblés supplémentaires (`WATER144_TEXTURE_PASS`), Vulkan en
|
||||
1 min 18 s et OpenGL en 1 min 4 s, chargent de vraies textures personnelles
|
||||
32×32 à deux images animées pour l’eau calme et courante. Grille, animation
|
||||
visible et retour aux textures natives sont vérifiés après désactivation.
|
||||
Les fixtures, leurs alpha et les captures restent dans les preuves locales.
|
||||
|
||||
### Protocole de performance
|
||||
|
||||
MacBookPro17,1, Apple M1, 8 Gio, macOS 26.5.1 (25F80), Java 25,
|
||||
fenêtre logique 854×480, cible Retina **1708×960 pixels**, distance de rendu 6 chunks,
|
||||
ombres 32 blocs / 16 pixels, PBR 50 %, autres effets activés.
|
||||
VSync désactivée, limite 260 (illimitée), limitation AFK désactivée.
|
||||
Le mod de test vérifie le limiteur effectif et journalise le mode de présentation.
|
||||
Pendant les échantillons uniquement, il omet l’attente artificielle de
|
||||
`FramerateLimiter.limitDisplayFPS`, afin de mesurer sans temporisation
|
||||
applicative. Cette sonde reste absente du JAR de production. Les temps couvrent
|
||||
la frame native, présentation comprise, et ne sont pas des temps GPU isolés.
|
||||
Pour chaque scène : OFF/ON/ON/OFF/ON/OFF/OFF/ON, dix secondes par passage,
|
||||
avec stabilisation des sections puis quatre secondes de chauffe entre passages.
|
||||
Le déplacement avance de 120 blocs et laisse les sections se compiler
|
||||
naturellement, sans vidange artificielle des caches des autres effets.
|
||||
La scène de déplacement prolonge le bassin hors de la vue initiale : son terrain
|
||||
est préparé côté serveur de test, puis découvert normalement par le client.
|
||||
Le vérificateur exige des compilations réelles dans chaque passage en mouvement.
|
||||
Les moyennes des quatre médianes et P95 par état sont comparées avec les seuils
|
||||
+5 % et +2 ms. Les temps de compilation de sections sont consignés également.
|
||||
|
||||
Les essais courts initiaux, les exécutions concurrentes et les vérifications
|
||||
incomplètes du limiteur restent conservés comme preuves de développement ;
|
||||
ils ne fondent pas ces résultats. Les séries finales ont été exécutées après
|
||||
la fin du chantier SSR. Les variations négatives sont du bruit de mesure,
|
||||
pas une promesse d’accélération. Vulkan reste proche de 120 images/s dans
|
||||
cette chaîne de présentation, malgré le mode `IMMEDIATE` et l’absence de
|
||||
limiteur applicatif : les conclusions sont limitées à la machine et aux
|
||||
conditions ci-dessus.
|
||||
|
||||
| Moteur / scène | Médiane OFF → ON (ms) | Variation | P95 OFF → ON (ms) | Δ P95 (ms) |
|
||||
| --- | --- | ---: | --- | ---: |
|
||||
| vulkan / mixed | 8.069 → 8.092 | +0.29 % | 9.091 → 9.101 | +0.010 |
|
||||
| vulkan / large | 8.078 → 8.062 | -0.20 % | 9.126 → 9.114 | -0.012 |
|
||||
| vulkan / moving | 8.123 → 8.103 | -0.24 % | 9.103 → 9.080 | -0.023 |
|
||||
| opengl / mixed | 4.545 → 4.407 | -3.02 % | 7.080 → 6.733 | -0.347 |
|
||||
| opengl / large | 4.879 → 5.110 | +4.73 % | 7.519 → 7.767 | +0.248 |
|
||||
| opengl / moving | 6.817 → 6.338 | -7.02 % | 10.618 → 9.759 | -0.859 |
|
||||
|
||||
Les six comparaisons respectent les seuils : médiane ≤ +5 %, P95 ≤ +2 ms.
|
||||
Le cas limitant est la grande surface OpenGL (+4,73 %, +0,248 ms).
|
||||
Les compilations de sections sont mesurées sur le parcours en mouvement :
|
||||
|
||||
| Moteur | Sections OFF / ON | Médiane OFF → ON (ms) | P95 OFF → ON (ms) |
|
||||
| --- | ---: | --- | --- |
|
||||
| vulkan | 222 / 220 | 1.889 → 2.186 | 4.307 → 4.898 |
|
||||
| opengl | 222 / 220 | 1.528 → 1.540 | 5.492 → 5.227 |
|
||||
|
||||
Pour reproduire, avec Java 25 et un environnement graphique disponible :
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
|
||||
-PsanctuaryPixelShadows122ClientTests=true -PsanctuaryWater144ClientTests=true \
|
||||
-PsanctuaryClientNoVsync=true -PsanctuaryQuickTests=true \
|
||||
-PsanctuaryClientGraphicsBackend=vulkan > build/water144-vulkan.log 2>&1
|
||||
python3 scripts/check_water_performance.py build/water144-vulkan.log
|
||||
```
|
||||
|
||||
Répéter avec `opengl`, sans autre client graphique simultané. Le vérificateur
|
||||
refuse les séries incomplètes et les dépassements ; il écrit le reçu détaillé
|
||||
près du journal. Le marqueur `WATER144_PASS` atteste les contrôles fonctionnels.
|
||||
|
||||
### Assemblage et publication
|
||||
|
||||
Le GameTest serveur dédié reste exclu conformément au refus antérieur de son
|
||||
EULA. La commande de livraison est `./gradlew check build assemblePack
|
||||
assembleTestPack -x :sanctuary:runGameTest` ; la suite complète non exclue
|
||||
n’est pas déclarée réussie. Les contrôles natifs de l’eau utilisent le serveur
|
||||
intégré d’un monde jetable. Aucun essai Windows n’est revendiqué.
|
||||
|
||||
L’assemblage avec l’exclusion ci-dessus a réussi en 2 min 33 s (126 tâches).
|
||||
`python3 scripts/pack.py check` réussit. Les cinq archives passent le contrôle
|
||||
ZIP ; les deux MRpack embarquent exactement le même JAR Sanctuary et déclarent
|
||||
Fabric API 0.160.5+26.3 avec l’empreinte du manifeste source. L’export MRpack est
|
||||
exécuté depuis chaque dossier de staging packwiz. Les dépendances imbriquées
|
||||
Demeure et JEI sont vérifiées par l’assemblage natif du dépôt.
|
||||
|
||||
[Release beta.144](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.144) publiée depuis
|
||||
le commit source `83c377acc1b5a4d4137d7a93c397531f77915303` ; tag exact `beta.144`.
|
||||
Canal packwiz stable : `c8e28d878a55703bdc28fc046242b42b2fcfa9f6`.
|
||||
|
||||
Deux synchronisations isolées puis deux synchronisations de l’instance
|
||||
**Sanctuary Beta** ont réussi, jeu et lanceur fermés. Les secondes passes sont
|
||||
idempotentes. Un seul JAR Sanctuary est actif, et son SHA-256 correspond
|
||||
exactement à celui de la release et du manifeste stable.
|
||||
Les **923 fichiers personnels** suivis (sauvegardes, configurations,
|
||||
packs, captures et mods non gérés) ont conservé leurs empreintes. Aucun monde
|
||||
personnel ouvert. Sauvegarde des fichiers remplacés :
|
||||
`sanctuary-backups/before-beta.144/`.
|
||||
|
||||
Les neuf liens publics (cinq livrables, trois captures, manifeste SHA-256)
|
||||
ont été retéléchargés anonymement et leurs empreintes comparées. Les reçus de
|
||||
publication, de téléchargement et des quatre synchronisations restent dans
|
||||
`build/water144-publish.log`, `build/water144-public-downloads.json`,
|
||||
`build/water144-isolated.json` et `build/water144-prism.json`.
|
||||
Le checkout initial beta.122 et ses modifications restent préservés.
|
||||
Les sources sont sur `codex/water-beta144`, dans le worktree isolé.
|
||||
Les archives vérifiées sont aussi copiées dans `sanctuary-beta/build/`.
|
||||
Les preuves détaillées restent dans les dossiers ignorés :
|
||||
`build/water144-*.log`, `build/water144-*-performance.json`,
|
||||
`build/water144-*-isolation.json` et `build/water144-evidence/`.
|
||||
|
||||
## Artefacts vérifiés
|
||||
|
||||
| Artefact | Octets | SHA-256 |
|
||||
| --- | ---: | --- |
|
||||
| [sanctuary-beta.144.jar](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.144/sanctuary-beta.144.jar) | 11343257 | `1ddbe941f3262da8a1497ea5df4a992e260b80ae95b341623dd68f8f9729ae88` |
|
||||
| [Sanctuary-beta.144.mrpack](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.144/Sanctuary-beta.144.mrpack) | 10483196 | `d5ef5a781f5ee796117210d1f6afc96172461315faee34097323beff541ce6f0` |
|
||||
| [Sanctuary-Test-beta.144.mrpack](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.144/Sanctuary-Test-beta.144.mrpack) | 10502119 | `e7cc32ac19ae98ebbe7a9aac3dc08decdf09396c8175b597f87051399ff3812e` |
|
||||
| [Sanctuary-Template-beta.144.zip](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.144/Sanctuary-Template-beta.144.zip) | 4354678 | `e510c0040dc9053492715ac91381b7dbc5a6f7c7defbf81e7502d8e18dbb0ee6` |
|
||||
| [Sanctuary-Prism-auto-update.zip](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.144/Sanctuary-Prism-auto-update.zip) | 92423 | `c0920823f40d709c14e074d8d949f2cc3681c24140bf86f21c3be345cfc3e1f6` |
|
||||
|
||||
## Captures comparatives
|
||||
|
||||
Pièces jointes de la release, issues du client Vulkan de validation :
|
||||
[OFF](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.144/Sanctuary-beta.144-water-off.png),
|
||||
[ON](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.144/Sanctuary-beta.144-water-on.png),
|
||||
[profil étang en proximité](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.144/Sanctuary-beta.144-water-pond.png).
|
||||
Les captures sont exportées à 854×480 ; la cible de présentation mesurée est
|
||||
1708×960. Le [manifeste SHA-256](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.144/Sanctuary-beta.144-SHA256SUMS.txt) inclut les cinq
|
||||
livrables et ces trois captures. Les journaux et échantillons détaillés restent
|
||||
locaux, dans `build/water144-evidence/` et les reçus `build/water144-*.json`.
|
||||
@@ -14,6 +14,105 @@ 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
|
||||
|
||||
Trois profils selon le fond, teinte olive d’étang, océans natifs clairs et
|
||||
brume aquatique adaptée. [Contrat, captures et mesures](natural-water-beta144.md).
|
||||
[Release beta.144](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.144)
|
||||
publiée depuis `83c377acc1b5a4d4137d7a93c397531f77915303` ; canal
|
||||
`c8e28d878a55703bdc28fc046242b42b2fcfa9f6`. Deux synchronisations isolées puis deux
|
||||
dans l’instance Sanctuary Beta réussies et idempotentes. Un seul JAR Sanctuary
|
||||
actif, identique à l’artefact public. Les 923 fichiers personnels suivis gardent
|
||||
leurs empreintes ; aucun monde personnel ouvert. Sauvegarde préalable :
|
||||
`sanctuary-backups/before-beta.144/`. Les cinq livrables et leurs SHA-256 sont
|
||||
vérifiés dans `sanctuary-beta/build/`. Textures beta.121, Minecraft 26.3 et
|
||||
dépendances exactes conservés.
|
||||
|
||||
## beta.143 — Neige saisonnière en temps réel
|
||||
|
||||
Accumulation espacée par un facteur 12 et fonte par le facteur temps réel 72,
|
||||
avec le sampler natif de précipitation conservé. Protections inchangées pour
|
||||
la neige construite, les biomes froids, l'hiver, les profils neigeux et la
|
||||
météo Vanilla. [Contrat et vérifications](realtime-snow-melt-beta143.md).
|
||||
[Release beta.143](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.143)
|
||||
publiée ; canal `bafff5e9fcdd95c5dd184953a34ada805e455797`. Deux synchronisations
|
||||
isolées puis deux dans l'instance Sanctuary Beta réussies. Les 923 fichiers
|
||||
personnels suivis gardent leurs hashes. Aucun monde personnel ouvert ;
|
||||
sauvegarde préalable `sanctuary-backups/before-beta.143/`. Archives vérifiées
|
||||
dans `sanctuary-beta/build/`.
|
||||
|
||||
## beta.142 — Vulkan par défaut et stabilité PBR
|
||||
|
||||
Vulkan natif prioritaire, rafraîchissements PBR stables et intensité initiale
|
||||
80 %, calculs de reflets bornés. [Contrat et vérifications](pbr-refresh-beta142.md).
|
||||
[Release beta.142](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.142)
|
||||
publiée ; canal `d661260e2bbabb0326c86d6cc543daf986979596`. Deux synchronisations isolées
|
||||
puis deux dans l'instance Sanctuary Beta réussies. Les 923 fichiers personnels
|
||||
suivis gardent leurs hashes. Aucun monde personnel ouvert ; sauvegarde préalable
|
||||
`sanctuary-backups/before-beta.142/`. Archives vérifiées dans `sanctuary-beta/build/`.
|
||||
Un choix OpenGL explicite reste conservé : sélectionner Vulkan dans les réglages
|
||||
vidéo et redémarrer pour changer ce choix. La capture Windows de bruit en
|
||||
mouvement reste à reconfirmer sur son pilote ; les contrôles OpenGL/Vulkan Mac
|
||||
et la correction du domaine numérique du reflet sont documentés dans le ticket.
|
||||
|
||||
## beta.141 — Direction des lumières pour les normales PBR
|
||||
|
||||
Directions locales mises en cache, sources portées, confiance réduite lorsque
|
||||
les sources s'opposent et fondus. Activer PBR et Lumières colorées ; portée PBR
|
||||
de 32 blocs. [Contrat et vérifications](directional-lights-beta141.md).
|
||||
[Release beta.141](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.141)
|
||||
publiée ; canal `7e6c84400f80db24bf5204cd21d628194ae9f0f7`. Deux synchronisations isolées
|
||||
puis deux dans l'instance Sanctuary Beta réussies. Les 923 fichiers personnels
|
||||
suivis gardent leurs hashes. Aucun monde personnel ouvert ; sauvegarde préalable
|
||||
`sanctuary-backups/before-beta.141/`. Archives vérifiées dans `sanctuary-beta/build/`.
|
||||
|
||||
## beta.140 — Lumières portées et fondus
|
||||
|
||||
Couleurs des objets en main et sur la tête, distances 8/16/32/64 et fondus
|
||||
préservant la teinte. Torches natives par défaut, option chaude, cuivre vert
|
||||
et redstone rouge. [Contrat et vérifications](colored-dynamic-beta140.md).
|
||||
[Release beta.140](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.140)
|
||||
publiée ; canal `2c360a0759d4a164f82c89ac2bef04ff69d50875`. Deux synchronisations isolées
|
||||
puis deux dans l'instance Sanctuary Beta réussies. Les 923 fichiers personnels
|
||||
suivis gardent leurs hashes. Aucun monde personnel ouvert ; sauvegarde préalable
|
||||
`sanctuary-backups/before-beta.140/`. Archives vérifiées dans `sanctuary-beta/build/`.
|
||||
|
||||
## beta.138 — Densité des rayons selon la météo
|
||||
|
||||
Brouillard plus dense et diffus, précipitations et atténuation de l'orage.
|
||||
Réglage manuel conservé, aucune nouvelle option ni modification de sauvegarde.
|
||||
[Contrat et vérifications](shader-beta138.md).
|
||||
[Release beta.138](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.138)
|
||||
publiée ; canal `c049423de1971079b9d61874d83adeb6f1ad49f7`. Deux synchronisations isolées,
|
||||
puis deux dans la même instance Sanctuary Beta réussies. Les 923 fichiers
|
||||
personnels suivis gardent leurs hashes. Aucun monde personnel ouvert ;
|
||||
sauvegarde préalable `sanctuary-backups/before-beta.138/`.
|
||||
Packs normal/Test/template vérifiés dans `sanctuary-beta/build/`.
|
||||
|
||||
## beta.137 — Rayons solaires volumétriques
|
||||
|
||||
Rayons à travers les ouvertures et le feuillage, activés à 35 %, indépendants
|
||||
du bloom et de l'affichage des ombres de surface. Carte solaire partagée.
|
||||
[Contrat et vérifications](shader-beta137.md).
|
||||
[Release beta.137](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.137)
|
||||
publiée ; canal `68cb39a5edbf5d34def02f670e2488ee852605b8`. Deux synchronisations isolées,
|
||||
puis deux dans la même instance Sanctuary Beta réussies. Les 923 fichiers
|
||||
personnels suivis gardent leurs hashes. Aucun monde personnel ouvert ;
|
||||
sauvegarde préalable `sanctuary-backups/before-beta.137/`.
|
||||
Packs normal/Test/template vérifiés dans `sanctuary-beta/build/`.
|
||||
|
||||
## beta.136 — Lens flare rectangulaire
|
||||
|
||||
Rectangles à dégradé doux pour le soleil et la lune, activés à 30 % et
|
||||
réglables indépendamment du bloom. Occultation par les blocs et phases lunaires.
|
||||
[Contrat et vérifications](shader-beta136.md).
|
||||
[Release beta.136](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.136)
|
||||
publiée ; canal `dd5b545a7cfbb957f2c70e9dc17034b44caef98f`. Deux synchronisations isolées,
|
||||
puis deux dans la même instance Sanctuary Beta réussies. Les 923 fichiers
|
||||
personnels suivis gardent leurs hashes. Aucun monde personnel ouvert ;
|
||||
sauvegarde préalable `sanctuary-backups/before-beta.136/`.
|
||||
Packs normal/Test/template vérifiés dans `sanctuary-beta/build/`.
|
||||
|
||||
## beta.135 — SSGI en proximité et nouveaux défauts
|
||||
|
||||
Normales stables près des surfaces, atténuation progressive du rebond au contact.
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
# PBR-142 — Stabilité du relief et Vulkan par défaut
|
||||
|
||||
Socle beta.141 `7ce01c4`, branche `codex/pbr-refresh-beta142`.
|
||||
Minecraft 26.3 / Java 25. Livraison beta.142.
|
||||
|
||||
## Contrat
|
||||
|
||||
Corriger le clignotement lors de la reconstruction des sections PBR : conserver
|
||||
le rendu précédent jusqu'à ce que le remplaçant soit disponible et éviter les
|
||||
reconstructions dues seulement aux lumières dynamiques. Une invalidation pendant
|
||||
une reconstruction doit être conservée, sans annuler indéfiniment le travail.
|
||||
Le PBR passe à 80 % par défaut, valeur confirmée par l'utilisateur ; les choix
|
||||
explicitement enregistrés restent conservés.
|
||||
|
||||
La capture `2026-09-18_04.19.34.png` montre des points très clairs, plus fins
|
||||
que les pixels des textures, sur des parois et un plafond de grotte. L'utilisateur
|
||||
confirme OpenGL et leur disparition quand seul le PBR est coupé. Examiner
|
||||
les lectures de textures et les calculs aux angles rasants séparément du cache.
|
||||
|
||||
Tests natifs prévus : recalcul de section sous observation, invalidations
|
||||
répétées, remplacement atomique, suppression réelle de blocs, lumière portée,
|
||||
cache et préférences ; OpenGL et Vulkan. Pas de monde personnel ouvert.
|
||||
|
||||
## Précision du signalement et implémentation
|
||||
|
||||
L'utilisateur précise ensuite que le bruit apparaît **en mouvement**, et demande
|
||||
le passage à Vulkan. Il ne faut donc pas attribuer les points de sa capture au
|
||||
seul clignotement de reconstruction : ce sont deux vérifications distinctes.
|
||||
|
||||
- Les sections PBR publiées restent affichées jusqu'au remplacement de leur
|
||||
maillage. Une nouvelle invalidation durant la construction est mémorisée ;
|
||||
elle n'annule plus continuellement le travail. Le budget permanent reste
|
||||
24 Mio, avec temporairement l'ancien et le nouveau maillage d'une section.
|
||||
- Les invalidations de lumière dynamique ne régénèrent plus la géométrie PBR.
|
||||
Les changements de blocs et de lumière du ciel restent pris en compte.
|
||||
- Lectures explicites des texels de profondeur/scène et du niveau de matériau,
|
||||
gradients de texture calculés avant les rejets de fragments. Les produits
|
||||
scalaires des reflets sont bornés, la puissance entière de Fresnel est
|
||||
explicite, les résultats non finis ne remplacent pas la scène native.
|
||||
Aucun flou ajouté aux textures.
|
||||
- Le choix graphique natif « Par défaut » devient « Vulkan (par défaut) »
|
||||
(FR/EN) et essaie Vulkan en premier. Minecraft conserve son repli OpenGL si
|
||||
Vulkan ne peut pas être initialisé. Un choix **OpenGL explicite** reste
|
||||
disponible ; pour une installation ainsi réglée, sélectionner Vulkan dans
|
||||
les réglages vidéo puis redémarrer. Aucun moteur externe ni VulkanMod ajouté.
|
||||
- 80 % est la nouvelle intensité PBR initiale. Les valeurs déjà enregistrées
|
||||
explicitement restent conservées.
|
||||
|
||||
Le code Minecraft 26.3 a été inspecté : `PreferredGraphicsApi.VULKAN` essaie
|
||||
Vulkan puis OpenGL ; le choix natif `DEFAULT` essayait auparavant OpenGL d'abord.
|
||||
Tous les effets Sanctuary utilisent déjà l'API graphique native. Le seul hook
|
||||
OpenGL direct est le correctif spécifique de lecture des panoramas dans
|
||||
`GlCommandEncoder` ; il ne remplace pas la voie Vulkan native.
|
||||
|
||||
Référence pour les lectures après contrôle divergent :
|
||||
[documentation Khronos des samplers](https://wikis.khronos.org/opengl/Sampler_%28GLSL%29).
|
||||
La correction rend ces lectures explicites ; elle ne constitue pas à elle seule
|
||||
une reproduction du défaut sur le pilote Windows de l'utilisateur.
|
||||
|
||||
## Observations de diagnostic
|
||||
|
||||
Le détecteur de points blancs isolés (minimum RGB 170, au moins cinq voisins
|
||||
sur huit sous 120) compte 669 points sur l'image utilisateur et zéro sur la
|
||||
première grotte de contrôle. Il s'agit de scènes, résolutions et pilotes différents,
|
||||
pas d'un avant/après reproduisant exactement le bug utilisateur. Les rendus
|
||||
natifs OpenGL de contrôle étaient déjà exempts de ce grain avant les nouvelles
|
||||
protections du shader ; le signalement Windows reste donc à reconfirmer là-bas.
|
||||
|
||||
Le test de rafraîchissement conserve les mêmes maillages publiés durant douze
|
||||
invalidations consécutives, compare l'image et vérifie que des remplacements
|
||||
aboutissent. Le test de torche alterne quatre fois les torches normales/des âmes
|
||||
sans reconstruction des normales. Le contrôle de mouvement presse réellement
|
||||
Avancer, maintient la torche et le PBR actifs et vérifie un déplacement d'au moins
|
||||
un bloc avant de vérifier les 24 captures. Les angles -30°, 0° et 35° sont aussi
|
||||
contrôlés, ainsi que les effets combinés et les préférences.
|
||||
|
||||
### Domaine invalide du reflet de la torche en main
|
||||
|
||||
Pour la lumière du joueur en vue subjective, la source est la caméra : le
|
||||
vecteur lumière et le vecteur de vue sont parallèles. L'ancien
|
||||
`pow(1 - max(dot(v,h),0),5)` n'avait aucune borne supérieure pour ce produit
|
||||
scalaire. Un calcul reproductible Float32 (100 000 positions, graine 142)
|
||||
donne 19 991 bases légèrement négatives, jusqu'à -2,3841858e-7, contre zéro
|
||||
après clamp. C'est une reproduction numérique d'un **domaine de calcul invalide**,
|
||||
pas une reproduction des pixels produits par le pilote utilisateur.
|
||||
|
||||
GLSL 3.30 ne définit pas `pow` pour une base négative, même avec un exposant
|
||||
entier : [spécification Khronos, fonctions exponentielles](https://registry.khronos.org/OpenGL/specs/gl/GLSLangSpec.3.30.pdf).
|
||||
Le correctif borne les cosinus à [0,1] et remplace cette puissance par des
|
||||
multiplications. La torche alignée sur la caméra ne dépend donc plus du traitement
|
||||
de cette entrée invalide par le pilote. Reçu numérique ignoré :
|
||||
`build/pbr142-fresnel-domain.json`.
|
||||
|
||||
### Mise à jour des fixtures antérieures
|
||||
|
||||
La suite Vulkan complète s'est arrêtée au contrôle SSGI de coin : l'oracle
|
||||
ne trouvait plus de pierre neutre (`rows=0`), car les lumières colorées ajoutées
|
||||
depuis beta.139/140 teintaient la pierre avec la lave. Le test de proximité SSGI
|
||||
et le test PBR solaire isolent désormais leur effet en coupant cette autre passe.
|
||||
Aucun seuil n'a été diminué et aucun changement de rendu SSGI n'est livré.
|
||||
Le sous-ensemble en échec est relancé séparément ; les étapes précédentes
|
||||
(ombres, CTM, bloom, lens flare, déplacement SSGI) avaient réussi.
|
||||
|
||||
## Vérifications natives
|
||||
|
||||
Sur Mac Apple M1, Java 25 et Minecraft 26.3 :
|
||||
|
||||
- OpenGL : suite couleurs/directions/rafraîchissement en 3 min 36 s, puis
|
||||
déplacement continu avec torche active en 1 min 36 s. Zéro point blanc isolé
|
||||
sur les 24 images de mouvement et les trois angles de grotte.
|
||||
- Vulkan/MoltenVK 1.4.2 : sélection automatique sans argument de moteur,
|
||||
couleurs/directions/rafraîchissement en 3 min 47 s. Dernier test continu avec
|
||||
torche active en 1 min 37 s, zéro point blanc isolé sur les 24 images.
|
||||
- Ombrages lune/midi/feuilles, distances et précisions, stabilité temps réel,
|
||||
CTM, bloom (minerais, torche, lampe, lave, soleil), émissifs indépendants et
|
||||
lens flare validés sous Vulkan dans la suite d'effets. Le déplacement SSGI
|
||||
sur douze blocs passe également. La suite s'arrête ensuite sur l'oracle
|
||||
ancien de couleur de pierre décrit ci-dessus : ne pas présenter ce lancement
|
||||
de 7 min 15 s comme une réussite complète.
|
||||
- Reprise ciblée après isolation de cet oracle : SSGI près des surfaces puis
|
||||
PBR extérieur réussis sous Vulkan en 2 min 55 s. Proximité 35/100, haute
|
||||
résolution, léger mouvement, toit ajouté/retiré, reload, resize, zéro/OFF et
|
||||
effets combinés. Gain SSGI au coin test : 6,01 sur 99 lignes à 35 %.
|
||||
- Les nouvelles valeurs initiales et les choix enregistrés sont testés, dont
|
||||
une intensité PBR explicitement enregistrée à 50 qui reste à 50.
|
||||
|
||||
Les premiers essais du nouveau contrôle de mouvement ont permis de corriger
|
||||
son entrée clavier : `pressKey` ne maintient pas une touche enfoncée. Le test
|
||||
final utilise `holdKey` puis `releaseKey`, vérifie le déplacement réel et refuse
|
||||
la disparition de la lumière portée ou du PBR pendant les captures.
|
||||
|
||||
Les jeux de test utilisent uniquement des mondes neufs et ignorés. La capture
|
||||
et le pilote Windows utilisateur n'ont pas été reproduits à l'identique ; leur
|
||||
retest reste ouvert malgré les corrections numériques et les contrôles natifs.
|
||||
|
||||
## Construction et archives
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
réussit en **2 min 38 s**, 126 tâches. Le GameTest serveur dédié reste exclu
|
||||
selon le refus EULA antérieur ; les suites clientes ci-dessus ont réellement
|
||||
utilisé les deux moteurs natifs. `git diff --check` passe.
|
||||
|
||||
Les archives normale/Test embarquent le même JAR, leurs versions/index/hashes
|
||||
sont vérifiés, leurs sources correspondent aux fichiers courants. Le template
|
||||
contient exactement les ressources attendues. Les onze entrées de production
|
||||
modifiées par rapport à beta.141 sont limitées au PBR, au choix graphique et
|
||||
aux traductions ; les ressources de jeu antérieures sont conservées.
|
||||
|
||||
SHA-256 du JAR : `0bc2da9df2ef004e6dc78932cfde7b31b7717c63c83ceed789d151177c41bc1a`.
|
||||
|
||||
- `Sanctuary-beta.142.mrpack` : 10464746 octets ; SHA-256 `dba4fd8e1e3b20136faa1c1de3494df3cb250c87de199c04e07883c786ca48ee`.
|
||||
- `Sanctuary-Test-beta.142.mrpack` : 10483672 octets ; SHA-256 `d56b81a4aea6649a40a3c49dedee904989943a518ad0ae7c520b808dd0f9b9a7`.
|
||||
|
||||
Reçu détaillé ignoré : `build/shader142-artifact.json`.
|
||||
|
||||
## Livraison réalisée
|
||||
|
||||
[Release beta.142](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.142)
|
||||
publiée depuis `ea151dd5df8c7c5abd0107656f40c723efbf2ebe`. Canal stable :
|
||||
`d661260e2bbabb0326c86d6cc543daf986979596`. Archives normale/Test, ZIP d'amorçage Prism et template
|
||||
publiés avec vérification de leurs téléchargements ; tag et artefacts immuables.
|
||||
|
||||
Deux synchronisations isolées puis deux synchronisations de l'instance
|
||||
**Sanctuary Beta** réussies. Les 923 fichiers personnels suivis ont conservé
|
||||
leurs hashes, notamment mondes, réglages et packs personnels. Aucun monde
|
||||
personnel ouvert. Sauvegarde préalable : `sanctuary-backups/before-beta.142/`.
|
||||
L'instance locale conserve son réglage graphique natif « default », qui démarre
|
||||
désormais avec Vulkan prioritaire. Un Windows explicitement réglé sur OpenGL
|
||||
doit sélectionner Vulkan dans les réglages vidéo et redémarrer.
|
||||
|
||||
Archives locales également dans `sanctuary-beta/build/`. Le checkout de travail
|
||||
historique modifié `sanctuary-beta` n'a pas été réinitialisé ; seules ces archives
|
||||
ignorées y ont été déposées. Sources sur `codex/pbr-refresh-beta142`.
|
||||
@@ -0,0 +1,68 @@
|
||||
# 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,85 @@
|
||||
# SHADER-136 — Lens flare rectangulaire doux
|
||||
|
||||
Demande : ajouter un lens flare avec des rectangles dégradés, exclusivement
|
||||
pour le soleil et la lune. Socle beta.135, branche `codex/lens-flare-beta136`.
|
||||
Minecraft 26.3 / Java 25. Aucun changement de gameplay ou de monde.
|
||||
|
||||
## Contrat
|
||||
|
||||
Effet activé initialement à **30 %**, intensité réglable 0–100, indépendant du halo de bloom.
|
||||
Rectangles translucides aux bords doux, suivant la position de l'astre à l'écran.
|
||||
La visibilité provient de l'image émissive céleste native, avant les blocs :
|
||||
les murs masquent l'effet, les sources de terrain ne le déclenchent pas.
|
||||
L'astre sortant du champ doit s'atténuer sans sauter sur le bord opposé.
|
||||
Pas d'historique, ni lecture GPU vers le CPU, ni texture générée sur disque.
|
||||
|
||||
## Rendu et budget
|
||||
|
||||
Trois rectangles à dégradé doux par astre : un halo horizontal près de sa
|
||||
position et deux reflets plus petits le long de l'axe passant par le centre de
|
||||
l'écran. Teinte solaire chaude, lunaire légèrement bleue. Intégration de la
|
||||
lumière réellement dessinée par le ciel natif ; phases de lune conservées.
|
||||
Atténuation aux bords de l'écran et à l'horizon, sans projection derrière la caméra.
|
||||
|
||||
Le masque céleste est mesuré avant l'ajout des émissifs de terrain. Réduction
|
||||
vers une texture 2 × 1 RGBA16F (512 échantillons au maximum au total), puis une
|
||||
passe de composition avec deux lectures de cette texture et des dégradés analytiques.
|
||||
La cible d'émission plein écran existante est réutilisée. Quand seul le flare
|
||||
fonctionne, aucun maillage d'émissif de terrain ni tampon de flou n'est construit.
|
||||
OFF et zéro libèrent la cible du flare ; les autres effets gardent leur autonomie.
|
||||
|
||||
## Vérification
|
||||
|
||||
Le scénario client natif réussit en **1 min 52 s** sur macOS / Apple M1 / OpenGL.
|
||||
Soleil OFF/30/80, lune et nouvelle lune, mur occultant, source hors champ,
|
||||
glowstone exclue, image statique, redimensionnement et rechargement des ressources.
|
||||
Le flare reste actif sans bloom ; OFF/zéro libèrent ses ressources. Préférences
|
||||
vierges, partielles, personnalisées, bornage et libellés FR/EN vérifiés.
|
||||
Les contrôles existants du bloom et des minerais émissifs passent également.
|
||||
Log : `build/shader136-client-final.log`, marqueurs `EMISSIVE131_PASS` et
|
||||
`LENS_FLARE136_PASS`. Les captures natives restent dans le dossier de tests ; comparatif autonome
|
||||
`build/Shader-beta.136-comparaison.html` (soleil OFF/30/80 et lune OFF/30).
|
||||
|
||||
Une première tentative s'est arrêtée sur l'attente d'un angle de caméra :
|
||||
Minecraft normalise 180° en −180°. Le scénario utilise maintenant un angle
|
||||
canonique ; ce changement concerne uniquement le test.
|
||||
|
||||
## Limites
|
||||
|
||||
Effet optique en espace écran : il s'atténue quand l'astre quitte le champ et
|
||||
à l'horizon. Le masque natif du ciel contrôle l'occultation ; la lune reste
|
||||
volontairement beaucoup plus discrète. Le brouillard de submersion suspend
|
||||
l'effet comme le bloom existant. Aucun essai Windows/Vulkan ni mesure de FPS
|
||||
revendiqués.
|
||||
|
||||
|
||||
## Construction et archives
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
réussit en **2 min 29 s**, 126 tâches. Le serveur GameTest dédié reste exclu
|
||||
conformément au refus EULA antérieur. Log : `build/shader136-build.log`.
|
||||
Comparaison à beta.135 : **9 entrées de production** modifiées, uniquement
|
||||
le rendu, ses préférences et ses libellés. Gameplay et textures des gemmes
|
||||
strictement conservés. JAR normal/Test identique, sources et template vérifiés.
|
||||
Reçu local : `build/shader136-artifact.json`.
|
||||
|
||||
- `Sanctuary-beta.136.mrpack` : 10424821 octets ; SHA-256 `808006bf2b0cb3fe63d3e16ae3a0003d27ad0b7e793a3a05f25b5d73ae88bc8d`.
|
||||
|
||||
- `Sanctuary-Test-beta.136.mrpack` : 10443744 octets ; SHA-256 `70f593ad16d1f4fd4305b276ce2727a63f87bcb84df45e55840c1427a314a1eb`.
|
||||
|
||||
JAR : `086afede22903f8988889f6085631451f65d6d55c2e4c81f249d2234698e68e5`.
|
||||
|
||||
## Livraison
|
||||
|
||||
[Release beta.136](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.136)
|
||||
publiée depuis `71296a9cfb0f6b1a6e6f0c48145f3cc432c8afd0`. Canal packwiz
|
||||
`dd5b545a7cfbb957f2c70e9dc17034b44caef98f` ; téléchargements publics vérifiés par SHA-256.
|
||||
Deux synchronisations isolées puis deux dans la même instance **Sanctuary Beta**
|
||||
réussissent. Un seul JAR beta.136 actif ; les **923 fichiers personnels suivis**,
|
||||
dont sauvegardes et préférences, gardent leurs hashes. Copie préalable dans
|
||||
`sanctuary-backups/before-beta.136/`, aucun monde personnel ouvert.
|
||||
|
||||
Packs normal/Test/template et comparatif natif également vérifiés dans
|
||||
`sanctuary-beta/build/`. Le checkout original garde ses changements préexistants.
|
||||
Reçus locaux ignorés : `build/shader136-artifact.json`, `build/shader136-isolated.json`,
|
||||
`build/shader136-prism.json`, `build/shader136-publication.log`.
|
||||
@@ -0,0 +1,96 @@
|
||||
# SHADER-137 — Rayons solaires volumétriques
|
||||
|
||||
Demande validée : faisceaux légers découpés par les blocs, carte d'ombres partagée,
|
||||
feuillage perméable, rendu natif préservé, option et intensité indépendantes du bloom.
|
||||
Socle beta.136, branche `codex/sunshafts-beta137`, Minecraft 26.3 / Java 25.
|
||||
Aucun changement de monde ni de gameplay.
|
||||
|
||||
## Contrat
|
||||
|
||||
Soleil uniquement ; activé initialement à 35 %, intensité 0–100.
|
||||
Intégration de 16 segments par pixel à un quart de chaque dimension.
|
||||
Reconstruction guidée par la profondeur, sans historique ni bruit animé.
|
||||
Réutilisation du masque de feuillage déjà stable dans les ombres.
|
||||
La carte solaire peut fonctionner sans afficher les ombres sur les surfaces.
|
||||
Portée bornée à 64 blocs et à la moitié de la portée des ombres.
|
||||
Atténuation aux limites de la carte, à l'horizon et avec la météo.
|
||||
|
||||
## Rendu et coût borné
|
||||
|
||||
Deux passes : intégration dans une cible RGBA16F de largeur et hauteur divisées
|
||||
par quatre, puis reconstruction guidée par la profondeur. Seize segments
|
||||
quadratiques privilégient les premiers mètres ; quatre lectures fixes de la
|
||||
carte d'ombres par segment adoucissent seulement la lumière dans l'air.
|
||||
La dernière portion du rayon rétrécit continûment à l'approche d'une surface.
|
||||
La lumière utilise une teinte solaire chaude mélangée au brouillard natif.
|
||||
|
||||
La carte d'ombres et ses maillages sont partagés, y compris les trous de
|
||||
transmission déterministes des feuilles. Aucune copie d'image plein écran,
|
||||
aucune duplication du terrain, aucune lecture GPU côté CPU ni accumulation
|
||||
entre images dans le rendu de production. OFF/zéro libèrent le volume ; la
|
||||
carte d'ombres reste seulement si les ombres de surface sont activées.
|
||||
Le soleil sous l'horizon et la submersion suspendent les rayons.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Scénario natif réussi en **1 min 41 s** sur macOS / Apple M1 / OpenGL : pièce
|
||||
entièrement fermée, toit ajouré OFF/35/100, feuilles puis ciel ouvert, déplacement
|
||||
de quatre blocs et retour, caméra proche d'un mur à 0,5 puis 0,125 bloc,
|
||||
soleil hors champ, nuit, OFF/zéro, carte d'ombres partagée, effet combiné,
|
||||
redimensionnement à 961 × 541 et rechargement des ressources. Préférences
|
||||
vierges/partielles/personnelles, bornage et libellés FR/EN également vérifiés.
|
||||
Log : `build/shader137-client-final.log`, marqueur `SUNSHAFT137_PASS`.
|
||||
Comparatif autonome : `build/Shader-beta.137-comparaison.html`.
|
||||
|
||||
La première tentative s'est arrêtée sur une comparaison de gain brut entre
|
||||
feuillage et ciel ouvert. La composition dispose de moins de marge sur un mur
|
||||
déjà clair : le test compare maintenant la contribution normalisée sur les mêmes
|
||||
surfaces, et les feuilles du scénario sont persistantes. Aucun seuil du rendu
|
||||
n'a été assoupli pour faire passer ce contrôle.
|
||||
|
||||
Les contrôles natifs existants du bloom, des vingt textures de minerais émissifs
|
||||
et du lens flare réussissent également en **1 min 32 s**, avec leurs seuils
|
||||
inchangés. Log : `build/shader137-regression.log`, marqueurs `EMISSIVE131_PASS`
|
||||
et `LENS_FLARE136_PASS`. Les autres effets sont explicitement isolés dans les
|
||||
scénarios qui nécessitent une image témoin sans rayons.
|
||||
|
||||
## Limites
|
||||
|
||||
Le volume utilise les formes présentes dans la carte d'ombres existante :
|
||||
il n'ajoute pas de projection d'ombre des entités ou des nuages. Le feuillage
|
||||
reste une approximation par transmission déterministe. Portée maximale de
|
||||
64 blocs, atténuée à son extrémité. Le faible nombre de segments et la résolution
|
||||
réduite peuvent lisser les ouvertures très fines. Aucun résultat Windows/Vulkan
|
||||
ni mesure de FPS revendiqués.
|
||||
|
||||
|
||||
## Construction et archives
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
réussit en **2 min 27 s**, 126 tâches. Le serveur GameTest dédié reste exclu
|
||||
conformément au refus EULA antérieur. Log : `build/shader137-build.log`.
|
||||
Comparaison à beta.136 : **11 entrées de production** modifiées, uniquement
|
||||
le rendu, ses préférences et ses libellés. Gameplay et textures des gemmes
|
||||
strictement conservés. JAR normal/Test identique, sources et template vérifiés.
|
||||
Reçu local : `build/shader137-artifact.json`.
|
||||
|
||||
- `Sanctuary-beta.137.mrpack` : 10431723 octets ; SHA-256 `6684d1dbe48cd3031b373a03d4380519fdbf5d841e1c7cb5e290dec63b5ba9f6`.
|
||||
|
||||
- `Sanctuary-Test-beta.137.mrpack` : 10450650 octets ; SHA-256 `32b0cee60152b6a5c8f31b248391c79a5fdf47b649de74902b066457ad4785ef`.
|
||||
|
||||
JAR : `f1fbc106b4b048753f9a669af37e9c12f92168af486d4072f514e86920c81faa`.
|
||||
|
||||
## Livraison
|
||||
|
||||
[Release beta.137](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.137)
|
||||
publiée depuis `2d7072b8a8febac53354e9d0063246ac1e843e37`. Canal packwiz
|
||||
`68cb39a5edbf5d34def02f670e2488ee852605b8` ; téléchargements publics vérifiés par SHA-256.
|
||||
Deux synchronisations isolées puis deux dans la même instance **Sanctuary Beta**
|
||||
réussissent. Un seul JAR beta.137 actif ; les **923 fichiers personnels suivis**,
|
||||
dont sauvegardes et préférences, gardent leurs hashes. Copie préalable dans
|
||||
`sanctuary-backups/before-beta.137/`, aucun monde personnel ouvert.
|
||||
|
||||
Packs normal/Test/template et comparatif natif également vérifiés dans
|
||||
`sanctuary-beta/build/`. Le checkout original garde ses changements préexistants.
|
||||
Reçus locaux ignorés : `build/shader137-artifact.json`, `build/shader137-isolated.json`,
|
||||
`build/shader137-prism.json`, `build/shader137-publication.log`.
|
||||
@@ -0,0 +1,101 @@
|
||||
# SHADER-138 — Épaisseur des rayons et météo
|
||||
|
||||
Demande : épaissir les sunshafts avec la météo, surtout le brouillard.
|
||||
Socle beta.137, branche `codex/sunshaft-weather-beta138`, Minecraft 26.3 / Java 25.
|
||||
Aucune modification des règles météo, des sauvegardes ou du gameplay.
|
||||
|
||||
## Contrat
|
||||
|
||||
Densité et diffusion liées au brouillard environnemental réellement rendu,
|
||||
indépendamment de la limite de distance de rendu. Pluie/neige natives :
|
||||
renforcement plus modéré ; orage : réduction supplémentaire du soleil.
|
||||
Transitions continues, mêmes deux passes et seize segments. Le réglage
|
||||
manuel conserve sa valeur, aucune nouvelle option.
|
||||
|
||||
## Rendu
|
||||
|
||||
Le brouillard utilise `FogData.environmentalEnd`, séparé de la distance de
|
||||
rendu native. Sa contribution croît en douceur entre 192 et 80 blocs de
|
||||
visibilité. La densité passe de ×1 à ×3,5 pour le brouillard seul ; la diffusion
|
||||
angulaire s'élargit et la lumière lointaine s'éteint plus vite. Les coefficients
|
||||
ne dépendent d'aucun bruit animé ni historique d'image.
|
||||
|
||||
Pluie et neige utilisent le niveau natif de précipitation : densité jusqu'à
|
||||
×1,8, diffusion plus légère. La lumière disponible tombe progressivement à
|
||||
25 % sous une pluie maximale et à 8,75 % sous un orage maximal. La carte solaire
|
||||
reste disponible pour le volume même lorsque Minecraft cache entièrement
|
||||
son disque solaire ; les ombres sur les surfaces conservent leur comportement.
|
||||
Le brouillard et la météo continuent leurs transitions natives.
|
||||
|
||||
Même cible au quart de chaque dimension, deux passes, seize segments et
|
||||
quatre lectures de la carte par segment. L'empreinte de l'ombre ne change pas :
|
||||
la météo ne crée pas d'ouverture artificielle à travers un mur.
|
||||
|
||||
## Vérification
|
||||
|
||||
Scénario client natif réussi en **3 min 29 s**, macOS / Apple M1 / OpenGL.
|
||||
Les contrôles des volumes beta.137 passent à nouveau : ouvertures, feuilles,
|
||||
pièce fermée, mouvement/retour, proximité, soleil hors champ, intensité,
|
||||
OFF/zéro/nuit, redimensionnement, rechargement, préférences et libellés FR/EN.
|
||||
|
||||
La suite utilise ensuite les vraies commandes serveur de météo : clair,
|
||||
`weather fog`, pluie, orage et retour au clair, dans le même décor avec le
|
||||
curseur maintenu à 35. Transition progressive du brouillard, rechargement
|
||||
sous brouillard, absence d'éclairage d'une pièce fermée et restitution de
|
||||
l'image claire initiale vérifiés. La contribution normalisée du volume dans
|
||||
cette scène passe d'environ 0,0034 au clair à 0,0236 sous brouillard ; ce n'est
|
||||
pas un multiplicateur universel de luminosité. L'orage reste sous la pluie.
|
||||
|
||||
Log : `build/shader138-client-final.log`, marqueurs `SUNSHAFT137_PASS` et
|
||||
`SUNSHAFT138_WEATHER_PASS`. Comparatif natif autonome :
|
||||
`build/Shader-beta.138-comparaison.html`.
|
||||
|
||||
## Diagnostic de la pluie
|
||||
|
||||
La première exécution native a validé le brouillard, puis s'est arrêtée en
|
||||
attendant des rayons sous une pluie maximale. Minecraft 26.3 fournit alors
|
||||
`rainBrightness = 1 - rainLevel`, soit zéro : beta.137 ne construisait plus la
|
||||
carte solaire. Le volume reçoit désormais son atténuation météo propre et
|
||||
conserve la carte nécessaire, sans réactiver les ombres de surface sous la
|
||||
pluie maximale. Log de diagnostic : `build/shader138-client-first.log`.
|
||||
|
||||
## Limites
|
||||
|
||||
Coefficients artistiques bornés, sans simulation physique de gouttes ni de
|
||||
nuages. Le brouillard environnemental est distinct de la simple distance
|
||||
d'affichage ; la pluie et la neige partagent le niveau natif de précipitation.
|
||||
Les limites des volumes beta.137 restent applicables : 64 blocs maximum,
|
||||
formes de la carte d'ombres et feuillage approximé. Pas de mesure de FPS ni
|
||||
de validation Windows/Vulkan revendiquée.
|
||||
|
||||
|
||||
## Construction et archives
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
réussit en **2 min 20 s**, 126 tâches. Le serveur GameTest dédié reste exclu
|
||||
conformément au refus EULA antérieur. Log : `build/shader138-build.log`.
|
||||
Comparaison à beta.137 : **6 entrées de production** modifiées, uniquement
|
||||
le rendu et les aides FR/EN. Gameplay et textures des gemmes strictement
|
||||
conservés. JAR normal/Test identique, sources et template vérifiés.
|
||||
Reçu local : `build/shader138-artifact.json`.
|
||||
|
||||
- `Sanctuary-beta.138.mrpack` : 10432860 octets ; SHA-256 `67093d3a53e3d90c31e8fb940d709ccf41ad730595e358346fa27ad744b44460`.
|
||||
|
||||
- `Sanctuary-Test-beta.138.mrpack` : 10451787 octets ; SHA-256 `1ff39158b47f44a831b6506c8c2636441b2bff02644d20b1698330097e5ff059`.
|
||||
|
||||
JAR : `44b6dd14f0e962fcb6f1efd61e0e7bb2c8dbb033cef81598582ec16c46a46518`.
|
||||
|
||||
## Livraison
|
||||
|
||||
[Release beta.138](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.138)
|
||||
publiée depuis `daf36fdeacca707930092363589c89984af78f3b`. Canal packwiz
|
||||
`c049423de1971079b9d61874d83adeb6f1ad49f7` ; téléchargements publics vérifiés par SHA-256.
|
||||
Deux synchronisations isolées puis deux dans la même instance **Sanctuary Beta**
|
||||
réussissent. Un seul JAR beta.138 actif ; les **923 fichiers personnels suivis**,
|
||||
dont sauvegardes et préférences, gardent leurs hashes. Copie préalable dans
|
||||
`sanctuary-backups/before-beta.138/`, aucun monde personnel ouvert.
|
||||
|
||||
Packs normal/Test/template et comparatif natif également vérifiés dans
|
||||
`sanctuary-beta/build/`. Le checkout original garde ses changements préexistants.
|
||||
Reçus locaux ignorés : `build/shader138-artifact.json`, `build/shader138-isolated.json`,
|
||||
`build/shader138-prism.json`, `build/shader138-publication.log`.
|
||||
@@ -0,0 +1,99 @@
|
||||
# SHADER-139 — Compatibilité Vulkan du shader beta.138
|
||||
|
||||
Socle **beta.138**, Minecraft **26.3**, Java **25**, branche
|
||||
`codex/vulkan-shaders-beta139`.
|
||||
|
||||
Le signalement initial concernait un bloom invisible même à 100 %. Le joueur
|
||||
indique ensuite que le rendu fonctionne de nouveau sur Windows et demande
|
||||
la compatibilité Vulkan. Les changements expérimentaux d'intensité ont été
|
||||
retirés : les shaders et les valeurs par défaut restent ceux de beta.138.
|
||||
|
||||
## Contrat
|
||||
|
||||
Utiliser les mêmes pipelines RenderPearl sur OpenGL et Vulkan. Vérifier le
|
||||
backend réellement utilisé, car Minecraft peut retomber sur OpenGL lorsque
|
||||
Vulkan ne démarre pas. Tester les effets dans de nouveaux mondes ignorés,
|
||||
sans ouvrir de sauvegarde personnelle ni modifier le backend de l'instance Prism.
|
||||
|
||||
Le lancement des tests accepte désormais `-PsanctuaryClientGraphicsBackend=vulkan`
|
||||
ou `opengl`. Les tests de shaders refusent tout repli silencieux et consignent
|
||||
le nom du backend, du GPU et du pilote. Cette option concerne uniquement
|
||||
le client de développement.
|
||||
|
||||
## Reproduction
|
||||
|
||||
```sh
|
||||
JAVA_HOME=/chemin/vers/jdk-25 ./gradlew :sanctuary:runClientGameTest \
|
||||
-PsanctuaryClientTests=true -PsanctuaryPixelShadows122ClientTests=true \
|
||||
-PsanctuaryClientGraphicsBackend=vulkan \
|
||||
-PsanctuaryClientNoVsync=true -PsanctuaryQuickTests=true
|
||||
```
|
||||
|
||||
Sur Windows, utiliser `gradlew.bat` avec les mêmes propriétés et Java 25.
|
||||
Ajouter `-PsanctuaryClientImprovedTransparency=true` pour tester la transparence
|
||||
améliorée ; le choix initial est restauré après le test.
|
||||
Pour les rayons et la météo, remplacer `sanctuaryPixelShadows122ClientTests`
|
||||
par `sanctuarySunshaft137ClientTests`. Pour isoler le bloom et le lens flare,
|
||||
utiliser `sanctuaryBloom130ClientTests`.
|
||||
|
||||
## Diagnostic initial
|
||||
|
||||
Le client Vulkan démarre ici sur **Apple M1 / MoltenVK 1.4.2**, API 1.2.350.
|
||||
Le test du bloom réussit en **2 min 19 s** : les masques des vingt minerais,
|
||||
les contrôles OFF/zéro, intensité/diffusion, l'occlusion, le rechargement,
|
||||
les torches, glowstone, lampes, lave, soleil et le lens flare sont vérifiés.
|
||||
Les mesures de référence du bloom sont identiques à celles d'OpenGL :
|
||||
14649 pixels de minerai, gain du halo faible 218131, fort 1970502,
|
||||
gain solaire 7708. Log : `build/shader139-vulkan-first.log`.
|
||||
|
||||
Cela confirme cette combinaison précise ; ce n'est pas une validation des
|
||||
pilotes NVIDIA/AMD/Intel sous Windows. En cas de retour du problème, le
|
||||
`logs/latest.log` de la session Windows concernée est nécessaire pour relever
|
||||
le backend effectif, le GPU, le pilote et les éventuelles erreurs de pipeline.
|
||||
|
||||
## Suite Vulkan
|
||||
|
||||
`sanctuaryPixelShadows122ClientTests` réussit en **8 min 50 s**. Les contrôles
|
||||
couvrent ombres diurnes/nocturnes, feuillage, stabilité en temps réel,
|
||||
highlights CTM, bloom et lens flare, SSGI en déplacement et près des surfaces,
|
||||
PBR directionnel, rechargement, redimensionnement et interfaces FR/EN.
|
||||
Log : `build/shader139-vulkan-suite2.log`.
|
||||
|
||||
Le bloom et le lens flare réussissent aussi avec
|
||||
`sanctuaryClientImprovedTransparency=true`, en **1 min 45 s**. Le backend
|
||||
est bien Vulkan, sa profondeur est en `[0, 1]` et l'option de transparence
|
||||
est active. Les mesures du bloom restent identiques.
|
||||
Log : `build/shader139-vulkan-transparency.log`.
|
||||
|
||||
La suite météo/rayons beta.138 n'a pas été rejouée sous Vulkan dans ce ticket ;
|
||||
les résultats ci-dessus portent sur les effets explicitement listés.
|
||||
Le joueur confirme que le retour à OpenGL rétablit l'effet sur Windows.
|
||||
Ce défaut particulier reste à reproduire à partir d'un journal récent.
|
||||
|
||||
Un premier lancement utilisait un nom de propriété incomplet et a exécuté
|
||||
la suite générale, échouant dans Blocodex avant les tests de shaders.
|
||||
Ce lancement n'est pas compté comme une validation Vulkan des shaders.
|
||||
La commande corrigée utilise `sanctuaryPixelShadows122ClientTests`.
|
||||
|
||||
## Construction
|
||||
|
||||
`./gradlew check build -x :sanctuary:runGameTest` réussit ; le serveur
|
||||
GameTest dédié reste exclu conformément au refus EULA antérieur.
|
||||
Log : `build/shader139-check-build.log`. Le JAR reconstruit est **strictement
|
||||
identique, octet pour octet**, au JAR Sanctuary de la release beta.138.
|
||||
La distribution ne change pas et ne nécessite donc pas de nouvel assemblage.
|
||||
|
||||
## Distribution
|
||||
|
||||
Ce ticket ajoute des options de lancement de tests et leur documentation.
|
||||
Les shaders de production et les réglages de beta.138 ne sont pas changés.
|
||||
Aucune nouvelle livraison binaire, aucun tag beta.139 ni mise à jour de
|
||||
l'instance personnelle n'est effectué pour une vérification seule.
|
||||
|
||||
## Référence du moteur
|
||||
|
||||
Minecraft 26.3 compile les shaders OpenGL et Vulkan via ShaderC et expose
|
||||
`RENDERPEARL_DEPTH_IS_ZERO_TO_ONE` pour la convention de profondeur.
|
||||
[Notes officielles Minecraft 26.3](https://feedback.minecraft.net/hc/en-us/articles/48913133328013-Minecraft-Java-Edition-26-3).
|
||||
Les effets Sanctuary transmettent déjà la convention du périphérique aux
|
||||
reconstructions de position ; aucun remplacement spéculatif n'est appliqué.
|
||||
@@ -107,3 +107,9 @@ Les contrôles ne revendiquent pas un essai avec Iris ou un autre moteur externe
|
||||
Les ombres portées pixélisées sont livrées dans [beta.127](shader-beta127.md),
|
||||
intégrées sur la beta.126. Leur préparation locale portait un numéro beta.122
|
||||
non publié ; la release beta.122 existante n'est pas remplacée.
|
||||
|
||||
## beta.144 — Eau naturelle
|
||||
|
||||
Le [ticket WATER-144](natural-water-beta144.md) ajoute les profils clair,
|
||||
légèrement teinté et étang au rendu natif des fluides et à leur brouillard.
|
||||
La couleur du biome et les textures pixélisées restent la base.
|
||||
|
||||
+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.135
|
||||
pack_version=beta.135
|
||||
mod_version=beta.144
|
||||
pack_version=beta.144
|
||||
resource_pack_version=beta.121
|
||||
maven_group=fr.koka.sanctuary
|
||||
jei_version=30.32.0-sanctuary.3
|
||||
|
||||
@@ -25,6 +25,22 @@ 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.water144', providers.gradleProperty('sanctuaryWater144ClientTests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.water144ExtrasOnly', providers.gradleProperty('sanctuaryWater144ExtrasOnly').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.water144PerfOnly', providers.gradleProperty('sanctuaryWater144PerfOnly').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.pbr142', providers.gradleProperty('sanctuaryPbr142ClientTests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.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 +109,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') }
|
||||
@@ -103,6 +123,9 @@ tasks.named('processGametestResources') {
|
||||
inputs.property('sanctuarySsgiCloseup135ClientTests', closeup135Client)
|
||||
if (closeup135Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.shader.SsgiCloseup135ClientChecks') }
|
||||
|
||||
def sunshaft137Client = providers.gradleProperty('sanctuarySunshaft137ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuarySunshaft137ClientTests', sunshaft137Client)
|
||||
if (sunshaft137Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.shader.Sunshafts137ClientChecks') }
|
||||
def bloom130Client = providers.gradleProperty('sanctuaryBloom130ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryBloom130ClientTests', bloom130Client)
|
||||
if (bloom130Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.shader.Bloom130ClientChecks') }
|
||||
|
||||
+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);}
|
||||
|
||||
+2
-1
@@ -23,7 +23,7 @@ public final class Bloom130ClientChecks implements FabricClientGameTest {
|
||||
String ns=name.equals("ruby")||name.equals("sapphire")?"sanctuary:":"minecraft:";result.add(ns+name+"_ore");result.add(ns+"deepslate_"+name+"_ore");}
|
||||
result.add("minecraft:nether_quartz_ore");result.add("minecraft:nether_gold_ore");return List.copyOf(result);}
|
||||
static void run(ClientGameTestContext c,TestServerContext server){
|
||||
c.runOnClient(m->{preferences();VanillaLight.setPbr(false);VanillaLight.setSsgi(false);analyseTextures();VanillaLight.setSaturation(100);VanillaLight.setPixelShadows(false);VanillaLight.setEdgeHighlights(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);});
|
||||
c.runOnClient(m->{preferences();VanillaLight.setSunshafts(false);VanillaLight.setLensFlare(false);VanillaLight.setPbr(false);VanillaLight.setSsgi(false);analyseTextures();VanillaLight.setSaturation(100);VanillaLight.setPixelShadows(false);VanillaLight.setEdgeHighlights(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);});
|
||||
server.runOnServer(s->{command(s,"time set 18000");
|
||||
for(int x=4;x<=20;x++)for(int z=4;z<=20;z++){
|
||||
s.overworld().setBlockAndUpdate(new BlockPos(x,64,z),Blocks.CONCRETE.black().defaultBlockState());
|
||||
@@ -97,6 +97,7 @@ public final class Bloom130ClientChecks implements FabricClientGameTest {
|
||||
unchanged(coveredOff,capture(c,null),"Zero bloom leaves no residual glow");
|
||||
c.runOnClient(m->{VanillaLight.setBloomIntensity(35);VanillaLight.setBloom(false);VanillaLight.setOreEmission(true);VanillaLight.setOreIntensity(50);});
|
||||
System.out.println("EMISSIVE131_PASS independent cores with halo OFF/zero; blur resources released; defaults; masks for 20 ore textures; selective night glow; halo intensity/spread; no through-wall glow; live edits; torch/glowstone/lamp/lava/sun; resource reload; defaults/migration; orePixels="+orePixels+" oreGain="+oreGain+" haloLow="+low+" haloHigh="+high+" sunHalo="+sunlight);
|
||||
LensFlare136ClientChecks.run(c,server);
|
||||
}
|
||||
private static BlockPos orePos(int i){return new BlockPos(8+i%5,65,8+i/5);}
|
||||
private static void preferences(){try{
|
||||
|
||||
+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;}
|
||||
}
|
||||
+1
@@ -160,6 +160,7 @@ final class EdgeHighlights129ClientChecks {
|
||||
check(VanillaLight.edgeHighlights(),"Toggle persists independently");
|
||||
VanillaLight.setEdgeHighlights(false);
|
||||
VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);
|
||||
VanillaLight.setSunshafts(false);VanillaLight.setLensFlare(false);
|
||||
VanillaLight.setPbr(false);VanillaLight.setSsgi(false);
|
||||
} catch(java.io.IOException e) { throw new AssertionError(e); }
|
||||
}
|
||||
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import static fr.koka.sanctuary.client.shader.PixelShadows122ClientChecks.*;
|
||||
import java.nio.file.Files;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.TestServerContext;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.network.chat.Component;
|
||||
|
||||
/** Native sky image, not a generated mockup; explicit independent effect controls. */
|
||||
final class LensFlare136ClientChecks {
|
||||
static void run(ClientGameTestContext c,TestServerContext server){
|
||||
c.runOnClient(m->{preferences();VanillaLight.setEnabled(true);VanillaLight.setSaturation(100);VanillaLight.setPbr(false);VanillaLight.setSsgi(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);VanillaLight.setPixelShadows(false);VanillaLight.setEdgeHighlights(false);VanillaLight.setSunshafts(false);VanillaLight.setLensFlare(false);});
|
||||
server.runOnServer(s->{command(s,"time add 1");command(s,"time set 3000");});
|
||||
camera(c,server,100.5,130,100.5,70);aim(c,server,false);
|
||||
var off=capture(c,"flare-sun-off");enable(c,true);var on=capture(c,"flare-sun-30");
|
||||
long low=gain(off,on);check(low>1000,"Default sun flare is visible: "+low);
|
||||
c.runOnClient(m->check(!Bloom.haloAllocated()&&Bloom.flareAllocated(),"Flare runs without bloom blur targets"));
|
||||
unchanged(on,capture(c,null),"Static rectangular flare has no animated noise");
|
||||
c.runOnClient(m->VanillaLight.setLensFlareIntensity(80));frames(c);var high=capture(c,"flare-sun-80");
|
||||
check(gain(off,high)>low*1.5,"Flare intensity changes native pixels");
|
||||
var reload=c.computeOnClient(net.minecraft.client.Minecraft::reloadResourcePacks);c.waitFor(m->reload.isDone()&&m.gui.overlay()==null,1200);reload.join();frames(c);
|
||||
unchanged(high,capture(c,"flare-reloaded"),"Flare recovers after resource reload");
|
||||
int width=c.computeOnClient(m->m.getWindow().getScreenWidth()),height=c.computeOnClient(m->m.getWindow().getScreenHeight());
|
||||
c.runOnClient(m->m.getWindow().setWindowed(961,541));c.waitFor(m->m.gameRenderer.mainRenderTarget().width!=off.width(),200);frames(c);
|
||||
enable(c,false);var smallOff=capture(c,null);enable(c,true);check(gain(smallOff,capture(c,"flare-resized"))>1000,"Flare survives odd-sized targets");
|
||||
c.runOnClient(m->m.getWindow().setWindowed(width,height));c.waitFor(m->m.gameRenderer.mainRenderTarget().width==off.width(),200);frames(c);
|
||||
var direction=c.computeOnClient(m->PixelShadows.sunDirection(m.gameRenderer.gameRenderState().levelRenderState.skyRenderState.sunAngle));
|
||||
var eye=c.computeOnClient(m->m.player.getEyePosition());var center=BlockPos.containing(eye.x+direction.x*8,eye.y+direction.y*8,eye.z+direction.z*8);
|
||||
cover(server,center,true);c.waitTicks(35);frames(c);var hiddenOn=capture(c,"flare-sun-covered");enable(c,false);
|
||||
unchanged(hiddenOn,capture(c,null),"Occluded sun produces no rectangular flare through a wall");
|
||||
cover(server,center,false);c.waitTicks(35);aim(c,server,false);
|
||||
c.runOnClient(m->{VanillaLight.setBloom(true);VanillaLight.setBloomIntensity(20);VanillaLight.setLensFlareIntensity(30);});enable(c,true);capture(c,"flare-bloom-combined");
|
||||
c.runOnClient(m->VanillaLight.setBloom(false));look(c,server,0,80);enable(c,false);var away=capture(c,null);enable(c,true);unchanged(away,capture(c,"flare-away"),"Behind-camera celestial sources produce no reflected screen edge");
|
||||
server.runOnServer(s->s.overworld().setBlockAndUpdate(new BlockPos(100,129,104),Blocks.GLOWSTONE.defaultBlockState()));
|
||||
look(c,server,0,35);c.waitTicks(25);enable(c,false);var lampOff=capture(c,null);enable(c,true);unchanged(lampOff,capture(c,"flare-glowstone-excluded"),"Visible glowstone does not create a celestial flare");
|
||||
server.runOnServer(s->command(s,"time set 15000"));c.waitTicks(20);aim(c,server,true);
|
||||
enable(c,false);var moonOff=capture(c,"flare-moon-off");enable(c,true);long moon=gain(moonOff,capture(c,"flare-moon-30"));
|
||||
check(moon>200,"Native visible moon creates its own subtle flare: "+moon);
|
||||
server.runOnServer(s->command(s,"time set 111000"));c.waitFor(m->m.gameRenderer.gameRenderState().levelRenderState.skyRenderState.moonPhase.index()==4,200);aim(c,server,true);
|
||||
enable(c,false);var newOff=capture(c,null);enable(c,true);check(gain(newOff,capture(c,"flare-new-moon"))<moon*.1,"Dark new moon does not retain full moon flare");
|
||||
c.runOnClient(m->VanillaLight.setLensFlareIntensity(0));c.waitTicks(8);c.runOnClient(m->check(!Bloom.flareAllocated()&&!Bloom.allocated(),"Zero frees all resources with other effects OFF"));
|
||||
unchanged(newOff,capture(c,null),"Zero flare restores source image");
|
||||
c.runOnClient(m->{VanillaLight.setLensFlareIntensity(30);VanillaLight.setSunshafts(false);VanillaLight.setLensFlare(false);});
|
||||
for(String locale:new String[]{"fr_fr","en_us"})c.runOnClient(m->{
|
||||
m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());
|
||||
for(String suffix:new String[]{"lens_flare","lens_flare_help","lens_flare_intensity","lens_flare_intensity_help"}){
|
||||
String key="sanctuary.shaders."+suffix;check(!Component.translatable(key).getString().equals(key),"Localized flare control: "+locale+" "+key);
|
||||
}
|
||||
});
|
||||
System.out.println("LENS_FLARE136_PASS sunGain="+low+" moonGain="+moon+" visibility, resize, reload, strength, independent bloom, zero, preferences, FR/EN");
|
||||
}
|
||||
private static void cover(TestServerContext server,BlockPos center,boolean on){server.runOnServer(s->{for(int x=-4;x<=4;x++)for(int y=-4;y<=4;y++)for(int z=-4;z<=4;z++)s.overworld().setBlockAndUpdate(center.offset(x,y,z),on?Blocks.STONE.defaultBlockState():Blocks.AIR.defaultBlockState());});}
|
||||
private static void aim(ClientGameTestContext c,TestServerContext server,boolean moon){
|
||||
var direction=c.computeOnClient(m->PixelShadows.sunDirection(moon?m.gameRenderer.gameRenderState().levelRenderState.skyRenderState.moonAngle:m.gameRenderer.gameRenderState().levelRenderState.skyRenderState.sunAngle));
|
||||
look(c,server,net.minecraft.util.Mth.wrapDegrees((float)Math.toDegrees(Math.atan2(-direction.x,direction.z))+12),-(float)Math.toDegrees(Math.asin(direction.y))+8);
|
||||
}
|
||||
private static void frames(ClientGameTestContext c){int before=c.computeOnClient(m->Bloom.renderedFrames());c.waitFor(m->Bloom.applied()&&Bloom.ready()&&Bloom.renderedFrames()>before+10,1200);c.waitTicks(8);}
|
||||
private static void enable(ClientGameTestContext c,boolean value){c.runOnClient(m->VanillaLight.setLensFlare(value));if(value)frames(c);else c.waitTicks(8);}
|
||||
private static long gain(Frame off,Frame on){long sum=0;for(int i=0;i<off.pixels().length;i++)for(int shift:new int[]{0,8,16})sum+=Math.max(0,(on.pixels()[i]>>shift&255)-(off.pixels()[i]>>shift&255));return sum;}
|
||||
private static void preferences(){try{
|
||||
var path=FabricLoader.getInstance().getConfigDir().resolve("sanctuary-shaders.json");Files.writeString(path,"{\"bloom\":false,\"bloomIntensity\":20}");forgetPreferences();
|
||||
check(VanillaLight.lensFlare()&&VanillaLight.lensFlareIntensity()==30&&!VanillaLight.bloom()&&VanillaLight.bloomIntensity()==20,"Flare defaults preserve bloom choices");
|
||||
VanillaLight.setSunshafts(false);VanillaLight.setLensFlare(false);VanillaLight.setLensFlareIntensity(73);forgetPreferences();check(!VanillaLight.lensFlare()&&VanillaLight.lensFlareIntensity()==73,"Saved custom flare retained");
|
||||
for(int n:new int[]{-10,0,30,100,150}){VanillaLight.setLensFlareIntensity(n);forgetPreferences();check(VanillaLight.lensFlareIntensity()==Math.clamp(n,0,100),"Flare intensity persists and clamps");}
|
||||
VanillaLight.setLensFlareIntensity(30);
|
||||
}catch(java.io.IOException e){throw new AssertionError(e);}}
|
||||
}
|
||||
+2
-2
@@ -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();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());
|
||||
@@ -61,7 +61,7 @@ public final class Pbr134ClientChecks implements FabricClientGameTest {
|
||||
}
|
||||
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");}
|
||||
VanillaLight.setPbrIntensity(50);
|
||||
}catch(Exception e){throw new AssertionError(e);}}
|
||||
|
||||
+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);}
|
||||
}
|
||||
+28
-7
@@ -36,12 +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;
|
||||
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=bloomOnly;this.ssgiOnly=ssgiOnly;this.pbrOnly=pbrOnly;this.closeupOnly=closeupOnly;}
|
||||
PixelShadows122ClientChecks(boolean bloomOnly,boolean ssgiOnly,boolean pbrOnly,boolean closeupOnly){this(bloomOnly,ssgiOnly,pbrOnly,closeupOnly,false);}
|
||||
PixelShadows122ClientChecks(boolean bloomOnly,boolean ssgiOnly,boolean pbrOnly,boolean closeupOnly,boolean sunshaftOnly){this(bloomOnly,ssgiOnly,pbrOnly,closeupOnly,sunshaftOnly,false);}
|
||||
PixelShadows122ClientChecks(boolean bloomOnly,boolean ssgiOnly,boolean pbrOnly,boolean closeupOnly,boolean sunshaftOnly,boolean coloredOnly){this.coloredOnly=coloredOnly;this.sunshaftOnly=sunshaftOnly;this.bloomOnly=bloomOnly;this.ssgiOnly=ssgiOnly;this.pbrOnly=pbrOnly;this.closeupOnly=closeupOnly;}
|
||||
private static final int FLOOR = 64;
|
||||
private static final AABB PILLAR = new AABB(0, FLOOR + 1, 0, 2, FLOOR + 9, 2);
|
||||
private static final BlockPos SLAB = new BlockPos(5, FLOOR + 3, -5);
|
||||
@@ -50,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 {
|
||||
@@ -81,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 -> {
|
||||
@@ -136,6 +151,9 @@ 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(pbrOnly){Pbr134ClientChecks.run(c,server);return;}
|
||||
if(ssgiOnly){Ssgi132ClientChecks.run(c,server);return;}
|
||||
@@ -266,7 +284,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
||||
});
|
||||
});
|
||||
c.waitTicks(3);
|
||||
c.takeScreenshot("sanctuary-beta135-shader-options-" + locale);
|
||||
c.takeScreenshot("sanctuary-beta142-shader-options-" + locale);
|
||||
}
|
||||
c.runOnClient(m -> m.gui.setScreen(null));
|
||||
}
|
||||
@@ -435,17 +453,19 @@ 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.ssgiIntensity()==35,"PBR default 80, SSGI strength retained");
|
||||
Files.writeString(path,"{\"pbr\":false,\"ssgi\":false,\"bloomIntensity\":73}");forgetPreferences();
|
||||
check(!VanillaLight.pbr()&&!VanillaLight.ssgi()&&VanillaLight.bloomIntensity()==73,"Explicit personal toggles and intensity survive changed defaults");
|
||||
Files.writeString(path,"{\"bloom\":false}");forgetPreferences();
|
||||
check(!VanillaLight.bloom()&&VanillaLight.bloomIntensity()==20&&VanillaLight.pbr()&&VanillaLight.ssgi(),"Missing fields get new defaults without replacing explicit values");
|
||||
Files.writeString(path,"{\"pbrIntensity\":50}");forgetPreferences();check(VanillaLight.pbrIntensity()==50,"Explicit PBR 50 is preserved");
|
||||
System.out.println("SHADER135_DEFAULTS_PASS fresh, partial and personal preferences");
|
||||
} catch(IOException e){throw new AssertionError(e);}
|
||||
}
|
||||
|
||||
private static void checkPreferences() {
|
||||
checkDefaults135();
|
||||
VanillaLight.setSunshafts(false);VanillaLight.setLensFlare(false);
|
||||
VanillaLight.setPbr(false);
|
||||
VanillaLight.setSsgi(false);
|
||||
VanillaLight.setBloom(false);
|
||||
@@ -549,7 +569,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
||||
}
|
||||
|
||||
static Frame capture(ClientGameTestContext c, String name) {
|
||||
if (name != null) c.takeScreenshot("sanctuary-beta135-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) {
|
||||
@@ -650,11 +670,11 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
||||
}
|
||||
|
||||
private record Options(int distance, int simulationDistance, int fov, boolean bob,
|
||||
boolean pause, CameraType camera, String language) {
|
||||
boolean pause, CameraType camera, String language, boolean improvedTransparency) {
|
||||
static Options capture(Minecraft m) {
|
||||
return new Options(m.options.renderDistance().get(), m.options.simulationDistance().get(), m.options.fov().get(),
|
||||
m.options.bobView().get(), m.options.pauseOnLostFocus,
|
||||
m.options.getCameraType(), m.getLanguageManager().getSelected());
|
||||
m.options.getCameraType(), m.getLanguageManager().getSelected(), m.options.improvedTransparency().get());
|
||||
}
|
||||
|
||||
void restore(Minecraft m) {
|
||||
@@ -664,6 +684,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
||||
m.options.bobView().set(bob);
|
||||
m.options.pauseOnLostFocus = pause;
|
||||
m.options.setCameraType(camera);
|
||||
m.options.improvedTransparency().set(improvedTransparency);
|
||||
m.getLanguageManager().setSelected(language);
|
||||
m.getLanguageManager().onResourceManagerReload(m.getResourceManager());
|
||||
}
|
||||
|
||||
+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.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);
|
||||
}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import static fr.koka.sanctuary.client.shader.PixelShadows122ClientChecks.*;
|
||||
import java.nio.file.Files;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.TestServerContext;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.util.Mth;
|
||||
|
||||
/** Native solar volume in a sealed room, a slotted roof and a leaf canopy. */
|
||||
public final class Sunshafts137ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c){new PixelShadows122ClientChecks(false,false,false,false,true).runTest(c);}
|
||||
static void run(ClientGameTestContext c,TestServerContext server){
|
||||
c.runOnClient(m->{preferences();VanillaLight.setEnabled(true);VanillaLight.setSaturation(100);VanillaLight.setPbr(false);VanillaLight.setSsgi(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);VanillaLight.setLensFlare(false);VanillaLight.setEdgeHighlights(false);VanillaLight.setPixelShadows(false);VanillaLight.setSunshafts(false);VanillaLight.setShadowDistance(64);});
|
||||
server.runOnServer(s->{command(s,"time add 1");command(s,"time set 3000");
|
||||
for(int x=76;x<=124;x++)for(int z=76;z<=124;z++){
|
||||
s.overworld().setBlockAndUpdate(new BlockPos(x,96,z),Blocks.STONE_BRICKS.defaultBlockState());
|
||||
if(x==76||x==124||z==76||z==124)for(int y=97;y<=113;y++)s.overworld().setBlockAndUpdate(new BlockPos(x,y,z),Blocks.STONE_BRICKS.defaultBlockState());
|
||||
}
|
||||
});
|
||||
roof(server,0);camera(c,server,100.5,101,110.5,70);aim(c,server);c.waitTicks(50);
|
||||
var closedOff=capture(c,"shafts-closed-off");enable(c,true);var closed=capture(c,"shafts-closed-on");
|
||||
check(gain(closedOff,closed)<300,"Sealed room receives no sunlight through opaque blocks: "+gain(closedOff,closed));
|
||||
c.runOnClient(m->check(!PixelShadows.applied()&&PixelShadows.depthReady()&&!Bloom.allocated(),"Volume alone shares depth without surface shadows or bloom"));
|
||||
roof(server,1);settle(c);enable(c,false);var off=capture(c,"shafts-slots-off");enable(c,true);var on=capture(c,"shafts-slots-35");long low=gain(off,on);
|
||||
check(low>1000,"Roof openings form visible shafts: "+low);unchanged(on,capture(c,null),"Static shafts have no animated sampling noise");
|
||||
c.runOnClient(m->VanillaLight.setSunshaftIntensity(100));settle(c);var strong=capture(c,"shafts-slots-100");check(gain(off,strong)>low*1.5,"Intensity increases volume energy");
|
||||
camera(c,server,104.5,101,110.5,70);aim(c,server);settle(c);capture(c,"shafts-moved-four-blocks");
|
||||
camera(c,server,100.5,101,110.5,70);aim(c,server);settle(c);unchanged(strong,capture(c,"shafts-returned"),"Moving and returning leaves no history or trailing light");
|
||||
var reload=c.computeOnClient(net.minecraft.client.Minecraft::reloadResourcePacks);c.waitFor(m->reload.isDone()&&m.gui.overlay()==null,1200);reload.join();settle(c);unchanged(strong,capture(c,"shafts-reloaded"),"Volume recovers after resource reload");
|
||||
int w=c.computeOnClient(m->m.getWindow().getScreenWidth()),h=c.computeOnClient(m->m.getWindow().getScreenHeight());
|
||||
c.runOnClient(m->m.getWindow().setWindowed(961,541));c.waitFor(m->m.gameRenderer.mainRenderTarget().width!=off.width(),200);settle(c);
|
||||
enable(c,false);var small=capture(c,null);enable(c,true);check(gain(small,capture(c,"shafts-resized"))>1000,"Bilateral volume survives odd dimensions");
|
||||
c.runOnClient(m->m.getWindow().setWindowed(w,h));c.waitFor(m->m.gameRenderer.mainRenderTarget().width==off.width(),200);settle(c);
|
||||
roof(server,2);settle(c);enable(c,false);var leafOff=capture(c,null);enable(c,true);var leafOn=capture(c,"shafts-leaves");long leaf=gain(leafOff,leafOn);double leafEnergy=energy(leafOff,leafOn);check(leaf>1000,"Leaf canopy transmits light: "+leaf);
|
||||
roof(server,3);settle(c);enable(c,false);var openOff=capture(c,null);enable(c,true);var openOn=capture(c,"shafts-open");long open=gain(openOff,openOn);double openEnergy=energy(openOff,openOn);check(openEnergy>leafEnergy,"Leaf canopy attenuates normalized scattering on identical walls: "+leafEnergy+" / "+openEnergy);
|
||||
look(c,server,0,0);settle(c);enable(c,false);var sideOff=capture(c,null);enable(c,true);check(gain(sideOff,capture(c,"shafts-sun-offscreen"))>1000,"Volume persists with sun outside the viewport");
|
||||
long[] close=new long[2];int index=0;
|
||||
for(double z:new double[]{123.5,123.875}){camera(c,server,100.5,101,z,70);look(c,server,0,0);settle(c);enable(c,false);var nearOff=capture(c,null);enable(c,true);close[index++]=gain(nearOff,capture(c,"shafts-wall-"+z));}
|
||||
check(close[1]<=close[0]+100,"Approaching wall attenuates scattering instead of increasing it: "+close[0]+" / "+close[1]);
|
||||
c.runOnClient(m->VanillaLight.setSunshaftIntensity(0));c.waitTicks(10);c.runOnClient(m->check(!Sunshafts.allocated()&&PixelShadows.shadowMap()==null,"Zero frees volume and unneeded shared map"));
|
||||
c.runOnClient(m->VanillaLight.setSunshaftIntensity(35));enable(c,true);
|
||||
server.runOnServer(s->command(s,"time set 18000"));c.waitTicks(30);c.runOnClient(m->check(!Sunshafts.allocated()&&!Sunshafts.applied(),"Solar volume stops at night"));
|
||||
server.runOnServer(s->command(s,"time set 3000"));camera(c,server,100.5,101,110.5,70);aim(c,server);settle(c);
|
||||
c.runOnClient(m->{VanillaLight.setPixelShadows(true);VanillaLight.setPbr(true);VanillaLight.setSsgi(true);VanillaLight.setBloom(true);VanillaLight.setLensFlare(true);VanillaLight.setEdgeHighlights(true);});activeFrames(c);capture(c,"shafts-combined");
|
||||
c.runOnClient(m->{VanillaLight.setSunshafts(false);});c.waitTicks(10);c.runOnClient(m->check(!Sunshafts.allocated()&&PixelShadows.applied(),"OFF preserves independently enabled surface shadows"));
|
||||
for(String locale:new String[]{"fr_fr","en_us"})c.runOnClient(m->{m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());for(String suffix:new String[]{"sunshafts","sunshafts_help","sunshaft_intensity","sunshaft_intensity_help"}){String key="sanctuary.shaders."+suffix;check(!Component.translatable(key).getString().equals(key),"Localized volume controls: "+locale);}});
|
||||
System.out.println("SUNSHAFT137_PASS slotGain="+low+" leafGain="+leaf+" openGain="+open+" closed room, offscreen sun, camera return, proximity, reload/resize, shared map, zero/night, preferences, FR/EN");
|
||||
SunshaftWeather138ClientChecks.run(c,server);
|
||||
}
|
||||
static void roof(TestServerContext server,int mode){server.runOnServer(s->{for(int x=76;x<=124;x++)for(int z=76;z<=124;z++)s.overworld().setBlockAndUpdate(new BlockPos(x,113,z),(mode==3||mode==1&&Math.floorMod(z,8)<3?Blocks.AIR:mode==2?Blocks.OAK_LEAVES:Blocks.OAK_PLANKS).defaultBlockState().trySetValue(net.minecraft.world.level.block.state.properties.BlockStateProperties.PERSISTENT,true));});}
|
||||
static void aim(ClientGameTestContext c,TestServerContext server){var d=c.computeOnClient(m->PixelShadows.sunDirection(m.gameRenderer.gameRenderState().levelRenderState.skyRenderState.sunAngle));look(c,server,Mth.wrapDegrees((float)Math.toDegrees(Math.atan2(-d.x,d.z))+25),-12);}
|
||||
static void settle(ClientGameTestContext c){int before=c.computeOnClient(m->Sunshafts.renderedFrames());c.waitFor(m->Sunshafts.applied()&&PixelShadows.ready()&&Sunshafts.renderedFrames()>before+10,1200);c.waitTicks(30);}
|
||||
static void enable(ClientGameTestContext c,boolean enabled){c.runOnClient(m->VanillaLight.setSunshafts(enabled));if(enabled)settle(c);else c.waitTicks(10);}
|
||||
private static long gain(Frame off,Frame on){long sum=0;for(int i=0;i<off.pixels().length;i++)for(int shift:new int[]{0,8,16})sum+=Math.max(0,(on.pixels()[i]>>shift&255)-(off.pixels()[i]>>shift&255));return sum;}
|
||||
// Screen blending has less headroom on a brighter vanilla wall. Remove that factor
|
||||
// before comparing roofs, and exclude the roof itself where geometry changes.
|
||||
static double energy(Frame off,Frame on){double sum=0;int n=0;for(int y=off.height()/3;y<off.height();y++)for(int x=0;x<off.width();x++){int i=y*off.width()+x;for(int shift:new int[]{0,8,16}){int a=off.pixels()[i]>>shift&255,b=on.pixels()[i]>>shift&255;if(a<240){sum+=Math.max(0,b-a)/(255.0-a);n++;}}}return sum/Math.max(1,n);}
|
||||
private static void preferences(){try{var p=FabricLoader.getInstance().getConfigDir().resolve("sanctuary-shaders.json");Files.writeString(p,"{\"bloom\":false,\"pixelShadows\":false}");forgetPreferences();check(VanillaLight.sunshafts()&&VanillaLight.sunshaftIntensity()==35&&!VanillaLight.bloom()&&!VanillaLight.pixelShadows(),"Missing shaft settings preserve existing choices");VanillaLight.setSunshafts(false);VanillaLight.setSunshaftIntensity(73);forgetPreferences();check(!VanillaLight.sunshafts()&&VanillaLight.sunshaftIntensity()==73,"Custom shafts persist");for(int n:new int[]{-10,0,35,100,150}){VanillaLight.setSunshaftIntensity(n);forgetPreferences();check(VanillaLight.sunshaftIntensity()==Math.clamp(n,0,100),"Intensity clamps and persists");}VanillaLight.setSunshaftIntensity(35);}catch(java.io.IOException e){throw new AssertionError(e);}}
|
||||
}
|
||||
+215
@@ -0,0 +1,215 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
import static fr.koka.sanctuary.client.shader.PixelShadows122ClientChecks.*;
|
||||
import fr.koka.sanctuary.gametest.Water144Metrics;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.*;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
|
||||
final class Water144ClientChecks {
|
||||
static void run(ClientGameTestContext c,TestServerContext server){
|
||||
c.runOnClient(m->{
|
||||
VanillaLight.setEnabled(true);VanillaLight.setNaturalWater(true);VanillaLight.setWaterIntensity(100);
|
||||
VanillaLight.setPbr(false);VanillaLight.setSsgi(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);
|
||||
VanillaLight.setPixelShadows(false);VanillaLight.setColoredLights(false);VanillaLight.setSunshafts(false);VanillaLight.setLensFlare(false);VanillaLight.setEdgeHighlights(false);
|
||||
});
|
||||
server.runOnServer(s->{
|
||||
command(s,"fill 47 94 -1 96 95 33 quartz_block");
|
||||
command(s,"fill 47 96 -1 47 100 33 glass");command(s,"fill 96 96 -1 96 100 33 glass");
|
||||
command(s,"fill 47 96 -1 96 100 -1 glass");command(s,"fill 47 96 33 96 100 33 glass");
|
||||
command(s,"fill 64 95 0 79 95 32 sand");command(s,"fill 80 95 0 95 95 32 mud");
|
||||
command(s,"fill 48 96 0 95 99 32 water");
|
||||
command(s,"setblock 84 96 8 seagrass");
|
||||
for(int x=50;x<95;x+=4)command(s,"setblock "+x+" 96 20 gold_block");
|
||||
});
|
||||
camera(c,server,72,125,16,70);c.waitTicks(50);settle(c);
|
||||
if(Boolean.getBoolean("sanctuary.test.water144ExtrasOnly")){
|
||||
resourceOverride(c);
|
||||
camera(c,server,88.5,96.2,8.5,70);look(c,server,0,0);c.waitTicks(20);capture(c,"pond-final-fog");
|
||||
var green=c.computeOnClient(m->new org.joml.Vector4f(m.gameRenderer.gameRenderState().levelRenderState.cameraRenderState.fogData.color));
|
||||
enable(c,false);var blue=c.computeOnClient(m->new org.joml.Vector4f(m.gameRenderer.gameRenderState().levelRenderState.cameraRenderState.fogData.color));capture(c,"native-final-fog");
|
||||
check(green.y>green.z&&green.y<Math.max(blue.x,Math.max(blue.y,blue.z))*.8,"Olive fog stays muted after native vision adaptation");
|
||||
enable(c,true);extras(c,server);return;
|
||||
}
|
||||
if(!Boolean.getBoolean("sanctuary.test.water144PerfOnly")){
|
||||
c.runOnClient(m->{
|
||||
check(NaturalWater.sample(m.level,56.5,99.1,8.5).equals(NaturalWater.CLEAR),"Constructed bed clear");
|
||||
check(NaturalWater.sample(m.level,72.5,99.1,8.5).equals(NaturalWater.LIGHT),"Sand lightly tinted");
|
||||
check(NaturalWater.sample(m.level,88.5,99.1,8.5).equals(NaturalWater.POND),"Mud pond");
|
||||
check(NaturalWater.sample(m.level,84.5,99.1,8.5).equals(NaturalWater.POND),"Seagrass is not a floor");
|
||||
check(NaturalWater.sample(m.level,88.5,110.1,8.5).equals(NaturalWater.CLEAR),"No bed in bounded search uses clear");
|
||||
var unloaded=new BlockPos(1_000_016,96,1_000_016);
|
||||
check(!NaturalWater.loaded(m.level,unloaded.getX()>>4,unloaded.getZ()>>4),"Fixture region is unloaded");
|
||||
var missing=new NaturalWater.Region(m.level,62500,5,62500);
|
||||
check(missing.corner(null,unloaded,0,0).equals(NaturalWater.CLEAR),"Unavailable biome snapshot forces clear without reading missing terrain");
|
||||
check(!NaturalWater.loaded(m.level,unloaded.getX()>>4,unloaded.getZ()>>4),"Classification does not load chunks");
|
||||
var boundary=NaturalWater.sample(m.level,80,99.1,8.5);check(boundary.tint()>.1&&boundary.tint()<.85,"Section boundary mixes profiles");
|
||||
});
|
||||
enable(c,false);var off=capture(c,"surface-off");enable(c,true);var on=capture(c,"surface-on");
|
||||
check(difference(off,on)>100000,"Visible surface colour and opacity change");
|
||||
c.runOnClient(m->VanillaLight.setWaterIntensity(0));settle(c);var zero=capture(c,"surface-zero");
|
||||
check(difference(off,zero)<difference(off,on)/4,"Zero restores native water (animated texture tolerance)");
|
||||
c.runOnClient(m->VanillaLight.setWaterIntensity(100));settle(c);
|
||||
camera(c,server,88.5,103,4.5,70);look(c,server,0,28);capture(c,"pond-closeup");
|
||||
camera(c,server,72,125,16,70);
|
||||
server.runOnServer(s->command(s,"fill 80 95 0 95 95 32 quartz_block"));c.waitTicks(30);settle(c);var changed=capture(c,"bed-changed");
|
||||
check(difference(changed,on)>100000,"Changing bed four blocks below surface refreshes water");
|
||||
server.runOnServer(s->command(s,"fill 80 95 0 95 95 32 mud"));c.waitTicks(25);settle(c);
|
||||
server.runOnServer(s->command(s,"fill 80 103 0 95 103 12 stone"));c.waitTicks(25);settle(c);
|
||||
c.runOnClient(m->check(NaturalWater.sample(m.level,88.5,99.1,8.5).equals(NaturalWater.POND),"Roof does not replace floor rule"));capture(c,"covered-pond");
|
||||
server.runOnServer(s->command(s,"fill 80 103 0 95 103 12 air"));c.waitTicks(20);
|
||||
for(int x:new int[]{56,72,88}){
|
||||
camera(c,server,x+.5,96.2,8.5,70);look(c,server,0,0);c.waitTicks(20);capture(c,"immersed-"+x);
|
||||
c.runOnClient(m->{check(m.gameRenderer.mainCamera().getFluidInCamera()==net.minecraft.world.level.material.FogType.WATER,"Camera immersed");
|
||||
float expected=x==56?1:x==72?.7f:.4f;check(Math.abs(NaturalWater.fogProfile().fog()-expected)<.001,"Immersion uses corresponding profile");});
|
||||
}
|
||||
var pondColor=c.computeOnClient(m->new org.joml.Vector4f(m.gameRenderer.gameRenderState().levelRenderState.cameraRenderState.fogData.color));
|
||||
check(pondColor.y>pondColor.z&&pondColor.y<.8f,"Olive fog avoids native neon normalization");
|
||||
float pondFog=c.computeOnClient(m->m.gameRenderer.gameRenderState().levelRenderState.cameraRenderState.fogData.environmentalEnd);
|
||||
enable(c,false);float nativeFog=c.computeOnClient(m->m.gameRenderer.gameRenderState().levelRenderState.cameraRenderState.fogData.environmentalEnd);
|
||||
check(Math.abs(pondFog/nativeFog-.4)<.01,"Rendered underwater fog distance matches pond profile");capture(c,"immersed-native");enable(c,true);
|
||||
camera(c,server,72,125,16,70);
|
||||
server.runOnServer(s->command(s,"fillbiome 80 88 0 95 104 32 minecraft:ocean"));c.waitTicks(35);settle(c);
|
||||
c.runOnClient(m->{
|
||||
check(NaturalWater.sample(m.level,88.5,99.1,8.5).equals(NaturalWater.CLEAR),"Ocean overrides muddy bed");
|
||||
boolean found=false;
|
||||
for(int x=72;x<89;x++){
|
||||
var p=new BlockPos(x,99,8);
|
||||
if(!NaturalWater.ocean(m.level,p)&&NaturalWater.ocean(m.level,p.east())){
|
||||
var left=new NaturalWater.Region(m.level,(x>>4)-1,5,-1);
|
||||
var right=new NaturalWater.Region(m.level,((x+1)>>4)-1,5,-1);
|
||||
check(left.corner(null,p,1,0).equals(right.corner(null,p.east(),0,0)),"Ocean edge emits identical shared vertices");
|
||||
found=true;break;
|
||||
}
|
||||
}
|
||||
check(found,"Native ocean boundary exercised");
|
||||
});capture(c,"ocean-over-mud");
|
||||
server.runOnServer(s->command(s,"fillbiome 80 88 0 95 104 32 minecraft:plains"));c.waitTicks(30);
|
||||
var reload=c.computeOnClient(net.minecraft.client.Minecraft::reloadResourcePacks);c.waitFor(m->reload.isDone()&&m.gui.overlay()==null,1200);reload.join();settle(c);capture(c,"resources-reloaded");
|
||||
resourceOverride(c);
|
||||
camera(c,server,400,120,16,70);c.waitTicks(30);camera(c,server,72,125,16,70);settle(c);
|
||||
c.runOnClient(m->{check(NaturalWater.sample(m.level,88.5,99.1,8.5).equals(NaturalWater.POND),"Reloaded chunks keep correct profile");
|
||||
VanillaLight.setNaturalWater(false);VanillaLight.setWaterIntensity(37);forgetPreferences();check(!VanillaLight.naturalWater()&&VanillaLight.waterIntensity()==37,"Water preferences persist");
|
||||
VanillaLight.setNaturalWater(true);VanillaLight.setWaterIntensity(100);
|
||||
for(String locale:new String[]{"fr_fr","en_us"}){m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());
|
||||
for(String key:new String[]{"natural_water","natural_water_help","water_intensity","water_intensity_help","water_intensity_value"}){String full="sanctuary.shaders."+key;check(!Component.translatable(full,100).getString().equals(full),"FR/EN "+full);}}
|
||||
});settle(c);
|
||||
}
|
||||
// All effects enabled; fixed inherited test quality (shadows 32 blocks, PBR 50%) for A/B comparison.
|
||||
c.runOnClient(m->{VanillaLight.setPbr(true);VanillaLight.setSsgi(true);VanillaLight.setBloom(true);VanillaLight.setOreEmission(true);
|
||||
VanillaLight.setPixelShadows(true);VanillaLight.setColoredLights(true);VanillaLight.setSunshafts(true);VanillaLight.setLensFlare(true);VanillaLight.setEdgeHighlights(true);});
|
||||
c.waitTicks(80);capture(c,"combined");
|
||||
benchmark(c,server);
|
||||
extras(c,server);
|
||||
System.out.println("WATER144_PASS surface, immersion, beds, ocean, section seam, roof, plants, invalidation, reload, preferences, FR/EN and combined rendering");
|
||||
}
|
||||
private static void enable(ClientGameTestContext c,boolean value){c.runOnClient(m->VanillaLight.setNaturalWater(value));settle(c);}
|
||||
private static void settle(ClientGameTestContext c){c.waitTicks(8);c.waitFor(m->m.levelRenderer.hasRenderedAllSections(),1200);c.waitTicks(20);}
|
||||
private static long difference(Frame a,Frame b){long value=0;for(int i=0;i<a.pixels().length;i++)value+=channelDifference(a.pixels()[i],b.pixels()[i]);return value;}
|
||||
private static void extras(ClientGameTestContext c,TestServerContext server){
|
||||
server.runOnServer(s->{
|
||||
for(int x:new int[]{100,108}){
|
||||
command(s,"fill "+x+" 95 0 "+(x+4)+" 104 16 glass");
|
||||
command(s,"fill "+(x+1)+" 96 1 "+(x+3)+" 103 15 air");
|
||||
command(s,"fill "+(x+1)+" 95 1 "+(x+3)+" 95 15 mud");
|
||||
}
|
||||
command(s,"setblock 102 102 2 water");command(s,"setblock 110 102 2 lava");
|
||||
command(s,"setblock 56 99 8 oak_slab[waterlogged=true]");
|
||||
});
|
||||
c.waitTicks(80);camera(c,server,106,103,25,70);look(c,server,179,20);settle(c);
|
||||
c.runOnClient(m->{
|
||||
var flow=m.level.getFluidState(new BlockPos(102,101,2));check(flow.is(net.minecraft.tags.FluidTags.WATER)&&!flow.isSource(),"Real falling water is rendered");
|
||||
check(m.level.getFluidState(new BlockPos(110,101,2)).is(net.minecraft.tags.FluidTags.LAVA),"Lava cascade remains lava");
|
||||
check(m.level.getBlockState(new BlockPos(56,99,8)).getValue(net.minecraft.world.level.block.state.properties.BlockStateProperties.WATERLOGGED),"Waterlogged slab preserved");
|
||||
});
|
||||
enable(c,false);var off=capture(c,"cascades-off");enable(c,true);var on=capture(c,"cascades-on");
|
||||
check(difference(off,on)>1000,"Waterfall responds to water option");
|
||||
int lava=0,stable=0;for(int i=0;i<off.pixels().length;i++){int a=off.pixels()[i];int r=a>>16&255,g=a>>8&255,b=a&255;
|
||||
if(r>160&&r>g*1.3&&b<90){lava++;if(channelDifference(a,on.pixels()[i])<70)stable++;}}
|
||||
check(lava>100&&stable>lava*.8,"Lava colours remain native (animation tolerance)");
|
||||
camera(c,server,56.5,102,8.5,70);capture(c,"waterlogged-slab");
|
||||
System.out.println("WATER144_EXTRAS_PASS falling water, unchanged lava and waterlogged slab");
|
||||
}
|
||||
private static net.minecraft.client.renderer.texture.SpriteContents waterSprite(net.minecraft.client.Minecraft m,String name){
|
||||
return m.getAtlasManager().getAtlasOrThrow(net.minecraft.data.AtlasIds.BLOCKS)
|
||||
.getSprite(net.minecraft.resources.Identifier.withDefaultNamespace("block/"+name)).contents();
|
||||
}
|
||||
private static void resourceOverride(ClientGameTestContext c){
|
||||
int originalWidth=c.computeOnClient(m->waterSprite(m,"water_still").width());
|
||||
int originalFrames=c.computeOnClient(m->waterSprite(m,"water_still").getUniqueFrames().size());
|
||||
var directory=c.computeOnClient(m->m.getResourcePackDirectory().resolve("water144-fixture"));
|
||||
try{
|
||||
Files.createDirectories(directory.resolve("assets/sanctuary"));
|
||||
var mod=net.fabricmc.loader.api.FabricLoader.getInstance().getModContainer("sanctuary").orElseThrow();
|
||||
Files.copy(mod.findPath("resourcepacks/template/pack.mcmeta").orElseThrow(),directory.resolve("pack.mcmeta"),StandardCopyOption.REPLACE_EXISTING);
|
||||
var textures=directory.resolve("assets/minecraft/textures/block");Files.createDirectories(textures);
|
||||
try(var pixels=new com.mojang.blaze3d.platform.NativeImage(32,64,false)){
|
||||
for(int y=0;y<64;y++)for(int x=0;x<32;x++)pixels.setPixel(x,y,((x/4+y/4+y/32)&1)==0?0xd0ffffff:0x80404040);
|
||||
for(String name:new String[]{"water_still","water_flow"}){
|
||||
pixels.writeToFile(textures.resolve(name+".png"));
|
||||
Files.writeString(textures.resolve(name+".png.mcmeta"),"{\"animation\":{\"frametime\":4,\"frames\":[0,1]}}");
|
||||
}
|
||||
}
|
||||
Files.writeString(directory.resolve("assets/sanctuary/water_materials.json"),"{\"light\":[],\"pond\":[\"minecraft:quartz_block\"]}");
|
||||
}catch(Exception e){throw new AssertionError(e);}
|
||||
c.runOnClient(m->{var repo=m.getResourcePackRepository();repo.reload();check(repo.addPack("file/water144-fixture"),"Custom materials pack discovered");});
|
||||
var future=c.computeOnClient(net.minecraft.client.Minecraft::reloadResourcePacks);c.waitFor(m->future.isDone()&&m.gui.overlay()==null,1200);future.join();settle(c);
|
||||
c.runOnClient(m->{check(NaturalWater.sample(m.level,56.5,99.1,8.5).equals(NaturalWater.POND),"Resource pack replaces material family");
|
||||
check(NaturalWater.sample(m.level,88.5,99.1,8.5).equals(NaturalWater.CLEAR),"Replacement removes bundled assignment");});
|
||||
c.runOnClient(m->{for(String name:new String[]{"water_still","water_flow"}){
|
||||
var sprite=waterSprite(m,name);check(sprite.width()==32&&sprite.height()==32&&sprite.getUniqueFrames().size()==2,"Custom pixel grid and animation loaded: "+name);
|
||||
}});
|
||||
var first=capture(c,"custom-materials");long animated=0;
|
||||
for(int i=0;i<8;i++){c.waitTicks(1);animated=Math.max(animated,difference(first,capture(c,"custom-animation-"+i)));}
|
||||
check(animated>100000,"Custom water animation remains visible");
|
||||
c.runOnClient(m->m.getResourcePackRepository().removePack("file/water144-fixture"));
|
||||
var restore=c.computeOnClient(net.minecraft.client.Minecraft::reloadResourcePacks);c.waitFor(m->restore.isDone()&&m.gui.overlay()==null,1200);restore.join();settle(c);
|
||||
c.runOnClient(m->{
|
||||
check(NaturalWater.sample(m.level,88.5,99.1,8.5).equals(NaturalWater.POND),"Disabling custom pack restores defaults");
|
||||
check(waterSprite(m,"water_still").width()==originalWidth&&waterSprite(m,"water_still").getUniqueFrames().size()==originalFrames,"Native water texture restored");
|
||||
});
|
||||
System.out.println("WATER144_TEXTURE_PASS custom 32px still/flow textures, alpha, two animation frames and restoration");
|
||||
}
|
||||
private static void benchmark(ClientGameTestContext c,TestServerContext s){
|
||||
int oldLimit=c.computeOnClient(m->m.options.framerateLimit().get());
|
||||
var oldInactivity=c.computeOnClient(m->m.options.inactivityFpsLimit().get());
|
||||
c.runOnClient(m->{m.options.framerateLimit().set(net.minecraft.client.Options.UNLIMITED_FRAMERATE_CUTOFF);m.options.inactivityFpsLimit().set(net.minecraft.client.InactivityFpsLimit.MINIMIZED);m.getFramerateLimitTracker().setFramerateLimit(net.minecraft.client.Options.UNLIMITED_FRAMERATE_CUTOFF);});
|
||||
var results=new LinkedHashMap<String,Object>();
|
||||
try {
|
||||
for(String scene:new String[]{"mixed","large","moving"}){
|
||||
if(scene.equals("large")){s.runOnServer(server->{command(server,"fill 31 94 -17 112 100 -17 glass");command(server,"fill 31 94 48 112 100 48 glass");
|
||||
command(server,"fill 31 94 -17 31 100 48 glass");command(server,"fill 112 94 -17 112 100 48 glass");
|
||||
command(server,"fill 32 93 -16 111 93 47 quartz_block");command(server,"fill 32 94 -16 111 94 47 sand");command(server,"fill 32 95 -16 111 99 47 water");});c.waitTicks(50);}
|
||||
if(scene.equals("moving")){
|
||||
// Test fixture only: prepare water beyond the initial client view so movement really compiles terrain.
|
||||
s.runOnServer(server->{
|
||||
command(server,"forceload add 112 -17 288 48");
|
||||
for(int x=7;x<=18;x++)for(int z=-2;z<=3;z++)server.overworld().getChunk(x,z);
|
||||
command(server,"fill 112 94 -17 288 100 -17 glass");command(server,"fill 112 94 48 288 100 48 glass");
|
||||
command(server,"fill 288 94 -17 288 100 48 glass");
|
||||
for(int x=112;x<288;x+=16){
|
||||
command(server,"fill "+x+" 93 -16 "+(x+15)+" 93 47 quartz_block");
|
||||
command(server,"fill "+x+" 94 -16 "+(x+15)+" 94 47 sand");
|
||||
command(server,"fill "+x+" 95 -16 "+(x+15)+" 99 47 water");
|
||||
}
|
||||
});c.waitTicks(80);
|
||||
}
|
||||
camera(c,s,72,125,16,70);settle(c);
|
||||
for(boolean enabled:new boolean[]{false,true,true,false,true,false,false,true}){
|
||||
camera(c,s,72,125,16,70);enable(c,enabled);c.waitTicks(80);c.runOnClient(m->{
|
||||
check(m.getFramerateLimitTracker().getFramerateLimit()==260&&!m.options.enableVsync().get(),"Uncapped native limiter and VSync disabled");
|
||||
System.out.println("WATER144_CONDITIONS limit="+m.getFramerateLimitTracker().getFramerateLimit()+" throttle="+m.getFramerateLimitTracker().getThrottleReason()+" renderLimit="+m.gameRenderer.gameRenderState().framerateLimit+" surface="+m.windowSurface().currentConfiguration()+" limiterBypass=true");
|
||||
Water144Metrics.start();});
|
||||
if(scene.equals("moving")){
|
||||
for(int i=0;i<200;i++){final int step=i;c.runOnClient(m->{m.player.setPos(48+step*.6,125,16);m.player.setDeltaMovement(net.minecraft.world.phys.Vec3.ZERO);});c.waitTicks(1);}
|
||||
}else c.waitTicks(200);
|
||||
var values=c.computeOnClient(m->Water144Metrics.stop());String key=scene+"-"+enabled+"-"+results.size();results.put(key,values);
|
||||
System.out.println("WATER144_PERF "+key+" "+Arrays.toString(values));
|
||||
}
|
||||
}
|
||||
try{Files.writeString(Path.of("water144-performance.json"),new com.google.gson.GsonBuilder().setPrettyPrinting().create().toJson(results));}catch(Exception e){throw new AssertionError(e);}
|
||||
}finally{c.runOnClient(m->{m.options.framerateLimit().set(oldLimit);m.getFramerateLimitTracker().setFramerateLimit(oldLimit);m.options.inactivityFpsLimit().set(oldInactivity);});}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
import java.util.*;
|
||||
public final class Water144Metrics {
|
||||
public static volatile boolean recording;
|
||||
private static final List<Long> frames=new ArrayList<>(),sections=new ArrayList<>();
|
||||
public static synchronized void frame(long ns){if(recording)frames.add(ns);}
|
||||
public static synchronized void section(long ns){if(recording)sections.add(ns);}
|
||||
public static synchronized void start(){frames.clear();sections.clear();recording=true;}
|
||||
public static synchronized double[] stop(){recording=false;return new double[]{frames.size(),quantile(frames,.5),quantile(frames,.95),sections.size(),quantile(sections,.5),quantile(sections,.95)};}
|
||||
private static double quantile(List<Long> values,double q){if(values.isEmpty())return 0;var sorted=values.stream().mapToLong(Long::longValue).sorted().toArray();return sorted[Math.min(sorted.length-1,(int)(sorted.length*q))]/1e6;}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package fr.koka.sanctuary.gametest.mixin;
|
||||
|
||||
import 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;}
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package fr.koka.sanctuary.gametest.mixin;
|
||||
import fr.koka.sanctuary.gametest.Water144Metrics;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.*;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import org.spongepowered.asm.mixin.*;
|
||||
import org.spongepowered.asm.mixin.injection.*;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
@Mixin(Minecraft.class)
|
||||
abstract class Water144FrameProbeMixin {
|
||||
@Unique private long sanctuary$start;
|
||||
// Benchmark only: the extracted frame state can retain a 120 FPS limit despite the option/tracker.
|
||||
@WrapOperation(method="renderFrame",at=@At(value="INVOKE",target="Lnet/minecraft/client/FramerateLimiter;limitDisplayFPS(I)V"))
|
||||
private void sanctuary$uncapped(int fps,Operation<Void> original){if(!Water144Metrics.recording)original.call(fps);}
|
||||
|
||||
@Inject(method="renderFrame",at=@At("HEAD")) private void start(boolean tick,CallbackInfo ci){if(Water144Metrics.recording)sanctuary$start=System.nanoTime();}
|
||||
@Inject(method="renderFrame",at=@At("RETURN")) private void end(boolean tick,CallbackInfo ci){if(Water144Metrics.recording)Water144Metrics.frame(System.nanoTime()-sanctuary$start);}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package fr.koka.sanctuary.gametest.mixin;
|
||||
import fr.koka.sanctuary.gametest.Water144Metrics;
|
||||
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
|
||||
import net.minecraft.client.renderer.chunk.*;
|
||||
import net.minecraft.client.renderer.SectionBufferBuilderPack;
|
||||
import net.minecraft.core.SectionPos;
|
||||
import com.mojang.blaze3d.vertex.VertexSorting;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
@Mixin(SectionCompiler.class)
|
||||
abstract class Water144SectionProbeMixin {
|
||||
@WrapMethod(method="compile")
|
||||
private SectionCompiler.Results compile(SectionPos pos,RenderSectionRegion region,VertexSorting sorting,SectionBufferBuilderPack buffers,Operation<SectionCompiler.Results> original){
|
||||
boolean record=Water144Metrics.recording;long start=record?System.nanoTime():0;
|
||||
try{return original.call(pos,region,sorting,buffers);}finally{if(record)Water144Metrics.section(System.nanoTime()-start);}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@
|
||||
"defaultRequire": 1
|
||||
},
|
||||
"client": [
|
||||
"Colored139LightmapProbeMixin",
|
||||
"Fourneau094ParticleProbeMixin",
|
||||
"Tooltip093FontProbeMixin",
|
||||
"ClientVsync046Mixin",
|
||||
@@ -18,6 +19,8 @@
|
||||
"Atlas026TextureProbeMixin",
|
||||
"Inventory013MerchantAccess",
|
||||
"Dynamic030TerrainProbeMixin",
|
||||
"Star040RenderProbeMixin"
|
||||
"Star040RenderProbeMixin",
|
||||
"Water144FrameProbeMixin",
|
||||
"Water144SectionProbeMixin"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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,16 +42,18 @@ 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")?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_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 SKY=screen("bloom_sky",null,true,false);
|
||||
private static final RenderPipeline DOWNSAMPLE=screen("bloom_downsample",null,false,false);
|
||||
private static final RenderPipeline BLUR=screen("bloom_blur",null,false,true);
|
||||
private static final RenderPipeline CORE=screen("bloom_core",new BlendFunction(BlendFactor.ONE,BlendFactor.ONE_MINUS_SRC_ALPHA,BlendFactor.ZERO,BlendFactor.ONE),false,false);
|
||||
private static final RenderPipeline APPLY=screen("bloom_apply",new BlendFunction(BlendFactor.ONE,BlendFactor.ONE_MINUS_SRC_COLOR,BlendFactor.ZERO,BlendFactor.ONE),true,false);
|
||||
private static final RenderPipeline FLARE_SOURCE=screen("lens_flare_source",null,true,false);
|
||||
private static final RenderPipeline FLARE=screen("lens_flare",new BlendFunction(BlendFactor.ONE,BlendFactor.ONE_MINUS_SRC_COLOR,BlendFactor.ZERO,BlendFactor.ONE),true,false);
|
||||
private static final BloomTerrain TERRAIN=new BloomTerrain();
|
||||
private static final Matrix4f projection=new Matrix4f();
|
||||
private static TextureTarget emission,small,ping;
|
||||
private static TextureTarget emission,small,ping,flareSource;
|
||||
private static MappableRingBuffer scene,horizontal,vertical;
|
||||
private static int frames;
|
||||
private static boolean applied;
|
||||
@@ -62,8 +64,9 @@ public final class Bloom {
|
||||
public static void render(){
|
||||
applied=false;var mc=Minecraft.getInstance();
|
||||
boolean halo=VanillaLight.bloom()&&VanillaLight.bloomIntensity()>0;
|
||||
boolean flare=VanillaLight.lensFlare()&&VanillaLight.lensFlareIntensity()>0;
|
||||
boolean cores=VanillaLight.oreEmission()&&VanillaLight.oreIntensity()>0;
|
||||
if(!VanillaLight.active()||(!halo&&!cores)||mc.level==null){release();return;}
|
||||
if(!VanillaLight.active()||(!halo&&!cores&&!flare)||mc.level==null){release();return;}
|
||||
if(PanoramaCapture.rendering())return;
|
||||
var state=mc.gameRenderer.gameRenderState().levelRenderState;var camera=state.cameraRenderState;
|
||||
if(camera.fogType!=FogType.NONE)return;
|
||||
@@ -71,21 +74,28 @@ public final class Bloom {
|
||||
if(emission==null||emission.width!=main.width||emission.height!=main.height){
|
||||
targetsClose();emission=target("emission",main.width,main.height);
|
||||
}
|
||||
if(scene==null)scene=uniforms("bloom scene",144);
|
||||
if(scene==null)scene=uniforms("bloom scene",192);
|
||||
if(halo){
|
||||
if(small==null){small=target("bloom low",Math.max(1,main.width/8),Math.max(1,main.height/8));ping=target("bloom ping",small.width,small.height);}
|
||||
if(horizontal==null){horizontal=uniforms("bloom horizontal",16);vertical=uniforms("bloom vertical",16);}
|
||||
}else haloClose();
|
||||
var entries=TERRAIN.update();var atlas=TERRAIN.materials.atlas();
|
||||
if(flare){if(flareSource==null)flareSource=target("lens flare sources",2,1);}else flareClose();
|
||||
// A celestial-only flare never needs to scan or mesh terrain emitters.
|
||||
var entries=(halo||cores)?TERRAIN.update():List.<BloomTerrain.Entry>of();
|
||||
if(!halo&&!cores)TERRAIN.close();
|
||||
var atlas=TERRAIN.materials.atlas();
|
||||
var vp=new Matrix4f(projection).mul(camera.viewRotationMatrix);var inverse=new Matrix4f(vp).invert();
|
||||
var sun=PixelShadows.sunDirection(state.skyRenderState.sunAngle);var moon=PixelShadows.sunDirection(state.skyRenderState.moonAngle);
|
||||
float celestial=state.skyRenderState.skybox==DimensionType.Skybox.OVERWORLD?state.skyRenderState.rainBrightness:0;
|
||||
var flareSun=projectFlare(vp,sun,celestial);
|
||||
var flareMoon=projectFlare(vp,moon,celestial*.35f);
|
||||
var fog=camera.fogData;scene.rotate();
|
||||
try(var mapped=scene.currentBuffer().map(false,true)){
|
||||
Std140Builder.intoBuffer(mapped.data()).putMat4f(inverse)
|
||||
.putVec4(VanillaLight.oreEmission()?VanillaLight.oreIntensity()/100f:0,RenderSystem.getDevice().getDeviceInfo().isZZeroToOne()?1:0,VanillaLight.bloomIntensity()/50f,0)
|
||||
.putVec4(sun.x,sun.y,sun.z,celestial).putVec4(moon.x,moon.y,moon.z,celestial*.35f)
|
||||
.putVec4(fog.renderDistanceStart,fog.renderDistanceEnd,fog.environmentalStart,fog.environmentalEnd).putVec4(fog.color.w,0,0,0);
|
||||
.putVec4(fog.renderDistanceStart,fog.renderDistanceEnd,fog.environmentalStart,fog.environmentalEnd).putVec4(fog.color.w,0,0,0)
|
||||
.putVec4(flareSun).putVec4(flareMoon).putVec4(flare?VanillaLight.lensFlareIntensity()/100f:0,(float)main.width/main.height,0,0);
|
||||
}
|
||||
if(halo){
|
||||
float radius=.25f+VanillaLight.bloomRadius()*.0075f;
|
||||
@@ -98,9 +108,11 @@ public final class Bloom {
|
||||
transforms.add(RenderSystem.getDynamicUniforms().writeTransform(new Matrix4f(vp).translate(offset),new Vector4f(1,1,1,1),offset,new Matrix4f()));max=Math.max(max,entry.indices());}
|
||||
var indices=RenderSystem.getSequentialBuffer(PrimitiveTopology.QUADS);indices.requestIndexCount(max);
|
||||
var nearest=RenderSystem.getSamplerCache().getClampToEdge(FilterMode.NEAREST);
|
||||
if(halo)screen(SKY,emission.getColorTextureView(),main.getColorTextureView(),true,null);
|
||||
if(halo||flare)screen(SKY,emission.getColorTextureView(),main.getColorTextureView(),true,null);
|
||||
else try(var clear=RenderSystem.getDevice().createCommandEncoder().createRenderPass(()->"Sanctuary clear emission",emission.getColorTextureView(),Optional.of(new Vector4f(0,0,0,0)))){ }
|
||||
|
||||
// Reduce the celestial-only mask before drawing any torch, ore or other terrain emitter.
|
||||
if(flare)screen(FLARE_SOURCE,flareSource.getColorTextureView(),emission.getColorTextureView(),true,null);
|
||||
if(atlas!=null&&!entries.isEmpty())try(var pass=RenderSystem.getDevice().createCommandEncoder().createRenderPass(()->"Sanctuary visible emitters",emission.getColorTextureView(),Optional.empty())){
|
||||
pass.setPipeline(RenderSystem.getCompiledPipeline(EMITTERS));pass.setUniform("BloomScene",scene.currentBuffer());pass.setUniform("WorldDepth",main.getDepthTextureView(),nearest);
|
||||
pass.setUniform("BlockAtlas",mc.getTextureManager().getTexture(TextureAtlas.LOCATION_BLOCKS).getTextureView(),nearest);pass.setUniform("EmissionMasks",atlas.getTextureView(),nearest);
|
||||
@@ -114,8 +126,19 @@ public final class Bloom {
|
||||
}
|
||||
if(cores)screen(CORE,main.getColorTextureView(),emission.getColorTextureView(),false,null);
|
||||
if(halo)screen(APPLY,main.getColorTextureView(),small.getColorTextureView(),true,null);
|
||||
if(flare)screen(FLARE,main.getColorTextureView(),flareSource.getColorTextureView(),true,null);
|
||||
applied=true;frames++;
|
||||
}
|
||||
private static Vector4f projectFlare(Matrix4f vp,Vector3f direction,float strength){
|
||||
var clip=vp.transform(new Vector4f(direction,0));
|
||||
if(clip.w<=.05f||direction.y<=0||strength<=0)return new Vector4f(0,0,0,0);
|
||||
float x=clip.x/clip.w*.5f+.5f,y=clip.y/clip.w*.5f+.5f;
|
||||
// Actual sprite light is measured on GPU. This is only its projected sampling region.
|
||||
float radius=Math.min(.3f,Math.abs(projection.m11())*.08f/clip.w);
|
||||
float horizon=Math.clamp(direction.y/.08f,0,1);
|
||||
return new Vector4f(x,y,radius,strength*horizon*horizon*(3-2*horizon));
|
||||
}
|
||||
private static void flareClose(){if(flareSource!=null){flareSource.destroyBuffers();flareSource=null;}}
|
||||
private static void screen(RenderPipeline pipeline,GpuTextureView output,GpuTextureView input,boolean useScene,MappableRingBuffer blur){
|
||||
try(var pass=RenderSystem.getDevice().createCommandEncoder().createRenderPass(()->"Sanctuary bloom",output,Optional.empty())){
|
||||
pass.setPipeline(RenderSystem.getCompiledPipeline(pipeline));pass.setUniform("InputTexture",input,RenderSystem.getSamplerCache().getClampToEdge(pipeline==CORE||pipeline==SKY?FilterMode.NEAREST:FilterMode.LINEAR));
|
||||
@@ -129,8 +152,10 @@ public final class Bloom {
|
||||
if(small!=null){small.destroyBuffers();ping.destroyBuffers();small=ping=null;}
|
||||
if(horizontal!=null){horizontal.close();vertical.close();horizontal=vertical=null;}
|
||||
}
|
||||
private static void targetsClose(){haloClose();if(emission!=null){emission.destroyBuffers();emission=null;}}
|
||||
private static void targetsClose(){haloClose();flareClose();if(emission!=null){emission.destroyBuffers();emission=null;}}
|
||||
public static void release(){TERRAIN.close();targetsClose();if(scene!=null){scene.close();scene=null;}applied=false;}
|
||||
static TextureTarget flareTarget(){return flareSource;}
|
||||
static boolean flareAllocated(){return flareSource!=null;}
|
||||
static boolean haloAllocated(){return small!=null||horizontal!=null;}
|
||||
static boolean applied(){return applied;}
|
||||
static boolean allocated(){return emission!=null;}
|
||||
|
||||
+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,172 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import com.google.gson.*;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import java.util.*;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.resource.v1.ResourceLoader;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import net.minecraft.client.renderer.block.BlockAndTintGetter;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.packs.PackType;
|
||||
import net.minecraft.server.packs.resources.ResourceManagerReloadListener;
|
||||
import net.minecraft.tags.BiomeTags;
|
||||
import net.minecraft.tags.FluidTags;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
/** Client-only water materials; no world writes, per-frame terrain scan or additional render pass. */
|
||||
public final class NaturalWater {
|
||||
public static final int DEPTH=8;
|
||||
public record Profile(float tint,float alpha,float fog) {
|
||||
public Profile mix(Profile b,float t){if(this==b||t==0)return this;if(t==1)return b;return new Profile(tint+(b.tint-tint)*t,alpha+(b.alpha-alpha)*t,fog+(b.fog-fog)*t);}
|
||||
}
|
||||
public static final Profile CLEAR=new Profile(0,.55f,1),LIGHT=new Profile(.10f,.80f,.70f),POND=new Profile(.85f,1,.40f);
|
||||
private static final Profile[] PROFILES={CLEAR,LIGHT,POND};
|
||||
private static volatile Map<Block,Integer> materials=Map.of();
|
||||
private static volatile float strength;
|
||||
private static ClientLevel previousLevel;
|
||||
private static Profile fogFrom=CLEAR,fogTarget=CLEAR;
|
||||
private static long fogChanged;
|
||||
private static boolean wasUnderwater;
|
||||
private NaturalWater(){}
|
||||
public static void register(){
|
||||
ResourceLoader.get(PackType.CLIENT_RESOURCES).registerReloadListener(Identifier.parse("sanctuary:water_materials"),
|
||||
(ResourceManagerReloadListener) manager->{
|
||||
try(var reader=manager.getResourceOrThrow(Identifier.parse("sanctuary:water_materials.json")).openAsReader()){
|
||||
var root=JsonParser.parseReader(reader).getAsJsonObject();var next=new HashMap<Block,Integer>();
|
||||
for(var entry:Map.of("light",1,"pond",2).entrySet()) for(var id:root.getAsJsonArray(entry.getKey())){
|
||||
var key=Identifier.parse(id.getAsString());
|
||||
if(!BuiltInRegistries.BLOCK.containsKey(key))throw new IllegalArgumentException("Unknown water material "+key);
|
||||
if(next.put(BuiltInRegistries.BLOCK.getValue(key),entry.getValue())!=null)throw new IllegalArgumentException("Duplicate water material "+key);
|
||||
}
|
||||
materials=Map.copyOf(next);
|
||||
}catch(Exception e){SanctuaryMod.LOGGER.warn("Invalid water materials; using bundled defaults",e);materials=defaults();}
|
||||
});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(client->{
|
||||
float next=VanillaLight.active()&&VanillaLight.naturalWater()?VanillaLight.waterIntensity()/100f:0;
|
||||
if(previousLevel!=client.level||strength!=next){previousLevel=client.level;strength=next;wasUnderwater=false;
|
||||
if(client.level!=null)invalidateWaterSections(client);}
|
||||
if(client.level==null||next==0){wasUnderwater=false;return;}
|
||||
var camera=client.gameRenderer.mainCamera();
|
||||
if(camera.getFluidInCamera()!=net.minecraft.world.level.material.FogType.WATER){wasUnderwater=false;return;}
|
||||
var pos=camera.position();var target=sample(client.level,pos.x,pos.y,pos.z);
|
||||
if(!wasUnderwater){fogFrom=fogTarget=target;fogChanged=System.nanoTime();wasUnderwater=true;}
|
||||
else if(!target.equals(fogTarget)){fogFrom=fogProfile();fogTarget=target;fogChanged=System.nanoTime();}
|
||||
});
|
||||
}
|
||||
/** Preference changes only rebuild loaded water meshes, retaining unrelated shader caches. */
|
||||
private static void invalidateWaterSections(Minecraft client){
|
||||
var level=client.level;
|
||||
var center=BlockPos.containing(client.gameRenderer.mainCamera().position());
|
||||
int radius=client.options.getEffectiveRenderDistance()+1;
|
||||
for(int z=(center.getZ()>>4)-radius;z<=(center.getZ()>>4)+radius;z++)
|
||||
for(int x=(center.getX()>>4)-radius;x<=(center.getX()>>4)+radius;x++){
|
||||
var chunk=level.getChunkSource().getChunk(x,z,net.minecraft.world.level.chunk.status.ChunkStatus.FULL,false);
|
||||
if(chunk==null)continue;
|
||||
var sections=chunk.getSections();
|
||||
for(int i=0;i<sections.length;i++)if(!sections[i].hasOnlyAir()
|
||||
&§ions[i].getStates().maybeHas(state->state.getFluidState().is(FluidTags.WATER)))
|
||||
client.levelExtractor.setSectionDirty(x,level.getSectionYFromSectionIndex(i),z);
|
||||
}
|
||||
}
|
||||
private static Map<Block,Integer> defaults(){
|
||||
var result=new HashMap<Block,Integer>();
|
||||
for(String name:new String[]{"sand","red_sand","gravel","clay"})result.put(BuiltInRegistries.BLOCK.getValue(Identifier.withDefaultNamespace(name)),1);
|
||||
for(String name:new String[]{"dirt","coarse_dirt","rooted_dirt","grass_block","podzol","mycelium","mud","muddy_mangrove_roots"})result.put(BuiltInRegistries.BLOCK.getValue(Identifier.withDefaultNamespace(name)),2);
|
||||
return Map.copyOf(result);
|
||||
}
|
||||
public static float strength(){return strength;}
|
||||
public static Profile fogProfile(){return fogFrom.mix(fogTarget,Math.clamp((System.nanoTime()-fogChanged)/500_000_000f,0,1));}
|
||||
// ClientLevel.hasChunk always returns true; inspect the cache without requesting a chunk.
|
||||
public static boolean loaded(ClientLevel level,int x,int z){
|
||||
return level.getChunkSource().getChunk(x,z,net.minecraft.world.level.chunk.status.ChunkStatus.FULL,false)!=null;
|
||||
}
|
||||
public static boolean ocean(ClientLevel level,BlockPos pos){return loaded(level,pos.getX()>>4,pos.getZ()>>4)&&level.getBiome(pos).is(BiomeTags.IS_OCEAN);}
|
||||
private static int classify(java.util.function.Function<BlockPos,BlockState> read,BlockPos pos,Map<Block,Integer> table){
|
||||
var cursor=pos.mutable();
|
||||
for(int i=1;i<=DEPTH;i++){
|
||||
cursor.set(pos.getX(),pos.getY()-i,pos.getZ());var state=read.apply(cursor);
|
||||
if(state==null||state.isAir())return 0;
|
||||
if(state.isSolid()||!state.getCollisionShape(net.minecraft.world.level.EmptyBlockGetter.INSTANCE,cursor).isEmpty()){
|
||||
// Plants have no collision; a waterlogged solid remains a basin floor.
|
||||
return table.getOrDefault(state.getBlock(),0);
|
||||
}
|
||||
if(!state.getFluidState().is(FluidTags.WATER))return 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
public static Profile sample(ClientLevel level,double x,double y,double z){
|
||||
var pos=BlockPos.containing(x,y,z);if(ocean(level,pos))return CLEAR;
|
||||
int bx=(int)Math.floor(x-.5),bz=(int)Math.floor(z-.5),by=pos.getY();
|
||||
float dx=(float)(x-.5-bx),dz=(float)(z-.5-bz);
|
||||
return live(level,bx,by,bz).mix(live(level,bx+1,by,bz),dx).mix(live(level,bx,by,bz+1).mix(live(level,bx+1,by,bz+1),dx),dz);
|
||||
}
|
||||
private static Profile live(ClientLevel l,int x,int y,int z){
|
||||
var p=new BlockPos(x,y,z);if(!loaded(l,x>>4,z>>4)||ocean(l,p))return CLEAR;
|
||||
return PROFILES[classify(l::getBlockState,p,materials)];
|
||||
}
|
||||
/** Owned by a single native section compilation, with ocean data captured before worker submission. */
|
||||
public static final class Region {
|
||||
private final int ox,oy,oz;
|
||||
private final net.minecraft.world.level.biome.BiomeManager biomes;
|
||||
private byte[] oceanKinds;
|
||||
private byte[] profiles;
|
||||
private final Map<Block,Integer> table=materials;
|
||||
public final float intensity=strength;
|
||||
public Region(ClientLevel level,int minX,int minY,int minZ){
|
||||
ox=(minX+1)*16;oy=(minY+1)*16;oz=(minZ+1)*16;
|
||||
// Copy quart holders, then keep the native seeded biome zoom on the immutable copy.
|
||||
// Sampling only quart centres would misclassify the jagged edges of ocean biomes.
|
||||
var copied=new java.util.ArrayList<net.minecraft.core.Holder<net.minecraft.world.level.biome.Biome>>(216);
|
||||
int qx=(ox>>2)-1,qy=(oy>>2)-1,qz=(oz>>2)-1;
|
||||
for(int y=0;y<6;y++)for(int z=0;z<6;z++)for(int x=0;x<6;x++)
|
||||
copied.add(loaded(level,(qx+x)>>2,(qz+z)>>2)?level.getBiomeManager().getNoiseBiomeAtQuart(qx+x,qy+y,qz+z):null);
|
||||
biomes=level.getBiomeManager().withDifferentSource((x,y,z)->copied.get(((y-qy)*6+z-qz)*6+x-qx));
|
||||
}
|
||||
// A missing biome snapshot also forces clear water at unloaded region edges.
|
||||
public boolean ocean(int x,int y,int z){
|
||||
if(oceanKinds==null)oceanKinds=new byte[18*16*18];
|
||||
int key=((y-oy)*18+z-oz+1)*18+x-ox+1;
|
||||
int code=oceanKinds[key];if(code==0){var biome=biomes.getBiome(x,y,z);code=biome==null||biome.is(BiomeTags.IS_OCEAN)?2:1;oceanKinds[key]=(byte)code;}
|
||||
return code==2;
|
||||
}
|
||||
private Profile profile(BlockAndTintGetter region,int x,int y,int z){
|
||||
if(x<ox-1||x>ox+16||y<oy||y>oy+15||z<oz-1||z>oz+16)return CLEAR;
|
||||
if(ocean(x,y,z))return CLEAR;
|
||||
int key=((y-oy)*18+z-oz+1)*18+x-ox+1;
|
||||
if(profiles==null)profiles=new byte[18*16*18];
|
||||
int code=profiles[key];if(code==0){code=1+classify(region::getBlockState,new BlockPos(x,y,z),table);profiles[key]=(byte)code;}
|
||||
return PROFILES[code-1];
|
||||
}
|
||||
public Profile corner(BlockAndTintGetter region,BlockPos p,int dx,int dz){
|
||||
if(ocean(p.getX(),p.getY(),p.getZ()))return CLEAR;
|
||||
int x=p.getX()+dx-1,y=p.getY(),z=p.getZ()+dz-1;
|
||||
// Both cells sharing an ocean edge must emit the same clear vertex.
|
||||
// The one-block transition stays on the non-ocean side, preserving ocean priority.
|
||||
if(ocean(x,y,z)||ocean(x+1,y,z)||ocean(x,y,z+1)||ocean(x+1,y,z+1))return CLEAR;
|
||||
return profile(region,x,y,z).mix(profile(region,x+1,y,z),.5f).mix(profile(region,x,y,z+1).mix(profile(region,x+1,y,z+1),.5f),.5f);
|
||||
}
|
||||
}
|
||||
public interface RegionAccess { Region sanctuary$waterRegion(); }
|
||||
public static int color(int color,int base,Profile profile,float intensity){
|
||||
float tint=profile.tint*intensity;
|
||||
int r=color>>16&255,g=color>>8&255,b=color&255;
|
||||
// Restore directional face shading after tinting, including custom biome/resource-pack colours.
|
||||
float shade=(r+g+b)/(float)Math.max(1,(base>>16&255)+(base>>8&255)+(base&255));
|
||||
int rr=Math.clamp(Math.round(r*(1-tint)+120*shade*tint),0,255);
|
||||
int gg=Math.clamp(Math.round(g*(1-tint)+132*shade*tint),0,255);
|
||||
int bb=Math.clamp(Math.round(b*(1-tint)+72*shade*tint),0,255);
|
||||
int a=Math.round((color>>>24)*(1+(profile.alpha-1)*intensity));
|
||||
return a<<24|rr<<16|gg<<8|bb;
|
||||
}
|
||||
public static void dirty(BlockPos pos){
|
||||
if(strength==0)return;
|
||||
var renderer=Minecraft.getInstance().levelExtractor;
|
||||
renderer.setSectionRangeDirty((pos.getX()-1)>>4,pos.getY()>>4,(pos.getZ()-1)>>4,
|
||||
(pos.getX()+1)>>4,(pos.getY()+DEPTH)>>4,(pos.getZ()+1)>>4);
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ final class PbrTerrain implements AutoCloseable {
|
||||
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){}
|
||||
@@ -45,7 +46,11 @@ final class PbrTerrain implements AutoCloseable {
|
||||
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 +61,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(materials::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<=MAX_BYTES){
|
||||
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();
|
||||
@@ -118,18 +130,22 @@ 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);}
|
||||
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();}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ public final class PixelShadows {
|
||||
private static TextureTarget shadowMap;
|
||||
private static MappableRingBuffer uniforms;
|
||||
private static int renderedFrames;
|
||||
private static boolean applied;
|
||||
private static boolean applied, depthReady;
|
||||
|
||||
private PixelShadows() { }
|
||||
public static void register() {
|
||||
@@ -73,8 +73,9 @@ public final class PixelShadows {
|
||||
/** Called after the world's depth is resolved and before the first-person hand and GUI. */
|
||||
public static void render() {
|
||||
applied = false;
|
||||
depthReady = false;
|
||||
var client = Minecraft.getInstance();
|
||||
if (!VanillaLight.active() || !VanillaLight.pixelShadows() || client.level == null) {
|
||||
if (!VanillaLight.active() || (!VanillaLight.pixelShadows() && !(VanillaLight.sunshafts() && VanillaLight.sunshaftIntensity()>0)) || client.level == null) {
|
||||
release();
|
||||
return;
|
||||
}
|
||||
@@ -84,12 +85,16 @@ public final class PixelShadows {
|
||||
CameraRenderState camera = state.cameraRenderState;
|
||||
var sun = sunDirection(sky.sunAngle);
|
||||
boolean lunar = sun.y <= .08f;
|
||||
if (lunar && !VanillaLight.pixelShadows()) { release(); return; }
|
||||
if (lunar) sun = sunDirection(sky.moonAngle);
|
||||
if (sky.skybox != DimensionType.Skybox.OVERWORLD || camera.fogType != FogType.NONE || sun.y <= .08f) return;
|
||||
float moon = sky.moonPhase == null ? 1 : (float)(.5 + .5 * Math.cos(sky.moonPhase.index() * Math.PI / 4));
|
||||
float strength = (lunar ? .10f * moon : .38f)
|
||||
* Math.clamp((sun.y - .08f) / .16f, 0, 1) * sky.rainBrightness;
|
||||
if (strength <= .001f) return;
|
||||
// Rain hides the native solar sprite completely. The atmospheric volume still
|
||||
// needs occluders for its weak scattered daylight; surface shadows stay off.
|
||||
boolean solarVolume=!lunar&&VanillaLight.sunshafts()&&VanillaLight.sunshaftIntensity()>0;
|
||||
if (strength <= .001f && !solarVolume) return;
|
||||
int distance = VanillaLight.shadowDistance(), pixels = VanillaLight.shadowPixels();
|
||||
TERRAIN.update(client.level, camera.pos, distance, sun);
|
||||
int size = mapSize(distance, pixels);
|
||||
@@ -139,6 +144,8 @@ public final class PixelShadows {
|
||||
pass.drawIndexed(entry.indexCount(), 1, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
depthReady = !lunar;
|
||||
if (!VanillaLight.pixelShadows() || strength <= .001f) { renderedFrames++; return; }
|
||||
var main = client.gameRenderer.mainRenderTarget();
|
||||
var nearest = RenderSystem.getSamplerCache().getClampToEdge(FilterMode.NEAREST);
|
||||
try (var pass = encoder.createRenderPass(() -> "Sanctuary pixel shadows", main.getColorTextureView(), Optional.empty())) {
|
||||
@@ -168,7 +175,9 @@ public final class PixelShadows {
|
||||
if (shadowMap != null) { shadowMap.destroyBuffers(); shadowMap = null; }
|
||||
if (uniforms != null) { uniforms.close(); uniforms = null; }
|
||||
applied = false;
|
||||
depthReady = false;
|
||||
}
|
||||
static boolean depthReady() { return depthReady; }
|
||||
static int renderedFrames() { return renderedFrames; }
|
||||
static boolean applied() { return applied; }
|
||||
static boolean ready() { return TERRAIN.settled(); }
|
||||
|
||||
@@ -28,6 +28,7 @@ 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"))
|
||||
@@ -44,7 +45,7 @@ public final class 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 dirtySection(int x,int y,int z){if(!fr.koka.sanctuary.client.DynamicLightClient.invalidatingSections())TERRAIN.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,8 +53,8 @@ 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);
|
||||
// Padding supplies valid inactive PixelShadows and local-light uniform bindings.
|
||||
if(scene==null)scene=new MappableRingBuffer(()->"Sanctuary PBR scene",GpuBuffer.USAGE_UNIFORM|GpuBuffer.USAGE_MAP_WRITE,176+ColoredDynamicLights.LIMIT*32);
|
||||
var entries=TERRAIN.update();var atlas=TERRAIN.materials.atlas();
|
||||
var vp=new Matrix4f(projection).mul(camera.viewRotationMatrix);var fog=camera.fogData;
|
||||
var sun=PixelShadows.sunDirection(sky.sunAngle);boolean lunar=sun.y<=.08f;
|
||||
@@ -65,7 +66,7 @@ public final class ProceduralPbr {
|
||||
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(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;
|
||||
@@ -81,7 +82,10 @@ public final class ProceduralPbr {
|
||||
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());
|
||||
pass.setUniform("MaterialAtlas",atlas.getTextureView(),nearest);
|
||||
if(ColoredLights.directionalReady())ColoredLights.bindDirections(pass);
|
||||
else{pass.setUniform("ColoredLightScene",scene.currentBuffer());pass.setUniform("LightDirection",source.getColorTextureView(),nearest);pass.setUniform("PreviousDirection",source.getColorTextureView(),nearest);}
|
||||
pass.setIndexBuffer(indices.getBuffer(),indices.type());
|
||||
for(int i=0;i<entries.size();i++){var entry=entries.get(i);pass.setUniform("DynamicTransforms",transforms.get(i));pass.setVertexBuffer(0,entry.vertices().slice());pass.drawIndexed(entry.indices(),1,0,0,0);}
|
||||
}
|
||||
applied=true;frames++;
|
||||
@@ -89,5 +93,8 @@ public final class ProceduralPbr {
|
||||
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 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,9 @@ 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.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 +26,14 @@ 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.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);
|
||||
@@ -30,6 +41,12 @@ public final class ShaderOptionsScreen extends OptionsSubScreen {
|
||||
Component.translatable("sanctuary.shaders.bloom_help")),VanillaLight.bloom(),VanillaLight::setBloom));
|
||||
slider("bloom_intensity",VanillaLight.bloomIntensity(),VanillaLight::setBloomIntensity);
|
||||
slider("bloom_radius",VanillaLight.bloomRadius(),VanillaLight::setBloomRadius);
|
||||
list.addBig(OptionInstance.createBoolean("sanctuary.shaders.sunshafts",OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders.sunshafts_help")),VanillaLight.sunshafts(),VanillaLight::setSunshafts));
|
||||
slider("sunshaft_intensity",VanillaLight.sunshaftIntensity(),VanillaLight::setSunshaftIntensity);
|
||||
list.addBig(OptionInstance.createBoolean("sanctuary.shaders.lens_flare",OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders.lens_flare_help")),VanillaLight.lensFlare(),VanillaLight::setLensFlare));
|
||||
slider("lens_flare_intensity",VanillaLight.lensFlareIntensity(),VanillaLight::setLensFlareIntensity);
|
||||
list.addBig(OptionInstance.createBoolean("sanctuary.shaders.ore_emission",OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders.ore_emission_help")),VanillaLight.oreEmission(),VanillaLight::setOreEmission));
|
||||
slider("ore_intensity",VanillaLight.oreIntensity(),VanillaLight::setOreIntensity);
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import com.mojang.blaze3d.buffers.Std140Builder;
|
||||
import com.mojang.blaze3d.pipeline.TextureTarget;
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.renderpearl.api.GpuFormat;
|
||||
import com.mojang.renderpearl.api.buffers.GpuBuffer;
|
||||
import com.mojang.renderpearl.api.pipeline.*;
|
||||
import com.mojang.renderpearl.api.textures.FilterMode;
|
||||
import fr.koka.sanctuary.client.panorama.PanoramaCapture;
|
||||
import java.util.Optional;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.MappableRingBuffer;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
/** Solar scattering in the existing shadow volume. No scene copy, readback, temporal history or terrain duplication. */
|
||||
public final class Sunshafts {
|
||||
private static final BindGroupLayout SCENE=BindGroupLayout.builder()
|
||||
.withUniform("PixelShadows",UniformType.UNIFORM_BUFFER)
|
||||
.withUniform("ShaftParams",UniformType.UNIFORM_BUFFER)
|
||||
.withUniform("WorldDepth",UniformType.COMBINED_IMAGE_SAMPLER).build();
|
||||
private static RenderPipeline pipeline(String name,boolean composite){
|
||||
var inputs=BindGroupLayout.builder();
|
||||
if(composite)inputs.withUniform("ShaftLight",UniformType.COMBINED_IMAGE_SAMPLER);
|
||||
else inputs.withUniform("SunDepth",UniformType.COMBINED_IMAGE_SAMPLER);
|
||||
return RenderPipelines.register(RenderPipeline.builder().withLocation(Identifier.parse("sanctuary:pipeline/"+name))
|
||||
.withVertexShader("core/screenquad").withFragmentShader(Identifier.parse("sanctuary:core/"+name))
|
||||
.withBindGroupLayout(SCENE).withBindGroupLayout(inputs.build())
|
||||
.withPrimitiveTopology(PrimitiveTopology.TRIANGLES).withCull(false)
|
||||
.withColorTargetState(new ColorTargetState(composite?Optional.of(new BlendFunction(BlendFactor.ONE,BlendFactor.ONE_MINUS_SRC_COLOR,BlendFactor.ZERO,BlendFactor.ONE)):Optional.empty(),
|
||||
composite?GpuFormat.RGBA8_UNORM:GpuFormat.RGBA16_FLOAT,composite?ColorTargetState.WRITE_COLOR:ColorTargetState.WRITE_ALL))
|
||||
.withDepthStencilState(Optional.empty()).build());
|
||||
}
|
||||
private static final RenderPipeline GATHER=pipeline("sunshaft_gather",false),APPLY=pipeline("sunshaft_apply",true);
|
||||
private static TextureTarget light;
|
||||
private static MappableRingBuffer settings;
|
||||
private static boolean applied;
|
||||
private static int frames;
|
||||
private Sunshafts(){}
|
||||
public static void render(){
|
||||
applied=false;var mc=Minecraft.getInstance();
|
||||
if(!VanillaLight.active()||!VanillaLight.sunshafts()||VanillaLight.sunshaftIntensity()==0||mc.level==null){release();return;}
|
||||
if(PanoramaCapture.rendering())return;
|
||||
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,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(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())){
|
||||
pass.setPipeline(RenderSystem.getCompiledPipeline(GATHER));pass.setUniform("PixelShadows",PixelShadows.materialUniforms());pass.setUniform("ShaftParams",settings.currentBuffer());
|
||||
pass.setUniform("WorldDepth",main.getDepthTextureView(),nearest);pass.setUniform("SunDepth",PixelShadows.materialDepth(),nearest);pass.draw(3,1,0,0);
|
||||
}
|
||||
try(var pass=encoder.createRenderPass(()->"Sanctuary solar volume composite",main.getColorTextureView(),Optional.empty())){
|
||||
pass.setPipeline(RenderSystem.getCompiledPipeline(APPLY));pass.setUniform("PixelShadows",PixelShadows.materialUniforms());pass.setUniform("ShaftParams",settings.currentBuffer());
|
||||
pass.setUniform("WorldDepth",main.getDepthTextureView(),nearest);pass.setUniform("ShaftLight",light.getColorTextureView(),nearest);pass.draw(3,1,0,0);
|
||||
}
|
||||
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;}
|
||||
static int renderedFrames(){return frames;}
|
||||
static TextureTarget target(){return light;}
|
||||
}
|
||||
@@ -16,12 +16,22 @@ public final class VanillaLight {
|
||||
private VanillaLight() { }
|
||||
private static final class Preferences {
|
||||
boolean enabled=true;
|
||||
boolean naturalWater=true;
|
||||
int waterIntensity=100;
|
||||
int saturation=142;
|
||||
boolean pixelShadows=true;
|
||||
boolean bloom=true;
|
||||
boolean sunshafts=true;
|
||||
int sunshaftIntensity=35;
|
||||
boolean lensFlare=true;
|
||||
int lensFlareIntensity=30;
|
||||
boolean pbr=true;
|
||||
int pbrIntensity=50;
|
||||
int pbrIntensity=80;
|
||||
boolean ssgi=true;
|
||||
boolean coloredLights=true;
|
||||
boolean warmTorches=false;
|
||||
int coloredLightIntensity=30;
|
||||
int coloredLightDistance=16;
|
||||
int ssgiIntensity=35;
|
||||
int bloomIntensity=20;
|
||||
int bloomRadius=50;
|
||||
@@ -39,13 +49,18 @@ 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();
|
||||
preferences.sunshaftIntensity=Math.clamp(preferences.sunshaftIntensity,0,100);
|
||||
preferences.lensFlareIntensity=Math.clamp(preferences.lensFlareIntensity,0,100);
|
||||
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.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);
|
||||
@@ -54,6 +69,10 @@ 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(){enabled();return preferences.naturalWater;}
|
||||
public static int waterIntensity(){enabled();return preferences.waterIntensity;}
|
||||
public static void setNaturalWater(boolean value){enabled();preferences.naturalWater=value;save();}
|
||||
public static void setWaterIntensity(int value){enabled();preferences.waterIntensity=Math.clamp(value,0,100);save();}
|
||||
public static int saturation(){enabled();return preferences.saturation;}
|
||||
public static boolean edgeHighlights(){enabled();return preferences.edgeHighlights;}
|
||||
public static int edgeDistance(){enabled();return preferences.edgeDistance;}
|
||||
@@ -61,10 +80,27 @@ public final class VanillaLight {
|
||||
public static int edgeIntensity(){enabled();return preferences.edgeIntensity;}
|
||||
public static void setEdgeHighlights(boolean value){enabled();preferences.edgeHighlights=value;save();}
|
||||
public static void setEdgeIntensity(int value){enabled();preferences.edgeIntensity=Math.clamp(value,0,100);save();}
|
||||
public static boolean sunshafts(){enabled();return preferences.sunshafts;}
|
||||
public static int sunshaftIntensity(){enabled();return preferences.sunshaftIntensity;}
|
||||
public static void setSunshafts(boolean value){enabled();preferences.sunshafts=value;save();}
|
||||
public static void setSunshaftIntensity(int value){enabled();preferences.sunshaftIntensity=Math.clamp(value,0,100);save();}
|
||||
public static boolean lensFlare(){enabled();return preferences.lensFlare;}
|
||||
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 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();
|
||||
}
|
||||
+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()); }
|
||||
}
|
||||
|
||||
+5
-2
@@ -4,9 +4,11 @@ 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;
|
||||
import fr.koka.sanctuary.client.shader.Sunshafts;
|
||||
import fr.koka.sanctuary.client.shader.ScreenSpaceGI;
|
||||
import net.minecraft.client.renderer.GameRenderer;
|
||||
import net.minecraft.client.renderer.ProjectionMatrixBuffer;
|
||||
@@ -24,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(); }
|
||||
private void sanctuary$shadows(CallbackInfo ci) { ColoredLights.prepare(); PixelShadows.render(); ProceduralPbr.render(); ColoredLights.render(); ScreenSpaceGI.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(); }
|
||||
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"));
|
||||
}
|
||||
}
|
||||
@@ -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,5 @@
|
||||
{
|
||||
"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,8 +2296,33 @@
|
||||
"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_help": "Experimental: procedural block normals and roughness, sun, moon and local light highlights. Enable Colored lights for local direction. Range 32 blocks. ON at 80% by default.",
|
||||
"sanctuary.shaders.pbr_intensity": "PBR strength",
|
||||
"sanctuary.shaders.pbr_intensity_help": "Material relief and highlights. Zero stops rendering and frees the cache. Original textures remain unchanged.",
|
||||
"sanctuary.shaders.pbr_intensity_value": "PBR: %s%%"
|
||||
"sanctuary.shaders.pbr_intensity_value": "PBR: %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",
|
||||
"sanctuary.shaders.lens_flare_intensity_help": "Celestial reflection strength. Zero disables the effect.",
|
||||
"sanctuary.shaders.lens_flare_intensity_value": "Lens flare: %s %%",
|
||||
"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. 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": "Biome colour, clarity from the bed. Clear oceans. Also changes underwater visibility.",
|
||||
"sanctuary.shaders.water_intensity": "Water intensity",
|
||||
"sanctuary.shaders.water_intensity_help": "At 0%, keeps native rendering. At 100%, applies all three profiles.",
|
||||
"sanctuary.shaders.water_intensity_value": "Water intensity: %s%%"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"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,8 +2296,33 @@
|
||||
"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_help": "Expérimental : normales procédurales et rugosité des blocs, reflets du soleil, de la lune et des lumières locales. Activer Lumières colorées pour la direction locale. Portée 32 blocs. Activé par défaut à 80 %.",
|
||||
"sanctuary.shaders.pbr_intensity": "Intensité du PBR",
|
||||
"sanctuary.shaders.pbr_intensity_help": "Relief et reflets des matériaux. Zéro coupe le calcul et libère le cache. Ne modifie pas les textures originales.",
|
||||
"sanctuary.shaders.pbr_intensity_value": "PBR : %s %%"
|
||||
"sanctuary.shaders.pbr_intensity_value": "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",
|
||||
"sanctuary.shaders.lens_flare_intensity_help": "Force des reflets célestes. Zéro désactive le calcul.",
|
||||
"sanctuary.shaders.lens_flare_intensity_value": "Lens flare : %s %%",
|
||||
"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. 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": "Couleur du biome, clarté selon le fond. Océans clairs. Modifie aussi la visibilité sous l’eau.",
|
||||
"sanctuary.shaders.water_intensity": "Intensité de l’eau",
|
||||
"sanctuary.shaders.water_intensity_help": "À 0 %, conserve le rendu natif. À 100 %, applique les trois profils.",
|
||||
"sanctuary.shaders.water_intensity_value": "Intensité de l’eau: %s%%"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,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,24 @@
|
||||
#version 330
|
||||
#extension GL_ARB_separate_shader_objects : require
|
||||
#include <sanctuary:bloom_scene.glsl>
|
||||
uniform sampler2D InputTexture;
|
||||
layout(location=0) in vec2 texCoord;
|
||||
layout(location=0) out vec4 fragColor;
|
||||
float softBox(vec2 center,vec2 halfSize){
|
||||
vec2 q=abs((texCoord-center)*vec2(FlareParams.y,1))/halfSize;
|
||||
float fade=1.0-smoothstep(.08,1.0,max(q.x,q.y));
|
||||
return fade*fade;
|
||||
}
|
||||
vec3 flare(vec4 source,vec3 light,vec3 tint){
|
||||
vec2 axis=source.xy-vec2(.5);
|
||||
float shape=softBox(source.xy,vec2(.26,.075))*.55;
|
||||
shape+=softBox(vec2(.5)-axis*.4,vec2(.115,.085))*.35;
|
||||
shape+=softBox(vec2(.5)-axis*.9,vec2(.055,.045))*.22;
|
||||
return light*tint*shape;
|
||||
}
|
||||
void main(){
|
||||
vec3 sun=texture(InputTexture,vec2(.25,.5)).rgb;
|
||||
vec3 moon=texture(InputTexture,vec2(.75,.5)).rgb;
|
||||
vec3 light=flare(FlareSun,sun,vec3(1,.92,.78))+flare(FlareMoon,moon,vec3(.75,.87,1));
|
||||
fragColor=vec4(1.0-exp(-light*FlareParams.x*6.0),0.0);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
#version 330
|
||||
#extension GL_ARB_separate_shader_objects : require
|
||||
#include <sanctuary:bloom_scene.glsl>
|
||||
uniform sampler2D InputTexture;
|
||||
layout(location=0) in vec2 texCoord;
|
||||
layout(location=0) out vec4 fragColor;
|
||||
void main(){
|
||||
vec4 source=gl_FragCoord.x<1.0?FlareSun:FlareMoon;
|
||||
fragColor=vec4(0.0);
|
||||
if(source.w<=0.0)return;
|
||||
float border=min(min(source.x,source.y),min(1.0-source.x,1.0-source.y));
|
||||
float edge=smoothstep(0.0,.08,border);
|
||||
if(edge<=0.0)return;
|
||||
vec3 color=vec3(0);
|
||||
// Two output pixels only: stable area integration, not a full-screen sampling loop.
|
||||
for(int y=0;y<16;y++)for(int x=0;x<16;x++){
|
||||
vec2 offset=(vec2(x,y)+.5)/16.0*2.0-1.0;
|
||||
vec2 uv=source.xy+offset*source.z/vec2(FlareParams.y,1);
|
||||
if(any(lessThan(uv,vec2(0)))||any(greaterThan(uv,vec2(1))))continue;
|
||||
color+=texture(InputTexture,uv).rgb;
|
||||
}
|
||||
fragColor=vec4(color/256.0*source.w*edge,1.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,.25),alpha=roughness*roughness,a2=alpha*alpha;
|
||||
float denom=nh*nh*(a2-1.0)+1.0,distribution=a2/max(3.14159265*denom*denom,.00001);
|
||||
float k=(roughness+1.0)*(roughness+1.0)/8.0;
|
||||
float geometry=nv/(nv*(1.0-k)+k)*nl/max(nl*(1.0-k)+k,.00001);
|
||||
// Explicit integer power stays defined even at round-off limits on OpenGL drivers.
|
||||
float grazing=1.0-vh,grazing2=grazing*grazing;
|
||||
vec3 f0=mix(vec3(.04),albedo,material.a),fresnel=f0+(1.0-f0)*(grazing2*grazing2*grazing);
|
||||
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,40 @@ 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;
|
||||
vec2 uv=gl_FragCoord.xy/vec2(textureSize(WorldDepth,0));float depth=texelFetch(WorldDepth,ivec2(gl_FragCoord.xy),0).r;
|
||||
vec4 projected=PbrInverseVP*vec4(uv*2.0-1.0,PbrParams.y>0.5?depth:depth*2.0-1.0,1.0);
|
||||
vec3 visible=projected.xyz/projected.w;
|
||||
if(depth<=0.0||abs(length(visible)-length(relativePosition))>0.035||length(relativePosition)>=PbrParams.w||abs(det)<1e-12)discard;
|
||||
vec4 texel=texture(BlockAtlas,blockUv);if(texel.a<0.99)discard;
|
||||
// Keep atlas derivatives valid after per-fragment depth/alpha rejection.
|
||||
vec4 texel=textureGrad(BlockAtlas,blockUv,tx,ty);if(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,maskUv,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;
|
||||
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;
|
||||
vec3 specular=localShine(n,v,l,material,albedo);
|
||||
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;
|
||||
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=(max(dot(n,arrival.xyz),0.0)-max(dot(baseNormal,arrival.xyz),0.0))*.7;
|
||||
lit=clamp(lit*(1.0+localRelief*localAmount),0.0,1.0);
|
||||
}
|
||||
vec3 shine=min((specular*amount+localShine(n,v,arrival.xyz,material,albedo)*localAmount)*PbrParams.x*3.0,vec3(0.5));
|
||||
lit+=(1.0-lit)*(1.0-exp(-shine));
|
||||
if(any(isnan(lit))||any(isinf(lit)))discard;
|
||||
fragColor=vec4(sqrt(clamp(lit,0.0,1.0)),1.0);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
#version 330
|
||||
#extension GL_ARB_separate_shader_objects : require
|
||||
#include <sanctuary:sunshafts.glsl>
|
||||
uniform sampler2D ShaftLight;
|
||||
layout(location=0) in vec2 texCoord;
|
||||
layout(location=0) out vec4 fragColor;
|
||||
void main(){
|
||||
float end=shaftEnd(shaftTexel(texCoord));
|
||||
vec2 size=vec2(textureSize(ShaftLight,0)),at=texCoord*size-.5,base=floor(at),f=fract(at);
|
||||
vec3 sum=vec3(0);float weight=0.0;
|
||||
for(int y=0;y<2;y++)for(int x=0;x<2;x++){
|
||||
vec4 light=texture(ShaftLight,(base+vec2(x,y)+.5)/size);
|
||||
float tolerance=.15+end*.035;
|
||||
float compatible=1.0-smoothstep(tolerance*.2,tolerance,abs(light.a-end));
|
||||
float w=(x==0?1.0-f.x:f.x)*(y==0?1.0-f.y:f.y)*compatible;
|
||||
sum+=light.rgb*w;weight+=w;
|
||||
}
|
||||
if(weight<.0001)discard;
|
||||
// Smooth energy shoulder and near-contact fade; preserve framebuffer alpha.
|
||||
fragColor=vec4(1.0-exp(-sum/weight*smoothstep(.03,.2,end)),0);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
#version 330
|
||||
#extension GL_ARB_separate_shader_objects : require
|
||||
#include <sanctuary:sunshafts.glsl>
|
||||
uniform sampler2D SunDepth;
|
||||
layout(location=0) in vec2 texCoord;
|
||||
layout(location=0) out vec4 fragColor;
|
||||
float sunlight(vec3 point){
|
||||
vec4 light=LightMatrix*vec4(point+CameraFractionPixels.xyz,1);
|
||||
vec3 q=light.xyz/light.w;q.xy=q.xy*.5+.5;
|
||||
if(DistanceClipFog.y<.5)q.z=q.z*.5+.5;
|
||||
// No clamped shadow-map edge may become a fictitious column of light.
|
||||
float border=min(min(q.x,q.y),min(1.0-q.x,1.0-q.y));
|
||||
if(border<=0.0||q.z<=0.0||q.z>=1.0)return 0.0;
|
||||
vec2 size=vec2(textureSize(SunDepth,0));
|
||||
// Fixed world-scale footprint softens the volume, not the block textures.
|
||||
vec2 footprint=max(vec2(1.0)/size,vec2(.12/(DistanceClipFog.x+16.0)));
|
||||
float lit=0.0;
|
||||
for(int y=0;y<2;y++)for(int x=0;x<2;x++){
|
||||
float depth=texture(SunDepth,q.xy+(vec2(x,y)-.5)*footprint).r;
|
||||
lit+=smoothstep(-.00035,.00035,depth-q.z);
|
||||
}
|
||||
return lit*.25*smoothstep(0.0,.06,border)*smoothstep(0.0,.03,min(q.z,1.0-q.z));
|
||||
}
|
||||
void main(){
|
||||
vec2 uv=shaftTexel(texCoord);float end=shaftEnd(uv);
|
||||
vec3 ray=normalize(shaftPosition(uv,.0001));
|
||||
float total=0.0,range=ShaftSettings.y;
|
||||
// Quadratic, deterministic segments resolve nearby occluders without frame noise.
|
||||
// Partial last segments fade continuously as a wall approaches the camera.
|
||||
for(int i=0;i<16;i++){
|
||||
float a=float(i)/16.0,b=float(i+1)/16.0;
|
||||
float start=range*a*a,stop=min(end,range*b*b);
|
||||
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*ShaftAtmosphere.z)*distanceFade;
|
||||
}
|
||||
// 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*ShaftAtmosphere.x;
|
||||
fragColor=vec4(ShaftColor.rgb*amount,end);
|
||||
}
|
||||
@@ -6,6 +6,9 @@ layout(std140) uniform BloomScene {
|
||||
vec4 MoonDirection;
|
||||
vec4 FogDistance;
|
||||
vec4 FogInfo;
|
||||
vec4 FlareSun; // projected UV, sampling radius in screen-height units, visibility
|
||||
vec4 FlareMoon;
|
||||
vec4 FlareParams; // strength, aspect ratio, reserved, reserved
|
||||
};
|
||||
vec3 bloomPosition(vec2 uv,float depth) {
|
||||
vec4 p=InverseViewProjection*vec4(uv*2.0-1.0,BloomParams.y>0.5?depth:depth*2.0-1.0,1.0);
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
layout(std140) uniform PixelShadows {
|
||||
mat4 InverseViewProjection;
|
||||
mat4 LightMatrix;
|
||||
vec4 CameraFractionPixels;
|
||||
vec4 SunStrength;
|
||||
vec4 DistanceClipFog;
|
||||
vec4 EnvironmentFog;
|
||||
};
|
||||
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));
|
||||
return (clamp(floor(uv*size),vec2(0),size-1.0)+.5)/size;
|
||||
}
|
||||
vec3 shaftPosition(vec2 uv,float depth){
|
||||
float z=DistanceClipFog.y>.5?depth:depth*2.0-1.0;
|
||||
vec4 p=InverseViewProjection*vec4(uv*2.0-1.0,z,1);
|
||||
return p.xyz/p.w;
|
||||
}
|
||||
float shaftEnd(vec2 uv){
|
||||
float depth=texture(WorldDepth,uv).r;
|
||||
return depth<=0.0?ShaftSettings.y:min(ShaftSettings.y,length(shaftPosition(uv,depth)));
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -277,7 +277,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.135"
|
||||
version = "beta.144"
|
||||
pack-format = "packwiz:1.1.0"
|
||||
|
||||
[index]
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check the eight alternating water benchmark passages per scene.
|
||||
|
||||
Usage: python3 scripts/check_water_performance.py build/water144-vulkan.log
|
||||
Writes the detailed receipt beside the log, and rejects incomplete/over-budget runs.
|
||||
The caller must also verify that no other graphics test ran during measurement.
|
||||
"""
|
||||
import json,re,sys
|
||||
from pathlib import Path
|
||||
log_path=Path(sys.argv[1])
|
||||
backend=log_path.stem
|
||||
log=log_path.read_text()
|
||||
rows={key:json.loads(data) for key,data in re.findall(r"WATER144_PERF (\S+) (\[[^\n]+\])",log)}
|
||||
assert len(rows)==24, ("Incomplete measurements",len(rows))
|
||||
assert log.count("WATER144_CONDITIONS limit=260 throttle=NONE")==24, "Measurements must be uncapped"
|
||||
result={"backend":backend,"samples":rows,"scenes":{}}
|
||||
for scene in ("mixed","large","moving"):
|
||||
off=[v for k,v in rows.items() if k.startswith(scene+"-false-")]
|
||||
on=[v for k,v in rows.items() if k.startswith(scene+"-true-")]
|
||||
assert len(off)==len(on)==4 and all(v[0]>50 for v in off+on)
|
||||
if scene=="moving":
|
||||
assert all(v[3]>0 for v in off+on), "Moving scene must compile terrain sections"
|
||||
median_off=sum(v[1] for v in off)/len(off);median_on=sum(v[1] for v in on)/len(on)
|
||||
p95_off=sum(v[2] for v in off)/len(off);p95_on=sum(v[2] for v in on)/len(on)
|
||||
value={"offMedianMs":median_off,"onMedianMs":median_on,"medianIncreasePercent":100*(median_on/median_off-1),"offP95Ms":p95_off,"onP95Ms":p95_on,"p95IncreaseMs":p95_on-p95_off}
|
||||
value["passed"]=value["medianIncreasePercent"]<=5 and value["p95IncreaseMs"]<=2
|
||||
result["scenes"][scene]=value
|
||||
log_path.with_name(log_path.stem+"-performance.json").write_text(json.dumps(result,indent=2)+"\n")
|
||||
print(json.dumps(result["scenes"],indent=2))
|
||||
assert all(v["passed"] for v in result["scenes"].values()), "Performance acceptance not met"
|
||||
@@ -8,3 +8,6 @@ ajoute les ombres portées pixélisées optionnelles, configurables depuis
|
||||
Ce dossier reste réservé aux futurs packs externes. Leur ajout au manifeste
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user