Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7fada570ef | ||
|
|
b7cf4bc294 | ||
|
|
fc4a0cb68e | ||
|
|
17172fd18b | ||
|
|
216d3f75b5 | ||
|
|
e48fc656c7 | ||
|
|
7e21781a9a | ||
|
|
d941b91265 | ||
|
|
078d3e6833 | ||
|
|
afa77f3d62 | ||
|
|
534dfec4f8 | ||
|
|
26e305d15f | ||
|
|
35103a7ea8 | ||
|
|
6502883cc4 | ||
|
|
a4e96c0d99 | ||
|
|
7c63865157 | ||
|
|
00be1eb0d0 | ||
|
|
adde24aa7e |
@@ -20,13 +20,12 @@ La [vision complète](docs/vision.md) conserve les intentions ; le
|
||||
L’économie, les claims et les dimensions décrits dans la vision restent
|
||||
à implémenter.
|
||||
|
||||
La version **beta.126** ajoute le rubis et le saphir, leurs minerais et leurs
|
||||
blocs, rangés après l'émeraude. Leur génération est configurable et limitée
|
||||
aux nouveaux chunks de Sanctuary. L'argile classique du Clay Workshop conserve
|
||||
les couleurs originales des modèles. La commande `/sanctuary discovery`
|
||||
enrichit les découvertes communes, globalement ou précisément, et actualise
|
||||
les recettes et catalogues liés. Le [ticket de livraison](docs/gems-beta126.md)
|
||||
décrit les règles, la compatibilité et les vérifications.
|
||||
La version **beta.135** corrige le SSGI en contre-plongée et près des surfaces :
|
||||
normales indépendantes de la taille des pixels et atténuation progressive au
|
||||
contact. Bloom initial à **20 %**, PBR et SSGI **activés par défaut**.
|
||||
**Options → Shaders** ; réglages existants conservés.
|
||||
Le [ticket de livraison](docs/shader-beta135.md) décrit le rendu et ses limites.
|
||||
Contrôles ciblés et archives vérifiés ; livraison prête.
|
||||
La [livraison précédente](docs/clay-workshop-integration-beta110.md) réunit déjà
|
||||
l’atelier, les œufs et la neige saisonnière dans l’instance **Sanctuary Beta**.
|
||||
Les [synchronisations antérieures](docs/git-sync-beta092.md) restent documentées.
|
||||
@@ -37,6 +36,75 @@ La [carte native beta.005](docs/atlas-beta005.md) prend le relais après
|
||||
l’étude de Xaero et des alternatives libres.
|
||||
|
||||
|
||||
## beta.135 — SSGI près des surfaces
|
||||
|
||||
Correction des coupures à courte distance, sans flouter les textures.
|
||||
[Contrat et vérifications](docs/shader-beta135.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.135/Sanctuary-beta.135.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.135/Sanctuary-Test-beta.135.mrpack).
|
||||
|
||||
## beta.134 — PBR optionnel et SSGI filtré
|
||||
|
||||
Relief et reflets des matériaux ; lissage de la lumière indirecte.
|
||||
[Contrat et vérifications](docs/shader-beta134.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.134/Sanctuary-beta.134.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.134/Sanctuary-Test-beta.134.mrpack).
|
||||
|
||||
## beta.133 — SSGI renforcé
|
||||
|
||||
Couleurs voisines plus visibles, même budget de rayons et de cibles GPU.
|
||||
[Contrat et vérifications](docs/shader-beta133.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.133/Sanctuary-beta.133.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.133/Sanctuary-Test-beta.133.mrpack).
|
||||
|
||||
## beta.132 — SSGI : diffusion locale des couleurs
|
||||
|
||||
Rebond diffus des couleurs visibles, intensité réglable et reconstruction
|
||||
préservant les silhouettes. Expérimental, initialement désactivé.
|
||||
[Contrat et vérifications](docs/shader-beta132.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.132/Sanctuary-beta.132.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.132/Sanctuary-Test-beta.132.mrpack).
|
||||
|
||||
## beta.131 — Émissifs indépendants et précision étendue
|
||||
|
||||
Bloom, émissifs et highlight activés initialement ; halo facultatif et cinq
|
||||
précisions communes de 8 à 128 pixels/bloc.
|
||||
[Contrat et vérifications](docs/shader-beta131.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.131/Sanctuary-beta.131.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.131/Sanctuary-Test-beta.131.mrpack).
|
||||
|
||||
## beta.130 — Bloom, minerais émissifs et précision commune
|
||||
|
||||
Halo réglable des sources lumineuses et inclusions des vingt textures de
|
||||
minerais ; réglages par défaut 32/128/16 et ombres activées.
|
||||
[Contrat et vérifications](docs/shader-beta130.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.130/Sanctuary-beta.130.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.130/Sanctuary-Test-beta.130.mrpack).
|
||||
|
||||
## beta.129 — Reflets CTM et ombres jusqu'à 256 blocs
|
||||
|
||||
Reflets connectés réglables, cinq portées indépendantes, soleil plein à midi
|
||||
et correction du réalignement cyclique de la grille d'ombres.
|
||||
[Contrat et vérifications](docs/shader-beta129.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.129/Sanctuary-beta.129.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.129/Sanctuary-Test-beta.129.mrpack).
|
||||
|
||||
## beta.128 — Ombres stables, lune et feuillage
|
||||
|
||||
Grille de réception stabilisée, contours nets des blocs, atténuation à midi,
|
||||
ombre lunaire légère et feuillage nuancé selon son épaisseur.
|
||||
[Contrat et vérifications](docs/shader-beta128.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.128/Sanctuary-beta.128.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.128/Sanctuary-Test-beta.128.mrpack).
|
||||
|
||||
## beta.127 — Shader : ombres portées pixélisées
|
||||
|
||||
Ombres solaires natives, finesse 8/16/32 pixels par bloc et portée 16/32/64 blocs.
|
||||
Option désactivée par défaut, réglable sans rechargement des ressources.
|
||||
[Contrat et vérifications](docs/shader-beta127.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.127/Sanctuary-beta.127.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.127/Sanctuary-Test-beta.127.mrpack).
|
||||
|
||||
## beta.126 — Gemmes, argile et découvertes
|
||||
|
||||
Rubis et saphir avec filons configurables, couleurs originales avec l'argile,
|
||||
|
||||
@@ -134,3 +134,19 @@ le template reproduit exactement ses ressources exportables.
|
||||
| `Sanctuary-beta.126.mrpack` | `716221ce24549781ab91f5437ad2310497b198a4b61c43c40c67a27984365ae5` |
|
||||
| `Sanctuary-Test-beta.126.mrpack` | `bbf22d7dd72e6746538b4ec28686f699225d4a59372304e5db42140a80e88bd1` |
|
||||
| `sanctuary-beta.126.jar` | `85dbe1b41d98751029bb62e87d259522f8609758896c797b1e02fa7656ec0342` |
|
||||
|
||||
## Publication et synchronisation
|
||||
|
||||
[Release beta.126](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.126)
|
||||
publiée depuis `99860af312a1737d938812f3515fcc4acb4291b2` ; artefacts
|
||||
immuables retéléchargés et vérifiés. Canal packwiz : `31749b95d21ce5bced6b9e5759499da6f8a67e6e`.
|
||||
Deux synchronisations isolées, puis deux synchronisations de l'instance existante
|
||||
**Sanctuary Beta** ont réussi. Un seul JAR Sanctuary beta.126, correspondant
|
||||
à l'empreinte ci-dessus. Les **923 fichiers personnels** suivis sont conservés,
|
||||
sauvegardes restées fermées ; copie préalable dans
|
||||
`sanctuary-backups/before-beta.126/`.
|
||||
|
||||
Packs normal/Test et template copiés et vérifiés dans
|
||||
`sanctuary-beta/build/`. Reçus locaux : `build/gems126-artifact.json`,
|
||||
`build/gems126-isolated.json`, `build/gems126-prism.json` et
|
||||
`build/gems126-publication.log`.
|
||||
|
||||
+104
@@ -14,6 +14,110 @@ 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.134 — PBR optionnel et SSGI filtré
|
||||
|
||||
Normales procédurales en cache, rugosité/métallicité par famille et reflets
|
||||
directionnels. Filtrage SSGI et suppression de sa forte réinjection neutre.
|
||||
[Contrat et vérifications](shader-beta134.md).
|
||||
[Release beta.134](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.134)
|
||||
publiée ; canal `35ec0f6e145002f6367944df5b75e2703958eaef`. Deux synchronisations isolées,
|
||||
puis deux dans la même instance Sanctuary Beta réussies. Les 923 fichiers
|
||||
personnels suivis gardent leurs hashes. Aucun monde personnel ouvert ;
|
||||
sauvegarde préalable `sanctuary-backups/before-beta.134/`.
|
||||
Packs normal/Test/template vérifiés dans `sanctuary-beta/build/`.
|
||||
|
||||
## beta.133 — SSGI renforcé
|
||||
|
||||
Diffusion des couleurs rendue visible au réglage 35, même budget de rayons
|
||||
et cibles GPU. [Contrat et vérifications](shader-beta133.md).
|
||||
[Release beta.133](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.133)
|
||||
publiée ; canal `9ceb5a5353c9d1b8bb7834a20e818fd65096ff98`. Deux synchronisations isolées,
|
||||
puis deux dans la même instance Sanctuary Beta réussies. Les 923 fichiers
|
||||
personnels suivis gardent leurs hashes. Aucun monde personnel ouvert ;
|
||||
sauvegarde préalable `sanctuary-backups/before-beta.133/`.
|
||||
Packs normal/Test/template vérifiés dans `sanctuary-beta/build/`.
|
||||
|
||||
## beta.132 — SSGI expérimental
|
||||
|
||||
Diffusion locale des couleurs visibles, rayon 2,5 blocs, intensité réglable,
|
||||
initialement OFF. [Contrat et vérifications](shader-beta132.md).
|
||||
[Release beta.132](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.132)
|
||||
publiée ; canal `9edaaedc0205c525ecce9de66b5ca7827c56c28f`. Deux synchronisations isolées,
|
||||
puis deux dans la même instance Sanctuary Beta réussies. Les 923 fichiers
|
||||
personnels suivis gardent leurs hashes. Aucun monde personnel ouvert ;
|
||||
sauvegarde préalable `sanctuary-backups/before-beta.132/`.
|
||||
Packs normal/Test/template vérifiés dans `sanctuary-beta/build/`.
|
||||
|
||||
## beta.131 — émissifs indépendants et précision étendue
|
||||
|
||||
Bloom, émissifs et highlight ON initialement. Couper le halo ou régler son
|
||||
intensité à zéro garde les inclusions lumineuses. Précisions 64/128 ajoutées,
|
||||
défaut 16 conservé. [Contrat et vérifications](shader-beta131.md).
|
||||
[Release beta.131](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.131)
|
||||
publiée ; canal `efb18c274d95d988a9566b8c52751469f002cdda`. Deux synchronisations isolées,
|
||||
puis deux dans la même instance Sanctuary Beta réussies. Les 923 fichiers
|
||||
personnels suivis sont conservés, préférences et sauvegardes comprises.
|
||||
Aucun monde personnel ouvert ; sauvegarde `sanctuary-backups/before-beta.131/`.
|
||||
Archives normal/Test/template vérifiées dans `sanctuary-beta/build/`.
|
||||
|
||||
## beta.130 — bloom et précision commune
|
||||
|
||||
Bloom réglable, masques sélectifs des vingt textures de minerais, précision
|
||||
commune aux ombres et au highlight. Réglages initiaux 32/128/16 et Pixel Shadows
|
||||
activé ; anciennes préférences conservées. [Contrat et vérifications](shader-beta130.md).
|
||||
[Release beta.130](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.130)
|
||||
publiée ; canal `68e06353bf880af04c1c72d4e3a159f11f94b619`. Deux synchronisations isolées
|
||||
puis deux dans la même instance **Sanctuary Beta** réussies. Les 923 fichiers
|
||||
personnels suivis, réglages de shaders compris, gardent leurs hashes. Aucun
|
||||
monde personnel ouvert ; sauvegarde préalable `sanctuary-backups/before-beta.130/`.
|
||||
Packs normal/Test et template vérifiés et copiés dans `sanctuary-beta/build/`.
|
||||
|
||||
## beta.129 — reflets CTM et portées 16–256 blocs
|
||||
|
||||
Reflets connectés entre tous les matériaux coplanaires, portée indépendante,
|
||||
ombres à 128/256 blocs, intensité rétablie à midi et correction du réalignement
|
||||
cyclique de la grille lumineuse. [Contrat et vérifications](shader-beta129.md).
|
||||
[Release beta.129](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.129)
|
||||
publiée ; canal `2eaa0130a4cd475c598beefdab40f48fc8681ab7`. Deux synchronisations isolées
|
||||
puis deux dans la même instance **Sanctuary Beta** réussies. Les 923 fichiers
|
||||
personnels suivis, réglages de shaders compris, sont conservés. Aucun monde
|
||||
personnel ouvert ; copie préalable dans `sanctuary-backups/before-beta.129/`.
|
||||
Packs normal/Test et template copiés et vérifiés dans `sanctuary-beta/build/`.
|
||||
|
||||
## beta.128 — stabilité des ombres, lune et feuillage
|
||||
|
||||
Ombres des blocs nettes et stabilisées, atténuation à midi, lune légère et
|
||||
feuillage nuancé selon son épaisseur. [Contrat et vérifications](shader-beta128.md).
|
||||
[Release beta.128](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.128)
|
||||
publiée ; canal `eba01726ee132a6e548d129fa7ee04f24c880917`. Deux synchronisations isolées
|
||||
puis deux dans la même instance **Sanctuary Beta** réussies. Les 923 fichiers
|
||||
personnels suivis, réglages de shaders compris, sont conservés. Aucun monde
|
||||
personnel ouvert ; copie préalable dans `sanctuary-backups/before-beta.128/`.
|
||||
Packs normal/Test et template copiés et vérifiés dans `sanctuary-beta/build/`.
|
||||
|
||||
## beta.127 — shader et ombres pixélisées
|
||||
|
||||
Ombres solaires optionnelles intégrées à Sanctuary · Vanilla Light, avec finesse
|
||||
et portée réglables. Socle beta.126 conservé. [Contrat et vérifications](shader-beta127.md).
|
||||
[Release beta.127](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.127)
|
||||
publiée ; canal `7fd24a43e01ee497a23311e3c78e1dd3dddd877a`. Deux synchronisations
|
||||
isolées puis deux dans la même instance **Sanctuary Beta** réussies.
|
||||
Les 923 fichiers personnels suivis, réglages de shaders compris, sont conservés.
|
||||
Aucun monde personnel ouvert ; copie préalable dans `sanctuary-backups/before-beta.127/`.
|
||||
Packs normal/Test et template copiés et vérifiés dans `sanctuary-beta/build/`.
|
||||
|
||||
## beta.126 — gemmes, argile et découvertes
|
||||
|
||||
Rubis et saphir, nouvelles textures d'items, argile conservant les couleurs
|
||||
originales et commandes de découverte communes aux recettes et catalogues.
|
||||
[Contrat et vérifications](gems-beta126.md).
|
||||
[Release beta.126](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.126)
|
||||
publiée ; canal `31749b95d21ce5bced6b9e5759499da6f8a67e6e`. Deux synchronisations
|
||||
isolées puis deux dans la même instance **Sanctuary Beta** réussies.
|
||||
Les 923 fichiers personnels et réglages suivis sont inchangés ; sauvegardes
|
||||
restées fermées. Copie préalable dans `sanctuary-backups/before-beta.126/`.
|
||||
Packs normal/Test et template copiés et vérifiés dans `sanctuary-beta/build/`.
|
||||
|
||||
## beta.125 — placement, éclats et animaux sculptés
|
||||
|
||||
Coins et trois hauteurs, particules de voxels et espèces découvertes au Clay
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
# Préparation locale des ombres portées pixélisées (ancien numéro beta.122)
|
||||
|
||||
Ticket du 17 septembre 2026, branche locale `codex/pixel-shadows-beta122`.
|
||||
Cette préparation n'a jamais été publiée sous le numéro beta.122, déjà utilisé
|
||||
par une autre livraison. Elle est intégrée au socle beta.126 et livrée en
|
||||
[beta.127](shader-beta127.md). Les chemins et mesures ci-dessous décrivent
|
||||
les essais historiques dans le dossier de préparation.
|
||||
Demande : ajouter aux réglages de Sanctuary · Vanilla Light des ombres portées
|
||||
pixélisées, avec une finesse et une distance réglables.
|
||||
|
||||
## Utilisation
|
||||
|
||||
Dans **Options → Shaders**, activer **Sanctuary · Vanilla Light**, puis
|
||||
**Ombres portées pixélisées**. L'option est désactivée par défaut pour conserver
|
||||
le coût et le rendu des installations existantes. Les choix sont :
|
||||
|
||||
- finesse de **8, 16 ou 32 pixels par bloc**, défaut 16 ;
|
||||
- distance de **16, 32 ou 64 blocs**, défaut 32.
|
||||
|
||||
Ces préférences sont conservées dans `config/sanctuary-shaders.json`, avec les
|
||||
anciens champs de couleur. Leurs valeurs numériques sont ramenées aux choix
|
||||
admis au chargement. Les libellés et explications sont disponibles en FR/EN.
|
||||
Les réglages s'appliquent sans recharger les ressources.
|
||||
|
||||
## Rendu et périmètre
|
||||
|
||||
Une carte de profondeur rend les formes d'occultation des blocs solides chargés
|
||||
autour de la caméra, indépendamment de leur présence à l'écran. Les dalles et
|
||||
escaliers conservent leurs formes. Le cache est invalidé par les changements de
|
||||
blocs, les paquets de chunks et les reconstructions du moteur natif.
|
||||
La direction solaire vient du ciel natif extrait par Minecraft, et respecte
|
||||
ainsi l'angle solaire de Sanctuary. L'effet s'atténue sous la pluie, à l'horizon,
|
||||
dans la brume et à la limite de sa portée. Il s'arrête la nuit, dans les dimensions
|
||||
sans soleil, dans les fluides et lorsqu'un moteur externe suspend Vanilla Light.
|
||||
|
||||
La profondeur du monde est lue directement par le GPU après le rendu du monde,
|
||||
avant la main et l'interface. L'inverse de la projection réelle inclut les effets
|
||||
de caméra. Les points recevant l'ombre sont arrondis sur une grille ancrée dans
|
||||
le monde ; le plan de la face est conservé pour limiter les auto-ombres.
|
||||
La carte solaire utilise également une origine alignée sur ses texels.
|
||||
Le HUD, la main, les étoiles et les captures panoramiques techniques gardent leur
|
||||
chemin de rendu. Aucun niveau de lumière de gameplay, règle serveur, sauvegarde
|
||||
ou générateur n'est changé. Aucune dépendance de rendu n'est ajoutée.
|
||||
|
||||
Cette première version couvre les **formes d'occultation des blocs** : les entités
|
||||
conservent leurs ombres natives ; les feuillages ajourés, modèles non occultants,
|
||||
block entities animés, eau et verre ne projettent pas de nouvelles silhouettes.
|
||||
Les transparences et particules déjà composées peuvent recevoir l'assombrissement
|
||||
du terrain derrière elles. Il n'y a pas d'ombres de lune ou de torches, ni de
|
||||
séparation de l'éclairage local émissif dans cette passe d'assombrissement.
|
||||
|
||||
## Coût et cycle de vie
|
||||
|
||||
Les maillages sont mis en cache par section de 16³, avec au plus quatre sections
|
||||
commencées par image et un budget souple de 3 ms pour leur construction. Les
|
||||
ombres apparaissent progressivement pendant la préparation après activation ou
|
||||
déplacement. Le volume des obstacles part de la portée choisie avec une marge de huit blocs,
|
||||
puis s’étend vers le soleil sur deux fois cette portée, arrondi aux sections.
|
||||
Les obstacles au-delà de ce volume ou dans des chunks non chargés ne sont pas
|
||||
représentés. Les mises à jour de lumière seule ne reconstruisent pas les formes.
|
||||
Le cache de sommets est plafonné à 96 Mio et omet les formes pathologiques qui
|
||||
dépassent ses limites. Les textures de profondeur font entre 512 et 4 096 pixels
|
||||
de côté selon finesse et portée. Le coût exact dépend de la scène et du GPU.
|
||||
|
||||
Les ressources sont libérées à la désactivation, au changement de monde, à la
|
||||
reconstruction des ressources et à l'arrêt du client. Cette livraison est locale :
|
||||
aucun avancement du canal packwiz ni installation personnelle ne fait partie du
|
||||
ticket.
|
||||
|
||||
## Vérification
|
||||
|
||||
Validation native sur **Minecraft 26.3, Java 25, macOS / Apple M1, OpenGL 4.1**,
|
||||
dans un nouveau monde plat de développement, graine **122** :
|
||||
|
||||
- Comparaison GPU activation/désactivation sur une plateforme blanche avec
|
||||
pilier, dalle et escalier : **920 échantillons assombris**, 63 412 inchangés,
|
||||
aucun éclairci sur les 64 332 échantillons de sol retenus.
|
||||
- Pilier entièrement hors du frustum : **42 048 échantillons** restent dans
|
||||
son ombre. Le retrait du pilier restitue la lumière sur **42 048 / 42 048**
|
||||
échantillons et préserve les 23 616 échantillons déjà éclairés. Sa repose
|
||||
restitue la même ombre, sans désactiver le cache entre ces manipulations.
|
||||
- Les trois finesses et les trois portées sont appliquées en direct ; les cartes
|
||||
GPU sont redimensionnées aux dimensions attendues. Désactiver l'option libère
|
||||
la texture et ne soumet plus de passe d'ombres.
|
||||
- À minuit, activer ou désactiver l'option donne la même image et aucune passe
|
||||
solaire n'est soumise. Au retour du jour, le rendu reprend.
|
||||
- Le rechargement des ressources restitue la même image. Les préférences
|
||||
persistent à la relecture du fichier et leurs valeurs extrêmes sont bornées.
|
||||
- Menus FR/EN inspectés en capture ; les libellés tiennent dans les contrôles
|
||||
natifs à la taille de test. Les vues de finesse 8/16/32 sont conservées.
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runClientGameTest \
|
||||
-PsanctuaryClientTests=true -PsanctuaryPixelShadows122ClientTests=true \
|
||||
-PsanctuaryClientNoVsync=true -PsanctuaryQuickTests=true
|
||||
```
|
||||
|
||||
Succès en **1 min 17 s**, marqueur `PIXEL_SHADOWS122_CLIENT_PASS` dans
|
||||
`build/pixel-shadows122-client.log`. Captures inspectées dans
|
||||
`build/pixel-shadows122-evidence/final/`. Les pixels sont relus uniquement dans
|
||||
le test ; le code de rendu de production n'effectue aucune lecture CPU de GPU.
|
||||
Les premiers démarrages ont permis de corriger les identifiants de pipelines,
|
||||
la directive GLSL `#include` attendue par 26.3 et deux libellés répétés du menu.
|
||||
|
||||
La commande de livraison est :
|
||||
|
||||
```sh
|
||||
./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest
|
||||
```
|
||||
|
||||
Le serveur GameTest dédié est exclu, conformément au refus de son EULA déjà
|
||||
consigné dans les livraisons précédentes. Le test graphique utilise le serveur
|
||||
intégré de son monde neuf. Aucun monde personnel n'est ouvert.
|
||||
La construction finale **réussit en 2 min 18 s** (126 tâches), journal
|
||||
`build/pixel-shadows122-build-final.log`. Le contrôle des archives confirme
|
||||
**868 sources Java** et **1 869 classes** identiques aux sorties construites,
|
||||
les ressources GLSL/FR/EN exactes dans le JAR et le template, et le même JAR
|
||||
dans les profils packwiz normal et Test. Aucune fixture cliente n'est embarquée.
|
||||
Reçu : `build/pixel-shadows122-artifact.json`.
|
||||
|
||||
Artefacts locaux : `mods/sanctuary/build/libs/sanctuary-beta.122.jar`,
|
||||
`build/packwiz/`, `build/packwiz-test/` et `build/Sanctuary-Template-beta.122.zip`.
|
||||
SHA-256 du JAR :
|
||||
`66987681cede1d2adc1dda449078e8fa85310eb2c3fecd7a905d772ce75739c9`.
|
||||
|
||||
Pas de mesure de FPS, d'essai Windows/Vulkan, de pack Iris ni de silhouettes
|
||||
animées revendiqués. Le shader reste un assombrissement graphique appliqué au
|
||||
rendu opaque et déjà composé, avec les limites de transparence décrites plus haut.
|
||||
@@ -0,0 +1,88 @@
|
||||
# SHADER-127 — intégration des ombres portées pixélisées
|
||||
|
||||
Demande du 17 septembre 2026 : livrer le shader dans **beta.127**.
|
||||
Branche `codex/shader-beta127`, socle publié beta.126, Minecraft 26.3 / Java 25.
|
||||
|
||||
La préparation locale [des ombres pixélisées](pixel-shadows-source122.md) est
|
||||
intégrée sans écraser les changements non publiés du dossier d'origine.
|
||||
Les gemmes, dernières textures de rubis/saphir, l'argile spéciale et les
|
||||
commandes de découverte de beta.126 sont conservées.
|
||||
|
||||
## Utilisation
|
||||
|
||||
**Options → Shaders → Sanctuary · Vanilla Light → Ombres portées pixélisées**.
|
||||
L'option est désactivée par défaut ; son activation reste un choix du joueur.
|
||||
Finesse : 8/16/32 pixels par bloc ; portée : 16/32/64 blocs. Défauts : 16 et 32.
|
||||
Les paramètres FR/EN s'appliquent immédiatement et persistent dans
|
||||
`config/sanctuary-shaders.json`. La mise à jour préserve les réglages personnels.
|
||||
Le rendu est natif au mod : aucun pack Iris, téléchargement supplémentaire ou
|
||||
nouvelle dépendance de rendu.
|
||||
|
||||
## Périmètre
|
||||
|
||||
Ombres solaires des formes d'occultation des blocs chargés, incluant dalles,
|
||||
escaliers et obstacles hors écran. Carte de profondeur GPU, grille ancrée dans
|
||||
le monde, atténuation selon soleil, pluie et brume. L'effet s'arrête la nuit,
|
||||
dans les dimensions sans soleil, dans les fluides ou avec un moteur externe.
|
||||
Le HUD et la main ne passent pas dans cet effet. La lumière de gameplay,
|
||||
les règles serveur, les sauvegardes et la génération ne sont pas modifiées.
|
||||
|
||||
La préparation limite le cache à 96 Mio et sa construction à quatre sections
|
||||
par image dans un budget souple de 3 ms. L'intégration supprime seulement une
|
||||
invalidation de bord exécutée deux fois à l'identique dans le cache de terrain.
|
||||
Les formes non occultantes, feuillages ajourés, eau/verre et entités animées ne
|
||||
projettent pas de nouvelles silhouettes. Les transparences déjà composées
|
||||
peuvent recevoir l'assombrissement du terrain situé derrière elles. Pas
|
||||
d'ombres de torches/lune ni de mesure de FPS revendiquée.
|
||||
|
||||
## Vérifications et livraison
|
||||
|
||||
Client natif Minecraft 26.3 / Java 25 sur macOS, monde neuf plat, graine 122 :
|
||||
`PixelShadows122ClientChecks` réussit sur la beta.127 en **1 min 26 s**.
|
||||
Le nom de cette fixture historique est conservé pour suivre la même scène.
|
||||
|
||||
- Activation : 920 échantillons de sol assombris, 63 412 inchangés, aucun éclairci.
|
||||
- Pilier hors écran : 42 048 échantillons restent ombrés ; le retrait du pilier
|
||||
restitue la lumière sur les 42 048 et conserve les 23 616 déjà éclairés.
|
||||
La repose rétablit la même ombre sans désactiver le cache.
|
||||
- Trois finesses et trois portées, redimensionnement GPU, désactivation/libération,
|
||||
arrêt nocturne, reprise diurne et rechargement des ressources vérifiés.
|
||||
- Préférences persistantes et bornées ; captures FR/EN inspectées, sans texte
|
||||
tronqué. La scène pilier/dalle/escalier est également inspectée visuellement.
|
||||
|
||||
Logs : `build/shader127-client.log`. Captures :
|
||||
`mods/sanctuary/build/run/clientGameTest/screenshots/`.
|
||||
Le serveur GameTest dédié reste exclu conformément au refus antérieur de son
|
||||
EULA. Aucun monde personnel n'est ouvert. Aucun essai Windows/Vulkan ou FPS
|
||||
n'est revendiqué ; la validation graphique est celle du client natif local.
|
||||
|
||||
|
||||
Build `./gradlew check build assemblePack assembleTestPack
|
||||
-x :sanctuary:runGameTest` réussi en **2 min 25 s**, 126 tâches. Les sources
|
||||
Java dans les archives correspondent aux sources du ticket. La comparaison
|
||||
avec le JAR beta.126 ne trouve que **21 entrées de production modifiées**,
|
||||
toutes liées au shader, à son initialisation, ses mixins et ses libellés.
|
||||
Les règles de jeu, données, textures de gemmes, sculptures et découvertes
|
||||
sont identiques. Normal et Test contiennent le même JAR ; le template exporte
|
||||
exactement les ressources du mod. Aucune fixture de test ni monde embarqué.
|
||||
|
||||
| Artefact | SHA-256 |
|
||||
| --- | --- |
|
||||
| `Sanctuary-beta.127.mrpack` | `23fa6ca0c5d1f1033e13e5370d669491b398d5a99e13f8bd1e983f998a24a20a` |
|
||||
| `Sanctuary-Test-beta.127.mrpack` | `f185f3a743ba23cd6c024d1c1e46587706bf4f3edb5e14d7e5dee522a1ebf2c2` |
|
||||
| `sanctuary-beta.127.jar` | `3ef3fab67f4f703864a1acc9ae2395c636d206f3128d3399140eaf045a46d5f3` |
|
||||
|
||||
## Publication et synchronisation
|
||||
|
||||
[Release beta.127](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.127)
|
||||
publiée depuis `00be1eb0d0bfe042442d90255385033a7765092f` ; artefacts immuables retéléchargés
|
||||
et vérifiés. Canal packwiz : `7fd24a43e01ee497a23311e3c78e1dd3dddd877a`.
|
||||
Deux synchronisations isolées puis deux dans l'instance existante
|
||||
**Sanctuary Beta** ont réussi. Les **923 fichiers personnels** suivis,
|
||||
y compris les préférences de shaders, sont conservés. Aucun monde personnel
|
||||
ouvert ; copie préalable dans `sanctuary-backups/before-beta.127/`.
|
||||
|
||||
Les packs normal/Test et le template sont aussi copiés et vérifiés dans
|
||||
`sanctuary-beta/build/`. Reçus locaux : `build/shader127-artifact.json`,
|
||||
`build/shader127-isolated.json`, `build/shader127-prism.json` et journal de
|
||||
publication `build/shader127-publication.log`.
|
||||
@@ -0,0 +1,106 @@
|
||||
# SHADER-128 — stabilité, lune et feuillage
|
||||
|
||||
Demandes du 17 septembre 2026 : réduire le jitter des bords et à midi, conserver
|
||||
des ombres de blocs nettes et un feuillage nuancé, ajouter une ombre lunaire
|
||||
fine et une transmission
|
||||
irrégulière du feuillage selon son épaisseur. Branche `codex/moon-foliage-beta128`,
|
||||
socle publié beta.127, Minecraft 26.3 / Java 25.
|
||||
|
||||
## Résultat
|
||||
|
||||
Les faces de voxels reçoivent l'ombre sur une grille fixe ; leur plan est
|
||||
stabilisé au 1/16 de bloc et les faces inclinées conservent leur normale. La
|
||||
carte de profondeur utilise un ancrage par 16 blocs avec alignement de ses
|
||||
texels. **Les ombres des blocs restent nettes** : le créateur a écarté leur
|
||||
adoucissement après l'essai visuel. Le filtrage est réservé au feuillage.
|
||||
Le soleil continue de se déplacer : ce mouvement naturel peut toujours faire
|
||||
avancer une ombre d'une case. L'objectif est de supprimer les oscillations
|
||||
liées à la caméra, pas de figer le temps.
|
||||
|
||||
Entre une élévation solaire de 0,92 et le zénith, l'intensité diminue doucement
|
||||
jusqu'à 25 % de sa valeur habituelle : alpha maximal 0,095 au lieu de 0,38.
|
||||
La lune réutilise la même carte et son angle natif ; alpha maximal 0,10 selon
|
||||
sa phase, nul à la nouvelle lune. Horizon, pluie, portée et brume atténuent
|
||||
les deux lumières. Pas de seconde passe de carte lunaire simultanée.
|
||||
|
||||
Les blocs du tag vanilla `leaves` projettent une couverture probabiliste fixe
|
||||
sur une grille de quart de bloc. Les faces internes restent présentes : plus
|
||||
le rayon traverse de couches, moins il a de chances de passer. Il s'agit d'une
|
||||
approximation statistique de l'épaisseur, pas d'un comptage des feuilles ni
|
||||
d'une lecture des trous de leur texture. Quatre échantillons bilinéaires sur
|
||||
un quart de bloc mélangent cette couverture
|
||||
en nuances, évaluées sur la grille pixélisée. Un masque R8 distingue feuilles
|
||||
et blocs dans la même passe de profondeur, pour garder les silhouettes opaques
|
||||
nettes. Les comparaisons suivent le plan récepteur pour éviter l'auto-ombrage
|
||||
sur les surfaces obliques. Le motif ne dépend ni du temps ni de la caméra et
|
||||
ne demande aucune lecture GPU côté CPU en production.
|
||||
Les budgets du cache restent 96 Mio, quatre sections et 3 ms souples par image.
|
||||
Le feuillage ajoute des faces ; son filtrage utilise seize comparaisons de
|
||||
profondeur et, selon le cas, autant de lectures du masque. Les ombres opaques
|
||||
pleines utilisent le chemin court. Le masque prend 4 Mio à la définition
|
||||
par défaut, 16 Mio au maximum ; aucune promesse de FPS n'est faite.
|
||||
|
||||
Réglages existants et libellés FR/EN conservés/actualisés. Les ombres restent
|
||||
optionnelles et désactivées par défaut. Règles, éclairage de gameplay, mondes
|
||||
et génération inchangés. Les limitations de beta.127 concernant les entités,
|
||||
fluides, verre et autres formes non occultantes subsistent.
|
||||
|
||||
## Vérifications
|
||||
|
||||
`PixelShadows122ClientChecks`, fixture historique étendue, réussit en **1 min
|
||||
44 s** dans un monde neuf plat, graine 122, client natif macOS/OpenGL.
|
||||
|
||||
- Soleil : 920 échantillons de sol ombrés et 63 412 inchangés ; retrait/repose
|
||||
d'un pilier hors écran restitue puis rétablit son ombre.
|
||||
- Lune : ombre localisée mesurée ; arrêt des passes à la nouvelle lune.
|
||||
- Midi : ombre atténuée et **zéro échantillon de transition adoucie sur 65 664**
|
||||
dans la scène de contrôle du contour opaque.
|
||||
- Caméra : petits déplacements autour de l'ancien ancrage entier et du nouvel
|
||||
ancrage par 16 blocs, retour au point initial, petites rotations en vue
|
||||
oblique, instants avant/après midi et passage de 5999 à 6001 ticks.
|
||||
Comparaisons d'images avec tolérance de trois niveaux par canal et moins
|
||||
de 1 % d'échantillons changés pour ces petits mouvements.
|
||||
- Vue oblique : 19 637 échantillons ombrés, 46 027 inchangés sur le sol blanc.
|
||||
- Feuillage : assombrissement moyen mesuré d'environ **18,6 %** pour une couche,
|
||||
**31,7 %** pour quatre, contre **36,8 %** sous une couverture solide.
|
||||
Motif fixe dans le temps ; modification des couches invalide le cache actif.
|
||||
- Trois finesses/trois portées, désactivation/libération GPU, préférences
|
||||
persistantes, rechargement des ressources, textes FR/EN et captures inspectés.
|
||||
|
||||
Commande native : `./gradlew :sanctuary:runClientGameTest
|
||||
-PsanctuaryClientTests=true -PsanctuaryPixelShadows122ClientTests=true
|
||||
-PsanctuaryClientNoVsync=true -PsanctuaryQuickTests=true` sous Java 25.
|
||||
Logs : `build/shader128-client.log` ; captures sous
|
||||
`mods/sanctuary/build/run/clientGameTest/screenshots/`.
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack
|
||||
-x :sanctuary:runGameTest` réussit en **2 min 24 s**, 126 tâches.
|
||||
Les sources archivées correspondent au ticket. **13 entrées de production**
|
||||
diffèrent de beta.127, limitées aux classes du shader, GLSL et libellés FR/EN.
|
||||
Gemmes, sculptures, découvertes, données et règles de jeu restent identiques.
|
||||
Normal et Test embarquent le même JAR ; le template correspond exactement aux
|
||||
ressources exportables. Aucun monde ni fixture de test embarqué.
|
||||
Le serveur GameTest dédié reste exclu conformément au refus antérieur de son
|
||||
EULA. Aucun monde personnel utilisé. Aucun essai Windows/Vulkan revendiqué.
|
||||
|
||||
## Artefacts vérifiés
|
||||
|
||||
| Artefact | SHA-256 |
|
||||
| --- | --- |
|
||||
| `Sanctuary-beta.128.mrpack` | `84125b18366afde52c8cba4bc7007257b1c3d8b79d413338abe64d0e0ae8feaf` |
|
||||
| `Sanctuary-Test-beta.128.mrpack` | `6bfb10d8d63875bfb84e813d4f0a9faf5db30902cc0400deda0993c08b86e597` |
|
||||
| `sanctuary-beta.128.jar` | `c54cbae7efaa7059c8fd4d28801087983379958fe34eedff51682d106d649c6a` |
|
||||
|
||||
## Publication et synchronisation
|
||||
|
||||
[Release beta.128](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.128)
|
||||
publiée depuis `a4e96c0d99c976231d7ca92b1db6bdb6aa7083c6` ; les artefacts immuables ont été
|
||||
retéléchargés et vérifiés. Canal packwiz : `eba01726ee132a6e548d129fa7ee04f24c880917`.
|
||||
Deux synchronisations isolées puis deux dans l'instance existante
|
||||
**Sanctuary Beta** ont réussi. Les **923 fichiers personnels** suivis,
|
||||
y compris les réglages de shaders, sont conservés. Aucun monde personnel
|
||||
ouvert ; copie préalable dans `sanctuary-backups/before-beta.128/`.
|
||||
Packs normal/Test et template copiés et vérifiés dans `sanctuary-beta/build/`.
|
||||
|
||||
Reçus locaux : `build/shader128-artifact.json`, `build/shader128-isolated.json`,
|
||||
`build/shader128-prism.json` ; journal : `build/shader128-publication.log`.
|
||||
@@ -0,0 +1,119 @@
|
||||
# SHADER-129 — arêtes connectées et portées étendues
|
||||
|
||||
Demandes du 17 septembre 2026 : highlight optionnel, puis raccordement CTM de
|
||||
tous les blocs coplanaires, portée indépendante 16–256 blocs, ombres à 128/256,
|
||||
suppression de l'atténuation au zénith et du réalignement cyclique des ombres.
|
||||
Branche `codex/edge-highlights-beta129`, socle beta.128, Minecraft 26.3 / Java 25.
|
||||
|
||||
## Résultat
|
||||
|
||||
Dans **Options → Shaders → Sanctuary · Vanilla Light**, les reflets sur les
|
||||
arêtes sont désactivés par défaut. L'intensité va de 0 à 100 ; la valeur 30
|
||||
éclaircit au maximum de 12 %. Leur portée se règle indépendamment des ombres :
|
||||
**16, 32, 64, 128 ou 256 blocs**. Les anciens réglages restent conservés.
|
||||
|
||||
Le highlight raccorde les surfaces plates, y compris entre matériaux différents,
|
||||
sans dessiner les jonctions intérieures. Il suit les contours visibles du relief,
|
||||
y compris les dalles et escaliers. Il multiplie la couleur existante pour garder
|
||||
les pixels des textures et composer avec les ombres. Ce CTM géométrique utilise
|
||||
la profondeur déjà produite par Minecraft : aucune liste de blocs autorisés,
|
||||
aucun pack CTM à fournir, aucune exploration CPU du volume de 256 blocs.
|
||||
Une cassure/repose modifie le contour dès que le terrain natif est réaffiché.
|
||||
|
||||
Le trait proche suit la grille de 1/16 de bloc. À distance, la couverture devient
|
||||
partielle pour limiter le scintillement des traits plus petits qu'un pixel.
|
||||
La portée choisie s'efface progressivement dans son dernier quart et respecte
|
||||
la brume ainsi que la distance de rendu. Une passe plein écran, quatre sondes
|
||||
de profondeur et un tampon uniforme de 176 octets ; aucun retour GPU vers CPU
|
||||
en production. L'arrêt de l'option ou une intensité nulle libèrent ce tampon.
|
||||
|
||||
Ce n'est pas un remplacement des textures par des variantes CTM. Le calcul
|
||||
concerne les faces visibles alignées sur la grille : les faces hors écran,
|
||||
les surfaces transparentes absentes de la profondeur et les géométries
|
||||
arbitrairement inclinées ne sont pas reconstruites. À très grande distance,
|
||||
les détails plus petits qu'un pixel peuvent rester imperceptibles. Aucun
|
||||
chargement de chunk forcé, aucune promesse de FPS.
|
||||
|
||||
Les ombres proposent les cinq mêmes portées. La carte reste plafonnée à
|
||||
4096 × 4096 : les grandes distances réduisent sa précision spatiale. Le cache
|
||||
garde ses limites de 96 Mio, quatre sections et 3 ms souples de construction
|
||||
par image ; la réconciliation des chunks est limitée aux ticks et changements
|
||||
de caméra/terrain plutôt qu'à toutes les images.
|
||||
|
||||
L'intensité solaire reste pleine au zénith. Les contours opaques restent nets,
|
||||
le filtrage du feuillage et les ombres légères de la lune sont conservés.
|
||||
Le réalignement de la grille lumineuse a lieu au changement d'ancrage de la
|
||||
caméra, avec transfert de phase par texels entiers. La rotation du soleil ne
|
||||
réarrondit plus la projection de l'origine mondiale à chaque image, source
|
||||
de sauts périodiques amplifiés aux grandes coordonnées. Le soleil continue
|
||||
d'avancer ; une ombre pixélisée peut naturellement changer de case. Cette
|
||||
correction ne modifie pas l'horloge serveur ni sa cadence native.
|
||||
|
||||
Textes FR/EN. Aucun changement de règles, sauvegardes, génération, gemmes,
|
||||
sculptures ou découvertes.
|
||||
|
||||
## Vérifications et livraison
|
||||
|
||||
Le client natif réussit en **2 min 41 s**, dans un monde neuf plat, graine 122,
|
||||
sur macOS / Apple M1 / OpenGL :
|
||||
|
||||
- CTM : 596 échantillons éclaircis sur le contour et 65 068 conservés ; les
|
||||
20 centres et 20 jonctions coplanaires ciblés restent intacts, y compris
|
||||
au changement de matériau. L'intensité 90 produit environ trois fois le gain.
|
||||
- Ouverture d'un trou : 716 échantillons éclaircis ; la fermeture restitue
|
||||
l'image connectée. Au-delà de 16 blocs, le réglage 16 supprime le contour ;
|
||||
le réglage 64 le rétablit (254 échantillons éclaircis).
|
||||
- Cinq portées CTM et cinq portées d'ombres, persistance/clamp, migration des
|
||||
anciennes préférences, arrêt/libération, rechargement, caméra et vue oblique
|
||||
avec ombres. Captures françaises/anglaises inspectées ; suppression ensuite
|
||||
d'un préfixe redondant dans le libellé de portée.
|
||||
- Soleil, lune, nouvelle lune, ombres opaques nettes à midi et transmission
|
||||
du feuillage restent vérifiés. Casters hors écran et retrait/repose actifs.
|
||||
- 2 000 petits pas solaires aux coordonnées ±100 000 vérifient l'absence de
|
||||
saut de translation de la carte ; changement d'ancrage par texels entiers.
|
||||
La séquence du vrai service realtime suit 294 images : angle avancé de
|
||||
0,000694 radian, aucun rebond dans les 65 664 échantillons de sol suivis.
|
||||
Sur cette courte séquence, aucune case opaque n'a changé ; cela ne constitue
|
||||
pas une mesure de mouvement visible sur une journée entière.
|
||||
|
||||
La fixture initiale capturait parfois le terrain avant la fin de sa construction
|
||||
native : elle attend désormais la réception de toute la plateforme et la fin
|
||||
compilée des sections. Aucun changement de gameplay pour contourner ce problème.
|
||||
|
||||
Commande : `./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true
|
||||
-PsanctuaryPixelShadows122ClientTests=true -PsanctuaryClientNoVsync=true
|
||||
-PsanctuaryQuickTests=true`. Log `build/shader129-client.log`, captures sous
|
||||
`mods/sanctuary/build/run/clientGameTest/screenshots/`.
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
réussit en **2 min 17 s**, 126 tâches. Une dernière passe d’assemblage intègre
|
||||
la simplification du libellé de portée (2 min 18 s). Les archives sont vérifiées :
|
||||
**17 entrées de production** diffèrent de beta.128, limitées au shader et à ses
|
||||
interfaces. Même JAR dans normal/Test, sources archivées conformes, ressources
|
||||
du template exactes, assets des gemmes inchangés. Aucun monde ni test embarqué.
|
||||
Le serveur GameTest dédié
|
||||
reste exclu conformément au refus antérieur de son EULA. Aucun monde personnel
|
||||
utilisé pour les essais, aucun résultat Windows/Vulkan revendiqué.
|
||||
|
||||
|
||||
Archives vérifiées dans `build/` et recopiées dans le dossier de livraison
|
||||
habituel `sanctuary-beta/build/` :
|
||||
|
||||
- Normal : 10 353 946 octets, SHA-256
|
||||
`2c7735194faab8a699004e69ad543ce6e8fabdebe297b759bffd31fea2dcc2e8`.
|
||||
- Test : 10 372 872 octets, SHA-256
|
||||
`cca572ee313d1d7e66ed745bb48bac62490998760d9b7bbfc7a1f6d60c430dce`.
|
||||
- JAR Sanctuary :
|
||||
`ca3e2ff4e0717a3a54c1d57478d43aade39d001b71c4eec745d76f010f7def54`.
|
||||
|
||||
[Release beta.129](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.129)
|
||||
publiée depuis `35103a7ea8635dc4ed26293f00e86d99aaacfca8`. Canal packwiz
|
||||
`2eaa0130a4cd475c598beefdab40f48fc8681ab7`. Deux synchronisations isolées puis deux synchronisations
|
||||
dans l'instance **Sanctuary Beta** ont réussi. Les **923 fichiers personnels**
|
||||
suivis, dont les réglages de shader, sont inchangés. Aucun monde personnel ouvert.
|
||||
Sauvegarde préalable des fichiers gérés : `sanctuary-backups/before-beta.129/`.
|
||||
Le JAR installé a le hash vérifié ci-dessus. Reçus locaux :
|
||||
`build/shader129-artifact.json`, `build/shader129-isolated.json` et
|
||||
`build/shader129-prism.json`. Le checkout initial avec ses modifications en
|
||||
cours a été préservé ; seule la copie des archives versionnées rejoint son
|
||||
répertoire `build/`.
|
||||
@@ -0,0 +1,131 @@
|
||||
# SHADER-130 — bloom et précision commune
|
||||
|
||||
Demandes du 17 septembre 2026 : bloom réglable du soleil et des sources
|
||||
lumineuses ; inclusions émissives du fer, cuivre, redstone, lapis, diamant,
|
||||
émeraude, rubis, saphir, quartz et or ; réglage commun « Précision » pour
|
||||
les ombres et le highlight. Branche `codex/bloom-beta130`, socle beta.129,
|
||||
Minecraft 26.3 / Java 25.
|
||||
|
||||
## Résultat
|
||||
|
||||
Dans **Options → Shaders → Sanctuary · Vanilla Light**, le bloom possède
|
||||
un interrupteur, une intensité et une diffusion réglables de 0 à 100.
|
||||
Les minerais ont leur propre interrupteur et intensité. Le bloom reste
|
||||
optionnel, désactivé initialement, avec une intensité préparée à 35, une
|
||||
diffusion à 50 et une émission des minerais à 50. Une intensité globale
|
||||
nulle arrête les passes et libère les ressources de cet effet.
|
||||
|
||||
Les nouveaux réglages par défaut sont **Pixel Shadows activé**, distance
|
||||
des ombres **128 blocs**, highlight **32 blocs** et **Précision 16 pixels
|
||||
par bloc**. La précision unique propose 8, 16 et 32 et contrôle simultanément
|
||||
ombres et arêtes. La clé JSON historique `shadowPixels` est conservée :
|
||||
les préférences déjà personnalisées ne sont pas écrasées.
|
||||
|
||||
Les vingt textures de minerais sont analysées depuis les ressources actives.
|
||||
La palette de pierre, de deepslate ou de netherrack est comparée aux pixels
|
||||
colorés des inclusions, avec un traitement du quartz et de l'or du Nether.
|
||||
Les reflets clairs accolés aux inclusions sont conservés. Le masque est
|
||||
calculé une fois par texture et conservé en cache jusqu'au rechargement des
|
||||
ressources. Une texture optionnelle `<nom>_e.png` peut préciser ce masque
|
||||
pour une texture candidate ; sa luminance maximale multipliée par l'alpha
|
||||
remplace la détection automatique. Les couleurs affichées viennent toujours
|
||||
de l'atlas Minecraft actif, y compris son animation.
|
||||
|
||||
Les sources sont les faces visibles des modèles de blocs lumineux, la lave,
|
||||
le soleil et la lune. Les lampes éteintes ne produisent pas de halo. Le
|
||||
niveau lumineux natif pondère les sources ordinaires ; l'émission des minerais
|
||||
est purement graphique. La profondeur du terrain élimine les inclusions
|
||||
cachées et masque le soleil derrière les constructions. Le cœur émissif
|
||||
conserve ses pixels nets, tandis qu'un halo séparé est diffusé à résolution
|
||||
réduite puis composé sur l'image.
|
||||
|
||||
Aucun niveau de lumière serveur, spawn, règle de jeu, format de sauvegarde,
|
||||
identifiant ou paramètre de génération ne change. Aucun monde personnel
|
||||
n'est ouvert pour les essais.
|
||||
|
||||
## Coût et limites
|
||||
|
||||
La géométrie suit uniquement les sections natives visibles et déjà chargées.
|
||||
Leur palette permet d'écarter rapidement les sections sans source ; le cache
|
||||
invalide les sections touchées lors des modifications. Construction répartie
|
||||
sur les images, au plus quatre sections démarrées et un budget souple de
|
||||
3 ms, cache GPU plafonné à 32 Mio et 16 384 faces par section. Une section
|
||||
qui dépasse les limites est omise. Aucun chargement de chunk forcé.
|
||||
|
||||
Le masque GPU est plafonné à 256 textures, chacune échantillonnée au plus en
|
||||
64 × 64 ; les textures natives 16 × 16 conservent leurs texels. Les masques
|
||||
animés utilisent leur première image, tandis que la couleur reste animée.
|
||||
La carte d'émission est à la résolution de l'écran ; la diffusion séparable
|
||||
utilise deux cibles flottantes à 1/8 de largeur et de hauteur, sans retour
|
||||
GPU vers CPU en production. Les allocations sont libérées à l'arrêt, au
|
||||
rechargement et à la déconnexion, et recréées au changement de taille.
|
||||
|
||||
Cette première version ne reconstruit pas les sources hors écran, les
|
||||
particules, les entités lumineuses ni les modèles dessinés exclusivement par
|
||||
un renderer de block entity. Les packs très différents peuvent demander un
|
||||
masque explicite. Les silhouettes des liquides sont approximées puis
|
||||
masquées par la profondeur native. Aucune promesse de FPS ni validation
|
||||
Windows/Vulkan ; les essais graphiques portent sur macOS / Apple M1 / OpenGL.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Le test client ciblé réussit en **1 min 29 s**, dans un nouveau monde plat,
|
||||
graine 122. Il analyse les vingt textures, refuse les pixels gris de leur
|
||||
roche, mesure le gain nocturne et les variations réelles d'intensité/diffusion.
|
||||
Couvrir les minerais de pierre supprime toute source émissive. Retirer les
|
||||
blocs rétablit l'émission ; le rechargement des ressources fait de même.
|
||||
Torche, glowstone, lampe alimentée, lave et soleil sont vérifiés, ainsi que
|
||||
l'arrêt de la lampe et l'occultation du soleil par de la pierre. Les valeurs
|
||||
par défaut et la conservation d'anciens choix personnalisés passent.
|
||||
|
||||
Log ciblé : `build/shader130-client-bloom.log` ; masques inspectables dans
|
||||
`mods/sanctuary/build/run/clientGameTest/bloom130-ore-masks.png` et son CSV.
|
||||
La suite shader complète réussit en **3 min 16 s** : casters hors écran,
|
||||
retrait/repose, trois précisions, cinq portées, lune, zénith, feuillage,
|
||||
realtime, caméra et rechargement restent vérifiés. Les arêtes CTM restent
|
||||
raccordées entre matériaux ; 1 295 échantillons du contour sont éclaircis
|
||||
à précision 8, 596 à 16 et 120 à 32. Les captures françaises/anglaises et
|
||||
le rendu des minerais/du soleil sont inspectés.
|
||||
|
||||
Commandes natives : `./gradlew :sanctuary:runClientGameTest
|
||||
-PsanctuaryClientTests=true -PsanctuaryPixelShadows122ClientTests=true
|
||||
-PsanctuaryClientNoVsync=true -PsanctuaryQuickTests=true`. Le test ciblé utilise
|
||||
`-PsanctuaryBloom130ClientTests=true` à la place de la propriété PixelShadows.
|
||||
Log complet : `build/shader130-client-full.log` ; captures sous
|
||||
`mods/sanctuary/build/run/clientGameTest/screenshots/`.
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
réussit en **2 min 24 s**, 126 tâches. Le serveur GameTest dédié reste exclu
|
||||
conformément au refus antérieur de son EULA. La comparaison au binaire beta.129
|
||||
trouve **24 entrées de production** modifiées, toutes limitées au shader et à
|
||||
ses interfaces. Gameplay et textures des gemmes inchangés ; aucun test,
|
||||
monde ou modèle GLB embarqué. Les sources archivées correspondent aux sources
|
||||
Java, les packs normal/Test embarquent le même JAR et le template contient
|
||||
exactement les ressources prévues.
|
||||
|
||||
Archives vérifiées dans `build/` et recopiées dans le dossier habituel
|
||||
`sanctuary-beta/build/` :
|
||||
|
||||
|
||||
- `Sanctuary-beta.130.mrpack` : 10386021 octets, SHA-256
|
||||
`7a3fcd4e496fa136eb3e0896fa7b856ae91771123186fa8df13c8fb1e0cf3e9a`.
|
||||
|
||||
- `Sanctuary-Test-beta.130.mrpack` : 10404944 octets, SHA-256
|
||||
`8de094fcbc58a300cc24d64c8d1b7931fdb8db354c75defa6bf2a2aa4a123a23`.
|
||||
|
||||
JAR : `1ac4d1b1339361fb665f03ca3bd359163db5037ffd90e684c5a461759f6076e7`.
|
||||
|
||||
[Release beta.130](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.130)
|
||||
publiée depuis `534dfec4f88169a200702b50d1ba87d85c107bee`. Canal packwiz
|
||||
`68e06353bf880af04c1c72d4e3a159f11f94b619`. Les téléchargements publics sont vérifiés par SHA-256.
|
||||
Deux synchronisations isolées, puis deux dans la même instance **Sanctuary
|
||||
Beta**, réussissent. Un seul JAR Sanctuary beta.130 est actif. Les
|
||||
**923 fichiers personnels suivis** conservent leurs hashes, dont les mondes,
|
||||
packs et préférences de shaders. Sauvegarde préalable dans
|
||||
`sanctuary-backups/before-beta.130/` de l'instance. Aucun monde personnel ouvert.
|
||||
|
||||
Reçus locaux ignorés : `build/shader130-artifact.json`,
|
||||
`build/shader130-isolated.json`, `build/shader130-prism.json` et
|
||||
`build/shader130-publication.log`. Le checkout original conserve ses
|
||||
modifications préexistantes ; seuls les trois artefacts versionnés vérifiés
|
||||
sont recopiés dans son dossier `build/`.
|
||||
@@ -0,0 +1,91 @@
|
||||
# SHADER-131 — émissifs indépendants et réglages initiaux
|
||||
|
||||
Demande : activer par défaut bloom, minerais émissifs et highlight ; garder
|
||||
les émissifs avec halo coupé ou intensité du bloom à zéro, sans bouton ajouté ;
|
||||
ajouter 64 et 128 pixels/bloc en options. Précision initiale 16, distances
|
||||
highlight 32 et ombres 128 conservées. Préférences personnalisées préservées.
|
||||
Branche `codex/emissive-beta131`, socle beta.130, Minecraft 26.3 / Java 25.
|
||||
|
||||
Le futur effet demandé est une légère teinte diffusée depuis les blocs
|
||||
voisins, pas des reflets reconnaissables. Étude seulement pour ce ticket ;
|
||||
aucun SSR ou éclairage indirect expérimental activé.
|
||||
|
||||
## Comportement et validation
|
||||
|
||||
Le curseur à zéro et l'interrupteur Bloom coupent uniquement le halo.
|
||||
Le réglage existant des minerais contrôle leurs inclusions nettes séparément.
|
||||
Sans halo, les cibles de diffusion GPU sont libérées ; l'émission garde
|
||||
seulement les ressources nécessaires. Tout désactiver libère l'ensemble.
|
||||
La grille commune propose 8, 16, 32, 64 et 128, sans augmenter le plafond
|
||||
4096 de la carte d'ombres. Libellés FR/EN et essais clients natifs sur les
|
||||
inclusions, l'absence de halo, les transitions et les nouvelles précisions.
|
||||
Aucun changement de lumière serveur, de génération ou de sauvegardes.
|
||||
|
||||
Le cœur émissif et le halo utilisent maintenant deux chemins indépendants.
|
||||
Les textures et leur analyse restent celles de beta.130. Avec le halo coupé,
|
||||
la carte d'émission est effacée avant les faces, le cœur net est composé,
|
||||
et les passes de ciel, réduction, diffusion et application du halo sont
|
||||
omises. Le retour au bloom recrée ses cibles. Aucun bouton ajouté, aides
|
||||
FR/EN adaptées et choix explicitement désactivés toujours conservés.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Suite client native réussie en **3 min 46 s**, monde plat neuf de graine 122,
|
||||
macOS / Apple M1 / OpenGL. Cinq précisions sauvegardées et chargées, bornes
|
||||
et seuils de normalisation, carte d'ombres plafonnée et arêtes CTM rendues
|
||||
à 64/128. Le gain mesuré des traits diminue de 0,02778 à précision 32 à
|
||||
0,02094 à 64, puis 0,01013 à 128 dans la même scène.
|
||||
|
||||
Les inclusions restent nettes sans halo : le rendu Bloom OFF correspond
|
||||
au rendu intensité zéro. Aucun gain mesuré hors du masque source, ressources
|
||||
de diffusion absentes ; couper aussi les émissifs libère l'ensemble et
|
||||
restitue l'image initiale. Réactiver le bloom restitue son image précédente.
|
||||
Captures OFF/zéro inspectées. Valeurs initiales ON vérifiées ; un ancien
|
||||
choix explicitement OFF reste OFF. Les essais beta.130 (vingt masques,
|
||||
occultation, sources, rechargement), lune, feuillage et realtime passent.
|
||||
|
||||
Le premier essai rencontrait une assertion historique attendant le highlight
|
||||
OFF lorsqu'une clé était absente. Elle attend désormais la nouvelle valeur
|
||||
ON ; les cas de préférences explicitement désactivées restent testés.
|
||||
|
||||
Commande : `./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true
|
||||
-PsanctuaryPixelShadows122ClientTests=true -PsanctuaryClientNoVsync=true
|
||||
-PsanctuaryQuickTests=true`. Log : `build/shader131-client-full.log` ; captures
|
||||
sous `mods/sanctuary/build/run/clientGameTest/screenshots/`.
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
réussit en **2 min 30 s**, 126 tâches. Les archives normal/Test, leurs sources
|
||||
et le template sont vérifiés. Six entrées de production changent : moteur
|
||||
Bloom, préférences, interface et libellés. Gameplay, shaders GLSL et textures
|
||||
restent identiques à beta.130. Aucun résultat Windows/Vulkan
|
||||
revendiqué ; le serveur GameTest dédié reste exclu conformément au refus
|
||||
antérieur de son EULA.
|
||||
|
||||
## Piste suivante — diffusion des couleurs
|
||||
|
||||
Choix confirmé : légère teinte des surfaces voisines. Une estimation SSGI
|
||||
utilise la couleur et la profondeur visibles pour des rebonds diffus ; voir
|
||||
la [documentation primaire Unity](https://docs.unity3d.com/Packages/com.unity.render-pipelines.high-definition@17.0/manual/Override-Screen-Space-GI.html).
|
||||
Proposition à expérimenter : faible intensité, courte portée autour des
|
||||
surfaces, calcul réduit, rejet par profondeur pour ne pas teinter au travers
|
||||
des murs. Vérifier les bords de l'écran, les blocs hors champ, la stabilité
|
||||
en mouvement et les textures avant de fixer une qualité. Cette piste n'est
|
||||
pas implémentée dans beta.131.
|
||||
|
||||
## Livraison
|
||||
|
||||
- `Sanctuary-beta.131.mrpack` : 10386312 octets ; SHA-256 `e02067e424bf4f54512d5cfb1b5cde0fe232aa6bdadc0d38981cd15817a6387a`.
|
||||
- `Sanctuary-Test-beta.131.mrpack` : 10405238 octets ; SHA-256 `298387cce5a5b7cdc09b3a9d24859b674a736b25abcbafa7995b813757641265`.
|
||||
|
||||
JAR : `87210da23f7781efccdbdbf5d4e2d3c1ff1857a7f326027b93d6a301c7dfdb0d`.
|
||||
|
||||
Archives vérifiées recopiées dans `sanctuary-beta/build/`. [Release beta.131](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.131)
|
||||
publiée depuis `078d3e6833daa7de7f6c6d0e1a6c17d5dbefe075`. Canal packwiz
|
||||
`efb18c274d95d988a9566b8c52751469f002cdda`. Téléchargements publics vérifiés par hash.
|
||||
Deux synchronisations isolées, puis deux dans la même instance **Sanctuary Beta**
|
||||
réussissent. Un seul JAR Sanctuary beta.131 ; les **923 fichiers personnels
|
||||
suivis** conservent leurs hashes, dont les préférences de shaders et les mondes.
|
||||
Sauvegarde préalable `sanctuary-backups/before-beta.131/` ; aucun monde personnel
|
||||
ouvert. Reçus ignorés : `build/shader131-artifact.json`,
|
||||
`build/shader131-isolated.json`, `build/shader131-prism.json` et
|
||||
`build/shader131-publication.log`. Le checkout original conserve ses changements.
|
||||
@@ -0,0 +1,106 @@
|
||||
# SHADER-132 — diffusion locale SSGI expérimentale
|
||||
|
||||
Demande : légère teinte diffusée des surfaces voisines, sans reflets miroir
|
||||
ni shader d'eau. Branche `codex/ssgi-beta132`, socle beta.131, Minecraft 26.3 /
|
||||
Java 25. Aucun changement de génération, sauvegarde ou lumière serveur.
|
||||
|
||||
## Résultat
|
||||
|
||||
**Options → Shaders → SSGI · couleurs diffusées** active un rebond diffus
|
||||
local. L'effet expérimental est initialement OFF, avec une intensité préparée
|
||||
à **35 %** ; le curseur propose 0–100. À zéro ou OFF, ses cibles et uniformes
|
||||
GPU sont libérés. Les autres effets et leurs préférences sont conservés.
|
||||
|
||||
La couleur d'une surface visible peut teinter les surfaces proches qui lui
|
||||
font face. Le calcul porte à **2,5 blocs** du récepteur et s'estompe entre
|
||||
24 et 32 blocs de la caméra. Il utilise une copie de la couleur native après
|
||||
les ombres, avec la profondeur et des normales reconstruites. Le premier
|
||||
obstacle arrête chaque rayon, même si son épaisseur empêche de retenir une
|
||||
intersection. Les plans coplanaires ne s'éclairent pas eux-mêmes.
|
||||
|
||||
Douze directions hémisphériques fixes, au maximum dix étapes par rayon,
|
||||
sont évaluées à demi-résolution en largeur et hauteur. La reconstruction
|
||||
plein écran est guidée par la profondeur pour conserver les silhouettes et
|
||||
les détails des textures. Le cœur des émissifs, le highlight et le bloom
|
||||
sont composés ensuite : pas de halo réinjecté dans le rebond ni de boucle
|
||||
de rétroaction d'une image sur l'autre. Aucune lecture GPU vers CPU ni
|
||||
recherche de chunks en production. Pas de bruit temporel ou d'historique.
|
||||
|
||||
Deux cibles, RGBA8 plein écran et RGBA16 flottant à demi-résolution, soit
|
||||
environ six octets par pixel de l'écran, plus 176 octets d'uniformes par
|
||||
segment du tampon natif. Redimensionnement, arrêt, déconnexion et
|
||||
rechargement libèrent/recréent les ressources possédées. Ce budget n'est
|
||||
pas une garantie de FPS ; le SSGI coûte davantage qu'un simple réglage de
|
||||
colorimétrie.
|
||||
|
||||
## Limites de cette version
|
||||
|
||||
L'estimation utilise seulement la couleur et la profondeur visibles. Les
|
||||
blocs hors champ ou cachés n'apportent pas leur couleur. Le résultat peut
|
||||
changer lorsqu'ils entrent ou sortent de l'image ; une atténuation près
|
||||
des bords limite la coupure. Les surfaces transparentes absentes de la
|
||||
profondeur ne sont pas reconstruites. Petits objets et intersections plus
|
||||
minces que l'échantillonnage peuvent manquer. Il s'agit d'une approximation
|
||||
diffuse locale, pas d'une simulation complète d'éclairage global ni de
|
||||
reflets spéculaires. Aucune modification de l'eau et aucune source hors
|
||||
champ déduite ou générée.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Test ciblé natif réussi en **1 min** sur macOS / Apple M1 / OpenGL, nouveau
|
||||
monde plat de graine 122. À intensité 90, les échantillons du sol blanc
|
||||
montrent un gain rouge cumulé de 8 968 contre 20 bleu devant le mur rouge,
|
||||
puis 9 168 bleu contre 23 rouge devant le mur bleu. Le sol seul est inchangé.
|
||||
OFF/zéro restituent l'image de base et libèrent les cibles ; intensité 20
|
||||
réduit le gain. Changer la couleur du mur complètement occulté laisse
|
||||
l'image inchangée. Rechargement, caméra fixe, mouvement infinitésimal et
|
||||
retour à la caméra d'origine passent, sans accumulation de couleur.
|
||||
|
||||
Deux ajustements de fixture ont précédé ce résultat : inclure dans les
|
||||
mesures le blanc légèrement bleuté par l'éclairage natif, et formater les
|
||||
petits angles de `/tp` en décimal car la commande refuse la notation
|
||||
scientifique. Aucun comportement du jeu modifié pour ces corrections.
|
||||
|
||||
Log ciblé : `build/shader132-client-ssgi.log`. La suite complète réussit en
|
||||
**4 min 18 s** et vérifie aussi une fenêtre 961 × 541, le retour à sa taille
|
||||
initiale, le SSGI avec ombres/highlight/bloom/émissifs actifs, ainsi que
|
||||
l'arrêt global du shader. Les captures françaises et anglaises passent.
|
||||
Les contrôles précédents des cinq précisions, ombres, lune, feuillage, CTM,
|
||||
bloom et émissifs restent réussis. Log : `build/shader132-client-full.log`.
|
||||
|
||||
Commandes : `./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true
|
||||
-PsanctuaryPixelShadows122ClientTests=true -PsanctuaryClientNoVsync=true
|
||||
-PsanctuaryQuickTests=true`. Pour le test ciblé, remplacer la propriété
|
||||
PixelShadows par `-PsanctuarySsgi132ClientTests=true`.
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
réussit en **2 min 25 s**, 126 tâches. Les packs normal/Test contiennent le
|
||||
même JAR ; sources archivées et template de ressources correspondent aux
|
||||
sources. Comparaison avec beta.131 : **11 entrées de production** modifiées,
|
||||
limitées au SSGI, à ses options et à ses points d'appel. Gameplay et textures
|
||||
des gemmes inchangés, aucun monde ni test embarqué. Aucun résultat
|
||||
Windows/Vulkan revendiqué.
|
||||
Le serveur GameTest dédié reste exclu conformément au refus antérieur de
|
||||
son EULA. Les captures panoramiques et les milieux de brume sous l'eau/lave
|
||||
ou neige poudreuse suspendent cette passe, comme les autres effets de
|
||||
profondeur Sanctuary.
|
||||
|
||||
## Livraison
|
||||
|
||||
- `Sanctuary-beta.132.mrpack` : 10394026 octets, SHA-256 `a7c0823e00f36925919645e32e7792242ec1d01015b41bee5b4f6285b9fc533d`.
|
||||
- `Sanctuary-Test-beta.132.mrpack` : 10412949 octets, SHA-256 `9bc30a7d6d5834f9323f15f0c39d3f49d200e1a97d739266ab373c9e1ba4a8fd`.
|
||||
|
||||
JAR : `83eb2d091a7acd85eaa8b3b6a00a40ea483f9eeb2a30c033bdf68b4d717c5142`.
|
||||
|
||||
Archives vérifiées recopiées dans `sanctuary-beta/build/`. [Release beta.132](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.132)
|
||||
publiée depuis `7e21781a9a08c669c7d6b65653bd9d408887689c`. Canal packwiz
|
||||
`9edaaedc0205c525ecce9de66b5ca7827c56c28f` ; téléchargements publics vérifiés par SHA-256.
|
||||
Deux synchronisations isolées, puis deux dans la même instance **Sanctuary Beta**,
|
||||
réussissent. Un seul JAR beta.132 actif ; les **923 fichiers personnels suivis**,
|
||||
dont sauvegardes et préférences de shaders, gardent leurs hashes. Sauvegarde
|
||||
préalable `sanctuary-backups/before-beta.132/` ; aucun monde personnel ouvert.
|
||||
Le checkout original conserve ses changements préexistants.
|
||||
|
||||
Reçus locaux ignorés : `build/shader132-artifact.json`,
|
||||
`build/shader132-isolated.json`, `build/shader132-prism.json`,
|
||||
`build/shader132-publication.log`.
|
||||
@@ -0,0 +1,106 @@
|
||||
# SHADER-133 — SSGI visible au réglage courant
|
||||
|
||||
Demande : le rebond diffus de beta.132 est trop discret pour être distingué.
|
||||
Branche `codex/ssgi-strength-beta133`, socle beta.132, Minecraft 26.3 / Java 25.
|
||||
|
||||
## Changement
|
||||
|
||||
La collecte conserve douze directions, dix étapes, une demi-résolution et
|
||||
un rayon de 2,5 blocs. L'atténuation spatiale devient linéaire plutôt que
|
||||
quadratique. Le mélange est reconstruit en lumière approximativement linéaire,
|
||||
puis réencodé une seule fois. Un gain artistique plus fort rend la couleur
|
||||
lisible et une courbe progressive évite de couper brutalement les canaux
|
||||
à blanc. Les surfaces sans rebond restent intactes. La force demeure réglable
|
||||
de 0 à 100, avec le même réglage initial de 35 ; aucune préférence enregistrée
|
||||
n'est écrasée. SSGI reste optionnel, initialement désactivé.
|
||||
|
||||
Aucun rayon, cible GPU ou texture de matériau supplémentaire. La composition
|
||||
ajoute une exponentielle et une racine carrée par canal ; le coût exact en FPS
|
||||
reste à mesurer sur la machine du joueur. Les normales de géométrie orientent
|
||||
déjà le rebond. Pas de PBR ni de normales procédurales de textures ajoutés.
|
||||
|
||||
## Piste matériaux
|
||||
|
||||
Pour une étape distincte, des normales de faible amplitude calculées au
|
||||
chargement des textures, mises en cache, et une rugosité par famille de matériaux
|
||||
permettraient d'étudier du relief et des reflets orientés. Une simple conversion
|
||||
luminosité → hauteur ne connaît pas le relief réel : un motif peint peut devenir
|
||||
une bosse artificielle. Commencer sur pierre, briques et bois avec une amplitude
|
||||
réglable serait préférable à une génération aveugle pour tous les blocs.
|
||||
La passe actuelle ne possède ni atlas de normales, ni identifiants de matériaux,
|
||||
ni albédo non éclairé ; des images seules ne suffiraient pas à les exploiter.
|
||||
|
||||
## Limites conservées
|
||||
|
||||
Approximation diffuse à partir de la couleur déjà éclairée, pas un transport
|
||||
physique complet. Seules les surfaces visibles contribuent, avec variations
|
||||
possibles aux entrées/sorties d'écran. Premier obstacle bloquant, pas d'historique
|
||||
ni de bruit temporel ; aucune modification d'eau, monde ou lumière serveur.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Suite client native complète réussie en **4 min 29 s**, sur macOS / Apple M1 /
|
||||
OpenGL, dans un monde de test plat (graine 122). Les comparaisons ON/OFF à 35 %
|
||||
mesurent, sur les pixels neutres du sol :
|
||||
|
||||
| Mur | Pixels avec gain ≥ 8/255 | Pixels avec gain ≥ 16/255 | Gain cumulé du canal dominant |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| Rouge | 16 487 | 7 311 | 305 221 |
|
||||
| Bleu | 14 954 | 7 328 | 309 991 |
|
||||
|
||||
À 90 %, les gains dominants atteignent 654 683 et 655 867, contre 8 968 et
|
||||
9 168 au même réglage dans la fixture beta.132. Ce rapport concerne uniquement
|
||||
cette scène et ces pixels, pas une promesse de gain uniforme dans tous les mondes.
|
||||
Les captures confirment une bande colorée au pied du mur. Un premier réglage
|
||||
intermédiaire a été rejeté car aucun pixel du sol ne dépassait 8/255 à 35 % ;
|
||||
un second a passé les contrôles ciblés, puis la force a encore été augmentée
|
||||
après examen visuel. Les valeurs finales sont celles de la suite complète.
|
||||
|
||||
Le test impose maintenant plus de 5 000 pixels à 8/255 et plus de 500 à 16/255
|
||||
au réglage 35, pour les deux couleurs. Intensité, OFF/zéro, absence de rebond
|
||||
coplanaire, occultation d'un mur changeant de couleur, caméra fixe, déplacement
|
||||
infinitésimal, retour de caméra, redimensionnement et rechargement passent.
|
||||
Ombres, lune, feuillage, CTM, cinq précisions, bloom, émissifs indépendants et
|
||||
interfaces FR/EN restent validés, y compris avec le SSGI actif.
|
||||
|
||||
Commande : `./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true
|
||||
-PsanctuaryPixelShadows122ClientTests=true -PsanctuaryClientNoVsync=true
|
||||
-PsanctuaryQuickTests=true`. Log : `build/shader133-client-full.log`.
|
||||
Le nom de la propriété ciblée `sanctuarySsgi132ClientTests` est conservé.
|
||||
|
||||
Comparateur local autonome : `build/SSGI-beta.133-comparaison.html`, captures
|
||||
natives OFF/35/90 des murs rouge/bleu, sans retouche. Pas de validation
|
||||
Windows/Vulkan ni de mesure de coût FPS revendiquée.
|
||||
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
réussit en **2 min 21 s**, 126 tâches. Le serveur GameTest dédié reste exclu
|
||||
conformément au refus EULA antérieur. Log : `build/shader133-build.log`.
|
||||
Les archives normal/Test contiennent le même JAR ; sources archivées et template
|
||||
de ressources correspondent aux sources. Comparaison avec beta.132 : **5 entrées
|
||||
de production** changées (classe du compositeur, deux shaders, libellés FR/EN).
|
||||
Gameplay, textures, sauvegardes et dépendances inchangés.
|
||||
|
||||
## Artefacts
|
||||
|
||||
- `Sanctuary-beta.133.mrpack` : 10394282 octets, SHA-256 `0c90a070da7f969706abeeec1bdfb0830bb52244c20fccbca4fd876f3fbe0564`.
|
||||
- `Sanctuary-Test-beta.133.mrpack` : 10413204 octets, SHA-256 `e01f0355bf2cce18156fe94a069e142bedc498624ba0b6e2729ae0f23ea73e80`.
|
||||
|
||||
JAR : `a01cae4a98606007b533ca09ec9f072f19a1db2e9b8de466f4ad075d5c1fc185`.
|
||||
|
||||
|
||||
## Livraison
|
||||
|
||||
[Release beta.133](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.133)
|
||||
publiée depuis `216d3f75b576bd2ed531653012f9cc6c4801d87a`. Canal packwiz
|
||||
`9ceb5a5353c9d1b8bb7834a20e818fd65096ff98` ; téléchargements publics vérifiés par SHA-256.
|
||||
Deux synchronisations isolées puis deux synchronisations de la même instance
|
||||
**Sanctuary Beta** réussissent. Un seul JAR beta.133 actif ; les **923 fichiers
|
||||
personnels suivis**, dont sauvegardes et préférences, gardent leurs hashes.
|
||||
Sauvegarde préalable `sanctuary-backups/before-beta.133/`, aucun monde personnel
|
||||
ouvert. Le checkout original conserve ses changements préexistants.
|
||||
|
||||
Les archives normal/Test/template et le comparateur natif sont également dans
|
||||
`sanctuary-beta/build/`. Reçus locaux ignorés : `build/shader133-artifact.json`,
|
||||
`build/shader133-isolated.json`, `build/shader133-prism.json` et
|
||||
`build/shader133-publication.log`.
|
||||
@@ -0,0 +1,109 @@
|
||||
# SHADER-134 — PBR procédural optionnel et herbe SSGI
|
||||
|
||||
Demande : ajouter en option des normales calculées à partir des textures actives,
|
||||
avec rugosité des matériaux et reflets orientés. Retour : le SSGI renforcé
|
||||
éclaire artificiellement l'intérieur de l'herbe ; vérifier et corriger ce cas.
|
||||
Branche `codex/pbr-beta134`, socle beta.133, Minecraft 26.3 / Java 25.
|
||||
|
||||
## Contrat
|
||||
|
||||
PBR initialement OFF, intensité 50 réglable 0–100. Cartes de normales à partir
|
||||
des différences de luminance, amplitude limitée ; rugosité/métallicité par
|
||||
famille de blocs, repli rugueux pour les types inconnus. Analyse des ressources
|
||||
actives conservée en cache jusqu'à désactivation ou rechargement ; pas de fichiers
|
||||
écrits dans le pack personnel, pas de GPU readback en production.
|
||||
|
||||
Portée 32 blocs, sections visibles construites progressivement. Budget de maillage
|
||||
24 Mio, atlas de 4 096 matériaux en 16 × 16 avec bordures (1 280 × 1 280 RGBA8,
|
||||
6,25 Mio), copie RGBA8 de l'image courante et uniformes. Les textures de plus haute
|
||||
résolution sont échantillonnées à 16 × 16 ; première image des textures animées.
|
||||
La couleur et l'animation restent celles de l'atlas natif. Pas de parallax ni
|
||||
de géométrie ajoutée aux blocs ; les motifs clairs peuvent donner un faux relief.
|
||||
|
||||
La passe utilise une réponse spéculaire GGX (normales, rugosité, métallicité,
|
||||
Fresnel) et module l'éclairage diffus déjà rendu. Elle ne remplace pas tout le
|
||||
pipeline Minecraft par un moteur PBR physique. Soleil/lune, exposition au ciel et
|
||||
carte d'ombres active limitent les reflets ; pas de reflets miroir, d'environnement
|
||||
SSR, ni de direction locale déduite pour chaque torche. Aucun rendu PBR d'eau,
|
||||
d'entités, d'items ou de modèles spéciaux hors maillage de terrain.
|
||||
|
||||
## SSGI
|
||||
|
||||
Les normales des faces presque axiales sont stabilisées sur leur axe. Deux passes
|
||||
spatiales filtrent la lumière indirecte à demi-résolution, avec rejet des voisins
|
||||
hors du plan du récepteur et interpolation de l'échantillonnage. Les textures de
|
||||
l'image restent nettes. Un tampon RGBA16F supplémentaire à demi-résolution porte
|
||||
le budget des cibles SSGI à environ 8 octets par pixel plein écran (contre 6).
|
||||
Pas d'historique temporel ni de rémanence ajoutés.
|
||||
|
||||
Un parcours de 12 blocs a reproduit des bandes sur un mur et un éclaircissement
|
||||
artificiel d'herbe à 35 et 100. Le filtrage seul ne suffisait pas : la collecte
|
||||
réinjectait aussi trop fortement la lumière ambiante neutre. Elle conserve
|
||||
maintenant la composante chromatique de la source, en soustrayant son canal
|
||||
linéaire minimum. L'éclairage ambiant neutre reste celui de Minecraft. Ce réglage
|
||||
vise le transfert de couleur voulu, pas une simulation physique complète du
|
||||
rebond blanc. Les sources hors champ/cachées restent absentes du SSGI ; le résultat
|
||||
peut encore varier lors d'un déplacement important. PBR et SSGI restent indépendants.
|
||||
|
||||
## Vérification
|
||||
|
||||
Test PBR ciblé réussi en 1 min 13 s : relief éclairé/ombré, intensités 20/50/100,
|
||||
soleil matin/soir, cache stable, préférences, OFF/zéro, mouvement infinitésimal,
|
||||
redimensionnement, rechargement et combinaison des effets. Le premier parcours
|
||||
SSGI est conservé comme reproduction ; résultats finaux ci-dessous.
|
||||
|
||||
|
||||
Le parcours corrigé a réussi en **2 min 47 s** avec 13 positions réparties sur
|
||||
12 blocs, à 35 et 100, chaque fois comparées à OFF. Un mob immobile, des brins
|
||||
d'herbe et un mur figurent dans la scène. Retour au point initial sans rémanence.
|
||||
Sur les captures de ce parcours, le maximum du gain vert moyen mesuré dans
|
||||
l'herbe passe sous 9,73 niveaux à 35 et 24,51 à 100. Le test impose ensuite
|
||||
respectivement des limites de 12 et 30 à chaque position. L'essai reproduisant
|
||||
le défaut atteignait notamment 23,68 et 49,20 à l'extrémité droite.
|
||||
Ces mesures décrivent cette scène, pas une garantie d'absence d'artefacts
|
||||
dans tous les mondes ni sur toutes les cartes graphiques.
|
||||
|
||||
|
||||
La suite client complète réussit en **7 min** sur macOS / Apple M1 / OpenGL.
|
||||
Elle inclut le parcours latéral et ses seuils de gain, les deux couleurs de rebond,
|
||||
le PBR, les ombres, le feuillage, les cinq précisions, le CTM, les émissifs, le
|
||||
bloom, les préférences et les écrans FR/EN. Log : `build/shader134-client-full.log`.
|
||||
Le contrôle PBR final réussit en **1 min 19 s** : placement/retrait d'un toit,
|
||||
invalidations de lumière/maillage et rendu combiné avec les ombres actives. Les
|
||||
reflets utilisent maintenant la grille des ombres pour leur test de visibilité. Aucun résultat Windows/Vulkan ni
|
||||
mesure de FPS revendiqués.
|
||||
|
||||
|
||||
## Construction et artefacts
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
réussit en **2 min 22 s**, 126 tâches. Le serveur GameTest dédié reste exclu
|
||||
conformément au refus EULA antérieur. Log : `build/shader134-build.log`.
|
||||
Packs normal/Test : même JAR Sanctuary. Sources archivées et template identiques
|
||||
aux sources. Comparaison à beta.133 : **23 entrées de production** changées,
|
||||
limitées au rendu, à ses options et points d'appel. Gameplay et gemmes inchangés.
|
||||
|
||||
- `Sanctuary-beta.134.mrpack` : 10421971 octets, SHA-256 `d5bc79acb1d56e7dcf83c976156fdb2fcde80c98e443bde8a6ab312d56887b7f`.
|
||||
- `Sanctuary-Test-beta.134.mrpack` : 10440898 octets, SHA-256 `04308ebb9adaf76b30a3cbf4fa32ab897e76842803fc8c8411cfa32952d1bdd5`.
|
||||
|
||||
JAR : `a6e69622a5e6629f70dd19b7494ad3b00916bc02169b73635f0e2d563929b7fa`.
|
||||
|
||||
Comparateur local : `build/Shader-beta.134-comparaison.html`. Il contient les
|
||||
captures natives PBR OFF/50/100 et le parcours SSGI de 12 blocs OFF/35/100.
|
||||
Le parcours est une succession de positions enregistrées, pas une mesure des FPS.
|
||||
|
||||
|
||||
## Livraison
|
||||
|
||||
[Release beta.134](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.134)
|
||||
publiée depuis `fc4a0cb68ed2d89163c8e74773976df5bab4f3f1`. Canal packwiz
|
||||
`35ec0f6e145002f6367944df5b75e2703958eaef` ; téléchargements publics vérifiés par SHA-256.
|
||||
Deux synchronisations isolées puis deux dans la même instance **Sanctuary Beta**
|
||||
réussissent. Un seul JAR beta.134 actif ; les **923 fichiers personnels suivis**,
|
||||
dont sauvegardes et préférences, gardent leurs hashes. Copie préalable dans
|
||||
`sanctuary-backups/before-beta.134/`, aucun monde personnel ouvert.
|
||||
|
||||
Packs normal/Test/template et comparateur natif vérifiés également dans
|
||||
`sanctuary-beta/build/`. Le checkout original garde ses changements préexistants.
|
||||
Reçus locaux ignorés : `build/shader134-artifact.json`, `build/shader134-isolated.json`,
|
||||
`build/shader134-prism.json`, `build/shader134-publication.log`.
|
||||
@@ -0,0 +1,101 @@
|
||||
# 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`.
|
||||
@@ -102,3 +102,8 @@ client natif : témoin `SHADER040_CLIENT_PASS` dans
|
||||
`build/operator040-client-final.log`. Captures dans
|
||||
`build/operator040-evidence/final/`, reçu dans `build/operator040-artifact.json`.
|
||||
Les contrôles ne revendiquent pas un essai avec Iris ou un autre moteur externe.
|
||||
|
||||
|
||||
Les ombres portées pixélisées sont livrées dans [beta.127](shader-beta127.md),
|
||||
intégrées sur la beta.126. Leur préparation locale portait un numéro beta.122
|
||||
non publié ; la release beta.122 existante n'est pas remplacée.
|
||||
|
||||
+2
-2
@@ -9,8 +9,8 @@ loom_version=1.17.20
|
||||
fabric_api_version=0.160.5+26.3
|
||||
|
||||
# Release counter: beta.001, .002, .003, ... (see docs/versioning.md).
|
||||
mod_version=beta.126
|
||||
pack_version=beta.126
|
||||
mod_version=beta.135
|
||||
pack_version=beta.135
|
||||
resource_pack_version=beta.121
|
||||
maven_group=fr.koka.sanctuary
|
||||
jei_version=30.32.0-sanctuary.3
|
||||
|
||||
@@ -93,6 +93,24 @@ tasks.named('runGameTest') {
|
||||
}
|
||||
|
||||
tasks.named('processGametestResources') {
|
||||
def pbr134Client = providers.gradleProperty('sanctuaryPbr134ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryPbr134ClientTests', pbr134Client)
|
||||
if (pbr134Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.shader.Pbr134ClientChecks') }
|
||||
def ssgi132Client = providers.gradleProperty('sanctuarySsgi132ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuarySsgi132ClientTests', ssgi132Client)
|
||||
if (ssgi132Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.shader.Ssgi132ClientChecks') }
|
||||
def closeup135Client = providers.gradleProperty('sanctuarySsgiCloseup135ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuarySsgiCloseup135ClientTests', closeup135Client)
|
||||
if (closeup135Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.shader.SsgiCloseup135ClientChecks') }
|
||||
|
||||
def bloom130Client = providers.gradleProperty('sanctuaryBloom130ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryBloom130ClientTests', bloom130Client)
|
||||
if (bloom130Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.shader.Bloom130ClientChecks') }
|
||||
|
||||
def shadows122Client = providers.gradleProperty('sanctuaryPixelShadows122ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryPixelShadows122ClientTests', shadows122Client)
|
||||
if (shadows122Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.shader.PixelShadows122ClientChecks') }
|
||||
|
||||
def gems126Client = providers.gradleProperty('sanctuaryGems126ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryGems126ClientTests', gems126Client)
|
||||
if (gems126Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Gems126ClientChecks') }
|
||||
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import static fr.koka.sanctuary.client.shader.PixelShadows122ClientChecks.*;
|
||||
import com.mojang.blaze3d.platform.NativeImage;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.TestServerContext;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.minecraft.client.Screenshot;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
|
||||
/** Actual source texels, native GPU masks, occlusion and user controls in an isolated world. */
|
||||
public final class Bloom130ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c){new PixelShadows122ClientChecks(true).runTest(c);}
|
||||
static final List<String> ORES=ores();
|
||||
private static List<String> ores(){var result=new ArrayList<String>();for(String name:List.of("iron","copper","redstone","lapis","diamond","emerald","ruby","sapphire","gold")){
|
||||
String ns=name.equals("ruby")||name.equals("sapphire")?"sanctuary:":"minecraft:";result.add(ns+name+"_ore");result.add(ns+"deepslate_"+name+"_ore");}
|
||||
result.add("minecraft:nether_quartz_ore");result.add("minecraft:nether_gold_ore");return List.copyOf(result);}
|
||||
static void run(ClientGameTestContext c,TestServerContext server){
|
||||
c.runOnClient(m->{preferences();VanillaLight.setPbr(false);VanillaLight.setSsgi(false);analyseTextures();VanillaLight.setSaturation(100);VanillaLight.setPixelShadows(false);VanillaLight.setEdgeHighlights(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);});
|
||||
server.runOnServer(s->{command(s,"time set 18000");
|
||||
for(int x=4;x<=20;x++)for(int z=4;z<=20;z++){
|
||||
s.overworld().setBlockAndUpdate(new BlockPos(x,64,z),Blocks.CONCRETE.black().defaultBlockState());
|
||||
for(int y=65;y<=70;y++)s.overworld().setBlockAndUpdate(new BlockPos(x,y,z),Blocks.AIR.defaultBlockState());
|
||||
}
|
||||
for(int i=0;i<ORES.size();i++){var state=BuiltInRegistries.BLOCK.getValue(Identifier.parse(ORES.get(i))).defaultBlockState();check(state.getLightEmission()==0,"Ores do not acquire server-side light: "+ORES.get(i));s.overworld().setBlockAndUpdate(orePos(i),state);}
|
||||
s.overworld().setBlockAndUpdate(new BlockPos(14,65,9),Blocks.REDSTONE_LAMP.defaultBlockState());
|
||||
});
|
||||
camera(c,server,10.5,71,9.5,60);c.waitTicks(40);c.waitFor(m->m.levelRenderer.hasRenderedAllSections(),1200);
|
||||
var off=capture(c,"bloom-ores-off");
|
||||
c.runOnClient(m->{VanillaLight.setOreEmission(false);VanillaLight.setBloom(true);});frames(c);
|
||||
check(luminous(emission(c))==0,"White/grey rock, ordinary surfaces and unlit lamps do not seed bloom");
|
||||
unchanged(off,capture(c,"bloom-ores-disabled"),"Disabling ore emission leaves the original scene intact");
|
||||
c.runOnClient(m->{VanillaLight.setOreEmission(true);VanillaLight.setOreIntensity(100);});frames(c);
|
||||
var source=emission(c);int orePixels=luminous(source);check(orePixels>100,"Actual exposed ore inclusions enter the GPU mask: "+orePixels);
|
||||
var on=capture(c,"bloom-ores-on");double oreGain=gain(off,on,null);check(oreGain>1000,"Ores visibly glow in the native night scene: "+oreGain);
|
||||
c.runOnClient(m->VanillaLight.setBloom(false));frames(c);
|
||||
var coreOnly=capture(c,"emissive-bloom-off");
|
||||
check(gain(off,coreOnly,null)>1000,"Emissive cores survive bloom OFF");
|
||||
check(gain(off,coreOnly,source)<1,"No halo outside source texels with bloom OFF");
|
||||
c.runOnClient(m->check(!Bloom.haloAllocated(),"Bloom OFF frees blur targets and uniforms"));
|
||||
c.runOnClient(m->{VanillaLight.setBloom(true);VanillaLight.setBloomIntensity(0);});frames(c);
|
||||
unchanged(coreOnly,capture(c,"emissive-bloom-zero"),"Zero bloom matches OFF while retaining emissive cores");
|
||||
c.runOnClient(m->check(!Bloom.haloAllocated(),"Zero bloom frees blur resources"));
|
||||
c.runOnClient(m->VanillaLight.setOreEmission(false));c.waitTicks(8);
|
||||
c.runOnClient(m->check(!Bloom.allocated()&&!Bloom.applied(),"Zero halo and disabled cores release everything"));
|
||||
unchanged(off,capture(c,null),"Disabling both restores the original scene");
|
||||
c.runOnClient(m->{VanillaLight.setOreEmission(true);VanillaLight.setBloomIntensity(35);});frames(c);
|
||||
unchanged(on,capture(c,null),"Re-enabling bloom restores its original image");
|
||||
c.runOnClient(m->VanillaLight.setBloomIntensity(10));frames(c);double low=gain(off,capture(c,null),source);
|
||||
c.runOnClient(m->VanillaLight.setBloomIntensity(90));frames(c);double high=gain(off,capture(c,"bloom-strength-90"),source);
|
||||
check(high>Math.max(30,low*2),"Bloom strength changes the halo outside source pixels: low="+low+" high="+high);
|
||||
c.runOnClient(m->VanillaLight.setBloomRadius(0));frames(c);var tight=capture(c,"bloom-spread-0");
|
||||
c.runOnClient(m->VanillaLight.setBloomRadius(100));frames(c);var wide=capture(c,"bloom-spread-100");
|
||||
check(difference(tight,wide)>100,"Spread changes the real GPU halo");
|
||||
c.runOnClient(m->VanillaLight.setOreIntensity(0));frames(c);check(luminous(emission(c))==0,"Zero ore strength suppresses both cores and halos");
|
||||
unchanged(off,capture(c,null),"Zero ore strength restores the source image");
|
||||
c.runOnClient(m->{VanillaLight.setOreIntensity(100);VanillaLight.setBloomIntensity(35);VanillaLight.setBloomRadius(50);});frames(c);
|
||||
server.runOnServer(s->{for(int i=0;i<ORES.size();i++)s.overworld().setBlockAndUpdate(orePos(i).above(),Blocks.STONE.defaultBlockState());});c.waitTicks(25);frames(c);
|
||||
check(luminous(emission(c))==0,"Covering veins with stone removes all emission: no through-wall ore glow");
|
||||
capture(c,"bloom-covered-ores");
|
||||
server.runOnServer(s->{for(int i=0;i<ORES.size();i++)s.overworld().setBlockAndUpdate(orePos(i).above(),Blocks.AIR.defaultBlockState());});c.waitTicks(25);frames(c);
|
||||
check(luminous(emission(c))>100,"Removing covers reveals the genuine visible inclusion masks again");
|
||||
var reload=c.computeOnClient(net.minecraft.client.Minecraft::reloadResourcePacks);c.waitFor(m->reload.isDone()&&m.gui.overlay()==null,1200);reload.join();frames(c);
|
||||
check(luminous(emission(c))>100,"Resource reload rebuilds emitter meshes and masks");
|
||||
c.runOnClient(m->VanillaLight.setOreEmission(false));
|
||||
server.runOnServer(s->s.overworld().setBlockAndUpdate(new BlockPos(14,65,9),Blocks.TORCH.defaultBlockState()));c.waitTicks(25);frames(c);
|
||||
int torch=luminous(emission(c));check(torch>3,"A native torch seeds bloom: "+torch);capture(c,"bloom-torch");
|
||||
server.runOnServer(s->s.overworld().setBlockAndUpdate(new BlockPos(14,65,9),Blocks.GLOWSTONE.defaultBlockState()));c.waitTicks(25);frames(c);
|
||||
check(luminous(emission(c))>torch,"A full luminous block contributes its real textured surface");capture(c,"bloom-glowstone");
|
||||
server.runOnServer(s->s.overworld().setBlockAndUpdate(new BlockPos(14,65,9),Blocks.STONE.defaultBlockState()));c.waitTicks(25);frames(c);
|
||||
check(luminous(emission(c))==0,"Replacing the light source removes its cached emission");
|
||||
server.runOnServer(s->{s.overworld().setBlockAndUpdate(new BlockPos(14,65,9),Blocks.REDSTONE_LAMP.defaultBlockState());s.overworld().setBlockAndUpdate(new BlockPos(14,64,9),Blocks.REDSTONE_BLOCK.defaultBlockState());});
|
||||
c.waitTicks(25);frames(c);check(luminous(emission(c))>10,"Powered lamp emits through its on-state texture");capture(c,"bloom-lamp-on");
|
||||
server.runOnServer(s->s.overworld().setBlockAndUpdate(new BlockPos(14,64,9),Blocks.CONCRETE.black().defaultBlockState()));
|
||||
c.waitTicks(25);frames(c);check(luminous(emission(c))==0,"Unpowered lamp loses its glow after its native state update");
|
||||
server.runOnServer(s->s.overworld().setBlockAndUpdate(new BlockPos(14,65,9),Blocks.LAVA.defaultBlockState()));
|
||||
c.waitTicks(25);frames(c);check(luminous(emission(c))>10,"Native visible lava seeds bloom");capture(c,"bloom-lava");
|
||||
server.runOnServer(s->command(s,"time set 3000"));camera(c,server,10.5,90,9.5,70);
|
||||
var direction=c.computeOnClient(m->PixelShadows.sunDirection(m.gameRenderer.gameRenderState().levelRenderState.skyRenderState.sunAngle));
|
||||
look(c,server,(float)Math.toDegrees(Math.atan2(-direction.x,direction.z)),-(float)Math.toDegrees(Math.asin(direction.y)));
|
||||
c.runOnClient(m->VanillaLight.setBloom(false));c.waitTicks(8);var sunOff=capture(c,"bloom-sun-off");
|
||||
c.runOnClient(m->VanillaLight.setBloom(true));frames(c);var sunMask=emission(c);
|
||||
check(luminous(sunMask)>40,"The actual sun disc seeds bloom");
|
||||
double sunlight=gain(sunOff,capture(c,"bloom-sun-on"),sunMask);check(sunlight>100,"Sun halo extends beyond its source disc: "+sunlight);
|
||||
server.runOnServer(s->{var eye=s.getPlayerList().getPlayers().getFirst().getEyePosition();var center=BlockPos.containing(eye.x+direction.x*8,eye.y+direction.y*8,eye.z+direction.z*8);
|
||||
for(int x=-4;x<=4;x++)for(int y=-4;y<=4;y++)for(int z=-4;z<=4;z++)s.overworld().setBlockAndUpdate(center.offset(x,y,z),Blocks.STONE.defaultBlockState());});
|
||||
c.waitTicks(30);frames(c);check(luminous(emission(c))==0,"Solid terrain in front of the sun suppresses its bloom source");capture(c,"bloom-sun-occluded");
|
||||
c.runOnClient(m->VanillaLight.setBloom(false));c.waitTicks(6);var coveredOff=capture(c,null);
|
||||
c.runOnClient(m->{VanillaLight.setBloom(true);VanillaLight.setBloomIntensity(0);});c.waitTicks(6);c.runOnClient(m->check(!Bloom.applied()&&!Bloom.allocated(),"Zero bloom releases GPU resources"));
|
||||
unchanged(coveredOff,capture(c,null),"Zero bloom leaves no residual glow");
|
||||
c.runOnClient(m->{VanillaLight.setBloomIntensity(35);VanillaLight.setBloom(false);VanillaLight.setOreEmission(true);VanillaLight.setOreIntensity(50);});
|
||||
System.out.println("EMISSIVE131_PASS independent cores with halo OFF/zero; blur resources released; defaults; masks for 20 ore textures; selective night glow; halo intensity/spread; no through-wall glow; live edits; torch/glowstone/lamp/lava/sun; resource reload; defaults/migration; orePixels="+orePixels+" oreGain="+oreGain+" haloLow="+low+" haloHigh="+high+" sunHalo="+sunlight);
|
||||
}
|
||||
private static BlockPos orePos(int i){return new BlockPos(8+i%5,65,8+i/5);}
|
||||
private static void preferences(){try{
|
||||
var path=FabricLoader.getInstance().getConfigDir().resolve("sanctuary-shaders.json");Files.deleteIfExists(path);forgetPreferences();
|
||||
check(VanillaLight.pixelShadows()&&VanillaLight.shadowDistance()==128&&VanillaLight.shadowPixels()==16&&VanillaLight.edgeDistance()==32,"Fresh requested defaults: shadows ON, 128; highlights 32; shared precision 16");
|
||||
check(VanillaLight.bloom()&&VanillaLight.oreEmission()&&VanillaLight.edgeHighlights()&&VanillaLight.bloomIntensity()==20&&VanillaLight.bloomRadius()==50,"Bloom, ore emission and edge highlights default to ON");
|
||||
Files.writeString(path,"{\"bloom\":false,\"oreEmission\":false,\"edgeHighlights\":false,\"pixelShadows\":false,\"shadowDistance\":64,\"shadowPixels\":8,\"edgeDistance\":16}");forgetPreferences();
|
||||
check(!VanillaLight.bloom()&&!VanillaLight.oreEmission()&&!VanillaLight.edgeHighlights()&&!VanillaLight.pixelShadows()&&VanillaLight.shadowDistance()==64&&VanillaLight.shadowPixels()==8&&VanillaLight.edgeDistance()==16,"Existing user choices are retained");
|
||||
for(int n:new int[]{-10,0,35,100,400}){VanillaLight.setBloomIntensity(n);VanillaLight.setBloomRadius(n);VanillaLight.setOreIntensity(n);forgetPreferences();check(VanillaLight.bloomIntensity()==Math.clamp(n,0,100)&&VanillaLight.bloomRadius()==Math.clamp(n,0,100)&&VanillaLight.oreIntensity()==Math.clamp(n,0,100),"Persisted bloom controls are bounded");}
|
||||
VanillaLight.setBloomIntensity(35);VanillaLight.setBloomRadius(50);VanillaLight.setOreIntensity(50);VanillaLight.setShadowPixels(16);
|
||||
}catch(java.io.IOException e){throw new AssertionError(e);}}
|
||||
private static void analyseTextures(){
|
||||
var report=new StringBuilder("texture,selected,total,neutral_rock_selected\n");
|
||||
try(var materials=new BloomMaterials();var sheet=new NativeImage(5*16*6,4*32*6,false)){
|
||||
sheet.fillRect(0,0,sheet.getWidth(),sheet.getHeight(),0xFF16191E);
|
||||
for(int index=0;index<ORES.size();index++){
|
||||
var block=Identifier.parse(ORES.get(index));var id=block.withPath("block/"+block.getPath());
|
||||
try(var image=BloomMaterials.read(id)){
|
||||
int w=image.getWidth(),h=image.getHeight();int[] pixels=new int[w*h];for(int y=0;y<h;y++)for(int x=0;x<w;x++)pixels[y*w+x]=image.getPixel(x,y);
|
||||
var mask=materials.mask(id,pixels,w,h,true,false);int count=0,grey=0;
|
||||
for(int i=0;i<mask.length;i++){if(mask[i]>0){count++;int c=pixels[i],r=c>>16&255,g=c>>8&255,b=c&255;if(r==g&&g==b&&r<215)grey++;}}
|
||||
check(count>=4&&count<w*h*.7,"Texture has a selective inclusion mask: "+id+" "+count+"/"+mask.length);
|
||||
if(!id.getPath().contains("quartz"))check(grey==0,"Neutral host rock never emits: "+id);
|
||||
report.append(id).append(',').append(count).append(',').append(mask.length).append(',').append(grey).append('\n');
|
||||
for(int y=0;y<32*6;y++)for(int x=0;x<16*6;x++){
|
||||
int source=(y/6%16)*w/16*w+(x/6)*w/16;int color=pixels[source];if(y>=16*6&&mask[source]==0)color=0xFF16191E;
|
||||
sheet.setPixel((index%5)*16*6+x,(index/5)*32*6+y,color);
|
||||
}
|
||||
}
|
||||
}
|
||||
var root=FabricLoader.getInstance().getGameDir();sheet.writeToFile(root.resolve("bloom130-ore-masks.png"));Files.writeString(root.resolve("bloom130-ore-masks.csv"),report.toString());
|
||||
System.out.println("BLOOM130_MASKS\n"+report);
|
||||
}catch(java.io.IOException e){throw new AssertionError(e);}
|
||||
}
|
||||
private static void frames(ClientGameTestContext c){int before=c.computeOnClient(m->Bloom.renderedFrames());c.waitFor(m->Bloom.applied()&&Bloom.ready()&&Bloom.renderedFrames()>before+10,1200);c.waitTicks(8);}
|
||||
private static Frame emission(ClientGameTestContext c){var result=new CompletableFuture<Frame>();c.runOnClient(m->Screenshot.takeScreenshot(Bloom.emissionTarget(),image->{try(image){result.complete(new Frame(image.getWidth(),image.getHeight(),image.getPixels()));}}));c.waitFor(m->result.isDone(),200);return result.join();}
|
||||
private static int luminous(Frame frame){int count=0;for(int color:frame.pixels())if((color&0xFFFFFF)!=0)count++;return count;}
|
||||
private static double gain(Frame off,Frame on,Frame source){double result=0;for(int i=0;i<off.pixels().length;i++)if(source==null||(source.pixels()[i]&0xFFFFFF)==0)result+=Math.max(0,brightness(on.pixels()[i])-brightness(off.pixels()[i]));return result;}
|
||||
private static int difference(Frame a,Frame b){int n=0;for(int i=0;i<a.pixels().length;i++)if(channelDifference(a.pixels()[i],b.pixels()[i])>3)n++;return n;}
|
||||
}
|
||||
+221
@@ -0,0 +1,221 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import static fr.koka.sanctuary.client.shader.PixelShadows122ClientChecks.*;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.util.ArrayList;
|
||||
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 org.joml.Matrix4f;
|
||||
import org.joml.Vector4f;
|
||||
|
||||
/** Native GPU checks sharing the isolated flat-world fixture and its preference backup. */
|
||||
final class EdgeHighlights129ClientChecks {
|
||||
static void run(ClientGameTestContext c, TestServerContext server) {
|
||||
c.runOnClient(m -> preferences());
|
||||
server.runOnServer(s -> {
|
||||
for (int x=10;x<=14;x++) for (int z=6;z<=10;z++) {
|
||||
s.overworld().setBlockAndUpdate(new BlockPos(x,65,z),
|
||||
(Math.floorMod(x,8)<4 ? Blocks.STONE : Blocks.OAK_PLANKS).defaultBlockState());
|
||||
}
|
||||
});
|
||||
camera(c,server,12.5,72,8.5,60);
|
||||
c.waitTicks(35);
|
||||
shadows(c,false);
|
||||
edge(c,false);
|
||||
var off=capture(c,"edges-flat-off");
|
||||
c.waitTicks(6);
|
||||
unchanged(off,capture(c,null),"Highlight OFF control is stable");
|
||||
edge(c,true);
|
||||
var on=capture(c,"edges-flat-on-30");
|
||||
var low=measure(off,on);
|
||||
check(low.brighter>100 && low.stable>low.samples*.55 && low.darker<low.samples*.005,
|
||||
"Highlight brightens local strips, preserves face interiors: "+low);
|
||||
grid(c,off,on);
|
||||
c.runOnClient(m -> VanillaLight.setShadowPixels(8));frames(c);
|
||||
var coarse=measure(off,capture(c,"edges-shared-precision-8"));
|
||||
c.runOnClient(m -> VanillaLight.setShadowPixels(32));frames(c);
|
||||
var fine=measure(off,capture(c,"edges-shared-precision-32"));
|
||||
check(coarse.brighter()>low.brighter()*1.4 && fine.brighter()<low.brighter()*.8,
|
||||
"One precision setting controls the visible highlight width: coarse="+coarse+" middle="+low+" fine="+fine);
|
||||
for(int pixels:new int[]{64,128}){
|
||||
c.runOnClient(m -> VanillaLight.setShadowPixels(pixels));frames(c);
|
||||
var finer=measure(off,capture(c,"edges-shared-precision-"+pixels));
|
||||
check(finer.meanGain()<fine.meanGain() && finer.darker()==0,"High precision refines CTM without darkening: "+pixels+" "+finer);
|
||||
}
|
||||
c.runOnClient(m -> VanillaLight.setShadowPixels(16));frames(c);
|
||||
|
||||
for (int distance : new int[]{16,32,64,128,256}) {
|
||||
c.runOnClient(m -> VanillaLight.setEdgeDistance(distance));
|
||||
frames(c);
|
||||
check(c.computeOnClient(m -> VanillaLight.edgeDistance())==distance,"All CTM ranges are available");
|
||||
unchanged(on,capture(c,null),"Near CTM contours unchanged at range "+distance);
|
||||
}
|
||||
c.runOnClient(m -> VanillaLight.setEdgeDistance(32));
|
||||
c.runOnClient(m -> {
|
||||
check(PixelShadows.shadowMap()==null,"Highlight works without allocating a cast-shadow map");
|
||||
VanillaLight.setEdgeIntensity(90);
|
||||
});
|
||||
frames(c);
|
||||
var high=measure(off,capture(c,"edges-flat-on-90"));
|
||||
check(high.meanGain>low.meanGain*1.8,"Intensity changes visible brightness: low="+low+" high="+high);
|
||||
c.runOnClient(m -> VanillaLight.setEdgeIntensity(0));
|
||||
c.waitTicks(6);
|
||||
c.runOnClient(m -> check(!EdgeHighlights.applied() && !EdgeHighlights.allocated(),"Zero intensity frees its uniform buffer"));
|
||||
unchanged(off,capture(c,"edges-zero"),"Zero intensity reproduces OFF exactly");
|
||||
c.runOnClient(m -> VanillaLight.setEdgeIntensity(30));
|
||||
frames(c);
|
||||
camera(c,server,11.99999,72,8.5,60);
|
||||
var anchor=capture(c,null);
|
||||
camera(c,server,12.00001,72,8.5,60);
|
||||
unchanged(anchor,capture(c,"edges-anchor-crossing"),"Grid remains stable across an integer camera anchor");
|
||||
camera(c,server,12.5,72,8.5,60);
|
||||
unchanged(on,capture(c,null),"Returning to the camera restores the same grid");
|
||||
var reload=c.computeOnClient(net.minecraft.client.Minecraft::reloadResourcePacks);
|
||||
c.waitFor(m -> reload.isDone() && m.gui.overlay()==null,1200);
|
||||
reload.join();
|
||||
c.runOnClient(m -> {
|
||||
forgetPreferences();
|
||||
check(VanillaLight.edgeHighlights() && VanillaLight.edgeIntensity()==30,"Highlight preferences survive reload and disk reread");
|
||||
});
|
||||
frames(c);
|
||||
unchanged(on,capture(c,"edges-after-reload"),"Resource reload retains the highlight image");
|
||||
edge(c,false);
|
||||
unchanged(off,capture(c,"edges-off-restored"),"Disabling leaves no residual highlight");
|
||||
|
||||
// A new hole creates a connected boundary immediately; filling it removes that boundary.
|
||||
server.runOnServer(s -> s.overworld().setBlockAndUpdate(new BlockPos(12,65,8),Blocks.AIR.defaultBlockState()));
|
||||
c.waitTicks(20);
|
||||
var holeOff=capture(c,null);
|
||||
edge(c,true);
|
||||
var hole=measure(holeOff,capture(c,"edges-ctm-hole"));
|
||||
check(hole.brighter>low.brighter+20,"Removing an interior block creates a new CTM contour");
|
||||
server.runOnServer(s -> s.overworld().setBlockAndUpdate(new BlockPos(12,65,8),Blocks.STONE.defaultBlockState()));
|
||||
c.waitTicks(20);
|
||||
unchanged(on,capture(c,null),"Filling the hole reconnects the flat surface");
|
||||
// The actual radius gates a visible contour, independently of shadow range.
|
||||
camera(c,server,12.5,88,8.5,30);
|
||||
edge(c,false);
|
||||
var farOff=capture(c,null);
|
||||
c.runOnClient(m -> VanillaLight.setEdgeDistance(16));
|
||||
edge(c,true);
|
||||
unchanged(farOff,capture(c,"edges-beyond-16"),"CTM beyond selected 16-block radius is absent");
|
||||
c.runOnClient(m -> {VanillaLight.setEdgeDistance(64);VanillaLight.setEdgeIntensity(90);});
|
||||
frames(c);
|
||||
var far=measure(farOff,capture(c,"edges-inside-64"));
|
||||
check(far.brighter>40,"Extending the CTM radius restores a distant contour: "+far);
|
||||
c.runOnClient(m -> {VanillaLight.setEdgeDistance(32);VanillaLight.setEdgeIntensity(30);});
|
||||
edge(c,false);
|
||||
|
||||
server.runOnServer(s -> {
|
||||
for (int x=9;x<=13;x+=2) for(int y=65;y<=67;y++)
|
||||
s.overworld().setBlockAndUpdate(new BlockPos(x,y,12),
|
||||
(x==9 ? Blocks.CONCRETE.red() : x==11 ? Blocks.STONE_BRICKS : Blocks.OAK_PLANKS).defaultBlockState());
|
||||
s.overworld().setBlockAndUpdate(new BlockPos(10,65,11),Blocks.STONE_SLAB.defaultBlockState());
|
||||
s.overworld().setBlockAndUpdate(new BlockPos(12,65,11),Blocks.OAK_STAIRS.defaultBlockState());
|
||||
});
|
||||
camera(c,server,12.5,72,7.5,70);
|
||||
look(c,server,15,55);
|
||||
shadows(c,true);
|
||||
var reliefOff=capture(c,"edges-relief-with-shadows-off");
|
||||
edge(c,true);
|
||||
var reliefOn=capture(c,"edges-relief-with-shadows-on");
|
||||
var relief=measure(reliefOff,reliefOn);
|
||||
check(relief.brighter>100 && relief.stable>relief.samples*.5 && relief.darker<relief.samples*.01,
|
||||
"Highlight composes with cast shadows and coloured textured blocks: "+relief);
|
||||
look(c,server,15.00002f,55.00002f);
|
||||
unchanged(reliefOn,capture(c,null),"Tiny oblique camera rotation keeps highlights stable");
|
||||
c.runOnClient(m -> VanillaLight.setEnabled(false));
|
||||
c.waitTicks(6);
|
||||
c.runOnClient(m -> check(!EdgeHighlights.applied() && !EdgeHighlights.allocated(),"Main shader OFF also releases highlight resources"));
|
||||
c.runOnClient(m -> VanillaLight.setEnabled(true));
|
||||
frames(c);
|
||||
edge(c,false);
|
||||
System.out.println("EDGE_HIGHLIGHTS129_PASS connected contours and unchanged flat joins; independent shadows; intensity; zero/OFF release; "
|
||||
+"camera anchors; oblique textured scene; resource reload; legacy preference migration; low="+low+" high="+high+" relief="+relief);
|
||||
}
|
||||
|
||||
private static void preferences() {
|
||||
var path=FabricLoader.getInstance().getConfigDir().resolve("sanctuary-shaders.json");
|
||||
try {
|
||||
// Real prior-version JSON, without either new field. Preserve its existing settings.
|
||||
Files.writeString(path,"{\"enabled\":true,\"saturation\":100,\"pixelShadows\":true,\"shadowPixels\":16,\"shadowDistance\":32}");
|
||||
forgetPreferences();
|
||||
check(VanillaLight.edgeHighlights() && VanillaLight.edgeIntensity()==30 && VanillaLight.edgeDistance()==32,"Missing preference uses the new highlight ON default at intensity 30");
|
||||
check(VanillaLight.pixelShadows() && VanillaLight.saturation()==100,"Migration retains existing shader choices");
|
||||
for(int value:new int[]{-100,0,30,100,1000}) {
|
||||
VanillaLight.setEdgeIntensity(value);forgetPreferences();
|
||||
check(VanillaLight.edgeIntensity()==Math.clamp(value,0,100),"Saved intensity is bounded");
|
||||
}
|
||||
for(int[] pair:new int[][]{{-1,16},{24,32},{48,64},{96,128},{192,256},{9999,256}}) {
|
||||
VanillaLight.setEdgeDistance(pair[0]);forgetPreferences();
|
||||
check(VanillaLight.edgeDistance()==pair[1],"Saved CTM distance is bounded and normalized");
|
||||
}
|
||||
VanillaLight.setEdgeDistance(32);
|
||||
VanillaLight.setEdgeIntensity(30);
|
||||
VanillaLight.setEdgeHighlights(true);forgetPreferences();
|
||||
check(VanillaLight.edgeHighlights(),"Toggle persists independently");
|
||||
VanillaLight.setEdgeHighlights(false);
|
||||
VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);
|
||||
VanillaLight.setPbr(false);VanillaLight.setSsgi(false);
|
||||
} catch(java.io.IOException e) { throw new AssertionError(e); }
|
||||
}
|
||||
|
||||
private static void edge(ClientGameTestContext c,boolean enabled) {
|
||||
c.runOnClient(m -> VanillaLight.setEdgeHighlights(enabled));
|
||||
if(enabled)frames(c);
|
||||
else {
|
||||
c.waitTicks(6);
|
||||
c.runOnClient(m -> check(!EdgeHighlights.applied() && !EdgeHighlights.allocated(),"OFF frees resources"));
|
||||
}
|
||||
}
|
||||
|
||||
private static void frames(ClientGameTestContext c) {
|
||||
int before=c.computeOnClient(m -> EdgeHighlights.renderedFrames());
|
||||
c.waitFor(m -> EdgeHighlights.applied() && EdgeHighlights.renderedFrames()>before+8,200);
|
||||
c.waitTicks(6);
|
||||
}
|
||||
|
||||
private record Difference(int samples,int brighter,int stable,int darker,double meanGain) { }
|
||||
private static Difference measure(Frame off,Frame on) {
|
||||
int samples=0,brighter=0,stable=0,darker=0;double gain=0;
|
||||
for(int y=off.height()/10;y<off.height()*9/10;y+=2) for(int x=off.width()/10;x<off.width()*9/10;x+=2) {
|
||||
int i=y*off.width()+x,a=off.pixels()[i],b=on.pixels()[i];
|
||||
int delta=brightness(b)-brightness(a);samples++;gain+=delta;
|
||||
if(delta>=4)brighter++;
|
||||
if(delta<=-4)darker++;
|
||||
if(channelDifference(a,b)<=2)stable++;
|
||||
}
|
||||
var result=new Difference(samples,brighter,stable,darker,gain/samples);
|
||||
System.out.println("EDGE_HIGHLIGHTS129_PIXELS "+result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void grid(ClientGameTestContext c,Frame off,Frame on) {
|
||||
var samples=c.computeOnClient(m -> {
|
||||
var camera=m.gameRenderer.gameRenderState().levelRenderState.cameraRenderState;
|
||||
var vp=new Matrix4f(camera.projectionMatrix).mul(camera.viewRotationMatrix);
|
||||
var result=new ArrayList<int[]>();
|
||||
for(int x=11;x<=14;x++) for(int z=6;z<=10;z++) for(int edge=0;edge<2;edge++) {
|
||||
var p=vp.transform(new Vector4f((float)(x+(edge==0?.5:1.0/32)-camera.pos.x),
|
||||
(float)(66-camera.pos.y),(float)(z+.5-camera.pos.z),1));
|
||||
int px=(int)((p.x/p.w*.5+.5)*off.width());
|
||||
int py=(int)((.5-p.y/p.w*.5)*off.height());
|
||||
if(px>=10 && px<off.width()-10 && py>=10 && py<off.height()-10)result.add(new int[]{py*off.width()+px,edge});
|
||||
}
|
||||
return result;
|
||||
});
|
||||
int centers=0,unchanged=0,edges=0,bright=0;
|
||||
for(var sample:samples) {
|
||||
int a=off.pixels()[sample[0]],b=on.pixels()[sample[0]];
|
||||
if(sample[1]==0){centers++;if(channelDifference(a,b)<=2)unchanged++;}
|
||||
else {edges++;if(brightness(b)-brightness(a)>=4)bright++;}
|
||||
}
|
||||
check(centers>=15 && unchanged>centers*.95 && edges>=15 && bright==0,
|
||||
"Connected block centers and coplanar joins remain untouched: centers="+unchanged+"/"+centers+" edges="+bright+"/"+edges);
|
||||
System.out.println("EDGE_HIGHLIGHTS129_GRID centers="+unchanged+"/"+centers+" edges="+bright+"/"+edges);
|
||||
}
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import static fr.koka.sanctuary.client.shader.PixelShadows122ClientChecks.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.TestServerContext;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Arrays;
|
||||
|
||||
public final class Pbr134ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c){new PixelShadows122ClientChecks(false,false,true).runTest(c);}
|
||||
static void run(ClientGameTestContext c,TestServerContext server){
|
||||
c.runOnClient(m->{preferences();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);
|
||||
c.runOnClient(m->check(analyses==ProceduralPbr.analyses()&&ProceduralPbr.materialCount()>0,"Source textures are cached, not regenerated every frame"));
|
||||
unchanged(on,capture(c,null),"Static PBR has no animated noise");
|
||||
look(c,server,.00002f,40.00002f);frames(c);unchanged(on,capture(c,null),"PBR tiny camera motion");look(c,server,0,40);frames(c);
|
||||
c.runOnClient(m->VanillaLight.setPbrIntensity(20));frames(c);var low=difference(off,capture(c,"pbr-20"));
|
||||
check(delta.total>low.total*1.3,"PBR strength affects actual GPU output");
|
||||
c.runOnClient(m->VanillaLight.setPbrIntensity(100));frames(c);var high=difference(off,capture(c,"pbr-100"));check(high.total>delta.total*1.2,"Strong PBR visibly increases relief");
|
||||
c.runOnClient(m->VanillaLight.setPbrIntensity(0));c.waitTicks(8);unchanged(off,capture(c,null),"PBR zero restores native image");
|
||||
c.runOnClient(m->check(!ProceduralPbr.allocated()&&ProceduralPbr.materialCount()==0,"Zero releases targets and material cache"));
|
||||
c.runOnClient(m->VanillaLight.setPbrIntensity(50));frames(c);
|
||||
server.runOnServer(s->command(s,"time set 11000"));settle(c);enable(c,false);var eveningOff=capture(c,null);enable(c,true);var eveningOn=capture(c,"pbr-evening");
|
||||
int directions=0;for(int i=0;i<on.pixels().length;i++){int am=(on.pixels()[i]>>16&255)-(off.pixels()[i]>>16&255),pm=(eveningOn.pixels()[i]>>16&255)-(eveningOff.pixels()[i]>>16&255);if(Math.abs(am-pm)>5)directions++;}
|
||||
check(directions>500,"Relief follows the moving sun: "+directions);
|
||||
var reload=c.computeOnClient(net.minecraft.client.Minecraft::reloadResourcePacks);c.waitFor(m->reload.isDone()&&m.gui.overlay()==null,1200);reload.join();frames(c);unchanged(eveningOn,capture(c,"pbr-reloaded"),"Resource reload rebuilds consistent cached materials");
|
||||
// Live geometry and light invalidation: an enclosing roof removes exposure to the sky.
|
||||
server.runOnServer(s->{for(int x=-8;x<=28;x++)for(int z=-8;z<=28;z++)s.overworld().setBlockAndUpdate(new BlockPos(x,73,z),Blocks.STONE.defaultBlockState());});
|
||||
settle(c);frames(c);var roofOn=capture(c,"pbr-roof-on");enable(c,false);var roofOff=capture(c,"pbr-roof-off");
|
||||
unchanged(roofOff,roofOn,"Roof prevents outdoor highlights on enclosed floor");enable(c,true);
|
||||
server.runOnServer(s->{for(int x=-8;x<=28;x++)for(int z=-8;z<=28;z++)s.overworld().setBlockAndUpdate(new BlockPos(x,73,z),Blocks.AIR.defaultBlockState());});
|
||||
settle(c);frames(c);unchanged(eveningOn,capture(c,"pbr-roof-removed"),"Removing roof restores material light without stale meshes");
|
||||
int oldWidth=c.computeOnClient(m->m.getWindow().getScreenWidth()),oldHeight=c.computeOnClient(m->m.getWindow().getScreenHeight());
|
||||
c.runOnClient(m->m.getWindow().setWindowed(961,541));c.waitFor(m->m.gameRenderer.mainRenderTarget().width!=on.width(),200);frames(c);capture(c,"pbr-resized");
|
||||
c.runOnClient(m->m.getWindow().setWindowed(oldWidth,oldHeight));c.waitFor(m->m.gameRenderer.mainRenderTarget().width==on.width(),200);frames(c);
|
||||
c.runOnClient(m->{VanillaLight.setPixelShadows(true);VanillaLight.setSsgi(true);VanillaLight.setBloom(true);VanillaLight.setOreEmission(true);VanillaLight.setEdgeHighlights(true);});
|
||||
c.waitFor(m->PixelShadows.applied()&&PixelShadows.ready()&&ScreenSpaceGI.applied()&&Bloom.applied()&&EdgeHighlights.applied()&&ProceduralPbr.applied(),1200);frames(c);capture(c,"pbr-combined");
|
||||
c.runOnClient(m->VanillaLight.setEnabled(false));c.waitTicks(8);c.runOnClient(m->check(!ProceduralPbr.allocated()&&ProceduralPbr.materialCount()==0,"Global OFF releases PBR"));
|
||||
c.runOnClient(m->{VanillaLight.setEnabled(true);VanillaLight.setPbr(false);VanillaLight.setSsgi(false);});
|
||||
System.out.println("PBR134_PASS normals; profiles; texture cache; directional lighting; intensity; OFF/zero; tiny camera; reload; resize; combined; default="+delta+" high="+high+" directional="+directions);
|
||||
}
|
||||
private static void materials(){
|
||||
int[] flat=new int[16];Arrays.fill(flat,0xFF808080);var profile=PbrMaterials.profile("bricks");
|
||||
for(int p:PbrMaterials.generate(flat,4,4,profile))check((p>>8&0xFFFF)==0x8080,"Flat source has neutral normal");
|
||||
flat[5]=0xFFFFFFFF;var slope=PbrMaterials.generate(flat,4,4,profile);check((slope[4]>>16&255)<128&&(slope[6]>>16&255)>128,"Height gradients produce opposite normal slopes");
|
||||
check(PbrMaterials.profile("iron_block").metal()>.8&&PbrMaterials.profile("iron_ore").metal()==0,"Metal block profile does not turn ore host rock into metal");
|
||||
check(PbrMaterials.profile("oak_planks").roughness()>PbrMaterials.profile("polished_andesite").roughness(),"Wood remains rougher than polished stone");
|
||||
}
|
||||
private static void preferences(){try{
|
||||
var p=FabricLoader.getInstance().getConfigDir().resolve("sanctuary-shaders.json");Files.writeString(p,"{\"ssgi\":true,\"ssgiIntensity\":35,\"bloom\":false}");forgetPreferences();
|
||||
check(VanillaLight.pbr()&&VanillaLight.pbrIntensity()==50&&VanillaLight.ssgi()&&!VanillaLight.bloom(),"Missing PBR setting defaults ON while preserving explicit old choices");
|
||||
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);}}
|
||||
private record Delta(int changed,int brighter,int darker,long total){}
|
||||
private static Delta difference(Frame off,Frame on){int changed=0,bright=0,dark=0;long total=0;for(int i=0;i<off.pixels().length;i++){
|
||||
int a=off.pixels()[i],b=on.pixels()[i],delta=(b>>16&255)-(a>>16&255);total+=Math.abs(delta);if(Math.abs(delta)>3)changed++;if(delta>3)bright++;if(delta< -3)dark++;
|
||||
}return new Delta(changed,bright,dark,total);}
|
||||
private static void settle(ClientGameTestContext c){c.waitTicks(35);c.waitFor(m->m.levelRenderer.hasRenderedAllSections(),1200);c.waitTicks(10);}
|
||||
private static void enable(ClientGameTestContext c,boolean on){c.runOnClient(m->VanillaLight.setPbr(on));if(on)frames(c);else c.waitTicks(8);}
|
||||
private static void frames(ClientGameTestContext c){int start=c.computeOnClient(m->ProceduralPbr.renderedFrames());c.waitFor(m->ProceduralPbr.applied()&&ProceduralPbr.ready()&&ProceduralPbr.renderedFrames()>start+10,1200);c.waitTicks(8);}
|
||||
}
|
||||
+674
@@ -0,0 +1,674 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.client.HelloWorldScreen;
|
||||
import fr.koka.sanctuary.client.IntroClient;
|
||||
import fr.koka.sanctuary.client.IntroSpawnFlash;
|
||||
import fr.koka.sanctuary.client.ProgressionClient;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.TestServerContext;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.minecraft.client.CameraType;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.Screenshot;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.client.renderer.culling.Frustum;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.StairBlock;
|
||||
import net.minecraft.world.level.gamerules.GameRules;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Real terrain/depth-map checks on a new flat seed 122 world; GPU readback is test-only. */
|
||||
public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
||||
private final boolean bloomOnly,ssgiOnly,pbrOnly,closeupOnly;
|
||||
public PixelShadows122ClientChecks(){this(false);}
|
||||
PixelShadows122ClientChecks(boolean bloomOnly){this(bloomOnly,false);}
|
||||
PixelShadows122ClientChecks(boolean bloomOnly,boolean ssgiOnly){this(bloomOnly,ssgiOnly,false);}
|
||||
PixelShadows122ClientChecks(boolean bloomOnly,boolean ssgiOnly,boolean pbrOnly){this(bloomOnly,ssgiOnly,pbrOnly,false);}
|
||||
PixelShadows122ClientChecks(boolean bloomOnly,boolean ssgiOnly,boolean pbrOnly,boolean closeupOnly){this.bloomOnly=bloomOnly;this.ssgiOnly=ssgiOnly;this.pbrOnly=pbrOnly;this.closeupOnly=closeupOnly;}
|
||||
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);
|
||||
private static final BlockPos STAIR = new BlockPos(5, FLOOR + 3, 5);
|
||||
private static final int DARK_DELTA = 8;
|
||||
|
||||
@Override
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
var path = FabricLoader.getInstance().getConfigDir().resolve("sanctuary-shaders.json");
|
||||
final byte[] originalPreferences;
|
||||
try {
|
||||
originalPreferences = Files.exists(path) ? Files.readAllBytes(path) : null;
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("Cannot back up shader preferences", e);
|
||||
}
|
||||
var originalOptions = c.computeOnClient(Options::capture);
|
||||
var onboarding = new AtomicBoolean(true);
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m -> {
|
||||
if (onboarding.get() && m.gui.screen() instanceof HelloWorldScreen screen) {
|
||||
// Public test support works across packages; enter through the native widgets.
|
||||
IntroClient.setQuickTestIntro(true);
|
||||
if (!screen.starterChoices().isEmpty()) Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.filter(b -> b.getClass().getSimpleName().equals("ColorButton"))
|
||||
.findFirst().orElseThrow().onPress(enter());
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.filter(b -> b.getMessage().getContents() instanceof TranslatableContents t
|
||||
&& t.getKey().equals("sanctuary.progression.enter"))
|
||||
.findFirst().orElseThrow().onPress(enter());
|
||||
onboarding.set(false);
|
||||
}
|
||||
});
|
||||
try {
|
||||
c.runOnClient(m -> {
|
||||
m.options.renderDistance().set(6);
|
||||
m.options.simulationDistance().set(5);
|
||||
m.options.bobView().set(false);
|
||||
m.options.pauseOnLostFocus = false;
|
||||
m.options.setCameraType(CameraType.FIRST_PERSON);
|
||||
checkPreferences();
|
||||
});
|
||||
try (var world = c.worldBuilder().adjustSettings(s -> {
|
||||
s.setName("PixelShadows122 " + UUID.randomUUID());
|
||||
s.setSeed("122");
|
||||
s.setAllowCommands(true);
|
||||
s.setGameMode(WorldCreationUiState.SelectedGameMode.CREATIVE);
|
||||
s.setWorldType(s.getNormalPresetList().stream().filter(e -> e.preset() != null
|
||||
&& e.preset().getRegisteredName().equals("sanctuary_test:flat"))
|
||||
.findFirst().orElseThrow());
|
||||
}).create()) {
|
||||
world.getConnection().waitForChunksDownload(2400);
|
||||
c.waitFor(m -> m.player != null && ProgressionClient.active()
|
||||
&& m.gui.screen() == null && !IntroSpawnFlash.active(), 1200);
|
||||
c.getInput().pressKey(InputConstants.KEY_F1);
|
||||
var server = world.getServer();
|
||||
server.runOnServer(s -> {
|
||||
command(s, "sanctuary time vanilla");
|
||||
command(s, "sanctuary weather vanilla");
|
||||
command(s, "weather clear 100000");
|
||||
if (s.getGlobalGameRules().get(GameRules.ADVANCE_TIME))
|
||||
command(s, "gamerule " + GameRules.ADVANCE_TIME.id() + " false");
|
||||
check(!s.getGlobalGameRules().get(GameRules.ADVANCE_TIME), "Native daylight rule is frozen");
|
||||
command(s, "time set 3000");
|
||||
command(s, "gamemode spectator @a");
|
||||
if (s.getGlobalGameRules().get(GameRules.SPAWN_MOBS))
|
||||
command(s, "gamerule " + GameRules.SPAWN_MOBS.id() + " false");
|
||||
var level = s.overworld();
|
||||
var entities = new java.util.ArrayList<net.minecraft.world.entity.Entity>();
|
||||
level.getAllEntities().forEach(entities::add);
|
||||
entities.stream().filter(e -> !(e instanceof net.minecraft.server.level.ServerPlayer))
|
||||
.forEach(net.minecraft.world.entity.Entity::discard);
|
||||
// The elevated platform isolates the samples from grass, mobs and flat-world lighting.
|
||||
for (int x = -24; x <= 24; x++) for (int z = -24; z <= 24; z++) {
|
||||
level.setBlockAndUpdate(new BlockPos(x, FLOOR, z), Blocks.CONCRETE.white().defaultBlockState());
|
||||
}
|
||||
pillar(s, true);
|
||||
// Coloured casters cannot be mistaken for neutral white receiving ground.
|
||||
level.setBlockAndUpdate(SLAB, Blocks.RED_SANDSTONE_SLAB.defaultBlockState());
|
||||
level.setBlockAndUpdate(STAIR, Blocks.RED_SANDSTONE_STAIRS.defaultBlockState()
|
||||
.setValue(StairBlock.FACING, Direction.WEST));
|
||||
});
|
||||
camera(c, server, -2, FLOOR + 18, 1, 70);
|
||||
c.waitFor(m -> m.level.getBlockState(new BlockPos(0, FLOOR + 8, 0)).is(Blocks.CONCRETE.red())
|
||||
&& m.level.getBlockState(SLAB).is(Blocks.RED_SANDSTONE_SLAB)
|
||||
&& m.level.getBlockState(STAIR).is(Blocks.RED_SANDSTONE_STAIRS), 200);
|
||||
c.waitFor(m -> {
|
||||
for(int x=-24;x<=24;x+=8) for(int z=-24;z<=24;z+=8)
|
||||
if(!m.level.getBlockState(new BlockPos(x,FLOOR,z)).is(Blocks.CONCRETE.white()))return false;
|
||||
return m.levelRenderer.hasRenderedAllSections();
|
||||
},1200);
|
||||
c.waitTicks(40);
|
||||
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(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;}
|
||||
// Keep VanillaLight enabled for both frames: changing its fog/grade would invalidate this oracle.
|
||||
shadows(c, false);
|
||||
var off = capture(c, "day-off");
|
||||
c.waitTicks(6);
|
||||
unchanged(off, capture(c, null), "Fixed camera/daylight OFF control");
|
||||
shadows(c, true);
|
||||
var on = capture(c, "day-on-pillar-slab-stair");
|
||||
var day = compare(off, on);
|
||||
localShadow(day, "Pillar/slab/stair daylight ground", .70);
|
||||
c.runOnClient(m -> {
|
||||
check(Float.compare(morning, m.gameRenderer.gameRenderState().levelRenderState.skyRenderState.sunAngle) == 0,
|
||||
"Native rule actually freezes the rendered sun angle");
|
||||
check(PixelShadows.shadowMap() != null, "Daylight allocates a real shadow target");
|
||||
check(PixelShadows.shadowMap().width == PixelShadows.mapSize(32, 16), "Shadow map follows selected quality");
|
||||
});
|
||||
for (int pixels : new int[]{8, 32, 64, 128, 16}) {
|
||||
c.runOnClient(m -> VanillaLight.setShadowPixels(pixels));
|
||||
activeFrames(c);
|
||||
c.runOnClient(m -> check(PixelShadows.shadowMap().width == PixelShadows.mapSize(32, pixels),
|
||||
"Live pixel density reallocates the correct depth map"));
|
||||
capture(c, "precision-" + pixels);
|
||||
}
|
||||
for (int distance : new int[]{16, 64, 128, 256, 32}) {
|
||||
c.runOnClient(m -> VanillaLight.setShadowDistance(distance));
|
||||
activeFrames(c);
|
||||
c.runOnClient(m -> check(PixelShadows.shadowMap().width == PixelShadows.mapSize(distance, 16),
|
||||
"Live distance reallocates the correct depth map"));
|
||||
}
|
||||
shadows(c, false);
|
||||
unchanged(off, capture(c, "day-off-restored"), "Disabling removes the shadow without residual grading");
|
||||
|
||||
// Aim at the shadow's far end, with its entire caster outside the actual camera frustum.
|
||||
shadows(c, true);
|
||||
double shadowX = c.computeOnClient(m -> {
|
||||
var sun = PixelShadows.sunDirection(m.gameRenderer.gameRenderState().levelRenderState.skyRenderState.sunAngle);
|
||||
check(sun.y > .2f && Math.abs(sun.x / sun.y) > .5f, "Morning has an oblique sun for offscreen casting");
|
||||
return 1.0 - 7.0 * sun.x / sun.y;
|
||||
});
|
||||
camera(c, server, shadowX, FLOOR + 5, 1, 30);
|
||||
c.runOnClient(m -> {
|
||||
var state = m.gameRenderer.gameRenderState().levelRenderState.cameraRenderState;
|
||||
var frustum = new Frustum(state.viewRotationMatrix, state.projectionMatrix);
|
||||
frustum.prepare(state.pos.x, state.pos.y, state.pos.z);
|
||||
check(!frustum.isVisible(PILLAR), "Whole pillar is outside the receiver camera frustum");
|
||||
check(!frustum.isVisible(new AABB(SLAB)) && !frustum.isVisible(new AABB(STAIR)),
|
||||
"Slab and stair are also outside the offscreen capture");
|
||||
});
|
||||
shadows(c, false);
|
||||
var offscreenOff = capture(c, "offscreen-off");
|
||||
shadows(c, true);
|
||||
var offscreenOn = capture(c, "offscreen-on");
|
||||
localShadow(compare(offscreenOff, offscreenOn), "Offscreen pillar still casts onto visible ground", .20);
|
||||
|
||||
// Do not toggle/release/rebuild the renderer here: real server edits must invalidate its live cache.
|
||||
server.runOnServer(s -> pillar(s, false));
|
||||
c.waitFor(m -> m.level.getBlockState(new BlockPos(0, FLOOR + 8, 0)).isAir(), 200);
|
||||
activeFrames(c);
|
||||
var removed = capture(c, "offscreen-caster-removed");
|
||||
restoredGround(offscreenOff, offscreenOn, removed);
|
||||
server.runOnServer(s -> pillar(s, true));
|
||||
c.waitFor(m -> m.level.getBlockState(new BlockPos(0, FLOOR + 8, 0)).is(Blocks.CONCRETE.red()), 200);
|
||||
activeFrames(c);
|
||||
var replaced = capture(c, "offscreen-caster-replaced");
|
||||
localShadow(compare(offscreenOff, replaced), "Replacing caster restores its shadow", .20);
|
||||
unchanged(offscreenOn, replaced, "Same caster restores the same ground shadow");
|
||||
|
||||
// beta.128: the moon now casts a weaker shadow using its native celestial angle.
|
||||
server.runOnServer(s -> command(s, "time set 15000"));
|
||||
c.waitFor(m -> PixelShadows.sunDirection(m.gameRenderer.gameRenderState().levelRenderState.skyRenderState.sunAngle).y < 0, 200);
|
||||
camera(c, server, -2, FLOOR + 18, 1, 70);
|
||||
shadows(c, false);
|
||||
var moonOff = capture(c, "moon-off");
|
||||
shadows(c, true);
|
||||
var moonOn = capture(c, "moon-on");
|
||||
var moonStats = opacity(moonOff, moonOn);
|
||||
check(moonStats.dark > 100 && moonStats.maximum > .025 && moonStats.maximum < .14,
|
||||
"Moon creates localized subtle shadows: " + moonStats);
|
||||
unchanged(moonOn, capture(c, null), "Frozen moon shadow is stable");
|
||||
server.runOnServer(s -> command(s, "time set 111000"));
|
||||
c.waitFor(m -> m.gameRenderer.gameRenderState().levelRenderState.skyRenderState.moonPhase.index() == 4
|
||||
&& !PixelShadows.applied(), 200);
|
||||
c.waitTicks(6);
|
||||
int newMoonFrames = c.computeOnClient(m -> PixelShadows.renderedFrames());
|
||||
c.waitTicks(6);
|
||||
c.runOnClient(m -> check(PixelShadows.renderedFrames() == newMoonFrames, "New moon submits no shadow pass"));
|
||||
noonAndFoliage(c, server);
|
||||
|
||||
c.waitFor(m -> PixelShadows.sunDirection(m.gameRenderer.gameRenderState().levelRenderState.skyRenderState.sunAngle).y > .2f,
|
||||
200);
|
||||
shadows(c, true);
|
||||
camera(c, server, -2, FLOOR + 18, 1, 70);
|
||||
c.waitTicks(30);
|
||||
var beforeReload = capture(c, "before-reload");
|
||||
var reload = c.computeOnClient(Minecraft::reloadResourcePacks);
|
||||
c.waitFor(m -> reload.isDone() && m.gui.overlay() == null, 1200);
|
||||
reload.join();
|
||||
c.runOnClient(m -> {
|
||||
forgetPreferences();
|
||||
check(VanillaLight.enabled() && VanillaLight.pixelShadows()
|
||||
&& VanillaLight.shadowPixels() == 16 && VanillaLight.shadowDistance() == 32,
|
||||
"Resource reload and disk reread retain enabled shadow preferences");
|
||||
});
|
||||
activeFrames(c);
|
||||
unchanged(beforeReload, capture(c, "after-reload"), "Resource reload preserves the rendered ground shadow");
|
||||
RealtimeShadows129ClientChecks.run(c, server);
|
||||
EdgeHighlights129ClientChecks.run(c, server);
|
||||
Bloom130ClientChecks.run(c, server);
|
||||
Ssgi132ClientChecks.run(c, server);
|
||||
Pbr134ClientChecks.run(c, server);
|
||||
c.getInput().pressKey(InputConstants.KEY_F1);
|
||||
for (String locale : new String[]{"fr_fr", "en_us"}) {
|
||||
c.runOnClient(m -> {
|
||||
m.getLanguageManager().setSelected(locale);
|
||||
m.getLanguageManager().onResourceManagerReload(m.getResourceManager());
|
||||
for (String key : new String[]{"pixel_shadows", "shadow_pixels", "shadow_distance", "edge_highlights", "edge_intensity", "edge_distance", "precision", "ssgi", "ssgi_intensity", "bloom", "bloom_radius", "ore_emission"}) {
|
||||
String fullKey = "sanctuary.shaders." + key;
|
||||
check(!Component.translatable(fullKey).getString().equals(fullKey), "Translated " + locale + " " + key);
|
||||
}
|
||||
m.gui.setScreen(new ShaderOptionsScreen(null));
|
||||
widgets(m.gui.screen()).filter(net.minecraft.client.gui.components.CycleButton.class::isInstance)
|
||||
.map(net.minecraft.client.gui.components.AbstractWidget.class::cast).forEach(button -> {
|
||||
check(m.font.width(button.getMessage()) < button.getWidth() - 8,
|
||||
"Localized shader cycle fits: " + button.getMessage().getString());
|
||||
});
|
||||
});
|
||||
c.waitTicks(3);
|
||||
c.takeScreenshot("sanctuary-beta135-shader-options-" + locale);
|
||||
}
|
||||
c.runOnClient(m -> m.gui.setScreen(null));
|
||||
}
|
||||
System.out.println("PIXEL_SHADOWS122_CLIENT_PASS: bounded saved preferences; native GPU ON/OFF ground; "
|
||||
+ "offscreen caster; live removal/replacement; subtle moon; full-strength noon; foliage density; camera stability; resource reload; FR/EN screenshots");
|
||||
} finally {
|
||||
onboarding.set(false);
|
||||
c.runOnClient(m -> {
|
||||
PixelShadows.release();
|
||||
originalOptions.restore(m);
|
||||
IntroClient.setQuickTestIntro(false);
|
||||
try {
|
||||
if (originalPreferences == null) Files.deleteIfExists(path);
|
||||
else Files.write(path, originalPreferences);
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("Cannot restore shader preferences", e);
|
||||
} finally {
|
||||
forgetPreferences();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static void noonAndFoliage(ClientGameTestContext c, TestServerContext server) {
|
||||
// Overhead caster keeps a visible noon shadow while the camera sits below it.
|
||||
server.runOnServer(s -> {
|
||||
pillar(s, false);
|
||||
s.overworld().setBlockAndUpdate(SLAB, Blocks.AIR.defaultBlockState());
|
||||
s.overworld().setBlockAndUpdate(STAIR, Blocks.AIR.defaultBlockState());
|
||||
canopy(s, 1, false, 3);
|
||||
command(s, "time set 6000");
|
||||
});
|
||||
camera(c, server, .99999, FLOOR + 5, 1, 70);
|
||||
c.waitTicks(30);
|
||||
shadows(c, false);
|
||||
var noonOff = capture(c, "noon-off");
|
||||
shadows(c, true);
|
||||
var noonOn = capture(c, "noon-on");
|
||||
var noonStats = opacity(noonOff, noonOn);
|
||||
crispEdge(noonOff, noonOn);
|
||||
check(noonStats.dark > 100 && noonStats.maximum > .34 && noonStats.maximum < .42,
|
||||
"Noon shadow keeps its full daytime strength: " + noonStats);
|
||||
camera(c, server, 1.00001, FLOOR + 5, 1, 70);
|
||||
unchanged(noonOn, capture(c, "noon-camera-subpixel"), "Noon tiny motion across former integer anchor");
|
||||
camera(c, server, .99999, FLOOR + 5, 1, 70);
|
||||
unchanged(noonOn, capture(c, null), "Noon camera return");
|
||||
look(c, server, 35, 60);
|
||||
shadows(c, false);
|
||||
var obliqueOff = capture(c, "noon-oblique-off");
|
||||
shadows(c, true);
|
||||
var obliqueNoon = capture(c, "noon-oblique");
|
||||
localShadow(compare(obliqueOff, obliqueNoon), "Oblique noon camera sees actual white receiving ground", .10);
|
||||
look(c, server, 35.00002f, 60.00002f);
|
||||
unchanged(obliqueNoon, capture(c, null), "Subpixel rotation with an oblique receiver");
|
||||
// Both sides of noon, frozen individually: no frame-to-frame edge flicker.
|
||||
for (int time : new int[]{5990, 6010}) {
|
||||
server.runOnServer(s -> command(s, "time set " + time));
|
||||
c.waitTicks(10);
|
||||
var stable = capture(c, "noon-" + time);
|
||||
c.waitTicks(10);
|
||||
unchanged(stable, capture(c, null), "Frozen near-noon " + time);
|
||||
}
|
||||
server.runOnServer(s -> command(s, "time set 5999"));
|
||||
c.waitTicks(10);
|
||||
var beforeNoon = capture(c, null);
|
||||
server.runOnServer(s -> command(s, "time set 6001"));
|
||||
c.waitTicks(10);
|
||||
unchanged(beforeNoon, capture(c, "noon-crossing"), "Continuous passage through noon has no large flip");
|
||||
server.runOnServer(s -> {
|
||||
canopy(s, 1, true, 16);
|
||||
command(s, "time set 3000");
|
||||
});
|
||||
camera(c, server, 0, FLOOR + 5, 0, 60);
|
||||
c.waitTicks(30);
|
||||
shadows(c, false);
|
||||
var thinOff = capture(c, "leaves-thin-off");
|
||||
shadows(c, true);
|
||||
var thinOn = capture(c, "leaves-thin-on");
|
||||
var thin = opacity(thinOff, thinOn);
|
||||
c.waitTicks(8);
|
||||
unchanged(thinOn, capture(c, null), "Leaf noise is independent of frame/time");
|
||||
// Keep the live cache: adding inner leaf layers must invalidate it too.
|
||||
server.runOnServer(s -> canopy(s, 4, true, 16));
|
||||
c.waitTicks(30);
|
||||
activeFrames(c);
|
||||
var thickOn = capture(c, "leaves-thick-on");
|
||||
shadows(c, false);
|
||||
var thickOff = capture(c, "leaves-thick-off");
|
||||
var thick = opacity(thickOff, thickOn);
|
||||
server.runOnServer(s -> canopy(s, 4, false, 16));
|
||||
c.waitTicks(30);
|
||||
var solidOff = capture(c, "canopy-solid-off");
|
||||
shadows(c, true);
|
||||
var solid = opacity(solidOff, capture(c, "canopy-solid-on"));
|
||||
check(thin.mean > .02 && thick.mean > thin.mean + .025 && solid.mean > thick.mean + .01,
|
||||
"Depth increases foliage opacity, bounded by solid: thin=" + thin + " thick=" + thick + " solid=" + solid);
|
||||
check(thin.dark > 100 && thin.lit > 100, "Thin foliage has both light holes and shadows: " + thin);
|
||||
server.runOnServer(s -> {
|
||||
for (int x=-16;x<=16;x++) for(int z=-16;z<=16;z++) for(int y=FLOOR+10;y<FLOOR+13;y++)
|
||||
s.overworld().setBlockAndUpdate(new BlockPos(x,y,z), Blocks.AIR.defaultBlockState());
|
||||
canopy(s, 1, true, 16);
|
||||
});
|
||||
c.waitTicks(30);
|
||||
// Crossing a 16-block origin boundary must preserve the same world shadow.
|
||||
camera(c, server, 7.99999, FLOOR + 5, 0, 60);
|
||||
activeFrames(c);
|
||||
var boundary = capture(c, null);
|
||||
camera(c, server, 8.00001, FLOOR + 5, 0, 60);
|
||||
activeFrames(c);
|
||||
unchanged(boundary, capture(c, "anchor-boundary"), "Subpixel motion across 16-block anchor");
|
||||
System.out.println("SHADOWS128_PASS moon/noon plus stable anchors; foliage thin=" + thin + " thick=" + thick + " solid=" + solid);
|
||||
server.runOnServer(s -> {
|
||||
for (int x=-16;x<=16;x++) for(int z=-16;z<=16;z++) for(int y=FLOOR+9;y<FLOOR+13;y++)
|
||||
s.overworld().setBlockAndUpdate(new BlockPos(x,y,z), Blocks.AIR.defaultBlockState());
|
||||
pillar(s, true);
|
||||
});
|
||||
}
|
||||
|
||||
private static void canopy(MinecraftServer s, int layers, boolean leaves, int radius) {
|
||||
var state = leaves ? Blocks.OAK_LEAVES.defaultBlockState().setValue(net.minecraft.world.level.block.LeavesBlock.PERSISTENT, true)
|
||||
: Blocks.CONCRETE.red().defaultBlockState();
|
||||
for (int x=-radius;x<=radius;x++) for(int z=-radius;z<=radius;z++) for(int y=FLOOR+9;y<FLOOR+9+layers;y++)
|
||||
s.overworld().setBlockAndUpdate(new BlockPos(x,y,z), state);
|
||||
}
|
||||
|
||||
private static void crispEdge(Frame off, Frame on) {
|
||||
int samples = 0, intermediate = 0;
|
||||
for (int y=off.height/10;y<off.height*9/10;y+=2) for (int x=off.width/10;x<off.width*9/10;x+=2) {
|
||||
int i=y*off.width+x;
|
||||
if (!whiteGround(off.pixels[i])) continue;
|
||||
samples++;
|
||||
double ratio=(brightness(off.pixels[i])-brightness(on.pixels[i]))/(double)brightness(off.pixels[i]);
|
||||
if (ratio > .015 && ratio < .32) intermediate++;
|
||||
}
|
||||
check(samples > 1000 && intermediate < samples / 200,
|
||||
"Opaque noon edge stays crisp, no foliage smoothing: " + intermediate + "/" + samples);
|
||||
System.out.println("SHADOWS128_CRISP intermediate=" + intermediate + "/" + samples);
|
||||
}
|
||||
|
||||
private record Opacity(int samples, int dark, int lit, double mean, double maximum) { }
|
||||
private static Opacity opacity(Frame off, Frame on) {
|
||||
sameSize(off,on);
|
||||
int samples=0,dark=0,lit=0;double sum=0,maximum=0;
|
||||
for(int y=off.height/10;y<off.height*9/10;y+=2) for(int x=off.width/10;x<off.width*9/10;x+=2) {
|
||||
int i=y*off.width+x,a=off.pixels[i],b=on.pixels[i];
|
||||
int r=a>>16&255,g=a>>8&255,blue=a&255;
|
||||
if(Math.min(r,Math.min(g,blue))<24 || Math.max(r,Math.max(g,blue))-Math.min(r,Math.min(g,blue))>64)continue;
|
||||
double ratio=(brightness(a)-brightness(b))/(double)brightness(a);
|
||||
samples++;sum+=ratio;maximum=Math.max(maximum,ratio);
|
||||
if(ratio>.02)dark++;else if(Math.abs(ratio)<.01)lit++;
|
||||
}
|
||||
check(samples>1000,"White concrete (including blue moonlight) available for shadow opacity");
|
||||
var result=new Opacity(samples,dark,lit,sum/samples,maximum);
|
||||
System.out.println("SHADOWS128_OPACITY " + result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static java.util.stream.Stream<net.minecraft.client.gui.components.events.GuiEventListener> widgets(
|
||||
net.minecraft.client.gui.components.events.ContainerEventHandler root) {
|
||||
return root.children().stream().flatMap(widget -> widget instanceof net.minecraft.client.gui.components.events.ContainerEventHandler child
|
||||
? 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.setPbr(false);
|
||||
VanillaLight.setSsgi(false);
|
||||
VanillaLight.setBloom(false);
|
||||
VanillaLight.setOreEmission(false);
|
||||
VanillaLight.setEnabled(true);
|
||||
VanillaLight.setEdgeHighlights(false);
|
||||
VanillaLight.setSaturation(100);
|
||||
for (int[] pair : new int[][]{{Integer.MIN_VALUE, 8}, {11, 8}, {12, 16}, {16, 16}, {23, 16}, {24, 32}, {47,32}, {48,64}, {64,64}, {95,64}, {96,128}, {128,128}, {Integer.MAX_VALUE,128}}) {
|
||||
VanillaLight.setShadowPixels(pair[0]);
|
||||
forgetPreferences();
|
||||
check(VanillaLight.shadowPixels() == pair[1], "Persisted pixel density rounds/clamps " + pair[0]);
|
||||
}
|
||||
for (int[] pair : new int[][]{{Integer.MIN_VALUE, 16}, {23, 16}, {24, 32}, {32, 32}, {47, 32}, {48, 64}, {95,64}, {96,128}, {191,128}, {192,256}, {256,256}, {Integer.MAX_VALUE, 256}}) {
|
||||
VanillaLight.setShadowDistance(pair[0]);
|
||||
forgetPreferences();
|
||||
check(VanillaLight.shadowDistance() == pair[1], "Persisted shadow distance rounds/clamps " + pair[0]);
|
||||
}
|
||||
for (boolean enabled : new boolean[]{true, false}) {
|
||||
VanillaLight.setPixelShadows(enabled);
|
||||
forgetPreferences();
|
||||
check(VanillaLight.pixelShadows() == enabled && VanillaLight.enabled(), "Saved shadow toggle is independent of main shader");
|
||||
}
|
||||
VanillaLight.setShadowPixels(16);
|
||||
VanillaLight.setShadowDistance(32);
|
||||
}
|
||||
|
||||
static void forgetPreferences() {
|
||||
try {
|
||||
var field = VanillaLight.class.getDeclaredField("preferences");
|
||||
field.setAccessible(true);
|
||||
field.set(null, null);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new AssertionError("Cannot reread actual shader preferences", e);
|
||||
}
|
||||
}
|
||||
|
||||
static void camera(ClientGameTestContext c, TestServerContext server, double x, double y, double z, int fov) {
|
||||
server.runOnServer(s -> {
|
||||
var p = s.getPlayerList().getPlayers().getFirst();
|
||||
command(s, "tp @a " + x + " " + y + " " + z + " 0 90");
|
||||
p.setDeltaMovement(Vec3.ZERO);
|
||||
});
|
||||
c.waitFor(m -> m.player.position().distanceToSqr(new Vec3(x, y, z)) < .0001
|
||||
&& Math.abs(m.player.getXRot() - 90) < .001, 200);
|
||||
c.runOnClient(m -> {
|
||||
m.options.fov().set(fov);
|
||||
m.player.setYRot(0);
|
||||
m.player.yRotO = 0;
|
||||
m.player.setXRot(90);
|
||||
m.player.xRotO = 90;
|
||||
m.player.setDeltaMovement(Vec3.ZERO);
|
||||
});
|
||||
c.waitTicks(15);
|
||||
}
|
||||
|
||||
static void look(ClientGameTestContext c, TestServerContext server, float yaw, float pitch) {
|
||||
server.runOnServer(s -> {
|
||||
var p = s.getPlayerList().getPlayers().getFirst();
|
||||
command(s, "tp @a " + p.getX() + " " + p.getY() + " " + p.getZ() + String.format(java.util.Locale.ROOT," %.8f %.8f",yaw,pitch));
|
||||
});
|
||||
c.waitFor(m -> Math.abs(m.player.getXRot() - pitch) < .00001
|
||||
&& Math.abs(m.player.getYRot() - yaw) < .00001, 200);
|
||||
c.waitTicks(15);
|
||||
}
|
||||
|
||||
private static void pillar(MinecraftServer s, boolean present) {
|
||||
for (int x = 0; x < 2; x++) for (int z = 0; z < 2; z++) for (int y = FLOOR + 1; y <= FLOOR + 8; y++) {
|
||||
s.overworld().setBlockAndUpdate(new BlockPos(x, y, z),
|
||||
(present ? Blocks.CONCRETE.red() : Blocks.AIR).defaultBlockState());
|
||||
}
|
||||
}
|
||||
|
||||
static void command(MinecraftServer s, String command) {
|
||||
try {
|
||||
// Dispatcher exceptions fail the fixture instead of leaving an ignored command error in chat.
|
||||
s.getCommands().getDispatcher().execute(command, s.createCommandSourceStack());
|
||||
} catch (com.mojang.brigadier.exceptions.CommandSyntaxException e) {
|
||||
throw new AssertionError("Fixture command failed: " + command, e);
|
||||
}
|
||||
}
|
||||
|
||||
static void shadows(ClientGameTestContext c, boolean enabled) {
|
||||
c.runOnClient(m -> VanillaLight.setPixelShadows(enabled));
|
||||
if (enabled) activeFrames(c);
|
||||
else {
|
||||
c.waitTicks(6);
|
||||
int frames = c.computeOnClient(m -> {
|
||||
check(!PixelShadows.applied() && PixelShadows.shadowMap() == null, "OFF releases the GPU shadow target");
|
||||
return PixelShadows.renderedFrames();
|
||||
});
|
||||
c.waitTicks(3);
|
||||
c.runOnClient(m -> check(PixelShadows.renderedFrames() == frames, "OFF submits no shadow frames"));
|
||||
}
|
||||
}
|
||||
|
||||
static void activeFrames(ClientGameTestContext c) {
|
||||
int before = c.computeOnClient(m -> PixelShadows.renderedFrames());
|
||||
c.waitFor(m -> PixelShadows.applied() && PixelShadows.ready() && PixelShadows.renderedFrames() > before + 12, 200);
|
||||
// Allow the bounded terrain cache and native light/chunk uploads to settle before reading pixels.
|
||||
c.waitTicks(30);
|
||||
}
|
||||
|
||||
static Frame capture(ClientGameTestContext c, String name) {
|
||||
if (name != null) c.takeScreenshot("sanctuary-beta135-shader-" + name);
|
||||
var result = new CompletableFuture<Frame>();
|
||||
c.runOnClient(m -> Screenshot.takeScreenshot(m.gameRenderer.mainRenderTarget(), image -> {
|
||||
try (image) {
|
||||
result.complete(new Frame(image.getWidth(), image.getHeight(), image.getPixels()));
|
||||
} catch (Throwable e) {
|
||||
result.completeExceptionally(e);
|
||||
}
|
||||
}));
|
||||
c.waitFor(m -> result.isDone(), 200);
|
||||
return result.join();
|
||||
}
|
||||
|
||||
record Frame(int width, int height, int[] pixels) { }
|
||||
private record Difference(int sampled, int ground, int dark, int stable, int brighter) { }
|
||||
|
||||
private static Difference compare(Frame before, Frame after) {
|
||||
sameSize(before, after);
|
||||
int sampled = 0, ground = 0, dark = 0, stable = 0, brighter = 0;
|
||||
// Skip borders and sample every other pixel; no PNG decoding, image libraries or golden screenshot dependency.
|
||||
for (int y = before.height / 10; y < before.height * 9 / 10; y += 2) {
|
||||
for (int x = before.width / 10; x < before.width * 9 / 10; x += 2) {
|
||||
sampled++;
|
||||
int a = before.pixels[y * before.width + x], b = after.pixels[y * after.width + x];
|
||||
if (!whiteGround(a)) continue;
|
||||
ground++;
|
||||
int delta = brightness(a) - brightness(b);
|
||||
if (delta >= DARK_DELTA) dark++;
|
||||
if (delta <= -DARK_DELTA) brighter++;
|
||||
if (channelDifference(a, b) <= 3) stable++;
|
||||
}
|
||||
}
|
||||
return new Difference(sampled, ground, dark, stable, brighter);
|
||||
}
|
||||
|
||||
private static void localShadow(Difference d, String label, double minimumStable) {
|
||||
System.out.println("PIXEL_SHADOWS122_PIXELS " + label + ": " + d);
|
||||
check(d.ground > d.sampled * .50, label + ": a majority of samples must be actual white receiving ground: " + d);
|
||||
check(d.dark >= Math.max(24, d.ground / 1000), label + ": measurable cast-shadow darkening: " + d);
|
||||
check(d.stable > d.ground * minimumStable, label + ": unshadowed ground stays unchanged; reject a global tint: " + d);
|
||||
check(d.brighter < Math.max(12, d.ground / 100), label + ": shadows must not brighten ground: " + d);
|
||||
}
|
||||
|
||||
static void unchanged(Frame before, Frame after, String label) {
|
||||
sameSize(before, after);
|
||||
int sampled = 0, changed = 0;
|
||||
for (int y = before.height / 10; y < before.height * 9 / 10; y += 2) {
|
||||
for (int x = before.width / 10; x < before.width * 9 / 10; x += 2) {
|
||||
sampled++;
|
||||
int i = y * before.width + x;
|
||||
if (channelDifference(before.pixels[i], after.pixels[i]) > 3) changed++;
|
||||
}
|
||||
}
|
||||
check(sampled > 0 && changed <= sampled / 100, label + ": changed=" + changed + "/" + sampled);
|
||||
}
|
||||
|
||||
private static void restoredGround(Frame off, Frame on, Frame removed) {
|
||||
sameSize(off, on);
|
||||
sameSize(off, removed);
|
||||
int shadow = 0, restored = 0, lit = 0, stable = 0;
|
||||
for (int y = off.height / 10; y < off.height * 9 / 10; y += 2) {
|
||||
for (int x = off.width / 10; x < off.width * 9 / 10; x += 2) {
|
||||
int i = y * off.width + x;
|
||||
if (!whiteGround(off.pixels[i])) continue;
|
||||
if (brightness(off.pixels[i]) - brightness(on.pixels[i]) >= DARK_DELTA) {
|
||||
shadow++;
|
||||
if (brightness(removed.pixels[i]) - brightness(on.pixels[i]) >= DARK_DELTA
|
||||
&& channelDifference(off.pixels[i], removed.pixels[i]) <= 4) restored++;
|
||||
} else if (channelDifference(off.pixels[i], on.pixels[i]) <= 3) {
|
||||
lit++;
|
||||
if (channelDifference(on.pixels[i], removed.pixels[i]) <= 3) stable++;
|
||||
}
|
||||
}
|
||||
}
|
||||
check(shadow >= 24 && restored > shadow * .85,
|
||||
"Removing offscreen caster relights its former shadow: " + restored + "/" + shadow);
|
||||
check(lit >= 24 && stable > lit * .98,
|
||||
"Removing caster preserves already sunlit ground: " + stable + "/" + lit);
|
||||
System.out.println("PIXEL_SHADOWS122_EDIT restored=" + restored + "/" + shadow + " litStable=" + stable + "/" + lit);
|
||||
}
|
||||
|
||||
private static boolean whiteGround(int color) {
|
||||
int r = color >> 16 & 255, g = color >> 8 & 255, b = color & 255;
|
||||
return Math.min(r, Math.min(g, b)) > 100 && Math.max(r, Math.max(g, b)) - Math.min(r, Math.min(g, b)) < 24;
|
||||
}
|
||||
|
||||
static int brightness(int color) {
|
||||
return ((color >> 16 & 255) + (color >> 8 & 255) + (color & 255)) / 3;
|
||||
}
|
||||
|
||||
static int channelDifference(int a, int b) {
|
||||
return Math.max(Math.abs((a >> 16 & 255) - (b >> 16 & 255)),
|
||||
Math.max(Math.abs((a >> 8 & 255) - (b >> 8 & 255)), Math.abs((a & 255) - (b & 255))));
|
||||
}
|
||||
|
||||
private static void sameSize(Frame a, Frame b) {
|
||||
check(a.width == b.width && a.height == b.height && a.pixels.length == b.pixels.length,
|
||||
"GPU comparison requires the same framebuffer dimensions");
|
||||
}
|
||||
|
||||
private record Options(int distance, int simulationDistance, int fov, boolean bob,
|
||||
boolean pause, CameraType camera, String language) {
|
||||
static Options capture(Minecraft m) {
|
||||
return new Options(m.options.renderDistance().get(), m.options.simulationDistance().get(), m.options.fov().get(),
|
||||
m.options.bobView().get(), m.options.pauseOnLostFocus,
|
||||
m.options.getCameraType(), m.getLanguageManager().getSelected());
|
||||
}
|
||||
|
||||
void restore(Minecraft m) {
|
||||
m.options.renderDistance().set(distance);
|
||||
m.options.simulationDistance().set(simulationDistance);
|
||||
m.options.fov().set(fov);
|
||||
m.options.bobView().set(bob);
|
||||
m.options.pauseOnLostFocus = pause;
|
||||
m.options.setCameraType(camera);
|
||||
m.getLanguageManager().setSelected(language);
|
||||
m.getLanguageManager().onResourceManagerReload(m.getResourceManager());
|
||||
}
|
||||
}
|
||||
|
||||
private static KeyEvent enter() { return new KeyEvent(InputConstants.KEY_RETURN, 0, 0); }
|
||||
static void check(boolean ok, String message) { if (!ok) throw new AssertionError(message); }
|
||||
}
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import static fr.koka.sanctuary.client.shader.PixelShadows122ClientChecks.*;
|
||||
|
||||
import fr.koka.sanctuary.realtime.RealtimeService;
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.TestServerContext;
|
||||
import net.fabricmc.fabric.api.client.rendering.v1.level.LevelRenderEvents;
|
||||
|
||||
final class RealtimeShadows129ClientChecks {
|
||||
private static void projectionContinuity() {
|
||||
// Far coordinates amplify the old per-frame rounding failure. Moving the sun must not
|
||||
// add a periodic translation to the shadow raster while the camera remains stationary.
|
||||
var grid=new ShadowProjection();
|
||||
float last=0; int jumps=0; float greatest=0;
|
||||
for(int i=0;i<2000;i++) {
|
||||
var matrix=grid.matrix(PixelShadows.sunDirection(.7f+i*.000001f),32,2048,100000,64,-100000,false);
|
||||
if(i>0){float step=Math.abs(matrix.m31()-last);greatest=Math.max(greatest,step);if(step>1e-6f)jumps++;}
|
||||
last=matrix.m31();
|
||||
}
|
||||
check(jumps==0,"Slow light rotation has no cyclical raster translation far from spawn: "+greatest);
|
||||
// Recentring transfers the phase modulo integral texels, preserving the sampled grid.
|
||||
var sun=PixelShadows.sunDirection(.71f);
|
||||
var before=grid.matrix(sun,32,2048,100000,64,-100000,false);
|
||||
var after=grid.matrix(sun,32,2048,100016,64,-100000,false);
|
||||
var a=before.transform(new org.joml.Vector4f(4,1,2,1));
|
||||
var b=after.transform(new org.joml.Vector4f(-12,1,2,1));
|
||||
for(float value:new float[]{(b.x-a.x)*1024,(b.y-a.y)*1024})
|
||||
check(Math.abs(value-Math.rint(value))<.001,"Camera recentering moves only whole shadow texels");
|
||||
}
|
||||
|
||||
static void run(ClientGameTestContext c, TestServerContext server) {
|
||||
projectionContinuity();
|
||||
var enabled=new AtomicBoolean(false);
|
||||
var trace=Collections.synchronizedList(new ArrayList<double[]>());
|
||||
LevelRenderEvents.END_EXTRACTION.register(context -> {
|
||||
if(enabled.get() && trace.size()<4096)trace.add(new double[]{System.nanoTime()/1e9,context.levelState().skyRenderState.sunAngle});
|
||||
});
|
||||
var original=new AtomicReference<Clock>();
|
||||
try {
|
||||
server.runOnServer(s -> {
|
||||
var session=RealtimeService.session(s);
|
||||
check(session!=null,"Isolated world has the actual real-time service");
|
||||
try {
|
||||
var field=session.getClass().getDeclaredField("clock");field.setAccessible(true);
|
||||
original.set((Clock)field.get(session));
|
||||
var morning=session.sample().day().noon().minusSeconds(3*3600);
|
||||
field.set(session,Clock.offset(Clock.systemUTC(),Duration.between(Instant.now(),morning)));
|
||||
check(RealtimeService.setMode(s,true),"Real-time mode enabled through actual server service");
|
||||
} catch(ReflectiveOperationException e) {throw new AssertionError(e);}
|
||||
catch(Exception e) {throw new AssertionError(e);}
|
||||
});
|
||||
c.waitFor(m -> PixelShadows.sunDirection(m.gameRenderer.gameRenderState().levelRenderState.skyRenderState.sunAngle).y>.2,200);
|
||||
c.waitTicks(40);
|
||||
double shadowX=c.computeOnClient(m -> {
|
||||
var sun=PixelShadows.sunDirection(m.gameRenderer.gameRenderState().levelRenderState.skyRenderState.sunAngle);
|
||||
return 1-7*sun.x/sun.y;
|
||||
});
|
||||
camera(c,server,shadowX,69,1,30);
|
||||
shadows(c,false);
|
||||
var off=capture(c,null);
|
||||
shadows(c,true);
|
||||
enabled.set(true);
|
||||
var frames=new ArrayList<Frame>();
|
||||
for(int i=0;i<24;i++) {c.waitTicks(6);frames.add(capture(c,i==0?"realtime-start":i==23?"realtime-end":null));}
|
||||
c.runOnClient(m -> enabled.set(false));
|
||||
double travel=Math.IEEEremainder(trace.getLast()[1]-trace.getFirst()[1],Math.PI*2);
|
||||
int reverse=0;double largest=0;
|
||||
for(int i=1;i<trace.size();i++) {
|
||||
double delta=Math.IEEEremainder(trace.get(i)[1]-trace.get(i-1)[1],Math.PI*2)*Math.signum(travel);
|
||||
if(delta<-.0000001){reverse++;largest=Math.max(largest,-delta);}
|
||||
}
|
||||
int samples=0,changed=0,rebound=0;
|
||||
for(int y=off.height()/10;y<off.height()*9/10;y+=2) for(int x=off.width()/10;x<off.width()*9/10;x+=2) {
|
||||
int index=y*off.width()+x,a=off.pixels()[index];
|
||||
int r=a>>16&255,g=a>>8&255,b=a&255;
|
||||
if(Math.min(r,Math.min(g,b))<100 || Math.max(r,Math.max(g,b))-Math.min(r,Math.min(g,b))>24)continue;
|
||||
samples++;int flips=0;
|
||||
boolean last=brightness(a)-brightness(frames.getFirst().pixels()[index])>=8;
|
||||
for(var frame:frames) {
|
||||
boolean dark=brightness(a)-brightness(frame.pixels()[index])>=8;
|
||||
if(last!=dark)flips++;
|
||||
last=dark;
|
||||
}
|
||||
if(flips>0)changed++;
|
||||
if(flips>1)rebound++;
|
||||
}
|
||||
check(trace.size()>30 && Math.abs(travel)>.00001,"Real-time shadows actually remain dynamic");
|
||||
check(rebound<Math.max(8,samples/1000),"Real-time opaque contours must not repeatedly rebound: "+rebound);
|
||||
System.out.println("REALTIME129_PASS frames="+trace.size()+" rawTravel="+travel+" reversals="+reverse+" largestReverse="+largest
|
||||
+" pixels="+samples+" changed="+changed+" rebound="+rebound);
|
||||
try {
|
||||
var out=new StringBuilder("seconds,raw_angle\n");
|
||||
for(var sample:trace)out.append(sample[0]).append(',').append(sample[1]).append('\n');
|
||||
java.nio.file.Files.writeString(net.fabricmc.loader.api.FabricLoader.getInstance().getGameDir().resolve("shadow129-clock.csv"),out);
|
||||
}catch(java.io.IOException e){throw new AssertionError(e);}
|
||||
} finally {
|
||||
enabled.set(false);
|
||||
server.runOnServer(s -> {
|
||||
try {
|
||||
RealtimeService.setMode(s,false);
|
||||
if(original.get()!=null){var session=RealtimeService.session(s);var field=session.getClass().getDeclaredField("clock");field.setAccessible(true);field.set(session,original.get());}
|
||||
command(s,"time set 3000");
|
||||
}catch(Exception e){throw new AssertionError(e);}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import static fr.koka.sanctuary.client.shader.PixelShadows122ClientChecks.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.TestServerContext;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import java.nio.file.Files;
|
||||
|
||||
public final class Ssgi132ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c){new PixelShadows122ClientChecks(false,true).runTest(c);}
|
||||
static void run(ClientGameTestContext c,TestServerContext server){
|
||||
c.runOnClient(m->{preferences();VanillaLight.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());
|
||||
}});
|
||||
camera(c,server,10.5,69,5.5,65);look(c,server,0,40);settle(c);
|
||||
var flatOff=capture(c,"ssgi-flat-off");enable(c,true);unchanged(flatOff,capture(c,"ssgi-flat-on"),"Coplanar ground does not bounce light onto itself");
|
||||
wall(server,12,Blocks.CONCRETE.red().defaultBlockState());settle(c);enable(c,false);var redOff=capture(c,"ssgi-red-off");
|
||||
c.runOnClient(m->VanillaLight.setSsgiIntensity(35));
|
||||
enable(c,true);var defaultRed=measure(redOff,capture(c,"ssgi-red-default-35"));
|
||||
System.out.println("SSGI133_DEFAULT "+defaultRed);
|
||||
check(defaultRed.visible>5000&&defaultRed.strong>500,"Default intensity produces 8/255 gain over 5000 pixels and 16/255 over 500 pixels: "+defaultRed);
|
||||
c.runOnClient(m->VanillaLight.setSsgiIntensity(90));frames(c);
|
||||
var redOn=capture(c,"ssgi-red-on");var red=measure(redOff,redOn);System.out.println("SSGI133_RED "+red);
|
||||
check(red.changed>30&&red.red>red.blue*1.5+20,"Visible red wall tints neutral ground: "+red);
|
||||
unchanged(redOn,capture(c,null),"Stationary SSGI has no animated noise or accumulating feedback");
|
||||
look(c,server,0.00002f,40.00002f);frames(c);unchanged(redOn,capture(c,null),"Subpixel motion keeps actual red bounce stable");
|
||||
look(c,server,5,40);frames(c);capture(c,"ssgi-camera-turned");
|
||||
look(c,server,0,40);frames(c);unchanged(redOn,capture(c,null),"Returning the camera leaves no temporal color trails");
|
||||
c.runOnClient(m->VanillaLight.setSsgiIntensity(20));frames(c);var low=measure(redOff,capture(c,"ssgi-strength-20"));
|
||||
check(red.red>low.red*1.5,"Intensity adjusts native color bounce: "+red+" / "+low);
|
||||
c.runOnClient(m->VanillaLight.setSsgiIntensity(0));c.waitTicks(6);unchanged(redOff,capture(c,null),"Zero intensity restores source image");
|
||||
c.runOnClient(m->check(!ScreenSpaceGI.allocated()&&!ScreenSpaceGI.applied(),"Zero frees SSGI targets"));
|
||||
c.runOnClient(m->VanillaLight.setSsgiIntensity(90));frames(c);
|
||||
int oldWidth=c.computeOnClient(m->m.getWindow().getScreenWidth()),oldHeight=c.computeOnClient(m->m.getWindow().getScreenHeight());
|
||||
c.runOnClient(m->m.getWindow().setWindowed(961,541));
|
||||
c.waitFor(m->m.gameRenderer.mainRenderTarget().width!=redOn.width(),200);frames(c);
|
||||
enable(c,false);var resizedOff=capture(c,null);enable(c,true);var resized=measure(resizedOff,capture(c,"ssgi-resized"));
|
||||
check(resized.changed>30&&resized.red>resized.blue*1.5+20,"Resizing reallocates a working SSGI pass: "+resized);
|
||||
c.runOnClient(m->m.getWindow().setWindowed(oldWidth,oldHeight));
|
||||
c.waitFor(m->m.gameRenderer.mainRenderTarget().width==redOn.width(),200);frames(c);
|
||||
wall(server,12,Blocks.CONCRETE.blue().defaultBlockState());settle(c);enable(c,false);var blueOff=capture(c,"ssgi-blue-off");enable(c,true);var blue=measure(blueOff,capture(c,"ssgi-blue-on"));
|
||||
c.runOnClient(m->VanillaLight.setSsgiIntensity(35));frames(c);
|
||||
var defaultBlue=measure(blueOff,capture(c,"ssgi-blue-default-35"));
|
||||
check(defaultBlue.visible>5000&&defaultBlue.strong>500&&defaultBlue.blue>defaultBlue.red*1.5+20,"Default blue bounce is visible too: "+defaultBlue);
|
||||
c.runOnClient(m->VanillaLight.setSsgiIntensity(90));frames(c);
|
||||
System.out.println("SSGI133_BLUE "+blue+" default="+defaultBlue);check(blue.changed>30&&blue.blue>blue.red*1.5+20,"Live blue wall changes bounced hue: "+blue);
|
||||
wall(server,11,Blocks.CONCRETE.white().defaultBlockState());settle(c);enable(c,false);var coveredOff=capture(c,null);enable(c,true);var covered=capture(c,"ssgi-covered-blue");
|
||||
wall(server,12,Blocks.CONCRETE.red().defaultBlockState());settle(c);unchanged(covered,capture(c,"ssgi-covered-red"),"Hidden wall color cannot pass through foreground wall");
|
||||
var reload=c.computeOnClient(net.minecraft.client.Minecraft::reloadResourcePacks);c.waitFor(m->reload.isDone()&&m.gui.overlay()==null,1200);reload.join();frames(c);unchanged(covered,capture(c,"ssgi-reloaded"),"Resource reload restores SSGI");
|
||||
look(c,server,0.00002f,40.00002f);frames(c);unchanged(covered,capture(c,null),"Subpixel camera movement keeps bounce stable");
|
||||
look(c,server,0,40);frames(c);enable(c,false);unchanged(coveredOff,capture(c,null),"OFF leaves no residual tint");
|
||||
c.runOnClient(m->check(!ScreenSpaceGI.allocated(),"OFF frees SSGI resources"));
|
||||
c.runOnClient(m->{VanillaLight.setSsgiIntensity(35);VanillaLight.setPixelShadows(true);VanillaLight.setEdgeHighlights(true);VanillaLight.setBloom(true);VanillaLight.setOreEmission(true);});
|
||||
enable(c,true);c.waitFor(m->PixelShadows.applied()&&PixelShadows.ready()&&EdgeHighlights.applied()&&Bloom.applied(),1200);
|
||||
capture(c,"ssgi-combined-shader");
|
||||
c.runOnClient(m->VanillaLight.setEnabled(false));c.waitTicks(8);
|
||||
c.runOnClient(m->check(!ScreenSpaceGI.allocated(),"Main shader OFF releases SSGI too"));
|
||||
c.runOnClient(m->VanillaLight.setEnabled(true));frames(c);enable(c,false);
|
||||
motion(c,server);
|
||||
SsgiCloseup135ClientChecks.run(c,server);
|
||||
System.out.println("SSGI133_PASS red/blue diffuse bounce; intensity; flat rejection; foreground occlusion; static stability; tiny camera motion; resize; reload; combined shader; OFF/zero; preferences; red="+red+" blue="+blue);
|
||||
}
|
||||
private static void motion(ClientGameTestContext c,TestServerContext server){
|
||||
c.runOnClient(m->{VanillaLight.setPbr(false);VanillaLight.setSsgi(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);VanillaLight.setPixelShadows(false);VanillaLight.setEdgeHighlights(false);});
|
||||
wall(server,11,Blocks.AIR.defaultBlockState());
|
||||
server.runOnServer(s->{
|
||||
command(s,"summon minecraft:cow 8.5 65 9.5 {NoAI:1b,Silent:1b,NoGravity:1b,Invulnerable:1b,Tags:[\"ssgi134_test\"]}");
|
||||
for(int x=11;x<=13;x++)for(int z=8;z<=10;z++){
|
||||
s.overworld().setBlockAndUpdate(new BlockPos(x,64,z),Blocks.GRASS_BLOCK.defaultBlockState());
|
||||
s.overworld().setBlockAndUpdate(new BlockPos(x,65,z),Blocks.SHORT_GRASS.defaultBlockState());
|
||||
}
|
||||
});settle(c);
|
||||
for(int intensity:new int[]{35,100}){
|
||||
c.runOnClient(m->VanillaLight.setSsgiIntensity(intensity));
|
||||
Frame start=null;double worstGrass=0;
|
||||
for(int step=0;step<=12;step++){
|
||||
double x=4.5+step;camera(c,server,x,66.5,5.5,65);
|
||||
look(c,server,(float)Math.toDegrees(Math.atan2(x-10.5,6.5)),10);
|
||||
enable(c,false);var base=capture(c,"ssgi-motion-"+intensity+"-"+step+"-off");
|
||||
enable(c,true);var frame=capture(c,"ssgi-motion-"+intensity+"-"+step+"-on");
|
||||
double grass=grassGain(base,frame);worstGrass=Math.max(worstGrass,grass);
|
||||
check(grass<(intensity==35?12:30),"Lateral view avoids excessive grass brightening at "+intensity+" / "+step+": "+grass);
|
||||
if(step==0)start=frame;
|
||||
}
|
||||
System.out.println("SSGI134_GRASS intensity="+intensity+" maxMeanGreenGain="+worstGrass);
|
||||
camera(c,server,4.5,66.5,5.5,65);look(c,server,(float)Math.toDegrees(Math.atan2(-6,6.5)),10);frames(c);
|
||||
unchanged(start,capture(c,null),"Twelve-block lateral movement returns without stale SSGI history at "+intensity);
|
||||
}
|
||||
enable(c,false);c.runOnClient(m->VanillaLight.setSsgiIntensity(35));
|
||||
server.runOnServer(s->command(s,"kill @e[tag=ssgi134_test]"));
|
||||
System.out.println("SSGI134_MOTION_PASS 12-block lateral sweep, grass/cow/blocks, OFF/ON at35 and100, return without history");
|
||||
}
|
||||
private static double grassGain(Frame off,Frame on){
|
||||
int count=0;long gain=0;
|
||||
for(int i=0;i<off.pixels().length;i++){
|
||||
int a=off.pixels()[i],r=a>>16&255,g=a>>8&255,b=a&255;
|
||||
if(g>r*1.15&&g>b*1.3&&g>45){count++;gain+=Math.max(0,(on.pixels()[i]>>8&255)-g);}
|
||||
}
|
||||
check(count>500,"Motion fixture includes visible vegetation");return gain/(double)count;
|
||||
}
|
||||
private static void wall(TestServerContext server,int z,BlockState state){server.runOnServer(s->{for(int x=6;x<=15;x++)for(int y=65;y<=(z==11?72:69);y++)s.overworld().setBlockAndUpdate(new BlockPos(x,y,z),state);});}
|
||||
private static void settle(ClientGameTestContext c){c.waitTicks(30);c.waitFor(m->m.levelRenderer.hasRenderedAllSections(),1200);c.waitTicks(8);}
|
||||
private static void enable(ClientGameTestContext c,boolean enabled){c.runOnClient(m->VanillaLight.setSsgi(enabled));if(enabled)frames(c);else c.waitTicks(8);}
|
||||
private static void frames(ClientGameTestContext c){int start=c.computeOnClient(m->ScreenSpaceGI.renderedFrames());c.waitFor(m->ScreenSpaceGI.applied()&&ScreenSpaceGI.renderedFrames()>start+10,1200);c.waitTicks(6);}
|
||||
private static void preferences(){try{
|
||||
var p=FabricLoader.getInstance().getConfigDir().resolve("sanctuary-shaders.json");Files.writeString(p,"{\"bloom\":false,\"shadowPixels\":128}");forgetPreferences();
|
||||
check(VanillaLight.ssgi()&&VanillaLight.ssgiIntensity()==35&&!VanillaLight.bloom()&&VanillaLight.shadowPixels()==128,"Legacy choices preserved; missing SSGI setting defaults ON at 35");
|
||||
for(int i:new int[]{-10,0,35,90,140}){VanillaLight.setSsgiIntensity(i);VanillaLight.setSsgi(true);forgetPreferences();check(VanillaLight.ssgi()&&VanillaLight.ssgiIntensity()==Math.clamp(i,0,100),"SSGI persists and clamps");}
|
||||
VanillaLight.setSsgiIntensity(90);
|
||||
}catch(Exception e){throw new AssertionError(e);}}
|
||||
private record Gain(int changed,int visible,int strong,double red,double green,double blue){}
|
||||
private static Gain measure(Frame off,Frame on){int count=0,visible=0,strong=0;double r=0,g=0,b=0;for(int i=0;i<off.pixels().length;i++){
|
||||
int a=off.pixels()[i],ar=a>>16&255,ag=a>>8&255,ab=a&255;
|
||||
if(Math.max(ar,Math.max(ag,ab))-Math.min(ar,Math.min(ag,ab))>25||ar<60||ar>245)continue;
|
||||
int z=on.pixels()[i];int dr=(z>>16&255)-ar,dg=(z>>8&255)-ag,db=(z&255)-ab;
|
||||
if(Math.max(dr,Math.max(dg,db))>=16)strong++;
|
||||
if(Math.max(dr,Math.max(dg,db))>=8)visible++;
|
||||
if(Math.max(dr,Math.max(dg,db))>1)count++;r+=Math.max(0,dr);g+=Math.max(0,dg);b+=Math.max(0,db);
|
||||
}return new Gain(count,visible,strong,r,g,b);}
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import static fr.koka.sanctuary.client.shader.PixelShadows122ClientChecks.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.TestServerContext;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
|
||||
/** Native low-angle and near-plane regression fixture; never opens a personal world. */
|
||||
public final class SsgiCloseup135ClientChecks implements net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c){new PixelShadows122ClientChecks(false,false,false,true).runTest(c);}
|
||||
static void run(ClientGameTestContext c,TestServerContext server){
|
||||
c.runOnClient(m->{VanillaLight.setEnabled(true);VanillaLight.setSaturation(100);VanillaLight.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);
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,8 @@ public final class SanctuaryKnowledgeClient implements ClientModInitializer {
|
||||
public static void registerClient() {
|
||||
SanctuaryResourcePack.register();
|
||||
VortexCloudRenderer.register();
|
||||
fr.koka.sanctuary.client.shader.PixelShadows.register();
|
||||
fr.koka.sanctuary.client.shader.EdgeHighlights.register();
|
||||
ProgressionClient.register();
|
||||
StarClient.register();
|
||||
WeatherClient.register();
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import com.mojang.blaze3d.buffers.Std140Builder;
|
||||
import com.mojang.blaze3d.pipeline.TextureTarget;
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.renderpearl.api.GpuFormat;
|
||||
import com.mojang.renderpearl.api.buffers.GpuBuffer;
|
||||
import com.mojang.renderpearl.api.buffers.GpuBufferSlice;
|
||||
import com.mojang.renderpearl.api.pipeline.*;
|
||||
import com.mojang.renderpearl.api.textures.FilterMode;
|
||||
import com.mojang.renderpearl.api.textures.GpuTextureView;
|
||||
import fr.koka.sanctuary.client.panorama.PanoramaCapture;
|
||||
import java.util.*;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.BindGroupLayouts;
|
||||
import net.minecraft.client.renderer.MappableRingBuffer;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.client.renderer.texture.TextureAtlas;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.level.dimension.DimensionType;
|
||||
import net.minecraft.world.level.material.FogType;
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Vector3f;
|
||||
import org.joml.Vector4f;
|
||||
|
||||
/** Selective emissive bloom. No brightness threshold on ordinary world blocks, no scene-copy or readback. */
|
||||
public final class Bloom {
|
||||
private static Identifier id(String path){return Identifier.parse("sanctuary:"+path);}
|
||||
private static final BindGroupLayout SCENE=BindGroupLayout.builder().withUniform("BloomScene",UniformType.UNIFORM_BUFFER)
|
||||
.withUniform("WorldDepth",UniformType.COMBINED_IMAGE_SAMPLER).build();
|
||||
private static final RenderPipeline EMITTERS=RenderPipelines.register(RenderPipeline.builder()
|
||||
.withLocation(id("pipeline/bloom_emitters")).withVertexShader(id("core/bloom_emitters")).withFragmentShader(id("core/bloom_emitters"))
|
||||
.withBindGroupLayout(BindGroupLayouts.DYNAMIC_TRANSFORMS).withBindGroupLayout(SCENE)
|
||||
.withBindGroupLayout(BindGroupLayout.builder().withUniform("BlockAtlas",UniformType.COMBINED_IMAGE_SAMPLER).withUniform("EmissionMasks",UniformType.COMBINED_IMAGE_SAMPLER).build())
|
||||
.withVertexBinding(0,BloomTerrain.FORMAT).withPrimitiveTopology(PrimitiveTopology.QUADS).withCull(false)
|
||||
.withColorTargetState(new ColorTargetState(Optional.empty(),GpuFormat.RGBA8_UNORM,ColorTargetState.WRITE_ALL))
|
||||
.withDepthStencilState(Optional.empty()).build());
|
||||
private static RenderPipeline screen(String name,BlendFunction blend,boolean scene,boolean blur){
|
||||
var layout=BindGroupLayout.builder().withUniform("InputTexture",UniformType.COMBINED_IMAGE_SAMPLER);
|
||||
if(blur)layout.withUniform("BlurParams",UniformType.UNIFORM_BUFFER);
|
||||
var builder=RenderPipeline.builder().withLocation(id("pipeline/"+name)).withVertexShader("core/screenquad").withFragmentShader(id("core/"+name)).withBindGroupLayout(layout.build());
|
||||
if(scene)builder.withBindGroupLayout(SCENE);
|
||||
return RenderPipelines.register(builder.withPrimitiveTopology(PrimitiveTopology.TRIANGLES).withCull(false)
|
||||
.withColorTargetState(new ColorTargetState(Optional.ofNullable(blend),name.equals("bloom_blur")||name.equals("bloom_downsample")?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 BloomTerrain TERRAIN=new BloomTerrain();
|
||||
private static final Matrix4f projection=new Matrix4f();
|
||||
private static TextureTarget emission,small,ping;
|
||||
private static MappableRingBuffer scene,horizontal,vertical;
|
||||
private static int frames;
|
||||
private static boolean applied;
|
||||
private Bloom(){}
|
||||
public static void captureProjection(Matrix4f matrix){projection.set(matrix);}
|
||||
public static void dirtyBlock(BlockPos pos){TERRAIN.dirty(pos);}
|
||||
public static void dirtyChunk(int x,int z){TERRAIN.dirtyChunk(x,z);}
|
||||
public static void render(){
|
||||
applied=false;var mc=Minecraft.getInstance();
|
||||
boolean halo=VanillaLight.bloom()&&VanillaLight.bloomIntensity()>0;
|
||||
boolean cores=VanillaLight.oreEmission()&&VanillaLight.oreIntensity()>0;
|
||||
if(!VanillaLight.active()||(!halo&&!cores)||mc.level==null){release();return;}
|
||||
if(PanoramaCapture.rendering())return;
|
||||
var state=mc.gameRenderer.gameRenderState().levelRenderState;var camera=state.cameraRenderState;
|
||||
if(camera.fogType!=FogType.NONE)return;
|
||||
var main=mc.gameRenderer.mainRenderTarget();
|
||||
if(emission==null||emission.width!=main.width||emission.height!=main.height){
|
||||
targetsClose();emission=target("emission",main.width,main.height);
|
||||
}
|
||||
if(scene==null)scene=uniforms("bloom scene",144);
|
||||
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();
|
||||
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 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);
|
||||
}
|
||||
if(halo){
|
||||
float radius=.25f+VanillaLight.bloomRadius()*.0075f;
|
||||
horizontal.rotate();try(var mapped=horizontal.currentBuffer().map(false,true)){Std140Builder.intoBuffer(mapped.data()).putVec4(radius/small.width,0,0,0);}
|
||||
vertical.rotate();try(var mapped=vertical.currentBuffer().map(false,true)){Std140Builder.intoBuffer(mapped.data()).putVec4(0,radius/small.height,0,0);}
|
||||
}
|
||||
// Gather transforms before opening any pass (the native dynamic ring may resize).
|
||||
var transforms=new ArrayList<GpuBufferSlice>(entries.size());int max=0;
|
||||
for(var entry:entries){var pos=entry.origin();var offset=new Vector3f((float)(pos.getX()-camera.pos.x),(float)(pos.getY()-camera.pos.y),(float)(pos.getZ()-camera.pos.z));
|
||||
transforms.add(RenderSystem.getDynamicUniforms().writeTransform(new Matrix4f(vp).translate(offset),new Vector4f(1,1,1,1),offset,new Matrix4f()));max=Math.max(max,entry.indices());}
|
||||
var indices=RenderSystem.getSequentialBuffer(PrimitiveTopology.QUADS);indices.requestIndexCount(max);
|
||||
var nearest=RenderSystem.getSamplerCache().getClampToEdge(FilterMode.NEAREST);
|
||||
if(halo)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)))){ }
|
||||
|
||||
if(atlas!=null&&!entries.isEmpty())try(var pass=RenderSystem.getDevice().createCommandEncoder().createRenderPass(()->"Sanctuary visible emitters",emission.getColorTextureView(),Optional.empty())){
|
||||
pass.setPipeline(RenderSystem.getCompiledPipeline(EMITTERS));pass.setUniform("BloomScene",scene.currentBuffer());pass.setUniform("WorldDepth",main.getDepthTextureView(),nearest);
|
||||
pass.setUniform("BlockAtlas",mc.getTextureManager().getTexture(TextureAtlas.LOCATION_BLOCKS).getTextureView(),nearest);pass.setUniform("EmissionMasks",atlas.getTextureView(),nearest);
|
||||
pass.setIndexBuffer(indices.getBuffer(),indices.type());
|
||||
for(int i=0;i<entries.size();i++){var entry=entries.get(i);pass.setUniform("DynamicTransforms",transforms.get(i));pass.setVertexBuffer(0,entry.vertices().slice());pass.drawIndexed(entry.indices(),1,0,0,0);}
|
||||
}
|
||||
if(halo){
|
||||
screen(DOWNSAMPLE,small.getColorTextureView(),emission.getColorTextureView(),false,null);
|
||||
screen(BLUR,ping.getColorTextureView(),small.getColorTextureView(),false,horizontal);
|
||||
screen(BLUR,small.getColorTextureView(),ping.getColorTextureView(),false,vertical);
|
||||
}
|
||||
if(cores)screen(CORE,main.getColorTextureView(),emission.getColorTextureView(),false,null);
|
||||
if(halo)screen(APPLY,main.getColorTextureView(),small.getColorTextureView(),true,null);
|
||||
applied=true;frames++;
|
||||
}
|
||||
private static void screen(RenderPipeline pipeline,GpuTextureView output,GpuTextureView input,boolean useScene,MappableRingBuffer blur){
|
||||
try(var pass=RenderSystem.getDevice().createCommandEncoder().createRenderPass(()->"Sanctuary bloom",output,Optional.empty())){
|
||||
pass.setPipeline(RenderSystem.getCompiledPipeline(pipeline));pass.setUniform("InputTexture",input,RenderSystem.getSamplerCache().getClampToEdge(pipeline==CORE||pipeline==SKY?FilterMode.NEAREST:FilterMode.LINEAR));
|
||||
if(useScene){pass.setUniform("BloomScene",scene.currentBuffer());pass.setUniform("WorldDepth",Minecraft.getInstance().gameRenderer.mainRenderTarget().getDepthTextureView(),RenderSystem.getSamplerCache().getClampToEdge(FilterMode.NEAREST));}
|
||||
if(blur!=null)pass.setUniform("BlurParams",blur.currentBuffer());pass.draw(3,1,0,0);
|
||||
}
|
||||
}
|
||||
private static TextureTarget target(String name,int w,int h){return new TextureTarget("Sanctuary "+name,w,h,name.equals("emission")?GpuFormat.RGBA8_UNORM:GpuFormat.RGBA16_FLOAT,null);}
|
||||
private static MappableRingBuffer uniforms(String name,int size){return new MappableRingBuffer(()->"Sanctuary "+name,GpuBuffer.USAGE_UNIFORM|GpuBuffer.USAGE_MAP_WRITE,size);}
|
||||
private static void haloClose(){
|
||||
if(small!=null){small.destroyBuffers();ping.destroyBuffers();small=ping=null;}
|
||||
if(horizontal!=null){horizontal.close();vertical.close();horizontal=vertical=null;}
|
||||
}
|
||||
private static void targetsClose(){haloClose();if(emission!=null){emission.destroyBuffers();emission=null;}}
|
||||
public static void release(){TERRAIN.close();targetsClose();if(scene!=null){scene.close();scene=null;}applied=false;}
|
||||
static boolean haloAllocated(){return small!=null||horizontal!=null;}
|
||||
static boolean applied(){return applied;}
|
||||
static boolean allocated(){return emission!=null;}
|
||||
static boolean ready(){return TERRAIN.settled();}
|
||||
static int renderedFrames(){return frames;}
|
||||
static TextureTarget emissionTarget(){return emission;}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import com.mojang.blaze3d.platform.NativeImage;
|
||||
import java.util.*;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.texture.DynamicTexture;
|
||||
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
/** Analyse source texels once per resource reload, never rendered frames or GPU pixels. */
|
||||
final class BloomMaterials implements AutoCloseable {
|
||||
static final int TILE=68, INNER=64, SIZE=TILE*16, MAX_SPRITES=256;
|
||||
record Slot(int x,int y,boolean empty) {
|
||||
float u(float local){return (x+2+Math.clamp(local,0,1)*INNER)/SIZE;}
|
||||
float v(float local){return (y+2+Math.clamp(local,0,1)*INNER)/SIZE;}
|
||||
}
|
||||
private final Map<String,Slot> slots=new HashMap<>();
|
||||
private final Map<BlockState,Boolean> ores=new IdentityHashMap<>();
|
||||
private final Map<Identifier,int[]> hosts=new HashMap<>();
|
||||
private DynamicTexture atlas;
|
||||
private boolean dirty;
|
||||
private static final Set<String> ORES=Set.of("iron","copper","redstone","lapis","diamond","emerald","ruby","sapphire","quartz","gold");
|
||||
|
||||
boolean ore(BlockState state){return ores.computeIfAbsent(state,s->{
|
||||
var id=BuiltInRegistries.BLOCK.getKey(s.getBlock());
|
||||
if(!id.getNamespace().equals("minecraft")&&!id.getNamespace().equals("sanctuary"))return false;
|
||||
String name=id.getPath().replaceFirst("^(deepslate_|nether_)","");
|
||||
return name.endsWith("_ore")&&ORES.contains(name.substring(0,name.length()-4));
|
||||
});}
|
||||
boolean candidate(BlockState state){return ore(state)||state.getLightEmission()>0||state.emissiveRendering();}
|
||||
|
||||
Slot slot(TextureAtlasSprite sprite,boolean ore,boolean fullEmission){
|
||||
String key=sprite.contents().name()+":"+ore+":"+fullEmission;
|
||||
var cached=slots.get(key);if(cached!=null)return cached;
|
||||
if(slots.size()>=MAX_SPRITES)return new Slot(0,0,true);
|
||||
int index=slots.size(),x=(index%16)*TILE,y=(index/16)*TILE;
|
||||
if(atlas==null){atlas=new DynamicTexture("Sanctuary emissive masks",SIZE,SIZE,false);atlas.getPixels().fillRect(0,0,SIZE,SIZE,0);}
|
||||
boolean any=false;
|
||||
var id=sprite.contents().name();
|
||||
try(var image=read(id)){
|
||||
int frameWidth=Math.min(image.getWidth(),sprite.contents().width()),frameHeight=Math.min(image.getHeight(),sprite.contents().height());
|
||||
int w=Math.min(INNER,frameWidth),h=Math.min(INNER,frameHeight);
|
||||
int[] colors=new int[w*h];for(int iy=0;iy<h;iy++)for(int ix=0;ix<w;ix++)colors[iy*w+ix]=image.getPixel(ix*frameWidth/w,iy*frameHeight/h);
|
||||
int[] mask=mask(id,colors,w,h,ore,fullEmission);
|
||||
// Standard optional _e texture takes precedence, including in personal resource packs.
|
||||
var override=Minecraft.getInstance().getResourceManager().getResource(texture(id.withSuffix("_e")));
|
||||
if(override.isPresent())try(var stream=override.get().open();var explicit=NativeImage.read(stream)){
|
||||
for(int iy=0;iy<h;iy++)for(int ix=0;ix<w;ix++){
|
||||
int c=explicit.getPixel(ix*explicit.getWidth()/w,iy*Math.min(explicit.getHeight(),frameHeight)/h);
|
||||
mask[iy*w+ix]=(c>>>24)*Math.max((c>>16)&255,Math.max((c>>8)&255,c&255))/255;
|
||||
}
|
||||
}
|
||||
for(int iy=0;iy<TILE;iy++)for(int ix=0;ix<TILE;ix++){
|
||||
int value=mask[Math.clamp((iy-2)*h/INNER,0,h-1)*w+Math.clamp((ix-2)*w/INNER,0,w-1)];
|
||||
any|=value>0;atlas.getPixels().setPixel(x+ix,y+iy,0xFF000000|value*0x010101);
|
||||
}
|
||||
}catch(java.io.IOException exception){
|
||||
fr.koka.sanctuary.SanctuaryMod.LOGGER.warn("Cannot analyse emissive texture {}",id,exception);
|
||||
}
|
||||
var result=new Slot(x,y,!any);slots.put(key,result);dirty=true;return result;
|
||||
}
|
||||
|
||||
int[] mask(Identifier id,int[] colors,int width,int height,boolean ore,boolean fullEmission){
|
||||
int[] result=new int[colors.length];String name=id.getPath();
|
||||
boolean quartz=name.contains("quartz"),nether=name.contains("nether_gold")||quartz;
|
||||
int[] host=ore?host(nether?"netherrack":name.contains("deepslate")?"deepslate":"stone"):new int[0];
|
||||
boolean structural=!ore&&!fullEmission&&(name.contains("furnace_side")||name.contains("furnace_top")
|
||||
||name.contains("smoker_side")||name.contains("smoker_top")||name.contains("smoker_bottom")
|
||||
||name.contains("campfire_log")&&!name.contains("lit")||name.contains("respawn_anchor_side")
|
||||
||name.contains("respawn_anchor_bottom"));
|
||||
for(int i=0;i<colors.length;i++){
|
||||
int c=colors[i],r=c>>16&255,g=c>>8&255,b=c&255,max=Math.max(r,Math.max(g,b)),min=Math.min(r,Math.min(g,b));
|
||||
if((c>>>24)<128||structural)continue;
|
||||
if(ore){
|
||||
int difference=255;
|
||||
for(int base:host){int br=base>>16&255,bg=base>>8&255,bb=base&255;
|
||||
difference=Math.min(difference,Math.max(Math.abs(r-br),Math.max(Math.abs(g-bg),Math.abs(b-bb))));}
|
||||
boolean inclusion=nether?(quartz?g>r*.65&&b>r*.5&&max>110:r>110&&g>75&&g>b*1.45):max-min>=10;
|
||||
if(inclusion&&difference>=10)result[i]=255;
|
||||
}else{
|
||||
// Luminosity is tied to the block state; cold/off variants never enter this path.
|
||||
// Dark casings/wood are suppressed; live atlas colours and animation stay native.
|
||||
float value=fullEmission?1:Math.clamp((max-100)/110f,0,1);
|
||||
if(name.contains("torch")||name.contains("furnace")||name.contains("smoker")||name.contains("campfire"))
|
||||
value*=Math.clamp((max-min-35)/65f,0,1);
|
||||
result[i]=Math.round(value*255);
|
||||
}
|
||||
}
|
||||
if(ore&&!nether){
|
||||
// Retain pale/white highlights touching coloured inclusions, without growing into grey rock.
|
||||
int[] seed=result.clone();
|
||||
for(int y=0;y<height;y++)for(int x=0;x<width;x++)if(seed[y*width+x]==0){
|
||||
int c=colors[y*width+x],r=c>>16&255,g=c>>8&255,b=c&255;
|
||||
if((c>>>24)<128||Math.min(r,Math.min(g,b))<215)continue;
|
||||
for(int dy=-1;dy<=1;dy++)for(int dx=-1;dx<=1;dx++)if(x+dx>=0&&x+dx<width&&y+dy>=0&&y+dy<height&&seed[(y+dy)*width+x+dx]>0)result[y*width+x]=255;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
private int[] host(String name){return hosts.computeIfAbsent(Identifier.withDefaultNamespace("block/"+name),id->{
|
||||
try(var image=read(id)){var values=new LinkedHashSet<Integer>();for(int y=0;y<Math.min(64,image.getHeight());y++)for(int x=0;x<Math.min(64,image.getWidth());x++)values.add(image.getPixel(x*image.getWidth()/Math.min(64,image.getWidth()),y*image.getHeight()/Math.min(64,image.getHeight())));return values.stream().mapToInt(Integer::intValue).toArray();}
|
||||
catch(java.io.IOException exception){return new int[0];}
|
||||
});}
|
||||
static Identifier texture(Identifier id){return Identifier.fromNamespaceAndPath(id.getNamespace(),"textures/"+id.getPath()+".png");}
|
||||
static NativeImage read(Identifier id)throws java.io.IOException{try(var stream=Minecraft.getInstance().getResourceManager().getResourceOrThrow(texture(id)).open()){return NativeImage.read(stream);}}
|
||||
DynamicTexture atlas(){if(dirty){atlas.upload();dirty=false;}return atlas;}
|
||||
int count(){return slots.size();}
|
||||
public void close(){if(atlas!=null){atlas.close();atlas=null;}slots.clear();ores.clear();hosts.clear();dirty=false;}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.blaze3d.vertex.BufferBuilder;
|
||||
import com.mojang.blaze3d.vertex.ByteBufferBuilder;
|
||||
import com.mojang.renderpearl.api.GpuFormat;
|
||||
import com.mojang.renderpearl.api.buffers.GpuBuffer;
|
||||
import com.mojang.renderpearl.api.pipeline.PrimitiveTopology;
|
||||
import com.mojang.renderpearl.api.vertex.VertexFormat;
|
||||
import java.util.*;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.model.geom.builders.UVPair;
|
||||
import net.minecraft.client.renderer.block.dispatch.BlockStateModelPart;
|
||||
import net.minecraft.client.renderer.texture.TextureAtlas;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.SectionPos;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.chunk.LevelChunkSection;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Native visible sections only, palette rejection and bounded incremental emitter meshing. */
|
||||
final class BloomTerrain implements AutoCloseable {
|
||||
static final VertexFormat FORMAT=VertexFormat.builder(0).addAttribute("Position",GpuFormat.RGB32_FLOAT)
|
||||
.addAttribute("Color",GpuFormat.RGBA8_UNORM).addAttribute("UV0",GpuFormat.RG32_FLOAT)
|
||||
.addAttribute("UV3",GpuFormat.RG32_FLOAT).build();
|
||||
private static final long MAX_BYTES=32L*1024*1024;
|
||||
private final Map<Long,Section> sections=new HashMap<>();
|
||||
final BloomMaterials materials=new BloomMaterials();
|
||||
private Build building;
|
||||
private long bytes;
|
||||
private Object models,level;
|
||||
private List<Section> visible=List.of();
|
||||
record Entry(BlockPos origin,GpuBuffer vertices,int indices){}
|
||||
List<Entry> update(){
|
||||
var mc=Minecraft.getInstance();var current=mc.getModelManager().getBlockStateModelSet();
|
||||
if(models!=current||level!=mc.level){close();models=current;level=mc.level;}
|
||||
long frame=mc.level.getGameTime();var active=new ArrayList<Section>();
|
||||
for(var render:mc.levelRenderer.visibleSections()){
|
||||
long key=render.getSectionNode();var pos=render.getRenderOrigin();
|
||||
var chunk=mc.level.getChunkSource().getChunk(pos.getX()>>4,pos.getZ()>>4,ChunkStatus.FULL,false);
|
||||
if(chunk==null)continue;
|
||||
var storage=chunk.getSection(mc.level.getSectionIndex(pos.getY()));
|
||||
var section=sections.get(key);
|
||||
if(section==null||section.storage!=storage){if(section!=null)remove(section);section=new Section(key,pos.immutable(),storage);sections.put(key,section);}
|
||||
section.seen=frame;active.add(section);
|
||||
}
|
||||
visible=active;
|
||||
for(var iterator=sections.values().iterator();iterator.hasNext();){var section=iterator.next();if(frame-section.seen>40){remove(section);iterator.remove();}}
|
||||
Vec3 camera=mc.gameRenderer.gameRenderState().levelRenderState.cameraRenderState.pos;
|
||||
long deadline=System.nanoTime()+3_000_000L;int started=building==null?0:1;
|
||||
while(System.nanoTime()<deadline){
|
||||
if(building==null){
|
||||
if(started++>=4)break;
|
||||
Section next=active.stream().filter(s->s.dirty).min(Comparator.comparingDouble(s->s.origin.distToCenterSqr(camera))).orElse(null);
|
||||
if(next==null)break;
|
||||
if(!next.storage.maybeHas(materials::candidate)){next.dirty=false;continue;}
|
||||
building=new Build(next);
|
||||
}
|
||||
if(!advance(building,deadline))break;
|
||||
var done=building;building=null;
|
||||
try(done){
|
||||
done.section.dirty=false;
|
||||
if(done.quads>0&&!done.overflow){
|
||||
try(var mesh=done.out.buildOrThrow()){
|
||||
long size=mesh.vertexBuffer().remaining();
|
||||
if(bytes+size<=MAX_BYTES){
|
||||
var buffer=RenderSystem.getDevice().createBuffer(()->"Sanctuary bloom emitters",GpuBuffer.USAGE_VERTEX|GpuBuffer.USAGE_COPY_DST,mesh.vertexBuffer());
|
||||
int count=mesh.drawState().indexCount();RenderSystem.getSequentialBuffer(PrimitiveTopology.QUADS).getBuffer(count);
|
||||
done.section.entry=new Entry(done.section.origin,buffer,count);bytes+=buffer.size();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return active.stream().map(s->s.entry).filter(Objects::nonNull).toList();
|
||||
}
|
||||
private boolean advance(Build build,long deadline){
|
||||
var mc=Minecraft.getInstance();var models=mc.getModelManager().getBlockStateModelSet();
|
||||
var parts=new ArrayList<BlockStateModelPart>();var random=RandomSource.create();
|
||||
while(build.cursor<4096){
|
||||
int i=build.cursor++,x=i&15,z=i>>4&15,y=i>>8;var state=build.section.storage.getBlockState(x,y,z);
|
||||
if(materials.candidate(state)){
|
||||
var pos=build.section.origin.offset(x,y,z);boolean ore=materials.ore(state);
|
||||
parts.clear();random.setSeed(state.getSeed(pos));if(!state.liquid())models.get(state).collectParts(random,parts);
|
||||
var tintSources=mc.getBlockColors().getTintSources(state);
|
||||
var offset=state.getOffset(pos);
|
||||
for(var part:parts)for(int f=0;f<7;f++){
|
||||
Direction face=f==6?null:Direction.values()[f];
|
||||
if(face!=null&&mc.level.getBlockState(pos.relative(face)).isSolidRender())continue;
|
||||
for(var quad:part.getQuads(face)){
|
||||
var material=quad.materialInfo();var sprite=material.sprite();
|
||||
if(!sprite.atlasLocation().equals(TextureAtlas.LOCATION_BLOCKS))continue;
|
||||
var slot=materials.slot(sprite,ore,material.lightEmission()>0||state.emissiveRendering()||sprite.isAnimated());
|
||||
if(slot.empty())continue;
|
||||
if(build.quads>=16384){build.overflow=true;return true;}
|
||||
int tint=0xFFFFFF,index=material.tintIndex();
|
||||
if(index>=0&&index<tintSources.size())tint=tintSources.get(index).colorInWorld(state,mc.level,pos)&0xFFFFFF;
|
||||
// Alpha marks ore vs. native emitter; the mask alpha is not a gameplay light value.
|
||||
int light=state.emissiveRendering()?15:Math.max(state.getLightEmission(),material.lightEmission());
|
||||
int alpha=ore?Math.round(light/15f*120):128+Math.round(light/15f*127);
|
||||
int color=(alpha<<24)|tint;
|
||||
for(int v=0;v<4;v++){
|
||||
var p=quad.position(v);float u=UVPair.unpackU(quad.packedUV(v)),tv=UVPair.unpackV(quad.packedUV(v));
|
||||
float localU=(u-sprite.getU0())/(sprite.getU1()-sprite.getU0()),localV=(tv-sprite.getV0())/(sprite.getV1()-sprite.getV0());
|
||||
build.out.addVertex(x+p.x()+(float)offset.x,y+p.y()+(float)offset.y,z+p.z()+(float)offset.z)
|
||||
.setColor(color).setUv(u,tv).setUv3(slot.u(localU),slot.v(localV));
|
||||
}
|
||||
build.quads++;
|
||||
}
|
||||
}
|
||||
if(state.liquid()&&state.getLightEmission()>0)fluid(build,pos,x,y,z,state.getFluidState().getHeight(mc.level,pos));
|
||||
}
|
||||
if((build.cursor&31)==0&&System.nanoTime()>=deadline)return build.cursor==4096;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
private void fluid(Build build,BlockPos pos,int x,int y,int z,float height){
|
||||
var mc=Minecraft.getInstance();var sprite=((TextureAtlas)mc.getTextureManager().getTexture(TextureAtlas.LOCATION_BLOCKS)).getSprite(net.minecraft.resources.Identifier.withDefaultNamespace("block/lava_still"));
|
||||
var slot=materials.slot(sprite,false,true);if(slot.empty())return;
|
||||
// Native world depth retains the actual fluid silhouette and occludes these candidate faces.
|
||||
float[][][] faces={{{0,height,0},{0,height,1},{1,height,1},{1,height,0}},{{0,0,0},{0,height,0},{1,height,0},{1,0,0}},
|
||||
{{0,0,1},{1,0,1},{1,height,1},{0,height,1}},{{0,0,0},{0,0,1},{0,height,1},{0,height,0}},{{1,0,0},{1,height,0},{1,height,1},{1,0,1}}};
|
||||
for(var face:faces){if(build.quads>=16384){build.overflow=true;return;}
|
||||
for(int i=0;i<4;i++){float u=i>=2?1:0,v=i==1||i==2?1:0;var p=face[i];
|
||||
build.out.addVertex(x+p[0],y+p[1],z+p[2]).setColor(0xFFFFFFFF).setUv(sprite.getU(u),sprite.getV(v)).setUv3(slot.u(u),slot.v(v));}
|
||||
build.quads++;
|
||||
}
|
||||
}
|
||||
void dirty(BlockPos pos){for(int x=(pos.getX()-1)>>4;x<=(pos.getX()+1)>>4;x++)for(int y=(pos.getY()-1)>>4;y<=(pos.getY()+1)>>4;y++)for(int z=(pos.getZ()-1)>>4;z<=(pos.getZ()+1)>>4;z++)dirty(sections.get(SectionPos.asLong(x,y,z)));}
|
||||
void dirtyChunk(int x,int z){for(var section:sections.values())if(Math.abs((section.origin.getX()>>4)-x)<=1&&Math.abs((section.origin.getZ()>>4)-z)<=1)dirty(section);}
|
||||
private void dirty(Section section){if(section!=null){remove(section);section.dirty=true;}}
|
||||
private void remove(Section section){if(building!=null&&building.section==section){building.close();building=null;}if(section.entry!=null){bytes-=section.entry.vertices.size();section.entry.vertices.close();section.entry=null;}}
|
||||
boolean settled(){return building==null&&visible.stream().noneMatch(s->s.dirty);}
|
||||
public void close(){if(building!=null){building.close();building=null;}for(var section:sections.values())remove(section);sections.clear();visible=List.of();materials.close();models=level=null;bytes=0;}
|
||||
private static final class Section{
|
||||
final long key;final BlockPos origin;final LevelChunkSection storage;long seen;boolean dirty=true;Entry entry;
|
||||
Section(long key,BlockPos origin,LevelChunkSection storage){this.key=key;this.origin=origin;this.storage=storage;}
|
||||
}
|
||||
private static final class Build implements AutoCloseable{
|
||||
final Section section;final ByteBufferBuilder buffer=new ByteBufferBuilder(65536);final BufferBuilder out=new BufferBuilder(buffer,PrimitiveTopology.QUADS,FORMAT);
|
||||
int cursor,quads;boolean overflow;
|
||||
Build(Section section){this.section=section;}
|
||||
public void close(){buffer.close();}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import com.mojang.blaze3d.buffers.Std140Builder;
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.renderpearl.api.GpuFormat;
|
||||
import com.mojang.renderpearl.api.buffers.GpuBuffer;
|
||||
import com.mojang.renderpearl.api.pipeline.*;
|
||||
import com.mojang.renderpearl.api.textures.FilterMode;
|
||||
import fr.koka.sanctuary.client.panorama.PanoramaCapture;
|
||||
import java.util.Optional;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
|
||||
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.MappableRingBuffer;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.level.material.FogType;
|
||||
import org.joml.Matrix4f;
|
||||
|
||||
/** Optional, texture-preserving highlights on visible voxel edges. No gameplay light changes. */
|
||||
public final class EdgeHighlights {
|
||||
// Destination * (1 + highlight): retain the block's existing colour and texture, including shadows.
|
||||
private static final RenderPipeline APPLY = RenderPipelines.register(RenderPipeline.builder()
|
||||
.withLocation(Identifier.parse("sanctuary:pipeline/edge_highlights"))
|
||||
.withVertexShader("core/screenquad")
|
||||
.withFragmentShader(Identifier.parse("sanctuary:core/edge_highlights"))
|
||||
.withBindGroupLayout(BindGroupLayout.builder()
|
||||
.withUniform("EdgeHighlights", UniformType.UNIFORM_BUFFER)
|
||||
.withUniform("WorldDepth", UniformType.COMBINED_IMAGE_SAMPLER).build())
|
||||
.withPrimitiveTopology(PrimitiveTopology.TRIANGLES).withCull(false)
|
||||
.withColorTargetState(new ColorTargetState(Optional.of(new BlendFunction(
|
||||
BlendFactor.DST_COLOR, BlendFactor.ONE, BlendFactor.ZERO, BlendFactor.ONE)),
|
||||
GpuFormat.RGBA8_UNORM, ColorTargetState.WRITE_COLOR))
|
||||
.withDepthStencilState(Optional.empty()).build());
|
||||
private static final Matrix4f projection = new Matrix4f();
|
||||
private static MappableRingBuffer uniforms;
|
||||
private static boolean applied;
|
||||
private static int renderedFrames;
|
||||
|
||||
private EdgeHighlights() { }
|
||||
|
||||
public static void register() {
|
||||
ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> release());
|
||||
ClientLifecycleEvents.CLIENT_STOPPING.register(client -> release());
|
||||
}
|
||||
|
||||
public static void captureProjection(Matrix4f matrix) { projection.set(matrix); }
|
||||
|
||||
/** Runs after cast shadows and before the hand and HUD; works without the shadow depth map. */
|
||||
public static void render() {
|
||||
applied = false;
|
||||
var client = Minecraft.getInstance();
|
||||
if (!VanillaLight.active() || !VanillaLight.edgeHighlights() || VanillaLight.edgeIntensity() == 0
|
||||
|| client.level == null) {
|
||||
release();
|
||||
return;
|
||||
}
|
||||
if (PanoramaCapture.rendering()) return;
|
||||
var camera = client.gameRenderer.gameRenderState().levelRenderState.cameraRenderState;
|
||||
if (camera.fogType != FogType.NONE) return;
|
||||
if (uniforms == null) uniforms = new MappableRingBuffer(() -> "Sanctuary edge highlights",
|
||||
GpuBuffer.USAGE_UNIFORM | GpuBuffer.USAGE_MAP_WRITE, 176);
|
||||
var viewProjection = new Matrix4f(projection).mul(camera.viewRotationMatrix);
|
||||
var inverse = new Matrix4f(viewProjection).invert();
|
||||
var fog = camera.fogData;
|
||||
uniforms.rotate();
|
||||
try (var mapped = uniforms.currentBuffer().map(false, true)) {
|
||||
Std140Builder.intoBuffer(mapped.data()).putMat4f(inverse).putMat4f(viewProjection)
|
||||
.putVec4((float)(camera.pos.x - Math.floor(camera.pos.x)),
|
||||
(float)(camera.pos.y - Math.floor(camera.pos.y)),
|
||||
(float)(camera.pos.z - Math.floor(camera.pos.z)), VanillaLight.shadowPixels())
|
||||
.putVec4(VanillaLight.edgeIntensity() * .004f,
|
||||
RenderSystem.getDevice().getDeviceInfo().isZZeroToOne() ? 1 : 0,
|
||||
fog.renderDistanceStart, fog.renderDistanceEnd)
|
||||
.putVec4(fog.environmentalStart, fog.environmentalEnd, fog.color.w, VanillaLight.edgeDistance());
|
||||
}
|
||||
var main = client.gameRenderer.mainRenderTarget();
|
||||
var encoder = RenderSystem.getDevice().createCommandEncoder();
|
||||
try (var pass = encoder.createRenderPass(() -> "Sanctuary edge highlights",
|
||||
main.getColorTextureView(), Optional.empty())) {
|
||||
pass.setPipeline(RenderSystem.getCompiledPipeline(APPLY));
|
||||
pass.setUniform("EdgeHighlights", uniforms.currentBuffer());
|
||||
pass.setUniform("WorldDepth", main.getDepthTextureView(),
|
||||
RenderSystem.getSamplerCache().getClampToEdge(FilterMode.NEAREST));
|
||||
pass.draw(3, 1, 0, 0);
|
||||
}
|
||||
renderedFrames++;
|
||||
applied = true;
|
||||
}
|
||||
|
||||
public static void release() {
|
||||
if (uniforms != null) { uniforms.close(); uniforms = null; }
|
||||
applied = false;
|
||||
}
|
||||
|
||||
static boolean applied() { return applied; }
|
||||
static boolean allocated() { return uniforms != null; }
|
||||
static int renderedFrames() { return renderedFrames; }
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import com.mojang.blaze3d.platform.NativeImage;
|
||||
import java.util.*;
|
||||
import net.minecraft.client.renderer.texture.DynamicTexture;
|
||||
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
/** One source-texture analysis per cached material. No frame readback and no generated files on disk. */
|
||||
final class PbrMaterials implements AutoCloseable {
|
||||
static final int INNER=16,TILE=20,GRID=64,SIZE=TILE*GRID,MAX_SPRITES=GRID*GRID;
|
||||
record Profile(float roughness,float metal){}
|
||||
record Slot(int x,int y,boolean empty){
|
||||
float u(float f){return (x+2+Math.clamp(f,0,1)*INNER)/SIZE;}
|
||||
float v(float f){return (y+2+Math.clamp(f,0,1)*INNER)/SIZE;}
|
||||
}
|
||||
private final Map<String,Slot> slots=new HashMap<>();
|
||||
private final Map<BlockState,Profile> profiles=new IdentityHashMap<>();
|
||||
private DynamicTexture atlas;private boolean dirty;private int analyses;
|
||||
boolean candidate(BlockState state){return !state.isAir()&&!state.liquid();}
|
||||
Profile profile(BlockState state){return profiles.computeIfAbsent(state,s->profile(BuiltInRegistries.BLOCK.getKey(s.getBlock()).getPath()));}
|
||||
static Profile profile(String name){
|
||||
if(name.equals("iron_block")||name.equals("gold_block")||name.equals("netherite_block")||name.contains("copper")&&!name.contains("ore")&&!name.contains("raw"))
|
||||
return new Profile(name.contains("oxidized")?.65f:.32f,name.contains("oxidized")?.35f:.85f);
|
||||
if(name.contains("polished")||name.contains("glazed")||name.equals("diamond_block")||name.equals("emerald_block")||name.equals("ruby_block")||name.equals("sapphire_block"))return new Profile(.42f,0);
|
||||
if(name.contains("wool")||name.contains("leaves"))return new Profile(.98f,0);
|
||||
if(name.contains("planks")||name.contains("log")||name.contains("wood"))return new Profile(.88f,0);
|
||||
return new Profile(.82f,0);
|
||||
}
|
||||
Slot slot(TextureAtlasSprite sprite,BlockState state){
|
||||
var profile=profile(state);String key=sprite.contents().name()+":"+profile;
|
||||
var existing=slots.get(key);if(existing!=null)return existing;
|
||||
if(slots.size()>=MAX_SPRITES)return new Slot(0,0,true);
|
||||
int i=slots.size(),x=i%GRID*TILE,y=i/GRID*TILE;
|
||||
if(atlas==null){atlas=new DynamicTexture("Sanctuary procedural PBR",SIZE,SIZE,false);atlas.getPixels().fillRect(0,0,SIZE,SIZE,0x008080FF);}
|
||||
boolean empty=false;
|
||||
try(var image=BloomMaterials.read(sprite.contents().name())){
|
||||
int fw=Math.min(image.getWidth(),sprite.contents().width()),fh=Math.min(image.getHeight(),sprite.contents().height());
|
||||
int[] colors=new int[INNER*INNER];
|
||||
for(int py=0;py<INNER;py++)for(int px=0;px<INNER;px++)colors[py*INNER+px]=image.getPixel(px*fw/INNER,py*fh/INNER);
|
||||
int[] pixels=generate(colors,INNER,INNER,profile);analyses++;
|
||||
for(int py=0;py<TILE;py++)for(int px=0;px<TILE;px++)atlas.getPixels().setPixel(x+px,y+py,pixels[Math.clamp(py-2,0,INNER-1)*INNER+Math.clamp(px-2,0,INNER-1)]);
|
||||
}catch(java.io.IOException e){empty=true;fr.koka.sanctuary.SanctuaryMod.LOGGER.warn("Cannot analyse PBR texture {}",sprite.contents().name(),e);}
|
||||
var result=new Slot(x,y,empty);slots.put(key,result);dirty=true;return result;
|
||||
}
|
||||
static int[] generate(int[] colors,int w,int h,Profile profile){
|
||||
int[] result=new int[colors.length];
|
||||
for(int y=0;y<h;y++)for(int x=0;x<w;x++){
|
||||
int center=colors[y*w+x];float base=luma(center);
|
||||
float left=height(colors[y*w+Math.floorMod(x-1,w)],base),right=height(colors[y*w+(x+1)%w],base);
|
||||
float up=height(colors[Math.floorMod(y-1,h)*w+x],base),down=height(colors[(y+1)%h*w+x],base);
|
||||
float nx=Math.clamp((left-right)*2,-.45f,.45f),ny=Math.clamp((up-down)*2,-.45f,.45f);
|
||||
if((center>>>24)<128){nx=ny=0;}
|
||||
int red=Math.round((nx*.5f+.5f)*255),green=Math.round((ny*.5f+.5f)*255);
|
||||
result[y*w+x]=(Math.round(profile.metal*255)<<24)|(red<<16)|(green<<8)|Math.round(profile.roughness*255);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
private static float height(int color,float fallback){return (color>>>24)<128?fallback:luma(color);}
|
||||
private static float luma(int c){return ((c>>16&255)*.2126f+(c>>8&255)*.7152f+(c&255)*.0722f)/255;}
|
||||
DynamicTexture atlas(){if(dirty){atlas.upload();dirty=false;}return atlas;}
|
||||
int count(){return slots.size();}int analyses(){return analyses;}
|
||||
public void close(){if(atlas!=null){atlas.close();atlas=null;}slots.clear();profiles.clear();dirty=false;analyses=0;}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.blaze3d.vertex.BufferBuilder;
|
||||
import com.mojang.blaze3d.vertex.ByteBufferBuilder;
|
||||
import com.mojang.renderpearl.api.GpuFormat;
|
||||
import com.mojang.renderpearl.api.buffers.GpuBuffer;
|
||||
import com.mojang.renderpearl.api.pipeline.PrimitiveTopology;
|
||||
import com.mojang.renderpearl.api.vertex.VertexFormat;
|
||||
import java.util.*;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.model.geom.builders.UVPair;
|
||||
import net.minecraft.client.renderer.block.dispatch.BlockStateModelPart;
|
||||
import net.minecraft.client.renderer.texture.TextureAtlas;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.SectionPos;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.chunk.LevelChunkSection;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Nearby native sections, bounded incremental meshing; models and resource UV rotations remain native. */
|
||||
final class PbrTerrain implements AutoCloseable {
|
||||
static final VertexFormat FORMAT=VertexFormat.builder(0).addAttribute("Position",GpuFormat.RGB32_FLOAT)
|
||||
.addAttribute("Color",GpuFormat.RGBA8_UNORM).addAttribute("UV0",GpuFormat.RG32_FLOAT)
|
||||
.addAttribute("UV3",GpuFormat.RG32_FLOAT).build();
|
||||
private static final long MAX_BYTES=24L*1024*1024;
|
||||
private final Map<Long,Section> sections=new HashMap<>();
|
||||
final PbrMaterials materials=new PbrMaterials();
|
||||
private Build building;
|
||||
private long bytes;
|
||||
private Object models,level;
|
||||
private List<Section> visible=List.of();
|
||||
record Entry(BlockPos origin,GpuBuffer vertices,int indices){}
|
||||
List<Entry> update(){
|
||||
var mc=Minecraft.getInstance();var current=mc.getModelManager().getBlockStateModelSet();
|
||||
if(models!=current||level!=mc.level){close();models=current;level=mc.level;}
|
||||
Vec3 camera=mc.gameRenderer.gameRenderState().levelRenderState.cameraRenderState.pos;
|
||||
long frame=mc.level.getGameTime();var active=new ArrayList<Section>();
|
||||
for(var render:mc.levelRenderer.visibleSections()){
|
||||
long key=render.getSectionNode();var pos=render.getRenderOrigin();
|
||||
if(pos.offset(8,8,8).distToCenterSqr(camera)>48*48)continue;
|
||||
var chunk=mc.level.getChunkSource().getChunk(pos.getX()>>4,pos.getZ()>>4,ChunkStatus.FULL,false);
|
||||
if(chunk==null)continue;
|
||||
var storage=chunk.getSection(mc.level.getSectionIndex(pos.getY()));
|
||||
var section=sections.get(key);
|
||||
if(section==null||section.storage!=storage){if(section!=null)remove(section);section=new Section(key,pos.immutable(),storage);sections.put(key,section);}
|
||||
section.seen=frame;active.add(section);
|
||||
}
|
||||
visible=active;
|
||||
for(var iterator=sections.values().iterator();iterator.hasNext();){var section=iterator.next();if(frame-section.seen>40){remove(section);iterator.remove();}}
|
||||
long deadline=System.nanoTime()+3_000_000L;int started=building==null?0:1;
|
||||
while(System.nanoTime()<deadline){
|
||||
if(building==null){
|
||||
if(started++>=4)break;
|
||||
Section next=active.stream().filter(s->s.dirty).min(Comparator.comparingDouble(s->s.origin.distToCenterSqr(camera))).orElse(null);
|
||||
if(next==null)break;
|
||||
if(!next.storage.maybeHas(materials::candidate)){next.dirty=false;continue;}
|
||||
building=new Build(next);
|
||||
}
|
||||
if(!advance(building,deadline))break;
|
||||
var done=building;building=null;
|
||||
try(done){
|
||||
done.section.dirty=false;
|
||||
if(done.quads>0&&!done.overflow){
|
||||
try(var mesh=done.out.buildOrThrow()){
|
||||
long size=mesh.vertexBuffer().remaining();
|
||||
if(bytes+size<=MAX_BYTES){
|
||||
var buffer=RenderSystem.getDevice().createBuffer(()->"Sanctuary PBR surfaces",GpuBuffer.USAGE_VERTEX|GpuBuffer.USAGE_COPY_DST,mesh.vertexBuffer());
|
||||
int count=mesh.drawState().indexCount();RenderSystem.getSequentialBuffer(PrimitiveTopology.QUADS).getBuffer(count);
|
||||
done.section.entry=new Entry(done.section.origin,buffer,count);bytes+=buffer.size();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return active.stream().map(s->s.entry).filter(Objects::nonNull).toList();
|
||||
}
|
||||
private boolean advance(Build build,long deadline){
|
||||
var mc=Minecraft.getInstance();var models=mc.getModelManager().getBlockStateModelSet();
|
||||
var parts=new ArrayList<BlockStateModelPart>();var random=RandomSource.create();
|
||||
while(build.cursor<4096){
|
||||
int i=build.cursor++,x=i&15,z=i>>4&15,y=i>>8;var state=build.section.storage.getBlockState(x,y,z);
|
||||
if(materials.candidate(state)){
|
||||
var pos=build.section.origin.offset(x,y,z);
|
||||
parts.clear();random.setSeed(state.getSeed(pos));if(!state.liquid())models.get(state).collectParts(random,parts);
|
||||
var tintSources=mc.getBlockColors().getTintSources(state);
|
||||
var offset=state.getOffset(pos);
|
||||
for(var part:parts)for(int f=0;f<7;f++){
|
||||
Direction face=f==6?null:Direction.values()[f];
|
||||
if(face!=null&&mc.level.getBlockState(pos.relative(face)).isSolidRender())continue;
|
||||
for(var quad:part.getQuads(face)){
|
||||
var material=quad.materialInfo();var sprite=material.sprite();
|
||||
if(!sprite.atlasLocation().equals(TextureAtlas.LOCATION_BLOCKS))continue;
|
||||
var slot=materials.slot(sprite,state);
|
||||
if(slot.empty())continue;
|
||||
if(build.quads>=16384){build.overflow=true;return true;}
|
||||
int tint=0xFFFFFF,index=material.tintIndex();
|
||||
if(index>=0&&index<tintSources.size())tint=tintSources.get(index).colorInWorld(state,mc.level,pos)&0xFFFFFF;
|
||||
int sky=mc.level.getBrightness(net.minecraft.world.level.LightLayer.SKY,face==null?pos:pos.relative(face));
|
||||
int color=(Math.round(sky/15f*255)<<24)|tint;
|
||||
for(int v=0;v<4;v++){
|
||||
var p=quad.position(v);float u=UVPair.unpackU(quad.packedUV(v)),tv=UVPair.unpackV(quad.packedUV(v));
|
||||
float localU=(u-sprite.getU0())/(sprite.getU1()-sprite.getU0()),localV=(tv-sprite.getV0())/(sprite.getV1()-sprite.getV0());
|
||||
build.out.addVertex(x+p.x()+(float)offset.x,y+p.y()+(float)offset.y,z+p.z()+(float)offset.z)
|
||||
.setColor(color).setUv(u,tv).setUv3(slot.u(localU),slot.v(localV));
|
||||
}
|
||||
build.quads++;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
if(System.nanoTime()>=deadline)return build.cursor==4096;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
void dirtySection(int x,int y,int z){dirty(sections.get(SectionPos.asLong(x,y,z)));}
|
||||
void dirty(BlockPos pos){for(int x=(pos.getX()-1)>>4;x<=(pos.getX()+1)>>4;x++)for(int y=(pos.getY()-1)>>4;y<=(pos.getY()+1)>>4;y++)for(int z=(pos.getZ()-1)>>4;z<=(pos.getZ()+1)>>4;z++)dirty(sections.get(SectionPos.asLong(x,y,z)));}
|
||||
void dirtyChunk(int x,int z){for(var section:sections.values())if(Math.abs((section.origin.getX()>>4)-x)<=1&&Math.abs((section.origin.getZ()>>4)-z)<=1)dirty(section);}
|
||||
private void dirty(Section section){if(section!=null){remove(section);section.dirty=true;}}
|
||||
private void remove(Section section){if(building!=null&&building.section==section){building.close();building=null;}if(section.entry!=null){bytes-=section.entry.vertices.size();section.entry.vertices.close();section.entry=null;}}
|
||||
boolean settled(){return building==null&&visible.stream().noneMatch(s->s.dirty);}
|
||||
public void close(){if(building!=null){building.close();building=null;}for(var section:sections.values())remove(section);sections.clear();visible=List.of();materials.close();models=level=null;bytes=0;}
|
||||
private static final class Section{
|
||||
final long key;final BlockPos origin;final LevelChunkSection storage;long seen;boolean dirty=true;Entry entry;
|
||||
Section(long key,BlockPos origin,LevelChunkSection storage){this.key=key;this.origin=origin;this.storage=storage;}
|
||||
}
|
||||
private static final class Build implements AutoCloseable{
|
||||
final Section section;final ByteBufferBuilder buffer=new ByteBufferBuilder(65536);final BufferBuilder out=new BufferBuilder(buffer,PrimitiveTopology.QUADS,FORMAT);
|
||||
int cursor,quads;boolean overflow;
|
||||
Build(Section section){this.section=section;}
|
||||
public void close(){buffer.close();}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
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.blaze3d.vertex.DefaultVertexFormat;
|
||||
import com.mojang.renderpearl.api.GpuFormat;
|
||||
import com.mojang.renderpearl.api.buffers.GpuBuffer;
|
||||
import com.mojang.renderpearl.api.buffers.GpuBufferSlice;
|
||||
import com.mojang.renderpearl.api.pipeline.*;
|
||||
import com.mojang.renderpearl.api.textures.FilterMode;
|
||||
import fr.koka.sanctuary.client.panorama.PanoramaCapture;
|
||||
import java.util.*;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
|
||||
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.client.renderer.BindGroupLayouts;
|
||||
import net.minecraft.client.renderer.MappableRingBuffer;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.client.renderer.state.level.CameraRenderState;
|
||||
import net.minecraft.world.level.dimension.DimensionType;
|
||||
import net.minecraft.world.level.material.FogType;
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Vector3f;
|
||||
import org.joml.Vector4f;
|
||||
|
||||
/** Native sun/moon depth map and world-grid receivers. Never reads GPU pixels or changes gameplay light. */
|
||||
public final class PixelShadows {
|
||||
private static final RenderPipeline DEPTH = RenderPipelines.register(RenderPipeline.builder()
|
||||
.withLocation(Identifier.parse("sanctuary:pipeline/pixel_shadow_depth"))
|
||||
.withVertexShader(Identifier.parse("sanctuary:core/pixel_shadow_depth"))
|
||||
.withFragmentShader(Identifier.parse("sanctuary:core/pixel_shadow_depth"))
|
||||
.withBindGroupLayout(BindGroupLayouts.DYNAMIC_TRANSFORMS)
|
||||
.withVertexBinding(0, DefaultVertexFormat.POSITION_COLOR)
|
||||
.withPrimitiveTopology(PrimitiveTopology.QUADS).withCull(false)
|
||||
.withColorTargetState(new ColorTargetState(Optional.empty(), GpuFormat.R8_UNORM, ColorTargetState.WRITE_COLOR))
|
||||
.withDepthStencilState(new DepthStencilState(CompareOp.LESS_THAN, true)).build());
|
||||
private static final RenderPipeline APPLY = RenderPipelines.register(RenderPipeline.builder()
|
||||
.withLocation(Identifier.parse("sanctuary:pipeline/pixel_shadows"))
|
||||
.withVertexShader("core/screenquad")
|
||||
.withFragmentShader(Identifier.parse("sanctuary:core/pixel_shadows"))
|
||||
.withBindGroupLayout(BindGroupLayout.builder()
|
||||
.withUniform("PixelShadows", UniformType.UNIFORM_BUFFER)
|
||||
.withUniform("WorldDepth", UniformType.COMBINED_IMAGE_SAMPLER)
|
||||
.withUniform("SunDepth", UniformType.COMBINED_IMAGE_SAMPLER)
|
||||
.withUniform("SunMaterial", UniformType.COMBINED_IMAGE_SAMPLER).build())
|
||||
.withPrimitiveTopology(PrimitiveTopology.TRIANGLES).withCull(false)
|
||||
.withColorTargetState(new ColorTargetState(Optional.of(BlendFunction.TRANSLUCENT), GpuFormat.RGBA8_UNORM, ColorTargetState.WRITE_COLOR))
|
||||
.withDepthStencilState(Optional.empty()).build());
|
||||
private static final ShadowTerrain TERRAIN = new ShadowTerrain();
|
||||
private static final Matrix4f projection = new Matrix4f();
|
||||
private static final ShadowProjection lightGrid = new ShadowProjection();
|
||||
private static TextureTarget shadowMap;
|
||||
private static MappableRingBuffer uniforms;
|
||||
private static int renderedFrames;
|
||||
private static boolean applied;
|
||||
|
||||
private PixelShadows() { }
|
||||
public static void register() {
|
||||
ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> release());
|
||||
ClientLifecycleEvents.CLIENT_STOPPING.register(client -> release());
|
||||
}
|
||||
public static void captureProjection(Matrix4f matrix) { projection.set(matrix); }
|
||||
public static void dirtyBlock(BlockPos pos) {
|
||||
for (int x = (pos.getX()-1)>>4; x <= (pos.getX()+1)>>4; x++)
|
||||
for (int y = (pos.getY()-1)>>4; y <= (pos.getY()+1)>>4; y++)
|
||||
for (int z = (pos.getZ()-1)>>4; z <= (pos.getZ()+1)>>4; z++) TERRAIN.dirtySection(x,y,z);
|
||||
}
|
||||
public static void dirtyChunk(int x, int z) { TERRAIN.dirtyChunk(x, z); }
|
||||
|
||||
/** Called after the world's depth is resolved and before the first-person hand and GUI. */
|
||||
public static void render() {
|
||||
applied = false;
|
||||
var client = Minecraft.getInstance();
|
||||
if (!VanillaLight.active() || !VanillaLight.pixelShadows() || client.level == null) {
|
||||
release();
|
||||
return;
|
||||
}
|
||||
if (PanoramaCapture.rendering()) return;
|
||||
var state = client.gameRenderer.gameRenderState().levelRenderState;
|
||||
var sky = state.skyRenderState;
|
||||
CameraRenderState camera = state.cameraRenderState;
|
||||
var sun = sunDirection(sky.sunAngle);
|
||||
boolean lunar = sun.y <= .08f;
|
||||
if (lunar) sun = sunDirection(sky.moonAngle);
|
||||
if (sky.skybox != DimensionType.Skybox.OVERWORLD || camera.fogType != FogType.NONE || sun.y <= .08f) return;
|
||||
float moon = sky.moonPhase == null ? 1 : (float)(.5 + .5 * Math.cos(sky.moonPhase.index() * Math.PI / 4));
|
||||
float strength = (lunar ? .10f * moon : .38f)
|
||||
* Math.clamp((sun.y - .08f) / .16f, 0, 1) * sky.rainBrightness;
|
||||
if (strength <= .001f) return;
|
||||
int distance = VanillaLight.shadowDistance(), pixels = VanillaLight.shadowPixels();
|
||||
TERRAIN.update(client.level, camera.pos, distance, sun);
|
||||
int size = mapSize(distance, pixels);
|
||||
if (shadowMap == null || shadowMap.width != size) {
|
||||
if (shadowMap != null) shadowMap.destroyBuffers();
|
||||
shadowMap = new TextureTarget("Sanctuary celestial shadows", size, size, GpuFormat.R8_UNORM, GpuFormat.D32_FLOAT);
|
||||
}
|
||||
if (uniforms == null) uniforms = new MappableRingBuffer(() -> "Sanctuary pixel shadows",
|
||||
GpuBuffer.USAGE_UNIFORM | GpuBuffer.USAGE_MAP_WRITE, 192);
|
||||
double ox = anchor(camera.pos.x), oy = anchor(camera.pos.y), oz = anchor(camera.pos.z);
|
||||
boolean zeroToOne = RenderSystem.getDevice().getDeviceInfo().isZZeroToOne();
|
||||
var light = lightGrid.matrix(sun, distance, size, ox, oy, oz, zeroToOne);
|
||||
var inverse = new Matrix4f(projection).mul(camera.viewRotationMatrix).invert();
|
||||
uniforms.rotate();
|
||||
var fog = camera.fogData;
|
||||
try (var mapped = uniforms.currentBuffer().map(false, true)) {
|
||||
Std140Builder.intoBuffer(mapped.data()).putMat4f(inverse).putMat4f(light)
|
||||
.putVec4((float)(camera.pos.x - ox), (float)(camera.pos.y - oy), (float)(camera.pos.z - oz), pixels)
|
||||
.putVec4(sun.x, sun.y, sun.z, strength)
|
||||
.putVec4(distance, zeroToOne ? 1 : 0, fog.renderDistanceStart, fog.renderDistanceEnd)
|
||||
.putVec4(fog.environmentalStart, fog.environmentalEnd, fog.color.w, 0);
|
||||
}
|
||||
var entries = TERRAIN.entries();
|
||||
var transforms = new ArrayList<GpuBufferSlice>(entries.size());
|
||||
int maxIndices = 0;
|
||||
for (var entry : entries) {
|
||||
transforms.add(RenderSystem.getDynamicUniforms().writeTransform(new Matrix4f(light)
|
||||
.translate((float)(entry.x() - ox), (float)(entry.y() - oy), (float)(entry.z() - oz)),
|
||||
new Vector4f(1, 1, 1, 1),
|
||||
// Bounded world coordinates keep foliage noise precise far from spawn.
|
||||
new Vector3f(Math.floorMod(entry.x(), 4096), Math.floorMod(entry.y(), 4096),
|
||||
Math.floorMod(entry.z(), 4096)), new Matrix4f()));
|
||||
maxIndices = Math.max(maxIndices, entry.indexCount());
|
||||
}
|
||||
var indices = RenderSystem.getSequentialBuffer(PrimitiveTopology.QUADS);
|
||||
indices.requestIndexCount(maxIndices);
|
||||
var encoder = RenderSystem.getDevice().createCommandEncoder();
|
||||
try (var pass = encoder.createRenderPass(com.mojang.renderpearl.api.commands.RenderPassDescriptor.builder(() -> "Sanctuary celestial depth")
|
||||
.withColorAttachment(shadowMap.getColorTextureView(), Optional.of(new Vector4f(0, 0, 0, 0)))
|
||||
.withDepthAttachment(shadowMap.getDepthTextureView(), OptionalDouble.of(1)).build())) {
|
||||
pass.setPipeline(RenderSystem.getCompiledPipeline(DEPTH));
|
||||
if (maxIndices > 0) pass.setIndexBuffer(indices.getBuffer(), indices.type());
|
||||
for (int i = 0; i < entries.size(); i++) {
|
||||
var entry = entries.get(i);
|
||||
pass.setUniform("DynamicTransforms", transforms.get(i));
|
||||
pass.setVertexBuffer(0, entry.vertices().slice());
|
||||
pass.drawIndexed(entry.indexCount(), 1, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
var main = client.gameRenderer.mainRenderTarget();
|
||||
var nearest = RenderSystem.getSamplerCache().getClampToEdge(FilterMode.NEAREST);
|
||||
try (var pass = encoder.createRenderPass(() -> "Sanctuary pixel shadows", main.getColorTextureView(), Optional.empty())) {
|
||||
pass.setPipeline(RenderSystem.getCompiledPipeline(APPLY));
|
||||
pass.setUniform("PixelShadows", uniforms.currentBuffer());
|
||||
pass.setUniform("WorldDepth", main.getDepthTextureView(), nearest);
|
||||
pass.setUniform("SunDepth", shadowMap.getDepthTextureView(), nearest);
|
||||
pass.setUniform("SunMaterial", shadowMap.getColorTextureView(), nearest);
|
||||
pass.draw(3, 1, 0, 0);
|
||||
}
|
||||
renderedFrames++;
|
||||
applied = true;
|
||||
}
|
||||
|
||||
static double anchor(double coordinate) { return Math.floor(coordinate / 16.0 + .5) * 16.0; }
|
||||
static com.mojang.renderpearl.api.buffers.GpuBuffer materialUniforms(){return uniforms.currentBuffer();}
|
||||
static com.mojang.renderpearl.api.textures.GpuTextureView materialDepth(){return shadowMap.getDepthTextureView();}
|
||||
static Vector3f sunDirection(float angle) { return new Vector3f(-(float)Math.sin(angle), (float)Math.cos(angle), 0); }
|
||||
static int mapSize(int distance, int pixels) {
|
||||
int size = 512;
|
||||
while (size < 2 * (distance + 16) * pixels && size < 4096) size *= 2;
|
||||
return size;
|
||||
}
|
||||
public static void release() {
|
||||
TERRAIN.close();
|
||||
lightGrid.reset();
|
||||
if (shadowMap != null) { shadowMap.destroyBuffers(); shadowMap = null; }
|
||||
if (uniforms != null) { uniforms.close(); uniforms = null; }
|
||||
applied = false;
|
||||
}
|
||||
static int renderedFrames() { return renderedFrames; }
|
||||
static boolean applied() { return applied; }
|
||||
static boolean ready() { return TERRAIN.settled(); }
|
||||
static TextureTarget shadowMap() { return shadowMap; }
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import com.mojang.blaze3d.buffers.Std140Builder;
|
||||
import com.mojang.blaze3d.pipeline.TextureTarget;
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.renderpearl.api.GpuFormat;
|
||||
import com.mojang.renderpearl.api.buffers.GpuBuffer;
|
||||
import com.mojang.renderpearl.api.buffers.GpuBufferSlice;
|
||||
import com.mojang.renderpearl.api.pipeline.*;
|
||||
import com.mojang.renderpearl.api.textures.FilterMode;
|
||||
import fr.koka.sanctuary.client.panorama.PanoramaCapture;
|
||||
import java.util.*;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.BindGroupLayouts;
|
||||
import net.minecraft.client.renderer.MappableRingBuffer;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.client.renderer.texture.TextureAtlas;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.level.dimension.DimensionType;
|
||||
import net.minecraft.world.level.material.FogType;
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Vector3f;
|
||||
import org.joml.Vector4f;
|
||||
|
||||
/** Optional local material pass: cached texture normals, GGX highlights and diffuse relief. */
|
||||
public final class ProceduralPbr {
|
||||
private static final BindGroupLayout SCENE=BindGroupLayout.builder().withUniform("PbrScene",UniformType.UNIFORM_BUFFER)
|
||||
.withUniform("PixelShadows",UniformType.UNIFORM_BUFFER).withUniform("SunDepth",UniformType.COMBINED_IMAGE_SAMPLER)
|
||||
.withUniform("WorldDepth",UniformType.COMBINED_IMAGE_SAMPLER).withUniform("SceneColor",UniformType.COMBINED_IMAGE_SAMPLER)
|
||||
.withUniform("BlockAtlas",UniformType.COMBINED_IMAGE_SAMPLER).withUniform("MaterialAtlas",UniformType.COMBINED_IMAGE_SAMPLER).build();
|
||||
private static final RenderPipeline PIPELINE=RenderPipelines.register(RenderPipeline.builder()
|
||||
.withLocation(Identifier.parse("sanctuary:pipeline/pbr")).withVertexShader(Identifier.parse("sanctuary:core/pbr"))
|
||||
.withFragmentShader(Identifier.parse("sanctuary:core/pbr")).withBindGroupLayout(BindGroupLayouts.DYNAMIC_TRANSFORMS).withBindGroupLayout(SCENE)
|
||||
.withVertexBinding(0,PbrTerrain.FORMAT).withPrimitiveTopology(PrimitiveTopology.QUADS).withCull(false)
|
||||
.withColorTargetState(new ColorTargetState(Optional.empty(),GpuFormat.RGBA8_UNORM,ColorTargetState.WRITE_COLOR))
|
||||
.withDepthStencilState(Optional.empty()).build());
|
||||
private static final PbrTerrain TERRAIN=new PbrTerrain();
|
||||
private static final Matrix4f projection=new Matrix4f();
|
||||
private static TextureTarget source;
|
||||
private static MappableRingBuffer scene;
|
||||
private static boolean applied;private static int frames;
|
||||
private ProceduralPbr(){}
|
||||
public static void captureProjection(Matrix4f matrix){projection.set(matrix);}
|
||||
public static void dirtyBlock(BlockPos pos){TERRAIN.dirty(pos);}
|
||||
public static void dirtyChunk(int x,int z){TERRAIN.dirtyChunk(x,z);}
|
||||
public static void dirtySection(int x,int y,int z){TERRAIN.dirtySection(x,y,z);}
|
||||
public static void render(){
|
||||
applied=false;var mc=Minecraft.getInstance();
|
||||
if(!VanillaLight.active()||!VanillaLight.pbr()||VanillaLight.pbrIntensity()==0||mc.level==null){release();return;}
|
||||
var state=mc.gameRenderer.gameRenderState().levelRenderState;var camera=state.cameraRenderState;var sky=state.skyRenderState;
|
||||
if(PanoramaCapture.rendering()||camera.fogType!=FogType.NONE||sky.skybox!=DimensionType.Skybox.OVERWORLD)return;
|
||||
var main=mc.gameRenderer.mainRenderTarget();
|
||||
if(source==null||source.width!=main.width||source.height!=main.height){if(source!=null)source.destroyBuffers();source=new TextureTarget("Sanctuary PBR source",main.width,main.height,GpuFormat.RGBA8_UNORM,null);}
|
||||
// The extra padding also supplies a valid inactive PixelShadows binding (192 bytes).
|
||||
if(scene==null)scene=new MappableRingBuffer(()->"Sanctuary PBR scene",GpuBuffer.USAGE_UNIFORM|GpuBuffer.USAGE_MAP_WRITE,192);
|
||||
var entries=TERRAIN.update();var atlas=TERRAIN.materials.atlas();
|
||||
var vp=new Matrix4f(projection).mul(camera.viewRotationMatrix);var fog=camera.fogData;
|
||||
var sun=PixelShadows.sunDirection(sky.sunAngle);boolean lunar=sun.y<=.08f;
|
||||
if(lunar)sun=PixelShadows.sunDirection(sky.moonAngle);
|
||||
float moon=sky.moonPhase==null?1:(float)(.5+.5*Math.cos(sky.moonPhase.index()*Math.PI/4));
|
||||
float strength=(lunar?.12f*moon:1)*sky.rainBrightness*Math.clamp((sun.y-.04f)/.16f,0,1);
|
||||
boolean shadow=PixelShadows.applied();
|
||||
scene.rotate();try(var mapped=scene.currentBuffer().map(false,true)){
|
||||
Std140Builder.intoBuffer(mapped.data()).putMat4f(new Matrix4f(vp).invert())
|
||||
.putVec4(sun.x,sun.y,sun.z,strength)
|
||||
.putVec4(VanillaLight.pbrIntensity()/100f,RenderSystem.getDevice().getDeviceInfo().isZZeroToOne()?1:0,shadow?1:0,32)
|
||||
.putVec4(fog.renderDistanceStart,fog.renderDistanceEnd,fog.environmentalStart,fog.environmentalEnd).putVec4(fog.color.w,0,0,0)
|
||||
.putVec4(0,0,0,0).putVec4(0,0,0,0).putVec4(0,0,0,0).putVec4(0,0,0,0);
|
||||
}
|
||||
var transforms=new ArrayList<GpuBufferSlice>(entries.size());int max=0;
|
||||
for(var entry:entries){var p=entry.origin();var offset=new Vector3f((float)(p.getX()-camera.pos.x),(float)(p.getY()-camera.pos.y),(float)(p.getZ()-camera.pos.z));
|
||||
transforms.add(RenderSystem.getDynamicUniforms().writeTransform(new Matrix4f(vp).translate(offset),new Vector4f(1,1,1,1),offset,new Matrix4f()));max=Math.max(max,entry.indices());}
|
||||
var indices=RenderSystem.getSequentialBuffer(PrimitiveTopology.QUADS);indices.requestIndexCount(max);
|
||||
var encoder=RenderSystem.getDevice().createCommandEncoder();
|
||||
encoder.copyTextureToTexture(main.getColorTexture(),source.getColorTexture(),0,0,0,0,0,main.width,main.height);
|
||||
var nearest=RenderSystem.getSamplerCache().getClampToEdge(FilterMode.NEAREST);
|
||||
if(atlas!=null&&!entries.isEmpty())try(var pass=encoder.createRenderPass(()->"Sanctuary PBR materials",main.getColorTextureView(),Optional.empty())){
|
||||
pass.setPipeline(RenderSystem.getCompiledPipeline(PIPELINE));pass.setUniform("PbrScene",scene.currentBuffer());
|
||||
pass.setUniform("PixelShadows",shadow?PixelShadows.materialUniforms():scene.currentBuffer());
|
||||
pass.setUniform("SunDepth",shadow?PixelShadows.materialDepth():main.getDepthTextureView(),nearest);
|
||||
pass.setUniform("WorldDepth",main.getDepthTextureView(),nearest);pass.setUniform("SceneColor",source.getColorTextureView(),nearest);
|
||||
pass.setUniform("BlockAtlas",mc.getTextureManager().getTexture(TextureAtlas.LOCATION_BLOCKS).getTextureView(),nearest);
|
||||
pass.setUniform("MaterialAtlas",atlas.getTextureView(),nearest);pass.setIndexBuffer(indices.getBuffer(),indices.type());
|
||||
for(int i=0;i<entries.size();i++){var entry=entries.get(i);pass.setUniform("DynamicTransforms",transforms.get(i));pass.setVertexBuffer(0,entry.vertices().slice());pass.drawIndexed(entry.indices(),1,0,0,0);}
|
||||
}
|
||||
applied=true;frames++;
|
||||
}
|
||||
public static void release(){TERRAIN.close();if(source!=null){source.destroyBuffers();source=null;}if(scene!=null){scene.close();scene=null;}applied=false;}
|
||||
static boolean applied(){return applied;}static boolean ready(){return TERRAIN.settled();}
|
||||
static boolean allocated(){return source!=null;}static int renderedFrames(){return frames;}
|
||||
static int materialCount(){return TERRAIN.materials.count();}static int analyses(){return TERRAIN.materials.analyses();}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import com.mojang.blaze3d.buffers.Std140Builder;
|
||||
import com.mojang.blaze3d.pipeline.TextureTarget;
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.renderpearl.api.GpuFormat;
|
||||
import com.mojang.renderpearl.api.buffers.GpuBuffer;
|
||||
import com.mojang.renderpearl.api.pipeline.*;
|
||||
import com.mojang.renderpearl.api.textures.FilterMode;
|
||||
import fr.koka.sanctuary.client.panorama.PanoramaCapture;
|
||||
import java.util.Optional;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.MappableRingBuffer;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.level.material.FogType;
|
||||
import org.joml.Matrix4f;
|
||||
|
||||
/** Bounded, single-bounce diffuse lighting from this frame's visible surfaces. No GPU readback or history. */
|
||||
public final class ScreenSpaceGI {
|
||||
private static final BindGroupLayout LAYOUT=BindGroupLayout.builder()
|
||||
.withUniform("GIParams",UniformType.UNIFORM_BUFFER)
|
||||
.withUniform("WorldDepth",UniformType.COMBINED_IMAGE_SAMPLER)
|
||||
.withUniform("SceneColor",UniformType.COMBINED_IMAGE_SAMPLER)
|
||||
.withUniform("IndirectLight",UniformType.COMBINED_IMAGE_SAMPLER).build();
|
||||
private static RenderPipeline pipeline(String name,boolean composite){
|
||||
return RenderPipelines.register(RenderPipeline.builder().withLocation(Identifier.parse("sanctuary:pipeline/"+name))
|
||||
.withVertexShader("core/screenquad").withFragmentShader(Identifier.parse("sanctuary:core/"+name))
|
||||
.withBindGroupLayout(LAYOUT).withBindGroupLayout(BindGroupLayout.builder().withUniform("GIBlur",UniformType.UNIFORM_BUFFER).build()).withPrimitiveTopology(PrimitiveTopology.TRIANGLES).withCull(false)
|
||||
.withColorTargetState(new ColorTargetState(composite?Optional.of(new BlendFunction(BlendFactor.ONE,BlendFactor.ZERO,BlendFactor.ZERO,BlendFactor.ONE)):Optional.empty(),
|
||||
composite?GpuFormat.RGBA8_UNORM:GpuFormat.RGBA16_FLOAT,composite?ColorTargetState.WRITE_COLOR:ColorTargetState.WRITE_ALL))
|
||||
.withDepthStencilState(Optional.empty()).build());
|
||||
}
|
||||
private static final RenderPipeline GATHER=pipeline("ssgi_gather",false),APPLY=pipeline("ssgi_apply",true),FILTER=pipeline("ssgi_filter",false);
|
||||
private static final Matrix4f projection=new Matrix4f();
|
||||
private static TextureTarget source,indirect,filtered;
|
||||
private static MappableRingBuffer uniforms,horizontal,vertical;
|
||||
private static boolean applied;
|
||||
private static int frames;
|
||||
private ScreenSpaceGI(){}
|
||||
public static void captureProjection(Matrix4f matrix){projection.set(matrix);}
|
||||
public static void render(){
|
||||
applied=false;var mc=Minecraft.getInstance();
|
||||
if(!VanillaLight.active()||!VanillaLight.ssgi()||VanillaLight.ssgiIntensity()==0||mc.level==null){release();return;}
|
||||
if(PanoramaCapture.rendering())return;
|
||||
var camera=mc.gameRenderer.gameRenderState().levelRenderState.cameraRenderState;
|
||||
if(camera.fogType!=FogType.NONE)return;
|
||||
var main=mc.gameRenderer.mainRenderTarget();
|
||||
if(source==null||source.width!=main.width||source.height!=main.height){
|
||||
release();source=new TextureTarget("Sanctuary SSGI source",main.width,main.height,GpuFormat.RGBA8_UNORM,null);
|
||||
indirect=new TextureTarget("Sanctuary SSGI indirect",Math.max(1,(main.width+1)/2),Math.max(1,(main.height+1)/2),GpuFormat.RGBA16_FLOAT,null);
|
||||
filtered=new TextureTarget("Sanctuary SSGI filtered",indirect.width,indirect.height,GpuFormat.RGBA16_FLOAT,null);
|
||||
}
|
||||
if(uniforms==null){
|
||||
uniforms=new MappableRingBuffer(()->"Sanctuary SSGI",GpuBuffer.USAGE_UNIFORM|GpuBuffer.USAGE_MAP_WRITE,176);
|
||||
horizontal=new MappableRingBuffer(()->"Sanctuary SSGI filter X",GpuBuffer.USAGE_UNIFORM|GpuBuffer.USAGE_MAP_WRITE,16);
|
||||
vertical=new MappableRingBuffer(()->"Sanctuary SSGI filter Y",GpuBuffer.USAGE_UNIFORM|GpuBuffer.USAGE_MAP_WRITE,16);
|
||||
}
|
||||
horizontal.rotate();try(var m=horizontal.currentBuffer().map(false,true)){Std140Builder.intoBuffer(m.data()).putVec4(1,0,0,0);}
|
||||
vertical.rotate();try(var m=vertical.currentBuffer().map(false,true)){Std140Builder.intoBuffer(m.data()).putVec4(0,1,0,0);}
|
||||
|
||||
var vp=new Matrix4f(projection).mul(camera.viewRotationMatrix);var fog=camera.fogData;
|
||||
uniforms.rotate();try(var mapped=uniforms.currentBuffer().map(false,true)){
|
||||
Std140Builder.intoBuffer(mapped.data()).putMat4f(new Matrix4f(vp).invert()).putMat4f(vp)
|
||||
.putVec4(VanillaLight.ssgiIntensity()/100f,RenderSystem.getDevice().getDeviceInfo().isZZeroToOne()?1:0,2.5f,32)
|
||||
.putVec4(fog.renderDistanceStart,fog.renderDistanceEnd,fog.environmentalStart,fog.environmentalEnd)
|
||||
.putVec4(fog.color.w,0,0,0);
|
||||
}
|
||||
// Snapshot once, before adding indirect light: no feedback, no texture sampled while attached for writing.
|
||||
var encoder=RenderSystem.getDevice().createCommandEncoder();
|
||||
encoder.copyTextureToTexture(main.getColorTexture(),source.getColorTexture(),0,0,0,0,0,main.width,main.height);
|
||||
var nearest=RenderSystem.getSamplerCache().getClampToEdge(FilterMode.NEAREST);
|
||||
try(var pass=encoder.createRenderPass(()->"Sanctuary SSGI gather",indirect.getColorTextureView(),Optional.empty())){
|
||||
pass.setPipeline(RenderSystem.getCompiledPipeline(GATHER));pass.setUniform("GIBlur",horizontal.currentBuffer());pass.setUniform("GIParams",uniforms.currentBuffer());
|
||||
pass.setUniform("WorldDepth",main.getDepthTextureView(),nearest);pass.setUniform("SceneColor",source.getColorTextureView(),nearest);
|
||||
// Unused by gather; bind a separate texture, never its own output.
|
||||
pass.setUniform("IndirectLight",source.getColorTextureView(),nearest);pass.draw(3,1,0,0);
|
||||
}
|
||||
filter(encoder,indirect,filtered,horizontal);
|
||||
filter(encoder,filtered,indirect,vertical);
|
||||
try(var pass=encoder.createRenderPass(()->"Sanctuary SSGI composite",main.getColorTextureView(),Optional.empty())){
|
||||
pass.setPipeline(RenderSystem.getCompiledPipeline(APPLY));pass.setUniform("GIBlur",horizontal.currentBuffer());pass.setUniform("GIParams",uniforms.currentBuffer());
|
||||
pass.setUniform("WorldDepth",main.getDepthTextureView(),nearest);pass.setUniform("SceneColor",source.getColorTextureView(),nearest);
|
||||
pass.setUniform("IndirectLight",indirect.getColorTextureView(),nearest);pass.draw(3,1,0,0);
|
||||
}
|
||||
applied=true;frames++;
|
||||
}
|
||||
private static void filter(com.mojang.renderpearl.api.commands.CommandEncoder encoder,TextureTarget input,TextureTarget output,MappableRingBuffer direction){
|
||||
var main=Minecraft.getInstance().gameRenderer.mainRenderTarget();var nearest=RenderSystem.getSamplerCache().getClampToEdge(FilterMode.NEAREST);
|
||||
try(var pass=encoder.createRenderPass(()->"Sanctuary SSGI bilateral filter",output.getColorTextureView(),Optional.empty())){
|
||||
pass.setPipeline(RenderSystem.getCompiledPipeline(FILTER));pass.setUniform("GIParams",uniforms.currentBuffer());pass.setUniform("GIBlur",direction.currentBuffer());
|
||||
pass.setUniform("WorldDepth",main.getDepthTextureView(),nearest);pass.setUniform("SceneColor",source.getColorTextureView(),nearest);
|
||||
pass.setUniform("IndirectLight",input.getColorTextureView(),RenderSystem.getSamplerCache().getClampToEdge(FilterMode.LINEAR));pass.draw(3,1,0,0);
|
||||
}
|
||||
}
|
||||
public static void release(){if(source!=null){source.destroyBuffers();indirect.destroyBuffers();filtered.destroyBuffers();source=indirect=filtered=null;}if(uniforms!=null){uniforms.close();horizontal.close();vertical.close();uniforms=horizontal=vertical=null;}applied=false;}
|
||||
static boolean applied(){return applied;}
|
||||
static boolean allocated(){return source!=null;}
|
||||
static int renderedFrames(){return frames;}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import java.util.List;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.OptionInstance;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
@@ -15,5 +17,41 @@ public final class ShaderOptionsScreen extends OptionsSubScreen {
|
||||
list.addBig(new OptionInstance<Integer>("sanctuary.shaders.saturation",OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders.saturation_help")),(caption,value)->Component.translatable("sanctuary.shaders.saturation_value",value),
|
||||
new OptionInstance.IntRange(0,200),VanillaLight.saturation(),VanillaLight::setSaturation));
|
||||
list.addBig(new OptionInstance<Integer>("sanctuary.shaders.precision",OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders.precision_help")),(caption,value)->Component.translatable("sanctuary.shaders.precision_value",value),
|
||||
new OptionInstance.Enum<>(List.of(8,16,32,64,128),Codec.INT),VanillaLight.shadowPixels(),VanillaLight::setShadowPixels));
|
||||
list.addBig(OptionInstance.createBoolean("sanctuary.shaders.pbr",OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders.pbr_help")),VanillaLight.pbr(),VanillaLight::setPbr));
|
||||
slider("pbr_intensity",VanillaLight.pbrIntensity(),VanillaLight::setPbrIntensity);
|
||||
list.addBig(OptionInstance.createBoolean("sanctuary.shaders.ssgi",OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders.ssgi_help")),VanillaLight.ssgi(),VanillaLight::setSsgi));
|
||||
slider("ssgi_intensity",VanillaLight.ssgiIntensity(),VanillaLight::setSsgiIntensity);
|
||||
list.addBig(OptionInstance.createBoolean("sanctuary.shaders.bloom",OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders.bloom_help")),VanillaLight.bloom(),VanillaLight::setBloom));
|
||||
slider("bloom_intensity",VanillaLight.bloomIntensity(),VanillaLight::setBloomIntensity);
|
||||
slider("bloom_radius",VanillaLight.bloomRadius(),VanillaLight::setBloomRadius);
|
||||
list.addBig(OptionInstance.createBoolean("sanctuary.shaders.ore_emission",OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders.ore_emission_help")),VanillaLight.oreEmission(),VanillaLight::setOreEmission));
|
||||
slider("ore_intensity",VanillaLight.oreIntensity(),VanillaLight::setOreIntensity);
|
||||
list.addBig(OptionInstance.createBoolean("sanctuary.shaders.edge_highlights",OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders.edge_highlights_help")),
|
||||
VanillaLight.edgeHighlights(),VanillaLight::setEdgeHighlights));
|
||||
list.addBig(new OptionInstance<Integer>("sanctuary.shaders.edge_intensity",OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders.edge_intensity_help")),(caption,value)->Component.translatable("sanctuary.shaders.edge_intensity_value",value),
|
||||
new OptionInstance.IntRange(0,100),VanillaLight.edgeIntensity(),VanillaLight::setEdgeIntensity));
|
||||
list.addBig(new OptionInstance<Integer>("sanctuary.shaders.edge_distance",OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders.edge_distance_help")),(caption,value)->Component.translatable("sanctuary.shaders.edge_distance_value",value),
|
||||
new OptionInstance.Enum<>(List.of(16,32,64,128,256),Codec.INT),VanillaLight.edgeDistance(),VanillaLight::setEdgeDistance));
|
||||
list.addBig(OptionInstance.createBoolean("sanctuary.shaders.pixel_shadows",OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders.pixel_shadows_help")),
|
||||
VanillaLight.pixelShadows(),VanillaLight::setPixelShadows));
|
||||
list.addBig(new OptionInstance<Integer>("sanctuary.shaders.shadow_distance",OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders.shadow_distance_help")),(caption,value)->Component.translatable("sanctuary.shaders.shadow_distance_value",value),
|
||||
new OptionInstance.Enum<>(List.of(16,32,64,128,256),Codec.INT),VanillaLight.shadowDistance(),VanillaLight::setShadowDistance));
|
||||
}
|
||||
private void slider(String key,int value,java.util.function.Consumer<Integer> setter){
|
||||
list.addBig(new OptionInstance<Integer>("sanctuary.shaders."+key,OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders."+key+"_help")),(caption,v)->Component.translatable("sanctuary.shaders."+key+"_value",v),
|
||||
new OptionInstance.IntRange(0,100),value,setter::accept));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Vector3f;
|
||||
|
||||
/** Keeps the raster grid continuous as the sun moves; only camera recentering snaps to texels. */
|
||||
final class ShadowProjection {
|
||||
private boolean initialized;
|
||||
private int distance, size;
|
||||
private double x, y, z, phaseX, phaseY;
|
||||
|
||||
Matrix4f matrix(Vector3f sun, int distance, int size, double x, double y, double z, boolean zeroToOne) {
|
||||
float extent = distance + 16, reach = 4 * (distance + 16);
|
||||
var view = new Matrix4f().lookAt(sun.x * reach, sun.y * reach, sun.z * reach,
|
||||
0, 0, 0, 0, 0, 1);
|
||||
double texel = 2.0 * extent / size;
|
||||
if (!initialized || this.distance != distance || this.size != size) {
|
||||
phaseX = phaseY = 0;
|
||||
} else if (this.x != x || this.y != y || this.z != z) {
|
||||
double dx = x - this.x, dy = y - this.y, dz = z - this.z;
|
||||
phaseX = Math.IEEEremainder(phaseX + view.m00() * dx + view.m10() * dy + view.m20() * dz, texel);
|
||||
phaseY = Math.IEEEremainder(phaseY + view.m01() * dx + view.m11() * dy + view.m21() * dz, texel);
|
||||
}
|
||||
// Re-rounding the projected WORLD origin every frame made a slowly rotating light jump
|
||||
// a raster texel periodically, particularly far from spawn. Retain this phase in between
|
||||
// camera anchor changes: the light can rotate continuously without moving the raster origin.
|
||||
initialized = true;
|
||||
this.distance = distance; this.size = size;
|
||||
this.x = x; this.y = y; this.z = z;
|
||||
var result = new Matrix4f().setOrtho(-extent, extent, -extent, extent, .1f, reach * 2, zeroToOne).mul(view);
|
||||
result.m30(result.m30() + (float)(phaseX / extent));
|
||||
result.m31(result.m31() + (float)(phaseY / extent));
|
||||
return result;
|
||||
}
|
||||
|
||||
void reset() { initialized = false; }
|
||||
}
|
||||
@@ -0,0 +1,528 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.blaze3d.vertex.BufferBuilder;
|
||||
import com.mojang.blaze3d.vertex.ByteBufferBuilder;
|
||||
import com.mojang.blaze3d.vertex.DefaultVertexFormat;
|
||||
import com.mojang.renderpearl.api.buffers.GpuBuffer;
|
||||
import com.mojang.renderpearl.api.pipeline.PrimitiveTopology;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.tags.BlockTags;
|
||||
import net.minecraft.core.SectionPos;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import net.minecraft.world.level.chunk.LevelChunkSection;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.joml.Vector3fc;
|
||||
|
||||
/**
|
||||
* Render-thread-only, read-only terrain casters. Call update BEFORE opening any render pass.
|
||||
* Positions use POSITION_COLOR (three floats plus a foliage flag), relative to the entry's block
|
||||
* origin; add that origin in the draw transform. Indices belong to RenderSystem's shared
|
||||
* QUADS sequential buffer, which update grows before drawing. This cache never owns it.
|
||||
*
|
||||
* <p>Call dirtySection for edits (including affected neighbors), or dirtyChunk for a chunk
|
||||
* packet that mutates an existing chunk. Identity changes/unloads are detected automatically.
|
||||
* Call close on disable/disconnect/resource teardown; it is idempotent and the cache is reusable.
|
||||
* A different ClientLevel, or update(null, ...), also releases all owned resources.
|
||||
*/
|
||||
public final class ShadowTerrain implements AutoCloseable {
|
||||
/** Bounds metadata/work as well as geometry; shader options reach 256 blocks. */
|
||||
public static final int MAX_DISTANCE = 256;
|
||||
private static final int MAX_SECTIONS_PER_UPDATE = 4;
|
||||
private static final long BUILD_NANOS = 3_000_000L;
|
||||
private static final long MAX_GPU_BYTES = 96L * 1024 * 1024;
|
||||
private static final int MAX_SECTION_QUADS = 65_536;
|
||||
private static final int MAX_SHAPES = 1024;
|
||||
private static final int MAX_BOXES = 256;
|
||||
private static final Direction[] DIRECTIONS = Direction.values();
|
||||
|
||||
/** Borrowed buffer: owned exclusively by this cache, valid until its next update/close. */
|
||||
public record Entry(int x, int y, int z, GpuBuffer vertices, int indexCount) { }
|
||||
|
||||
private final Map<Long, LevelChunk> chunks = new HashMap<>();
|
||||
private final Map<Long, Section> sections = new HashMap<>();
|
||||
private final Map<BlockState, Shape> shapes = new IdentityHashMap<>();
|
||||
private ClientLevel level;
|
||||
private Bounds bounds;
|
||||
private Build building;
|
||||
private long gpuBytes;
|
||||
private List<Entry> entries = List.of();
|
||||
private boolean entriesChanged;
|
||||
private long reconciledTick = Long.MIN_VALUE;
|
||||
private boolean metadataDirty = true;
|
||||
private int cameraSectionX, cameraSectionY, cameraSectionZ;
|
||||
|
||||
/**
|
||||
* Refreshes loaded section metadata, then meshes at most four sections within a soft 3 ms
|
||||
* budget. A large section resumes next call; a single shape lookup or GPU upload cannot be
|
||||
* preempted. distance is in blocks, 0..MAX_DISTANCE. The receiver volume plus an eight-block guard
|
||||
* is extruded by twice that distance toward the sun, rounded to sections and clipped
|
||||
* to the level's build height. This includes loaded casters outside the camera frustum.
|
||||
*
|
||||
* <p>The 96 MiB vertex budget favors nearer sections. Evicted/deferred meshes are retried
|
||||
* when room becomes available or the camera enters another section. Pathological geometry
|
||||
* exceeding 256 boxes per state or 65,536 quads per section is omitted, never approximated
|
||||
* by a full cube or uploaded partially. It can be retried after invalidation.
|
||||
*/
|
||||
public List<Entry> update(ClientLevel level, Vec3 camera, int distance, Vector3fc sun) {
|
||||
RenderSystem.assertOnRenderThread();
|
||||
if (level == null) {
|
||||
close();
|
||||
return entries;
|
||||
}
|
||||
Objects.requireNonNull(camera, "camera");
|
||||
if (distance < 0 || distance > MAX_DISTANCE || !Double.isFinite(camera.x)
|
||||
|| !Double.isFinite(camera.y) || !Double.isFinite(camera.z)) {
|
||||
throw new IllegalArgumentException("Finite camera and shadow distance 0.." + MAX_DISTANCE + " required");
|
||||
}
|
||||
if (this.level != level) {
|
||||
close();
|
||||
this.level = level;
|
||||
}
|
||||
int radius = distance + 8;
|
||||
double dx = sun.x() * distance * 2, dy = sun.y() * distance * 2, dz = sun.z() * distance * 2;
|
||||
Bounds next = new Bounds(section(camera.x - radius + Math.min(0, dx)), section(camera.x + radius + Math.max(0, dx)),
|
||||
Math.max(level.getMinSectionY(), section(camera.y - radius + Math.min(0, dy))),
|
||||
Math.min(level.getMaxSectionY() - 1, section(camera.y + radius + Math.max(0, dy))),
|
||||
section(camera.z - radius + Math.min(0, dz)), section(camera.z + radius + Math.max(0, dz)));
|
||||
int cx = section(camera.x), cy = section(camera.y), cz = section(camera.z);
|
||||
boolean moved = !next.equals(bounds) || cx != cameraSectionX || cy != cameraSectionY || cz != cameraSectionZ;
|
||||
if (bounds != null && !next.equals(bounds)) {
|
||||
// Changing the caster boundary changes which neighboring faces may be culled.
|
||||
for (Section section : sections.values()) {
|
||||
if (bounds.onBoundary(section) || next.onBoundary(section)) dirty(section);
|
||||
}
|
||||
}
|
||||
bounds = next;
|
||||
cameraSectionX = cx;
|
||||
cameraSectionY = cy;
|
||||
cameraSectionZ = cz;
|
||||
// Large ranges must not rescan loaded chunk metadata at the display's frame rate.
|
||||
if (moved || metadataDirty || reconciledTick != level.getGameTime()) {
|
||||
reconcile();
|
||||
reconciledTick = level.getGameTime();
|
||||
metadataDirty = false;
|
||||
}
|
||||
if (moved) {
|
||||
retryDeferred();
|
||||
}
|
||||
|
||||
long deadline = System.nanoTime() + BUILD_NANOS;
|
||||
int started = building == null ? 0 : 1;
|
||||
try {
|
||||
while (System.nanoTime() < deadline) {
|
||||
if (building == null) {
|
||||
if (started == MAX_SECTIONS_PER_UPDATE) break;
|
||||
Section nearest = nearestDirty(camera);
|
||||
if (nearest == null) break;
|
||||
building = new Build(nearest);
|
||||
started++;
|
||||
}
|
||||
if (!advance(building, deadline)) break;
|
||||
Build finished = building;
|
||||
building = null;
|
||||
try (finished) {
|
||||
upload(finished, camera);
|
||||
}
|
||||
}
|
||||
} catch (RuntimeException | Error failure) {
|
||||
cancelBuild();
|
||||
throw failure;
|
||||
}
|
||||
return entries();
|
||||
}
|
||||
|
||||
/** Immutable snapshot; consume after update and before another update or close. */
|
||||
public List<Entry> entries() {
|
||||
RenderSystem.assertOnRenderThread();
|
||||
if (entriesChanged) {
|
||||
var result = new ArrayList<Entry>();
|
||||
for (Section section : sections.values()) {
|
||||
if (section.entry != null) result.add(section.entry);
|
||||
}
|
||||
entries = List.copyOf(result);
|
||||
entriesChanged = false;
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
/** Outstanding rebuilds, including the in-progress section; excludes memory-deferred work. */
|
||||
public int pendingSections() {
|
||||
RenderSystem.assertOnRenderThread();
|
||||
int pending = 0;
|
||||
for (Section section : sections.values()) {
|
||||
if (section.dirty && !section.deferred) pending++;
|
||||
}
|
||||
return pending;
|
||||
}
|
||||
|
||||
/** No runnable mesh work after the last update; may still omit memory/complexity-limited sections. */
|
||||
public boolean settled() {
|
||||
return pendingSections() == 0;
|
||||
}
|
||||
|
||||
/** Coordinates are section coordinates, not block origins. Caller also invalidates neighbors. */
|
||||
public void dirtySection(int sx, int sy, int sz) {
|
||||
RenderSystem.assertOnRenderThread();
|
||||
Section section = sections.get(SectionPos.asLong(sx, sy, sz));
|
||||
if (section != null) dirty(section);
|
||||
else metadataDirty = true;
|
||||
}
|
||||
|
||||
/** Coordinates are chunk coordinates. Caller also invalidates adjacent chunks as needed. */
|
||||
public void dirtyChunk(int cx, int cz) {
|
||||
RenderSystem.assertOnRenderThread();
|
||||
metadataDirty = true;
|
||||
for (Section section : sections.values()) {
|
||||
if (section.x == cx && section.z == cz) dirty(section);
|
||||
}
|
||||
}
|
||||
|
||||
private void dirty(Section section) {
|
||||
section.dirty = true;
|
||||
section.deferred = false;
|
||||
if (building != null && building.section == section) cancelBuild();
|
||||
}
|
||||
|
||||
/** Poll only chunk identities and section emptiness; never scan unchanged block storage. */
|
||||
private void reconcile() {
|
||||
var loaded = new HashMap<Long, LevelChunk>();
|
||||
if (bounds.minY <= bounds.maxY) {
|
||||
for (int x = bounds.minX; x <= bounds.maxX; x++) {
|
||||
for (int z = bounds.minZ; z <= bounds.maxZ; z++) {
|
||||
LevelChunk chunk = level.getChunkSource().getChunk(x, z, ChunkStatus.FULL, false);
|
||||
long key = ChunkPos.pack(x, z);
|
||||
if (chunk != null) loaded.put(key, chunk);
|
||||
if (chunks.get(key) != chunk) dirtyChunkAndNeighbors(x, z);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (long key : chunks.keySet()) {
|
||||
if (!loaded.containsKey(key)) dirtyChunkAndNeighbors(ChunkPos.getX(key), ChunkPos.getZ(key));
|
||||
}
|
||||
chunks.clear();
|
||||
chunks.putAll(loaded);
|
||||
|
||||
boolean freed = false;
|
||||
for (Iterator<Section> iterator = sections.values().iterator(); iterator.hasNext();) {
|
||||
Section section = iterator.next();
|
||||
LevelChunk chunk = chunks.get(ChunkPos.pack(section.x, section.z));
|
||||
if (!bounds.contains(section.x, section.y, section.z) || chunk != section.chunk
|
||||
|| chunk.getSection(level.getSectionIndexFromSectionY(section.y)) != section.storage
|
||||
|| section.storage.hasOnlyAir()) {
|
||||
if (building != null && building.section == section) cancelBuild();
|
||||
freed |= section.entry != null;
|
||||
release(section);
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
for (LevelChunk chunk : chunks.values()) {
|
||||
int x = chunk.getPos().x(), z = chunk.getPos().z();
|
||||
for (int y = bounds.minY; y <= bounds.maxY; y++) {
|
||||
LevelChunkSection storage = chunk.getSection(level.getSectionIndexFromSectionY(y));
|
||||
long key = SectionPos.asLong(x, y, z);
|
||||
if (!storage.hasOnlyAir() && !sections.containsKey(key)) {
|
||||
sections.put(key, new Section(x, y, z, chunk, storage));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (freed) retryDeferred();
|
||||
}
|
||||
|
||||
private void dirtyChunkAndNeighbors(int x, int z) {
|
||||
dirtyChunk(x, z);
|
||||
dirtyChunk(x - 1, z);
|
||||
dirtyChunk(x + 1, z);
|
||||
dirtyChunk(x, z - 1);
|
||||
dirtyChunk(x, z + 1);
|
||||
}
|
||||
|
||||
private void retryDeferred() {
|
||||
for (Section section : sections.values()) {
|
||||
if (section.deferred) {
|
||||
section.deferred = false;
|
||||
section.dirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Section nearestDirty(Vec3 camera) {
|
||||
Section nearest = null;
|
||||
double best = Double.POSITIVE_INFINITY;
|
||||
for (Section section : sections.values()) {
|
||||
if (section.dirty && !section.deferred) {
|
||||
double distance = section.distanceSquared(camera);
|
||||
if (distance < best) {
|
||||
best = distance;
|
||||
nearest = section;
|
||||
}
|
||||
}
|
||||
}
|
||||
return nearest;
|
||||
}
|
||||
|
||||
private boolean advance(Build build, long deadline) {
|
||||
while (build.cursor < 4096) {
|
||||
int index = build.cursor++;
|
||||
int x = index & 15, z = (index >> 4) & 15, y = index >> 8;
|
||||
var state = build.section.storage.getBlockState(x, y, z);
|
||||
boolean foliage = state.is(BlockTags.LEAVES);
|
||||
Shape shape = foliage ? LEAVES : shape(state);
|
||||
if (shape != null) {
|
||||
if (shape.boxes == null) {
|
||||
build.oversized = true;
|
||||
return true;
|
||||
}
|
||||
int hidden = 0;
|
||||
for (Direction direction : DIRECTIONS) {
|
||||
if (foliage) break; // Retain internal leaf faces: each layer contributes transmission.
|
||||
BlockState neighbor = neighbor(build.section, x + direction.getStepX(),
|
||||
y + direction.getStepY(), z + direction.getStepZ());
|
||||
Shape other = neighbor == null ? null : shape(neighbor);
|
||||
if (other != null && (other.fullFaces & (1 << direction.getOpposite().ordinal())) != 0) {
|
||||
hidden |= 1 << direction.ordinal();
|
||||
}
|
||||
}
|
||||
for (AABB box : shape.boxes) {
|
||||
for (Direction direction : DIRECTIONS) {
|
||||
if ((hidden & (1 << direction.ordinal())) != 0 && atBoundary(box, direction)) continue;
|
||||
if (build.quads == MAX_SECTION_QUADS) {
|
||||
build.oversized = true;
|
||||
return true;
|
||||
}
|
||||
face(build.vertices, box, x, y, z, direction, foliage ? 0xFFFFFFFF : 0xFF000000);
|
||||
build.quads++;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ((build.cursor & 31) == 0 && System.nanoTime() >= deadline) return build.cursor == 4096;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private BlockState neighbor(Section section, int x, int y, int z) {
|
||||
if ((x | y | z) >= 0 && x < 16 && y < 16 && z < 16) {
|
||||
return section.storage.getBlockState(x, y, z);
|
||||
}
|
||||
int sx = section.x + (x >> 4), sy = section.y + (y >> 4), sz = section.z + (z >> 4);
|
||||
// Keep the exterior faces of the caster volume; no dependency on chunks outside it.
|
||||
if (!bounds.contains(sx, sy, sz)) return null;
|
||||
LevelChunk chunk = chunks.get(ChunkPos.pack(sx, sz));
|
||||
if (chunk == null) return null;
|
||||
return chunk.getSection(level.getSectionIndexFromSectionY(sy)).getBlockState(x & 15, y & 15, z & 15);
|
||||
}
|
||||
|
||||
private Shape shape(BlockState state) {
|
||||
// Retain the solid part of waterlogged slabs/stairs, but never mesh the fluid itself.
|
||||
if (state.isAir() || state.is(BlockTags.LEAVES) || !state.canOcclude() || state.liquid()) return null;
|
||||
Shape cached = shapes.get(state);
|
||||
if (cached != null) return cached;
|
||||
// In 26.3 occlusion shapes are cached on BlockState and take no level/position argument.
|
||||
var voxel = state.getOcclusionShape();
|
||||
var boxes = new ArrayList<AABB>();
|
||||
voxel.forAllBoxes((x0, y0, z0, x1, y1, z1) -> {
|
||||
if (boxes.size() <= MAX_BOXES) boxes.add(new AABB(x0, y0, z0, x1, y1, z1));
|
||||
});
|
||||
int fullFaces = 0;
|
||||
for (Direction direction : DIRECTIONS) {
|
||||
if (Block.isFaceFull(voxel, direction)) fullFaces |= 1 << direction.ordinal();
|
||||
}
|
||||
// An omitted shape must not hide the faces of the surrounding geometry.
|
||||
Shape result = boxes.size() > MAX_BOXES ? new Shape(null, 0) : new Shape(boxes.toArray(AABB[]::new), fullFaces);
|
||||
if (shapes.size() == MAX_SHAPES) shapes.clear();
|
||||
shapes.put(state, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private void upload(Build build, Vec3 camera) {
|
||||
Section section = build.section;
|
||||
if (build.oversized || build.quads == 0) {
|
||||
release(section);
|
||||
section.dirty = false;
|
||||
retryDeferred();
|
||||
return;
|
||||
}
|
||||
try (var mesh = build.vertices.buildOrThrow()) {
|
||||
long bytes = mesh.vertexBuffer().remaining();
|
||||
long previous = section.entry == null ? 0 : section.entry.vertices.size();
|
||||
// Release the old buffer before replacement, so even transient owned GPU memory is bounded.
|
||||
release(section);
|
||||
while (gpuBytes + bytes > MAX_GPU_BYTES) {
|
||||
Section farthest = null;
|
||||
double far = section.distanceSquared(camera);
|
||||
for (Section candidate : sections.values()) {
|
||||
double distance = candidate.distanceSquared(camera);
|
||||
if (candidate.entry != null && distance > far) {
|
||||
farthest = candidate;
|
||||
far = distance;
|
||||
}
|
||||
}
|
||||
if (farthest == null) {
|
||||
section.deferred = true;
|
||||
return;
|
||||
}
|
||||
release(farthest);
|
||||
farthest.dirty = true;
|
||||
farthest.deferred = true;
|
||||
}
|
||||
int indexCount = mesh.drawState().indexCount();
|
||||
RenderSystem.getSequentialBuffer(PrimitiveTopology.QUADS).getBuffer(indexCount);
|
||||
GpuBuffer buffer = RenderSystem.getDevice().createBuffer(() -> "Sanctuary shadow terrain "
|
||||
+ section.x + "," + section.y + "," + section.z,
|
||||
GpuBuffer.USAGE_VERTEX | GpuBuffer.USAGE_COPY_DST, mesh.vertexBuffer());
|
||||
section.entry = new Entry(section.x << 4, section.y << 4, section.z << 4, buffer, indexCount);
|
||||
gpuBytes += buffer.size();
|
||||
section.dirty = false;
|
||||
entriesChanged = true;
|
||||
if (bytes < previous) retryDeferred();
|
||||
}
|
||||
}
|
||||
|
||||
private void release(Section section) {
|
||||
if (section.entry != null) {
|
||||
GpuBuffer buffer = section.entry.vertices;
|
||||
gpuBytes -= buffer.size();
|
||||
buffer.close();
|
||||
section.entry = null;
|
||||
entriesChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void cancelBuild() {
|
||||
if (building != null) {
|
||||
building.close();
|
||||
building = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
RenderSystem.assertOnRenderThread();
|
||||
cancelBuild();
|
||||
for (Section section : sections.values()) release(section);
|
||||
sections.clear();
|
||||
chunks.clear();
|
||||
reconciledTick = Long.MIN_VALUE;
|
||||
metadataDirty = true;
|
||||
shapes.clear();
|
||||
entries = List.of();
|
||||
entriesChanged = false;
|
||||
gpuBytes = 0;
|
||||
bounds = null;
|
||||
level = null;
|
||||
}
|
||||
|
||||
private static int section(double coordinate) {
|
||||
return (int) Math.floor(coordinate / 16.0);
|
||||
}
|
||||
|
||||
private static boolean atBoundary(AABB box, Direction direction) {
|
||||
return switch (direction) {
|
||||
case DOWN -> box.minY == 0;
|
||||
case UP -> box.maxY == 1;
|
||||
case NORTH -> box.minZ == 0;
|
||||
case SOUTH -> box.maxZ == 1;
|
||||
case WEST -> box.minX == 0;
|
||||
case EAST -> box.maxX == 1;
|
||||
};
|
||||
}
|
||||
|
||||
/** Counterclockwise exterior faces, matching the native sequential QUADS index order. */
|
||||
private static void face(BufferBuilder out, AABB box, int x, int y, int z, Direction direction, int color) {
|
||||
float x0 = x + (float) box.minX, x1 = x + (float) box.maxX;
|
||||
float y0 = y + (float) box.minY, y1 = y + (float) box.maxY;
|
||||
float z0 = z + (float) box.minZ, z1 = z + (float) box.maxZ;
|
||||
switch (direction) {
|
||||
case DOWN -> {
|
||||
out.addVertex(x0, y0, z0).setColor(color); out.addVertex(x1, y0, z0).setColor(color);
|
||||
out.addVertex(x1, y0, z1).setColor(color); out.addVertex(x0, y0, z1).setColor(color);
|
||||
}
|
||||
case UP -> {
|
||||
out.addVertex(x0, y1, z0).setColor(color); out.addVertex(x0, y1, z1).setColor(color);
|
||||
out.addVertex(x1, y1, z1).setColor(color); out.addVertex(x1, y1, z0).setColor(color);
|
||||
}
|
||||
case NORTH -> {
|
||||
out.addVertex(x0, y0, z0).setColor(color); out.addVertex(x0, y1, z0).setColor(color);
|
||||
out.addVertex(x1, y1, z0).setColor(color); out.addVertex(x1, y0, z0).setColor(color);
|
||||
}
|
||||
case SOUTH -> {
|
||||
out.addVertex(x0, y0, z1).setColor(color); out.addVertex(x1, y0, z1).setColor(color);
|
||||
out.addVertex(x1, y1, z1).setColor(color); out.addVertex(x0, y1, z1).setColor(color);
|
||||
}
|
||||
case WEST -> {
|
||||
out.addVertex(x0, y0, z0).setColor(color); out.addVertex(x0, y0, z1).setColor(color);
|
||||
out.addVertex(x0, y1, z1).setColor(color); out.addVertex(x0, y1, z0).setColor(color);
|
||||
}
|
||||
case EAST -> {
|
||||
out.addVertex(x1, y0, z0).setColor(color); out.addVertex(x1, y1, z0).setColor(color);
|
||||
out.addVertex(x1, y1, z1).setColor(color); out.addVertex(x1, y0, z1).setColor(color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record Bounds(int minX, int maxX, int minY, int maxY, int minZ, int maxZ) {
|
||||
boolean contains(int x, int y, int z) {
|
||||
return x >= minX && x <= maxX && y >= minY && y <= maxY && z >= minZ && z <= maxZ;
|
||||
}
|
||||
|
||||
boolean onBoundary(Section section) {
|
||||
return section.x == minX || section.x == maxX || section.y == minY || section.y == maxY
|
||||
|| section.z == minZ || section.z == maxZ;
|
||||
}
|
||||
}
|
||||
|
||||
private record Shape(AABB[] boxes, int fullFaces) { }
|
||||
private static final Shape LEAVES = new Shape(new AABB[]{new AABB(0, 0, 0, 1, 1, 1)}, 0);
|
||||
|
||||
private static final class Section {
|
||||
final int x, y, z;
|
||||
final LevelChunk chunk;
|
||||
final LevelChunkSection storage;
|
||||
Entry entry;
|
||||
boolean dirty = true;
|
||||
boolean deferred;
|
||||
|
||||
Section(int x, int y, int z, LevelChunk chunk, LevelChunkSection storage) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
this.chunk = chunk;
|
||||
this.storage = storage;
|
||||
}
|
||||
|
||||
double distanceSquared(Vec3 camera) {
|
||||
double dx = x * 16.0 + 8 - camera.x, dy = y * 16.0 + 8 - camera.y, dz = z * 16.0 + 8 - camera.z;
|
||||
return dx * dx + dy * dy + dz * dz;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Build implements AutoCloseable {
|
||||
final Section section;
|
||||
final ByteBufferBuilder bytes = new ByteBufferBuilder(64 * 1024);
|
||||
final BufferBuilder vertices = new BufferBuilder(bytes, PrimitiveTopology.QUADS, DefaultVertexFormat.POSITION_COLOR);
|
||||
int cursor, quads;
|
||||
boolean oversized;
|
||||
|
||||
Build(Section section) {
|
||||
this.section = section;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
bytes.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,25 +14,95 @@ public final class VanillaLight {
|
||||
||FabricLoader.getInstance().isModLoaded("oculus")||FabricLoader.getInstance().isModLoaded("canvas");
|
||||
private static Preferences preferences;
|
||||
private VanillaLight() { }
|
||||
private static final class Preferences { boolean enabled=true;int saturation=142; }
|
||||
private static final class Preferences {
|
||||
boolean enabled=true;
|
||||
int saturation=142;
|
||||
boolean pixelShadows=true;
|
||||
boolean bloom=true;
|
||||
boolean pbr=true;
|
||||
int pbrIntensity=50;
|
||||
boolean ssgi=true;
|
||||
int ssgiIntensity=35;
|
||||
int bloomIntensity=20;
|
||||
int bloomRadius=50;
|
||||
boolean oreEmission=true;
|
||||
int oreIntensity=50;
|
||||
boolean edgeHighlights=true;
|
||||
int edgeIntensity=30;
|
||||
int edgeDistance=32;
|
||||
int shadowPixels=16;
|
||||
int shadowDistance=128;
|
||||
}
|
||||
private static Path path(){return FabricLoader.getInstance().getConfigDir().resolve("sanctuary-shaders.json");}
|
||||
public static boolean enabled(){
|
||||
if(preferences==null){
|
||||
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.pbrIntensity=Math.clamp(preferences.pbrIntensity,0,100);
|
||||
preferences.ssgiIntensity=Math.clamp(preferences.ssgiIntensity,0,100);
|
||||
preferences.bloomIntensity=Math.clamp(preferences.bloomIntensity,0,100);
|
||||
preferences.bloomRadius=Math.clamp(preferences.bloomRadius,0,100);
|
||||
preferences.oreIntensity=Math.clamp(preferences.oreIntensity,0,100);
|
||||
preferences.edgeIntensity=Math.clamp(preferences.edgeIntensity,0,100);
|
||||
preferences.edgeDistance=normalizeShadowDistance(preferences.edgeDistance);
|
||||
preferences.saturation=Math.clamp(preferences.saturation,0,200);
|
||||
preferences.shadowPixels=normalizePrecision(preferences.shadowPixels);
|
||||
preferences.shadowDistance=normalizeShadowDistance(preferences.shadowDistance);
|
||||
}
|
||||
return preferences.enabled;
|
||||
}
|
||||
public static boolean externalEngine(){return EXTERNAL_ENGINE;}
|
||||
public static boolean active(){return ProgressionClient.active()&&!EXTERNAL_ENGINE&&enabled();}
|
||||
public static int saturation(){enabled();return preferences.saturation;}
|
||||
public static boolean edgeHighlights(){enabled();return preferences.edgeHighlights;}
|
||||
public static int edgeDistance(){enabled();return preferences.edgeDistance;}
|
||||
public static void setEdgeDistance(int value){enabled();preferences.edgeDistance=normalizeShadowDistance(value);save();}
|
||||
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 pbr(){enabled();return preferences.pbr;}
|
||||
public static int pbrIntensity(){enabled();return preferences.pbrIntensity;}
|
||||
public static void setPbr(boolean value){enabled();preferences.pbr=value;save();}
|
||||
public static void setPbrIntensity(int value){enabled();preferences.pbrIntensity=Math.clamp(value,0,100);save();}
|
||||
public static boolean ssgi(){enabled();return preferences.ssgi;}
|
||||
public static int ssgiIntensity(){enabled();return preferences.ssgiIntensity;}
|
||||
public static void setSsgi(boolean value){enabled();preferences.ssgi=value;save();}
|
||||
public static void setSsgiIntensity(int value){enabled();preferences.ssgiIntensity=Math.clamp(value,0,100);save();}
|
||||
public static boolean bloom(){enabled();return preferences.bloom;}
|
||||
public static int bloomIntensity(){enabled();return preferences.bloomIntensity;}
|
||||
public static int bloomRadius(){enabled();return preferences.bloomRadius;}
|
||||
public static boolean oreEmission(){enabled();return preferences.oreEmission;}
|
||||
public static int oreIntensity(){enabled();return preferences.oreIntensity;}
|
||||
public static void setBloom(boolean value){enabled();preferences.bloom=value;save();}
|
||||
public static void setBloomIntensity(int value){enabled();preferences.bloomIntensity=Math.clamp(value,0,100);save();}
|
||||
public static void setBloomRadius(int value){enabled();preferences.bloomRadius=Math.clamp(value,0,100);save();}
|
||||
public static void setOreEmission(boolean value){enabled();preferences.oreEmission=value;save();}
|
||||
public static void setOreIntensity(int value){enabled();preferences.oreIntensity=Math.clamp(value,0,100);save();}
|
||||
public static boolean pixelShadows(){enabled();return preferences.pixelShadows;}
|
||||
public static int shadowPixels(){enabled();return preferences.shadowPixels;}
|
||||
public static int shadowDistance(){enabled();return preferences.shadowDistance;}
|
||||
public static void setSaturation(int value){enabled();preferences.saturation=Math.clamp(value,0,200);save();}
|
||||
public static void setPixelShadows(boolean value){enabled();preferences.pixelShadows=value;save();}
|
||||
public static void setShadowPixels(int value){enabled();preferences.shadowPixels=normalizePrecision(value);save();}
|
||||
public static void setShadowDistance(int value){enabled();preferences.shadowDistance=normalizeShadowDistance(value);save();}
|
||||
public static void setEnabled(boolean value){
|
||||
enabled();preferences.enabled=value;
|
||||
save();
|
||||
}
|
||||
/** Nearest supported value, clamped at either end; midpoint ties round up. */
|
||||
private static int normalizePrecision(int value){
|
||||
if(value<12)return 8;
|
||||
if(value<24)return 16;
|
||||
if(value<48)return 32;
|
||||
return value<96?64:128;
|
||||
}
|
||||
private static int normalizeShadowDistance(int value){
|
||||
if(value<24)return 16;
|
||||
if(value<48)return 32;
|
||||
if(value<96)return 64;
|
||||
return value<192?128:256;
|
||||
}
|
||||
private static void save(){
|
||||
try{
|
||||
Files.createDirectories(path().getParent());var temporary=path().resolveSibling(path().getFileName()+".tmp");
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package fr.koka.sanctuary.mixin.client;
|
||||
|
||||
import fr.koka.sanctuary.client.shader.PixelShadows;
|
||||
import fr.koka.sanctuary.client.shader.ProceduralPbr;
|
||||
import fr.koka.sanctuary.client.shader.Bloom;
|
||||
import fr.koka.sanctuary.client.shader.ScreenSpaceGI;
|
||||
import net.minecraft.client.renderer.extract.LevelExtractor;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
/** Geometry changes only: lighting-only section invalidations do not rebuild shadow casters. */
|
||||
@Mixin(LevelExtractor.class)
|
||||
abstract class PixelShadowDirtyMixin {
|
||||
@Inject(method="blockChanged", at=@At("HEAD"))
|
||||
private void sanctuary$dirty(BlockPos pos, int flags, CallbackInfo ci) {
|
||||
PixelShadows.dirtyBlock(pos); ProceduralPbr.dirtyBlock(pos); Bloom.dirtyBlock(pos);
|
||||
}
|
||||
@Inject(method="allChanged", at=@At("HEAD"))
|
||||
private void sanctuary$reload(CallbackInfo ci) { PixelShadows.release(); ProceduralPbr.release(); Bloom.release(); ScreenSpaceGI.release(); }
|
||||
@Inject(method="setSectionDirty", at=@At("HEAD"))
|
||||
private void sanctuary$pbrLight(int x,int y,int z,CallbackInfo ci){ProceduralPbr.dirtySection(x,y,z);}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package fr.koka.sanctuary.mixin.client;
|
||||
|
||||
import fr.koka.sanctuary.client.shader.PixelShadows;
|
||||
import fr.koka.sanctuary.client.shader.ProceduralPbr;
|
||||
import fr.koka.sanctuary.client.shader.Bloom;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
@Mixin(ClientLevel.class)
|
||||
abstract class PixelShadowLevelMixin {
|
||||
@Inject(method="setBlocksDirty", at=@At("HEAD"))
|
||||
private void sanctuary$block(BlockPos pos, BlockState before, BlockState after, CallbackInfo ci) {
|
||||
if (before != after) {PixelShadows.dirtyBlock(pos); ProceduralPbr.dirtyBlock(pos);Bloom.dirtyBlock(pos);}
|
||||
}
|
||||
|
||||
@Inject(method="onChunkLoaded", at=@At("TAIL"))
|
||||
private void sanctuary$loaded(ChunkPos pos, CallbackInfo ci) { PixelShadows.dirtyChunk(pos.x(), pos.z()); ProceduralPbr.dirtyChunk(pos.x(), pos.z()); Bloom.dirtyChunk(pos.x(), pos.z()); }
|
||||
@Inject(method="unload", at=@At("HEAD"))
|
||||
private void sanctuary$unload(LevelChunk chunk, CallbackInfo ci) { PixelShadows.dirtyChunk(chunk.getPos().x(), chunk.getPos().z()); ProceduralPbr.dirtyChunk(chunk.getPos().x(), chunk.getPos().z()); Bloom.dirtyChunk(chunk.getPos().x(), chunk.getPos().z()); }
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package fr.koka.sanctuary.mixin.client;
|
||||
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
|
||||
import com.mojang.renderpearl.api.buffers.GpuBufferSlice;
|
||||
import fr.koka.sanctuary.client.shader.PixelShadows;
|
||||
import fr.koka.sanctuary.client.shader.ProceduralPbr;
|
||||
import fr.koka.sanctuary.client.shader.EdgeHighlights;
|
||||
import fr.koka.sanctuary.client.shader.Bloom;
|
||||
import fr.koka.sanctuary.client.shader.ScreenSpaceGI;
|
||||
import net.minecraft.client.renderer.GameRenderer;
|
||||
import net.minecraft.client.renderer.ProjectionMatrixBuffer;
|
||||
import org.joml.Matrix4f;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
@Mixin(GameRenderer.class)
|
||||
abstract class PixelShadowRendererMixin {
|
||||
@WrapOperation(method="renderLevel", at=@At(value="INVOKE", target="Lnet/minecraft/client/renderer/ProjectionMatrixBuffer;getBuffer(Lorg/joml/Matrix4f;)Lcom/mojang/renderpearl/api/buffers/GpuBufferSlice;"))
|
||||
private GpuBufferSlice sanctuary$projection(ProjectionMatrixBuffer buffer, Matrix4f matrix, Operation<GpuBufferSlice> original) {
|
||||
PixelShadows.captureProjection(matrix);
|
||||
ProceduralPbr.captureProjection(matrix);
|
||||
EdgeHighlights.captureProjection(matrix);
|
||||
Bloom.captureProjection(matrix);
|
||||
ScreenSpaceGI.captureProjection(matrix);
|
||||
return original.call(buffer, matrix);
|
||||
}
|
||||
@Inject(method="renderLevel", at=@At(value="INVOKE", target="Lnet/minecraft/client/renderer/GameRenderer;render3dHud(Lnet/minecraft/client/renderer/state/level/CameraRenderState;Lnet/minecraft/client/renderer/state/level/PlayerRenderState;Lnet/minecraft/client/renderer/state/OptionsRenderState;Z)V"))
|
||||
private void sanctuary$shadows(CallbackInfo ci) { PixelShadows.render(); ProceduralPbr.render(); ScreenSpaceGI.render(); EdgeHighlights.render(); Bloom.render(); }
|
||||
@Inject(method={"close", "setLevel"}, at=@At("HEAD"))
|
||||
private void sanctuary$close(CallbackInfo ci) { PixelShadows.release(); ProceduralPbr.release(); EdgeHighlights.release(); Bloom.release(); ScreenSpaceGI.release(); }
|
||||
}
|
||||
@@ -1285,7 +1285,7 @@
|
||||
"sanctuary.shaders.saturation": "Saturation",
|
||||
"sanctuary.shaders.saturation_value": "Saturation: %s%%",
|
||||
"sanctuary.shaders.saturation_help": "0%: grayscale. 100%: neutral. 142%: default setting. 200%: more vivid colours. Requires Vanilla Light; interface colours stay unchanged.",
|
||||
"sanctuary.shaders.help": "Subtle colour grading and progressive fog from your viewpoint to the visible limit. No extra shadows or reflections.",
|
||||
"sanctuary.shaders.help": "Subtle colour grading and progressive fog from your viewpoint to the visible limit. Optional pixel sun shadows, with no extra reflections.",
|
||||
"sanctuary.shaders.external": "Sanctuary Vanilla Light is suspended while another shader engine is installed. Your preference is kept.",
|
||||
"sanctuary.cosmetic.bed_other": "Decorative bed: you cannot sleep in a worn bed.",
|
||||
"block.sanctuary.slime_springboard": "Temporary slime",
|
||||
@@ -2256,5 +2256,47 @@
|
||||
"block.sanctuary.sapphire_ore": "Sapphire Ore",
|
||||
"block.sanctuary.deepslate_sapphire_ore": "Deepslate Sapphire Ore",
|
||||
"sanctuary.discovery.granted": "Discoveries added for %s: %s blocks, %s items, %s species. Linked recipes updated; reopen catalogues.",
|
||||
"sanctuary.discovery.invalid": "Choose a discoverable block, item or species."
|
||||
"sanctuary.discovery.invalid": "Choose a discoverable block, item or species.",
|
||||
"sanctuary.shaders.edge_highlights": "Edge highlights",
|
||||
"sanctuary.shaders.edge_highlights_help": "Connected highlights along terrain contours. Flat surfaces connect, even across different blocks. Preserves textures. ON by default; requires Vanilla Light.",
|
||||
"sanctuary.shaders.edge_intensity": "Edge intensity",
|
||||
"sanctuary.shaders.edge_intensity_help": "Highlight strength along edges. Independent of cast shadows.",
|
||||
"sanctuary.shaders.edge_intensity_value": "Edge intensity: %s %%",
|
||||
"sanctuary.shaders.pixel_shadows": "Pixel shadows",
|
||||
"sanctuary.shaders.pixel_shadows_help": "Sun shadows and subtle moon shadows on a pixel grid. Dappled foliage and stable contours. Enabled by default at 128 blocks; requires Vanilla Light.",
|
||||
"sanctuary.shaders.shadow_pixels": "Shadow precision",
|
||||
"sanctuary.shaders.shadow_pixels_value": "%s pixels/block",
|
||||
"sanctuary.shaders.shadow_pixels_help": "Grid precision: 8, 16, 32, 64 or 128 pixels per block. Higher values make shadow pixels finer. Requires Vanilla Light and pixel shadows.",
|
||||
"sanctuary.shaders.shadow_distance": "Shadow distance",
|
||||
"sanctuary.shaders.shadow_distance_value": "%s blocks",
|
||||
"sanctuary.shaders.shadow_distance_help": "Shadow range: 16, 32, 64, 128 or 256 blocks, within loaded terrain. Higher distances cost more performance. Requires Vanilla Light and pixel shadows.",
|
||||
"sanctuary.shaders.edge_distance": "Highlight distance",
|
||||
"sanctuary.shaders.edge_distance_value": "%s blocks",
|
||||
"sanctuary.shaders.edge_distance_help": "Independent range: 16, 32, 64, 128 or 256 blocks. Limited to visible terrain and render distance. Distant contours become naturally finer.",
|
||||
"sanctuary.shaders.precision": "Precision",
|
||||
"sanctuary.shaders.precision_help": "Shared grid for shadows and edge highlights: 8, 16, 32, 64 or 128 pixels per block. The shadow map retains its resolution cap.",
|
||||
"sanctuary.shaders.precision_value": "%s pixels/block",
|
||||
"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, 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.",
|
||||
"sanctuary.shaders.bloom_radius_value": "Spread: %s%%",
|
||||
"sanctuary.shaders.ore_emission": "Emissive ores",
|
||||
"sanctuary.shaders.ore_emission_help": "Makes ore inclusions glow while preserving their host rock, even with bloom off or at zero. ON by default. Visual effect only.",
|
||||
"sanctuary.shaders.ore_intensity": "Ore emission",
|
||||
"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. 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. 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%%"
|
||||
}
|
||||
|
||||
@@ -1285,7 +1285,7 @@
|
||||
"sanctuary.shaders.saturation": "Saturation",
|
||||
"sanctuary.shaders.saturation_value": "Saturation : %s %%",
|
||||
"sanctuary.shaders.saturation_help": "0 % : noir et blanc. 100 % : neutre. 142 % : réglage par défaut. 200 % : couleurs plus vives. Avec Vanilla Light activé ; l’interface garde ses couleurs.",
|
||||
"sanctuary.shaders.help": "Couleurs légèrement retravaillées et brume progressive depuis ton point de vue jusqu’à la limite visible. Sans ombres ni reflets supplémentaires.",
|
||||
"sanctuary.shaders.help": "Couleurs légèrement retravaillées et brume progressive depuis ton point de vue jusqu’à la limite visible. Ombres pixelisées du soleil en option, sans reflets supplémentaires.",
|
||||
"sanctuary.shaders.external": "Sanctuary Vanilla Light est suspendu lorsqu’un autre moteur de shaders est installé. Votre préférence est conservée.",
|
||||
"sanctuary.cosmetic.bed_other": "Lit décoratif : impossible de dormir dans un lit porté.",
|
||||
"block.sanctuary.slime_springboard": "Slime éphémère",
|
||||
@@ -2256,5 +2256,47 @@
|
||||
"block.sanctuary.sapphire_ore": "Minerai de saphir",
|
||||
"block.sanctuary.deepslate_sapphire_ore": "Minerai de saphir des abîmes",
|
||||
"sanctuary.discovery.granted": "Découvertes ajoutées pour %s : %s blocs, %s objets, %s espèces. Recettes liées actualisées ; rouvre les catalogues.",
|
||||
"sanctuary.discovery.invalid": "Choisis un bloc, un objet ou une espèce découvrable."
|
||||
"sanctuary.discovery.invalid": "Choisis un bloc, un objet ou une espèce découvrable.",
|
||||
"sanctuary.shaders.edge_highlights": "Reflets sur les arêtes",
|
||||
"sanctuary.shaders.edge_highlights_help": "Reflets connectés sur les contours du relief. Les surfaces plates se raccordent, même entre blocs différents. Préserve les textures. Activé par défaut ; nécessite Vanilla Light.",
|
||||
"sanctuary.shaders.edge_intensity": "Intensité des arêtes",
|
||||
"sanctuary.shaders.edge_intensity_help": "Force du reflet sur les arêtes. Le réglage est indépendant des ombres portées.",
|
||||
"sanctuary.shaders.edge_intensity_value": "Intensité des arêtes : %s %%",
|
||||
"sanctuary.shaders.pixel_shadows": "Ombres pixelisées",
|
||||
"sanctuary.shaders.pixel_shadows_help": "Ombres du soleil et ombres légères de la lune, sur une grille de pixels. Feuillage irrégulier et contours stables. Activées par défaut, portée 128 blocs ; nécessite Vanilla Light.",
|
||||
"sanctuary.shaders.shadow_pixels": "Précision des ombres",
|
||||
"sanctuary.shaders.shadow_pixels_value": "%s pixels/bloc",
|
||||
"sanctuary.shaders.shadow_pixels_help": "Précision de la grille : 8, 16, 32, 64 ou 128 pixels par bloc. Plus la valeur est élevée, plus les pixels des ombres sont fins. Nécessite Vanilla Light et les ombres pixelisées.",
|
||||
"sanctuary.shaders.shadow_distance": "Distance des ombres",
|
||||
"sanctuary.shaders.shadow_distance_value": "%s blocs",
|
||||
"sanctuary.shaders.shadow_distance_help": "Portée des ombres : 16, 32, 64, 128 ou 256 blocs, dans le terrain chargé. Une distance élevée coûte davantage en performances. Nécessite Vanilla Light et les ombres pixelisées.",
|
||||
"sanctuary.shaders.edge_distance": "Portée des arêtes",
|
||||
"sanctuary.shaders.edge_distance_value": "%s blocs",
|
||||
"sanctuary.shaders.edge_distance_help": "Portée indépendante : 16, 32, 64, 128 ou 256 blocs. Limitée au terrain visible et à la distance de rendu. Les contours lointains s’affinent naturellement.",
|
||||
"sanctuary.shaders.precision": "Précision",
|
||||
"sanctuary.shaders.precision_help": "Grille commune aux ombres et aux reflets sur les arêtes : 8, 16, 32, 64 ou 128 pixels par bloc. La carte d’ombres conserve sa résolution maximale.",
|
||||
"sanctuary.shaders.precision_value": "%s pixels/bloc",
|
||||
"sanctuary.shaders.bloom": "Bloom",
|
||||
"sanctuary.shaders.bloom_help": "Halo du soleil et des surfaces lumineuses. Activé par défaut. L’arrê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, 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.",
|
||||
"sanctuary.shaders.bloom_radius_value": "Diffusion : %s %%",
|
||||
"sanctuary.shaders.ore_emission": "Minerais émissifs",
|
||||
"sanctuary.shaders.ore_emission_help": "Fait briller les inclusions des minerais sans éclairer leur roche, même avec le bloom coupé ou à zéro. Activé par défaut. Effet visuel uniquement.",
|
||||
"sanctuary.shaders.ore_intensity": "Émission des minerais",
|
||||
"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. 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. 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 %%"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
#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(){vec3 halo=texture(InputTexture,texCoord).rgb;fragColor=vec4(clamp(halo*BloomParams.z,0.0,1.0),0.0);}
|
||||
@@ -0,0 +1,13 @@
|
||||
#version 330
|
||||
#extension GL_ARB_separate_shader_objects : require
|
||||
uniform sampler2D InputTexture;
|
||||
layout(std140) uniform BlurParams { vec4 Step; };
|
||||
layout(location=0) in vec2 texCoord;
|
||||
layout(location=0) out vec4 fragColor;
|
||||
// At most one low-resolution texel between taps: broad halos cannot turn into a sparse grid.
|
||||
void main(){
|
||||
float weights[13]=float[13](0.099908358,0.096834501,0.088168817,0.075414785,0.060597482,0.045741379,0.032435495,0.021606698,0.013521126,0.007948660,0.004389667,0.002277329,0.001109882);
|
||||
vec3 color=texture(InputTexture,texCoord).rgb*weights[0];
|
||||
for(int i=1;i<=12;i++){vec2 d=Step.xy*float(i);color+=(texture(InputTexture,texCoord+d).rgb+texture(InputTexture,texCoord-d).rgb)*weights[i];}
|
||||
fragColor=vec4(color,0.0);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
#version 330
|
||||
#extension GL_ARB_separate_shader_objects : require
|
||||
uniform sampler2D InputTexture;
|
||||
layout(location=0) in vec2 texCoord;
|
||||
layout(location=0) out vec4 fragColor;
|
||||
void main(){vec4 emission=texture(InputTexture,texCoord);fragColor=emission.a>0.0?emission:vec4(0.0);}
|
||||
@@ -0,0 +1,6 @@
|
||||
#version 330
|
||||
#extension GL_ARB_separate_shader_objects : require
|
||||
uniform sampler2D InputTexture;
|
||||
layout(location=0) in vec2 texCoord;
|
||||
layout(location=0) out vec4 fragColor;
|
||||
void main(){vec2 pixel=1.0/vec2(textureSize(InputTexture,0));vec3 sum=vec3(0.0);for(int y=0;y<8;y++)for(int x=0;x<8;x++)sum+=texture(InputTexture,texCoord+(vec2(x,y)-3.5)*pixel).rgb;fragColor=vec4(sum/64.0,0.0);}
|
||||
@@ -0,0 +1,22 @@
|
||||
#version 330
|
||||
#extension GL_ARB_separate_shader_objects : require
|
||||
#include <sanctuary:bloom_scene.glsl>
|
||||
uniform sampler2D BlockAtlas;
|
||||
uniform sampler2D EmissionMasks;
|
||||
layout(location=0) in vec3 relativePosition;
|
||||
layout(location=1) in vec2 blockUv;
|
||||
layout(location=2) in vec2 maskUv;
|
||||
layout(location=3) in vec4 tint;
|
||||
layout(location=0) out vec4 fragColor;
|
||||
void main(){
|
||||
vec2 uv=gl_FragCoord.xy/vec2(textureSize(WorldDepth,0));
|
||||
float depth=texture(WorldDepth,uv).r;
|
||||
if(depth>0.0&&length(relativePosition)>length(bloomPosition(uv,depth))+0.02)discard;
|
||||
vec4 source=texture(BlockAtlas,blockUv);
|
||||
float mask=texture(EmissionMasks,maskUv).r;
|
||||
bool ore=tint.a<0.5;
|
||||
float nativeLight=ore?tint.a*(255.0/120.0):max(0.0,(tint.a*255.0-128.0)/127.0);
|
||||
float amount=mask*(ore?max(BloomParams.x,nativeLight):nativeLight)*bloomFog(relativePosition);
|
||||
if(source.a<0.5||amount<0.001)discard;
|
||||
fragColor=vec4(source.rgb*tint.rgb*amount,ore?amount:0.0);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
#version 330
|
||||
#extension GL_ARB_separate_shader_objects : require
|
||||
#include <minecraft:dynamictransforms.glsl>
|
||||
layout(location=0) in vec3 Position;
|
||||
layout(location=1) in vec4 Color;
|
||||
layout(location=2) in vec2 UV0;
|
||||
layout(location=3) in vec2 UV3;
|
||||
layout(location=0) out vec3 relativePosition;
|
||||
layout(location=1) out vec2 blockUv;
|
||||
layout(location=2) out vec2 maskUv;
|
||||
layout(location=3) out vec4 tint;
|
||||
void main(){gl_Position=ModelViewMat*vec4(Position,1.0);relativePosition=Position+ModelOffset;blockUv=UV0;maskUv=UV3;tint=Color;}
|
||||
@@ -0,0 +1,15 @@
|
||||
#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(){
|
||||
fragColor=vec4(0.0);
|
||||
if(texture(WorldDepth,texCoord).r>0.0)return;
|
||||
vec3 ray=normalize(bloomPosition(texCoord,0.00001));
|
||||
float celestial=max(step(0.955,dot(ray,SunDirection.xyz))*SunDirection.w,step(0.975,dot(ray,MoonDirection.xyz))*MoonDirection.w);
|
||||
vec3 color=texture(InputTexture,texCoord).rgb;
|
||||
float luminance=dot(color,vec3(.2126,.7152,.0722));
|
||||
fragColor=vec4(color*smoothstep(.72,.98,luminance)*celestial,0.0);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
#version 330
|
||||
#extension GL_ARB_separate_shader_objects : require
|
||||
uniform sampler2D WorldDepth;
|
||||
layout(std140) uniform EdgeHighlights {
|
||||
mat4 InverseViewProjection;
|
||||
mat4 ViewProjection;
|
||||
vec4 CameraGrid;
|
||||
vec4 StrengthClipFog;
|
||||
vec4 EnvironmentFog;
|
||||
};
|
||||
layout(location = 0) in vec2 texCoord;
|
||||
layout(location = 0) out vec4 fragColor;
|
||||
|
||||
vec3 positionAt(vec2 uv, float depth) {
|
||||
float z = StrengthClipFog.y > 0.5 ? depth : depth * 2.0 - 1.0;
|
||||
vec4 p = InverseViewProjection * vec4(uv * 2.0 - 1.0, z, 1.0);
|
||||
return p.xyz / p.w;
|
||||
}
|
||||
|
||||
float exposed(vec3 probe, vec3 surface, vec3 normal) {
|
||||
vec4 clip = ViewProjection * vec4(probe, 1.0);
|
||||
if (clip.w <= 0.0) return 0.0;
|
||||
vec2 uv = clip.xy / clip.w * 0.5 + 0.5;
|
||||
if (any(lessThanEqual(uv, vec2(0.0))) || any(greaterThanEqual(uv, vec2(1.0)))) return 0.0;
|
||||
// Reconstruct at the sampled depth texel's centre, not at a different subpixel position.
|
||||
vec2 size = vec2(textureSize(WorldDepth, 0));
|
||||
uv = (floor(uv * size) + 0.5) / size;
|
||||
float depth = texture(WorldDepth, uv).r;
|
||||
if (depth <= 0.0) return 1.0;
|
||||
float behind = dot(positionAt(uv, depth) - surface, normal);
|
||||
// A coplanar neighbouring block contributes zero. Concave corners are not glowing seams.
|
||||
return 1.0 - smoothstep(-0.045, -0.020, behind);
|
||||
}
|
||||
|
||||
float fogAmount(float distance, float start, float end) {
|
||||
return clamp((distance - start) / max(end - start, 0.001), 0.0, 1.0);
|
||||
}
|
||||
|
||||
void main() {
|
||||
float depth = texture(WorldDepth, texCoord).r;
|
||||
vec3 relative = positionAt(texCoord, depth);
|
||||
vec3 dx = dFdx(relative), dy = dFdy(relative);
|
||||
vec3 normal = normalize(cross(dx, dy));
|
||||
if (dot(normal, -relative) < 0.0) normal = -normal;
|
||||
vec3 an = abs(normal);
|
||||
if (depth <= 0.0 || length(relative) >= EnvironmentFog.w || max(max(an.x, an.y), an.z) < 0.995) discard;
|
||||
vec3 axis = an.x > an.y && an.x > an.z ? vec3(1,0,0) : an.y > an.z ? vec3(0,1,0) : vec3(0,0,1);
|
||||
normal = axis * sign(dot(normal, axis));
|
||||
vec3 tangent = axis.x > 0.5 ? vec3(0,0,1) : vec3(1,0,0);
|
||||
vec3 bitangent = abs(cross(axis, tangent));
|
||||
float width = 1.0 / CameraGrid.w;
|
||||
vec3 p = relative + CameraGrid.xyz;
|
||||
float plane = floor(dot(p, axis) * CameraGrid.w + 0.5) * width;
|
||||
// Restrict to voxel planes; don't trace arbitrary moving models with a voxel-grid effect.
|
||||
if (abs(dot(p, axis) - plane) > 0.003) discard;
|
||||
vec3 q = (floor(p * CameraGrid.w + 0.0001) + 0.5) * width;
|
||||
q += axis * (plane - dot(q, axis));
|
||||
q -= CameraGrid.xyz;
|
||||
float probeWidth = max(width, max(length(dx), length(dy)));
|
||||
float edge = max(max(exposed(q + tangent * probeWidth, q, normal), exposed(q - tangent * probeWidth, q, normal)),
|
||||
max(exposed(q + bitangent * probeWidth, q, normal), exposed(q - bitangent * probeWidth, q, normal)));
|
||||
// Connected surfaces share their outline, irrespective of the block texture.
|
||||
// Integrate subpixel strips as coverage rather than deleting distant CTM outlines.
|
||||
float footprint = max(length(dx), length(dy));
|
||||
float fade = min(1.0, width / max(footprint, width));
|
||||
fade *= 1.0 - smoothstep(EnvironmentFog.w * 0.75, EnvironmentFog.w, length(relative));
|
||||
float fog = max(fogAmount(length(relative), EnvironmentFog.x, EnvironmentFog.y),
|
||||
fogAmount(max(length(relative.xz), abs(relative.y)), StrengthClipFog.z, StrengthClipFog.w));
|
||||
float highlight = edge * fade * StrengthClipFog.x * (1.0 - fog * EnvironmentFog.z);
|
||||
fragColor = vec4(vec3(highlight), 0.0);
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
#version 330
|
||||
#extension GL_ARB_separate_shader_objects : require
|
||||
uniform sampler2D WorldDepth;
|
||||
uniform sampler2D SceneColor;
|
||||
uniform sampler2D BlockAtlas;
|
||||
uniform sampler2D MaterialAtlas;
|
||||
uniform sampler2D SunDepth;
|
||||
layout(std140) uniform PbrScene {
|
||||
mat4 PbrInverseVP;
|
||||
vec4 PbrSun;
|
||||
vec4 PbrParams;
|
||||
vec4 PbrFog;
|
||||
vec4 PbrEnvironment;
|
||||
};
|
||||
layout(std140) uniform PixelShadows {
|
||||
mat4 InverseViewProjection;
|
||||
mat4 LightMatrix;
|
||||
vec4 CameraFractionPixels;
|
||||
vec4 SunStrength;
|
||||
vec4 DistanceClipFog;
|
||||
vec4 EnvironmentFog;
|
||||
};
|
||||
layout(location=0) in vec3 relativePosition;
|
||||
layout(location=1) in vec2 blockUv;
|
||||
layout(location=2) in vec2 maskUv;
|
||||
layout(location=3) in vec4 tint;
|
||||
layout(location=0) out vec4 fragColor;
|
||||
float visibility(vec3 p,vec3 n){
|
||||
if(PbrParams.z<0.5)return 1.0;
|
||||
vec3 world=p+CameraFractionPixels.xyz;
|
||||
vec3 snapped=(floor(world*CameraFractionPixels.w+.0001)+.5)/CameraFractionPixels.w;
|
||||
vec3 receiver=snapped+n*dot(world-snapped,n);
|
||||
vec4 light=LightMatrix*vec4(receiver+n*0.04+PbrSun.xyz*0.015,1.0);
|
||||
vec3 q=light.xyz/light.w;q.xy=q.xy*0.5+0.5;
|
||||
if(DistanceClipFog.y<0.5)q.z=q.z*0.5+0.5;
|
||||
if(any(lessThan(q,vec3(0)))||any(greaterThan(q,vec3(1))))return 1.0;
|
||||
return q.z>texture(SunDepth,q.xy).r+0.00015?0.0:1.0;
|
||||
}
|
||||
void main(){
|
||||
// Derivatives precede divergent discard; UV-space tangents preserve rotated/mirrored block textures.
|
||||
vec3 dx=dFdx(relativePosition),dy=dFdy(relativePosition);
|
||||
vec2 tx=dFdx(blockUv),ty=dFdy(blockUv);
|
||||
float det=tx.x*ty.y-tx.y*ty.x;
|
||||
vec3 baseNormal=normalize(cross(dx,dy));if(dot(baseNormal,-relativePosition)<0.0)baseNormal=-baseNormal;
|
||||
vec2 uv=gl_FragCoord.xy/vec2(textureSize(WorldDepth,0));float depth=texture(WorldDepth,uv).r;
|
||||
vec4 projected=PbrInverseVP*vec4(uv*2.0-1.0,PbrParams.y>0.5?depth:depth*2.0-1.0,1.0);
|
||||
vec3 visible=projected.xyz/projected.w;
|
||||
if(depth<=0.0||abs(length(visible)-length(relativePosition))>0.035||length(relativePosition)>=PbrParams.w||abs(det)<1e-12)discard;
|
||||
vec4 texel=texture(BlockAtlas,blockUv);if(texel.a<0.99)discard;
|
||||
vec3 tangent=normalize((dx*ty.y-dy*tx.y)/det),bitangent=normalize((dy*tx.x-dx*ty.x)/det);
|
||||
vec4 material=texture(MaterialAtlas,maskUv);
|
||||
// RG stores signed height derivatives; B roughness, A metalness, not transparency.
|
||||
vec2 slope=(material.rg*255.0-128.0)/127.0*PbrParams.x*2.0;
|
||||
vec3 n=normalize(baseNormal+tangent*slope.x+bitangent*slope.y);
|
||||
vec3 l=normalize(PbrSun.xyz),v=normalize(-relativePosition),h=normalize(l+v);
|
||||
float nl=max(dot(n,l),0.0),nv=max(dot(n,v),0.001),nh=max(dot(n,h),0.0),vh=max(dot(v,h),0.0);
|
||||
float roughness=max(material.b,0.25),alpha=roughness*roughness,a2=alpha*alpha;
|
||||
float denom=nh*nh*(a2-1.0)+1.0;
|
||||
float distribution=a2/max(3.14159265*denom*denom,0.00001);
|
||||
float k=(roughness+1.0)*(roughness+1.0)/8.0;
|
||||
float geometry=nv/(nv*(1.0-k)+k)*nl/max(nl*(1.0-k)+k,0.00001);
|
||||
vec3 albedo=texel.rgb*tint.rgb;albedo*=albedo;
|
||||
vec3 f0=mix(vec3(0.04),albedo,material.a);
|
||||
vec3 fresnel=f0+(1.0-f0)*pow(1.0-vh,5.0);
|
||||
vec3 specular=distribution*geometry*fresnel/max(4.0*nv*nl,0.0001)*nl;
|
||||
float fog=max(clamp((max(length(relativePosition.xz),abs(relativePosition.y))-PbrFog.x)/max(PbrFog.y-PbrFog.x,.001),0.0,1.0),
|
||||
clamp((length(relativePosition)-PbrFog.z)/max(PbrFog.w-PbrFog.z,.001),0.0,1.0));
|
||||
float amount=PbrSun.w*pow(tint.a,8.0)*visibility(relativePosition,baseNormal)*(1.0-fog*PbrEnvironment.x)
|
||||
*(1.0-smoothstep(PbrParams.w*.75,PbrParams.w,length(relativePosition)));
|
||||
if(amount<=0.0001)discard;
|
||||
vec3 scene=texture(SceneColor,uv).rgb;vec3 linear=scene*scene;
|
||||
// Native diffuse light already exists. Apply only the perturbed-normal difference, then add GGX.
|
||||
float relief=(nl-max(dot(baseNormal,l),0.0))*.7;
|
||||
vec3 lit=clamp(linear*(1.0+relief*amount),0.0,1.0);
|
||||
vec3 shine=min(specular*PbrParams.x*amount*3.0,vec3(0.5));
|
||||
lit+=(1.0-lit)*(1.0-exp(-shine));
|
||||
fragColor=vec4(sqrt(clamp(lit,0.0,1.0)),1.0);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
#version 330
|
||||
#extension GL_ARB_separate_shader_objects : require
|
||||
#include <minecraft:dynamictransforms.glsl>
|
||||
layout(location=0) in vec3 Position;
|
||||
layout(location=1) in vec4 Color;
|
||||
layout(location=2) in vec2 UV0;
|
||||
layout(location=3) in vec2 UV3;
|
||||
layout(location=0) out vec3 relativePosition;
|
||||
layout(location=1) out vec2 blockUv;
|
||||
layout(location=2) out vec2 maskUv;
|
||||
layout(location=3) out vec4 tint;
|
||||
void main(){gl_Position=ModelViewMat*vec4(Position,1.0);relativePosition=Position+ModelOffset;blockUv=UV0;maskUv=UV3;tint=Color;}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
#version 330
|
||||
#extension GL_ARB_separate_shader_objects : require
|
||||
layout(location = 0) in vec3 foliagePosition;
|
||||
layout(location = 1) flat in float foliage;
|
||||
layout(location = 0) out vec4 material;
|
||||
uint mixBits(uint h) {
|
||||
h ^= h >> 16; h *= 0x7feb352du; h ^= h >> 15; h *= 0x846ca68bu; return h ^ (h >> 16);
|
||||
}
|
||||
void main() {
|
||||
// Fixed quarter-block cells: no frame/time/camera seed, texture-alpha sampling or CPU ray march.
|
||||
// Each crossed leaf surface transmits 78%; independent layers converge towards solid shade.
|
||||
if (foliage > 0.5) {
|
||||
uvec3 p = uvec3(ivec3(floor((foliagePosition + 0.03125) * 4.0)));
|
||||
uint h = mixBits(p.x ^ mixBits(p.y + 0x9e3779b9u) ^ mixBits(p.z + 0x85ebca6bu));
|
||||
if (float(h & 65535u) / 65535.0 >= 0.22) discard;
|
||||
}
|
||||
material = vec4(foliage, 0.0, 0.0, 1.0);
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
#version 330
|
||||
#extension GL_ARB_separate_shader_objects : require
|
||||
#include <minecraft:dynamictransforms.glsl>
|
||||
layout(location = 0) in vec3 Position;
|
||||
layout(location = 1) in vec4 Color;
|
||||
layout(location = 0) out vec3 foliagePosition;
|
||||
layout(location = 1) flat out float foliage;
|
||||
void main() {
|
||||
gl_Position = ModelViewMat * vec4(Position, 1.0);
|
||||
foliagePosition = Position + ModelOffset;
|
||||
foliage = Color.r;
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
#version 330
|
||||
#extension GL_ARB_separate_shader_objects : require
|
||||
uniform sampler2D WorldDepth;
|
||||
uniform sampler2D SunDepth;
|
||||
uniform sampler2D SunMaterial;
|
||||
layout(std140) uniform PixelShadows {
|
||||
mat4 InverseViewProjection;
|
||||
mat4 LightMatrix;
|
||||
vec4 CameraFractionPixels;
|
||||
vec4 SunStrength;
|
||||
vec4 DistanceClipFog;
|
||||
vec4 EnvironmentFog;
|
||||
};
|
||||
layout(location = 0) in vec2 texCoord;
|
||||
layout(location = 0) out vec4 fragColor;
|
||||
float linearFog(float d, float start, float end) {
|
||||
return clamp((d - start) / max(end - start, 0.001), 0.0, 1.0);
|
||||
}
|
||||
float shadowAt(vec2 uv, vec3 receiver, vec2 planeSlope, bool includeOpaque) {
|
||||
if (!includeOpaque && texture(SunMaterial, uv).r < 0.5) return 0.0;
|
||||
// Compare each tap on the same receiving plane; an oblique floor must not shadow itself.
|
||||
float depth = receiver.z + dot(uv - receiver.xy, planeSlope);
|
||||
return depth > texture(SunDepth, uv).r + 0.00015 ? 1.0 : 0.0;
|
||||
}
|
||||
float bilinearShadow(vec2 uv, vec3 receiver, vec2 planeSlope, bool includeOpaque) {
|
||||
vec2 size = vec2(textureSize(SunDepth, 0));
|
||||
vec2 at = uv * size - 0.5;
|
||||
vec2 base = (floor(at) + 0.5) / size;
|
||||
vec2 t = fract(at), stepSize = 1.0 / size;
|
||||
return mix(mix(shadowAt(base,receiver,planeSlope,includeOpaque),
|
||||
shadowAt(base+vec2(stepSize.x,0.0),receiver,planeSlope,includeOpaque),t.x),
|
||||
mix(shadowAt(base+vec2(0.0,stepSize.y),receiver,planeSlope,includeOpaque),
|
||||
shadowAt(base+stepSize,receiver,planeSlope,includeOpaque),t.x),t.y);
|
||||
}
|
||||
float filteredShadow(vec3 coord, vec2 planeSlope, bool includeOpaque) {
|
||||
// Four fixed bilinear taps span a quarter block, averaging leaf coverage into softer shades.
|
||||
// The result is still evaluated once per world-grid cell, with no temporal/random jitter.
|
||||
float r = 0.125 / (2.0 * (DistanceClipFog.x + 16.0));
|
||||
return 0.25 * (bilinearShadow(coord.xy+vec2(-r,-r),coord,planeSlope,includeOpaque)
|
||||
+ bilinearShadow(coord.xy+vec2( r,-r),coord,planeSlope,includeOpaque)
|
||||
+ bilinearShadow(coord.xy+vec2(-r, r),coord,planeSlope,includeOpaque)
|
||||
+ bilinearShadow(coord.xy+vec2( r, r),coord,planeSlope,includeOpaque));
|
||||
}
|
||||
void main() {
|
||||
float depth = texture(WorldDepth, texCoord).r;
|
||||
float clipDepth = DistanceClipFog.y > 0.5 ? depth : depth * 2.0 - 1.0;
|
||||
vec4 projected = InverseViewProjection * vec4(texCoord * 2.0 - 1.0, clipDepth, 1.0);
|
||||
vec3 relative = projected.xyz / projected.w;
|
||||
// Derivatives before divergent branches; keep the receiving plane instead of snapping into a block.
|
||||
vec3 normal = normalize(cross(dFdx(relative), dFdy(relative)));
|
||||
if (dot(normal, -relative) < 0.0) normal = -normal;
|
||||
if (depth <= 0.0 || length(relative) >= DistanceClipFog.x) discard;
|
||||
// Depth derivatives vary with the camera. Use a stable axis for voxel faces.
|
||||
vec3 absoluteNormal = abs(normal);
|
||||
vec3 axis = absoluteNormal.x > absoluteNormal.y && absoluteNormal.x > absoluteNormal.z ? vec3(1,0,0)
|
||||
: absoluteNormal.y > absoluteNormal.z ? vec3(0,1,0) : vec3(0,0,1);
|
||||
bool voxelFace = max(max(absoluteNormal.x, absoluteNormal.y), absoluteNormal.z) > 0.995;
|
||||
if (voxelFace) normal = axis * sign(dot(normal,axis));
|
||||
vec3 p = relative + CameraFractionPixels.xyz;
|
||||
float pixels = CameraFractionPixels.w;
|
||||
vec3 snapped = (floor(p * pixels + 0.0001) + 0.5) / pixels;
|
||||
// Vanilla voxel faces live on the 1/16 grid. Quantize their plane as well as the two face axes.
|
||||
float plane = floor(dot(p,axis) * 16.0 + 0.5) / 16.0;
|
||||
vec3 q = voxelFace ? snapped + axis * (plane - dot(snapped,axis))
|
||||
: snapped + normal * dot(p - snapped, normal);
|
||||
q += normal * 0.04 + SunStrength.xyz * 0.015;
|
||||
vec4 light = LightMatrix * vec4(q, 1.0);
|
||||
vec3 coord = light.xyz / light.w;
|
||||
coord.xy = coord.xy * 0.5 + 0.5;
|
||||
if (DistanceClipFog.y < 0.5) coord.z = coord.z * 0.5 + 0.5;
|
||||
if (any(lessThan(coord, vec3(0.0))) || any(greaterThan(coord, vec3(1.0)))) discard;
|
||||
// Cofactors transform the receiver normal without a per-fragment matrix inverse.
|
||||
vec3 a = LightMatrix[0].xyz, b = LightMatrix[1].xyz, c = LightMatrix[2].xyz;
|
||||
vec3 lightNormal = normal.x * cross(b,c) + normal.y * cross(c,a) + normal.z * cross(a,b);
|
||||
vec2 planeSlope = abs(lightNormal.z) > 0.00000001
|
||||
? -lightNormal.xy / lightNormal.z * (DistanceClipFog.y > 0.5 ? 2.0 : 1.0) : vec2(0.0);
|
||||
bool leaf = texture(SunMaterial, coord.xy).r > 0.5;
|
||||
float hardShadow = shadowAt(coord.xy, coord, planeSlope, true);
|
||||
// Keep opaque-block edges crisp. Only foliage coverage receives the soft kernel.
|
||||
// Leaf taps may include opaque occluders behind them, preserving shade under roofs.
|
||||
float shade = !leaf && hardShadow > 0.5 ? 1.0 : filteredShadow(coord, planeSlope, leaf);
|
||||
float fade = 1.0 - smoothstep(DistanceClipFog.x * 0.8, DistanceClipFog.x, length(relative));
|
||||
float fog = max(linearFog(length(relative), EnvironmentFog.x, EnvironmentFog.y),
|
||||
linearFog(max(length(relative.xz), abs(relative.y)), DistanceClipFog.z, DistanceClipFog.w));
|
||||
fragColor = vec4(0.0, 0.0, 0.0, shade * SunStrength.w * fade * (1.0 - fog * EnvironmentFog.z));
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
#version 330
|
||||
#extension GL_ARB_separate_shader_objects : require
|
||||
#include <sanctuary:ssgi.glsl>
|
||||
layout(location=0) in vec2 texCoord;
|
||||
layout(location=0) out vec4 fragColor;
|
||||
void main(){
|
||||
vec2 surface=giTexel(texCoord);float depth=texture(WorldDepth,surface).r;
|
||||
if(depth<=0.0)discard;
|
||||
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++){
|
||||
vec2 uv=(base+vec2(x,y)+0.5)/size;
|
||||
vec4 sampleLight=texture(IndirectLight,uv);
|
||||
if(sampleLight.a<=0.0)continue;
|
||||
vec2 surfaceUV=giTexel(uv);vec3 q=giPosition(surfaceUV,texture(WorldDepth,surfaceUV).r);
|
||||
float plane=abs(dot(q-p,normal));
|
||||
// Bilateral reconstruction never blends a wall's light into the foreground silhouette.
|
||||
if(plane>0.06||length(q-p)>0.5)continue;
|
||||
float weight=(x==0?1.0-f.x:f.x)*(y==0?1.0-f.y:f.y)*(1.0-smoothstep(0.01,0.06,plane));
|
||||
bounce+=sampleLight.rgb*weight;total+=weight;
|
||||
}
|
||||
if(total<=0.0001)discard;
|
||||
if(max(bounce.r,max(bounce.g,bounce.b))<=0.0)discard;
|
||||
vec3 scene=texture(SceneColor,texCoord).rgb;
|
||||
vec3 linearScene=scene*scene;
|
||||
// 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*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);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
#version 330
|
||||
#extension GL_ARB_separate_shader_objects : require
|
||||
#include <sanctuary:ssgi.glsl>
|
||||
layout(std140) uniform GIBlur { vec4 Direction; };
|
||||
layout(location=0) in vec2 texCoord;
|
||||
layout(location=0) out vec4 fragColor;
|
||||
void main(){
|
||||
vec4 center=texture(IndirectLight,texCoord);fragColor=center;
|
||||
if(center.a<=0.0)return;
|
||||
vec2 uv=giTexel(texCoord);float depth=texture(WorldDepth,uv).r;
|
||||
if(depth<=0.0)return;
|
||||
vec3 p=giPosition(uv,depth),n=giNormal(uv,p);
|
||||
if(length(n)<.9)return;
|
||||
vec2 pixel=Direction.xy/vec2(textureSize(IndirectLight,0));
|
||||
vec2 nearUV=giTexel(texCoord+pixel);
|
||||
float nearDepth=texture(WorldDepth,nearUV).r;
|
||||
float footprint=nearDepth>0.0?length(giPosition(nearUV,nearDepth)-p):.12;
|
||||
// Approx. 0.36 block support, capped at 24 half-resolution pixels at close range.
|
||||
float stride=clamp(.12/max(footprint,.001),1.0,8.0);
|
||||
vec3 total=center.rgb;float weight=1.0;
|
||||
for(int i=-3;i<=3;i++){
|
||||
if(i==0)continue;
|
||||
vec2 tap=texCoord+pixel*(float(i)*stride);
|
||||
if(any(lessThan(tap,vec2(0)))||any(greaterThan(tap,vec2(1))))continue;
|
||||
vec4 light=texture(IndirectLight,tap);if(light.a<=0.0)continue;
|
||||
vec2 surface=giTexel(tap);float d=texture(WorldDepth,surface).r;if(d<=0.0)continue;
|
||||
vec3 q=giPosition(surface,d);float plane=abs(dot(q-p,n));
|
||||
// Only blur the indirect light on the same receiving plane, never scene textures or silhouettes.
|
||||
if(plane>.035||length(q-p)>.7)continue;
|
||||
float w=exp(-float(i*i)*.32)*(1.0-smoothstep(.01,.035,plane));
|
||||
total+=light.rgb*w;weight+=w;
|
||||
}
|
||||
fragColor=vec4(total/weight,center.a);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
#version 330
|
||||
#extension GL_ARB_separate_shader_objects : require
|
||||
#include <sanctuary:ssgi.glsl>
|
||||
layout(location=0) in vec2 texCoord;
|
||||
layout(location=0) out vec4 fragColor;
|
||||
void main(){
|
||||
fragColor=vec4(0.0);
|
||||
vec2 uv=giTexel(texCoord);float depth=texture(WorldDepth,uv).r;
|
||||
if(depth<=0.0)return;
|
||||
vec3 p=giPosition(uv,depth);float distance=length(p);
|
||||
if(distance>=GISettings.w)return;
|
||||
vec3 n=giNormal(uv,p);
|
||||
if(length(n)<0.9)return;
|
||||
vec3 tangent=normalize(cross(n,abs(n.y)<0.9?vec3(0,1,0):vec3(0,0,1)));
|
||||
vec3 bitangent=cross(n,tangent),sum=vec3(0.0);
|
||||
// Fixed hemisphere directions; no per-frame stochastic noise or temporal history.
|
||||
for(int ray=0;ray<12;ray++){
|
||||
float angle=6.2831853*(float(ray<8?ray:ray-8)+0.5)/float(ray<8?8:4);
|
||||
float elevation=ray<8?0.4:0.85;
|
||||
vec3 direction=n*elevation+(tangent*cos(angle)+bitangent*sin(angle))*sqrt(1.0-elevation*elevation);
|
||||
vec3 start=p+n*0.045;
|
||||
for(int step=1;step<=10;step++){
|
||||
float travel=GISettings.z*float(step)/10.0;
|
||||
vec3 probe=start+direction*travel;
|
||||
vec4 clip=ViewProjection*vec4(probe,1.0);
|
||||
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;
|
||||
if(hitDepth<=0.0)continue;
|
||||
vec3 hit=giPosition(hitUV,hitDepth);float behind=length(probe)-length(hit);
|
||||
if(behind<=0.015)continue;
|
||||
// First occluder terminates the ray even when too thick: never continue behind a wall.
|
||||
vec3 delta=hit-p;float separation=length(delta);
|
||||
if(behind<0.35&&separation>0.08&&separation<GISettings.z&&dot(delta,n)>0.03){
|
||||
vec3 hitNormal=giNormal(hitUV,hit);
|
||||
float facing=max(0.0,dot(hitNormal,-normalize(delta)));
|
||||
float edge=smoothstep(0.0,0.06,min(min(hitUV.x,hitUV.y),min(1.0-hitUV.x,1.0-hitUV.y)));
|
||||
vec3 color=texture(SceneColor,hitUV).rgb;
|
||||
float falloff=1.0-separation/GISettings.z;
|
||||
// Vanilla already supplies neutral ambient light. Transport the chromatic
|
||||
// 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)));
|
||||
float confidence=smoothstep(.05,.3,clip.w)*(1.0-smoothstep(.12,.35,behind));
|
||||
sum+=chroma*facing*falloff*edge*giFog(hit)*confidence;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
fragColor=vec4(sum/12.0*giFog(p)*(1.0-smoothstep(GISettings.w*0.75,GISettings.w,distance)),distance);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
uniform sampler2D WorldDepth;
|
||||
layout(std140) uniform BloomScene {
|
||||
mat4 InverseViewProjection;
|
||||
vec4 BloomParams;
|
||||
vec4 SunDirection;
|
||||
vec4 MoonDirection;
|
||||
vec4 FogDistance;
|
||||
vec4 FogInfo;
|
||||
};
|
||||
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);
|
||||
return p.xyz/p.w;
|
||||
}
|
||||
float bloomFog(vec3 position) {
|
||||
float a=clamp((max(length(position.xz),abs(position.y))-FogDistance.x)/max(.001,FogDistance.y-FogDistance.x),0.0,1.0);
|
||||
float b=clamp((length(position)-FogDistance.z)/max(.001,FogDistance.w-FogDistance.z),0.0,1.0);
|
||||
return 1.0-max(a,b)*FogInfo.x;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
uniform sampler2D WorldDepth;
|
||||
uniform sampler2D SceneColor;
|
||||
uniform sampler2D IndirectLight;
|
||||
layout(std140) uniform GIParams {
|
||||
mat4 InverseViewProjection;
|
||||
mat4 ViewProjection;
|
||||
vec4 GISettings; // strength, zero-to-one clip, local radius, view range
|
||||
vec4 GIFog;
|
||||
vec4 GIEnvironment;
|
||||
};
|
||||
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 (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=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);
|
||||
return 1.0-max(a,b)*GIEnvironment.x;
|
||||
}
|
||||
@@ -274,7 +274,10 @@
|
||||
"client.SuperPistonSyncMixin",
|
||||
"client.SeasonalPrecipitationMixin",
|
||||
"client.SpecialModelsAccess",
|
||||
"client.SculptureParticlesMixin"
|
||||
"client.SculptureParticlesMixin",
|
||||
"client.PixelShadowRendererMixin",
|
||||
"client.PixelShadowLevelMixin",
|
||||
"client.PixelShadowDirtyMixin"
|
||||
],
|
||||
"injectors": {
|
||||
"defaultRequire": 1
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
name = "Sanctuary"
|
||||
author = "KOKA99CAB"
|
||||
version = "beta.126"
|
||||
version = "beta.135"
|
||||
pack-format = "packwiz:1.1.0"
|
||||
|
||||
[index]
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
# Shader packs
|
||||
|
||||
Emplacement des sources et réglages propres à Sanctuary. Aucun shader n'est
|
||||
encore distribué. Les shaders externes seront ajoutés au manifeste packwiz
|
||||
avec version, URL, hash, licence et compatibilité vérifiée pour le jeu et Iris.
|
||||
Le shader natif **Sanctuary · Vanilla Light** est intégré au mod. La beta.127
|
||||
ajoute les ombres portées pixélisées optionnelles, configurables depuis
|
||||
**Options → Shaders**. Sources Java et GLSL dans `mods/sanctuary/` ;
|
||||
[réglages, limites et validation](../docs/shader-beta127.md).
|
||||
|
||||
Ce dossier reste réservé aux futurs packs externes. Leur ajout au manifeste
|
||||
packwiz nécessitera version, URL, hash, licence et compatibilité vérifiée pour
|
||||
la version exacte du jeu et du moteur Iris.
|
||||
|
||||
Reference in New Issue
Block a user