diff --git a/README.md b/README.md index 3a83742..3394b0b 100644 --- a/README.md +++ b/README.md @@ -20,12 +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 à implémenter. -La version **beta.133** renforce le **SSGI · couleurs diffusées** : atténuation -spatiale revue et composition lumineuse plus visible dès 35 %. -**Options → Shaders** ; intensité réglable, préférences existantes conservées. -Le [ticket de livraison](docs/shader-beta133.md) décrit le rendu et ses limites. -Release publiée et même instance **Sanctuary Beta** synchronisée, sauvegardes -et réglages conservés. +La version **beta.134** ajoute le **PBR · relief des matériaux** optionnel : +normales procédurales mises en cache, rugosité par matériau et reflets orientés. +Le SSGI reçoit un filtre spatial guidé par la profondeur et des normales de faces +stabilisées. **Options → Shaders** ; réglages existants conservés. +Le [ticket de livraison](docs/shader-beta134.md) décrit le rendu et ses limites. 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. @@ -36,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. +## beta.134 — PBR optionnel et SSGI filtré + +Relief et reflets des matériaux ; lissage de la lumière indirecte. +[Contrat et vérifications](docs/shader-beta134.md) · +[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.134/Sanctuary-beta.134.mrpack) · +[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.134/Sanctuary-Test-beta.134.mrpack). + ## beta.133 — SSGI renforcé Couleurs voisines plus visibles, même budget de rayons et de cibles GPU. diff --git a/docs/shader-beta134.md b/docs/shader-beta134.md new file mode 100644 index 0000000..98759b6 --- /dev/null +++ b/docs/shader-beta134.md @@ -0,0 +1,93 @@ +# SHADER-134 — PBR procédural optionnel et herbe SSGI + +Demande : ajouter en option des normales calculées à partir des textures actives, +avec rugosité des matériaux et reflets orientés. Retour : le SSGI renforcé +éclaire artificiellement l'intérieur de l'herbe ; vérifier et corriger ce cas. +Branche `codex/pbr-beta134`, socle beta.133, Minecraft 26.3 / Java 25. + +## Contrat + +PBR initialement OFF, intensité 50 réglable 0–100. Cartes de normales à partir +des différences de luminance, amplitude limitée ; rugosité/métallicité par +famille de blocs, repli rugueux pour les types inconnus. Analyse des ressources +actives conservée en cache jusqu'à désactivation ou rechargement ; pas de fichiers +écrits dans le pack personnel, pas de GPU readback en production. + +Portée 32 blocs, sections visibles construites progressivement. Budget de maillage +24 Mio, atlas de 4 096 matériaux en 16 × 16 avec bordures (1 280 × 1 280 RGBA8, +6,25 Mio), copie RGBA8 de l'image courante et uniformes. Les textures de plus haute +résolution sont échantillonnées à 16 × 16 ; première image des textures animées. +La couleur et l'animation restent celles de l'atlas natif. Pas de parallax ni +de géométrie ajoutée aux blocs ; les motifs clairs peuvent donner un faux relief. + +La passe utilise une réponse spéculaire GGX (normales, rugosité, métallicité, +Fresnel) et module l'éclairage diffus déjà rendu. Elle ne remplace pas tout le +pipeline Minecraft par un moteur PBR physique. Soleil/lune, exposition au ciel et +carte d'ombres active limitent les reflets ; pas de reflets miroir, d'environnement +SSR, ni de direction locale déduite pour chaque torche. Aucun rendu PBR d'eau, +d'entités, d'items ou de modèles spéciaux hors maillage de terrain. + +## SSGI + +Les normales des faces presque axiales sont stabilisées sur leur axe. Deux passes +spatiales filtrent la lumière indirecte à demi-résolution, avec rejet des voisins +hors du plan du récepteur et interpolation de l'échantillonnage. Les textures de +l'image restent nettes. Un tampon RGBA16F supplémentaire à demi-résolution porte +le budget des cibles SSGI à environ 8 octets par pixel plein écran (contre 6). +Pas d'historique temporel ni de rémanence ajoutés. + +Un parcours de 12 blocs a reproduit des bandes sur un mur et un éclaircissement +artificiel d'herbe à 35 et 100. Le filtrage seul ne suffisait pas : la collecte +réinjectait aussi trop fortement la lumière ambiante neutre. Elle conserve +maintenant la composante chromatique de la source, en soustrayant son canal +linéaire minimum. L'éclairage ambiant neutre reste celui de Minecraft. Ce réglage +vise le transfert de couleur voulu, pas une simulation physique complète du +rebond blanc. Les sources hors champ/cachées restent absentes du SSGI ; le résultat +peut encore varier lors d'un déplacement important. PBR et SSGI restent indépendants. + +## Vérification + +Test PBR ciblé réussi en 1 min 13 s : relief éclairé/ombré, intensités 20/50/100, +soleil matin/soir, cache stable, préférences, OFF/zéro, mouvement infinitésimal, +redimensionnement, rechargement et combinaison des effets. Le premier parcours +SSGI est conservé comme reproduction ; résultats finaux ci-dessous. + + +Le parcours corrigé a réussi en **2 min 47 s** avec 13 positions réparties sur +12 blocs, à 35 et 100, chaque fois comparées à OFF. Un mob immobile, des brins +d'herbe et un mur figurent dans la scène. Retour au point initial sans rémanence. +Sur les captures de ce parcours, le maximum du gain vert moyen mesuré dans +l'herbe passe sous 9,73 niveaux à 35 et 24,51 à 100. Le test impose ensuite +respectivement des limites de 12 et 30 à chaque position. L'essai reproduisant +le défaut atteignait notamment 23,68 et 49,20 à l'extrémité droite. +Ces mesures décrivent cette scène, pas une garantie d'absence d'artefacts +dans tous les mondes ni sur toutes les cartes graphiques. + + +La suite client complète réussit en **7 min** sur macOS / Apple M1 / OpenGL. +Elle inclut le parcours latéral et ses seuils de gain, les deux couleurs de rebond, +le PBR, les ombres, le feuillage, les cinq précisions, le CTM, les émissifs, le +bloom, les préférences et les écrans FR/EN. Log : `build/shader134-client-full.log`. +Le contrôle PBR final réussit en **1 min 19 s** : placement/retrait d'un toit, +invalidations de lumière/maillage et rendu combiné avec les ombres actives. Les +reflets utilisent maintenant la grille des ombres pour leur test de visibilité. Aucun résultat Windows/Vulkan ni +mesure de FPS revendiqués. + + +## Construction et artefacts + +`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest` +réussit en **2 min 22 s**, 126 tâches. Le serveur GameTest dédié reste exclu +conformément au refus EULA antérieur. Log : `build/shader134-build.log`. +Packs normal/Test : même JAR Sanctuary. Sources archivées et template identiques +aux sources. Comparaison à beta.133 : **23 entrées de production** changées, +limitées au rendu, à ses options et points d'appel. Gameplay et gemmes inchangés. + +- `Sanctuary-beta.134.mrpack` : 10421971 octets, SHA-256 `d5bc79acb1d56e7dcf83c976156fdb2fcde80c98e443bde8a6ab312d56887b7f`. +- `Sanctuary-Test-beta.134.mrpack` : 10440898 octets, SHA-256 `04308ebb9adaf76b30a3cbf4fa32ab897e76842803fc8c8411cfa32952d1bdd5`. + +JAR : `a6e69622a5e6629f70dd19b7494ad3b00916bc02169b73635f0e2d563929b7fa`. + +Comparateur local : `build/Shader-beta.134-comparaison.html`. Il contient les +captures natives PBR OFF/50/100 et le parcours SSGI de 12 blocs OFF/35/100. +Le parcours est une succession de positions enregistrées, pas une mesure des FPS. diff --git a/gradle.properties b/gradle.properties index cc9ecae..52d65ca 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.133 -pack_version=beta.133 +mod_version=beta.134 +pack_version=beta.134 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 ebadbff..13d7973 100644 --- a/mods/sanctuary/build.gradle +++ b/mods/sanctuary/build.gradle @@ -93,6 +93,9 @@ tasks.named('runGameTest') { } tasks.named('processGametestResources') { + def pbr134Client = providers.gradleProperty('sanctuaryPbr134ClientTests').map { it.toBoolean() }.getOrElse(false) + inputs.property('sanctuaryPbr134ClientTests', pbr134Client) + if (pbr134Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.shader.Pbr134ClientChecks') } 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') } diff --git a/mods/sanctuary/src/clientGametest/java/fr/koka/sanctuary/client/shader/Pbr134ClientChecks.java b/mods/sanctuary/src/clientGametest/java/fr/koka/sanctuary/client/shader/Pbr134ClientChecks.java new file mode 100644 index 0000000..18f45b5 --- /dev/null +++ b/mods/sanctuary/src/clientGametest/java/fr/koka/sanctuary/client/shader/Pbr134ClientChecks.java @@ -0,0 +1,73 @@ +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 java.nio.file.Files; +import java.util.Arrays; + +public final class Pbr134ClientChecks implements FabricClientGameTest { + public void runTest(ClientGameTestContext c){new PixelShadows122ClientChecks(false,false,true).runTest(c);} + static void run(ClientGameTestContext c,TestServerContext server){ + c.runOnClient(m->{preferences();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++){ + 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()); + }}); + camera(c,server,10.5,69,5.5,65);look(c,server,0,40);settle(c); + var off=capture(c,"pbr-off");enable(c,true);var on=capture(c,"pbr-50"); + var delta=difference(off,on);System.out.println("PBR134_DIFF "+delta);check(delta.changed>500&&delta.brighter>100&&delta.darker>100,"Texture relief has lit and shaded pixels: "+delta); + int analyses=c.computeOnClient(m->ProceduralPbr.analyses());c.waitTicks(20); + c.runOnClient(m->check(analyses==ProceduralPbr.analyses()&&ProceduralPbr.materialCount()>0,"Source textures are cached, not regenerated every frame")); + unchanged(on,capture(c,null),"Static PBR has no animated noise"); + look(c,server,.00002f,40.00002f);frames(c);unchanged(on,capture(c,null),"PBR tiny camera motion");look(c,server,0,40);frames(c); + c.runOnClient(m->VanillaLight.setPbrIntensity(20));frames(c);var low=difference(off,capture(c,"pbr-20")); + check(delta.total>low.total*1.3,"PBR strength affects actual GPU output"); + c.runOnClient(m->VanillaLight.setPbrIntensity(100));frames(c);var high=difference(off,capture(c,"pbr-100"));check(high.total>delta.total*1.2,"Strong PBR visibly increases relief"); + c.runOnClient(m->VanillaLight.setPbrIntensity(0));c.waitTicks(8);unchanged(off,capture(c,null),"PBR zero restores native image"); + c.runOnClient(m->check(!ProceduralPbr.allocated()&&ProceduralPbr.materialCount()==0,"Zero releases targets and material cache")); + c.runOnClient(m->VanillaLight.setPbrIntensity(50));frames(c); + server.runOnServer(s->command(s,"time set 11000"));settle(c);enable(c,false);var eveningOff=capture(c,null);enable(c,true);var eveningOn=capture(c,"pbr-evening"); + int directions=0;for(int i=0;i>16&255)-(off.pixels()[i]>>16&255),pm=(eveningOn.pixels()[i]>>16&255)-(eveningOff.pixels()[i]>>16&255);if(Math.abs(am-pm)>5)directions++;} + check(directions>500,"Relief follows the moving sun: "+directions); + var reload=c.computeOnClient(net.minecraft.client.Minecraft::reloadResourcePacks);c.waitFor(m->reload.isDone()&&m.gui.overlay()==null,1200);reload.join();frames(c);unchanged(eveningOn,capture(c,"pbr-reloaded"),"Resource reload rebuilds consistent cached materials"); + // Live geometry and light invalidation: an enclosing roof removes exposure to the sky. + server.runOnServer(s->{for(int x=-8;x<=28;x++)for(int z=-8;z<=28;z++)s.overworld().setBlockAndUpdate(new BlockPos(x,73,z),Blocks.STONE.defaultBlockState());}); + settle(c);frames(c);var roofOn=capture(c,"pbr-roof-on");enable(c,false);var roofOff=capture(c,"pbr-roof-off"); + unchanged(roofOff,roofOn,"Roof prevents outdoor highlights on enclosed floor");enable(c,true); + server.runOnServer(s->{for(int x=-8;x<=28;x++)for(int z=-8;z<=28;z++)s.overworld().setBlockAndUpdate(new BlockPos(x,73,z),Blocks.AIR.defaultBlockState());}); + settle(c);frames(c);unchanged(eveningOn,capture(c,"pbr-roof-removed"),"Removing roof restores material light without stale meshes"); + 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!=on.width(),200);frames(c);capture(c,"pbr-resized"); + c.runOnClient(m->m.getWindow().setWindowed(oldWidth,oldHeight));c.waitFor(m->m.gameRenderer.mainRenderTarget().width==on.width(),200);frames(c); + c.runOnClient(m->{VanillaLight.setPixelShadows(true);VanillaLight.setSsgi(true);VanillaLight.setBloom(true);VanillaLight.setOreEmission(true);VanillaLight.setEdgeHighlights(true);}); + c.waitFor(m->PixelShadows.applied()&&PixelShadows.ready()&&ScreenSpaceGI.applied()&&Bloom.applied()&&EdgeHighlights.applied()&&ProceduralPbr.applied(),1200);frames(c);capture(c,"pbr-combined"); + c.runOnClient(m->VanillaLight.setEnabled(false));c.waitTicks(8);c.runOnClient(m->check(!ProceduralPbr.allocated()&&ProceduralPbr.materialCount()==0,"Global OFF releases PBR")); + c.runOnClient(m->{VanillaLight.setEnabled(true);VanillaLight.setPbr(false);VanillaLight.setSsgi(false);}); + System.out.println("PBR134_PASS normals; profiles; texture cache; directional lighting; intensity; OFF/zero; tiny camera; reload; resize; combined; default="+delta+" high="+high+" directional="+directions); + } + private static void materials(){ + int[] flat=new int[16];Arrays.fill(flat,0xFF808080);var profile=PbrMaterials.profile("bricks"); + for(int p:PbrMaterials.generate(flat,4,4,profile))check((p>>8&0xFFFF)==0x8080,"Flat source has neutral normal"); + flat[5]=0xFFFFFFFF;var slope=PbrMaterials.generate(flat,4,4,profile);check((slope[4]>>16&255)<128&&(slope[6]>>16&255)>128,"Height gradients produce opposite normal slopes"); + check(PbrMaterials.profile("iron_block").metal()>.8&&PbrMaterials.profile("iron_ore").metal()==0,"Metal block profile does not turn ore host rock into metal"); + check(PbrMaterials.profile("oak_planks").roughness()>PbrMaterials.profile("polished_andesite").roughness(),"Wood remains rougher than polished stone"); + } + private static void preferences(){try{ + var p=FabricLoader.getInstance().getConfigDir().resolve("sanctuary-shaders.json");Files.writeString(p,"{\"ssgi\":true,\"ssgiIntensity\":35,\"bloom\":false}");forgetPreferences(); + check(!VanillaLight.pbr()&&VanillaLight.pbrIntensity()==50&&VanillaLight.ssgi()&&!VanillaLight.bloom(),"PBR optional default preserves old choices"); + for(int n:new int[]{-1,0,50,100,101}){VanillaLight.setPbr(true);VanillaLight.setPbrIntensity(n);forgetPreferences();check(VanillaLight.pbr()&&VanillaLight.pbrIntensity()==Math.clamp(n,0,100),"PBR preference persistence/clamp");} + VanillaLight.setPbrIntensity(50); + }catch(Exception e){throw new AssertionError(e);}} + private record Delta(int changed,int brighter,int darker,long total){} + private static Delta difference(Frame off,Frame on){int changed=0,bright=0,dark=0;long total=0;for(int i=0;i>16&255)-(a>>16&255);total+=Math.abs(delta);if(Math.abs(delta)>3)changed++;if(delta>3)bright++;if(delta< -3)dark++; + }return new Delta(changed,bright,dark,total);} + private static void settle(ClientGameTestContext c){c.waitTicks(35);c.waitFor(m->m.levelRenderer.hasRenderedAllSections(),1200);c.waitTicks(10);} + private static void enable(ClientGameTestContext c,boolean on){c.runOnClient(m->VanillaLight.setPbr(on));if(on)frames(c);else c.waitTicks(8);} + private static void frames(ClientGameTestContext c){int start=c.computeOnClient(m->ProceduralPbr.renderedFrames());c.waitFor(m->ProceduralPbr.applied()&&ProceduralPbr.ready()&&ProceduralPbr.renderedFrames()>start+10,1200);c.waitTicks(8);} +} 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 6adf886..2bd036f 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,10 +36,11 @@ 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,ssgiOnly; + private final boolean bloomOnly,ssgiOnly,pbrOnly; public PixelShadows122ClientChecks(){this(false);} PixelShadows122ClientChecks(boolean bloomOnly){this(bloomOnly,false);} - PixelShadows122ClientChecks(boolean bloomOnly,boolean ssgiOnly){this.bloomOnly=bloomOnly;this.ssgiOnly=ssgiOnly;} + PixelShadows122ClientChecks(boolean bloomOnly,boolean ssgiOnly){this(bloomOnly,ssgiOnly,false);} + PixelShadows122ClientChecks(boolean bloomOnly,boolean ssgiOnly,boolean pbrOnly){this.bloomOnly=bloomOnly;this.ssgiOnly=ssgiOnly;this.pbrOnly=pbrOnly;} 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); @@ -134,6 +135,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(pbrOnly){Pbr134ClientChecks.run(c,server);return;} 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. @@ -244,6 +246,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest { EdgeHighlights129ClientChecks.run(c, server); Bloom130ClientChecks.run(c, server); Ssgi132ClientChecks.run(c, server); + Pbr134ClientChecks.run(c, server); c.getInput().pressKey(InputConstants.KEY_F1); for (String locale : new String[]{"fr_fr", "en_us"}) { c.runOnClient(m -> { @@ -261,7 +264,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest { }); }); c.waitTicks(3); - c.takeScreenshot("sanctuary-beta133-shader-options-" + locale); + c.takeScreenshot("sanctuary-beta134-shader-options-" + locale); } c.runOnClient(m -> m.gui.setScreen(null)); } @@ -528,7 +531,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest { } static Frame capture(ClientGameTestContext c, String name) { - if (name != null) c.takeScreenshot("sanctuary-beta133-shader-" + name); + if (name != null) c.takeScreenshot("sanctuary-beta134-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 index f6cfdaa..c930094 100644 --- 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 @@ -62,8 +62,47 @@ public final class Ssgi132ClientChecks implements FabricClientGameTest { 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); + motion(c,server); System.out.println("SSGI133_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 motion(ClientGameTestContext c,TestServerContext server){ + c.runOnClient(m->{VanillaLight.setPbr(false);VanillaLight.setSsgi(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);VanillaLight.setPixelShadows(false);VanillaLight.setEdgeHighlights(false);}); + wall(server,11,Blocks.AIR.defaultBlockState()); + server.runOnServer(s->{ + command(s,"summon minecraft:cow 8.5 65 9.5 {NoAI:1b,Silent:1b,NoGravity:1b,Invulnerable:1b,Tags:[\"ssgi134_test\"]}"); + for(int x=11;x<=13;x++)for(int z=8;z<=10;z++){ + s.overworld().setBlockAndUpdate(new BlockPos(x,64,z),Blocks.GRASS_BLOCK.defaultBlockState()); + s.overworld().setBlockAndUpdate(new BlockPos(x,65,z),Blocks.SHORT_GRASS.defaultBlockState()); + } + });settle(c); + for(int intensity:new int[]{35,100}){ + c.runOnClient(m->VanillaLight.setSsgiIntensity(intensity)); + Frame start=null;double worstGrass=0; + for(int step=0;step<=12;step++){ + double x=4.5+step;camera(c,server,x,66.5,5.5,65); + look(c,server,(float)Math.toDegrees(Math.atan2(x-10.5,6.5)),10); + enable(c,false);var base=capture(c,"ssgi-motion-"+intensity+"-"+step+"-off"); + enable(c,true);var frame=capture(c,"ssgi-motion-"+intensity+"-"+step+"-on"); + double grass=grassGain(base,frame);worstGrass=Math.max(worstGrass,grass); + check(grass<(intensity==35?12:30),"Lateral view avoids excessive grass brightening at "+intensity+" / "+step+": "+grass); + if(step==0)start=frame; + } + System.out.println("SSGI134_GRASS intensity="+intensity+" maxMeanGreenGain="+worstGrass); + camera(c,server,4.5,66.5,5.5,65);look(c,server,(float)Math.toDegrees(Math.atan2(-6,6.5)),10);frames(c); + unchanged(start,capture(c,null),"Twelve-block lateral movement returns without stale SSGI history at "+intensity); + } + enable(c,false);c.runOnClient(m->VanillaLight.setSsgiIntensity(35)); + server.runOnServer(s->command(s,"kill @e[tag=ssgi134_test]")); + System.out.println("SSGI134_MOTION_PASS 12-block lateral sweep, grass/cow/blocks, OFF/ON at35 and100, return without history"); + } + private static double grassGain(Frame off,Frame on){ + int count=0;long gain=0; + for(int i=0;i>16&255,g=a>>8&255,b=a&255; + if(g>r*1.15&&g>b*1.3&&g>45){count++;gain+=Math.max(0,(on.pixels()[i]>>8&255)-g);} + } + check(count>500,"Motion fixture includes visible vegetation");return gain/(double)count; + } 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);} diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/shader/PbrMaterials.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/shader/PbrMaterials.java new file mode 100644 index 0000000..f68ab3d --- /dev/null +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/shader/PbrMaterials.java @@ -0,0 +1,65 @@ +package fr.koka.sanctuary.client.shader; + +import com.mojang.blaze3d.platform.NativeImage; +import java.util.*; +import net.minecraft.client.renderer.texture.DynamicTexture; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.world.level.block.state.BlockState; + +/** One source-texture analysis per cached material. No frame readback and no generated files on disk. */ +final class PbrMaterials implements AutoCloseable { + static final int INNER=16,TILE=20,GRID=64,SIZE=TILE*GRID,MAX_SPRITES=GRID*GRID; + record Profile(float roughness,float metal){} + record Slot(int x,int y,boolean empty){ + float u(float f){return (x+2+Math.clamp(f,0,1)*INNER)/SIZE;} + float v(float f){return (y+2+Math.clamp(f,0,1)*INNER)/SIZE;} + } + private final Map slots=new HashMap<>(); + private final Map profiles=new IdentityHashMap<>(); + private DynamicTexture atlas;private boolean dirty;private int analyses; + boolean candidate(BlockState state){return !state.isAir()&&!state.liquid();} + Profile profile(BlockState state){return profiles.computeIfAbsent(state,s->profile(BuiltInRegistries.BLOCK.getKey(s.getBlock()).getPath()));} + static Profile profile(String name){ + if(name.equals("iron_block")||name.equals("gold_block")||name.equals("netherite_block")||name.contains("copper")&&!name.contains("ore")&&!name.contains("raw")) + return new Profile(name.contains("oxidized")?.65f:.32f,name.contains("oxidized")?.35f:.85f); + if(name.contains("polished")||name.contains("glazed")||name.equals("diamond_block")||name.equals("emerald_block")||name.equals("ruby_block")||name.equals("sapphire_block"))return new Profile(.42f,0); + if(name.contains("wool")||name.contains("leaves"))return new Profile(.98f,0); + if(name.contains("planks")||name.contains("log")||name.contains("wood"))return new Profile(.88f,0); + return new Profile(.82f,0); + } + Slot slot(TextureAtlasSprite sprite,BlockState state){ + var profile=profile(state);String key=sprite.contents().name()+":"+profile; + var existing=slots.get(key);if(existing!=null)return existing; + if(slots.size()>=MAX_SPRITES)return new Slot(0,0,true); + int i=slots.size(),x=i%GRID*TILE,y=i/GRID*TILE; + if(atlas==null){atlas=new DynamicTexture("Sanctuary procedural PBR",SIZE,SIZE,false);atlas.getPixels().fillRect(0,0,SIZE,SIZE,0x008080FF);} + boolean empty=false; + try(var image=BloomMaterials.read(sprite.contents().name())){ + int fw=Math.min(image.getWidth(),sprite.contents().width()),fh=Math.min(image.getHeight(),sprite.contents().height()); + int[] colors=new int[INNER*INNER]; + for(int py=0;py>>24)<128){nx=ny=0;} + int red=Math.round((nx*.5f+.5f)*255),green=Math.round((ny*.5f+.5f)*255); + result[y*w+x]=(Math.round(profile.metal*255)<<24)|(red<<16)|(green<<8)|Math.round(profile.roughness*255); + } + return result; + } + private static float height(int color,float fallback){return (color>>>24)<128?fallback:luma(color);} + private static float luma(int c){return ((c>>16&255)*.2126f+(c>>8&255)*.7152f+(c&255)*.0722f)/255;} + DynamicTexture atlas(){if(dirty){atlas.upload();dirty=false;}return atlas;} + int count(){return slots.size();}int analyses(){return analyses;} + public void close(){if(atlas!=null){atlas.close();atlas=null;}slots.clear();profiles.clear();dirty=false;analyses=0;} +} diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/shader/PbrTerrain.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/shader/PbrTerrain.java new file mode 100644 index 0000000..c439a48 --- /dev/null +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/shader/PbrTerrain.java @@ -0,0 +1,135 @@ +package fr.koka.sanctuary.client.shader; + +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.BufferBuilder; +import com.mojang.blaze3d.vertex.ByteBufferBuilder; +import com.mojang.renderpearl.api.GpuFormat; +import com.mojang.renderpearl.api.buffers.GpuBuffer; +import com.mojang.renderpearl.api.pipeline.PrimitiveTopology; +import com.mojang.renderpearl.api.vertex.VertexFormat; +import java.util.*; +import net.minecraft.client.Minecraft; +import net.minecraft.client.model.geom.builders.UVPair; +import net.minecraft.client.renderer.block.dispatch.BlockStateModelPart; +import net.minecraft.client.renderer.texture.TextureAtlas; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.SectionPos; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.phys.Vec3; + +/** Nearby native sections, bounded incremental meshing; models and resource UV rotations remain native. */ +final class PbrTerrain implements AutoCloseable { + static final VertexFormat FORMAT=VertexFormat.builder(0).addAttribute("Position",GpuFormat.RGB32_FLOAT) + .addAttribute("Color",GpuFormat.RGBA8_UNORM).addAttribute("UV0",GpuFormat.RG32_FLOAT) + .addAttribute("UV3",GpuFormat.RG32_FLOAT).build(); + private static final long MAX_BYTES=24L*1024*1024; + private final Map sections=new HashMap<>(); + final PbrMaterials materials=new PbrMaterials(); + private Build building; + private long bytes; + private Object models,level; + private List
visible=List.of(); + record Entry(BlockPos origin,GpuBuffer vertices,int indices){} + List update(){ + var mc=Minecraft.getInstance();var current=mc.getModelManager().getBlockStateModelSet(); + if(models!=current||level!=mc.level){close();models=current;level=mc.level;} + Vec3 camera=mc.gameRenderer.gameRenderState().levelRenderState.cameraRenderState.pos; + long frame=mc.level.getGameTime();var active=new ArrayList
(); + for(var render:mc.levelRenderer.visibleSections()){ + long key=render.getSectionNode();var pos=render.getRenderOrigin(); + if(pos.offset(8,8,8).distToCenterSqr(camera)>48*48)continue; + var chunk=mc.level.getChunkSource().getChunk(pos.getX()>>4,pos.getZ()>>4,ChunkStatus.FULL,false); + if(chunk==null)continue; + var storage=chunk.getSection(mc.level.getSectionIndex(pos.getY())); + var section=sections.get(key); + if(section==null||section.storage!=storage){if(section!=null)remove(section);section=new Section(key,pos.immutable(),storage);sections.put(key,section);} + section.seen=frame;active.add(section); + } + visible=active; + for(var iterator=sections.values().iterator();iterator.hasNext();){var section=iterator.next();if(frame-section.seen>40){remove(section);iterator.remove();}} + long deadline=System.nanoTime()+3_000_000L;int started=building==null?0:1; + while(System.nanoTime()=4)break; + Section next=active.stream().filter(s->s.dirty).min(Comparator.comparingDouble(s->s.origin.distToCenterSqr(camera))).orElse(null); + if(next==null)break; + if(!next.storage.maybeHas(materials::candidate)){next.dirty=false;continue;} + building=new Build(next); + } + if(!advance(building,deadline))break; + var done=building;building=null; + try(done){ + done.section.dirty=false; + if(done.quads>0&&!done.overflow){ + try(var mesh=done.out.buildOrThrow()){ + long size=mesh.vertexBuffer().remaining(); + if(bytes+size<=MAX_BYTES){ + var buffer=RenderSystem.getDevice().createBuffer(()->"Sanctuary PBR surfaces",GpuBuffer.USAGE_VERTEX|GpuBuffer.USAGE_COPY_DST,mesh.vertexBuffer()); + int count=mesh.drawState().indexCount();RenderSystem.getSequentialBuffer(PrimitiveTopology.QUADS).getBuffer(count); + done.section.entry=new Entry(done.section.origin,buffer,count);bytes+=buffer.size(); + } + } + } + } + } + return active.stream().map(s->s.entry).filter(Objects::nonNull).toList(); + } + private boolean advance(Build build,long deadline){ + var mc=Minecraft.getInstance();var models=mc.getModelManager().getBlockStateModelSet(); + var parts=new ArrayList();var random=RandomSource.create(); + while(build.cursor<4096){ + int i=build.cursor++,x=i&15,z=i>>4&15,y=i>>8;var state=build.section.storage.getBlockState(x,y,z); + if(materials.candidate(state)){ + var pos=build.section.origin.offset(x,y,z); + parts.clear();random.setSeed(state.getSeed(pos));if(!state.liquid())models.get(state).collectParts(random,parts); + var tintSources=mc.getBlockColors().getTintSources(state); + var offset=state.getOffset(pos); + for(var part:parts)for(int f=0;f<7;f++){ + Direction face=f==6?null:Direction.values()[f]; + if(face!=null&&mc.level.getBlockState(pos.relative(face)).isSolidRender())continue; + for(var quad:part.getQuads(face)){ + var material=quad.materialInfo();var sprite=material.sprite(); + if(!sprite.atlasLocation().equals(TextureAtlas.LOCATION_BLOCKS))continue; + var slot=materials.slot(sprite,state); + if(slot.empty())continue; + if(build.quads>=16384){build.overflow=true;return true;} + int tint=0xFFFFFF,index=material.tintIndex(); + if(index>=0&&index=deadline)return build.cursor==4096; + } + return true; + } + void dirtySection(int x,int y,int z){dirty(sections.get(SectionPos.asLong(x,y,z)));} + void dirty(BlockPos pos){for(int x=(pos.getX()-1)>>4;x<=(pos.getX()+1)>>4;x++)for(int y=(pos.getY()-1)>>4;y<=(pos.getY()+1)>>4;y++)for(int z=(pos.getZ()-1)>>4;z<=(pos.getZ()+1)>>4;z++)dirty(sections.get(SectionPos.asLong(x,y,z)));} + void dirtyChunk(int x,int z){for(var section:sections.values())if(Math.abs((section.origin.getX()>>4)-x)<=1&&Math.abs((section.origin.getZ()>>4)-z)<=1)dirty(section);} + private void dirty(Section section){if(section!=null){remove(section);section.dirty=true;}} + private void remove(Section section){if(building!=null&&building.section==section){building.close();building=null;}if(section.entry!=null){bytes-=section.entry.vertices.size();section.entry.vertices.close();section.entry=null;}} + boolean settled(){return building==null&&visible.stream().noneMatch(s->s.dirty);} + public void close(){if(building!=null){building.close();building=null;}for(var section:sections.values())remove(section);sections.clear();visible=List.of();materials.close();models=level=null;bytes=0;} + private static final class Section{ + final long key;final BlockPos origin;final LevelChunkSection storage;long seen;boolean dirty=true;Entry entry; + Section(long key,BlockPos origin,LevelChunkSection storage){this.key=key;this.origin=origin;this.storage=storage;} + } + private static final class Build implements AutoCloseable{ + final Section section;final ByteBufferBuilder buffer=new ByteBufferBuilder(65536);final BufferBuilder out=new BufferBuilder(buffer,PrimitiveTopology.QUADS,FORMAT); + int cursor,quads;boolean overflow; + Build(Section section){this.section=section;} + public void close(){buffer.close();} + } +} diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/shader/PixelShadows.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/shader/PixelShadows.java index b8d991c..566db10 100644 --- a/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/shader/PixelShadows.java +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/shader/PixelShadows.java @@ -154,6 +154,8 @@ public final class PixelShadows { } static double anchor(double coordinate) { return Math.floor(coordinate / 16.0 + .5) * 16.0; } + static com.mojang.renderpearl.api.buffers.GpuBuffer materialUniforms(){return uniforms.currentBuffer();} + static com.mojang.renderpearl.api.textures.GpuTextureView materialDepth(){return shadowMap.getDepthTextureView();} static Vector3f sunDirection(float angle) { return new Vector3f(-(float)Math.sin(angle), (float)Math.cos(angle), 0); } static int mapSize(int distance, int pixels) { int size = 512; diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/shader/ProceduralPbr.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/shader/ProceduralPbr.java new file mode 100644 index 0000000..7c734a3 --- /dev/null +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/shader/ProceduralPbr.java @@ -0,0 +1,93 @@ +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.buffers.GpuBufferSlice; +import com.mojang.renderpearl.api.pipeline.*; +import com.mojang.renderpearl.api.textures.FilterMode; +import fr.koka.sanctuary.client.panorama.PanoramaCapture; +import java.util.*; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.BindGroupLayouts; +import net.minecraft.client.renderer.MappableRingBuffer; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.client.renderer.texture.TextureAtlas; +import net.minecraft.core.BlockPos; +import net.minecraft.resources.Identifier; +import net.minecraft.world.level.dimension.DimensionType; +import net.minecraft.world.level.material.FogType; +import org.joml.Matrix4f; +import org.joml.Vector3f; +import org.joml.Vector4f; + +/** Optional local material pass: cached texture normals, GGX highlights and diffuse relief. */ +public final class ProceduralPbr { + private static final BindGroupLayout SCENE=BindGroupLayout.builder().withUniform("PbrScene",UniformType.UNIFORM_BUFFER) + .withUniform("PixelShadows",UniformType.UNIFORM_BUFFER).withUniform("SunDepth",UniformType.COMBINED_IMAGE_SAMPLER) + .withUniform("WorldDepth",UniformType.COMBINED_IMAGE_SAMPLER).withUniform("SceneColor",UniformType.COMBINED_IMAGE_SAMPLER) + .withUniform("BlockAtlas",UniformType.COMBINED_IMAGE_SAMPLER).withUniform("MaterialAtlas",UniformType.COMBINED_IMAGE_SAMPLER).build(); + private static final RenderPipeline PIPELINE=RenderPipelines.register(RenderPipeline.builder() + .withLocation(Identifier.parse("sanctuary:pipeline/pbr")).withVertexShader(Identifier.parse("sanctuary:core/pbr")) + .withFragmentShader(Identifier.parse("sanctuary:core/pbr")).withBindGroupLayout(BindGroupLayouts.DYNAMIC_TRANSFORMS).withBindGroupLayout(SCENE) + .withVertexBinding(0,PbrTerrain.FORMAT).withPrimitiveTopology(PrimitiveTopology.QUADS).withCull(false) + .withColorTargetState(new ColorTargetState(Optional.empty(),GpuFormat.RGBA8_UNORM,ColorTargetState.WRITE_COLOR)) + .withDepthStencilState(Optional.empty()).build()); + private static final PbrTerrain TERRAIN=new PbrTerrain(); + private static final Matrix4f projection=new Matrix4f(); + private static TextureTarget source; + private static MappableRingBuffer scene; + private static boolean applied;private static int frames; + private ProceduralPbr(){} + public static void captureProjection(Matrix4f matrix){projection.set(matrix);} + public static void dirtyBlock(BlockPos pos){TERRAIN.dirty(pos);} + public static void dirtyChunk(int x,int z){TERRAIN.dirtyChunk(x,z);} + public static void dirtySection(int x,int y,int z){TERRAIN.dirtySection(x,y,z);} + public static void render(){ + applied=false;var mc=Minecraft.getInstance(); + if(!VanillaLight.active()||!VanillaLight.pbr()||VanillaLight.pbrIntensity()==0||mc.level==null){release();return;} + var state=mc.gameRenderer.gameRenderState().levelRenderState;var camera=state.cameraRenderState;var sky=state.skyRenderState; + if(PanoramaCapture.rendering()||camera.fogType!=FogType.NONE||sky.skybox!=DimensionType.Skybox.OVERWORLD)return; + var main=mc.gameRenderer.mainRenderTarget(); + if(source==null||source.width!=main.width||source.height!=main.height){if(source!=null)source.destroyBuffers();source=new TextureTarget("Sanctuary PBR source",main.width,main.height,GpuFormat.RGBA8_UNORM,null);} + // The extra padding also supplies a valid inactive PixelShadows binding (192 bytes). + if(scene==null)scene=new MappableRingBuffer(()->"Sanctuary PBR scene",GpuBuffer.USAGE_UNIFORM|GpuBuffer.USAGE_MAP_WRITE,192); + var entries=TERRAIN.update();var atlas=TERRAIN.materials.atlas(); + var vp=new Matrix4f(projection).mul(camera.viewRotationMatrix);var fog=camera.fogData; + var sun=PixelShadows.sunDirection(sky.sunAngle);boolean lunar=sun.y<=.08f; + if(lunar)sun=PixelShadows.sunDirection(sky.moonAngle); + float moon=sky.moonPhase==null?1:(float)(.5+.5*Math.cos(sky.moonPhase.index()*Math.PI/4)); + float strength=(lunar?.12f*moon:1)*sky.rainBrightness*Math.clamp((sun.y-.04f)/.16f,0,1); + boolean shadow=PixelShadows.applied(); + scene.rotate();try(var mapped=scene.currentBuffer().map(false,true)){ + Std140Builder.intoBuffer(mapped.data()).putMat4f(new Matrix4f(vp).invert()) + .putVec4(sun.x,sun.y,sun.z,strength) + .putVec4(VanillaLight.pbrIntensity()/100f,RenderSystem.getDevice().getDeviceInfo().isZZeroToOne()?1:0,shadow?1:0,32) + .putVec4(fog.renderDistanceStart,fog.renderDistanceEnd,fog.environmentalStart,fog.environmentalEnd).putVec4(fog.color.w,0,0,0) + .putVec4(0,0,0,0).putVec4(0,0,0,0).putVec4(0,0,0,0).putVec4(0,0,0,0); + } + var transforms=new ArrayList(entries.size());int max=0; + for(var entry:entries){var p=entry.origin();var offset=new Vector3f((float)(p.getX()-camera.pos.x),(float)(p.getY()-camera.pos.y),(float)(p.getZ()-camera.pos.z)); + transforms.add(RenderSystem.getDynamicUniforms().writeTransform(new Matrix4f(vp).translate(offset),new Vector4f(1,1,1,1),offset,new Matrix4f()));max=Math.max(max,entry.indices());} + var indices=RenderSystem.getSequentialBuffer(PrimitiveTopology.QUADS);indices.requestIndexCount(max); + 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); + if(atlas!=null&&!entries.isEmpty())try(var pass=encoder.createRenderPass(()->"Sanctuary PBR materials",main.getColorTextureView(),Optional.empty())){ + pass.setPipeline(RenderSystem.getCompiledPipeline(PIPELINE));pass.setUniform("PbrScene",scene.currentBuffer()); + pass.setUniform("PixelShadows",shadow?PixelShadows.materialUniforms():scene.currentBuffer()); + pass.setUniform("SunDepth",shadow?PixelShadows.materialDepth():main.getDepthTextureView(),nearest); + pass.setUniform("WorldDepth",main.getDepthTextureView(),nearest);pass.setUniform("SceneColor",source.getColorTextureView(),nearest); + pass.setUniform("BlockAtlas",mc.getTextureManager().getTexture(TextureAtlas.LOCATION_BLOCKS).getTextureView(),nearest); + pass.setUniform("MaterialAtlas",atlas.getTextureView(),nearest);pass.setIndexBuffer(indices.getBuffer(),indices.type()); + for(int i=0;i"Sanctuary SSGI",GpuBuffer.USAGE_UNIFORM|GpuBuffer.USAGE_MAP_WRITE,176); + if(uniforms==null){ + uniforms=new MappableRingBuffer(()->"Sanctuary SSGI",GpuBuffer.USAGE_UNIFORM|GpuBuffer.USAGE_MAP_WRITE,176); + horizontal=new MappableRingBuffer(()->"Sanctuary SSGI filter X",GpuBuffer.USAGE_UNIFORM|GpuBuffer.USAGE_MAP_WRITE,16); + vertical=new MappableRingBuffer(()->"Sanctuary SSGI filter Y",GpuBuffer.USAGE_UNIFORM|GpuBuffer.USAGE_MAP_WRITE,16); + } + horizontal.rotate();try(var m=horizontal.currentBuffer().map(false,true)){Std140Builder.intoBuffer(m.data()).putVec4(1,0,0,0);} + vertical.rotate();try(var m=vertical.currentBuffer().map(false,true)){Std140Builder.intoBuffer(m.data()).putVec4(0,1,0,0);} + 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) @@ -63,19 +71,29 @@ public final class ScreenSpaceGI { 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.setPipeline(RenderSystem.getCompiledPipeline(GATHER));pass.setUniform("GIBlur",horizontal.currentBuffer());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); } + filter(encoder,indirect,filtered,horizontal); + filter(encoder,filtered,indirect,vertical); try(var pass=encoder.createRenderPass(()->"Sanctuary SSGI composite",main.getColorTextureView(),Optional.empty())){ - pass.setPipeline(RenderSystem.getCompiledPipeline(APPLY));pass.setUniform("GIParams",uniforms.currentBuffer()); + pass.setPipeline(RenderSystem.getCompiledPipeline(APPLY));pass.setUniform("GIBlur",horizontal.currentBuffer());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;} + private static void filter(com.mojang.renderpearl.api.commands.CommandEncoder encoder,TextureTarget input,TextureTarget output,MappableRingBuffer direction){ + var main=Minecraft.getInstance().gameRenderer.mainRenderTarget();var nearest=RenderSystem.getSamplerCache().getClampToEdge(FilterMode.NEAREST); + try(var pass=encoder.createRenderPass(()->"Sanctuary SSGI bilateral filter",output.getColorTextureView(),Optional.empty())){ + pass.setPipeline(RenderSystem.getCompiledPipeline(FILTER));pass.setUniform("GIParams",uniforms.currentBuffer());pass.setUniform("GIBlur",direction.currentBuffer()); + pass.setUniform("WorldDepth",main.getDepthTextureView(),nearest);pass.setUniform("SceneColor",source.getColorTextureView(),nearest); + pass.setUniform("IndirectLight",input.getColorTextureView(),RenderSystem.getSamplerCache().getClampToEdge(FilterMode.LINEAR));pass.draw(3,1,0,0); + } + } + public static void release(){if(source!=null){source.destroyBuffers();indirect.destroyBuffers();filtered.destroyBuffers();source=indirect=filtered=null;}if(uniforms!=null){uniforms.close();horizontal.close();vertical.close();uniforms=horizontal=vertical=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 5b5e38f..b86646b 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.pbr",OptionInstance.cachedConstantTooltip( + Component.translatable("sanctuary.shaders.pbr_help")),VanillaLight.pbr(),VanillaLight::setPbr)); + slider("pbr_intensity",VanillaLight.pbrIntensity(),VanillaLight::setPbrIntensity); 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); 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 7df2f32..ce6ca51 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 pbr=false; + int pbrIntensity=50; boolean ssgi=false; int ssgiIntensity=35; int bloomIntensity=35; @@ -37,6 +39,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.pbrIntensity=Math.clamp(preferences.pbrIntensity,0,100); preferences.ssgiIntensity=Math.clamp(preferences.ssgiIntensity,0,100); preferences.bloomIntensity=Math.clamp(preferences.bloomIntensity,0,100); preferences.bloomRadius=Math.clamp(preferences.bloomRadius,0,100); @@ -58,6 +61,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 pbr(){enabled();return preferences.pbr;} + public static int pbrIntensity(){enabled();return preferences.pbrIntensity;} + public static void setPbr(boolean value){enabled();preferences.pbr=value;save();} + public static void setPbrIntensity(int value){enabled();preferences.pbrIntensity=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();} 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 fab2c38..39c1500 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 @@ -1,6 +1,7 @@ package fr.koka.sanctuary.mixin.client; import fr.koka.sanctuary.client.shader.PixelShadows; +import fr.koka.sanctuary.client.shader.ProceduralPbr; import fr.koka.sanctuary.client.shader.Bloom; import fr.koka.sanctuary.client.shader.ScreenSpaceGI; import net.minecraft.client.renderer.extract.LevelExtractor; @@ -15,8 +16,10 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; abstract class PixelShadowDirtyMixin { @Inject(method="blockChanged", at=@At("HEAD")) private void sanctuary$dirty(BlockPos pos, int flags, CallbackInfo ci) { - PixelShadows.dirtyBlock(pos); Bloom.dirtyBlock(pos); + PixelShadows.dirtyBlock(pos); ProceduralPbr.dirtyBlock(pos); Bloom.dirtyBlock(pos); } @Inject(method="allChanged", at=@At("HEAD")) - private void sanctuary$reload(CallbackInfo ci) { PixelShadows.release(); Bloom.release(); ScreenSpaceGI.release(); } + private void sanctuary$reload(CallbackInfo ci) { PixelShadows.release(); ProceduralPbr.release(); Bloom.release(); ScreenSpaceGI.release(); } + @Inject(method="setSectionDirty", at=@At("HEAD")) + private void sanctuary$pbrLight(int x,int y,int z,CallbackInfo ci){ProceduralPbr.dirtySection(x,y,z);} } diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/client/PixelShadowLevelMixin.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/client/PixelShadowLevelMixin.java index c1cda3f..ea28ae9 100644 --- a/mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/client/PixelShadowLevelMixin.java +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/client/PixelShadowLevelMixin.java @@ -1,6 +1,7 @@ package fr.koka.sanctuary.mixin.client; import fr.koka.sanctuary.client.shader.PixelShadows; +import fr.koka.sanctuary.client.shader.ProceduralPbr; import fr.koka.sanctuary.client.shader.Bloom; import net.minecraft.client.multiplayer.ClientLevel; import net.minecraft.core.BlockPos; @@ -16,11 +17,11 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; abstract class PixelShadowLevelMixin { @Inject(method="setBlocksDirty", at=@At("HEAD")) private void sanctuary$block(BlockPos pos, BlockState before, BlockState after, CallbackInfo ci) { - if (before != after) {PixelShadows.dirtyBlock(pos);Bloom.dirtyBlock(pos);} + if (before != after) {PixelShadows.dirtyBlock(pos); ProceduralPbr.dirtyBlock(pos);Bloom.dirtyBlock(pos);} } @Inject(method="onChunkLoaded", at=@At("TAIL")) - private void sanctuary$loaded(ChunkPos pos, CallbackInfo ci) { PixelShadows.dirtyChunk(pos.x(), pos.z()); Bloom.dirtyChunk(pos.x(), pos.z()); } + private void sanctuary$loaded(ChunkPos pos, CallbackInfo ci) { PixelShadows.dirtyChunk(pos.x(), pos.z()); ProceduralPbr.dirtyChunk(pos.x(), pos.z()); Bloom.dirtyChunk(pos.x(), pos.z()); } @Inject(method="unload", at=@At("HEAD")) - private void sanctuary$unload(LevelChunk chunk, CallbackInfo ci) { PixelShadows.dirtyChunk(chunk.getPos().x(), chunk.getPos().z()); Bloom.dirtyChunk(chunk.getPos().x(), chunk.getPos().z()); } + private void sanctuary$unload(LevelChunk chunk, CallbackInfo ci) { PixelShadows.dirtyChunk(chunk.getPos().x(), chunk.getPos().z()); ProceduralPbr.dirtyChunk(chunk.getPos().x(), chunk.getPos().z()); Bloom.dirtyChunk(chunk.getPos().x(), chunk.getPos().z()); } } 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 a348bb0..1696919 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 @@ -4,6 +4,7 @@ import com.llamalad7.mixinextras.injector.wrapoperation.Operation; import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; import com.mojang.renderpearl.api.buffers.GpuBufferSlice; import fr.koka.sanctuary.client.shader.PixelShadows; +import fr.koka.sanctuary.client.shader.ProceduralPbr; import fr.koka.sanctuary.client.shader.EdgeHighlights; import fr.koka.sanctuary.client.shader.Bloom; import fr.koka.sanctuary.client.shader.ScreenSpaceGI; @@ -20,13 +21,14 @@ abstract class PixelShadowRendererMixin { @WrapOperation(method="renderLevel", at=@At(value="INVOKE", target="Lnet/minecraft/client/renderer/ProjectionMatrixBuffer;getBuffer(Lorg/joml/Matrix4f;)Lcom/mojang/renderpearl/api/buffers/GpuBufferSlice;")) private GpuBufferSlice sanctuary$projection(ProjectionMatrixBuffer buffer, Matrix4f matrix, Operation original) { PixelShadows.captureProjection(matrix); + ProceduralPbr.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(); ScreenSpaceGI.render(); EdgeHighlights.render(); Bloom.render(); } + private void sanctuary$shadows(CallbackInfo ci) { PixelShadows.render(); ProceduralPbr.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(); ScreenSpaceGI.release(); } + private void sanctuary$close(CallbackInfo ci) { PixelShadows.release(); ProceduralPbr.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 f040ea6..2d08d73 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 @@ -2293,5 +2293,10 @@ "sanctuary.shaders.ssgi_help": "Experimental: enhanced 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%%" + "sanctuary.shaders.ssgi_intensity_value": "SSGI: %s%%", + "sanctuary.shaders.pbr": "PBR · material relief", + "sanctuary.shaders.pbr_help": "Experimental: procedural block normals and roughness, sun and moon highlights. Range 32 blocks. OFF by default.", + "sanctuary.shaders.pbr_intensity": "PBR strength", + "sanctuary.shaders.pbr_intensity_help": "Material relief and highlights. Zero stops rendering and frees the cache. Original textures remain unchanged.", + "sanctuary.shaders.pbr_intensity_value": "PBR: %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 a917219..c553e3d 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 @@ -2293,5 +2293,10 @@ "sanctuary.shaders.ssgi_help": "Expérimental : diffusion renforcée 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 %%" + "sanctuary.shaders.ssgi_intensity_value": "SSGI : %s %%", + "sanctuary.shaders.pbr": "PBR · relief des matériaux", + "sanctuary.shaders.pbr_help": "Expérimental : normales procédurales et rugosité des blocs, reflets du soleil et de la lune. Portée 32 blocs. Désactivé par défaut.", + "sanctuary.shaders.pbr_intensity": "Intensité du PBR", + "sanctuary.shaders.pbr_intensity_help": "Relief et reflets des matériaux. Zéro coupe le calcul et libère le cache. Ne modifie pas les textures originales.", + "sanctuary.shaders.pbr_intensity_value": "PBR : %s %%" } diff --git a/mods/sanctuary/src/main/resources/assets/sanctuary/shaders/core/pbr.fsh b/mods/sanctuary/src/main/resources/assets/sanctuary/shaders/core/pbr.fsh new file mode 100644 index 0000000..5684cc4 --- /dev/null +++ b/mods/sanctuary/src/main/resources/assets/sanctuary/shaders/core/pbr.fsh @@ -0,0 +1,78 @@ +#version 330 +#extension GL_ARB_separate_shader_objects : require +uniform sampler2D WorldDepth; +uniform sampler2D SceneColor; +uniform sampler2D BlockAtlas; +uniform sampler2D MaterialAtlas; +uniform sampler2D SunDepth; +layout(std140) uniform PbrScene { + mat4 PbrInverseVP; + vec4 PbrSun; + vec4 PbrParams; + vec4 PbrFog; + vec4 PbrEnvironment; +}; +layout(std140) uniform PixelShadows { + mat4 InverseViewProjection; + mat4 LightMatrix; + vec4 CameraFractionPixels; + vec4 SunStrength; + vec4 DistanceClipFog; + vec4 EnvironmentFog; +}; +layout(location=0) in vec3 relativePosition; +layout(location=1) in vec2 blockUv; +layout(location=2) in vec2 maskUv; +layout(location=3) in vec4 tint; +layout(location=0) out vec4 fragColor; +float visibility(vec3 p,vec3 n){ + if(PbrParams.z<0.5)return 1.0; + vec3 world=p+CameraFractionPixels.xyz; + vec3 snapped=(floor(world*CameraFractionPixels.w+.0001)+.5)/CameraFractionPixels.w; + vec3 receiver=snapped+n*dot(world-snapped,n); + vec4 light=LightMatrix*vec4(receiver+n*0.04+PbrSun.xyz*0.015,1.0); + vec3 q=light.xyz/light.w;q.xy=q.xy*0.5+0.5; + if(DistanceClipFog.y<0.5)q.z=q.z*0.5+0.5; + if(any(lessThan(q,vec3(0)))||any(greaterThan(q,vec3(1))))return 1.0; + return q.z>texture(SunDepth,q.xy).r+0.00015?0.0:1.0; +} +void main(){ + // Derivatives precede divergent discard; UV-space tangents preserve rotated/mirrored block textures. + vec3 dx=dFdx(relativePosition),dy=dFdy(relativePosition); + vec2 tx=dFdx(blockUv),ty=dFdy(blockUv); + float det=tx.x*ty.y-tx.y*ty.x; + vec3 baseNormal=normalize(cross(dx,dy));if(dot(baseNormal,-relativePosition)<0.0)baseNormal=-baseNormal; + vec2 uv=gl_FragCoord.xy/vec2(textureSize(WorldDepth,0));float depth=texture(WorldDepth,uv).r; + vec4 projected=PbrInverseVP*vec4(uv*2.0-1.0,PbrParams.y>0.5?depth:depth*2.0-1.0,1.0); + vec3 visible=projected.xyz/projected.w; + if(depth<=0.0||abs(length(visible)-length(relativePosition))>0.035||length(relativePosition)>=PbrParams.w||abs(det)<1e-12)discard; + vec4 texel=texture(BlockAtlas,blockUv);if(texel.a<0.99)discard; + vec3 tangent=normalize((dx*ty.y-dy*tx.y)/det),bitangent=normalize((dy*tx.x-dx*ty.x)/det); + vec4 material=texture(MaterialAtlas,maskUv); + // RG stores signed height derivatives; B roughness, A metalness, not transparency. + vec2 slope=(material.rg*255.0-128.0)/127.0*PbrParams.x*2.0; + vec3 n=normalize(baseNormal+tangent*slope.x+bitangent*slope.y); + vec3 l=normalize(PbrSun.xyz),v=normalize(-relativePosition),h=normalize(l+v); + float nl=max(dot(n,l),0.0),nv=max(dot(n,v),0.001),nh=max(dot(n,h),0.0),vh=max(dot(v,h),0.0); + float roughness=max(material.b,0.25),alpha=roughness*roughness,a2=alpha*alpha; + float denom=nh*nh*(a2-1.0)+1.0; + float distribution=a2/max(3.14159265*denom*denom,0.00001); + float k=(roughness+1.0)*(roughness+1.0)/8.0; + float geometry=nv/(nv*(1.0-k)+k)*nl/max(nl*(1.0-k)+k,0.00001); + vec3 albedo=texel.rgb*tint.rgb;albedo*=albedo; + vec3 f0=mix(vec3(0.04),albedo,material.a); + vec3 fresnel=f0+(1.0-f0)*pow(1.0-vh,5.0); + vec3 specular=distribution*geometry*fresnel/max(4.0*nv*nl,0.0001)*nl; + float fog=max(clamp((max(length(relativePosition.xz),abs(relativePosition.y))-PbrFog.x)/max(PbrFog.y-PbrFog.x,.001),0.0,1.0), + clamp((length(relativePosition)-PbrFog.z)/max(PbrFog.w-PbrFog.z,.001),0.0,1.0)); + float amount=PbrSun.w*pow(tint.a,8.0)*visibility(relativePosition,baseNormal)*(1.0-fog*PbrEnvironment.x) + *(1.0-smoothstep(PbrParams.w*.75,PbrParams.w,length(relativePosition))); + if(amount<=0.0001)discard; + vec3 scene=texture(SceneColor,uv).rgb;vec3 linear=scene*scene; + // Native diffuse light already exists. Apply only the perturbed-normal difference, then add GGX. + float relief=(nl-max(dot(baseNormal,l),0.0))*.7; + vec3 lit=clamp(linear*(1.0+relief*amount),0.0,1.0); + vec3 shine=min(specular*PbrParams.x*amount*3.0,vec3(0.5)); + lit+=(1.0-lit)*(1.0-exp(-shine)); + fragColor=vec4(sqrt(clamp(lit,0.0,1.0)),1.0); +} diff --git a/mods/sanctuary/src/main/resources/assets/sanctuary/shaders/core/pbr.vsh b/mods/sanctuary/src/main/resources/assets/sanctuary/shaders/core/pbr.vsh new file mode 100644 index 0000000..546538a --- /dev/null +++ b/mods/sanctuary/src/main/resources/assets/sanctuary/shaders/core/pbr.vsh @@ -0,0 +1,12 @@ +#version 330 +#extension GL_ARB_separate_shader_objects : require +#include +layout(location=0) in vec3 Position; +layout(location=1) in vec4 Color; +layout(location=2) in vec2 UV0; +layout(location=3) in vec2 UV3; +layout(location=0) out vec3 relativePosition; +layout(location=1) out vec2 blockUv; +layout(location=2) out vec2 maskUv; +layout(location=3) out vec4 tint; +void main(){gl_Position=ModelViewMat*vec4(Position,1.0);relativePosition=Position+ModelOffset;blockUv=UV0;maskUv=UV3;tint=Color;} diff --git a/mods/sanctuary/src/main/resources/assets/sanctuary/shaders/core/ssgi_filter.fsh b/mods/sanctuary/src/main/resources/assets/sanctuary/shaders/core/ssgi_filter.fsh new file mode 100644 index 0000000..b638f3a --- /dev/null +++ b/mods/sanctuary/src/main/resources/assets/sanctuary/shaders/core/ssgi_filter.fsh @@ -0,0 +1,34 @@ +#version 330 +#extension GL_ARB_separate_shader_objects : require +#include +layout(std140) uniform GIBlur { vec4 Direction; }; +layout(location=0) in vec2 texCoord; +layout(location=0) out vec4 fragColor; +void main(){ + vec4 center=texture(IndirectLight,texCoord);fragColor=center; + if(center.a<=0.0)return; + vec2 uv=giTexel(texCoord);float depth=texture(WorldDepth,uv).r; + if(depth<=0.0)return; + vec3 p=giPosition(uv,depth),n=giNormal(uv,p); + if(length(n)<.9)return; + vec2 pixel=Direction.xy/vec2(textureSize(IndirectLight,0)); + vec2 nearUV=giTexel(texCoord+pixel); + float nearDepth=texture(WorldDepth,nearUV).r; + float footprint=nearDepth>0.0?length(giPosition(nearUV,nearDepth)-p):.12; + // Approx. 0.36 block support, capped at 24 half-resolution pixels at close range. + float stride=clamp(.12/max(footprint,.001),1.0,8.0); + vec3 total=center.rgb;float weight=1.0; + for(int i=-3;i<=3;i++){ + if(i==0)continue; + vec2 tap=texCoord+pixel*(float(i)*stride); + if(any(lessThan(tap,vec2(0)))||any(greaterThan(tap,vec2(1))))continue; + vec4 light=texture(IndirectLight,tap);if(light.a<=0.0)continue; + vec2 surface=giTexel(tap);float d=texture(WorldDepth,surface).r;if(d<=0.0)continue; + vec3 q=giPosition(surface,d);float plane=abs(dot(q-p,n)); + // Only blur the indirect light on the same receiving plane, never scene textures or silhouettes. + if(plane>.035||length(q-p)>.7)continue; + float w=exp(-float(i*i)*.32)*(1.0-smoothstep(.01,.035,plane)); + total+=light.rgb*w;weight+=w; + } + fragColor=vec4(total/weight,center.a); +} 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 index 880ef71..d4d2bc7 100644 --- 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 @@ -38,8 +38,11 @@ void main(){ 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; - // Decode the scene approximately to linear light; one spatial falloff is enough. - sum+=color*color*facing*falloff*edge*giFog(hit); + // Vanilla already supplies neutral ambient light. Transport the chromatic + // remainder instead of re-injecting bright grey ground into every nearby face. + vec3 linear=color*color; + vec3 chroma=linear-vec3(min(linear.r,min(linear.g,linear.b))); + sum+=chroma*facing*falloff*edge*giFog(hit); } break; } 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 index 4bf3bd7..12faea7 100644 --- a/mods/sanctuary/src/main/resources/assets/sanctuary/shaders/include/ssgi.glsl +++ b/mods/sanctuary/src/main/resources/assets/sanctuary/shaders/include/ssgi.glsl @@ -22,6 +22,11 @@ vec3 giNormal(vec2 uv,vec3 p){ vec3 dx=length(r-p).995){ + n=a.x>a.y&&a.x>a.z?vec3(sign(n.x),0,0):a.y>a.z?vec3(0,sign(n.y),0):vec3(0,0,sign(n.z)); + } return dot(n,-p)<0.0?-n:n; } float giFog(vec3 p){ diff --git a/packwiz/pack.toml b/packwiz/pack.toml index 8350f9b..54036ba 100644 --- a/packwiz/pack.toml +++ b/packwiz/pack.toml @@ -1,6 +1,6 @@ name = "Sanctuary" author = "KOKA99CAB" -version = "beta.133" +version = "beta.134" pack-format = "packwiz:1.1.0" [index]