Compare commits

...
2 Commits
Author SHA1 Message Date
koka a4e96c0d99 Stabilize pixel shadows and add moonlight and foliage for beta.128
Build Sanctuary / build (push) Canceled after 0s
2026-09-17 18:33:07 +02:00
koka 7c63865157 Document verified beta.127 shader release and instance synchronization
Build Sanctuary / build (push) Canceled after 0s
2026-09-17 17:53:16 +02:00
14 changed files with 431 additions and 62 deletions
+12 -6
View File
@@ -20,12 +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.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 version **beta.128** stabilise les ombres pixélisées, atténue le soleil au
zénith et ajoute les ombres légères de la lune et le feuillage irrégulier.
**Options → Shaders → Sanctuary · Vanilla Light** : les réglages de finesse et
de portée sont conservés. Le [ticket de livraison](docs/shader-beta128.md)
décrit le résultat et ses vérifications. Les gemmes, sculptures et découvertes
de beta.126 restent inchangées.
La [livraison précédente](docs/clay-workshop-integration-beta110.md) réunit déjà
latelier, les œufs et la neige saisonnière dans linstance **Sanctuary Beta**.
Les [synchronisations antérieures](docs/git-sync-beta092.md) restent documentées.
@@ -36,6 +36,12 @@ La [carte native beta.005](docs/atlas-beta005.md) prend le relais après
l’étude de Xaero et des alternatives libres.
## 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).
## 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.
+11
View File
@@ -14,6 +14,17 @@ 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.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
+15
View File
@@ -71,3 +71,18 @@ exactement les ressources du mod. Aucune fixture de test ni monde embarqué.
| `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`.
+92
View File
@@ -0,0 +1,92 @@
# 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` |
+2 -2
View File
@@ -9,8 +9,8 @@ loom_version=1.17.20
fabric_api_version=0.160.5+26.3
# Release counter: beta.001, .002, .003, ... (see docs/versioning.md).
mod_version=beta.127
pack_version=beta.127
mod_version=beta.128
pack_version=beta.128
resource_pack_version=beta.121
maven_group=fr.koka.sanctuary
jei_version=30.32.0-sanctuary.3
@@ -100,7 +100,13 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
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());
@@ -185,21 +191,27 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
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"));
// 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);
unchanged(nightOn, capture(c, "midnight-off"), "Midnight ON/OFF image is unchanged");
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);
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);
@@ -234,12 +246,12 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
});
});
c.waitTicks(3);
c.takeScreenshot("sanctuary-beta122-shadows-options-" + locale);
c.takeScreenshot("sanctuary-beta128-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");
+ "offscreen caster; live removal/replacement; subtle moon; noon attenuation; foliage density; camera stability; resource reload; FR/EN screenshots");
} finally {
onboarding.set(false);
c.runOnClient(m -> {
@@ -258,6 +270,140 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
}
}
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 > .04 && noonStats.maximum < .14,
"Noon shadow is present but attenuated: " + 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 < .070) 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
@@ -299,10 +445,11 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
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);
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, 200);
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);
@@ -314,6 +461,16 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
c.waitTicks(15);
}
private 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() + " " + 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),
@@ -352,7 +509,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
}
private static Frame capture(ClientGameTestContext c, String name) {
if (name != null) c.takeScreenshot("sanctuary-beta122-shadows-" + name);
if (name != null) c.takeScreenshot("sanctuary-beta128-shadows-" + name);
var result = new CompletableFuture<Frame>();
c.runOnClient(m -> Screenshot.takeScreenshot(m.gameRenderer.mainRenderTarget(), image -> {
try (image) {
@@ -24,16 +24,18 @@ 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 depth map and world-grid receivers. Never reads GPU pixels or changes gameplay light. */
/** 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)
.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"))
@@ -42,7 +44,8 @@ public final class PixelShadows {
.withBindGroupLayout(BindGroupLayout.builder()
.withUniform("PixelShadows", UniformType.UNIFORM_BUFFER)
.withUniform("WorldDepth", UniformType.COMBINED_IMAGE_SAMPLER)
.withUniform("SunDepth", UniformType.COMBINED_IMAGE_SAMPLER).build())
.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());
@@ -79,19 +82,23 @@ public final class PixelShadows {
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 strength = .38f * Math.clamp((sun.y - .08f) / .16f, 0, 1) * sky.rainBrightness;
float moon = sky.moonPhase == null ? 1 : (float)(.5 + .5 * Math.cos(sky.moonPhase.index() * Math.PI / 4));
float strength = (lunar ? .10f * moon : .38f * zenithFade(sun.y))
* 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);
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 = Math.floor(camera.pos.x), oy = Math.floor(camera.pos.y), oz = Math.floor(camera.pos.z);
double ox = anchor(camera.pos.x), oy = anchor(camera.pos.y), oz = anchor(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();
@@ -109,13 +116,18 @@ public final class PixelShadows {
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))));
.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 sun depth")
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());
@@ -133,20 +145,28 @@ public final class PixelShadows {
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;
}
/** Smoothly soften overhead sunlight, without a noon on/off discontinuity. */
static float zenithFade(float elevation) {
float t = Math.clamp((elevation - .92f) / .08f, 0, 1);
return 1 - .75f * t * t * (3 - 2 * t);
}
static double anchor(double coordinate) { return Math.floor(coordinate / 16.0 + .5) * 16.0; }
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;
while (size < 2 * (distance + 16) * 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);
float extent = distance + 16, 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;
@@ -14,6 +14,7 @@ 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;
@@ -28,7 +29,7 @@ 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
* 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.
*
@@ -268,7 +269,9 @@ public final class ShadowTerrain implements AutoCloseable {
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));
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;
@@ -276,6 +279,7 @@ public final class ShadowTerrain implements AutoCloseable {
}
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);
@@ -290,7 +294,7 @@ public final class ShadowTerrain implements AutoCloseable {
build.oversized = true;
return true;
}
face(build.vertices, box, x, y, z, direction);
face(build.vertices, box, x, y, z, direction, foliage ? 0xFFFFFFFF : 0xFF000000);
build.quads++;
}
}
@@ -314,7 +318,7 @@ public final class ShadowTerrain implements AutoCloseable {
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;
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.
@@ -426,34 +430,34 @@ public final class ShadowTerrain implements AutoCloseable {
}
/** 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) {
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); out.addVertex(x1, y0, z0);
out.addVertex(x1, y0, z1); out.addVertex(x0, y0, z1);
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); out.addVertex(x0, y1, z1);
out.addVertex(x1, y1, z1); out.addVertex(x1, y1, z0);
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); out.addVertex(x0, y1, z0);
out.addVertex(x1, y1, z0); out.addVertex(x1, y0, z0);
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); out.addVertex(x1, y0, z1);
out.addVertex(x1, y1, z1); out.addVertex(x0, y1, z1);
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); out.addVertex(x0, y0, z1);
out.addVertex(x0, y1, z1); out.addVertex(x0, y1, z0);
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); out.addVertex(x1, y1, z0);
out.addVertex(x1, y1, z1); out.addVertex(x1, y0, z1);
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);
}
}
}
@@ -470,6 +474,7 @@ public final class ShadowTerrain implements AutoCloseable {
}
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;
@@ -496,7 +501,7 @@ public final class ShadowTerrain implements AutoCloseable {
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);
final BufferBuilder vertices = new BufferBuilder(bytes, PrimitiveTopology.QUADS, DefaultVertexFormat.POSITION_COLOR);
int cursor, quads;
boolean oversized;
@@ -2258,7 +2258,7 @@
"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.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.pixel_shadows_help": "Sun shadows and subtle moon shadows on a pixel grid. Dappled foliage and softer shadows at noon. Disabled by default; 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 or 32 pixels per block. Higher values make shadow pixels finer. Requires Vanilla Light and pixel shadows.",
@@ -2258,7 +2258,7 @@
"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.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.pixel_shadows_help": "Ombres du soleil et ombres légères de la lune, sur une grille de pixels. Feuillage irrégulier et ombres atténuées à midi. Désactivées par défaut ; 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 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.",
@@ -1,3 +1,18 @@
#version 330
#extension GL_ARB_separate_shader_objects : require
void main() { }
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);
}
@@ -2,6 +2,11 @@
#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;
}
@@ -2,6 +2,7 @@
#extension GL_ARB_separate_shader_objects : require
uniform sampler2D WorldDepth;
uniform sampler2D SunDepth;
uniform sampler2D SunMaterial;
layout(std140) uniform PixelShadows {
mat4 InverseViewProjection;
mat4 LightMatrix;
@@ -15,6 +16,31 @@ 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;
@@ -24,18 +50,35 @@ void main() {
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.5) / pixels;
vec3 q = snapped + normal * dot(p - snapped, normal);
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;
float blocker = texture(SunDepth, coord.xy).r;
float shade = coord.z > blocker + 0.00015 ? 1.0 : 0.0;
// 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));
+1 -1
View File
@@ -1,6 +1,6 @@
name = "Sanctuary"
author = "KOKA99CAB"
version = "beta.127"
version = "beta.128"
pack-format = "packwiz:1.1.0"
[index]