Compare commits

..
2 Commits
Author SHA1 Message Date
koka fc4a0cb68e feat: add optional PBR and filter SSGI in beta.134
Build Sanctuary / build (push) Canceled after 0s
2026-09-17 22:54:38 +02:00
koka 17172fd18b docs: record beta.133 release and Prism synchronization
Build Sanctuary / build (push) Canceled after 0s
2026-09-17 22:19:32 +02:00
27 changed files with 748 additions and 30 deletions
+12 -4
View File
@@ -20,10 +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.
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à
latelier, les œufs et la neige saisonnière dans linstance **Sanctuary Beta**.
Les [synchronisations antérieures](docs/git-sync-beta092.md) restent documentées.
@@ -34,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.
+11
View File
@@ -14,6 +14,17 @@ JAR Sanctuary sont des pièces jointes de releases Gitea. Aucun JAR de mod ou
dépendance téléchargée n'entre dans l'historique Git. Le canal Beta est distinct
de l'ancien pack 26.2.
## beta.133 — SSGI renforcé
Diffusion des couleurs rendue visible au réglage 35, même budget de rayons
et cibles GPU. [Contrat et vérifications](shader-beta133.md).
[Release beta.133](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.133)
publiée ; canal `9ceb5a5353c9d1b8bb7834a20e818fd65096ff98`. Deux synchronisations isolées,
puis deux dans la même instance Sanctuary Beta réussies. Les 923 fichiers
personnels suivis gardent leurs hashes. Aucun monde personnel ouvert ;
sauvegarde préalable `sanctuary-backups/before-beta.133/`.
Packs normal/Test/template vérifiés dans `sanctuary-beta/build/`.
## beta.132 — SSGI expérimental
Diffusion locale des couleurs visibles, rayon 2,5 blocs, intensité réglable,
+17
View File
@@ -87,3 +87,20 @@ Gameplay, textures, sauvegardes et dépendances inchangés.
- `Sanctuary-Test-beta.133.mrpack` : 10413204 octets, SHA-256 `e01f0355bf2cce18156fe94a069e142bedc498624ba0b6e2729ae0f23ea73e80`.
JAR : `a01cae4a98606007b533ca09ec9f072f19a1db2e9b8de466f4ad075d5c1fc185`.
## Livraison
[Release beta.133](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.133)
publiée depuis `216d3f75b576bd2ed531653012f9cc6c4801d87a`. Canal packwiz
`9ceb5a5353c9d1b8bb7834a20e818fd65096ff98` ; téléchargements publics vérifiés par SHA-256.
Deux synchronisations isolées puis deux synchronisations de la même instance
**Sanctuary Beta** réussissent. Un seul JAR beta.133 actif ; les **923 fichiers
personnels suivis**, dont sauvegardes et préférences, gardent leurs hashes.
Sauvegarde préalable `sanctuary-backups/before-beta.133/`, aucun monde personnel
ouvert. Le checkout original conserve ses changements préexistants.
Les archives normal/Test/template et le comparateur natif sont également dans
`sanctuary-beta/build/`. Reçus locaux ignorés : `build/shader133-artifact.json`,
`build/shader133-isolated.json`, `build/shader133-prism.json` et
`build/shader133-publication.log`.
+93
View File
@@ -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 0100. 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.
+2 -2
View File
@@ -9,8 +9,8 @@ loom_version=1.17.20
fabric_api_version=0.160.5+26.3
# Release counter: beta.001, .002, .003, ... (see docs/versioning.md).
mod_version=beta.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
+3
View File
@@ -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') }
@@ -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<on.pixels().length;i++){int am=(on.pixels()[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<off.pixels().length;i++){
int a=off.pixels()[i],b=on.pixels()[i],delta=(b>>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);}
}
@@ -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<Frame>();
c.runOnClient(m -> Screenshot.takeScreenshot(m.gameRenderer.mainRenderTarget(), image -> {
try (image) {
@@ -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<off.pixels().length;i++){
int a=off.pixels()[i],r=a>>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);}
@@ -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<String,Slot> slots=new HashMap<>();
private final Map<BlockState,Profile> 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<INNER;py++)for(int px=0;px<INNER;px++)colors[py*INNER+px]=image.getPixel(px*fw/INNER,py*fh/INNER);
int[] pixels=generate(colors,INNER,INNER,profile);analyses++;
for(int py=0;py<TILE;py++)for(int px=0;px<TILE;px++)atlas.getPixels().setPixel(x+px,y+py,pixels[Math.clamp(py-2,0,INNER-1)*INNER+Math.clamp(px-2,0,INNER-1)]);
}catch(java.io.IOException e){empty=true;fr.koka.sanctuary.SanctuaryMod.LOGGER.warn("Cannot analyse PBR texture {}",sprite.contents().name(),e);}
var result=new Slot(x,y,empty);slots.put(key,result);dirty=true;return result;
}
static int[] generate(int[] colors,int w,int h,Profile profile){
int[] result=new int[colors.length];
for(int y=0;y<h;y++)for(int x=0;x<w;x++){
int center=colors[y*w+x];float base=luma(center);
float left=height(colors[y*w+Math.floorMod(x-1,w)],base),right=height(colors[y*w+(x+1)%w],base);
float up=height(colors[Math.floorMod(y-1,h)*w+x],base),down=height(colors[(y+1)%h*w+x],base);
float nx=Math.clamp((left-right)*2,-.45f,.45f),ny=Math.clamp((up-down)*2,-.45f,.45f);
if((center>>>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;}
}
@@ -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<Long,Section> sections=new HashMap<>();
final PbrMaterials materials=new PbrMaterials();
private Build building;
private long bytes;
private Object models,level;
private List<Section> visible=List.of();
record Entry(BlockPos origin,GpuBuffer vertices,int indices){}
List<Entry> 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<Section>();
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()<deadline){
if(building==null){
if(started++>=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<BlockStateModelPart>();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<tintSources.size())tint=tintSources.get(index).colorInWorld(state,mc.level,pos)&0xFFFFFF;
int sky=mc.level.getBrightness(net.minecraft.world.level.LightLayer.SKY,face==null?pos:pos.relative(face));
int color=(Math.round(sky/15f*255)<<24)|tint;
for(int v=0;v<4;v++){
var p=quad.position(v);float u=UVPair.unpackU(quad.packedUV(v)),tv=UVPair.unpackV(quad.packedUV(v));
float localU=(u-sprite.getU0())/(sprite.getU1()-sprite.getU0()),localV=(tv-sprite.getV0())/(sprite.getV1()-sprite.getV0());
build.out.addVertex(x+p.x()+(float)offset.x,y+p.y()+(float)offset.y,z+p.z()+(float)offset.z)
.setColor(color).setUv(u,tv).setUv3(slot.u(localU),slot.v(localV));
}
build.quads++;
}
}
}
if(System.nanoTime()>=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();}
}
}
@@ -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;
@@ -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<GpuBufferSlice>(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<entries.size();i++){var entry=entries.get(i);pass.setUniform("DynamicTransforms",transforms.get(i));pass.setVertexBuffer(0,entry.vertices().slice());pass.drawIndexed(entry.indices(),1,0,0,0);}
}
applied=true;frames++;
}
public static void release(){TERRAIN.close();if(source!=null){source.destroyBuffers();source=null;}if(scene!=null){scene.close();scene=null;}applied=false;}
static boolean applied(){return applied;}static boolean ready(){return TERRAIN.settled();}
static boolean allocated(){return source!=null;}static int renderedFrames(){return frames;}
static int materialCount(){return TERRAIN.materials.count();}static int analyses(){return TERRAIN.materials.analyses();}
}
@@ -26,15 +26,15 @@ public final class ScreenSpaceGI {
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)
.withBindGroupLayout(LAYOUT).withBindGroupLayout(BindGroupLayout.builder().withUniform("GIBlur",UniformType.UNIFORM_BUFFER).build()).withPrimitiveTopology(PrimitiveTopology.TRIANGLES).withCull(false)
.withColorTargetState(new ColorTargetState(composite?Optional.of(new BlendFunction(BlendFactor.ONE,BlendFactor.ZERO,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 RenderPipeline GATHER=pipeline("ssgi_gather",false),APPLY=pipeline("ssgi_apply",true),FILTER=pipeline("ssgi_filter",false);
private static final Matrix4f projection=new Matrix4f();
private static TextureTarget source,indirect;
private static MappableRingBuffer uniforms;
private static TextureTarget source,indirect,filtered;
private static MappableRingBuffer uniforms,horizontal,vertical;
private static boolean applied;
private static int frames;
private ScreenSpaceGI(){}
@@ -49,8 +49,16 @@ public final class ScreenSpaceGI {
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);
filtered=new TextureTarget("Sanctuary SSGI filtered",indirect.width,indirect.height,GpuFormat.RGBA16_FLOAT,null);
}
if(uniforms==null)uniforms=new MappableRingBuffer(()->"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;}
@@ -20,6 +20,9 @@ public final class ShaderOptionsScreen extends OptionsSubScreen {
list.addBig(new OptionInstance<Integer>("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);
@@ -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();}
@@ -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);}
}
@@ -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()); }
}
@@ -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<GpuBufferSlice> 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(); }
}
@@ -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%%"
}
@@ -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 %%"
}
@@ -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);
}
@@ -0,0 +1,12 @@
#version 330
#extension GL_ARB_separate_shader_objects : require
#include <minecraft:dynamictransforms.glsl>
layout(location=0) in vec3 Position;
layout(location=1) in vec4 Color;
layout(location=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;}
@@ -0,0 +1,34 @@
#version 330
#extension GL_ARB_separate_shader_objects : require
#include <sanctuary:ssgi.glsl>
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);
}
@@ -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;
}
@@ -22,6 +22,11 @@ vec3 giNormal(vec2 uv,vec3 p){
vec3 dx=length(r-p)<length(p-l)?r-p:p-l;
vec3 dy=length(u-p)<length(p-d)?u-p:p-d;
vec3 n=cross(dx,dy);n/=max(length(n),0.00001);
// Keep voxel planes exact: tiny depth-reconstruction errors must not rotate an entire ray fan.
vec3 a=abs(n);
if(max(a.x,max(a.y,a.z))>.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){
+1 -1
View File
@@ -1,6 +1,6 @@
name = "Sanctuary"
author = "KOKA99CAB"
version = "beta.133"
version = "beta.134"
pack-format = "packwiz:1.1.0"
[index]