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)