Compare commits

...
Author SHA1 Message Date
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
koka 0781cbd964 Record beta.136 publication and preserved Prism state
Build Sanctuary / build (push) Canceled after 0s
2026-09-18 00:28:20 +02:00
koka 71296a9cfb Add soft celestial lens flare for beta.136
Build Sanctuary / build (push) Canceled after 0s
2026-09-18 00:27:14 +02:00
koka 07d09c8508 docs: record beta.135 release and verified Prism update
Build Sanctuary / build (push) Canceled after 0s
2026-09-17 23:48:14 +02:00
koka 7fada570ef fix: stabilize close-range SSGI and update shader defaults for beta.135
Build Sanctuary / build (push) Canceled after 0s
2026-09-17 23:46:29 +02:00
33 changed files with 953 additions and 57 deletions
+26 -7
View File
@@ -20,13 +20,11 @@ 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.134** ajoute le **PBR · relief des matériaux** optionnel :
normales procédurales mises en cache, rugosité par matériau et reflets orientés.
Le SSGI reçoit un filtre spatial guidé par la profondeur et des normales de faces
stabilisées. **Options → Shaders** ; réglages existants conservés.
Le [ticket de livraison](docs/shader-beta134.md) décrit le rendu et ses limites.
Release publiée et même instance **Sanctuary Beta** synchronisée, sauvegardes
et réglages conservés.
La version **beta.137** ajoute des **rayons solaires volumétriques** qui traversent
les ouvertures du décor et le feuillage. Activés à 35 %, réglables indépendamment
du bloom dans **Options → Shaders**. Carte d'ombres partagée, portée de 64 blocs
maximum. Le [ticket](docs/shader-beta137.md) détaille les vérifications.
Release publiée et même instance Prism synchronisée ; sauvegardes et réglages conservés.
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.
@@ -37,6 +35,27 @@ La [carte native beta.005](docs/atlas-beta005.md) prend le relais après
l’étude de Xaero et des alternatives libres.
## beta.137 — Rayons solaires
Faisceaux volumétriques légers, intensité réglable et feuillage perméable.
[Contrat et vérifications](docs/shader-beta137.md) ·
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.137/Sanctuary-beta.137.mrpack) ·
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.137/Sanctuary-Test-beta.137.mrpack).
## beta.136 — Lens flare rectangulaire
Soleil et lune, reflets doux et intensité indépendante du bloom.
[Contrat et vérifications](docs/shader-beta136.md) ·
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.136/Sanctuary-beta.136.mrpack) ·
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.136/Sanctuary-Test-beta.136.mrpack).
## beta.135 — SSGI près des surfaces
Correction des coupures à courte distance, sans flouter les textures.
[Contrat et vérifications](docs/shader-beta135.md) ·
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.135/Sanctuary-beta.135.mrpack) ·
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.135/Sanctuary-Test-beta.135.mrpack).
## beta.134 — PBR optionnel et SSGI filtré
Relief et reflets des matériaux ; lissage de la lumière indirecte.
+36
View File
@@ -14,6 +14,42 @@ 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.137 — Rayons solaires volumétriques
Rayons à travers les ouvertures et le feuillage, activés à 35 %, indépendants
du bloom et de l'affichage des ombres de surface. Carte solaire partagée.
[Contrat et vérifications](shader-beta137.md).
[Release beta.137](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.137)
publiée ; canal `68cb39a5edbf5d34def02f670e2488ee852605b8`. Deux synchronisations isolées,
puis deux dans la même instance Sanctuary Beta réussies. Les 923 fichiers
personnels suivis gardent leurs hashes. Aucun monde personnel ouvert ;
sauvegarde préalable `sanctuary-backups/before-beta.137/`.
Packs normal/Test/template vérifiés dans `sanctuary-beta/build/`.
## beta.136 — Lens flare rectangulaire
Rectangles à dégradé doux pour le soleil et la lune, activés à 30 % et
réglables indépendamment du bloom. Occultation par les blocs et phases lunaires.
[Contrat et vérifications](shader-beta136.md).
[Release beta.136](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.136)
publiée ; canal `dd5b545a7cfbb957f2c70e9dc17034b44caef98f`. Deux synchronisations isolées,
puis deux dans la même instance Sanctuary Beta réussies. Les 923 fichiers
personnels suivis gardent leurs hashes. Aucun monde personnel ouvert ;
sauvegarde préalable `sanctuary-backups/before-beta.136/`.
Packs normal/Test/template vérifiés dans `sanctuary-beta/build/`.
## beta.135 — SSGI en proximité et nouveaux défauts
Normales stables près des surfaces, atténuation progressive du rebond au contact.
Bloom à 20 %, PBR et SSGI activés par défaut, choix personnels existants conservés.
[Contrat et vérifications](shader-beta135.md).
[Release beta.135](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.135)
publiée ; canal `5b0df577fcd036937c25be3a1f4580f9db3103a1`. Deux synchronisations isolées,
puis deux dans la même instance Sanctuary Beta réussies. Les 923 fichiers
personnels suivis gardent leurs hashes. Aucun monde personnel ouvert ;
sauvegarde préalable `sanctuary-backups/before-beta.135/`.
Packs normal/Test/template vérifiés dans `sanctuary-beta/build/`.
## beta.134 — PBR optionnel et SSGI filtré
Normales procédurales en cache, rugosité/métallicité par famille et reflets
+116
View File
@@ -0,0 +1,116 @@
# SHADER-135 — SSGI en contre-plongée et près des surfaces
Demande : supprimer les déchirures horizontales sous une surface, notamment
près de la lave, et atténuer proprement le rebond quand la caméra s'approche
d'une source ou d'un mur. Branche `codex/shader-closeup-beta135`, socle beta.134.
Minecraft 26.3 / Java 25 ; aucune modification de monde ou de gameplay.
## Diagnostic et correction
Le calcul des normales comparait l'aire du pixel en unités du monde à un seuil
fixe. À courte distance ou à haute résolution, cette aire devient naturellement
très petite : le rejet pouvait couper une surface continue. Les deux directions
sont maintenant normalisées avant leur produit vectoriel. Les texels de bord
sont bornés et les voisins sans profondeur ne créent plus de fausse surface.
La composition utilise le centre exact du texel de profondeur. Le rebond
s'atténue entre 0,4 et 0,06 bloc de distance au **plan** receveur, et lorsque la
vue devient presque tangente. Cette distance évite une découpe circulaire sur
un mur plat. L'image native reste nette ; aucune texture du décor n'est floutée.
Les sondes sortant du volume de projection sont rejetées. Leur contribution
s'atténue près du plan de caméra et de la limite d'épaisseur d'un impact, au lieu
d'une contribution pleine jusqu'à cette limite. Trois ressources GLSL modifiées ;
aucune passe, cible GPU, lecture CPU, histoire temporelle ou option ajoutée.
Les 12 rayons × 10 pas et les deux passes de filtrage spatial restent inchangés.
## Réglages initiaux
Demande ajoutée pendant la livraison : **bloom 20 %**, **PBR ON** et **SSGI ON**.
Les intensités PBR 50 et SSGI 35 restent inchangées. Les champs absents du fichier
de préférences prennent ces nouvelles valeurs ; les choix explicites du joueur
restent conservés. Aides françaises et anglaises mises à jour. Vérification
native supplémentaire des préférences vierges, partielles et personnalisées.
## Reproduction native
Deux scènes isolées : stone au-dessus de la lave, puis coin à 0,5 bloc d'une
paroi rouge. Caméra en contre-plongée, sept distances au plafond de 2 à 0,04 bloc,
comparaisons OFF/35/100 avant/après. Dans la première scène, à 0,1 bloc et
intensité 100, le saut maximal de gain rouge moyen entre lignes de pixels neutres
passe de **14,88 à 0,052** niveaux sur 255. Dans le coin, à 0,4 bloc et intensité
35, le rebond auparavant rejeté revient (gain moyen de **6,01** niveaux), puis
s'atténue à l'approche du contact. Ces chiffres concernent ces scènes uniquement.
Les deux essais ciblés corrigés réussissent en 1 min 18 s et 1 min 17 s sur
Apple M1 / OpenGL. Images avant/après conservées dans `build/closeup-*` ;
comparateur autonome `build/Shader-beta.135-comparaison.html`.
## Régression et livraison
Les contrôles natifs de la suite générale réussissent jusqu'au parcours SSGI :
ombres, lune, feuillage, temps réel, CTM, émissifs/bloom, couleurs de rebond,
occlusion, réglages, rechargement et déplacement latéral de 12 blocs à 35/100.
Log : `build/shader135-client-main.log`. La suite n'est pas présentée comme
entièrement réussie : elle s'est arrêtée sur une attente incorrecte du nouveau
test, qui demandait du rebond alors que sa source était sortie du champ.
Le scénario corrigé distingue la haute résolution avec source visible de la
vue très relevée sans source visible. Le bassin possède un fond pour isoler la
lave des essais suivants ; les particules du scénario précédent sont effacées
avant la comparaison PBR statique. Une première tentative avait aussi rencontré
le refus natif de remettre l'horloge à sa valeur actuelle ; la préparation de
l'heure est désormais répétable. Ces changements concernent uniquement les tests.
Le contrôle final ciblé **réussit en 2 min 52 s** : les deux scènes, 35/100,
sept distances, 1280 × 720 avec source visible, source hors champ, mouvement
minuscule, rendu combiné, puis contrôles PBR complets (cache, intensités,
matériaux, soleil, toit, OFF/zéro, redimensionnement et rechargement).
Log : `build/shader135-client-final.log` ; marqueurs `SSGI135_CLOSEUP_PASS` et
`PBR134_PASS`. Les seuils de proximité et PBR sont conservés.
Le contrôle supplémentaire des nouveaux défauts et du PBR **réussit en
1 min 16 s** : préférences vierges, champs absents, valeurs personnelles conservées,
puis rendu PBR et combinaison des effets. Log : `build/shader135-defaults-client.log`.
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
réussit en **2 min 18 s**, 126 tâches. Le serveur GameTest dédié demeure exclu
conformément au refus EULA antérieur. Les classes des scénarios clients compilent
aussi après leur isolation explicite des effets activés par défaut.
Log : `build/shader135-build.log` ; compilation : `build/shader135-test-compile.log`.
## Archives vérifiées
Comparaison à beta.134 : **6 entrées de production** modifiées, limitées aux
trois ressources SSGI, aux valeurs initiales et aux aides FR/EN. Gameplay et
textures des gemmes inchangés. Packs normal/Test : JAR Sanctuary identique ;
archives de sources et template conformes aux sources. Reçu local ignoré :
`build/shader135-artifact.json`.
## Limites
Le SSGI utilise seulement les surfaces visibles : les sources hors champ restent
absentes et un grand changement de vue peut modifier le rebond. L'atténuation
proche est volontaire ; ce n'est pas une simulation volumétrique. Le brouillard
natif de submersion continue de suspendre le SSGI, comme en beta.134. Aucun
résultat Windows/Vulkan ni mesure de FPS revendiqués.
- `Sanctuary-beta.135.mrpack` : 10422391 octets ; SHA-256 `36ac278f9f5ecd2cefd69da16ea7d153ff44c5fc8e6bf0b3a3d6b71f5d767af6`.
- `Sanctuary-Test-beta.135.mrpack` : 10441314 octets ; SHA-256 `e096374f7ea0856b20392e5d277e94f6c3d411723d7ffc10ceccdd9f0bf5c4d4`.
JAR : `a68ccd04a92a962c46a76b26a80cb44a4faf7a13a562094dde51010ade594c10`.
## Livraison
[Release beta.135](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.135)
publiée depuis `7fada570efb5ed579e6e4d7bc2b87079c7131b31`. Canal packwiz
`5b0df577fcd036937c25be3a1f4580f9db3103a1` ; téléchargements publics vérifiés par SHA-256.
Deux synchronisations isolées puis deux dans la même instance **Sanctuary Beta**
réussissent. Un seul JAR beta.135 actif ; les **923 fichiers personnels suivis**,
dont sauvegardes et préférences, gardent leurs hashes. Copie préalable dans
`sanctuary-backups/before-beta.135/`, aucun monde personnel ouvert.
Packs normal/Test/template et comparateur natif également vérifiés dans
`sanctuary-beta/build/`. Le checkout original garde ses changements préexistants.
Reçus locaux ignorés : `build/shader135-artifact.json`, `build/shader135-isolated.json`,
`build/shader135-prism.json`, `build/shader135-publication.log`.
+85
View File
@@ -0,0 +1,85 @@
# SHADER-136 — Lens flare rectangulaire doux
Demande : ajouter un lens flare avec des rectangles dégradés, exclusivement
pour le soleil et la lune. Socle beta.135, branche `codex/lens-flare-beta136`.
Minecraft 26.3 / Java 25. Aucun changement de gameplay ou de monde.
## Contrat
Effet activé initialement à **30 %**, intensité réglable 0100, indépendant du halo de bloom.
Rectangles translucides aux bords doux, suivant la position de l'astre à l'écran.
La visibilité provient de l'image émissive céleste native, avant les blocs :
les murs masquent l'effet, les sources de terrain ne le déclenchent pas.
L'astre sortant du champ doit s'atténuer sans sauter sur le bord opposé.
Pas d'historique, ni lecture GPU vers le CPU, ni texture générée sur disque.
## Rendu et budget
Trois rectangles à dégradé doux par astre : un halo horizontal près de sa
position et deux reflets plus petits le long de l'axe passant par le centre de
l'écran. Teinte solaire chaude, lunaire légèrement bleue. Intégration de la
lumière réellement dessinée par le ciel natif ; phases de lune conservées.
Atténuation aux bords de l'écran et à l'horizon, sans projection derrière la caméra.
Le masque céleste est mesuré avant l'ajout des émissifs de terrain. Réduction
vers une texture 2 × 1 RGBA16F (512 échantillons au maximum au total), puis une
passe de composition avec deux lectures de cette texture et des dégradés analytiques.
La cible d'émission plein écran existante est réutilisée. Quand seul le flare
fonctionne, aucun maillage d'émissif de terrain ni tampon de flou n'est construit.
OFF et zéro libèrent la cible du flare ; les autres effets gardent leur autonomie.
## Vérification
Le scénario client natif réussit en **1 min 52 s** sur macOS / Apple M1 / OpenGL.
Soleil OFF/30/80, lune et nouvelle lune, mur occultant, source hors champ,
glowstone exclue, image statique, redimensionnement et rechargement des ressources.
Le flare reste actif sans bloom ; OFF/zéro libèrent ses ressources. Préférences
vierges, partielles, personnalisées, bornage et libellés FR/EN vérifiés.
Les contrôles existants du bloom et des minerais émissifs passent également.
Log : `build/shader136-client-final.log`, marqueurs `EMISSIVE131_PASS` et
`LENS_FLARE136_PASS`. Les captures natives restent dans le dossier de tests ; comparatif autonome
`build/Shader-beta.136-comparaison.html` (soleil OFF/30/80 et lune OFF/30).
Une première tentative s'est arrêtée sur l'attente d'un angle de caméra :
Minecraft normalise 180° en 180°. Le scénario utilise maintenant un angle
canonique ; ce changement concerne uniquement le test.
## Limites
Effet optique en espace écran : il s'atténue quand l'astre quitte le champ et
à l'horizon. Le masque natif du ciel contrôle l'occultation ; la lune reste
volontairement beaucoup plus discrète. Le brouillard de submersion suspend
l'effet comme le bloom existant. Aucun essai Windows/Vulkan ni mesure de FPS
revendiqués.
## Construction et archives
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
réussit en **2 min 29 s**, 126 tâches. Le serveur GameTest dédié reste exclu
conformément au refus EULA antérieur. Log : `build/shader136-build.log`.
Comparaison à beta.135 : **9 entrées de production** modifiées, uniquement
le rendu, ses préférences et ses libellés. Gameplay et textures des gemmes
strictement conservés. JAR normal/Test identique, sources et template vérifiés.
Reçu local : `build/shader136-artifact.json`.
- `Sanctuary-beta.136.mrpack` : 10424821 octets ; SHA-256 `808006bf2b0cb3fe63d3e16ae3a0003d27ad0b7e793a3a05f25b5d73ae88bc8d`.
- `Sanctuary-Test-beta.136.mrpack` : 10443744 octets ; SHA-256 `70f593ad16d1f4fd4305b276ce2727a63f87bcb84df45e55840c1427a314a1eb`.
JAR : `086afede22903f8988889f6085631451f65d6d55c2e4c81f249d2234698e68e5`.
## Livraison
[Release beta.136](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.136)
publiée depuis `71296a9cfb0f6b1a6e6f0c48145f3cc432c8afd0`. Canal packwiz
`dd5b545a7cfbb957f2c70e9dc17034b44caef98f` ; téléchargements publics vérifiés par SHA-256.
Deux synchronisations isolées puis deux dans la même instance **Sanctuary Beta**
réussissent. Un seul JAR beta.136 actif ; les **923 fichiers personnels suivis**,
dont sauvegardes et préférences, gardent leurs hashes. Copie préalable dans
`sanctuary-backups/before-beta.136/`, aucun monde personnel ouvert.
Packs normal/Test/template et comparatif natif également vérifiés dans
`sanctuary-beta/build/`. Le checkout original garde ses changements préexistants.
Reçus locaux ignorés : `build/shader136-artifact.json`, `build/shader136-isolated.json`,
`build/shader136-prism.json`, `build/shader136-publication.log`.
+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`.
+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.134
pack_version=beta.134
mod_version=beta.137
pack_version=beta.137
resource_pack_version=beta.121
maven_group=fr.koka.sanctuary
jei_version=30.32.0-sanctuary.3
+6
View File
@@ -99,7 +99,13 @@ tasks.named('processGametestResources') {
def ssgi132Client = providers.gradleProperty('sanctuarySsgi132ClientTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('sanctuarySsgi132ClientTests', ssgi132Client)
if (ssgi132Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.shader.Ssgi132ClientChecks') }
def closeup135Client = providers.gradleProperty('sanctuarySsgiCloseup135ClientTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('sanctuarySsgiCloseup135ClientTests', closeup135Client)
if (closeup135Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.shader.SsgiCloseup135ClientChecks') }
def sunshaft137Client = providers.gradleProperty('sanctuarySunshaft137ClientTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('sanctuarySunshaft137ClientTests', sunshaft137Client)
if (sunshaft137Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.shader.Sunshafts137ClientChecks') }
def bloom130Client = providers.gradleProperty('sanctuaryBloom130ClientTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('sanctuaryBloom130ClientTests', bloom130Client)
if (bloom130Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.shader.Bloom130ClientChecks') }
@@ -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();analyseTextures();VanillaLight.setSaturation(100);VanillaLight.setPixelShadows(false);VanillaLight.setEdgeHighlights(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);});
c.runOnClient(m->{preferences();VanillaLight.setSunshafts(false);VanillaLight.setLensFlare(false);VanillaLight.setPbr(false);VanillaLight.setSsgi(false);analyseTextures();VanillaLight.setSaturation(100);VanillaLight.setPixelShadows(false);VanillaLight.setEdgeHighlights(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);});
server.runOnServer(s->{command(s,"time set 18000");
for(int x=4;x<=20;x++)for(int z=4;z<=20;z++){
s.overworld().setBlockAndUpdate(new BlockPos(x,64,z),Blocks.CONCRETE.black().defaultBlockState());
@@ -97,12 +97,13 @@ public final class Bloom130ClientChecks implements FabricClientGameTest {
unchanged(coveredOff,capture(c,null),"Zero bloom leaves no residual glow");
c.runOnClient(m->{VanillaLight.setBloomIntensity(35);VanillaLight.setBloom(false);VanillaLight.setOreEmission(true);VanillaLight.setOreIntensity(50);});
System.out.println("EMISSIVE131_PASS independent cores with halo OFF/zero; blur resources released; defaults; masks for 20 ore textures; selective night glow; halo intensity/spread; no through-wall glow; live edits; torch/glowstone/lamp/lava/sun; resource reload; defaults/migration; orePixels="+orePixels+" oreGain="+oreGain+" haloLow="+low+" haloHigh="+high+" sunHalo="+sunlight);
LensFlare136ClientChecks.run(c,server);
}
private static BlockPos orePos(int i){return new BlockPos(8+i%5,65,8+i/5);}
private static void preferences(){try{
var path=FabricLoader.getInstance().getConfigDir().resolve("sanctuary-shaders.json");Files.deleteIfExists(path);forgetPreferences();
check(VanillaLight.pixelShadows()&&VanillaLight.shadowDistance()==128&&VanillaLight.shadowPixels()==16&&VanillaLight.edgeDistance()==32,"Fresh requested defaults: shadows ON, 128; highlights 32; shared precision 16");
check(VanillaLight.bloom()&&VanillaLight.oreEmission()&&VanillaLight.edgeHighlights()&&VanillaLight.bloomIntensity()==35&&VanillaLight.bloomRadius()==50,"Bloom, ore emission and edge highlights default to ON");
check(VanillaLight.bloom()&&VanillaLight.oreEmission()&&VanillaLight.edgeHighlights()&&VanillaLight.bloomIntensity()==20&&VanillaLight.bloomRadius()==50,"Bloom, ore emission and edge highlights default to ON");
Files.writeString(path,"{\"bloom\":false,\"oreEmission\":false,\"edgeHighlights\":false,\"pixelShadows\":false,\"shadowDistance\":64,\"shadowPixels\":8,\"edgeDistance\":16}");forgetPreferences();
check(!VanillaLight.bloom()&&!VanillaLight.oreEmission()&&!VanillaLight.edgeHighlights()&&!VanillaLight.pixelShadows()&&VanillaLight.shadowDistance()==64&&VanillaLight.shadowPixels()==8&&VanillaLight.edgeDistance()==16,"Existing user choices are retained");
for(int n:new int[]{-10,0,35,100,400}){VanillaLight.setBloomIntensity(n);VanillaLight.setBloomRadius(n);VanillaLight.setOreIntensity(n);forgetPreferences();check(VanillaLight.bloomIntensity()==Math.clamp(n,0,100)&&VanillaLight.bloomRadius()==Math.clamp(n,0,100)&&VanillaLight.oreIntensity()==Math.clamp(n,0,100),"Persisted bloom controls are bounded");}
@@ -160,6 +160,8 @@ final class EdgeHighlights129ClientChecks {
check(VanillaLight.edgeHighlights(),"Toggle persists independently");
VanillaLight.setEdgeHighlights(false);
VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);
VanillaLight.setSunshafts(false);VanillaLight.setLensFlare(false);
VanillaLight.setPbr(false);VanillaLight.setSsgi(false);
} catch(java.io.IOException e) { throw new AssertionError(e); }
}
@@ -0,0 +1,70 @@
package fr.koka.sanctuary.client.shader;
import static fr.koka.sanctuary.client.shader.PixelShadows122ClientChecks.*;
import java.nio.file.Files;
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
import net.fabricmc.fabric.api.client.gametest.v1.context.TestServerContext;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.network.chat.Component;
/** Native sky image, not a generated mockup; explicit independent effect controls. */
final class LensFlare136ClientChecks {
static void run(ClientGameTestContext c,TestServerContext server){
c.runOnClient(m->{preferences();VanillaLight.setEnabled(true);VanillaLight.setSaturation(100);VanillaLight.setPbr(false);VanillaLight.setSsgi(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);VanillaLight.setPixelShadows(false);VanillaLight.setEdgeHighlights(false);VanillaLight.setSunshafts(false);VanillaLight.setLensFlare(false);});
server.runOnServer(s->{command(s,"time add 1");command(s,"time set 3000");});
camera(c,server,100.5,130,100.5,70);aim(c,server,false);
var off=capture(c,"flare-sun-off");enable(c,true);var on=capture(c,"flare-sun-30");
long low=gain(off,on);check(low>1000,"Default sun flare is visible: "+low);
c.runOnClient(m->check(!Bloom.haloAllocated()&&Bloom.flareAllocated(),"Flare runs without bloom blur targets"));
unchanged(on,capture(c,null),"Static rectangular flare has no animated noise");
c.runOnClient(m->VanillaLight.setLensFlareIntensity(80));frames(c);var high=capture(c,"flare-sun-80");
check(gain(off,high)>low*1.5,"Flare intensity changes native pixels");
var reload=c.computeOnClient(net.minecraft.client.Minecraft::reloadResourcePacks);c.waitFor(m->reload.isDone()&&m.gui.overlay()==null,1200);reload.join();frames(c);
unchanged(high,capture(c,"flare-reloaded"),"Flare recovers after resource reload");
int width=c.computeOnClient(m->m.getWindow().getScreenWidth()),height=c.computeOnClient(m->m.getWindow().getScreenHeight());
c.runOnClient(m->m.getWindow().setWindowed(961,541));c.waitFor(m->m.gameRenderer.mainRenderTarget().width!=off.width(),200);frames(c);
enable(c,false);var smallOff=capture(c,null);enable(c,true);check(gain(smallOff,capture(c,"flare-resized"))>1000,"Flare survives odd-sized targets");
c.runOnClient(m->m.getWindow().setWindowed(width,height));c.waitFor(m->m.gameRenderer.mainRenderTarget().width==off.width(),200);frames(c);
var direction=c.computeOnClient(m->PixelShadows.sunDirection(m.gameRenderer.gameRenderState().levelRenderState.skyRenderState.sunAngle));
var eye=c.computeOnClient(m->m.player.getEyePosition());var center=BlockPos.containing(eye.x+direction.x*8,eye.y+direction.y*8,eye.z+direction.z*8);
cover(server,center,true);c.waitTicks(35);frames(c);var hiddenOn=capture(c,"flare-sun-covered");enable(c,false);
unchanged(hiddenOn,capture(c,null),"Occluded sun produces no rectangular flare through a wall");
cover(server,center,false);c.waitTicks(35);aim(c,server,false);
c.runOnClient(m->{VanillaLight.setBloom(true);VanillaLight.setBloomIntensity(20);VanillaLight.setLensFlareIntensity(30);});enable(c,true);capture(c,"flare-bloom-combined");
c.runOnClient(m->VanillaLight.setBloom(false));look(c,server,0,80);enable(c,false);var away=capture(c,null);enable(c,true);unchanged(away,capture(c,"flare-away"),"Behind-camera celestial sources produce no reflected screen edge");
server.runOnServer(s->s.overworld().setBlockAndUpdate(new BlockPos(100,129,104),Blocks.GLOWSTONE.defaultBlockState()));
look(c,server,0,35);c.waitTicks(25);enable(c,false);var lampOff=capture(c,null);enable(c,true);unchanged(lampOff,capture(c,"flare-glowstone-excluded"),"Visible glowstone does not create a celestial flare");
server.runOnServer(s->command(s,"time set 15000"));c.waitTicks(20);aim(c,server,true);
enable(c,false);var moonOff=capture(c,"flare-moon-off");enable(c,true);long moon=gain(moonOff,capture(c,"flare-moon-30"));
check(moon>200,"Native visible moon creates its own subtle flare: "+moon);
server.runOnServer(s->command(s,"time set 111000"));c.waitFor(m->m.gameRenderer.gameRenderState().levelRenderState.skyRenderState.moonPhase.index()==4,200);aim(c,server,true);
enable(c,false);var newOff=capture(c,null);enable(c,true);check(gain(newOff,capture(c,"flare-new-moon"))<moon*.1,"Dark new moon does not retain full moon flare");
c.runOnClient(m->VanillaLight.setLensFlareIntensity(0));c.waitTicks(8);c.runOnClient(m->check(!Bloom.flareAllocated()&&!Bloom.allocated(),"Zero frees all resources with other effects OFF"));
unchanged(newOff,capture(c,null),"Zero flare restores source image");
c.runOnClient(m->{VanillaLight.setLensFlareIntensity(30);VanillaLight.setSunshafts(false);VanillaLight.setLensFlare(false);});
for(String locale:new String[]{"fr_fr","en_us"})c.runOnClient(m->{
m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());
for(String suffix:new String[]{"lens_flare","lens_flare_help","lens_flare_intensity","lens_flare_intensity_help"}){
String key="sanctuary.shaders."+suffix;check(!Component.translatable(key).getString().equals(key),"Localized flare control: "+locale+" "+key);
}
});
System.out.println("LENS_FLARE136_PASS sunGain="+low+" moonGain="+moon+" visibility, resize, reload, strength, independent bloom, zero, preferences, FR/EN");
}
private static void cover(TestServerContext server,BlockPos center,boolean on){server.runOnServer(s->{for(int x=-4;x<=4;x++)for(int y=-4;y<=4;y++)for(int z=-4;z<=4;z++)s.overworld().setBlockAndUpdate(center.offset(x,y,z),on?Blocks.STONE.defaultBlockState():Blocks.AIR.defaultBlockState());});}
private static void aim(ClientGameTestContext c,TestServerContext server,boolean moon){
var direction=c.computeOnClient(m->PixelShadows.sunDirection(moon?m.gameRenderer.gameRenderState().levelRenderState.skyRenderState.moonAngle:m.gameRenderer.gameRenderState().levelRenderState.skyRenderState.sunAngle));
look(c,server,net.minecraft.util.Mth.wrapDegrees((float)Math.toDegrees(Math.atan2(-direction.x,direction.z))+12),-(float)Math.toDegrees(Math.asin(direction.y))+8);
}
private static void frames(ClientGameTestContext c){int before=c.computeOnClient(m->Bloom.renderedFrames());c.waitFor(m->Bloom.applied()&&Bloom.ready()&&Bloom.renderedFrames()>before+10,1200);c.waitTicks(8);}
private static void enable(ClientGameTestContext c,boolean value){c.runOnClient(m->VanillaLight.setLensFlare(value));if(value)frames(c);else c.waitTicks(8);}
private static long gain(Frame off,Frame on){long sum=0;for(int i=0;i<off.pixels().length;i++)for(int shift:new int[]{0,8,16})sum+=Math.max(0,(on.pixels()[i]>>shift&255)-(off.pixels()[i]>>shift&255));return sum;}
private static void preferences(){try{
var path=FabricLoader.getInstance().getConfigDir().resolve("sanctuary-shaders.json");Files.writeString(path,"{\"bloom\":false,\"bloomIntensity\":20}");forgetPreferences();
check(VanillaLight.lensFlare()&&VanillaLight.lensFlareIntensity()==30&&!VanillaLight.bloom()&&VanillaLight.bloomIntensity()==20,"Flare defaults preserve bloom choices");
VanillaLight.setSunshafts(false);VanillaLight.setLensFlare(false);VanillaLight.setLensFlareIntensity(73);forgetPreferences();check(!VanillaLight.lensFlare()&&VanillaLight.lensFlareIntensity()==73,"Saved custom flare retained");
for(int n:new int[]{-10,0,30,100,150}){VanillaLight.setLensFlareIntensity(n);forgetPreferences();check(VanillaLight.lensFlareIntensity()==Math.clamp(n,0,100),"Flare intensity persists and clamps");}
VanillaLight.setLensFlareIntensity(30);
}catch(java.io.IOException e){throw new AssertionError(e);}}
}
@@ -13,12 +13,14 @@ import java.util.Arrays;
public final class Pbr134ClientChecks implements FabricClientGameTest {
public void runTest(ClientGameTestContext c){new PixelShadows122ClientChecks(false,false,true).runTest(c);}
static void run(ClientGameTestContext c,TestServerContext server){
c.runOnClient(m->{preferences();materials();VanillaLight.setEnabled(true);VanillaLight.setSaturation(100);VanillaLight.setPbr(false);VanillaLight.setSsgi(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);VanillaLight.setEdgeHighlights(false);VanillaLight.setPixelShadows(false);});
c.runOnClient(m->{preferences();VanillaLight.setSunshafts(false);VanillaLight.setLensFlare(false);materials();VanillaLight.setEnabled(true);VanillaLight.setSaturation(100);VanillaLight.setPbr(false);VanillaLight.setSsgi(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);VanillaLight.setEdgeHighlights(false);VanillaLight.setPixelShadows(false);});
server.runOnServer(s->{command(s,"time set 1000");for(int x=-5;x<=24;x++)for(int z=-5;z<=24;z++){
s.overworld().setBlockAndUpdate(new BlockPos(x,64,z),(x<10?Blocks.BRICKS:x<17?Blocks.OAK_PLANKS:Blocks.IRON_BLOCK).defaultBlockState());
for(int y=65;y<=102;y++)s.overworld().setBlockAndUpdate(new BlockPos(x,y,z),Blocks.AIR.defaultBlockState());
}});
camera(c,server,10.5,69,5.5,65);look(c,server,0,40);settle(c);
// The preceding lava/mob fixture may leave animated particles after its blocks are removed.
c.runOnClient(m->m.particleEngine.clearParticles());c.waitTicks(2);
var off=capture(c,"pbr-off");enable(c,true);var on=capture(c,"pbr-50");
var delta=difference(off,on);System.out.println("PBR134_DIFF "+delta);check(delta.changed>500&&delta.brighter>100&&delta.darker>100,"Texture relief has lit and shaded pixels: "+delta);
int analyses=c.computeOnClient(m->ProceduralPbr.analyses());c.waitTicks(20);
@@ -59,7 +61,7 @@ public final class Pbr134ClientChecks implements FabricClientGameTest {
}
private static void preferences(){try{
var p=FabricLoader.getInstance().getConfigDir().resolve("sanctuary-shaders.json");Files.writeString(p,"{\"ssgi\":true,\"ssgiIntensity\":35,\"bloom\":false}");forgetPreferences();
check(!VanillaLight.pbr()&&VanillaLight.pbrIntensity()==50&&VanillaLight.ssgi()&&!VanillaLight.bloom(),"PBR optional default preserves old choices");
check(VanillaLight.pbr()&&VanillaLight.pbrIntensity()==50&&VanillaLight.ssgi()&&!VanillaLight.bloom(),"Missing PBR setting defaults ON while preserving explicit old choices");
for(int n:new int[]{-1,0,50,100,101}){VanillaLight.setPbr(true);VanillaLight.setPbrIntensity(n);forgetPreferences();check(VanillaLight.pbr()&&VanillaLight.pbrIntensity()==Math.clamp(n,0,100),"PBR preference persistence/clamp");}
VanillaLight.setPbrIntensity(50);
}catch(Exception e){throw new AssertionError(e);}}
@@ -36,11 +36,13 @@ 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;
private final boolean bloomOnly,ssgiOnly,pbrOnly,closeupOnly,sunshaftOnly;
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=bloomOnly;this.ssgiOnly=ssgiOnly;this.pbrOnly=pbrOnly;}
PixelShadows122ClientChecks(boolean bloomOnly,boolean ssgiOnly,boolean pbrOnly){this(bloomOnly,ssgiOnly,pbrOnly,false);}
PixelShadows122ClientChecks(boolean bloomOnly,boolean ssgiOnly,boolean pbrOnly,boolean closeupOnly){this(bloomOnly,ssgiOnly,pbrOnly,closeupOnly,false);}
PixelShadows122ClientChecks(boolean bloomOnly,boolean ssgiOnly,boolean pbrOnly,boolean closeupOnly,boolean sunshaftOnly){this.sunshaftOnly=sunshaftOnly;this.bloomOnly=bloomOnly;this.ssgiOnly=ssgiOnly;this.pbrOnly=pbrOnly;this.closeupOnly=closeupOnly;}
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);
@@ -135,6 +137,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(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;}
if(bloomOnly){Bloom130ClientChecks.run(c,server);return;}
@@ -264,7 +268,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
});
});
c.waitTicks(3);
c.takeScreenshot("sanctuary-beta134-shader-options-" + locale);
c.takeScreenshot("sanctuary-beta137-shader-options-" + locale);
}
c.runOnClient(m -> m.gui.setScreen(null));
}
@@ -428,7 +432,24 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
? java.util.stream.Stream.concat(java.util.stream.Stream.of(widget), widgets(child)) : java.util.stream.Stream.of(widget));
}
private static void checkDefaults135() {
try {
var path=FabricLoader.getInstance().getConfigDir().resolve("sanctuary-shaders.json");
Files.deleteIfExists(path);forgetPreferences();
check(VanillaLight.bloom()&&VanillaLight.bloomIntensity()==20&&VanillaLight.pbr()&&VanillaLight.ssgi(),"Fresh defaults: bloom20, PBR and SSGI ON");
check(VanillaLight.pbrIntensity()==50&&VanillaLight.ssgiIntensity()==35,"Existing effect strengths retained");
Files.writeString(path,"{\"pbr\":false,\"ssgi\":false,\"bloomIntensity\":73}");forgetPreferences();
check(!VanillaLight.pbr()&&!VanillaLight.ssgi()&&VanillaLight.bloomIntensity()==73,"Explicit personal toggles and intensity survive changed defaults");
Files.writeString(path,"{\"bloom\":false}");forgetPreferences();
check(!VanillaLight.bloom()&&VanillaLight.bloomIntensity()==20&&VanillaLight.pbr()&&VanillaLight.ssgi(),"Missing fields get new defaults without replacing explicit values");
System.out.println("SHADER135_DEFAULTS_PASS fresh, partial and personal preferences");
} catch(IOException e){throw new AssertionError(e);}
}
private static void checkPreferences() {
checkDefaults135();
VanillaLight.setSunshafts(false);VanillaLight.setLensFlare(false);
VanillaLight.setPbr(false);
VanillaLight.setSsgi(false);
VanillaLight.setBloom(false);
VanillaLight.setOreEmission(false);
@@ -531,7 +552,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
}
static Frame capture(ClientGameTestContext c, String name) {
if (name != null) c.takeScreenshot("sanctuary-beta134-shader-" + name);
if (name != null) c.takeScreenshot("sanctuary-beta137-shader-" + name);
var result = new CompletableFuture<Frame>();
c.runOnClient(m -> Screenshot.takeScreenshot(m.gameRenderer.mainRenderTarget(), image -> {
try (image) {
@@ -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.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());
@@ -63,6 +63,7 @@ public final class Ssgi132ClientChecks implements FabricClientGameTest {
c.runOnClient(m->check(!ScreenSpaceGI.allocated(),"Main shader OFF releases SSGI too"));
c.runOnClient(m->VanillaLight.setEnabled(true));frames(c);enable(c,false);
motion(c,server);
SsgiCloseup135ClientChecks.run(c,server);
System.out.println("SSGI133_PASS red/blue diffuse bounce; intensity; flat rejection; foreground occlusion; static stability; tiny camera motion; resize; reload; combined shader; OFF/zero; preferences; red="+red+" blue="+blue);
}
private static void motion(ClientGameTestContext c,TestServerContext server){
@@ -109,7 +110,7 @@ public final class Ssgi132ClientChecks implements FabricClientGameTest {
private static void frames(ClientGameTestContext c){int start=c.computeOnClient(m->ScreenSpaceGI.renderedFrames());c.waitFor(m->ScreenSpaceGI.applied()&&ScreenSpaceGI.renderedFrames()>start+10,1200);c.waitTicks(6);}
private static void preferences(){try{
var p=FabricLoader.getInstance().getConfigDir().resolve("sanctuary-shaders.json");Files.writeString(p,"{\"bloom\":false,\"shadowPixels\":128}");forgetPreferences();
check(!VanillaLight.ssgi()&&VanillaLight.ssgiIntensity()==35&&!VanillaLight.bloom()&&VanillaLight.shadowPixels()==128,"Legacy choices preserved; experimental SSGI defaults OFF at 35");
check(VanillaLight.ssgi()&&VanillaLight.ssgiIntensity()==35&&!VanillaLight.bloom()&&VanillaLight.shadowPixels()==128,"Legacy choices preserved; missing SSGI setting defaults ON at 35");
for(int i:new int[]{-10,0,35,90,140}){VanillaLight.setSsgiIntensity(i);VanillaLight.setSsgi(true);forgetPreferences();check(VanillaLight.ssgi()&&VanillaLight.ssgiIntensity()==Math.clamp(i,0,100),"SSGI persists and clamps");}
VanillaLight.setSsgiIntensity(90);
}catch(Exception e){throw new AssertionError(e);}}
@@ -0,0 +1,78 @@
package fr.koka.sanctuary.client.shader;
import static fr.koka.sanctuary.client.shader.PixelShadows122ClientChecks.*;
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
import net.fabricmc.fabric.api.client.gametest.v1.context.TestServerContext;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.block.Blocks;
/** Native low-angle and near-plane regression fixture; never opens a personal world. */
public final class SsgiCloseup135ClientChecks implements net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest {
public void runTest(ClientGameTestContext c){new PixelShadows122ClientChecks(false,false,false,true).runTest(c);}
static void run(ClientGameTestContext c,TestServerContext server){
c.runOnClient(m->{VanillaLight.setEnabled(true);VanillaLight.setSaturation(100);VanillaLight.setSsgi(false);VanillaLight.setPbr(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);VanillaLight.setPixelShadows(false);VanillaLight.setEdgeHighlights(false);});
server.runOnServer(s->{
// Minecraft 26.3 rejects setting a clock to its existing value.
command(s,"time add 1");command(s,"time set 6000");
for(int x=-5;x<=24;x++)for(int z=-5;z<=24;z++){
// Contain lava: no flow below the platform into the following PBR fixture.
s.overworld().setBlockAndUpdate(new BlockPos(x,63,z),Blocks.STONE.defaultBlockState());
for(int y=64;y<=102;y++)s.overworld().setBlockAndUpdate(new BlockPos(x,y,z),y==64?Blocks.STONE.defaultBlockState():Blocks.AIR.defaultBlockState());
if(x>=5&&x<=16&&z>=4&&z<=14)s.overworld().setBlockAndUpdate(new BlockPos(x,64,z),Blocks.LAVA.defaultBlockState());
if(x>=5&&x<=16&&z>=8&&z<=14)s.overworld().setBlockAndUpdate(new BlockPos(x,68,z),Blocks.STONE.defaultBlockState());
if(x>=5&&x<=16&&z==12)for(int y=65;y<=67;y++)s.overworld().setBlockAndUpdate(new BlockPos(x,y,z),Blocks.CONCRETE.red().defaultBlockState());
}
});c.waitTicks(40);c.waitFor(m->m.levelRenderer.hasRenderedAllSections(),1200);
for(int scene=0;scene<2;scene++)for(int intensity:new int[]{35,100}){
c.runOnClient(m->VanillaLight.setSsgiIntensity(intensity));
for(int step=0;step<7;step++){
// Eye approaches the underside, crossing the old fixed normal-area threshold.
double gap=new double[]{2.0,1.0,.65,.4,.2,.1,.04}[step];
camera(c,server,10.5,68-gap-1.62,scene==0?9.5:11.5,65);look(c,server,0,-25);
c.runOnClient(m->VanillaLight.setSsgi(false));c.waitTicks(8);
var off=capture(c,"closeup-"+scene+"-"+intensity+"-"+step+"-off");
c.runOnClient(m->VanillaLight.setSsgi(true));c.waitFor(m->ScreenSpaceGI.applied(),1200);c.waitTicks(12);
var on=capture(c,"closeup-"+scene+"-"+intensity+"-"+step+"-on");
var stats=rowJump(off,on);
System.out.println("SSGI135_CLOSEUP scene="+scene+" intensity="+intensity+" gap="+gap+" "+stats);
if(scene==0&&gap<=.1)check(stats.jump<2&&stats.gain<.1,"Near-plane bounce fades without a horizontal bright strip: "+stats);
if(scene==1&&gap==.4)check(stats.rows>30&&stats.gain>1,"Close corner retains actual bounce instead of rejecting small world pixels: "+stats);
if(gap==.04)check(stats.gain<.05,"At contact, indirect light fades to the source image: "+stats);
}
}
int width=c.computeOnClient(m->m.getWindow().getScreenWidth()),height=c.computeOnClient(m->m.getWindow().getScreenHeight());
camera(c,server,10.5,65.98,11.5,65);look(c,server,0,-25);
c.runOnClient(m->{VanillaLight.setSsgiIntensity(100);m.getWindow().setWindowed(1280,720);});
c.waitFor(m->m.gameRenderer.mainRenderTarget().width!=width,200);c.waitTicks(15);
c.runOnClient(m->VanillaLight.setSsgi(false));c.waitTicks(8);var largeOff=capture(c,"closeup-large-off");
c.runOnClient(m->VanillaLight.setSsgi(true));c.waitTicks(15);var large=capture(c,"closeup-large-on");
var largeStats=rowJump(largeOff,large);check(largeStats.rows>30&&largeStats.gain>1,"High resolution with a visible source retains bounce: "+largeStats);
look(c,server,.00002f,-25.00002f);c.waitTicks(10);unchanged(large,capture(c,null),"Closeup has no subpixel normal threshold flicker");
look(c,server,0,-65);c.runOnClient(m->VanillaLight.setSsgi(false));c.waitTicks(8);var steepOff=capture(c,"closeup-offscreen-source-off");
c.runOnClient(m->VanillaLight.setSsgi(true));c.waitTicks(15);unchanged(steepOff,capture(c,"closeup-offscreen-source-on"),"Looking away from the source does not invent hidden light");
c.runOnClient(m->m.getWindow().setWindowed(width,height));c.waitFor(m->m.gameRenderer.mainRenderTarget().width==width,200);c.waitTicks(15);
c.runOnClient(m->{VanillaLight.setPbr(true);VanillaLight.setBloom(true);VanillaLight.setOreEmission(true);VanillaLight.setPixelShadows(true);VanillaLight.setEdgeHighlights(true);});
c.waitFor(m->ProceduralPbr.applied()&&ProceduralPbr.ready()&&Bloom.applied()&&PixelShadows.applied()&&PixelShadows.ready(),1200);
c.waitTicks(15);capture(c,"closeup-combined");
c.runOnClient(m->{VanillaLight.setSsgi(false);VanillaLight.setSsgiIntensity(35);VanillaLight.setPbr(false);});
System.out.println("SSGI135_CLOSEUP_PASS low-angle lava/stone and seven camera distances at 35/100");
}
private record Rows(double jump,double gain,int rows){}
private static Rows rowJump(Frame a,Frame b){
double previous=0,max=0,gain=0;int rows=0;
for(int y=a.height()/8;y<a.height()*7/8;y++){
double sum=0;int count=0;
for(int x=a.width()/4;x<a.width()*3/4;x++){
int i=y*a.width()+x,pa=a.pixels()[i],pb=b.pixels()[i];
int r=pa>>16&255,g=pa>>8&255,blue=pa&255;
if(Math.max(r,Math.max(g,blue))-Math.min(r,Math.min(g,blue))<12&&g>15){
sum+=Math.max(0,(pb>>16&255)-r);count++;
}
}
if(count<a.width()/8)continue;
double mean=sum/count;if(rows>0)max=Math.max(max,Math.abs(mean-previous));
previous=mean;gain+=mean;rows++;
}
return new Rows(max,gain/Math.max(rows,1),rows);
}
}
@@ -0,0 +1,63 @@
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");
}
private static void roof(TestServerContext server,int mode){server.runOnServer(s->{for(int x=76;x<=124;x++)for(int z=76;z<=124;z++)s.overworld().setBlockAndUpdate(new BlockPos(x,113,z),(mode==3||mode==1&&Math.floorMod(z,8)<3?Blocks.AIR:mode==2?Blocks.OAK_LEAVES:Blocks.OAK_PLANKS).defaultBlockState().trySetValue(net.minecraft.world.level.block.state.properties.BlockStateProperties.PERSISTENT,true));});}
private static void aim(ClientGameTestContext c,TestServerContext server){var d=c.computeOnClient(m->PixelShadows.sunDirection(m.gameRenderer.gameRenderState().levelRenderState.skyRenderState.sunAngle));look(c,server,Mth.wrapDegrees((float)Math.toDegrees(Math.atan2(-d.x,d.z))+25),-12);}
private static void settle(ClientGameTestContext c){int before=c.computeOnClient(m->Sunshafts.renderedFrames());c.waitFor(m->Sunshafts.applied()&&PixelShadows.ready()&&Sunshafts.renderedFrames()>before+10,1200);c.waitTicks(30);}
private static void enable(ClientGameTestContext c,boolean enabled){c.runOnClient(m->VanillaLight.setSunshafts(enabled));if(enabled)settle(c);else c.waitTicks(10);}
private static long gain(Frame off,Frame on){long sum=0;for(int i=0;i<off.pixels().length;i++)for(int shift:new int[]{0,8,16})sum+=Math.max(0,(on.pixels()[i]>>shift&255)-(off.pixels()[i]>>shift&255));return sum;}
// Screen blending has less headroom on a brighter vanilla wall. Remove that factor
// before comparing roofs, and exclude the roof itself where geometry changes.
private static double energy(Frame off,Frame on){double sum=0;int n=0;for(int y=off.height()/3;y<off.height();y++)for(int x=0;x<off.width();x++){int i=y*off.width()+x;for(int shift:new int[]{0,8,16}){int a=off.pixels()[i]>>shift&255,b=on.pixels()[i]>>shift&255;if(a<240){sum+=Math.max(0,b-a)/(255.0-a);n++;}}}return sum/Math.max(1,n);}
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);}}
}
@@ -42,16 +42,18 @@ public final class Bloom {
var builder=RenderPipeline.builder().withLocation(id("pipeline/"+name)).withVertexShader("core/screenquad").withFragmentShader(id("core/"+name)).withBindGroupLayout(layout.build());
if(scene)builder.withBindGroupLayout(SCENE);
return RenderPipelines.register(builder.withPrimitiveTopology(PrimitiveTopology.TRIANGLES).withCull(false)
.withColorTargetState(new ColorTargetState(Optional.ofNullable(blend),name.equals("bloom_blur")||name.equals("bloom_downsample")?GpuFormat.RGBA16_FLOAT:GpuFormat.RGBA8_UNORM,blend==null?ColorTargetState.WRITE_ALL:ColorTargetState.WRITE_COLOR)).withDepthStencilState(Optional.empty()).build());
.withColorTargetState(new ColorTargetState(Optional.ofNullable(blend),name.equals("bloom_blur")||name.equals("bloom_downsample")||name.equals("lens_flare_source")?GpuFormat.RGBA16_FLOAT:GpuFormat.RGBA8_UNORM,blend==null?ColorTargetState.WRITE_ALL:ColorTargetState.WRITE_COLOR)).withDepthStencilState(Optional.empty()).build());
}
private static final RenderPipeline SKY=screen("bloom_sky",null,true,false);
private static final RenderPipeline DOWNSAMPLE=screen("bloom_downsample",null,false,false);
private static final RenderPipeline BLUR=screen("bloom_blur",null,false,true);
private static final RenderPipeline CORE=screen("bloom_core",new BlendFunction(BlendFactor.ONE,BlendFactor.ONE_MINUS_SRC_ALPHA,BlendFactor.ZERO,BlendFactor.ONE),false,false);
private static final RenderPipeline APPLY=screen("bloom_apply",new BlendFunction(BlendFactor.ONE,BlendFactor.ONE_MINUS_SRC_COLOR,BlendFactor.ZERO,BlendFactor.ONE),true,false);
private static final RenderPipeline FLARE_SOURCE=screen("lens_flare_source",null,true,false);
private static final RenderPipeline FLARE=screen("lens_flare",new BlendFunction(BlendFactor.ONE,BlendFactor.ONE_MINUS_SRC_COLOR,BlendFactor.ZERO,BlendFactor.ONE),true,false);
private static final BloomTerrain TERRAIN=new BloomTerrain();
private static final Matrix4f projection=new Matrix4f();
private static TextureTarget emission,small,ping;
private static TextureTarget emission,small,ping,flareSource;
private static MappableRingBuffer scene,horizontal,vertical;
private static int frames;
private static boolean applied;
@@ -62,8 +64,9 @@ public final class Bloom {
public static void render(){
applied=false;var mc=Minecraft.getInstance();
boolean halo=VanillaLight.bloom()&&VanillaLight.bloomIntensity()>0;
boolean flare=VanillaLight.lensFlare()&&VanillaLight.lensFlareIntensity()>0;
boolean cores=VanillaLight.oreEmission()&&VanillaLight.oreIntensity()>0;
if(!VanillaLight.active()||(!halo&&!cores)||mc.level==null){release();return;}
if(!VanillaLight.active()||(!halo&&!cores&&!flare)||mc.level==null){release();return;}
if(PanoramaCapture.rendering())return;
var state=mc.gameRenderer.gameRenderState().levelRenderState;var camera=state.cameraRenderState;
if(camera.fogType!=FogType.NONE)return;
@@ -71,21 +74,28 @@ public final class Bloom {
if(emission==null||emission.width!=main.width||emission.height!=main.height){
targetsClose();emission=target("emission",main.width,main.height);
}
if(scene==null)scene=uniforms("bloom scene",144);
if(scene==null)scene=uniforms("bloom scene",192);
if(halo){
if(small==null){small=target("bloom low",Math.max(1,main.width/8),Math.max(1,main.height/8));ping=target("bloom ping",small.width,small.height);}
if(horizontal==null){horizontal=uniforms("bloom horizontal",16);vertical=uniforms("bloom vertical",16);}
}else haloClose();
var entries=TERRAIN.update();var atlas=TERRAIN.materials.atlas();
if(flare){if(flareSource==null)flareSource=target("lens flare sources",2,1);}else flareClose();
// A celestial-only flare never needs to scan or mesh terrain emitters.
var entries=(halo||cores)?TERRAIN.update():List.<BloomTerrain.Entry>of();
if(!halo&&!cores)TERRAIN.close();
var atlas=TERRAIN.materials.atlas();
var vp=new Matrix4f(projection).mul(camera.viewRotationMatrix);var inverse=new Matrix4f(vp).invert();
var sun=PixelShadows.sunDirection(state.skyRenderState.sunAngle);var moon=PixelShadows.sunDirection(state.skyRenderState.moonAngle);
float celestial=state.skyRenderState.skybox==DimensionType.Skybox.OVERWORLD?state.skyRenderState.rainBrightness:0;
var flareSun=projectFlare(vp,sun,celestial);
var flareMoon=projectFlare(vp,moon,celestial*.35f);
var fog=camera.fogData;scene.rotate();
try(var mapped=scene.currentBuffer().map(false,true)){
Std140Builder.intoBuffer(mapped.data()).putMat4f(inverse)
.putVec4(VanillaLight.oreEmission()?VanillaLight.oreIntensity()/100f:0,RenderSystem.getDevice().getDeviceInfo().isZZeroToOne()?1:0,VanillaLight.bloomIntensity()/50f,0)
.putVec4(sun.x,sun.y,sun.z,celestial).putVec4(moon.x,moon.y,moon.z,celestial*.35f)
.putVec4(fog.renderDistanceStart,fog.renderDistanceEnd,fog.environmentalStart,fog.environmentalEnd).putVec4(fog.color.w,0,0,0);
.putVec4(fog.renderDistanceStart,fog.renderDistanceEnd,fog.environmentalStart,fog.environmentalEnd).putVec4(fog.color.w,0,0,0)
.putVec4(flareSun).putVec4(flareMoon).putVec4(flare?VanillaLight.lensFlareIntensity()/100f:0,(float)main.width/main.height,0,0);
}
if(halo){
float radius=.25f+VanillaLight.bloomRadius()*.0075f;
@@ -98,9 +108,11 @@ public final class Bloom {
transforms.add(RenderSystem.getDynamicUniforms().writeTransform(new Matrix4f(vp).translate(offset),new Vector4f(1,1,1,1),offset,new Matrix4f()));max=Math.max(max,entry.indices());}
var indices=RenderSystem.getSequentialBuffer(PrimitiveTopology.QUADS);indices.requestIndexCount(max);
var nearest=RenderSystem.getSamplerCache().getClampToEdge(FilterMode.NEAREST);
if(halo)screen(SKY,emission.getColorTextureView(),main.getColorTextureView(),true,null);
if(halo||flare)screen(SKY,emission.getColorTextureView(),main.getColorTextureView(),true,null);
else try(var clear=RenderSystem.getDevice().createCommandEncoder().createRenderPass(()->"Sanctuary clear emission",emission.getColorTextureView(),Optional.of(new Vector4f(0,0,0,0)))){ }
// Reduce the celestial-only mask before drawing any torch, ore or other terrain emitter.
if(flare)screen(FLARE_SOURCE,flareSource.getColorTextureView(),emission.getColorTextureView(),true,null);
if(atlas!=null&&!entries.isEmpty())try(var pass=RenderSystem.getDevice().createCommandEncoder().createRenderPass(()->"Sanctuary visible emitters",emission.getColorTextureView(),Optional.empty())){
pass.setPipeline(RenderSystem.getCompiledPipeline(EMITTERS));pass.setUniform("BloomScene",scene.currentBuffer());pass.setUniform("WorldDepth",main.getDepthTextureView(),nearest);
pass.setUniform("BlockAtlas",mc.getTextureManager().getTexture(TextureAtlas.LOCATION_BLOCKS).getTextureView(),nearest);pass.setUniform("EmissionMasks",atlas.getTextureView(),nearest);
@@ -114,8 +126,19 @@ public final class Bloom {
}
if(cores)screen(CORE,main.getColorTextureView(),emission.getColorTextureView(),false,null);
if(halo)screen(APPLY,main.getColorTextureView(),small.getColorTextureView(),true,null);
if(flare)screen(FLARE,main.getColorTextureView(),flareSource.getColorTextureView(),true,null);
applied=true;frames++;
}
private static Vector4f projectFlare(Matrix4f vp,Vector3f direction,float strength){
var clip=vp.transform(new Vector4f(direction,0));
if(clip.w<=.05f||direction.y<=0||strength<=0)return new Vector4f(0,0,0,0);
float x=clip.x/clip.w*.5f+.5f,y=clip.y/clip.w*.5f+.5f;
// Actual sprite light is measured on GPU. This is only its projected sampling region.
float radius=Math.min(.3f,Math.abs(projection.m11())*.08f/clip.w);
float horizon=Math.clamp(direction.y/.08f,0,1);
return new Vector4f(x,y,radius,strength*horizon*horizon*(3-2*horizon));
}
private static void flareClose(){if(flareSource!=null){flareSource.destroyBuffers();flareSource=null;}}
private static void screen(RenderPipeline pipeline,GpuTextureView output,GpuTextureView input,boolean useScene,MappableRingBuffer blur){
try(var pass=RenderSystem.getDevice().createCommandEncoder().createRenderPass(()->"Sanctuary bloom",output,Optional.empty())){
pass.setPipeline(RenderSystem.getCompiledPipeline(pipeline));pass.setUniform("InputTexture",input,RenderSystem.getSamplerCache().getClampToEdge(pipeline==CORE||pipeline==SKY?FilterMode.NEAREST:FilterMode.LINEAR));
@@ -129,8 +152,10 @@ public final class Bloom {
if(small!=null){small.destroyBuffers();ping.destroyBuffers();small=ping=null;}
if(horizontal!=null){horizontal.close();vertical.close();horizontal=vertical=null;}
}
private static void targetsClose(){haloClose();if(emission!=null){emission.destroyBuffers();emission=null;}}
private static void targetsClose(){haloClose();flareClose();if(emission!=null){emission.destroyBuffers();emission=null;}}
public static void release(){TERRAIN.close();targetsClose();if(scene!=null){scene.close();scene=null;}applied=false;}
static TextureTarget flareTarget(){return flareSource;}
static boolean flareAllocated(){return flareSource!=null;}
static boolean haloAllocated(){return small!=null||horizontal!=null;}
static boolean applied(){return applied;}
static boolean allocated(){return emission!=null;}
@@ -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,6 +85,7 @@ 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));
@@ -139,6 +141,8 @@ public final class PixelShadows {
pass.drawIndexed(entry.indexCount(), 1, 0, 0, 0);
}
}
depthReady = !lunar;
if (!VanillaLight.pixelShadows()) { 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 +172,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(); }
@@ -30,6 +30,12 @@ public final class ShaderOptionsScreen extends OptionsSubScreen {
Component.translatable("sanctuary.shaders.bloom_help")),VanillaLight.bloom(),VanillaLight::setBloom));
slider("bloom_intensity",VanillaLight.bloomIntensity(),VanillaLight::setBloomIntensity);
slider("bloom_radius",VanillaLight.bloomRadius(),VanillaLight::setBloomRadius);
list.addBig(OptionInstance.createBoolean("sanctuary.shaders.sunshafts",OptionInstance.cachedConstantTooltip(
Component.translatable("sanctuary.shaders.sunshafts_help")),VanillaLight.sunshafts(),VanillaLight::setSunshafts));
slider("sunshaft_intensity",VanillaLight.sunshaftIntensity(),VanillaLight::setSunshaftIntensity);
list.addBig(OptionInstance.createBoolean("sanctuary.shaders.lens_flare",OptionInstance.cachedConstantTooltip(
Component.translatable("sanctuary.shaders.lens_flare_help")),VanillaLight.lensFlare(),VanillaLight::setLensFlare));
slider("lens_flare_intensity",VanillaLight.lensFlareIntensity(),VanillaLight::setLensFlareIntensity);
list.addBig(OptionInstance.createBoolean("sanctuary.shaders.ore_emission",OptionInstance.cachedConstantTooltip(
Component.translatable("sanctuary.shaders.ore_emission_help")),VanillaLight.oreEmission(),VanillaLight::setOreEmission));
slider("ore_intensity",VanillaLight.oreIntensity(),VanillaLight::setOreIntensity);
@@ -0,0 +1,70 @@
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,32);
var fog=mc.gameRenderer.gameRenderState().levelRenderState.cameraRenderState.fogData;
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);
}
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++;
}
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 pbr=false;
boolean sunshafts=true;
int sunshaftIntensity=35;
boolean lensFlare=true;
int lensFlareIntensity=30;
boolean pbr=true;
int pbrIntensity=50;
boolean ssgi=false;
boolean ssgi=true;
int ssgiIntensity=35;
int bloomIntensity=35;
int bloomIntensity=20;
int bloomRadius=50;
boolean oreEmission=true;
int oreIntensity=50;
@@ -39,6 +43,8 @@ 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.ssgiIntensity=Math.clamp(preferences.ssgiIntensity,0,100);
preferences.bloomIntensity=Math.clamp(preferences.bloomIntensity,0,100);
@@ -61,6 +67,14 @@ public final class VanillaLight {
public static int edgeIntensity(){enabled();return preferences.edgeIntensity;}
public static void setEdgeHighlights(boolean value){enabled();preferences.edgeHighlights=value;save();}
public static void setEdgeIntensity(int value){enabled();preferences.edgeIntensity=Math.clamp(value,0,100);save();}
public static boolean sunshafts(){enabled();return preferences.sunshafts;}
public static int sunshaftIntensity(){enabled();return preferences.sunshaftIntensity;}
public static void setSunshafts(boolean value){enabled();preferences.sunshafts=value;save();}
public static void setSunshaftIntensity(int value){enabled();preferences.sunshaftIntensity=Math.clamp(value,0,100);save();}
public static boolean lensFlare(){enabled();return preferences.lensFlare;}
public static int lensFlareIntensity(){enabled();return preferences.lensFlareIntensity;}
public static void setLensFlare(boolean value){enabled();preferences.lensFlare=value;save();}
public static void setLensFlareIntensity(int value){enabled();preferences.lensFlareIntensity=Math.clamp(value,0,100);save();}
public static boolean pbr(){enabled();return preferences.pbr;}
public static int pbrIntensity(){enabled();return preferences.pbrIntensity;}
public static void setPbr(boolean value){enabled();preferences.pbr=value;save();}
@@ -7,6 +7,7 @@ import fr.koka.sanctuary.client.shader.PixelShadows;
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;
@@ -28,7 +29,7 @@ abstract class PixelShadowRendererMixin {
return original.call(buffer, matrix);
}
@Inject(method="renderLevel", at=@At(value="INVOKE", target="Lnet/minecraft/client/renderer/GameRenderer;render3dHud(Lnet/minecraft/client/renderer/state/level/CameraRenderState;Lnet/minecraft/client/renderer/state/level/PlayerRenderState;Lnet/minecraft/client/renderer/state/OptionsRenderState;Z)V"))
private void sanctuary$shadows(CallbackInfo ci) { PixelShadows.render(); ProceduralPbr.render(); ScreenSpaceGI.render(); EdgeHighlights.render(); Bloom.render(); }
private void sanctuary$shadows(CallbackInfo ci) { PixelShadows.render(); ProceduralPbr.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(); ScreenSpaceGI.release(); Sunshafts.release(); }
}
@@ -2279,7 +2279,7 @@
"sanctuary.shaders.bloom": "Bloom",
"sanctuary.shaders.bloom_help": "Glow around the sun and luminous surfaces. ON by default. Turning it off removes halos while keeping emissive ores. Requires Vanilla Light.",
"sanctuary.shaders.bloom_intensity": "Bloom strength",
"sanctuary.shaders.bloom_intensity_help": "Halo strength. Zero removes halos and their blur passes while keeping emissive ores.",
"sanctuary.shaders.bloom_intensity_help": "Halo strength, 20% by default. Zero removes halos and their blur passes while keeping emissive ores.",
"sanctuary.shaders.bloom_intensity_value": "Bloom: %s%%",
"sanctuary.shaders.bloom_radius": "Bloom spread",
"sanctuary.shaders.bloom_radius_help": "Halo size around luminous pixels. Texture details stay sharp.",
@@ -2290,13 +2290,23 @@
"sanctuary.shaders.ore_intensity_help": "Inclusion brightness. Does not change world light levels or mob spawning.",
"sanctuary.shaders.ore_intensity_value": "Ores: %s%%",
"sanctuary.shaders.ssgi": "SSGI · color bounce",
"sanctuary.shaders.ssgi_help": "Experimental: enhanced color bounce between nearby visible surfaces. Radius 2.5 blocks. Off-screen surfaces do not contribute. OFF by default.",
"sanctuary.shaders.ssgi_help": "Experimental: enhanced color bounce between nearby visible surfaces. Radius 2.5 blocks. Off-screen surfaces do not contribute. ON by default.",
"sanctuary.shaders.ssgi_intensity": "SSGI strength",
"sanctuary.shaders.ssgi_intensity_help": "Strength of diffuse color bounce. Zero stops rendering and frees its resources. Does not change world light.",
"sanctuary.shaders.ssgi_intensity_value": "SSGI: %s%%",
"sanctuary.shaders.pbr": "PBR · material relief",
"sanctuary.shaders.pbr_help": "Experimental: procedural block normals and roughness, sun and moon highlights. Range 32 blocks. OFF by default.",
"sanctuary.shaders.pbr_help": "Experimental: procedural block normals and roughness, sun and moon highlights. Range 32 blocks. ON by default.",
"sanctuary.shaders.pbr_intensity": "PBR strength",
"sanctuary.shaders.pbr_intensity_help": "Material relief and highlights. Zero stops rendering and frees the cache. Original textures remain unchanged.",
"sanctuary.shaders.pbr_intensity_value": "PBR: %s%%"
"sanctuary.shaders.pbr_intensity_value": "PBR: %s%%",
"sanctuary.shaders.lens_flare": "Lens flare · soft rectangles",
"sanctuary.shaders.lens_flare_help": "Soft rectangular sun and moon reflections. Obstacles hide the sources. Independent of the bloom halo.",
"sanctuary.shaders.lens_flare_intensity": "Lens flare intensity",
"sanctuary.shaders.lens_flare_intensity_help": "Celestial reflection strength. Zero disables the effect.",
"sanctuary.shaders.lens_flare_intensity_value": "Lens flare: %s %%",
"sanctuary.shaders.sunshafts": "Sunshafts",
"sanctuary.shaders.sunshafts_help": "Light beams through openings in the scenery. Independent of bloom; range follows shadows (up to 64 blocks).",
"sanctuary.shaders.sunshaft_intensity": "Sunshaft intensity",
"sanctuary.shaders.sunshaft_intensity_help": "Strength of light scattered in the air. Zero disables the effect.",
"sanctuary.shaders.sunshaft_intensity_value": "Sunshafts: %s %%"
}
@@ -2279,7 +2279,7 @@
"sanctuary.shaders.bloom": "Bloom",
"sanctuary.shaders.bloom_help": "Halo du soleil et des surfaces lumineuses. Activé par défaut. Larrêt coupe le halo et conserve les minerais émissifs. Nécessite Vanilla Light.",
"sanctuary.shaders.bloom_intensity": "Intensité du bloom",
"sanctuary.shaders.bloom_intensity_help": "Force du halo. Zéro supprime le halo et ses passes de diffusion, tout en conservant les minerais émissifs.",
"sanctuary.shaders.bloom_intensity_help": "Force du halo, 20 % par défaut. Zéro supprime le halo et ses passes de diffusion, tout en conservant les minerais émissifs.",
"sanctuary.shaders.bloom_intensity_value": "Bloom : %s %%",
"sanctuary.shaders.bloom_radius": "Diffusion du bloom",
"sanctuary.shaders.bloom_radius_help": "Taille du halo autour des pixels lumineux. Les détails des textures restent nets.",
@@ -2290,13 +2290,23 @@
"sanctuary.shaders.ore_intensity_help": "Luminosité des inclusions. Aucun changement de la lumière du monde ou des apparitions de mobs.",
"sanctuary.shaders.ore_intensity_value": "Minerais : %s %%",
"sanctuary.shaders.ssgi": "SSGI · couleurs diffusées",
"sanctuary.shaders.ssgi_help": "Expérimental : diffusion renforcée des couleurs entre surfaces visibles proches. Rayon 2,5 blocs. Les surfaces hors écran ne contribuent pas. Désactivé par défaut.",
"sanctuary.shaders.ssgi_help": "Expérimental : diffusion renforcée des couleurs entre surfaces visibles proches. Rayon 2,5 blocs. Les surfaces hors écran ne contribuent pas. Activé par défaut.",
"sanctuary.shaders.ssgi_intensity": "Intensité du SSGI",
"sanctuary.shaders.ssgi_intensity_help": "Force de la teinte diffusée. Zéro coupe le calcul et libère ses ressources. Aucun changement de lumière du monde.",
"sanctuary.shaders.ssgi_intensity_value": "SSGI : %s %%",
"sanctuary.shaders.pbr": "PBR · relief des matériaux",
"sanctuary.shaders.pbr_help": "Expérimental : normales procédurales et rugosité des blocs, reflets du soleil et de la lune. Portée 32 blocs. Désactivé par défaut.",
"sanctuary.shaders.pbr_help": "Expérimental : normales procédurales et rugosité des blocs, reflets du soleil et de la lune. Portée 32 blocs. Activé par défaut.",
"sanctuary.shaders.pbr_intensity": "Intensité du PBR",
"sanctuary.shaders.pbr_intensity_help": "Relief et reflets des matériaux. Zéro coupe le calcul et libère le cache. Ne modifie pas les textures originales.",
"sanctuary.shaders.pbr_intensity_value": "PBR : %s %%"
"sanctuary.shaders.pbr_intensity_value": "PBR : %s %%",
"sanctuary.shaders.lens_flare": "Lens flare · rectangles doux",
"sanctuary.shaders.lens_flare_help": "Reflets rectangulaires du soleil et de la lune, aux bords dégradés. Les obstacles masquent les sources. Indépendant du halo de bloom.",
"sanctuary.shaders.lens_flare_intensity": "Intensité du lens flare",
"sanctuary.shaders.lens_flare_intensity_help": "Force des reflets célestes. Zéro désactive le calcul.",
"sanctuary.shaders.lens_flare_intensity_value": "Lens flare : %s %%",
"sanctuary.shaders.sunshafts": "Rayons solaires",
"sanctuary.shaders.sunshafts_help": "Faisceaux de lumière dans les ouvertures du décor. Indépendants du bloom, portée liée aux ombres (64 blocs maximum).",
"sanctuary.shaders.sunshaft_intensity": "Intensité des rayons",
"sanctuary.shaders.sunshaft_intensity_help": "Force de la lumière dans lair. Zéro désactive le calcul.",
"sanctuary.shaders.sunshaft_intensity_value": "Rayons : %s %%"
}
@@ -0,0 +1,24 @@
#version 330
#extension GL_ARB_separate_shader_objects : require
#include <sanctuary:bloom_scene.glsl>
uniform sampler2D InputTexture;
layout(location=0) in vec2 texCoord;
layout(location=0) out vec4 fragColor;
float softBox(vec2 center,vec2 halfSize){
vec2 q=abs((texCoord-center)*vec2(FlareParams.y,1))/halfSize;
float fade=1.0-smoothstep(.08,1.0,max(q.x,q.y));
return fade*fade;
}
vec3 flare(vec4 source,vec3 light,vec3 tint){
vec2 axis=source.xy-vec2(.5);
float shape=softBox(source.xy,vec2(.26,.075))*.55;
shape+=softBox(vec2(.5)-axis*.4,vec2(.115,.085))*.35;
shape+=softBox(vec2(.5)-axis*.9,vec2(.055,.045))*.22;
return light*tint*shape;
}
void main(){
vec3 sun=texture(InputTexture,vec2(.25,.5)).rgb;
vec3 moon=texture(InputTexture,vec2(.75,.5)).rgb;
vec3 light=flare(FlareSun,sun,vec3(1,.92,.78))+flare(FlareMoon,moon,vec3(.75,.87,1));
fragColor=vec4(1.0-exp(-light*FlareParams.x*6.0),0.0);
}
@@ -0,0 +1,23 @@
#version 330
#extension GL_ARB_separate_shader_objects : require
#include <sanctuary:bloom_scene.glsl>
uniform sampler2D InputTexture;
layout(location=0) in vec2 texCoord;
layout(location=0) out vec4 fragColor;
void main(){
vec4 source=gl_FragCoord.x<1.0?FlareSun:FlareMoon;
fragColor=vec4(0.0);
if(source.w<=0.0)return;
float border=min(min(source.x,source.y),min(1.0-source.x,1.0-source.y));
float edge=smoothstep(0.0,.08,border);
if(edge<=0.0)return;
vec3 color=vec3(0);
// Two output pixels only: stable area integration, not a full-screen sampling loop.
for(int y=0;y<16;y++)for(int x=0;x<16;x++){
vec2 offset=(vec2(x,y)+.5)/16.0*2.0-1.0;
vec2 uv=source.xy+offset*source.z/vec2(FlareParams.y,1);
if(any(lessThan(uv,vec2(0)))||any(greaterThan(uv,vec2(1))))continue;
color+=texture(InputTexture,uv).rgb;
}
fragColor=vec4(color/256.0*source.w*edge,1.0);
}
@@ -4,9 +4,10 @@
layout(location=0) in vec2 texCoord;
layout(location=0) out vec4 fragColor;
void main(){
float depth=texture(WorldDepth,texCoord).r;
vec2 surface=giTexel(texCoord);float depth=texture(WorldDepth,surface).r;
if(depth<=0.0)discard;
vec3 p=giPosition(texCoord,depth),normal=giNormal(texCoord,p);
vec3 p=giPosition(surface,depth),normal=giNormal(surface,p);
float confidence=giConfidence(p,normal);if(confidence<=0.0)discard;
vec2 size=vec2(textureSize(IndirectLight,0)),coord=texCoord*size-0.5,base=floor(coord),f=fract(coord);
vec3 bounce=vec3(0.0);float total=0.0;
for(int y=0;y<2;y++)for(int x=0;x<2;x++){
@@ -27,7 +28,7 @@ void main(){
// Artistic single-bounce gain: the old gamma-space screen blend hid almost all
// of the transport on bright blocks. Compose in approximate linear light and
// encode once; a soft shoulder keeps strong bounce below channel clipping.
vec3 irradiance=bounce/total*(GISettings.x*96.0);
vec3 irradiance=bounce/total*(GISettings.x*96.0*confidence);
vec3 reflected=linearScene*irradiance;
vec3 lit=linearScene+(1.0-linearScene)*(1.0-exp(-reflected));
fragColor=vec4(sqrt(clamp(lit,0.0,1.0)),0.0);
@@ -23,7 +23,10 @@ void main(){
float travel=GISettings.z*float(step)/10.0;
vec3 probe=start+direction*travel;
vec4 clip=ViewProjection*vec4(probe,1.0);
if(clip.w<=0.0)break;
if(clip.w<=0.05)break;
float probeDepth=clip.z/clip.w;
if(GISettings.y<0.5)probeDepth=probeDepth*.5+.5;
if(probeDepth<=0.0||probeDepth>=1.0)break;
vec2 hitUV=clip.xy/clip.w*0.5+0.5;
if(any(lessThan(hitUV,vec2(0.002)))||any(greaterThan(hitUV,vec2(0.998))))break;
hitUV=giTexel(hitUV);float hitDepth=texture(WorldDepth,hitUV).r;
@@ -42,7 +45,8 @@ void main(){
// remainder instead of re-injecting bright grey ground into every nearby face.
vec3 linear=color*color;
vec3 chroma=linear-vec3(min(linear.r,min(linear.g,linear.b)));
sum+=chroma*facing*falloff*edge*giFog(hit);
float confidence=smoothstep(.05,.3,clip.w)*(1.0-smoothstep(.12,.35,behind));
sum+=chroma*facing*falloff*edge*giFog(hit)*confidence;
}
break;
}
@@ -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,44 @@
#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)*distanceFade;
}
float phase=.12+.88*pow(max(0.0,dot(ray,SunStrength.xyz)),4.0);
float weather=clamp(SunStrength.w/.38,0.0,1.0);
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;
fragColor=vec4(ShaftColor.rgb*amount,end);
}
@@ -6,6 +6,9 @@ layout(std140) uniform BloomScene {
vec4 MoonDirection;
vec4 FogDistance;
vec4 FogInfo;
vec4 FlareSun; // projected UV, sampling radius in screen-height units, visibility
vec4 FlareMoon;
vec4 FlareParams; // strength, aspect ratio, reserved, reserved
};
vec3 bloomPosition(vec2 uv,float depth) {
vec4 p=InverseViewProjection*vec4(uv*2.0-1.0,BloomParams.y>0.5?depth:depth*2.0-1.0,1.0);
@@ -12,23 +12,38 @@ vec3 giPosition(vec2 uv,float depth){
vec4 p=InverseViewProjection*vec4(uv*2.0-1.0,GISettings.y>0.5?depth:depth*2.0-1.0,1.0);
return p.xyz/p.w;
}
vec2 giTexel(vec2 uv){vec2 size=vec2(textureSize(WorldDepth,0));return (floor(uv*size)+0.5)/size;}
vec2 giTexel(vec2 uv){
vec2 size=vec2(textureSize(WorldDepth,0));
return (clamp(floor(uv*size),vec2(0),size-1.0)+0.5)/size;
}
vec3 giNeighbor(vec2 uv,vec3 center){
uv=giTexel(uv);float depth=texture(WorldDepth,uv).r;
return depth>0.0?giPosition(uv,depth):center;
}
vec3 giNormal(vec2 uv,vec3 p){
vec2 pixel=1.0/vec2(textureSize(WorldDepth,0));
vec3 l=giPosition(uv-vec2(pixel.x,0),texture(WorldDepth,uv-vec2(pixel.x,0)).r);
vec3 r=giPosition(uv+vec2(pixel.x,0),texture(WorldDepth,uv+vec2(pixel.x,0)).r);
vec3 d=giPosition(uv-vec2(0,pixel.y),texture(WorldDepth,uv-vec2(0,pixel.y)).r);
vec3 u=giPosition(uv+vec2(0,pixel.y),texture(WorldDepth,uv+vec2(0,pixel.y)).r);
vec3 dx=length(r-p)<length(p-l)?r-p:p-l;
vec3 dy=length(u-p)<length(p-d)?u-p:p-d;
vec3 n=cross(dx,dy);n/=max(length(n),0.00001);
// Keep voxel planes exact: tiny depth-reconstruction errors must not rotate an entire ray fan.
vec3 l=p-giNeighbor(uv-vec2(pixel.x,0),p),r=giNeighbor(uv+vec2(pixel.x,0),p)-p;
vec3 d=p-giNeighbor(uv-vec2(0,pixel.y),p),u=giNeighbor(uv+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;
// Normalize directions BEFORE the cross product. Pixel area approaches zero
// near a surface (or at high resolution); it is not a validity criterion.
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);
vec3 a=abs(n);
if(max(a.x,max(a.y,a.z))>.995){
n=a.x>a.y&&a.x>a.z?vec3(sign(n.x),0,0):a.y>a.z?vec3(0,sign(n.y),0):vec3(0,0,sign(n.z));
}
return dot(n,-p)<0.0?-n:n;
}
float giConfidence(vec3 p,vec3 n){
// Distance to the receiving plane, not radial distance: no circular cut in
// a flat wall. Fade before entering it; also soften nearly tangent views.
float planeDistance=abs(dot(p,n));
return smoothstep(.06,.4,planeDistance)*smoothstep(.025,.12,planeDistance/max(length(p),.0001));
}
float giFog(vec3 p){
float a=clamp((max(length(p.xz),abs(p.y))-GIFog.x)/max(GIFog.y-GIFog.x,0.001),0.0,1.0);
float b=clamp((length(p)-GIFog.z)/max(GIFog.w-GIFog.z,0.001),0.0,1.0);
@@ -0,0 +1,23 @@
layout(std140) uniform PixelShadows {
mat4 InverseViewProjection;
mat4 LightMatrix;
vec4 CameraFractionPixels;
vec4 SunStrength;
vec4 DistanceClipFog;
vec4 EnvironmentFog;
};
layout(std140) uniform ShaftParams { vec4 ShaftColor; vec4 ShaftSettings; };
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.134"
version = "beta.137"
pack-format = "packwiz:1.1.0"
[index]