Integrate native pixel shadows over beta.126 for beta.127
Build Sanctuary / build (push) Canceled after 0s
Build Sanctuary / build (push) Canceled after 0s
This commit is contained in:
@@ -20,15 +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 release est publiée
|
||||
et l’instance **Sanctuary Beta** synchronisée, sauvegardes et réglages conservés.
|
||||
Packs normal/Test et template sont aussi dans le dossier `build/`.
|
||||
La version **beta.127** intègre les ombres portées pixélisées au shader natif
|
||||
**Sanctuary · Vanilla Light**. Elles s'activent dans **Options → Shaders**, avec
|
||||
finesse et portée réglables. Le [ticket de livraison](docs/shader-beta127.md)
|
||||
décrit les réglages, les limites et les vérifications. Les ajouts de
|
||||
[beta.126](docs/gems-beta126.md) — gemmes, argile spéciale et commandes de
|
||||
découverte — sont conservés.
|
||||
La [livraison précédente](docs/clay-workshop-integration-beta110.md) réunit déjà
|
||||
l’atelier, les œufs et la neige saisonnière dans l’instance **Sanctuary Beta**.
|
||||
Les [synchronisations antérieures](docs/git-sync-beta092.md) restent documentées.
|
||||
@@ -39,6 +36,14 @@ La [carte native beta.005](docs/atlas-beta005.md) prend le relais après
|
||||
l’étude de Xaero et des alternatives libres.
|
||||
|
||||
|
||||
## 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,
|
||||
|
||||
@@ -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,73 @@
|
||||
# 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` |
|
||||
@@ -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.127
|
||||
pack_version=beta.127
|
||||
resource_pack_version=beta.121
|
||||
maven_group=fr.koka.sanctuary
|
||||
jei_version=30.32.0-sanctuary.3
|
||||
|
||||
@@ -93,6 +93,10 @@ tasks.named('runGameTest') {
|
||||
}
|
||||
|
||||
tasks.named('processGametestResources') {
|
||||
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') }
|
||||
|
||||
+477
@@ -0,0 +1,477 @@
|
||||
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 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");
|
||||
var level = s.overworld();
|
||||
// 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.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);
|
||||
|
||||
// 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, 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, 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");
|
||||
|
||||
server.runOnServer(s -> command(s, "time set midnight"));
|
||||
c.waitFor(m -> PixelShadows.sunDirection(m.gameRenderer.gameRenderState().levelRenderState.skyRenderState.sunAngle).y < 0,
|
||||
200);
|
||||
c.waitTicks(10);
|
||||
int nightFrames = c.computeOnClient(m -> {
|
||||
check(VanillaLight.pixelShadows() && !PixelShadows.applied(), "Enabled sun shadows are inactive at midnight");
|
||||
return PixelShadows.renderedFrames();
|
||||
});
|
||||
var nightOn = capture(c, "midnight-on");
|
||||
c.waitTicks(6);
|
||||
c.runOnClient(m -> check(PixelShadows.renderedFrames() == nightFrames, "Midnight submits no sun-shadow frames"));
|
||||
shadows(c, false);
|
||||
unchanged(nightOn, capture(c, "midnight-off"), "Midnight ON/OFF image is unchanged");
|
||||
|
||||
server.runOnServer(s -> command(s, "time set 3000"));
|
||||
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");
|
||||
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"}) {
|
||||
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-beta122-shadows-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; midnight idle; 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 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 checkPreferences() {
|
||||
VanillaLight.setEnabled(true);
|
||||
VanillaLight.setSaturation(100);
|
||||
for (int[] pair : new int[][]{{Integer.MIN_VALUE, 8}, {11, 8}, {12, 16}, {16, 16}, {23, 16}, {24, 32}, {Integer.MAX_VALUE, 32}}) {
|
||||
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}, {Integer.MAX_VALUE, 64}}) {
|
||||
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);
|
||||
}
|
||||
|
||||
private 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);
|
||||
}
|
||||
}
|
||||
|
||||
private static void camera(ClientGameTestContext c, TestServerContext server, double x, double y, double z, int fov) {
|
||||
server.runOnServer(s -> {
|
||||
var p = s.getPlayerList().getPlayers().getFirst();
|
||||
p.teleportTo(x, y, z);
|
||||
p.setDeltaMovement(Vec3.ZERO);
|
||||
});
|
||||
c.waitFor(m -> m.player.position().distanceToSqr(new Vec3(x, y, z)) < .0001, 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);
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
private 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);
|
||||
}
|
||||
}
|
||||
|
||||
private 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"));
|
||||
}
|
||||
}
|
||||
|
||||
private 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);
|
||||
}
|
||||
|
||||
private static Frame capture(ClientGameTestContext c, String name) {
|
||||
if (name != null) c.takeScreenshot("sanctuary-beta122-shadows-" + 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();
|
||||
}
|
||||
|
||||
private 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);
|
||||
}
|
||||
|
||||
private 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;
|
||||
}
|
||||
|
||||
private static int brightness(int color) {
|
||||
return ((color >> 16 & 255) + (color >> 8 & 255) + (color & 255)) / 3;
|
||||
}
|
||||
|
||||
private 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); }
|
||||
private static void check(boolean ok, String message) { if (!ok) throw new AssertionError(message); }
|
||||
}
|
||||
@@ -19,6 +19,7 @@ public final class SanctuaryKnowledgeClient implements ClientModInitializer {
|
||||
public static void registerClient() {
|
||||
SanctuaryResourcePack.register();
|
||||
VortexCloudRenderer.register();
|
||||
fr.koka.sanctuary.client.shader.PixelShadows.register();
|
||||
ProgressionClient.register();
|
||||
StarClient.register();
|
||||
WeatherClient.register();
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
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;
|
||||
|
||||
/** Native sun 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)
|
||||
.withPrimitiveTopology(PrimitiveTopology.QUADS).withCull(false)
|
||||
.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).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 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);
|
||||
if (sky.skybox != DimensionType.Skybox.OVERWORLD || camera.fogType != FogType.NONE || sun.y <= .08f) return;
|
||||
float strength = .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 sun shadows", size, size, null, GpuFormat.D32_FLOAT);
|
||||
}
|
||||
if (uniforms == null) uniforms = new MappableRingBuffer(() -> "Sanctuary pixel shadows",
|
||||
GpuBuffer.USAGE_UNIFORM | GpuBuffer.USAGE_MAP_WRITE, 192);
|
||||
double ox = Math.floor(camera.pos.x), oy = Math.floor(camera.pos.y), oz = Math.floor(camera.pos.z);
|
||||
boolean zeroToOne = RenderSystem.getDevice().getDeviceInfo().isZZeroToOne();
|
||||
var light = lightMatrix(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))));
|
||||
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 sun depth")
|
||||
.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.draw(3, 1, 0, 0);
|
||||
}
|
||||
renderedFrames++;
|
||||
applied = true;
|
||||
}
|
||||
|
||||
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 + 8) * pixels && size < 4096) size *= 2;
|
||||
return size;
|
||||
}
|
||||
static Matrix4f lightMatrix(Vector3f sun, int distance, int size, double x, double y, double z, boolean zeroToOne) {
|
||||
float extent = distance + 8, reach = 4 * (distance + 16);
|
||||
// A fixed up axis is nonsingular even at noon. Snap the light-space origin to texels.
|
||||
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;
|
||||
double sx = view.m00() * x + view.m10() * y + view.m20() * z;
|
||||
double sy = view.m01() * x + view.m11() * y + view.m21() * z;
|
||||
var result = new Matrix4f().setOrtho(-extent, extent, -extent, extent, .1f, reach * 2, zeroToOne).mul(view);
|
||||
result.m30(result.m30() + (float)((sx - Math.rint(sx / texel) * texel) / extent));
|
||||
result.m31(result.m31() + (float)((sy - Math.rint(sy / texel) * texel) / extent));
|
||||
return result;
|
||||
}
|
||||
public static void release() {
|
||||
TERRAIN.close();
|
||||
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; }
|
||||
}
|
||||
@@ -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,14 @@ 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(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_pixels",OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders.shadow_pixels_help")),(caption,value)->Component.translatable("sanctuary.shaders.shadow_pixels_value",value),
|
||||
new OptionInstance.Enum<>(List.of(8,16,32),Codec.INT),VanillaLight.shadowPixels(),VanillaLight::setShadowPixels));
|
||||
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),Codec.INT),VanillaLight.shadowDistance(),VanillaLight::setShadowDistance));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,512 @@
|
||||
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.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 DefaultVertexFormat.POSITION (three floats), 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; current shader options use 16, 32 and 64. */
|
||||
public static final int MAX_DISTANCE = 128;
|
||||
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 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;
|
||||
reconcile();
|
||||
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);
|
||||
}
|
||||
|
||||
/** Coordinates are chunk coordinates. Caller also invalidates adjacent chunks as needed. */
|
||||
public void dirtyChunk(int cx, int cz) {
|
||||
RenderSystem.assertOnRenderThread();
|
||||
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;
|
||||
Shape shape = shape(build.section.storage.getBlockState(x, y, z));
|
||||
if (shape != null) {
|
||||
if (shape.boxes == null) {
|
||||
build.oversized = true;
|
||||
return true;
|
||||
}
|
||||
int hidden = 0;
|
||||
for (Direction direction : DIRECTIONS) {
|
||||
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);
|
||||
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.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();
|
||||
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) {
|
||||
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); out.addVertex(x1, y0, z0);
|
||||
out.addVertex(x1, y0, z1); out.addVertex(x0, y0, z1);
|
||||
}
|
||||
case UP -> {
|
||||
out.addVertex(x0, y1, z0); out.addVertex(x0, y1, z1);
|
||||
out.addVertex(x1, y1, z1); out.addVertex(x1, y1, z0);
|
||||
}
|
||||
case NORTH -> {
|
||||
out.addVertex(x0, y0, z0); out.addVertex(x0, y1, z0);
|
||||
out.addVertex(x1, y1, z0); out.addVertex(x1, y0, z0);
|
||||
}
|
||||
case SOUTH -> {
|
||||
out.addVertex(x0, y0, z1); out.addVertex(x1, y0, z1);
|
||||
out.addVertex(x1, y1, z1); out.addVertex(x0, y1, z1);
|
||||
}
|
||||
case WEST -> {
|
||||
out.addVertex(x0, y0, z0); out.addVertex(x0, y0, z1);
|
||||
out.addVertex(x0, y1, z1); out.addVertex(x0, y1, z0);
|
||||
}
|
||||
case EAST -> {
|
||||
out.addVertex(x1, y0, z0); out.addVertex(x1, y1, z0);
|
||||
out.addVertex(x1, y1, z1); out.addVertex(x1, y0, z1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 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);
|
||||
int cursor, quads;
|
||||
boolean oversized;
|
||||
|
||||
Build(Section section) {
|
||||
this.section = section;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
bytes.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,13 @@ 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=false;
|
||||
int shadowPixels=16;
|
||||
int shadowDistance=32;
|
||||
}
|
||||
private static Path path(){return FabricLoader.getInstance().getConfigDir().resolve("sanctuary-shaders.json");}
|
||||
public static boolean enabled(){
|
||||
if(preferences==null){
|
||||
@@ -22,17 +28,30 @@ public final class VanillaLight {
|
||||
catch(Exception e){SanctuaryMod.LOGGER.warn("Cannot read Sanctuary shader preference",e);}
|
||||
if(preferences==null)preferences=new Preferences();
|
||||
preferences.saturation=Math.clamp(preferences.saturation,0,200);
|
||||
preferences.shadowPixels=normalizeShadowValue(preferences.shadowPixels,8,16,32);
|
||||
preferences.shadowDistance=normalizeShadowValue(preferences.shadowDistance,16,32,64);
|
||||
}
|
||||
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 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=normalizeShadowValue(value,8,16,32);save();}
|
||||
public static void setShadowDistance(int value){enabled();preferences.shadowDistance=normalizeShadowValue(value,16,32,64);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 normalizeShadowValue(int value,int low,int middle,int high){
|
||||
if(value<(low+middle)/2)return low;
|
||||
return value<(middle+high)/2?middle:high;
|
||||
}
|
||||
private static void save(){
|
||||
try{
|
||||
Files.createDirectories(path().getParent());var temporary=path().resolveSibling(path().getFileName()+".tmp");
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package fr.koka.sanctuary.mixin.client;
|
||||
|
||||
import fr.koka.sanctuary.client.shader.PixelShadows;
|
||||
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);
|
||||
}
|
||||
@Inject(method="allChanged", at=@At("HEAD"))
|
||||
private void sanctuary$reload(CallbackInfo ci) { PixelShadows.release(); }
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package fr.koka.sanctuary.mixin.client;
|
||||
|
||||
import fr.koka.sanctuary.client.shader.PixelShadows;
|
||||
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);
|
||||
}
|
||||
|
||||
@Inject(method="onChunkLoaded", at=@At("TAIL"))
|
||||
private void sanctuary$loaded(ChunkPos pos, CallbackInfo ci) { PixelShadows.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()); }
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
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 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);
|
||||
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(); }
|
||||
@Inject(method={"close", "setLevel"}, at=@At("HEAD"))
|
||||
private void sanctuary$close(CallbackInfo ci) { PixelShadows.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,13 @@
|
||||
"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.pixel_shadows": "Pixel shadows",
|
||||
"sanctuary.shaders.pixel_shadows_help": "Sun shadows cast by solid blocks, aligned to a pixel grid. Disabled by default. Requires Vanilla Light; gameplay light levels stay unchanged.",
|
||||
"sanctuary.shaders.shadow_pixels": "Shadow precision",
|
||||
"sanctuary.shaders.shadow_pixels_value": "%s pixels/block",
|
||||
"sanctuary.shaders.shadow_pixels_help": "Grid precision: 8, 16 or 32 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 or 64 blocks. Higher distances cost more performance. Requires Vanilla Light and pixel shadows."
|
||||
}
|
||||
|
||||
@@ -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,13 @@
|
||||
"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.pixel_shadows": "Ombres pixelisées",
|
||||
"sanctuary.shaders.pixel_shadows_help": "Ombres du soleil projetées par les blocs solides, alignées sur une grille de pixels. Désactivées par défaut. Nécessite Vanilla Light ; ne change pas la lumière du jeu.",
|
||||
"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 ou 32 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 ou 64 blocs. Une distance plus élevée coûte davantage en performances. Nécessite Vanilla Light et les ombres pixelisées."
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
#version 330
|
||||
#extension GL_ARB_separate_shader_objects : require
|
||||
void main() { }
|
||||
@@ -0,0 +1,7 @@
|
||||
#version 330
|
||||
#extension GL_ARB_separate_shader_objects : require
|
||||
#include <minecraft:dynamictransforms.glsl>
|
||||
layout(location = 0) in vec3 Position;
|
||||
void main() {
|
||||
gl_Position = ModelViewMat * vec4(Position, 1.0);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
#version 330
|
||||
#extension GL_ARB_separate_shader_objects : require
|
||||
uniform sampler2D WorldDepth;
|
||||
uniform sampler2D SunDepth;
|
||||
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);
|
||||
}
|
||||
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;
|
||||
vec3 p = relative + CameraFractionPixels.xyz;
|
||||
float pixels = CameraFractionPixels.w;
|
||||
vec3 snapped = (floor(p * pixels) + 0.5) / pixels;
|
||||
vec3 q = 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;
|
||||
float blocker = texture(SunDepth, coord.xy).r;
|
||||
float shade = coord.z > blocker + 0.00015 ? 1.0 : 0.0;
|
||||
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));
|
||||
}
|
||||
@@ -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.127"
|
||||
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