diff --git a/README.md b/README.md
index 04d397a..3d65f0a 100644
--- a/README.md
+++ b/README.md
@@ -20,15 +20,12 @@ La [vision complète](docs/vision.md) conserve les intentions ; le
L’économie, les claims et les dimensions décrits dans la vision restent
à implémenter.
-La version **beta.131** active par défaut le bloom, les minerais émissifs et
-le highlight. Couper le bloom ou mettre son intensité à zéro conserve les
-inclusions émissives sans halo. La précision commune ajoute 64 et 128 pixels
-par bloc en options ; sa valeur initiale reste 16. Distances initiales :
-ombres 128 blocs et highlight 32 blocs. **Options → Shaders → Sanctuary ·
-Vanilla Light**. Les réglages personnalisés sont conservés.
-Le [ticket de livraison](docs/shader-beta131.md) détaille les vérifications.
-Release publiée et même instance **Sanctuary Beta** synchronisée, sauvegardes
-et réglages conservés.
+La version **beta.132** ajoute le **SSGI · couleurs diffusées** expérimental :
+les surfaces visibles proches se transmettent une légère teinte. Effet
+optionnel, intensité réglable, rayon local de 2,5 blocs.
+**Options → Shaders** ; les réglages existants restent conservés.
+Le [ticket de livraison](docs/shader-beta132.md) décrit le rendu, les limites
+et la validation.
La [livraison précédente](docs/clay-workshop-integration-beta110.md) réunit déjà
l’atelier, les œufs et la neige saisonnière dans l’instance **Sanctuary Beta**.
Les [synchronisations antérieures](docs/git-sync-beta092.md) restent documentées.
@@ -39,6 +36,12 @@ La [carte native beta.005](docs/atlas-beta005.md) prend le relais après
l’étude de Xaero et des alternatives libres.
+## beta.132 — SSGI : diffusion locale des couleurs
+
+Rebond diffus des couleurs visibles, intensité réglable et reconstruction
+préservant les silhouettes. Expérimental, initialement désactivé.
+[Contrat et vérifications](docs/shader-beta132.md).
+
## beta.131 — Émissifs indépendants et précision étendue
Bloom, émissifs et highlight activés initialement ; halo facultatif et cinq
diff --git a/docs/shader-beta132.md b/docs/shader-beta132.md
new file mode 100644
index 0000000..f30c007
--- /dev/null
+++ b/docs/shader-beta132.md
@@ -0,0 +1,96 @@
+# SHADER-132 — diffusion locale SSGI expérimentale
+
+Demande : légère teinte diffusée des surfaces voisines, sans reflets miroir
+ni shader d'eau. Branche `codex/ssgi-beta132`, socle beta.131, Minecraft 26.3 /
+Java 25. Aucun changement de génération, sauvegarde ou lumière serveur.
+
+## Résultat
+
+**Options → Shaders → SSGI · couleurs diffusées** active un rebond diffus
+local. L'effet expérimental est initialement OFF, avec une intensité préparée
+à **35 %** ; le curseur propose 0–100. À zéro ou OFF, ses cibles et uniformes
+GPU sont libérés. Les autres effets et leurs préférences sont conservés.
+
+La couleur d'une surface visible peut teinter les surfaces proches qui lui
+font face. Le calcul porte à **2,5 blocs** du récepteur et s'estompe entre
+24 et 32 blocs de la caméra. Il utilise une copie de la couleur native après
+les ombres, avec la profondeur et des normales reconstruites. Le premier
+obstacle arrête chaque rayon, même si son épaisseur empêche de retenir une
+intersection. Les plans coplanaires ne s'éclairent pas eux-mêmes.
+
+Douze directions hémisphériques fixes, au maximum dix étapes par rayon,
+sont évaluées à demi-résolution en largeur et hauteur. La reconstruction
+plein écran est guidée par la profondeur pour conserver les silhouettes et
+les détails des textures. Le cœur des émissifs, le highlight et le bloom
+sont composés ensuite : pas de halo réinjecté dans le rebond ni de boucle
+de rétroaction d'une image sur l'autre. Aucune lecture GPU vers CPU ni
+recherche de chunks en production. Pas de bruit temporel ou d'historique.
+
+Deux cibles, RGBA8 plein écran et RGBA16 flottant à demi-résolution, soit
+environ six octets par pixel de l'écran, plus 176 octets d'uniformes par
+segment du tampon natif. Redimensionnement, arrêt, déconnexion et
+rechargement libèrent/recréent les ressources possédées. Ce budget n'est
+pas une garantie de FPS ; le SSGI coûte davantage qu'un simple réglage de
+colorimétrie.
+
+## Limites de cette version
+
+L'estimation utilise seulement la couleur et la profondeur visibles. Les
+blocs hors champ ou cachés n'apportent pas leur couleur. Le résultat peut
+changer lorsqu'ils entrent ou sortent de l'image ; une atténuation près
+des bords limite la coupure. Les surfaces transparentes absentes de la
+profondeur ne sont pas reconstruites. Petits objets et intersections plus
+minces que l'échantillonnage peuvent manquer. Il s'agit d'une approximation
+diffuse locale, pas d'une simulation complète d'éclairage global ni de
+reflets spéculaires. Aucune modification de l'eau et aucune source hors
+champ déduite ou générée.
+
+## Vérifications
+
+Test ciblé natif réussi en **1 min** sur macOS / Apple M1 / OpenGL, nouveau
+monde plat de graine 122. À intensité 90, les échantillons du sol blanc
+montrent un gain rouge cumulé de 8 968 contre 20 bleu devant le mur rouge,
+puis 9 168 bleu contre 23 rouge devant le mur bleu. Le sol seul est inchangé.
+OFF/zéro restituent l'image de base et libèrent les cibles ; intensité 20
+réduit le gain. Changer la couleur du mur complètement occulté laisse
+l'image inchangée. Rechargement, caméra fixe, mouvement infinitésimal et
+retour à la caméra d'origine passent, sans accumulation de couleur.
+
+Deux ajustements de fixture ont précédé ce résultat : inclure dans les
+mesures le blanc légèrement bleuté par l'éclairage natif, et formater les
+petits angles de `/tp` en décimal car la commande refuse la notation
+scientifique. Aucun comportement du jeu modifié pour ces corrections.
+
+Log ciblé : `build/shader132-client-ssgi.log`. La suite complète réussit en
+**4 min 18 s** et vérifie aussi une fenêtre 961 × 541, le retour à sa taille
+initiale, le SSGI avec ombres/highlight/bloom/émissifs actifs, ainsi que
+l'arrêt global du shader. Les captures françaises et anglaises passent.
+Les contrôles précédents des cinq précisions, ombres, lune, feuillage, CTM,
+bloom et émissifs restent réussis. Log : `build/shader132-client-full.log`.
+
+Commandes : `./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true
+-PsanctuaryPixelShadows122ClientTests=true -PsanctuaryClientNoVsync=true
+-PsanctuaryQuickTests=true`. Pour le test ciblé, remplacer la propriété
+PixelShadows par `-PsanctuarySsgi132ClientTests=true`.
+
+`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
+réussit en **2 min 25 s**, 126 tâches. Les packs normal/Test contiennent le
+même JAR ; sources archivées et template de ressources correspondent aux
+sources. Comparaison avec beta.131 : **11 entrées de production** modifiées,
+limitées au SSGI, à ses options et à ses points d'appel. Gameplay et textures
+des gemmes inchangés, aucun monde ni test embarqué. Aucun résultat
+Windows/Vulkan revendiqué.
+Le serveur GameTest dédié reste exclu conformément au refus antérieur de
+son EULA. Les captures panoramiques et les milieux de brume sous l'eau/lave
+ou neige poudreuse suspendent cette passe, comme les autres effets de
+profondeur Sanctuary.
+
+## Livraison
+
+- `Sanctuary-beta.132.mrpack` : 10394026 octets, SHA-256 `a7c0823e00f36925919645e32e7792242ec1d01015b41bee5b4f6285b9fc533d`.
+- `Sanctuary-Test-beta.132.mrpack` : 10412949 octets, SHA-256 `9bc30a7d6d5834f9323f15f0c39d3f49d200e1a97d739266ab373c9e1ba4a8fd`.
+
+JAR : `83eb2d091a7acd85eaa8b3b6a00a40ea483f9eeb2a30c033bdf68b4d717c5142`.
+
+Archives vérifiées recopiées dans `sanctuary-beta/build/`. Publication et
+synchronisation de la même instance après le commit source.
diff --git a/gradle.properties b/gradle.properties
index 2a34c03..1baeb4e 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -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.131
-pack_version=beta.131
+mod_version=beta.132
+pack_version=beta.132
resource_pack_version=beta.121
maven_group=fr.koka.sanctuary
jei_version=30.32.0-sanctuary.3
diff --git a/mods/sanctuary/build.gradle b/mods/sanctuary/build.gradle
index 52e5d3b..ebadbff 100644
--- a/mods/sanctuary/build.gradle
+++ b/mods/sanctuary/build.gradle
@@ -93,6 +93,10 @@ tasks.named('runGameTest') {
}
tasks.named('processGametestResources') {
+ def ssgi132Client = providers.gradleProperty('sanctuarySsgi132ClientTests').map { it.toBoolean() }.getOrElse(false)
+ inputs.property('sanctuarySsgi132ClientTests', ssgi132Client)
+ if (ssgi132Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.shader.Ssgi132ClientChecks') }
+
def bloom130Client = providers.gradleProperty('sanctuaryBloom130ClientTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('sanctuaryBloom130ClientTests', bloom130Client)
if (bloom130Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.shader.Bloom130ClientChecks') }
diff --git a/mods/sanctuary/src/clientGametest/java/fr/koka/sanctuary/client/shader/PixelShadows122ClientChecks.java b/mods/sanctuary/src/clientGametest/java/fr/koka/sanctuary/client/shader/PixelShadows122ClientChecks.java
index e02d179..d67abcb 100644
--- a/mods/sanctuary/src/clientGametest/java/fr/koka/sanctuary/client/shader/PixelShadows122ClientChecks.java
+++ b/mods/sanctuary/src/clientGametest/java/fr/koka/sanctuary/client/shader/PixelShadows122ClientChecks.java
@@ -36,9 +36,10 @@ import net.minecraft.world.phys.Vec3;
/** Real terrain/depth-map checks on a new flat seed 122 world; GPU readback is test-only. */
public final class PixelShadows122ClientChecks implements FabricClientGameTest {
- private final boolean bloomOnly;
+ private final boolean bloomOnly,ssgiOnly;
public PixelShadows122ClientChecks(){this(false);}
- PixelShadows122ClientChecks(boolean bloomOnly){this.bloomOnly=bloomOnly;}
+ PixelShadows122ClientChecks(boolean bloomOnly){this(bloomOnly,false);}
+ PixelShadows122ClientChecks(boolean bloomOnly,boolean ssgiOnly){this.bloomOnly=bloomOnly;this.ssgiOnly=ssgiOnly;}
private static final int FLOOR = 64;
private static final AABB PILLAR = new AABB(0, FLOOR + 1, 0, 2, FLOOR + 9, 2);
private static final BlockPos SLAB = new BlockPos(5, FLOOR + 3, -5);
@@ -133,6 +134,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
c.runOnClient(m -> check(VanillaLight.active(), "Native Sanctuary shader is active without an external engine"));
float morning = c.computeOnClient(m -> m.gameRenderer.gameRenderState().levelRenderState.skyRenderState.sunAngle);
+ if(ssgiOnly){Ssgi132ClientChecks.run(c,server);return;}
if(bloomOnly){Bloom130ClientChecks.run(c,server);return;}
// Keep VanillaLight enabled for both frames: changing its fog/grade would invalidate this oracle.
shadows(c, false);
@@ -241,12 +243,13 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
RealtimeShadows129ClientChecks.run(c, server);
EdgeHighlights129ClientChecks.run(c, server);
Bloom130ClientChecks.run(c, server);
+ Ssgi132ClientChecks.run(c, server);
c.getInput().pressKey(InputConstants.KEY_F1);
for (String locale : new String[]{"fr_fr", "en_us"}) {
c.runOnClient(m -> {
m.getLanguageManager().setSelected(locale);
m.getLanguageManager().onResourceManagerReload(m.getResourceManager());
- for (String key : new String[]{"pixel_shadows", "shadow_pixels", "shadow_distance", "edge_highlights", "edge_intensity", "edge_distance", "precision", "bloom", "bloom_radius", "ore_emission"}) {
+ for (String key : new String[]{"pixel_shadows", "shadow_pixels", "shadow_distance", "edge_highlights", "edge_intensity", "edge_distance", "precision", "ssgi", "ssgi_intensity", "bloom", "bloom_radius", "ore_emission"}) {
String fullKey = "sanctuary.shaders." + key;
check(!Component.translatable(fullKey).getString().equals(fullKey), "Translated " + locale + " " + key);
}
@@ -258,7 +261,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
});
});
c.waitTicks(3);
- c.takeScreenshot("sanctuary-beta131-shader-options-" + locale);
+ c.takeScreenshot("sanctuary-beta132-shader-options-" + locale);
}
c.runOnClient(m -> m.gui.setScreen(null));
}
@@ -423,6 +426,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
}
private static void checkPreferences() {
+ VanillaLight.setSsgi(false);
VanillaLight.setBloom(false);
VanillaLight.setOreEmission(false);
VanillaLight.setEnabled(true);
@@ -479,7 +483,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
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);
+ command(s, "tp @a " + p.getX() + " " + p.getY() + " " + p.getZ() + String.format(java.util.Locale.ROOT," %.8f %.8f",yaw,pitch));
});
c.waitFor(m -> Math.abs(m.player.getXRot() - pitch) < .00001
&& Math.abs(m.player.getYRot() - yaw) < .00001, 200);
@@ -524,7 +528,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
}
static Frame capture(ClientGameTestContext c, String name) {
- if (name != null) c.takeScreenshot("sanctuary-beta131-shader-" + name);
+ if (name != null) c.takeScreenshot("sanctuary-beta132-shader-" + name);
var result = new CompletableFuture();
c.runOnClient(m -> Screenshot.takeScreenshot(m.gameRenderer.mainRenderTarget(), image -> {
try (image) {
diff --git a/mods/sanctuary/src/clientGametest/java/fr/koka/sanctuary/client/shader/Ssgi132ClientChecks.java b/mods/sanctuary/src/clientGametest/java/fr/koka/sanctuary/client/shader/Ssgi132ClientChecks.java
new file mode 100644
index 0000000..67249e9
--- /dev/null
+++ b/mods/sanctuary/src/clientGametest/java/fr/koka/sanctuary/client/shader/Ssgi132ClientChecks.java
@@ -0,0 +1,75 @@
+package fr.koka.sanctuary.client.shader;
+
+import static fr.koka.sanctuary.client.shader.PixelShadows122ClientChecks.*;
+import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
+import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
+import net.fabricmc.fabric.api.client.gametest.v1.context.TestServerContext;
+import net.fabricmc.loader.api.FabricLoader;
+import net.minecraft.core.BlockPos;
+import net.minecraft.world.level.block.Blocks;
+import net.minecraft.world.level.block.state.BlockState;
+import java.nio.file.Files;
+
+public final class Ssgi132ClientChecks implements FabricClientGameTest {
+ public void runTest(ClientGameTestContext c){new PixelShadows122ClientChecks(false,true).runTest(c);}
+ static void run(ClientGameTestContext c,TestServerContext server){
+ c.runOnClient(m->{preferences();VanillaLight.setEnabled(true);VanillaLight.setSaturation(100);VanillaLight.setSsgi(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);VanillaLight.setPixelShadows(false);VanillaLight.setEdgeHighlights(false);});
+ server.runOnServer(s->{command(s,"time set 6000");for(int x=-5;x<=24;x++)for(int z=-5;z<=24;z++){
+ s.overworld().setBlockAndUpdate(new BlockPos(x,64,z),Blocks.CONCRETE.white().defaultBlockState());
+ for(int y=65;y<=102;y++)s.overworld().setBlockAndUpdate(new BlockPos(x,y,z),Blocks.AIR.defaultBlockState());
+ }});
+ camera(c,server,10.5,69,5.5,65);look(c,server,0,40);settle(c);
+ var flatOff=capture(c,"ssgi-flat-off");enable(c,true);unchanged(flatOff,capture(c,"ssgi-flat-on"),"Coplanar ground does not bounce light onto itself");
+ wall(server,12,Blocks.CONCRETE.red().defaultBlockState());settle(c);enable(c,false);var redOff=capture(c,"ssgi-red-off");
+ enable(c,true);var redOn=capture(c,"ssgi-red-on");var red=measure(redOff,redOn);System.out.println("SSGI132_RED "+red);
+ check(red.changed>30&&red.red>red.blue*1.5+20,"Visible red wall tints neutral ground: "+red);
+ unchanged(redOn,capture(c,null),"Stationary SSGI has no animated noise or accumulating feedback");
+ look(c,server,0.00002f,40.00002f);frames(c);unchanged(redOn,capture(c,null),"Subpixel motion keeps actual red bounce stable");
+ look(c,server,5,40);frames(c);capture(c,"ssgi-camera-turned");
+ look(c,server,0,40);frames(c);unchanged(redOn,capture(c,null),"Returning the camera leaves no temporal color trails");
+ c.runOnClient(m->VanillaLight.setSsgiIntensity(20));frames(c);var low=measure(redOff,capture(c,"ssgi-strength-20"));
+ check(red.red>low.red*1.5,"Intensity adjusts native color bounce: "+red+" / "+low);
+ c.runOnClient(m->VanillaLight.setSsgiIntensity(0));c.waitTicks(6);unchanged(redOff,capture(c,null),"Zero intensity restores source image");
+ c.runOnClient(m->check(!ScreenSpaceGI.allocated()&&!ScreenSpaceGI.applied(),"Zero frees SSGI targets"));
+ c.runOnClient(m->VanillaLight.setSsgiIntensity(90));frames(c);
+ int oldWidth=c.computeOnClient(m->m.getWindow().getScreenWidth()),oldHeight=c.computeOnClient(m->m.getWindow().getScreenHeight());
+ c.runOnClient(m->m.getWindow().setWindowed(961,541));
+ c.waitFor(m->m.gameRenderer.mainRenderTarget().width!=redOn.width(),200);frames(c);
+ enable(c,false);var resizedOff=capture(c,null);enable(c,true);var resized=measure(resizedOff,capture(c,"ssgi-resized"));
+ check(resized.changed>30&&resized.red>resized.blue*1.5+20,"Resizing reallocates a working SSGI pass: "+resized);
+ c.runOnClient(m->m.getWindow().setWindowed(oldWidth,oldHeight));
+ c.waitFor(m->m.gameRenderer.mainRenderTarget().width==redOn.width(),200);frames(c);
+ wall(server,12,Blocks.CONCRETE.blue().defaultBlockState());settle(c);enable(c,false);var blueOff=capture(c,"ssgi-blue-off");enable(c,true);var blue=measure(blueOff,capture(c,"ssgi-blue-on"));
+ System.out.println("SSGI132_BLUE "+blue);check(blue.changed>30&&blue.blue>blue.red*1.5+20,"Live blue wall changes bounced hue: "+blue);
+ wall(server,11,Blocks.CONCRETE.white().defaultBlockState());settle(c);enable(c,false);var coveredOff=capture(c,null);enable(c,true);var covered=capture(c,"ssgi-covered-blue");
+ wall(server,12,Blocks.CONCRETE.red().defaultBlockState());settle(c);unchanged(covered,capture(c,"ssgi-covered-red"),"Hidden wall color cannot pass through foreground wall");
+ var reload=c.computeOnClient(net.minecraft.client.Minecraft::reloadResourcePacks);c.waitFor(m->reload.isDone()&&m.gui.overlay()==null,1200);reload.join();frames(c);unchanged(covered,capture(c,"ssgi-reloaded"),"Resource reload restores SSGI");
+ look(c,server,0.00002f,40.00002f);frames(c);unchanged(covered,capture(c,null),"Subpixel camera movement keeps bounce stable");
+ look(c,server,0,40);frames(c);enable(c,false);unchanged(coveredOff,capture(c,null),"OFF leaves no residual tint");
+ c.runOnClient(m->check(!ScreenSpaceGI.allocated(),"OFF frees SSGI resources"));
+ c.runOnClient(m->{VanillaLight.setSsgiIntensity(35);VanillaLight.setPixelShadows(true);VanillaLight.setEdgeHighlights(true);VanillaLight.setBloom(true);VanillaLight.setOreEmission(true);});
+ enable(c,true);c.waitFor(m->PixelShadows.applied()&&PixelShadows.ready()&&EdgeHighlights.applied()&&Bloom.applied(),1200);
+ capture(c,"ssgi-combined-shader");
+ c.runOnClient(m->VanillaLight.setEnabled(false));c.waitTicks(8);
+ c.runOnClient(m->check(!ScreenSpaceGI.allocated(),"Main shader OFF releases SSGI too"));
+ c.runOnClient(m->VanillaLight.setEnabled(true));frames(c);enable(c,false);
+ System.out.println("SSGI132_PASS red/blue diffuse bounce; intensity; flat rejection; foreground occlusion; static stability; tiny camera motion; resize; reload; combined shader; OFF/zero; preferences; red="+red+" blue="+blue);
+ }
+ private static void wall(TestServerContext server,int z,BlockState state){server.runOnServer(s->{for(int x=6;x<=15;x++)for(int y=65;y<=(z==11?72:69);y++)s.overworld().setBlockAndUpdate(new BlockPos(x,y,z),state);});}
+ private static void settle(ClientGameTestContext c){c.waitTicks(30);c.waitFor(m->m.levelRenderer.hasRenderedAllSections(),1200);c.waitTicks(8);}
+ private static void enable(ClientGameTestContext c,boolean enabled){c.runOnClient(m->VanillaLight.setSsgi(enabled));if(enabled)frames(c);else c.waitTicks(8);}
+ private static void frames(ClientGameTestContext c){int start=c.computeOnClient(m->ScreenSpaceGI.renderedFrames());c.waitFor(m->ScreenSpaceGI.applied()&&ScreenSpaceGI.renderedFrames()>start+10,1200);c.waitTicks(6);}
+ private static void preferences(){try{
+ var p=FabricLoader.getInstance().getConfigDir().resolve("sanctuary-shaders.json");Files.writeString(p,"{\"bloom\":false,\"shadowPixels\":128}");forgetPreferences();
+ check(!VanillaLight.ssgi()&&VanillaLight.ssgiIntensity()==35&&!VanillaLight.bloom()&&VanillaLight.shadowPixels()==128,"Legacy choices preserved; experimental SSGI defaults OFF at 35");
+ for(int i:new int[]{-10,0,35,90,140}){VanillaLight.setSsgiIntensity(i);VanillaLight.setSsgi(true);forgetPreferences();check(VanillaLight.ssgi()&&VanillaLight.ssgiIntensity()==Math.clamp(i,0,100),"SSGI persists and clamps");}
+ VanillaLight.setSsgiIntensity(90);
+ }catch(Exception e){throw new AssertionError(e);}}
+ private record Gain(int changed,double red,double green,double blue){}
+ private static Gain measure(Frame off,Frame on){int count=0;double r=0,g=0,b=0;for(int i=0;i>16&255,ag=a>>8&255,ab=a&255;
+ if(Math.max(ar,Math.max(ag,ab))-Math.min(ar,Math.min(ag,ab))>25||ar<60||ar>245)continue;
+ int z=on.pixels()[i];int dr=(z>>16&255)-ar,dg=(z>>8&255)-ag,db=(z&255)-ab;
+ if(Math.max(dr,Math.max(dg,db))>1)count++;r+=Math.max(0,dr);g+=Math.max(0,dg);b+=Math.max(0,db);
+ }return new Gain(count,r,g,b);}
+}
diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/shader/ScreenSpaceGI.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/shader/ScreenSpaceGI.java
new file mode 100644
index 0000000..5afc4b3
--- /dev/null
+++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/shader/ScreenSpaceGI.java
@@ -0,0 +1,82 @@
+package fr.koka.sanctuary.client.shader;
+
+import com.mojang.blaze3d.buffers.Std140Builder;
+import com.mojang.blaze3d.pipeline.TextureTarget;
+import com.mojang.blaze3d.systems.RenderSystem;
+import com.mojang.renderpearl.api.GpuFormat;
+import com.mojang.renderpearl.api.buffers.GpuBuffer;
+import com.mojang.renderpearl.api.pipeline.*;
+import com.mojang.renderpearl.api.textures.FilterMode;
+import fr.koka.sanctuary.client.panorama.PanoramaCapture;
+import java.util.Optional;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.renderer.MappableRingBuffer;
+import net.minecraft.client.renderer.RenderPipelines;
+import net.minecraft.resources.Identifier;
+import net.minecraft.world.level.material.FogType;
+import org.joml.Matrix4f;
+
+/** Bounded, single-bounce diffuse lighting from this frame's visible surfaces. No GPU readback or history. */
+public final class ScreenSpaceGI {
+ private static final BindGroupLayout LAYOUT=BindGroupLayout.builder()
+ .withUniform("GIParams",UniformType.UNIFORM_BUFFER)
+ .withUniform("WorldDepth",UniformType.COMBINED_IMAGE_SAMPLER)
+ .withUniform("SceneColor",UniformType.COMBINED_IMAGE_SAMPLER)
+ .withUniform("IndirectLight",UniformType.COMBINED_IMAGE_SAMPLER).build();
+ private static RenderPipeline pipeline(String name,boolean composite){
+ return RenderPipelines.register(RenderPipeline.builder().withLocation(Identifier.parse("sanctuary:pipeline/"+name))
+ .withVertexShader("core/screenquad").withFragmentShader(Identifier.parse("sanctuary:core/"+name))
+ .withBindGroupLayout(LAYOUT).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("ssgi_gather",false),APPLY=pipeline("ssgi_apply",true);
+ private static final Matrix4f projection=new Matrix4f();
+ private static TextureTarget source,indirect;
+ private static MappableRingBuffer uniforms;
+ private static boolean applied;
+ private static int frames;
+ private ScreenSpaceGI(){}
+ public static void captureProjection(Matrix4f matrix){projection.set(matrix);}
+ public static void render(){
+ applied=false;var mc=Minecraft.getInstance();
+ if(!VanillaLight.active()||!VanillaLight.ssgi()||VanillaLight.ssgiIntensity()==0||mc.level==null){release();return;}
+ if(PanoramaCapture.rendering())return;
+ var camera=mc.gameRenderer.gameRenderState().levelRenderState.cameraRenderState;
+ if(camera.fogType!=FogType.NONE)return;
+ var main=mc.gameRenderer.mainRenderTarget();
+ if(source==null||source.width!=main.width||source.height!=main.height){
+ release();source=new TextureTarget("Sanctuary SSGI source",main.width,main.height,GpuFormat.RGBA8_UNORM,null);
+ indirect=new TextureTarget("Sanctuary SSGI indirect",Math.max(1,(main.width+1)/2),Math.max(1,(main.height+1)/2),GpuFormat.RGBA16_FLOAT,null);
+ }
+ if(uniforms==null)uniforms=new MappableRingBuffer(()->"Sanctuary SSGI",GpuBuffer.USAGE_UNIFORM|GpuBuffer.USAGE_MAP_WRITE,176);
+ var vp=new Matrix4f(projection).mul(camera.viewRotationMatrix);var fog=camera.fogData;
+ uniforms.rotate();try(var mapped=uniforms.currentBuffer().map(false,true)){
+ Std140Builder.intoBuffer(mapped.data()).putMat4f(new Matrix4f(vp).invert()).putMat4f(vp)
+ .putVec4(VanillaLight.ssgiIntensity()/100f,RenderSystem.getDevice().getDeviceInfo().isZZeroToOne()?1:0,2.5f,32)
+ .putVec4(fog.renderDistanceStart,fog.renderDistanceEnd,fog.environmentalStart,fog.environmentalEnd)
+ .putVec4(fog.color.w,0,0,0);
+ }
+ // Snapshot once, before adding indirect light: no feedback, no texture sampled while attached for writing.
+ var encoder=RenderSystem.getDevice().createCommandEncoder();
+ encoder.copyTextureToTexture(main.getColorTexture(),source.getColorTexture(),0,0,0,0,0,main.width,main.height);
+ var nearest=RenderSystem.getSamplerCache().getClampToEdge(FilterMode.NEAREST);
+ try(var pass=encoder.createRenderPass(()->"Sanctuary SSGI gather",indirect.getColorTextureView(),Optional.empty())){
+ pass.setPipeline(RenderSystem.getCompiledPipeline(GATHER));pass.setUniform("GIParams",uniforms.currentBuffer());
+ pass.setUniform("WorldDepth",main.getDepthTextureView(),nearest);pass.setUniform("SceneColor",source.getColorTextureView(),nearest);
+ // Unused by gather; bind a separate texture, never its own output.
+ pass.setUniform("IndirectLight",source.getColorTextureView(),nearest);pass.draw(3,1,0,0);
+ }
+ try(var pass=encoder.createRenderPass(()->"Sanctuary SSGI composite",main.getColorTextureView(),Optional.empty())){
+ pass.setPipeline(RenderSystem.getCompiledPipeline(APPLY));pass.setUniform("GIParams",uniforms.currentBuffer());
+ pass.setUniform("WorldDepth",main.getDepthTextureView(),nearest);pass.setUniform("SceneColor",source.getColorTextureView(),nearest);
+ pass.setUniform("IndirectLight",indirect.getColorTextureView(),nearest);pass.draw(3,1,0,0);
+ }
+ applied=true;frames++;
+ }
+ public static void release(){if(source!=null){source.destroyBuffers();indirect.destroyBuffers();source=indirect=null;}if(uniforms!=null){uniforms.close();uniforms=null;}applied=false;}
+ static boolean applied(){return applied;}
+ static boolean allocated(){return source!=null;}
+ static int renderedFrames(){return frames;}
+}
diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/shader/ShaderOptionsScreen.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/shader/ShaderOptionsScreen.java
index f691655..5b5e38f 100644
--- a/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/shader/ShaderOptionsScreen.java
+++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/shader/ShaderOptionsScreen.java
@@ -20,6 +20,9 @@ public final class ShaderOptionsScreen extends OptionsSubScreen {
list.addBig(new OptionInstance("sanctuary.shaders.precision",OptionInstance.cachedConstantTooltip(
Component.translatable("sanctuary.shaders.precision_help")),(caption,value)->Component.translatable("sanctuary.shaders.precision_value",value),
new OptionInstance.Enum<>(List.of(8,16,32,64,128),Codec.INT),VanillaLight.shadowPixels(),VanillaLight::setShadowPixels));
+ list.addBig(OptionInstance.createBoolean("sanctuary.shaders.ssgi",OptionInstance.cachedConstantTooltip(
+ Component.translatable("sanctuary.shaders.ssgi_help")),VanillaLight.ssgi(),VanillaLight::setSsgi));
+ slider("ssgi_intensity",VanillaLight.ssgiIntensity(),VanillaLight::setSsgiIntensity);
list.addBig(OptionInstance.createBoolean("sanctuary.shaders.bloom",OptionInstance.cachedConstantTooltip(
Component.translatable("sanctuary.shaders.bloom_help")),VanillaLight.bloom(),VanillaLight::setBloom));
slider("bloom_intensity",VanillaLight.bloomIntensity(),VanillaLight::setBloomIntensity);
diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/shader/VanillaLight.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/shader/VanillaLight.java
index ce15151..7df2f32 100644
--- a/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/shader/VanillaLight.java
+++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/shader/VanillaLight.java
@@ -19,6 +19,8 @@ public final class VanillaLight {
int saturation=142;
boolean pixelShadows=true;
boolean bloom=true;
+ boolean ssgi=false;
+ int ssgiIntensity=35;
int bloomIntensity=35;
int bloomRadius=50;
boolean oreEmission=true;
@@ -35,6 +37,7 @@ public final class VanillaLight {
try{if(Files.isRegularFile(path()))preferences=new GsonBuilder().create().fromJson(Files.readString(path()),Preferences.class);}
catch(Exception e){SanctuaryMod.LOGGER.warn("Cannot read Sanctuary shader preference",e);}
if(preferences==null)preferences=new Preferences();
+ preferences.ssgiIntensity=Math.clamp(preferences.ssgiIntensity,0,100);
preferences.bloomIntensity=Math.clamp(preferences.bloomIntensity,0,100);
preferences.bloomRadius=Math.clamp(preferences.bloomRadius,0,100);
preferences.oreIntensity=Math.clamp(preferences.oreIntensity,0,100);
@@ -55,6 +58,10 @@ public final class VanillaLight {
public static int edgeIntensity(){enabled();return preferences.edgeIntensity;}
public static void setEdgeHighlights(boolean value){enabled();preferences.edgeHighlights=value;save();}
public static void setEdgeIntensity(int value){enabled();preferences.edgeIntensity=Math.clamp(value,0,100);save();}
+ public static boolean ssgi(){enabled();return preferences.ssgi;}
+ public static int ssgiIntensity(){enabled();return preferences.ssgiIntensity;}
+ public static void setSsgi(boolean value){enabled();preferences.ssgi=value;save();}
+ public static void setSsgiIntensity(int value){enabled();preferences.ssgiIntensity=Math.clamp(value,0,100);save();}
public static boolean bloom(){enabled();return preferences.bloom;}
public static int bloomIntensity(){enabled();return preferences.bloomIntensity;}
public static int bloomRadius(){enabled();return preferences.bloomRadius;}
diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/client/PixelShadowDirtyMixin.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/client/PixelShadowDirtyMixin.java
index bcf7c88..fab2c38 100644
--- a/mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/client/PixelShadowDirtyMixin.java
+++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/client/PixelShadowDirtyMixin.java
@@ -2,6 +2,7 @@ package fr.koka.sanctuary.mixin.client;
import fr.koka.sanctuary.client.shader.PixelShadows;
import fr.koka.sanctuary.client.shader.Bloom;
+import fr.koka.sanctuary.client.shader.ScreenSpaceGI;
import net.minecraft.client.renderer.extract.LevelExtractor;
import net.minecraft.core.BlockPos;
import org.spongepowered.asm.mixin.Mixin;
@@ -17,5 +18,5 @@ abstract class PixelShadowDirtyMixin {
PixelShadows.dirtyBlock(pos); Bloom.dirtyBlock(pos);
}
@Inject(method="allChanged", at=@At("HEAD"))
- private void sanctuary$reload(CallbackInfo ci) { PixelShadows.release(); Bloom.release(); }
+ private void sanctuary$reload(CallbackInfo ci) { PixelShadows.release(); Bloom.release(); ScreenSpaceGI.release(); }
}
diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/client/PixelShadowRendererMixin.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/client/PixelShadowRendererMixin.java
index 945bed7..a348bb0 100644
--- a/mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/client/PixelShadowRendererMixin.java
+++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/client/PixelShadowRendererMixin.java
@@ -6,6 +6,7 @@ import com.mojang.renderpearl.api.buffers.GpuBufferSlice;
import fr.koka.sanctuary.client.shader.PixelShadows;
import fr.koka.sanctuary.client.shader.EdgeHighlights;
import fr.koka.sanctuary.client.shader.Bloom;
+import fr.koka.sanctuary.client.shader.ScreenSpaceGI;
import net.minecraft.client.renderer.GameRenderer;
import net.minecraft.client.renderer.ProjectionMatrixBuffer;
import org.joml.Matrix4f;
@@ -21,10 +22,11 @@ abstract class PixelShadowRendererMixin {
PixelShadows.captureProjection(matrix);
EdgeHighlights.captureProjection(matrix);
Bloom.captureProjection(matrix);
+ ScreenSpaceGI.captureProjection(matrix);
return original.call(buffer, matrix);
}
@Inject(method="renderLevel", at=@At(value="INVOKE", target="Lnet/minecraft/client/renderer/GameRenderer;render3dHud(Lnet/minecraft/client/renderer/state/level/CameraRenderState;Lnet/minecraft/client/renderer/state/level/PlayerRenderState;Lnet/minecraft/client/renderer/state/OptionsRenderState;Z)V"))
- private void sanctuary$shadows(CallbackInfo ci) { PixelShadows.render(); EdgeHighlights.render(); Bloom.render(); }
+ private void sanctuary$shadows(CallbackInfo ci) { PixelShadows.render(); ScreenSpaceGI.render(); EdgeHighlights.render(); Bloom.render(); }
@Inject(method={"close", "setLevel"}, at=@At("HEAD"))
- private void sanctuary$close(CallbackInfo ci) { PixelShadows.release(); EdgeHighlights.release(); Bloom.release(); }
+ private void sanctuary$close(CallbackInfo ci) { PixelShadows.release(); EdgeHighlights.release(); Bloom.release(); ScreenSpaceGI.release(); }
}
diff --git a/mods/sanctuary/src/main/resources/assets/sanctuary/lang/en_us.json b/mods/sanctuary/src/main/resources/assets/sanctuary/lang/en_us.json
index 271aeb7..5acae43 100644
--- a/mods/sanctuary/src/main/resources/assets/sanctuary/lang/en_us.json
+++ b/mods/sanctuary/src/main/resources/assets/sanctuary/lang/en_us.json
@@ -2288,5 +2288,10 @@
"sanctuary.shaders.ore_emission_help": "Makes ore inclusions glow while preserving their host rock, even with bloom off or at zero. ON by default. Visual effect only.",
"sanctuary.shaders.ore_intensity": "Ore emission",
"sanctuary.shaders.ore_intensity_help": "Inclusion brightness. Does not change world light levels or mob spawning.",
- "sanctuary.shaders.ore_intensity_value": "Ores: %s%%"
+ "sanctuary.shaders.ore_intensity_value": "Ores: %s%%",
+ "sanctuary.shaders.ssgi": "SSGI · color bounce",
+ "sanctuary.shaders.ssgi_help": "Experimental: subtle color bounce between nearby visible surfaces. Radius 2.5 blocks. Off-screen surfaces do not contribute. OFF by default.",
+ "sanctuary.shaders.ssgi_intensity": "SSGI strength",
+ "sanctuary.shaders.ssgi_intensity_help": "Strength of diffuse color bounce. Zero stops rendering and frees its resources. Does not change world light.",
+ "sanctuary.shaders.ssgi_intensity_value": "SSGI: %s%%"
}
diff --git a/mods/sanctuary/src/main/resources/assets/sanctuary/lang/fr_fr.json b/mods/sanctuary/src/main/resources/assets/sanctuary/lang/fr_fr.json
index b3d4aa2..899158f 100644
--- a/mods/sanctuary/src/main/resources/assets/sanctuary/lang/fr_fr.json
+++ b/mods/sanctuary/src/main/resources/assets/sanctuary/lang/fr_fr.json
@@ -2288,5 +2288,10 @@
"sanctuary.shaders.ore_emission_help": "Fait briller les inclusions des minerais sans éclairer leur roche, même avec le bloom coupé ou à zéro. Activé par défaut. Effet visuel uniquement.",
"sanctuary.shaders.ore_intensity": "Émission des minerais",
"sanctuary.shaders.ore_intensity_help": "Luminosité des inclusions. Aucun changement de la lumière du monde ou des apparitions de mobs.",
- "sanctuary.shaders.ore_intensity_value": "Minerais : %s %%"
+ "sanctuary.shaders.ore_intensity_value": "Minerais : %s %%",
+ "sanctuary.shaders.ssgi": "SSGI · couleurs diffusées",
+ "sanctuary.shaders.ssgi_help": "Expérimental : légère diffusion des couleurs entre surfaces visibles proches. Rayon 2,5 blocs. Les surfaces hors écran ne contribuent pas. Désactivé par défaut.",
+ "sanctuary.shaders.ssgi_intensity": "Intensité du SSGI",
+ "sanctuary.shaders.ssgi_intensity_help": "Force de la teinte diffusée. Zéro coupe le calcul et libère ses ressources. Aucun changement de lumière du monde.",
+ "sanctuary.shaders.ssgi_intensity_value": "SSGI : %s %%"
}
diff --git a/mods/sanctuary/src/main/resources/assets/sanctuary/shaders/core/ssgi_apply.fsh b/mods/sanctuary/src/main/resources/assets/sanctuary/shaders/core/ssgi_apply.fsh
new file mode 100644
index 0000000..08c614a
--- /dev/null
+++ b/mods/sanctuary/src/main/resources/assets/sanctuary/shaders/core/ssgi_apply.fsh
@@ -0,0 +1,26 @@
+#version 330
+#extension GL_ARB_separate_shader_objects : require
+#include
+layout(location=0) in vec2 texCoord;
+layout(location=0) out vec4 fragColor;
+void main(){
+ float depth=texture(WorldDepth,texCoord).r;
+ if(depth<=0.0)discard;
+ vec3 p=giPosition(texCoord,depth),normal=giNormal(texCoord,p);
+ vec2 size=vec2(textureSize(IndirectLight,0)),coord=texCoord*size-0.5,base=floor(coord),f=fract(coord);
+ vec3 bounce=vec3(0.0);float total=0.0;
+ for(int y=0;y<2;y++)for(int x=0;x<2;x++){
+ vec2 uv=(base+vec2(x,y)+0.5)/size;
+ vec4 sampleLight=texture(IndirectLight,uv);
+ if(sampleLight.a<=0.0)continue;
+ vec2 surfaceUV=giTexel(uv);vec3 q=giPosition(surfaceUV,texture(WorldDepth,surfaceUV).r);
+ float plane=abs(dot(q-p,normal));
+ // Bilateral reconstruction never blends a wall's light into the foreground silhouette.
+ if(plane>0.06||length(q-p)>0.5)continue;
+ float weight=(x==0?1.0-f.x:f.x)*(y==0?1.0-f.y:f.y)*(1.0-smoothstep(0.01,0.06,plane));
+ bounce+=sampleLight.rgb*weight;total+=weight;
+ }
+ if(total<=0.0001)discard;
+ vec3 albedo=texture(SceneColor,texCoord).rgb;
+ fragColor=vec4(clamp(albedo*bounce/total*GISettings.x*2.0,0.0,0.25),0.0);
+}
diff --git a/mods/sanctuary/src/main/resources/assets/sanctuary/shaders/core/ssgi_gather.fsh b/mods/sanctuary/src/main/resources/assets/sanctuary/shaders/core/ssgi_gather.fsh
new file mode 100644
index 0000000..31eb3c9
--- /dev/null
+++ b/mods/sanctuary/src/main/resources/assets/sanctuary/shaders/core/ssgi_gather.fsh
@@ -0,0 +1,47 @@
+#version 330
+#extension GL_ARB_separate_shader_objects : require
+#include
+layout(location=0) in vec2 texCoord;
+layout(location=0) out vec4 fragColor;
+void main(){
+ fragColor=vec4(0.0);
+ vec2 uv=giTexel(texCoord);float depth=texture(WorldDepth,uv).r;
+ if(depth<=0.0)return;
+ vec3 p=giPosition(uv,depth);float distance=length(p);
+ if(distance>=GISettings.w)return;
+ vec3 n=giNormal(uv,p);
+ if(length(n)<0.9)return;
+ vec3 tangent=normalize(cross(n,abs(n.y)<0.9?vec3(0,1,0):vec3(0,0,1)));
+ vec3 bitangent=cross(n,tangent),sum=vec3(0.0);
+ // Fixed hemisphere directions; no per-frame stochastic noise or temporal history.
+ for(int ray=0;ray<12;ray++){
+ float angle=6.2831853*(float(ray<8?ray:ray-8)+0.5)/float(ray<8?8:4);
+ float elevation=ray<8?0.4:0.85;
+ vec3 direction=n*elevation+(tangent*cos(angle)+bitangent*sin(angle))*sqrt(1.0-elevation*elevation);
+ vec3 start=p+n*0.045;
+ for(int step=1;step<=10;step++){
+ float travel=GISettings.z*float(step)/10.0;
+ vec3 probe=start+direction*travel;
+ vec4 clip=ViewProjection*vec4(probe,1.0);
+ if(clip.w<=0.0)break;
+ vec2 hitUV=clip.xy/clip.w*0.5+0.5;
+ if(any(lessThan(hitUV,vec2(0.002)))||any(greaterThan(hitUV,vec2(0.998))))break;
+ hitUV=giTexel(hitUV);float hitDepth=texture(WorldDepth,hitUV).r;
+ if(hitDepth<=0.0)continue;
+ vec3 hit=giPosition(hitUV,hitDepth);float behind=length(probe)-length(hit);
+ if(behind<=0.015)continue;
+ // First occluder terminates the ray even when too thick: never continue behind a wall.
+ vec3 delta=hit-p;float separation=length(delta);
+ if(behind<0.35&&separation>0.08&&separation0.03){
+ vec3 hitNormal=giNormal(hitUV,hit);
+ float facing=max(0.0,dot(hitNormal,-normalize(delta)));
+ float edge=smoothstep(0.0,0.06,min(min(hitUV.x,hitUV.y),min(1.0-hitUV.x,1.0-hitUV.y)));
+ vec3 color=texture(SceneColor,hitUV).rgb;
+ float falloff=1.0-separation/GISettings.z;
+ sum+=color*color*facing*falloff*falloff*edge*giFog(hit);
+ }
+ break;
+ }
+ }
+ fragColor=vec4(sum/12.0*giFog(p)*(1.0-smoothstep(GISettings.w*0.75,GISettings.w,distance)),distance);
+}
diff --git a/mods/sanctuary/src/main/resources/assets/sanctuary/shaders/include/ssgi.glsl b/mods/sanctuary/src/main/resources/assets/sanctuary/shaders/include/ssgi.glsl
new file mode 100644
index 0000000..4bf3bd7
--- /dev/null
+++ b/mods/sanctuary/src/main/resources/assets/sanctuary/shaders/include/ssgi.glsl
@@ -0,0 +1,31 @@
+uniform sampler2D WorldDepth;
+uniform sampler2D SceneColor;
+uniform sampler2D IndirectLight;
+layout(std140) uniform GIParams {
+ mat4 InverseViewProjection;
+ mat4 ViewProjection;
+ vec4 GISettings; // strength, zero-to-one clip, local radius, view range
+ vec4 GIFog;
+ vec4 GIEnvironment;
+};
+vec3 giPosition(vec2 uv,float depth){
+ vec4 p=InverseViewProjection*vec4(uv*2.0-1.0,GISettings.y>0.5?depth:depth*2.0-1.0,1.0);
+ return p.xyz/p.w;
+}
+vec2 giTexel(vec2 uv){vec2 size=vec2(textureSize(WorldDepth,0));return (floor(uv*size)+0.5)/size;}
+vec3 giNormal(vec2 uv,vec3 p){
+ vec2 pixel=1.0/vec2(textureSize(WorldDepth,0));
+ vec3 l=giPosition(uv-vec2(pixel.x,0),texture(WorldDepth,uv-vec2(pixel.x,0)).r);
+ vec3 r=giPosition(uv+vec2(pixel.x,0),texture(WorldDepth,uv+vec2(pixel.x,0)).r);
+ vec3 d=giPosition(uv-vec2(0,pixel.y),texture(WorldDepth,uv-vec2(0,pixel.y)).r);
+ vec3 u=giPosition(uv+vec2(0,pixel.y),texture(WorldDepth,uv+vec2(0,pixel.y)).r);
+ vec3 dx=length(r-p)