Compare commits

...
Author SHA1 Message Date
koka c05c338e6b Record beta.139 release and preserved Prism state
Build Sanctuary / build (push) Canceled after 0s
2026-09-18 02:27:19 +02:00
koka dc33ffa546 Add optional local colored lights for beta.139
Build Sanctuary / build (push) Canceled after 0s
2026-09-18 02:25:48 +02:00
koka 9d84a02ed9 Verify Sanctuary shaders on Vulkan with explicit backend tests
Build Sanctuary / build (push) Canceled after 0s
2026-09-18 01:59:13 +02:00
koka 91181865d5 Record beta.138 release and preserved Prism state
Build Sanctuary / build (push) Canceled after 0s
2026-09-18 01:25:31 +02:00
koka daf36fdeac Adapt solar volume density to weather for beta.138
Build Sanctuary / build (push) Canceled after 0s
2026-09-18 01:24:13 +02:00
koka 7f576d14d9 Record beta.137 release and verified Prism update
Build Sanctuary / build (push) Canceled after 0s
2026-09-18 01:01:59 +02:00
koka 2d7072b8a8 Add native solar volume rendering for beta.137
Build Sanctuary / build (push) Canceled after 0s
2026-09-18 01:00:20 +02:00
36 changed files with 1264 additions and 34 deletions
+27 -4
View File
@@ -20,11 +20,12 @@ 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.136** ajoute un **lens flare rectangulaire doux** pour le
soleil et la lune, activé à 30 % et réglable indépendamment du bloom.
**Options → Shaders** ; les blocs masquent les astres et leurs reflets.
Le [ticket de livraison](docs/shader-beta136.md) décrit le rendu et ses vérifications.
La version **beta.139** ajoute les **lumières colorées** optionnelles : les vrais
blocs lumineux teintent les surfaces voisines, même quand la source est hors
champ. Intensité indépendante, réglée à 35 % ; obstacles natifs et cache local.
Le [ticket](docs/colored-lights-beta139.md) décrit les limites et vérifications.
Release publiée et même instance Prism synchronisée ; sauvegardes et réglages conservés.
La [vérification Vulkan antérieure](docs/shader-vulkan-beta138.md) reste disponible.
La [livraison précédente](docs/clay-workshop-integration-beta110.md) réunit déjà
latelier, les œufs et la neige saisonnière dans linstance **Sanctuary Beta**.
Les [synchronisations antérieures](docs/git-sync-beta092.md) restent documentées.
@@ -35,6 +36,28 @@ La [carte native beta.005](docs/atlas-beta005.md) prend le relais après
l’étude de Xaero et des alternatives libres.
## 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.
+107
View File
@@ -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 01 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`.
+24
View File
@@ -14,6 +14,30 @@ 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.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
+96
View File
@@ -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é 0100.
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`.
+101
View File
@@ -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`.
+99
View File
@@ -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é.
+2 -2
View File
@@ -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.136
pack_version=beta.136
mod_version=beta.139
pack_version=beta.139
resource_pack_version=beta.121
maven_group=fr.koka.sanctuary
jei_version=30.32.0-sanctuary.3
+16
View File
@@ -25,6 +25,15 @@ 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.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 +102,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 +116,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') }
@@ -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.setLensFlare(false);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());
@@ -0,0 +1,80 @@
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();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.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()==35,"New option is opt-in, subtle at 35 percent");
Files.writeString(path,"{\"bloomIntensity\":20,\"ssgi\":false}");forgetPreferences();check(!VanillaLight.coloredLights()&&VanillaLight.coloredLightIntensity()==35&&!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);}}
}
@@ -160,7 +160,7 @@ final class EdgeHighlights129ClientChecks {
check(VanillaLight.edgeHighlights(),"Toggle persists independently");
VanillaLight.setEdgeHighlights(false);
VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);
VanillaLight.setLensFlare(false);
VanillaLight.setSunshafts(false);VanillaLight.setLensFlare(false);
VanillaLight.setPbr(false);VanillaLight.setSsgi(false);
} catch(java.io.IOException e) { throw new AssertionError(e); }
}
@@ -12,7 +12,7 @@ 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.setLensFlare(false);});
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");
@@ -43,7 +43,7 @@ final class LensFlare136ClientChecks {
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.setLensFlare(false);});
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"}){
@@ -63,7 +63,7 @@ final class LensFlare136ClientChecks {
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.setLensFlare(false);VanillaLight.setLensFlareIntensity(73);forgetPreferences();check(!VanillaLight.lensFlare()&&VanillaLight.lensFlareIntensity()==73,"Saved custom flare retained");
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);}}
@@ -13,7 +13,7 @@ import java.util.Arrays;
public final class Pbr134ClientChecks implements FabricClientGameTest {
public void runTest(ClientGameTestContext c){new PixelShadows122ClientChecks(false,false,true).runTest(c);}
static void run(ClientGameTestContext c,TestServerContext server){
c.runOnClient(m->{preferences();VanillaLight.setLensFlare(false);materials();VanillaLight.setEnabled(true);VanillaLight.setSaturation(100);VanillaLight.setPbr(false);VanillaLight.setSsgi(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);VanillaLight.setEdgeHighlights(false);VanillaLight.setPixelShadows(false);});
c.runOnClient(m->{preferences();VanillaLight.setSunshafts(false);VanillaLight.setLensFlare(false);materials();VanillaLight.setEnabled(true);VanillaLight.setSaturation(100);VanillaLight.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());
@@ -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,14 @@ 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", "");
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 +91,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 +149,8 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
c.runOnClient(m -> check(VanillaLight.active(), "Native Sanctuary shader is active without an external engine"));
float morning = c.computeOnClient(m -> m.gameRenderer.gameRenderState().levelRenderState.skyRenderState.sunAngle);
if(coloredOnly){ColoredLights139ClientChecks.run(c,server);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 +281,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
});
});
c.waitTicks(3);
c.takeScreenshot("sanctuary-beta136-shader-options-" + locale);
c.takeScreenshot("sanctuary-beta139-shader-options-" + locale);
}
c.runOnClient(m -> m.gui.setScreen(null));
}
@@ -446,7 +461,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
private static void checkPreferences() {
checkDefaults135();
VanillaLight.setLensFlare(false);
VanillaLight.setSunshafts(false);VanillaLight.setLensFlare(false);
VanillaLight.setPbr(false);
VanillaLight.setSsgi(false);
VanillaLight.setBloom(false);
@@ -550,7 +565,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
}
static Frame capture(ClientGameTestContext c, String name) {
if (name != null) c.takeScreenshot("sanctuary-beta136-shader-" + name);
if (name != null) c.takeScreenshot("sanctuary-beta139-shader-" + name);
var result = new CompletableFuture<Frame>();
c.runOnClient(m -> Screenshot.takeScreenshot(m.gameRenderer.mainRenderTarget(), image -> {
try (image) {
@@ -651,11 +666,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) {
@@ -665,6 +680,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());
}
@@ -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.setLensFlare(false);VanillaLight.setPbr(false);VanillaLight.setEnabled(true);VanillaLight.setSaturation(100);VanillaLight.setSsgi(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);VanillaLight.setPixelShadows(false);VanillaLight.setEdgeHighlights(false);});
c.runOnClient(m->{preferences();VanillaLight.setSunshafts(false);VanillaLight.setLensFlare(false);VanillaLight.setPbr(false);VanillaLight.setEnabled(true);VanillaLight.setSaturation(100);VanillaLight.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());
@@ -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);
}
}
@@ -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);}}
}
@@ -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;}
}
}
@@ -10,6 +10,7 @@
"defaultRequire": 1
},
"client": [
"Colored139LightmapProbeMixin",
"Fourneau094ParticleProbeMixin",
"Tooltip093FontProbeMixin",
"ClientVsync046Mixin",
@@ -0,0 +1,58 @@
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. */
final class ColoredLightMaterials {
private final Map<BlockState,Integer> colors=new IdentityHashMap<>();
private final Map<Identifier,Integer> sprites=new HashMap<>();
int color(BlockState state){return colors.computeIfAbsent(state,this::analyse);}
private int analyse(BlockState state){
var id=BuiltInRegistries.BLOCK.getKey(state.getBlock());String name=id.getPath();
if(id.getNamespace().equals("minecraft")){
if(name.startsWith("soul_"))return 0x55BDFF;
if(name.contains("redstone"))return 0xFF3020;
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);
}
void clear(){colors.clear();sprites.clear();}
int cachedStates(){return colors.size();}
}
@@ -0,0 +1,116 @@
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 final int SIDE=64,COUNT=SIDE*SIDE*SIDE,ATLAS=512;
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;
private BlockPos origin;
private long revision,published=-1,lastStart;
private int completed;
void update(){
var mc=Minecraft.getInstance();var modelSet=mc.getModelManager().getBlockStateModelSet();
if(level!=mc.level||models!=modelSet){close();level=mc.level;models=modelSet;}
var p=mc.gameRenderer.gameRenderState().levelRenderState.cameraRenderState.pos;
var wanted=new BlockPos(anchor(p.x),anchor(p.y),anchor(p.z));
long now=System.nanoTime();
// 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&&!inside(build.origin,BlockPos.containing(p))){build.close();build=null;}
if(build==null&&(texture==null||!wanted.equals(origin)||published!=revision)&&now-lastStart>=100_000_000L){
build=new Build(level,wanted,revision,materials);lastStart=now;
}
if(build!=null&&build.advance(now+2_000_000L)){
if(texture==null)texture=new DynamicTexture(()->"Sanctuary coloured light volume",build.takeImage());
else {texture.setPixels(build.takeImage());texture.upload();}
origin=build.origin;published=build.revision;build.close();build=null;completed++;
}
}
private static int anchor(double p){return Math.floorDiv((int)Math.floor(p),4)*4-SIDE/2;}
private static boolean inside(BlockPos base,BlockPos p){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)||build!=null&&inside(build.origin,p))revision++;}
void dirtyChunk(int x,int z){if(overlaps(origin,x,z)||build!=null&&overlaps(build.origin,x,z))revision++;}
void dirtySection(int x,int y,int z){if(overlapsSection(origin,x,y,z)||build!=null&&overlapsSection(build.origin,x,y,z))revision++;}
private static boolean overlapsSection(BlockPos o,int x,int y,int z){return overlaps(o,x,z)&&y>=(o.getY()>>4)&&y<=((o.getY()+SIDE-1)>>4);}
private static boolean overlaps(BlockPos o,int x,int z){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;}
int completed(){return completed;}
int cachedStates(){return materials.cachedStates();}
int sample(BlockPos p){if(texture==null||!inside(origin,p))return 0;int x=p.getX()-origin.getX(),y=p.getY()-origin.getY(),z=p.getZ()-origin.getZ();return texture.getPixels().getPixel(x+(y&7)*SIDE,z+(y>>3)*SIDE);}
public void close(){if(build!=null){build.close();build=null;}if(texture!=null){texture.close();texture=null;}materials.clear();level=null;models=null;origin=null;published=-1;revision=0;lastStart=0;}
private static final class Build implements AutoCloseable {
final ClientLevel level;final BlockPos origin;final long revision;final ColoredLightMaterials materials;
final BlockState[] states=new BlockState[COUNT];
final byte[] red=new byte[COUNT],green=new byte[COUNT],blue=new byte[COUNT],sky=new byte[COUNT];
final boolean[] queued=new boolean[COUNT];final IntArrayFIFOQueue queue=new IntArrayFIFOQueue();
final LevelChunk[] chunks=new LevelChunk[25];final int cx,cz;
final BlockPos.MutableBlockPos pos=new BlockPos.MutableBlockPos();
NativeImage image=new NativeImage(ATLAS,ATLAS,false);int scan,packed;
Build(ClientLevel level,BlockPos origin,long revision,ColoredLightMaterials materials){
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<5;z++)for(int x=0;x<5;x++)chunks[x+z*5]=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&63,z=i>>6&63,y=i>>12;
image.setPixel(x+(y&7)*SIDE,z+(y>>3)*SIDE,(sky[i]&255)<<24|(red[i]&255)<<16|(green[i]&255)<<8|(blue[i]&255));
if((packed&511)==0&&System.nanoTime()>=deadline)return false;
}
return true;
}
void scan(int i){
int x=origin.getX()+(i&63),z=origin.getZ()+(i>>6&63),y=origin.getY()+(i>>12);pos.set(x,y,z);
var chunk=chunks[((x>>4)-cx)+((z>>4)-cz)*5];
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))<=16)return;
int x=i&63,z=i>>6&63,y=i>>12;
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<<6)+(ny<<12);
if(r-16<=(red[n]&255)&&g-16<=(green[n]&255)&&b-16<=(blue[n]&255))continue;
int loss=net.minecraft.world.level.lighting.LightEngine.getLightDampeningInto(states[i],states[n],d,Math.max(1,states[n].getLightDampening()))*16;
int nr=Math.max(0,r-loss),ng=Math.max(0,g-loss),nb=Math.max(0,b-loss);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);
}
}
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;}}
}
// Logarithmic channels keep the source hue as light loses one native level per
// block. Independent maxima mix overlapping colours without summing unbounded light.
static int encode(int emission,int channel){return channel==0?0:Math.clamp((int)Math.round((emission+Math.log(channel/255.0)/Math.log(1.25))*16),0,240);}
}
@@ -0,0 +1,78 @@
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).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 MappableRingBuffer uniforms;
private static boolean applied;
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){VOLUME.dirtySection(x,y,z);}
public static void render(){
applied=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;
VOLUME.update();if(VOLUME.texture()==null)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 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,128);
var base=VOLUME.origin();var fog=camera.fogData;
uniforms.rotate();try(var mapped=uniforms.currentBuffer().map(false,true)){
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(ColoredLightVolume.SIDE,14,4,0);
}
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",VOLUME.texture().getTextureView(),nearest);pass.draw(3,1,0,0);
}
applied=true;frames++;
}
public static void release(){VOLUME.close();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 int materials(){return VOLUME.cachedStates();}
}
@@ -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(); }
@@ -23,6 +23,9 @@ 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));
slider("colored_light_intensity",VanillaLight.coloredLightIntensity(),VanillaLight::setColoredLightIntensity);
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 +33,9 @@ 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);
@@ -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;}
}
@@ -19,11 +19,15 @@ public final class VanillaLight {
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;
boolean ssgi=true;
boolean coloredLights=false;
int coloredLightIntensity=35;
int ssgiIntensity=35;
int bloomIntensity=20;
int bloomRadius=50;
@@ -41,8 +45,10 @@ 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.ssgiIntensity=Math.clamp(preferences.ssgiIntensity,0,100);
preferences.bloomIntensity=Math.clamp(preferences.bloomIntensity,0,100);
preferences.bloomRadius=Math.clamp(preferences.bloomRadius,0,100);
@@ -64,6 +70,10 @@ 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();}
@@ -72,6 +82,10 @@ public final class VanillaLight {
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 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 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();}
@@ -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,10 @@ 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);
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);}
}
@@ -1,6 +1,7 @@
package fr.koka.sanctuary.mixin.client;
import fr.koka.sanctuary.client.shader.PixelShadows;
import fr.koka.sanctuary.client.shader.ColoredLights;
import fr.koka.sanctuary.client.shader.ProceduralPbr;
import fr.koka.sanctuary.client.shader.Bloom;
import net.minecraft.client.multiplayer.ClientLevel;
@@ -17,11 +18,11 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
abstract class PixelShadowLevelMixin {
@Inject(method="setBlocksDirty", at=@At("HEAD"))
private void sanctuary$block(BlockPos pos, BlockState before, BlockState after, CallbackInfo ci) {
if (before != after) {PixelShadows.dirtyBlock(pos); ProceduralPbr.dirtyBlock(pos);Bloom.dirtyBlock(pos);}
if (before != after) {PixelShadows.dirtyBlock(pos); ProceduralPbr.dirtyBlock(pos);Bloom.dirtyBlock(pos); ColoredLights.dirtyBlock(pos);}
}
@Inject(method="onChunkLoaded", at=@At("TAIL"))
private void sanctuary$loaded(ChunkPos pos, CallbackInfo ci) { PixelShadows.dirtyChunk(pos.x(), pos.z()); ProceduralPbr.dirtyChunk(pos.x(), pos.z()); Bloom.dirtyChunk(pos.x(), pos.z()); }
private void sanctuary$loaded(ChunkPos pos, CallbackInfo ci) { PixelShadows.dirtyChunk(pos.x(), pos.z()); ProceduralPbr.dirtyChunk(pos.x(), pos.z()); Bloom.dirtyChunk(pos.x(), pos.z()); ColoredLights.dirtyChunk(pos.x(),pos.z()); }
@Inject(method="unload", at=@At("HEAD"))
private void sanctuary$unload(LevelChunk chunk, CallbackInfo ci) { PixelShadows.dirtyChunk(chunk.getPos().x(), chunk.getPos().z()); ProceduralPbr.dirtyChunk(chunk.getPos().x(), chunk.getPos().z()); Bloom.dirtyChunk(chunk.getPos().x(), chunk.getPos().z()); }
private void sanctuary$unload(LevelChunk chunk, CallbackInfo ci) { PixelShadows.dirtyChunk(chunk.getPos().x(), chunk.getPos().z()); ProceduralPbr.dirtyChunk(chunk.getPos().x(), chunk.getPos().z()); Bloom.dirtyChunk(chunk.getPos().x(), chunk.getPos().z()); ColoredLights.dirtyChunk(chunk.getPos().x(),chunk.getPos().z()); }
}
@@ -4,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) { 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(); }
}
@@ -2303,5 +2303,15 @@
"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.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 tint their surroundings, even off screen. Local propagation respects obstacles and does not change 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": "Subtle tint at 35%. Zero disables the calculation. Source changes update progressively."
}
@@ -2303,5 +2303,15 @@
"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.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 lair. 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 teintent leurs environs, même hors écran. Propagation locale avec obstacles, 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": "Teinte douce à 35 %. Zéro désactive le calcul. Les changements de sources sont actualisés progressivement."
}
@@ -0,0 +1,61 @@
#version 330
#extension GL_ARB_separate_shader_objects : require
uniform sampler2D WorldDepth;
uniform sampler2D SceneColor;
uniform sampler2D LightVolume;
layout(std140) uniform ColoredLightScene {
mat4 InverseViewProjection;
vec4 CameraStrength;
vec4 ClipSkyFog;
vec4 FogDistance;
vec4 VolumeSize;
};
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(ivec3 p){
p=clamp(p,ivec3(0),ivec3(63));
vec4 v=texelFetch(LightVolume,ivec2(p.x+(p.y%8)*64,p.z+(p.y/8)*64),0);
// Decode Q4.4 logarithmic RGB. Black stays exactly black.
v.rgb=max(vec3(0),(pow(vec3(.8),vec3(15)-v.rgb*(255.0/16.0))-vec3(pow(.8,15.0)))/(1.0-pow(.8,15.0)));
return v;
}
vec4 field(vec3 p){
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(base+ivec3(x,y,z))*w;
}
return result;
}
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 local=p+CameraStrength.xyz+n*(axis>.995?.51:.06);
vec3 edge=min(local,vec3(VolumeSize.x)-local);
float border=smoothstep(VolumeSize.y,VolumeSize.y+VolumeSize.z,min(edge.x,min(edge.y,edge.z)));if(border<=0.0)return;
vec4 light=field(local);float energy=max(light.r,max(light.g,light.b));if(energy<.0001)return;
float luminance=dot(light.rgb,vec3(.2126,.7152,.0722));
vec3 tint=clamp(light.rgb/max(luminance,.0001),vec3(.15),vec3(2));
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 strength=CameraStrength.w*.8*sqrt(energy)*border*(1.0-.8*light.a*ClipSkyFog.y)*(1.0-fog*ClipSkyFog.z);
fragColor=vec4(scene*mix(vec3(1),tint,strength),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);
}
@@ -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)));
}
+1 -1
View File
@@ -1,6 +1,6 @@
name = "Sanctuary"
author = "KOKA99CAB"
version = "beta.136"
version = "beta.139"
pack-format = "packwiz:1.1.0"
[index]