Add native solar volume rendering for beta.137
Build Sanctuary / build (push) Canceled after 0s

This commit is contained in:
koka
2026-09-18 01:00:20 +02:00
parent 0781cbd964
commit 2d7072b8a8
22 changed files with 367 additions and 26 deletions
+12 -5
View File
@@ -20,11 +20,11 @@ La [vision complète](docs/vision.md) conserve les intentions ; le
L’économie, les claims et les dimensions décrits dans la vision restent L’économie, les claims et les dimensions décrits dans la vision restent
à implémenter. à implémenter.
La version **beta.136** ajoute un **lens flare rectangulaire doux** pour le La version **beta.137** ajoute des **rayons solaires volumétriques** qui traversent
soleil et la lune, activé à 30 % et réglable indépendamment du bloom. les ouvertures du décor et le feuillage. Activés à 35 %, réglables indépendamment
**Options → Shaders** ; les blocs masquent les astres et leurs reflets. du bloom dans **Options → Shaders**. Carte d'ombres partagée, portée de 64 blocs
Le [ticket de livraison](docs/shader-beta136.md) décrit le rendu et ses vérifications. maximum. Le [ticket](docs/shader-beta137.md) détaille les vérifications.
Release publiée et même instance Prism synchronisée ; sauvegardes et réglages conservés. Livraison en cours de vérification.
La [livraison précédente](docs/clay-workshop-integration-beta110.md) réunit déjà 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**. latelier, les œufs et la neige saisonnière dans linstance **Sanctuary Beta**.
Les [synchronisations antérieures](docs/git-sync-beta092.md) restent documentées. Les [synchronisations antérieures](docs/git-sync-beta092.md) restent documentées.
@@ -35,6 +35,13 @@ La [carte native beta.005](docs/atlas-beta005.md) prend le relais après
l’étude de Xaero et des alternatives libres. l’étude de Xaero et des alternatives libres.
## beta.137 — Rayons solaires
Faisceaux volumétriques légers, intensité réglable et feuillage perméable.
[Contrat et vérifications](docs/shader-beta137.md) ·
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.137/Sanctuary-beta.137.mrpack) ·
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.137/Sanctuary-Test-beta.137.mrpack).
## beta.136 — Lens flare rectangulaire ## beta.136 — Lens flare rectangulaire
Soleil et lune, reflets doux et intensité indépendante du bloom. Soleil et lune, reflets doux et intensité indépendante du bloom.
+81
View File
@@ -0,0 +1,81 @@
# SHADER-137 — Rayons solaires volumétriques
Demande validée : faisceaux légers découpés par les blocs, carte d'ombres partagée,
feuillage perméable, rendu natif préservé, option et intensité indépendantes du bloom.
Socle beta.136, branche `codex/sunshafts-beta137`, Minecraft 26.3 / Java 25.
Aucun changement de monde ni de gameplay.
## Contrat
Soleil uniquement ; activé initialement à 35 %, intensité 0100.
Intégration de 16 segments par pixel à un quart de chaque dimension.
Reconstruction guidée par la profondeur, sans historique ni bruit animé.
Réutilisation du masque de feuillage déjà stable dans les ombres.
La carte solaire peut fonctionner sans afficher les ombres sur les surfaces.
Portée bornée à 64 blocs et à la moitié de la portée des ombres.
Atténuation aux limites de la carte, à l'horizon et avec la météo.
## Rendu et coût borné
Deux passes : intégration dans une cible RGBA16F de largeur et hauteur divisées
par quatre, puis reconstruction guidée par la profondeur. Seize segments
quadratiques privilégient les premiers mètres ; quatre lectures fixes de la
carte d'ombres par segment adoucissent seulement la lumière dans l'air.
La dernière portion du rayon rétrécit continûment à l'approche d'une surface.
La lumière utilise une teinte solaire chaude mélangée au brouillard natif.
La carte d'ombres et ses maillages sont partagés, y compris les trous de
transmission déterministes des feuilles. Aucune copie d'image plein écran,
aucune duplication du terrain, aucune lecture GPU côté CPU ni accumulation
entre images dans le rendu de production. OFF/zéro libèrent le volume ; la
carte d'ombres reste seulement si les ombres de surface sont activées.
Le soleil sous l'horizon et la submersion suspendent les rayons.
## Vérifications
Scénario natif réussi en **1 min 41 s** sur macOS / Apple M1 / OpenGL : pièce
entièrement fermée, toit ajouré OFF/35/100, feuilles puis ciel ouvert, déplacement
de quatre blocs et retour, caméra proche d'un mur à 0,5 puis 0,125 bloc,
soleil hors champ, nuit, OFF/zéro, carte d'ombres partagée, effet combiné,
redimensionnement à 961 × 541 et rechargement des ressources. Préférences
vierges/partielles/personnelles, bornage et libellés FR/EN également vérifiés.
Log : `build/shader137-client-final.log`, marqueur `SUNSHAFT137_PASS`.
Comparatif autonome : `build/Shader-beta.137-comparaison.html`.
La première tentative s'est arrêtée sur une comparaison de gain brut entre
feuillage et ciel ouvert. La composition dispose de moins de marge sur un mur
déjà clair : le test compare maintenant la contribution normalisée sur les mêmes
surfaces, et les feuilles du scénario sont persistantes. Aucun seuil du rendu
n'a été assoupli pour faire passer ce contrôle.
Les contrôles natifs existants du bloom, des vingt textures de minerais émissifs
et du lens flare réussissent également en **1 min 32 s**, avec leurs seuils
inchangés. Log : `build/shader137-regression.log`, marqueurs `EMISSIVE131_PASS`
et `LENS_FLARE136_PASS`. Les autres effets sont explicitement isolés dans les
scénarios qui nécessitent une image témoin sans rayons.
## Limites
Le volume utilise les formes présentes dans la carte d'ombres existante :
il n'ajoute pas de projection d'ombre des entités ou des nuages. Le feuillage
reste une approximation par transmission déterministe. Portée maximale de
64 blocs, atténuée à son extrémité. Le faible nombre de segments et la résolution
réduite peuvent lisser les ouvertures très fines. Aucun résultat Windows/Vulkan
ni mesure de FPS revendiqués.
## Construction et archives
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
réussit en **2 min 27 s**, 126 tâches. Le serveur GameTest dédié reste exclu
conformément au refus EULA antérieur. Log : `build/shader137-build.log`.
Comparaison à beta.136 : **11 entrées de production** modifiées, uniquement
le rendu, ses préférences et ses libellés. Gameplay et textures des gemmes
strictement conservés. JAR normal/Test identique, sources et template vérifiés.
Reçu local : `build/shader137-artifact.json`.
- `Sanctuary-beta.137.mrpack` : 10431723 octets ; SHA-256 `6684d1dbe48cd3031b373a03d4380519fdbf5d841e1c7cb5e290dec63b5ba9f6`.
- `Sanctuary-Test-beta.137.mrpack` : 10450650 octets ; SHA-256 `32b0cee60152b6a5c8f31b248391c79a5fdf47b649de74902b066457ad4785ef`.
JAR : `f1fbc106b4b048753f9a669af37e9c12f92168af486d4072f514e86920c81faa`.
+2 -2
View File
@@ -9,8 +9,8 @@ loom_version=1.17.20
fabric_api_version=0.160.5+26.3 fabric_api_version=0.160.5+26.3
# Release counter: beta.001, .002, .003, ... (see docs/versioning.md). # Release counter: beta.001, .002, .003, ... (see docs/versioning.md).
mod_version=beta.136 mod_version=beta.137
pack_version=beta.136 pack_version=beta.137
resource_pack_version=beta.121 resource_pack_version=beta.121
maven_group=fr.koka.sanctuary maven_group=fr.koka.sanctuary
jei_version=30.32.0-sanctuary.3 jei_version=30.32.0-sanctuary.3
+3
View File
@@ -103,6 +103,9 @@ tasks.named('processGametestResources') {
inputs.property('sanctuarySsgiCloseup135ClientTests', closeup135Client) inputs.property('sanctuarySsgiCloseup135ClientTests', closeup135Client)
if (closeup135Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.shader.SsgiCloseup135ClientChecks') } if (closeup135Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.shader.SsgiCloseup135ClientChecks') }
def sunshaft137Client = providers.gradleProperty('sanctuarySunshaft137ClientTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('sanctuarySunshaft137ClientTests', sunshaft137Client)
if (sunshaft137Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.shader.Sunshafts137ClientChecks') }
def bloom130Client = providers.gradleProperty('sanctuaryBloom130ClientTests').map { it.toBoolean() }.getOrElse(false) def bloom130Client = providers.gradleProperty('sanctuaryBloom130ClientTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('sanctuaryBloom130ClientTests', bloom130Client) inputs.property('sanctuaryBloom130ClientTests', bloom130Client)
if (bloom130Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.shader.Bloom130ClientChecks') } if (bloom130Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.shader.Bloom130ClientChecks') }
@@ -23,7 +23,7 @@ public final class Bloom130ClientChecks implements FabricClientGameTest {
String ns=name.equals("ruby")||name.equals("sapphire")?"sanctuary:":"minecraft:";result.add(ns+name+"_ore");result.add(ns+"deepslate_"+name+"_ore");} String ns=name.equals("ruby")||name.equals("sapphire")?"sanctuary:":"minecraft:";result.add(ns+name+"_ore");result.add(ns+"deepslate_"+name+"_ore");}
result.add("minecraft:nether_quartz_ore");result.add("minecraft:nether_gold_ore");return List.copyOf(result);} result.add("minecraft:nether_quartz_ore");result.add("minecraft:nether_gold_ore");return List.copyOf(result);}
static void run(ClientGameTestContext c,TestServerContext server){ static void run(ClientGameTestContext c,TestServerContext server){
c.runOnClient(m->{preferences();VanillaLight.setLensFlare(false);VanillaLight.setPbr(false);VanillaLight.setSsgi(false);analyseTextures();VanillaLight.setSaturation(100);VanillaLight.setPixelShadows(false);VanillaLight.setEdgeHighlights(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);}); c.runOnClient(m->{preferences();VanillaLight.setSunshafts(false);VanillaLight.setLensFlare(false);VanillaLight.setPbr(false);VanillaLight.setSsgi(false);analyseTextures();VanillaLight.setSaturation(100);VanillaLight.setPixelShadows(false);VanillaLight.setEdgeHighlights(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);});
server.runOnServer(s->{command(s,"time set 18000"); server.runOnServer(s->{command(s,"time set 18000");
for(int x=4;x<=20;x++)for(int z=4;z<=20;z++){ for(int x=4;x<=20;x++)for(int z=4;z<=20;z++){
s.overworld().setBlockAndUpdate(new BlockPos(x,64,z),Blocks.CONCRETE.black().defaultBlockState()); s.overworld().setBlockAndUpdate(new BlockPos(x,64,z),Blocks.CONCRETE.black().defaultBlockState());
@@ -160,7 +160,7 @@ final class EdgeHighlights129ClientChecks {
check(VanillaLight.edgeHighlights(),"Toggle persists independently"); check(VanillaLight.edgeHighlights(),"Toggle persists independently");
VanillaLight.setEdgeHighlights(false); VanillaLight.setEdgeHighlights(false);
VanillaLight.setBloom(false);VanillaLight.setOreEmission(false); VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);
VanillaLight.setLensFlare(false); VanillaLight.setSunshafts(false);VanillaLight.setLensFlare(false);
VanillaLight.setPbr(false);VanillaLight.setSsgi(false); VanillaLight.setPbr(false);VanillaLight.setSsgi(false);
} catch(java.io.IOException e) { throw new AssertionError(e); } } catch(java.io.IOException e) { throw new AssertionError(e); }
} }
@@ -12,7 +12,7 @@ import net.minecraft.network.chat.Component;
/** Native sky image, not a generated mockup; explicit independent effect controls. */ /** Native sky image, not a generated mockup; explicit independent effect controls. */
final class LensFlare136ClientChecks { final class LensFlare136ClientChecks {
static void run(ClientGameTestContext c,TestServerContext server){ static void run(ClientGameTestContext c,TestServerContext server){
c.runOnClient(m->{preferences();VanillaLight.setEnabled(true);VanillaLight.setSaturation(100);VanillaLight.setPbr(false);VanillaLight.setSsgi(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);VanillaLight.setPixelShadows(false);VanillaLight.setEdgeHighlights(false);VanillaLight.setLensFlare(false);}); c.runOnClient(m->{preferences();VanillaLight.setEnabled(true);VanillaLight.setSaturation(100);VanillaLight.setPbr(false);VanillaLight.setSsgi(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);VanillaLight.setPixelShadows(false);VanillaLight.setEdgeHighlights(false);VanillaLight.setSunshafts(false);VanillaLight.setLensFlare(false);});
server.runOnServer(s->{command(s,"time add 1");command(s,"time set 3000");}); server.runOnServer(s->{command(s,"time add 1");command(s,"time set 3000");});
camera(c,server,100.5,130,100.5,70);aim(c,server,false); camera(c,server,100.5,130,100.5,70);aim(c,server,false);
var off=capture(c,"flare-sun-off");enable(c,true);var on=capture(c,"flare-sun-30"); var off=capture(c,"flare-sun-off");enable(c,true);var on=capture(c,"flare-sun-30");
@@ -43,7 +43,7 @@ final class LensFlare136ClientChecks {
enable(c,false);var newOff=capture(c,null);enable(c,true);check(gain(newOff,capture(c,"flare-new-moon"))<moon*.1,"Dark new moon does not retain full moon flare"); enable(c,false);var newOff=capture(c,null);enable(c,true);check(gain(newOff,capture(c,"flare-new-moon"))<moon*.1,"Dark new moon does not retain full moon flare");
c.runOnClient(m->VanillaLight.setLensFlareIntensity(0));c.waitTicks(8);c.runOnClient(m->check(!Bloom.flareAllocated()&&!Bloom.allocated(),"Zero frees all resources with other effects OFF")); c.runOnClient(m->VanillaLight.setLensFlareIntensity(0));c.waitTicks(8);c.runOnClient(m->check(!Bloom.flareAllocated()&&!Bloom.allocated(),"Zero frees all resources with other effects OFF"));
unchanged(newOff,capture(c,null),"Zero flare restores source image"); unchanged(newOff,capture(c,null),"Zero flare restores source image");
c.runOnClient(m->{VanillaLight.setLensFlareIntensity(30);VanillaLight.setLensFlare(false);}); c.runOnClient(m->{VanillaLight.setLensFlareIntensity(30);VanillaLight.setSunshafts(false);VanillaLight.setLensFlare(false);});
for(String locale:new String[]{"fr_fr","en_us"})c.runOnClient(m->{ for(String locale:new String[]{"fr_fr","en_us"})c.runOnClient(m->{
m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager()); m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());
for(String suffix:new String[]{"lens_flare","lens_flare_help","lens_flare_intensity","lens_flare_intensity_help"}){ for(String suffix:new String[]{"lens_flare","lens_flare_help","lens_flare_intensity","lens_flare_intensity_help"}){
@@ -63,7 +63,7 @@ final class LensFlare136ClientChecks {
private static void preferences(){try{ private static void preferences(){try{
var path=FabricLoader.getInstance().getConfigDir().resolve("sanctuary-shaders.json");Files.writeString(path,"{\"bloom\":false,\"bloomIntensity\":20}");forgetPreferences(); var path=FabricLoader.getInstance().getConfigDir().resolve("sanctuary-shaders.json");Files.writeString(path,"{\"bloom\":false,\"bloomIntensity\":20}");forgetPreferences();
check(VanillaLight.lensFlare()&&VanillaLight.lensFlareIntensity()==30&&!VanillaLight.bloom()&&VanillaLight.bloomIntensity()==20,"Flare defaults preserve bloom choices"); check(VanillaLight.lensFlare()&&VanillaLight.lensFlareIntensity()==30&&!VanillaLight.bloom()&&VanillaLight.bloomIntensity()==20,"Flare defaults preserve bloom choices");
VanillaLight.setLensFlare(false);VanillaLight.setLensFlareIntensity(73);forgetPreferences();check(!VanillaLight.lensFlare()&&VanillaLight.lensFlareIntensity()==73,"Saved custom flare retained"); VanillaLight.setSunshafts(false);VanillaLight.setLensFlare(false);VanillaLight.setLensFlareIntensity(73);forgetPreferences();check(!VanillaLight.lensFlare()&&VanillaLight.lensFlareIntensity()==73,"Saved custom flare retained");
for(int n:new int[]{-10,0,30,100,150}){VanillaLight.setLensFlareIntensity(n);forgetPreferences();check(VanillaLight.lensFlareIntensity()==Math.clamp(n,0,100),"Flare intensity persists and clamps");} for(int n:new int[]{-10,0,30,100,150}){VanillaLight.setLensFlareIntensity(n);forgetPreferences();check(VanillaLight.lensFlareIntensity()==Math.clamp(n,0,100),"Flare intensity persists and clamps");}
VanillaLight.setLensFlareIntensity(30); VanillaLight.setLensFlareIntensity(30);
}catch(java.io.IOException e){throw new AssertionError(e);}} }catch(java.io.IOException e){throw new AssertionError(e);}}
@@ -13,7 +13,7 @@ import java.util.Arrays;
public final class Pbr134ClientChecks implements FabricClientGameTest { public final class Pbr134ClientChecks implements FabricClientGameTest {
public void runTest(ClientGameTestContext c){new PixelShadows122ClientChecks(false,false,true).runTest(c);} public void runTest(ClientGameTestContext c){new PixelShadows122ClientChecks(false,false,true).runTest(c);}
static void run(ClientGameTestContext c,TestServerContext server){ static void run(ClientGameTestContext c,TestServerContext server){
c.runOnClient(m->{preferences();VanillaLight.setLensFlare(false);materials();VanillaLight.setEnabled(true);VanillaLight.setSaturation(100);VanillaLight.setPbr(false);VanillaLight.setSsgi(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);VanillaLight.setEdgeHighlights(false);VanillaLight.setPixelShadows(false);}); c.runOnClient(m->{preferences();VanillaLight.setSunshafts(false);VanillaLight.setLensFlare(false);materials();VanillaLight.setEnabled(true);VanillaLight.setSaturation(100);VanillaLight.setPbr(false);VanillaLight.setSsgi(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);VanillaLight.setEdgeHighlights(false);VanillaLight.setPixelShadows(false);});
server.runOnServer(s->{command(s,"time set 1000");for(int x=-5;x<=24;x++)for(int z=-5;z<=24;z++){ server.runOnServer(s->{command(s,"time set 1000");for(int x=-5;x<=24;x++)for(int z=-5;z<=24;z++){
s.overworld().setBlockAndUpdate(new BlockPos(x,64,z),(x<10?Blocks.BRICKS:x<17?Blocks.OAK_PLANKS:Blocks.IRON_BLOCK).defaultBlockState()); s.overworld().setBlockAndUpdate(new BlockPos(x,64,z),(x<10?Blocks.BRICKS:x<17?Blocks.OAK_PLANKS:Blocks.IRON_BLOCK).defaultBlockState());
for(int y=65;y<=102;y++)s.overworld().setBlockAndUpdate(new BlockPos(x,y,z),Blocks.AIR.defaultBlockState()); for(int y=65;y<=102;y++)s.overworld().setBlockAndUpdate(new BlockPos(x,y,z),Blocks.AIR.defaultBlockState());
@@ -36,12 +36,13 @@ import net.minecraft.world.phys.Vec3;
/** Real terrain/depth-map checks on a new flat seed 122 world; GPU readback is test-only. */ /** Real terrain/depth-map checks on a new flat seed 122 world; GPU readback is test-only. */
public final class PixelShadows122ClientChecks implements FabricClientGameTest { public final class PixelShadows122ClientChecks implements FabricClientGameTest {
private final boolean bloomOnly,ssgiOnly,pbrOnly,closeupOnly; private final boolean bloomOnly,ssgiOnly,pbrOnly,closeupOnly,sunshaftOnly;
public PixelShadows122ClientChecks(){this(false);} public PixelShadows122ClientChecks(){this(false);}
PixelShadows122ClientChecks(boolean bloomOnly){this(bloomOnly,false);} PixelShadows122ClientChecks(boolean bloomOnly){this(bloomOnly,false);}
PixelShadows122ClientChecks(boolean bloomOnly,boolean ssgiOnly){this(bloomOnly,ssgiOnly,false);} PixelShadows122ClientChecks(boolean bloomOnly,boolean ssgiOnly){this(bloomOnly,ssgiOnly,false);}
PixelShadows122ClientChecks(boolean bloomOnly,boolean ssgiOnly,boolean pbrOnly){this(bloomOnly,ssgiOnly,pbrOnly,false);} PixelShadows122ClientChecks(boolean bloomOnly,boolean ssgiOnly,boolean pbrOnly){this(bloomOnly,ssgiOnly,pbrOnly,false);}
PixelShadows122ClientChecks(boolean bloomOnly,boolean ssgiOnly,boolean pbrOnly,boolean closeupOnly){this.bloomOnly=bloomOnly;this.ssgiOnly=ssgiOnly;this.pbrOnly=pbrOnly;this.closeupOnly=closeupOnly;} PixelShadows122ClientChecks(boolean bloomOnly,boolean ssgiOnly,boolean pbrOnly,boolean closeupOnly){this(bloomOnly,ssgiOnly,pbrOnly,closeupOnly,false);}
PixelShadows122ClientChecks(boolean bloomOnly,boolean ssgiOnly,boolean pbrOnly,boolean closeupOnly,boolean sunshaftOnly){this.sunshaftOnly=sunshaftOnly;this.bloomOnly=bloomOnly;this.ssgiOnly=ssgiOnly;this.pbrOnly=pbrOnly;this.closeupOnly=closeupOnly;}
private static final int FLOOR = 64; private static final int FLOOR = 64;
private static final AABB PILLAR = new AABB(0, FLOOR + 1, 0, 2, FLOOR + 9, 2); 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 SLAB = new BlockPos(5, FLOOR + 3, -5);
@@ -136,6 +137,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
c.runOnClient(m -> check(VanillaLight.active(), "Native Sanctuary shader is active without an external engine")); 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); float morning = c.computeOnClient(m -> m.gameRenderer.gameRenderState().levelRenderState.skyRenderState.sunAngle);
if(sunshaftOnly){Sunshafts137ClientChecks.run(c,server);return;}
if(closeupOnly){SsgiCloseup135ClientChecks.run(c,server);Pbr134ClientChecks.run(c,server);return;} if(closeupOnly){SsgiCloseup135ClientChecks.run(c,server);Pbr134ClientChecks.run(c,server);return;}
if(pbrOnly){Pbr134ClientChecks.run(c,server);return;} if(pbrOnly){Pbr134ClientChecks.run(c,server);return;}
if(ssgiOnly){Ssgi132ClientChecks.run(c,server);return;} if(ssgiOnly){Ssgi132ClientChecks.run(c,server);return;}
@@ -266,7 +268,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
}); });
}); });
c.waitTicks(3); c.waitTicks(3);
c.takeScreenshot("sanctuary-beta136-shader-options-" + locale); c.takeScreenshot("sanctuary-beta137-shader-options-" + locale);
} }
c.runOnClient(m -> m.gui.setScreen(null)); c.runOnClient(m -> m.gui.setScreen(null));
} }
@@ -446,7 +448,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
private static void checkPreferences() { private static void checkPreferences() {
checkDefaults135(); checkDefaults135();
VanillaLight.setLensFlare(false); VanillaLight.setSunshafts(false);VanillaLight.setLensFlare(false);
VanillaLight.setPbr(false); VanillaLight.setPbr(false);
VanillaLight.setSsgi(false); VanillaLight.setSsgi(false);
VanillaLight.setBloom(false); VanillaLight.setBloom(false);
@@ -550,7 +552,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
} }
static Frame capture(ClientGameTestContext c, String name) { static Frame capture(ClientGameTestContext c, String name) {
if (name != null) c.takeScreenshot("sanctuary-beta136-shader-" + name); if (name != null) c.takeScreenshot("sanctuary-beta137-shader-" + name);
var result = new CompletableFuture<Frame>(); var result = new CompletableFuture<Frame>();
c.runOnClient(m -> Screenshot.takeScreenshot(m.gameRenderer.mainRenderTarget(), image -> { c.runOnClient(m -> Screenshot.takeScreenshot(m.gameRenderer.mainRenderTarget(), image -> {
try (image) { try (image) {
@@ -13,7 +13,7 @@ import java.nio.file.Files;
public final class Ssgi132ClientChecks implements FabricClientGameTest { public final class Ssgi132ClientChecks implements FabricClientGameTest {
public void runTest(ClientGameTestContext c){new PixelShadows122ClientChecks(false,true).runTest(c);} public void runTest(ClientGameTestContext c){new PixelShadows122ClientChecks(false,true).runTest(c);}
static void run(ClientGameTestContext c,TestServerContext server){ static void run(ClientGameTestContext c,TestServerContext server){
c.runOnClient(m->{preferences();VanillaLight.setLensFlare(false);VanillaLight.setPbr(false);VanillaLight.setEnabled(true);VanillaLight.setSaturation(100);VanillaLight.setSsgi(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);VanillaLight.setPixelShadows(false);VanillaLight.setEdgeHighlights(false);}); c.runOnClient(m->{preferences();VanillaLight.setSunshafts(false);VanillaLight.setLensFlare(false);VanillaLight.setPbr(false);VanillaLight.setEnabled(true);VanillaLight.setSaturation(100);VanillaLight.setSsgi(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);VanillaLight.setPixelShadows(false);VanillaLight.setEdgeHighlights(false);});
server.runOnServer(s->{command(s,"time set 6000");for(int x=-5;x<=24;x++)for(int z=-5;z<=24;z++){ server.runOnServer(s->{command(s,"time set 6000");for(int x=-5;x<=24;x++)for(int z=-5;z<=24;z++){
s.overworld().setBlockAndUpdate(new BlockPos(x,64,z),Blocks.CONCRETE.white().defaultBlockState()); s.overworld().setBlockAndUpdate(new BlockPos(x,64,z),Blocks.CONCRETE.white().defaultBlockState());
for(int y=65;y<=102;y++)s.overworld().setBlockAndUpdate(new BlockPos(x,y,z),Blocks.AIR.defaultBlockState()); for(int y=65;y<=102;y++)s.overworld().setBlockAndUpdate(new BlockPos(x,y,z),Blocks.AIR.defaultBlockState());
@@ -0,0 +1,63 @@
package fr.koka.sanctuary.client.shader;
import static fr.koka.sanctuary.client.shader.PixelShadows122ClientChecks.*;
import java.nio.file.Files;
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
import net.fabricmc.fabric.api.client.gametest.v1.context.TestServerContext;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.network.chat.Component;
import net.minecraft.util.Mth;
/** Native solar volume in a sealed room, a slotted roof and a leaf canopy. */
public final class Sunshafts137ClientChecks implements FabricClientGameTest {
public void runTest(ClientGameTestContext c){new PixelShadows122ClientChecks(false,false,false,false,true).runTest(c);}
static void run(ClientGameTestContext c,TestServerContext server){
c.runOnClient(m->{preferences();VanillaLight.setEnabled(true);VanillaLight.setSaturation(100);VanillaLight.setPbr(false);VanillaLight.setSsgi(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);VanillaLight.setLensFlare(false);VanillaLight.setEdgeHighlights(false);VanillaLight.setPixelShadows(false);VanillaLight.setSunshafts(false);VanillaLight.setShadowDistance(64);});
server.runOnServer(s->{command(s,"time add 1");command(s,"time set 3000");
for(int x=76;x<=124;x++)for(int z=76;z<=124;z++){
s.overworld().setBlockAndUpdate(new BlockPos(x,96,z),Blocks.STONE_BRICKS.defaultBlockState());
if(x==76||x==124||z==76||z==124)for(int y=97;y<=113;y++)s.overworld().setBlockAndUpdate(new BlockPos(x,y,z),Blocks.STONE_BRICKS.defaultBlockState());
}
});
roof(server,0);camera(c,server,100.5,101,110.5,70);aim(c,server);c.waitTicks(50);
var closedOff=capture(c,"shafts-closed-off");enable(c,true);var closed=capture(c,"shafts-closed-on");
check(gain(closedOff,closed)<300,"Sealed room receives no sunlight through opaque blocks: "+gain(closedOff,closed));
c.runOnClient(m->check(!PixelShadows.applied()&&PixelShadows.depthReady()&&!Bloom.allocated(),"Volume alone shares depth without surface shadows or bloom"));
roof(server,1);settle(c);enable(c,false);var off=capture(c,"shafts-slots-off");enable(c,true);var on=capture(c,"shafts-slots-35");long low=gain(off,on);
check(low>1000,"Roof openings form visible shafts: "+low);unchanged(on,capture(c,null),"Static shafts have no animated sampling noise");
c.runOnClient(m->VanillaLight.setSunshaftIntensity(100));settle(c);var strong=capture(c,"shafts-slots-100");check(gain(off,strong)>low*1.5,"Intensity increases volume energy");
camera(c,server,104.5,101,110.5,70);aim(c,server);settle(c);capture(c,"shafts-moved-four-blocks");
camera(c,server,100.5,101,110.5,70);aim(c,server);settle(c);unchanged(strong,capture(c,"shafts-returned"),"Moving and returning leaves no history or trailing light");
var reload=c.computeOnClient(net.minecraft.client.Minecraft::reloadResourcePacks);c.waitFor(m->reload.isDone()&&m.gui.overlay()==null,1200);reload.join();settle(c);unchanged(strong,capture(c,"shafts-reloaded"),"Volume recovers after resource reload");
int w=c.computeOnClient(m->m.getWindow().getScreenWidth()),h=c.computeOnClient(m->m.getWindow().getScreenHeight());
c.runOnClient(m->m.getWindow().setWindowed(961,541));c.waitFor(m->m.gameRenderer.mainRenderTarget().width!=off.width(),200);settle(c);
enable(c,false);var small=capture(c,null);enable(c,true);check(gain(small,capture(c,"shafts-resized"))>1000,"Bilateral volume survives odd dimensions");
c.runOnClient(m->m.getWindow().setWindowed(w,h));c.waitFor(m->m.gameRenderer.mainRenderTarget().width==off.width(),200);settle(c);
roof(server,2);settle(c);enable(c,false);var leafOff=capture(c,null);enable(c,true);var leafOn=capture(c,"shafts-leaves");long leaf=gain(leafOff,leafOn);double leafEnergy=energy(leafOff,leafOn);check(leaf>1000,"Leaf canopy transmits light: "+leaf);
roof(server,3);settle(c);enable(c,false);var openOff=capture(c,null);enable(c,true);var openOn=capture(c,"shafts-open");long open=gain(openOff,openOn);double openEnergy=energy(openOff,openOn);check(openEnergy>leafEnergy,"Leaf canopy attenuates normalized scattering on identical walls: "+leafEnergy+" / "+openEnergy);
look(c,server,0,0);settle(c);enable(c,false);var sideOff=capture(c,null);enable(c,true);check(gain(sideOff,capture(c,"shafts-sun-offscreen"))>1000,"Volume persists with sun outside the viewport");
long[] close=new long[2];int index=0;
for(double z:new double[]{123.5,123.875}){camera(c,server,100.5,101,z,70);look(c,server,0,0);settle(c);enable(c,false);var nearOff=capture(c,null);enable(c,true);close[index++]=gain(nearOff,capture(c,"shafts-wall-"+z));}
check(close[1]<=close[0]+100,"Approaching wall attenuates scattering instead of increasing it: "+close[0]+" / "+close[1]);
c.runOnClient(m->VanillaLight.setSunshaftIntensity(0));c.waitTicks(10);c.runOnClient(m->check(!Sunshafts.allocated()&&PixelShadows.shadowMap()==null,"Zero frees volume and unneeded shared map"));
c.runOnClient(m->VanillaLight.setSunshaftIntensity(35));enable(c,true);
server.runOnServer(s->command(s,"time set 18000"));c.waitTicks(30);c.runOnClient(m->check(!Sunshafts.allocated()&&!Sunshafts.applied(),"Solar volume stops at night"));
server.runOnServer(s->command(s,"time set 3000"));camera(c,server,100.5,101,110.5,70);aim(c,server);settle(c);
c.runOnClient(m->{VanillaLight.setPixelShadows(true);VanillaLight.setPbr(true);VanillaLight.setSsgi(true);VanillaLight.setBloom(true);VanillaLight.setLensFlare(true);VanillaLight.setEdgeHighlights(true);});activeFrames(c);capture(c,"shafts-combined");
c.runOnClient(m->{VanillaLight.setSunshafts(false);});c.waitTicks(10);c.runOnClient(m->check(!Sunshafts.allocated()&&PixelShadows.applied(),"OFF preserves independently enabled surface shadows"));
for(String locale:new String[]{"fr_fr","en_us"})c.runOnClient(m->{m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());for(String suffix:new String[]{"sunshafts","sunshafts_help","sunshaft_intensity","sunshaft_intensity_help"}){String key="sanctuary.shaders."+suffix;check(!Component.translatable(key).getString().equals(key),"Localized volume controls: "+locale);}});
System.out.println("SUNSHAFT137_PASS slotGain="+low+" leafGain="+leaf+" openGain="+open+" closed room, offscreen sun, camera return, proximity, reload/resize, shared map, zero/night, preferences, FR/EN");
}
private static void roof(TestServerContext server,int mode){server.runOnServer(s->{for(int x=76;x<=124;x++)for(int z=76;z<=124;z++)s.overworld().setBlockAndUpdate(new BlockPos(x,113,z),(mode==3||mode==1&&Math.floorMod(z,8)<3?Blocks.AIR:mode==2?Blocks.OAK_LEAVES:Blocks.OAK_PLANKS).defaultBlockState().trySetValue(net.minecraft.world.level.block.state.properties.BlockStateProperties.PERSISTENT,true));});}
private static void aim(ClientGameTestContext c,TestServerContext server){var d=c.computeOnClient(m->PixelShadows.sunDirection(m.gameRenderer.gameRenderState().levelRenderState.skyRenderState.sunAngle));look(c,server,Mth.wrapDegrees((float)Math.toDegrees(Math.atan2(-d.x,d.z))+25),-12);}
private static void settle(ClientGameTestContext c){int before=c.computeOnClient(m->Sunshafts.renderedFrames());c.waitFor(m->Sunshafts.applied()&&PixelShadows.ready()&&Sunshafts.renderedFrames()>before+10,1200);c.waitTicks(30);}
private static void enable(ClientGameTestContext c,boolean enabled){c.runOnClient(m->VanillaLight.setSunshafts(enabled));if(enabled)settle(c);else c.waitTicks(10);}
private static long gain(Frame off,Frame on){long sum=0;for(int i=0;i<off.pixels().length;i++)for(int shift:new int[]{0,8,16})sum+=Math.max(0,(on.pixels()[i]>>shift&255)-(off.pixels()[i]>>shift&255));return sum;}
// Screen blending has less headroom on a brighter vanilla wall. Remove that factor
// before comparing roofs, and exclude the roof itself where geometry changes.
private static double energy(Frame off,Frame on){double sum=0;int n=0;for(int y=off.height()/3;y<off.height();y++)for(int x=0;x<off.width();x++){int i=y*off.width()+x;for(int shift:new int[]{0,8,16}){int a=off.pixels()[i]>>shift&255,b=on.pixels()[i]>>shift&255;if(a<240){sum+=Math.max(0,b-a)/(255.0-a);n++;}}}return sum/Math.max(1,n);}
private static void preferences(){try{var p=FabricLoader.getInstance().getConfigDir().resolve("sanctuary-shaders.json");Files.writeString(p,"{\"bloom\":false,\"pixelShadows\":false}");forgetPreferences();check(VanillaLight.sunshafts()&&VanillaLight.sunshaftIntensity()==35&&!VanillaLight.bloom()&&!VanillaLight.pixelShadows(),"Missing shaft settings preserve existing choices");VanillaLight.setSunshafts(false);VanillaLight.setSunshaftIntensity(73);forgetPreferences();check(!VanillaLight.sunshafts()&&VanillaLight.sunshaftIntensity()==73,"Custom shafts persist");for(int n:new int[]{-10,0,35,100,150}){VanillaLight.setSunshaftIntensity(n);forgetPreferences();check(VanillaLight.sunshaftIntensity()==Math.clamp(n,0,100),"Intensity clamps and persists");}VanillaLight.setSunshaftIntensity(35);}catch(java.io.IOException e){throw new AssertionError(e);}}
}
@@ -55,7 +55,7 @@ public final class PixelShadows {
private static TextureTarget shadowMap; private static TextureTarget shadowMap;
private static MappableRingBuffer uniforms; private static MappableRingBuffer uniforms;
private static int renderedFrames; private static int renderedFrames;
private static boolean applied; private static boolean applied, depthReady;
private PixelShadows() { } private PixelShadows() { }
public static void register() { public static void register() {
@@ -73,8 +73,9 @@ public final class PixelShadows {
/** Called after the world's depth is resolved and before the first-person hand and GUI. */ /** Called after the world's depth is resolved and before the first-person hand and GUI. */
public static void render() { public static void render() {
applied = false; applied = false;
depthReady = false;
var client = Minecraft.getInstance(); var client = Minecraft.getInstance();
if (!VanillaLight.active() || !VanillaLight.pixelShadows() || client.level == null) { if (!VanillaLight.active() || (!VanillaLight.pixelShadows() && !(VanillaLight.sunshafts() && VanillaLight.sunshaftIntensity()>0)) || client.level == null) {
release(); release();
return; return;
} }
@@ -84,6 +85,7 @@ public final class PixelShadows {
CameraRenderState camera = state.cameraRenderState; CameraRenderState camera = state.cameraRenderState;
var sun = sunDirection(sky.sunAngle); var sun = sunDirection(sky.sunAngle);
boolean lunar = sun.y <= .08f; boolean lunar = sun.y <= .08f;
if (lunar && !VanillaLight.pixelShadows()) { release(); return; }
if (lunar) sun = sunDirection(sky.moonAngle); if (lunar) sun = sunDirection(sky.moonAngle);
if (sky.skybox != DimensionType.Skybox.OVERWORLD || camera.fogType != FogType.NONE || sun.y <= .08f) return; if (sky.skybox != DimensionType.Skybox.OVERWORLD || camera.fogType != FogType.NONE || sun.y <= .08f) return;
float moon = sky.moonPhase == null ? 1 : (float)(.5 + .5 * Math.cos(sky.moonPhase.index() * Math.PI / 4)); float moon = sky.moonPhase == null ? 1 : (float)(.5 + .5 * Math.cos(sky.moonPhase.index() * Math.PI / 4));
@@ -139,6 +141,8 @@ public final class PixelShadows {
pass.drawIndexed(entry.indexCount(), 1, 0, 0, 0); pass.drawIndexed(entry.indexCount(), 1, 0, 0, 0);
} }
} }
depthReady = !lunar;
if (!VanillaLight.pixelShadows()) { renderedFrames++; return; }
var main = client.gameRenderer.mainRenderTarget(); var main = client.gameRenderer.mainRenderTarget();
var nearest = RenderSystem.getSamplerCache().getClampToEdge(FilterMode.NEAREST); var nearest = RenderSystem.getSamplerCache().getClampToEdge(FilterMode.NEAREST);
try (var pass = encoder.createRenderPass(() -> "Sanctuary pixel shadows", main.getColorTextureView(), Optional.empty())) { try (var pass = encoder.createRenderPass(() -> "Sanctuary pixel shadows", main.getColorTextureView(), Optional.empty())) {
@@ -168,7 +172,9 @@ public final class PixelShadows {
if (shadowMap != null) { shadowMap.destroyBuffers(); shadowMap = null; } if (shadowMap != null) { shadowMap.destroyBuffers(); shadowMap = null; }
if (uniforms != null) { uniforms.close(); uniforms = null; } if (uniforms != null) { uniforms.close(); uniforms = null; }
applied = false; applied = false;
depthReady = false;
} }
static boolean depthReady() { return depthReady; }
static int renderedFrames() { return renderedFrames; } static int renderedFrames() { return renderedFrames; }
static boolean applied() { return applied; } static boolean applied() { return applied; }
static boolean ready() { return TERRAIN.settled(); } static boolean ready() { return TERRAIN.settled(); }
@@ -30,6 +30,9 @@ public final class ShaderOptionsScreen extends OptionsSubScreen {
Component.translatable("sanctuary.shaders.bloom_help")),VanillaLight.bloom(),VanillaLight::setBloom)); Component.translatable("sanctuary.shaders.bloom_help")),VanillaLight.bloom(),VanillaLight::setBloom));
slider("bloom_intensity",VanillaLight.bloomIntensity(),VanillaLight::setBloomIntensity); slider("bloom_intensity",VanillaLight.bloomIntensity(),VanillaLight::setBloomIntensity);
slider("bloom_radius",VanillaLight.bloomRadius(),VanillaLight::setBloomRadius); slider("bloom_radius",VanillaLight.bloomRadius(),VanillaLight::setBloomRadius);
list.addBig(OptionInstance.createBoolean("sanctuary.shaders.sunshafts",OptionInstance.cachedConstantTooltip(
Component.translatable("sanctuary.shaders.sunshafts_help")),VanillaLight.sunshafts(),VanillaLight::setSunshafts));
slider("sunshaft_intensity",VanillaLight.sunshaftIntensity(),VanillaLight::setSunshaftIntensity);
list.addBig(OptionInstance.createBoolean("sanctuary.shaders.lens_flare",OptionInstance.cachedConstantTooltip( list.addBig(OptionInstance.createBoolean("sanctuary.shaders.lens_flare",OptionInstance.cachedConstantTooltip(
Component.translatable("sanctuary.shaders.lens_flare_help")),VanillaLight.lensFlare(),VanillaLight::setLensFlare)); Component.translatable("sanctuary.shaders.lens_flare_help")),VanillaLight.lensFlare(),VanillaLight::setLensFlare));
slider("lens_flare_intensity",VanillaLight.lensFlareIntensity(),VanillaLight::setLensFlareIntensity); slider("lens_flare_intensity",VanillaLight.lensFlareIntensity(),VanillaLight::setLensFlareIntensity);
@@ -0,0 +1,70 @@
package fr.koka.sanctuary.client.shader;
import com.mojang.blaze3d.buffers.Std140Builder;
import com.mojang.blaze3d.pipeline.TextureTarget;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.renderpearl.api.GpuFormat;
import com.mojang.renderpearl.api.buffers.GpuBuffer;
import com.mojang.renderpearl.api.pipeline.*;
import com.mojang.renderpearl.api.textures.FilterMode;
import fr.koka.sanctuary.client.panorama.PanoramaCapture;
import java.util.Optional;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.MappableRingBuffer;
import net.minecraft.client.renderer.RenderPipelines;
import net.minecraft.resources.Identifier;
/** Solar scattering in the existing shadow volume. No scene copy, readback, temporal history or terrain duplication. */
public final class Sunshafts {
private static final BindGroupLayout SCENE=BindGroupLayout.builder()
.withUniform("PixelShadows",UniformType.UNIFORM_BUFFER)
.withUniform("ShaftParams",UniformType.UNIFORM_BUFFER)
.withUniform("WorldDepth",UniformType.COMBINED_IMAGE_SAMPLER).build();
private static RenderPipeline pipeline(String name,boolean composite){
var inputs=BindGroupLayout.builder();
if(composite)inputs.withUniform("ShaftLight",UniformType.COMBINED_IMAGE_SAMPLER);
else inputs.withUniform("SunDepth",UniformType.COMBINED_IMAGE_SAMPLER);
return RenderPipelines.register(RenderPipeline.builder().withLocation(Identifier.parse("sanctuary:pipeline/"+name))
.withVertexShader("core/screenquad").withFragmentShader(Identifier.parse("sanctuary:core/"+name))
.withBindGroupLayout(SCENE).withBindGroupLayout(inputs.build())
.withPrimitiveTopology(PrimitiveTopology.TRIANGLES).withCull(false)
.withColorTargetState(new ColorTargetState(composite?Optional.of(new BlendFunction(BlendFactor.ONE,BlendFactor.ONE_MINUS_SRC_COLOR,BlendFactor.ZERO,BlendFactor.ONE)):Optional.empty(),
composite?GpuFormat.RGBA8_UNORM:GpuFormat.RGBA16_FLOAT,composite?ColorTargetState.WRITE_COLOR:ColorTargetState.WRITE_ALL))
.withDepthStencilState(Optional.empty()).build());
}
private static final RenderPipeline GATHER=pipeline("sunshaft_gather",false),APPLY=pipeline("sunshaft_apply",true);
private static TextureTarget light;
private static MappableRingBuffer settings;
private static boolean applied;
private static int frames;
private Sunshafts(){}
public static void render(){
applied=false;var mc=Minecraft.getInstance();
if(!VanillaLight.active()||!VanillaLight.sunshafts()||VanillaLight.sunshaftIntensity()==0||mc.level==null){release();return;}
if(PanoramaCapture.rendering())return;
if(!PixelShadows.depthReady()){release();return;}
var main=mc.gameRenderer.mainRenderTarget();int width=Math.max(1,(main.width+3)/4),height=Math.max(1,(main.height+3)/4);
if(light==null||light.width!=width||light.height!=height){release();light=new TextureTarget("Sanctuary sunshafts",width,height,GpuFormat.RGBA16_FLOAT,null);}
if(settings==null)settings=new MappableRingBuffer(()->"Sanctuary sunshafts",GpuBuffer.USAGE_UNIFORM|GpuBuffer.USAGE_MAP_WRITE,32);
var fog=mc.gameRenderer.gameRenderState().levelRenderState.cameraRenderState.fogData;
settings.rotate();try(var mapped=settings.currentBuffer().map(false,true)){
Std140Builder.intoBuffer(mapped.data()).putVec4(.85f+.15f*fog.color.x,.78f+.15f*fog.color.y,.66f+.15f*fog.color.z,0)
.putVec4(VanillaLight.sunshaftIntensity()/100f,Math.min(64,VanillaLight.shadowDistance()*.5f),0,0);
}
var encoder=RenderSystem.getDevice().createCommandEncoder();var nearest=RenderSystem.getSamplerCache().getClampToEdge(FilterMode.NEAREST);
try(var pass=encoder.createRenderPass(()->"Sanctuary solar volume",light.getColorTextureView(),Optional.empty())){
pass.setPipeline(RenderSystem.getCompiledPipeline(GATHER));pass.setUniform("PixelShadows",PixelShadows.materialUniforms());pass.setUniform("ShaftParams",settings.currentBuffer());
pass.setUniform("WorldDepth",main.getDepthTextureView(),nearest);pass.setUniform("SunDepth",PixelShadows.materialDepth(),nearest);pass.draw(3,1,0,0);
}
try(var pass=encoder.createRenderPass(()->"Sanctuary solar volume composite",main.getColorTextureView(),Optional.empty())){
pass.setPipeline(RenderSystem.getCompiledPipeline(APPLY));pass.setUniform("PixelShadows",PixelShadows.materialUniforms());pass.setUniform("ShaftParams",settings.currentBuffer());
pass.setUniform("WorldDepth",main.getDepthTextureView(),nearest);pass.setUniform("ShaftLight",light.getColorTextureView(),nearest);pass.draw(3,1,0,0);
}
applied=true;frames++;
}
public static void release(){if(light!=null){light.destroyBuffers();light=null;}if(settings!=null){settings.close();settings=null;}applied=false;}
static boolean applied(){return applied;}
static boolean allocated(){return light!=null;}
static int renderedFrames(){return frames;}
static TextureTarget target(){return light;}
}
@@ -19,6 +19,8 @@ public final class VanillaLight {
int saturation=142; int saturation=142;
boolean pixelShadows=true; boolean pixelShadows=true;
boolean bloom=true; boolean bloom=true;
boolean sunshafts=true;
int sunshaftIntensity=35;
boolean lensFlare=true; boolean lensFlare=true;
int lensFlareIntensity=30; int lensFlareIntensity=30;
boolean pbr=true; boolean pbr=true;
@@ -41,6 +43,7 @@ public final class VanillaLight {
try{if(Files.isRegularFile(path()))preferences=new GsonBuilder().create().fromJson(Files.readString(path()),Preferences.class);} try{if(Files.isRegularFile(path()))preferences=new GsonBuilder().create().fromJson(Files.readString(path()),Preferences.class);}
catch(Exception e){SanctuaryMod.LOGGER.warn("Cannot read Sanctuary shader preference",e);} catch(Exception e){SanctuaryMod.LOGGER.warn("Cannot read Sanctuary shader preference",e);}
if(preferences==null)preferences=new Preferences(); if(preferences==null)preferences=new Preferences();
preferences.sunshaftIntensity=Math.clamp(preferences.sunshaftIntensity,0,100);
preferences.lensFlareIntensity=Math.clamp(preferences.lensFlareIntensity,0,100); preferences.lensFlareIntensity=Math.clamp(preferences.lensFlareIntensity,0,100);
preferences.pbrIntensity=Math.clamp(preferences.pbrIntensity,0,100); preferences.pbrIntensity=Math.clamp(preferences.pbrIntensity,0,100);
preferences.ssgiIntensity=Math.clamp(preferences.ssgiIntensity,0,100); preferences.ssgiIntensity=Math.clamp(preferences.ssgiIntensity,0,100);
@@ -64,6 +67,10 @@ public final class VanillaLight {
public static int edgeIntensity(){enabled();return preferences.edgeIntensity;} public static int edgeIntensity(){enabled();return preferences.edgeIntensity;}
public static void setEdgeHighlights(boolean value){enabled();preferences.edgeHighlights=value;save();} public static void setEdgeHighlights(boolean value){enabled();preferences.edgeHighlights=value;save();}
public static void setEdgeIntensity(int value){enabled();preferences.edgeIntensity=Math.clamp(value,0,100);save();} public static void setEdgeIntensity(int value){enabled();preferences.edgeIntensity=Math.clamp(value,0,100);save();}
public static boolean sunshafts(){enabled();return preferences.sunshafts;}
public static int sunshaftIntensity(){enabled();return preferences.sunshaftIntensity;}
public static void setSunshafts(boolean value){enabled();preferences.sunshafts=value;save();}
public static void setSunshaftIntensity(int value){enabled();preferences.sunshaftIntensity=Math.clamp(value,0,100);save();}
public static boolean lensFlare(){enabled();return preferences.lensFlare;} public static boolean lensFlare(){enabled();return preferences.lensFlare;}
public static int lensFlareIntensity(){enabled();return preferences.lensFlareIntensity;} public static int lensFlareIntensity(){enabled();return preferences.lensFlareIntensity;}
public static void setLensFlare(boolean value){enabled();preferences.lensFlare=value;save();} public static void setLensFlare(boolean value){enabled();preferences.lensFlare=value;save();}
@@ -7,6 +7,7 @@ import fr.koka.sanctuary.client.shader.PixelShadows;
import fr.koka.sanctuary.client.shader.ProceduralPbr; import fr.koka.sanctuary.client.shader.ProceduralPbr;
import fr.koka.sanctuary.client.shader.EdgeHighlights; import fr.koka.sanctuary.client.shader.EdgeHighlights;
import fr.koka.sanctuary.client.shader.Bloom; import fr.koka.sanctuary.client.shader.Bloom;
import fr.koka.sanctuary.client.shader.Sunshafts;
import fr.koka.sanctuary.client.shader.ScreenSpaceGI; import fr.koka.sanctuary.client.shader.ScreenSpaceGI;
import net.minecraft.client.renderer.GameRenderer; import net.minecraft.client.renderer.GameRenderer;
import net.minecraft.client.renderer.ProjectionMatrixBuffer; import net.minecraft.client.renderer.ProjectionMatrixBuffer;
@@ -28,7 +29,7 @@ abstract class PixelShadowRendererMixin {
return original.call(buffer, 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")) @Inject(method="renderLevel", at=@At(value="INVOKE", target="Lnet/minecraft/client/renderer/GameRenderer;render3dHud(Lnet/minecraft/client/renderer/state/level/CameraRenderState;Lnet/minecraft/client/renderer/state/level/PlayerRenderState;Lnet/minecraft/client/renderer/state/OptionsRenderState;Z)V"))
private void sanctuary$shadows(CallbackInfo ci) { PixelShadows.render(); ProceduralPbr.render(); ScreenSpaceGI.render(); EdgeHighlights.render(); Bloom.render(); } private void sanctuary$shadows(CallbackInfo ci) { PixelShadows.render(); ProceduralPbr.render(); ScreenSpaceGI.render(); EdgeHighlights.render(); Bloom.render(); Sunshafts.render(); }
@Inject(method={"close", "setLevel"}, at=@At("HEAD")) @Inject(method={"close", "setLevel"}, at=@At("HEAD"))
private void sanctuary$close(CallbackInfo ci) { PixelShadows.release(); ProceduralPbr.release(); EdgeHighlights.release(); Bloom.release(); ScreenSpaceGI.release(); } private void sanctuary$close(CallbackInfo ci) { PixelShadows.release(); ProceduralPbr.release(); EdgeHighlights.release(); Bloom.release(); ScreenSpaceGI.release(); Sunshafts.release(); }
} }
@@ -2303,5 +2303,10 @@
"sanctuary.shaders.lens_flare_help": "Soft rectangular sun and moon reflections. Obstacles hide the sources. Independent of the bloom halo.", "sanctuary.shaders.lens_flare_help": "Soft rectangular sun and moon reflections. Obstacles hide the sources. Independent of the bloom halo.",
"sanctuary.shaders.lens_flare_intensity": "Lens flare intensity", "sanctuary.shaders.lens_flare_intensity": "Lens flare intensity",
"sanctuary.shaders.lens_flare_intensity_help": "Celestial reflection strength. Zero disables the effect.", "sanctuary.shaders.lens_flare_intensity_help": "Celestial reflection strength. Zero disables the effect.",
"sanctuary.shaders.lens_flare_intensity_value": "Lens flare: %s %%" "sanctuary.shaders.lens_flare_intensity_value": "Lens flare: %s %%",
"sanctuary.shaders.sunshafts": "Sunshafts",
"sanctuary.shaders.sunshafts_help": "Light beams through openings in the scenery. Independent of bloom; range follows shadows (up to 64 blocks).",
"sanctuary.shaders.sunshaft_intensity": "Sunshaft intensity",
"sanctuary.shaders.sunshaft_intensity_help": "Strength of light scattered in the air. Zero disables the effect.",
"sanctuary.shaders.sunshaft_intensity_value": "Sunshafts: %s %%"
} }
@@ -2303,5 +2303,10 @@
"sanctuary.shaders.lens_flare_help": "Reflets rectangulaires du soleil et de la lune, aux bords dégradés. Les obstacles masquent les sources. Indépendant du halo de bloom.", "sanctuary.shaders.lens_flare_help": "Reflets rectangulaires du soleil et de la lune, aux bords dégradés. Les obstacles masquent les sources. Indépendant du halo de bloom.",
"sanctuary.shaders.lens_flare_intensity": "Intensité du lens flare", "sanctuary.shaders.lens_flare_intensity": "Intensité du lens flare",
"sanctuary.shaders.lens_flare_intensity_help": "Force des reflets célestes. Zéro désactive le calcul.", "sanctuary.shaders.lens_flare_intensity_help": "Force des reflets célestes. Zéro désactive le calcul.",
"sanctuary.shaders.lens_flare_intensity_value": "Lens flare : %s %%" "sanctuary.shaders.lens_flare_intensity_value": "Lens flare : %s %%",
"sanctuary.shaders.sunshafts": "Rayons solaires",
"sanctuary.shaders.sunshafts_help": "Faisceaux de lumière dans les ouvertures du décor. Indépendants du bloom, portée liée aux ombres (64 blocs maximum).",
"sanctuary.shaders.sunshaft_intensity": "Intensité des rayons",
"sanctuary.shaders.sunshaft_intensity_help": "Force de la lumière dans lair. Zéro désactive le calcul.",
"sanctuary.shaders.sunshaft_intensity_value": "Rayons : %s %%"
} }
@@ -0,0 +1,21 @@
#version 330
#extension GL_ARB_separate_shader_objects : require
#include <sanctuary:sunshafts.glsl>
uniform sampler2D ShaftLight;
layout(location=0) in vec2 texCoord;
layout(location=0) out vec4 fragColor;
void main(){
float end=shaftEnd(shaftTexel(texCoord));
vec2 size=vec2(textureSize(ShaftLight,0)),at=texCoord*size-.5,base=floor(at),f=fract(at);
vec3 sum=vec3(0);float weight=0.0;
for(int y=0;y<2;y++)for(int x=0;x<2;x++){
vec4 light=texture(ShaftLight,(base+vec2(x,y)+.5)/size);
float tolerance=.15+end*.035;
float compatible=1.0-smoothstep(tolerance*.2,tolerance,abs(light.a-end));
float w=(x==0?1.0-f.x:f.x)*(y==0?1.0-f.y:f.y)*compatible;
sum+=light.rgb*w;weight+=w;
}
if(weight<.0001)discard;
// Smooth energy shoulder and near-contact fade; preserve framebuffer alpha.
fragColor=vec4(1.0-exp(-sum/weight*smoothstep(.03,.2,end)),0);
}
@@ -0,0 +1,44 @@
#version 330
#extension GL_ARB_separate_shader_objects : require
#include <sanctuary:sunshafts.glsl>
uniform sampler2D SunDepth;
layout(location=0) in vec2 texCoord;
layout(location=0) out vec4 fragColor;
float sunlight(vec3 point){
vec4 light=LightMatrix*vec4(point+CameraFractionPixels.xyz,1);
vec3 q=light.xyz/light.w;q.xy=q.xy*.5+.5;
if(DistanceClipFog.y<.5)q.z=q.z*.5+.5;
// No clamped shadow-map edge may become a fictitious column of light.
float border=min(min(q.x,q.y),min(1.0-q.x,1.0-q.y));
if(border<=0.0||q.z<=0.0||q.z>=1.0)return 0.0;
vec2 size=vec2(textureSize(SunDepth,0));
// Fixed world-scale footprint softens the volume, not the block textures.
vec2 footprint=max(vec2(1.0)/size,vec2(.12/(DistanceClipFog.x+16.0)));
float lit=0.0;
for(int y=0;y<2;y++)for(int x=0;x<2;x++){
float depth=texture(SunDepth,q.xy+(vec2(x,y)-.5)*footprint).r;
lit+=smoothstep(-.00035,.00035,depth-q.z);
}
return lit*.25*smoothstep(0.0,.06,border)*smoothstep(0.0,.03,min(q.z,1.0-q.z));
}
void main(){
vec2 uv=shaftTexel(texCoord);float end=shaftEnd(uv);
vec3 ray=normalize(shaftPosition(uv,.0001));
float total=0.0,range=ShaftSettings.y;
// Quadratic, deterministic segments resolve nearby occluders without frame noise.
// Partial last segments fade continuously as a wall approaches the camera.
for(int i=0;i<16;i++){
float a=float(i)/16.0,b=float(i+1)/16.0;
float start=range*a*a,stop=min(end,range*b*b);
float span=max(0.0,stop-start);if(span<=0.0)break;
float t=(start+stop)*.5;
float distanceFade=1.0-smoothstep(range*.7,range,t);
total+=sunlight(ray*t)*span*exp(-t/24.0)*distanceFade;
}
float phase=.12+.88*pow(max(0.0,dot(ray,SunStrength.xyz)),4.0);
float weather=clamp(SunStrength.w/.38,0.0,1.0);
float fogDistance=max(abs(ray.x),max(abs(ray.y),abs(ray.z)))*end;
float nativeFog=1.0-smoothstep(DistanceClipFog.z,max(DistanceClipFog.z+.01,DistanceClipFog.w),fogDistance);
float amount=total*.018*phase*weather*ShaftSettings.x*nativeFog;
fragColor=vec4(ShaftColor.rgb*amount,end);
}
@@ -0,0 +1,23 @@
layout(std140) uniform PixelShadows {
mat4 InverseViewProjection;
mat4 LightMatrix;
vec4 CameraFractionPixels;
vec4 SunStrength;
vec4 DistanceClipFog;
vec4 EnvironmentFog;
};
layout(std140) uniform ShaftParams { vec4 ShaftColor; vec4 ShaftSettings; };
uniform sampler2D WorldDepth;
vec2 shaftTexel(vec2 uv){
vec2 size=vec2(textureSize(WorldDepth,0));
return (clamp(floor(uv*size),vec2(0),size-1.0)+.5)/size;
}
vec3 shaftPosition(vec2 uv,float depth){
float z=DistanceClipFog.y>.5?depth:depth*2.0-1.0;
vec4 p=InverseViewProjection*vec4(uv*2.0-1.0,z,1);
return p.xyz/p.w;
}
float shaftEnd(vec2 uv){
float depth=texture(WorldDepth,uv).r;
return depth<=0.0?ShaftSettings.y:min(ShaftSettings.y,length(shaftPosition(uv,depth)));
}
+1 -1
View File
@@ -1,6 +1,6 @@
name = "Sanctuary" name = "Sanctuary"
author = "KOKA99CAB" author = "KOKA99CAB"
version = "beta.136" version = "beta.137"
pack-format = "packwiz:1.1.0" pack-format = "packwiz:1.1.0"
[index] [index]