Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc4a0cb68e | ||
|
|
17172fd18b |
@@ -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à
|
||||
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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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.
|
||||
+2
-2
@@ -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
|
||||
|
||||
@@ -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') }
|
||||
|
||||
+73
@@ -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);}
|
||||
}
|
||||
+7
-4
@@ -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) {
|
||||
|
||||
+39
@@ -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();}
|
||||
|
||||
+5
-2
@@ -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);}
|
||||
}
|
||||
|
||||
+4
-3
@@ -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
-2
@@ -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
@@ -1,6 +1,6 @@
|
||||
name = "Sanctuary"
|
||||
author = "KOKA99CAB"
|
||||
version = "beta.133"
|
||||
version = "beta.134"
|
||||
pack-format = "packwiz:1.1.0"
|
||||
|
||||
[index]
|
||||
|
||||
Reference in New Issue
Block a user