Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
534dfec4f8 |
@@ -20,15 +20,12 @@ La [vision complète](docs/vision.md) conserve les intentions ; le
|
||||
L’économie, les claims et les dimensions décrits dans la vision restent
|
||||
à implémenter.
|
||||
|
||||
La version **beta.129** ajoute les reflets CTM optionnels sur les contours du
|
||||
relief, avec une portée indépendante de 16 à 256 blocs. Les surfaces plates
|
||||
se raccordent entre tous les matériaux. Les ombres gagnent les portées
|
||||
128/256, gardent leur intensité à midi et ne réalignent plus leur grille à
|
||||
chaque mouvement du soleil. **Options → Shaders → Sanctuary · Vanilla Light**.
|
||||
Le [ticket de livraison](docs/shader-beta129.md) précise le résultat et ses
|
||||
vérifications. La release est publiée et l’instance **Sanctuary Beta** est
|
||||
synchronisée, sauvegardes et réglages conservés. Packs normal/Test et template
|
||||
sont disponibles dans `build/`.
|
||||
La version **beta.130** ajoute un bloom réglable, les inclusions émissives des
|
||||
minerais et une **Précision** commune aux ombres et au highlight. Par défaut :
|
||||
ombres activées à 128 blocs, highlight à 32 blocs, précision 16 pixels/bloc.
|
||||
**Options → Shaders → Sanctuary · Vanilla Light**. Les réglages personnalisés
|
||||
sont conservés. Le [ticket de livraison](docs/shader-beta130.md) précise le
|
||||
résultat, les vérifications et l'état de distribution.
|
||||
La [livraison précédente](docs/clay-workshop-integration-beta110.md) réunit déjà
|
||||
l’atelier, les œufs et la neige saisonnière dans l’instance **Sanctuary Beta**.
|
||||
Les [synchronisations antérieures](docs/git-sync-beta092.md) restent documentées.
|
||||
@@ -39,6 +36,12 @@ La [carte native beta.005](docs/atlas-beta005.md) prend le relais après
|
||||
l’étude de Xaero et des alternatives libres.
|
||||
|
||||
|
||||
## beta.130 — Bloom, minerais émissifs et précision commune
|
||||
|
||||
Halo réglable des sources lumineuses et inclusions des vingt textures de
|
||||
minerais ; réglages par défaut 32/128/16 et ombres activées.
|
||||
[Contrat et vérifications](docs/shader-beta130.md).
|
||||
|
||||
## beta.129 — Reflets CTM et ombres jusqu'à 256 blocs
|
||||
|
||||
Reflets connectés réglables, cinq portées indépendantes, soleil plein à midi
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
# SHADER-130 — bloom et précision commune
|
||||
|
||||
Demandes du 17 septembre 2026 : bloom réglable du soleil et des sources
|
||||
lumineuses ; inclusions émissives du fer, cuivre, redstone, lapis, diamant,
|
||||
émeraude, rubis, saphir, quartz et or ; réglage commun « Précision » pour
|
||||
les ombres et le highlight. Branche `codex/bloom-beta130`, socle beta.129,
|
||||
Minecraft 26.3 / Java 25.
|
||||
|
||||
## Résultat
|
||||
|
||||
Dans **Options → Shaders → Sanctuary · Vanilla Light**, le bloom possède
|
||||
un interrupteur, une intensité et une diffusion réglables de 0 à 100.
|
||||
Les minerais ont leur propre interrupteur et intensité. Le bloom reste
|
||||
optionnel, désactivé initialement, avec une intensité préparée à 35, une
|
||||
diffusion à 50 et une émission des minerais à 50. Une intensité globale
|
||||
nulle arrête les passes et libère les ressources de cet effet.
|
||||
|
||||
Les nouveaux réglages par défaut sont **Pixel Shadows activé**, distance
|
||||
des ombres **128 blocs**, highlight **32 blocs** et **Précision 16 pixels
|
||||
par bloc**. La précision unique propose 8, 16 et 32 et contrôle simultanément
|
||||
ombres et arêtes. La clé JSON historique `shadowPixels` est conservée :
|
||||
les préférences déjà personnalisées ne sont pas écrasées.
|
||||
|
||||
Les vingt textures de minerais sont analysées depuis les ressources actives.
|
||||
La palette de pierre, de deepslate ou de netherrack est comparée aux pixels
|
||||
colorés des inclusions, avec un traitement du quartz et de l'or du Nether.
|
||||
Les reflets clairs accolés aux inclusions sont conservés. Le masque est
|
||||
calculé une fois par texture et conservé en cache jusqu'au rechargement des
|
||||
ressources. Une texture optionnelle `<nom>_e.png` peut préciser ce masque
|
||||
pour une texture candidate ; sa luminance maximale multipliée par l'alpha
|
||||
remplace la détection automatique. Les couleurs affichées viennent toujours
|
||||
de l'atlas Minecraft actif, y compris son animation.
|
||||
|
||||
Les sources sont les faces visibles des modèles de blocs lumineux, la lave,
|
||||
le soleil et la lune. Les lampes éteintes ne produisent pas de halo. Le
|
||||
niveau lumineux natif pondère les sources ordinaires ; l'émission des minerais
|
||||
est purement graphique. La profondeur du terrain élimine les inclusions
|
||||
cachées et masque le soleil derrière les constructions. Le cœur émissif
|
||||
conserve ses pixels nets, tandis qu'un halo séparé est diffusé à résolution
|
||||
réduite puis composé sur l'image.
|
||||
|
||||
Aucun niveau de lumière serveur, spawn, règle de jeu, format de sauvegarde,
|
||||
identifiant ou paramètre de génération ne change. Aucun monde personnel
|
||||
n'est ouvert pour les essais.
|
||||
|
||||
## Coût et limites
|
||||
|
||||
La géométrie suit uniquement les sections natives visibles et déjà chargées.
|
||||
Leur palette permet d'écarter rapidement les sections sans source ; le cache
|
||||
invalide les sections touchées lors des modifications. Construction répartie
|
||||
sur les images, au plus quatre sections démarrées et un budget souple de
|
||||
3 ms, cache GPU plafonné à 32 Mio et 16 384 faces par section. Une section
|
||||
qui dépasse les limites est omise. Aucun chargement de chunk forcé.
|
||||
|
||||
Le masque GPU est plafonné à 256 textures, chacune échantillonnée au plus en
|
||||
64 × 64 ; les textures natives 16 × 16 conservent leurs texels. Les masques
|
||||
animés utilisent leur première image, tandis que la couleur reste animée.
|
||||
La carte d'émission est à la résolution de l'écran ; la diffusion séparable
|
||||
utilise deux cibles flottantes à 1/8 de largeur et de hauteur, sans retour
|
||||
GPU vers CPU en production. Les allocations sont libérées à l'arrêt, au
|
||||
rechargement et à la déconnexion, et recréées au changement de taille.
|
||||
|
||||
Cette première version ne reconstruit pas les sources hors écran, les
|
||||
particules, les entités lumineuses ni les modèles dessinés exclusivement par
|
||||
un renderer de block entity. Les packs très différents peuvent demander un
|
||||
masque explicite. Les silhouettes des liquides sont approximées puis
|
||||
masquées par la profondeur native. Aucune promesse de FPS ni validation
|
||||
Windows/Vulkan ; les essais graphiques portent sur macOS / Apple M1 / OpenGL.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Le test client ciblé réussit en **1 min 29 s**, dans un nouveau monde plat,
|
||||
graine 122. Il analyse les vingt textures, refuse les pixels gris de leur
|
||||
roche, mesure le gain nocturne et les variations réelles d'intensité/diffusion.
|
||||
Couvrir les minerais de pierre supprime toute source émissive. Retirer les
|
||||
blocs rétablit l'émission ; le rechargement des ressources fait de même.
|
||||
Torche, glowstone, lampe alimentée, lave et soleil sont vérifiés, ainsi que
|
||||
l'arrêt de la lampe et l'occultation du soleil par de la pierre. Les valeurs
|
||||
par défaut et la conservation d'anciens choix personnalisés passent.
|
||||
|
||||
Log ciblé : `build/shader130-client-bloom.log` ; masques inspectables dans
|
||||
`mods/sanctuary/build/run/clientGameTest/bloom130-ore-masks.png` et son CSV.
|
||||
La suite shader complète réussit en **3 min 16 s** : casters hors écran,
|
||||
retrait/repose, trois précisions, cinq portées, lune, zénith, feuillage,
|
||||
realtime, caméra et rechargement restent vérifiés. Les arêtes CTM restent
|
||||
raccordées entre matériaux ; 1 295 échantillons du contour sont éclaircis
|
||||
à précision 8, 596 à 16 et 120 à 32. Les captures françaises/anglaises et
|
||||
le rendu des minerais/du soleil sont inspectés.
|
||||
|
||||
Commandes natives : `./gradlew :sanctuary:runClientGameTest
|
||||
-PsanctuaryClientTests=true -PsanctuaryPixelShadows122ClientTests=true
|
||||
-PsanctuaryClientNoVsync=true -PsanctuaryQuickTests=true`. Le test ciblé utilise
|
||||
`-PsanctuaryBloom130ClientTests=true` à la place de la propriété PixelShadows.
|
||||
Log complet : `build/shader130-client-full.log` ; captures sous
|
||||
`mods/sanctuary/build/run/clientGameTest/screenshots/`.
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
réussit en **2 min 24 s**, 126 tâches. Le serveur GameTest dédié reste exclu
|
||||
conformément au refus antérieur de son EULA. La comparaison au binaire beta.129
|
||||
trouve **24 entrées de production** modifiées, toutes limitées au shader et à
|
||||
ses interfaces. Gameplay et textures des gemmes inchangés ; aucun test,
|
||||
monde ou modèle GLB embarqué. Les sources archivées correspondent aux sources
|
||||
Java, les packs normal/Test embarquent le même JAR et le template contient
|
||||
exactement les ressources prévues.
|
||||
|
||||
Archives vérifiées dans `build/` et recopiées dans le dossier habituel
|
||||
`sanctuary-beta/build/` :
|
||||
|
||||
|
||||
- `Sanctuary-beta.130.mrpack` : 10386021 octets, SHA-256
|
||||
`7a3fcd4e496fa136eb3e0896fa7b856ae91771123186fa8df13c8fb1e0cf3e9a`.
|
||||
|
||||
- `Sanctuary-Test-beta.130.mrpack` : 10404944 octets, SHA-256
|
||||
`8de094fcbc58a300cc24d64c8d1b7931fdb8db354c75defa6bf2a2aa4a123a23`.
|
||||
|
||||
JAR : `1ac4d1b1339361fb665f03ca3bd359163db5037ffd90e684c5a461759f6076e7`.
|
||||
|
||||
Publication et synchronisation de l'instance à effectuer après le commit source.
|
||||
+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.129
|
||||
pack_version=beta.129
|
||||
mod_version=beta.130
|
||||
pack_version=beta.130
|
||||
resource_pack_version=beta.121
|
||||
maven_group=fr.koka.sanctuary
|
||||
jei_version=30.32.0-sanctuary.3
|
||||
|
||||
@@ -93,6 +93,10 @@ tasks.named('runGameTest') {
|
||||
}
|
||||
|
||||
tasks.named('processGametestResources') {
|
||||
def bloom130Client = providers.gradleProperty('sanctuaryBloom130ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryBloom130ClientTests', bloom130Client)
|
||||
if (bloom130Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.shader.Bloom130ClientChecks') }
|
||||
|
||||
def shadows122Client = providers.gradleProperty('sanctuaryPixelShadows122ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryPixelShadows122ClientTests', shadows122Client)
|
||||
if (shadows122Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.shader.PixelShadows122ClientChecks') }
|
||||
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import static fr.koka.sanctuary.client.shader.PixelShadows122ClientChecks.*;
|
||||
import com.mojang.blaze3d.platform.NativeImage;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
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.client.Screenshot;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
|
||||
/** Actual source texels, native GPU masks, occlusion and user controls in an isolated world. */
|
||||
public final class Bloom130ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c){new PixelShadows122ClientChecks(true).runTest(c);}
|
||||
static final List<String> ORES=ores();
|
||||
private static List<String> ores(){var result=new ArrayList<String>();for(String name:List.of("iron","copper","redstone","lapis","diamond","emerald","ruby","sapphire","gold")){
|
||||
String ns=name.equals("ruby")||name.equals("sapphire")?"sanctuary:":"minecraft:";result.add(ns+name+"_ore");result.add(ns+"deepslate_"+name+"_ore");}
|
||||
result.add("minecraft:nether_quartz_ore");result.add("minecraft:nether_gold_ore");return List.copyOf(result);}
|
||||
static void run(ClientGameTestContext c,TestServerContext server){
|
||||
c.runOnClient(m->{preferences();analyseTextures();VanillaLight.setSaturation(100);VanillaLight.setPixelShadows(false);VanillaLight.setEdgeHighlights(false);VanillaLight.setBloom(false);});
|
||||
server.runOnServer(s->{command(s,"time set 18000");
|
||||
for(int x=4;x<=20;x++)for(int z=4;z<=20;z++){
|
||||
s.overworld().setBlockAndUpdate(new BlockPos(x,64,z),Blocks.CONCRETE.black().defaultBlockState());
|
||||
for(int y=65;y<=70;y++)s.overworld().setBlockAndUpdate(new BlockPos(x,y,z),Blocks.AIR.defaultBlockState());
|
||||
}
|
||||
for(int i=0;i<ORES.size();i++){var state=BuiltInRegistries.BLOCK.getValue(Identifier.parse(ORES.get(i))).defaultBlockState();check(state.getLightEmission()==0,"Ores do not acquire server-side light: "+ORES.get(i));s.overworld().setBlockAndUpdate(orePos(i),state);}
|
||||
s.overworld().setBlockAndUpdate(new BlockPos(14,65,9),Blocks.REDSTONE_LAMP.defaultBlockState());
|
||||
});
|
||||
camera(c,server,10.5,71,9.5,60);c.waitTicks(40);c.waitFor(m->m.levelRenderer.hasRenderedAllSections(),1200);
|
||||
var off=capture(c,"bloom-ores-off");
|
||||
c.runOnClient(m->{VanillaLight.setOreEmission(false);VanillaLight.setBloom(true);});frames(c);
|
||||
check(luminous(emission(c))==0,"White/grey rock, ordinary surfaces and unlit lamps do not seed bloom");
|
||||
unchanged(off,capture(c,"bloom-ores-disabled"),"Disabling ore emission leaves the original scene intact");
|
||||
c.runOnClient(m->{VanillaLight.setOreEmission(true);VanillaLight.setOreIntensity(100);});frames(c);
|
||||
var source=emission(c);int orePixels=luminous(source);check(orePixels>100,"Actual exposed ore inclusions enter the GPU mask: "+orePixels);
|
||||
var on=capture(c,"bloom-ores-on");double oreGain=gain(off,on,null);check(oreGain>1000,"Ores visibly glow in the native night scene: "+oreGain);
|
||||
c.runOnClient(m->VanillaLight.setBloomIntensity(10));frames(c);double low=gain(off,capture(c,null),source);
|
||||
c.runOnClient(m->VanillaLight.setBloomIntensity(90));frames(c);double high=gain(off,capture(c,"bloom-strength-90"),source);
|
||||
check(high>Math.max(30,low*2),"Bloom strength changes the halo outside source pixels: low="+low+" high="+high);
|
||||
c.runOnClient(m->VanillaLight.setBloomRadius(0));frames(c);var tight=capture(c,"bloom-spread-0");
|
||||
c.runOnClient(m->VanillaLight.setBloomRadius(100));frames(c);var wide=capture(c,"bloom-spread-100");
|
||||
check(difference(tight,wide)>100,"Spread changes the real GPU halo");
|
||||
c.runOnClient(m->VanillaLight.setOreIntensity(0));frames(c);check(luminous(emission(c))==0,"Zero ore strength suppresses both cores and halos");
|
||||
unchanged(off,capture(c,null),"Zero ore strength restores the source image");
|
||||
c.runOnClient(m->{VanillaLight.setOreIntensity(100);VanillaLight.setBloomIntensity(35);VanillaLight.setBloomRadius(50);});frames(c);
|
||||
server.runOnServer(s->{for(int i=0;i<ORES.size();i++)s.overworld().setBlockAndUpdate(orePos(i).above(),Blocks.STONE.defaultBlockState());});c.waitTicks(25);frames(c);
|
||||
check(luminous(emission(c))==0,"Covering veins with stone removes all emission: no through-wall ore glow");
|
||||
capture(c,"bloom-covered-ores");
|
||||
server.runOnServer(s->{for(int i=0;i<ORES.size();i++)s.overworld().setBlockAndUpdate(orePos(i).above(),Blocks.AIR.defaultBlockState());});c.waitTicks(25);frames(c);
|
||||
check(luminous(emission(c))>100,"Removing covers reveals the genuine visible inclusion masks again");
|
||||
var reload=c.computeOnClient(net.minecraft.client.Minecraft::reloadResourcePacks);c.waitFor(m->reload.isDone()&&m.gui.overlay()==null,1200);reload.join();frames(c);
|
||||
check(luminous(emission(c))>100,"Resource reload rebuilds emitter meshes and masks");
|
||||
c.runOnClient(m->VanillaLight.setOreEmission(false));
|
||||
server.runOnServer(s->s.overworld().setBlockAndUpdate(new BlockPos(14,65,9),Blocks.TORCH.defaultBlockState()));c.waitTicks(25);frames(c);
|
||||
int torch=luminous(emission(c));check(torch>3,"A native torch seeds bloom: "+torch);capture(c,"bloom-torch");
|
||||
server.runOnServer(s->s.overworld().setBlockAndUpdate(new BlockPos(14,65,9),Blocks.GLOWSTONE.defaultBlockState()));c.waitTicks(25);frames(c);
|
||||
check(luminous(emission(c))>torch,"A full luminous block contributes its real textured surface");capture(c,"bloom-glowstone");
|
||||
server.runOnServer(s->s.overworld().setBlockAndUpdate(new BlockPos(14,65,9),Blocks.STONE.defaultBlockState()));c.waitTicks(25);frames(c);
|
||||
check(luminous(emission(c))==0,"Replacing the light source removes its cached emission");
|
||||
server.runOnServer(s->{s.overworld().setBlockAndUpdate(new BlockPos(14,65,9),Blocks.REDSTONE_LAMP.defaultBlockState());s.overworld().setBlockAndUpdate(new BlockPos(14,64,9),Blocks.REDSTONE_BLOCK.defaultBlockState());});
|
||||
c.waitTicks(25);frames(c);check(luminous(emission(c))>10,"Powered lamp emits through its on-state texture");capture(c,"bloom-lamp-on");
|
||||
server.runOnServer(s->s.overworld().setBlockAndUpdate(new BlockPos(14,64,9),Blocks.CONCRETE.black().defaultBlockState()));
|
||||
c.waitTicks(25);frames(c);check(luminous(emission(c))==0,"Unpowered lamp loses its glow after its native state update");
|
||||
server.runOnServer(s->s.overworld().setBlockAndUpdate(new BlockPos(14,65,9),Blocks.LAVA.defaultBlockState()));
|
||||
c.waitTicks(25);frames(c);check(luminous(emission(c))>10,"Native visible lava seeds bloom");capture(c,"bloom-lava");
|
||||
server.runOnServer(s->command(s,"time set 3000"));camera(c,server,10.5,90,9.5,70);
|
||||
var direction=c.computeOnClient(m->PixelShadows.sunDirection(m.gameRenderer.gameRenderState().levelRenderState.skyRenderState.sunAngle));
|
||||
look(c,server,(float)Math.toDegrees(Math.atan2(-direction.x,direction.z)),-(float)Math.toDegrees(Math.asin(direction.y)));
|
||||
c.runOnClient(m->VanillaLight.setBloom(false));c.waitTicks(8);var sunOff=capture(c,"bloom-sun-off");
|
||||
c.runOnClient(m->VanillaLight.setBloom(true));frames(c);var sunMask=emission(c);
|
||||
check(luminous(sunMask)>40,"The actual sun disc seeds bloom");
|
||||
double sunlight=gain(sunOff,capture(c,"bloom-sun-on"),sunMask);check(sunlight>100,"Sun halo extends beyond its source disc: "+sunlight);
|
||||
server.runOnServer(s->{var eye=s.getPlayerList().getPlayers().getFirst().getEyePosition();var center=BlockPos.containing(eye.x+direction.x*8,eye.y+direction.y*8,eye.z+direction.z*8);
|
||||
for(int x=-4;x<=4;x++)for(int y=-4;y<=4;y++)for(int z=-4;z<=4;z++)s.overworld().setBlockAndUpdate(center.offset(x,y,z),Blocks.STONE.defaultBlockState());});
|
||||
c.waitTicks(30);frames(c);check(luminous(emission(c))==0,"Solid terrain in front of the sun suppresses its bloom source");capture(c,"bloom-sun-occluded");
|
||||
c.runOnClient(m->VanillaLight.setBloom(false));c.waitTicks(6);var coveredOff=capture(c,null);
|
||||
c.runOnClient(m->{VanillaLight.setBloom(true);VanillaLight.setBloomIntensity(0);});c.waitTicks(6);c.runOnClient(m->check(!Bloom.applied()&&!Bloom.allocated(),"Zero bloom releases GPU resources"));
|
||||
unchanged(coveredOff,capture(c,null),"Zero bloom leaves no residual glow");
|
||||
c.runOnClient(m->{VanillaLight.setBloomIntensity(35);VanillaLight.setBloom(false);VanillaLight.setOreEmission(true);VanillaLight.setOreIntensity(50);});
|
||||
System.out.println("BLOOM130_PASS masks for 20 ore textures; selective night glow; halo intensity/spread; no through-wall glow; live edits; torch/glowstone/lamp/lava/sun; resource reload; defaults/migration; orePixels="+orePixels+" oreGain="+oreGain+" haloLow="+low+" haloHigh="+high+" sunHalo="+sunlight);
|
||||
}
|
||||
private static BlockPos orePos(int i){return new BlockPos(8+i%5,65,8+i/5);}
|
||||
private static void preferences(){try{
|
||||
var path=FabricLoader.getInstance().getConfigDir().resolve("sanctuary-shaders.json");Files.deleteIfExists(path);forgetPreferences();
|
||||
check(VanillaLight.pixelShadows()&&VanillaLight.shadowDistance()==128&&VanillaLight.shadowPixels()==16&&VanillaLight.edgeDistance()==32,"Fresh requested defaults: shadows ON, 128; highlights 32; shared precision 16");
|
||||
check(!VanillaLight.bloom()&&VanillaLight.bloomIntensity()==35&&VanillaLight.bloomRadius()==50,"Bloom is opt-in with usable defaults");
|
||||
Files.writeString(path,"{\"pixelShadows\":false,\"shadowDistance\":64,\"shadowPixels\":8,\"edgeDistance\":16}");forgetPreferences();
|
||||
check(!VanillaLight.pixelShadows()&&VanillaLight.shadowDistance()==64&&VanillaLight.shadowPixels()==8&&VanillaLight.edgeDistance()==16,"Existing user choices are retained");
|
||||
for(int n:new int[]{-10,0,35,100,400}){VanillaLight.setBloomIntensity(n);VanillaLight.setBloomRadius(n);VanillaLight.setOreIntensity(n);forgetPreferences();check(VanillaLight.bloomIntensity()==Math.clamp(n,0,100)&&VanillaLight.bloomRadius()==Math.clamp(n,0,100)&&VanillaLight.oreIntensity()==Math.clamp(n,0,100),"Persisted bloom controls are bounded");}
|
||||
VanillaLight.setBloomIntensity(35);VanillaLight.setBloomRadius(50);VanillaLight.setOreIntensity(50);VanillaLight.setShadowPixels(16);
|
||||
}catch(java.io.IOException e){throw new AssertionError(e);}}
|
||||
private static void analyseTextures(){
|
||||
var report=new StringBuilder("texture,selected,total,neutral_rock_selected\n");
|
||||
try(var materials=new BloomMaterials();var sheet=new NativeImage(5*16*6,4*32*6,false)){
|
||||
sheet.fillRect(0,0,sheet.getWidth(),sheet.getHeight(),0xFF16191E);
|
||||
for(int index=0;index<ORES.size();index++){
|
||||
var block=Identifier.parse(ORES.get(index));var id=block.withPath("block/"+block.getPath());
|
||||
try(var image=BloomMaterials.read(id)){
|
||||
int w=image.getWidth(),h=image.getHeight();int[] pixels=new int[w*h];for(int y=0;y<h;y++)for(int x=0;x<w;x++)pixels[y*w+x]=image.getPixel(x,y);
|
||||
var mask=materials.mask(id,pixels,w,h,true,false);int count=0,grey=0;
|
||||
for(int i=0;i<mask.length;i++){if(mask[i]>0){count++;int c=pixels[i],r=c>>16&255,g=c>>8&255,b=c&255;if(r==g&&g==b&&r<215)grey++;}}
|
||||
check(count>=4&&count<w*h*.7,"Texture has a selective inclusion mask: "+id+" "+count+"/"+mask.length);
|
||||
if(!id.getPath().contains("quartz"))check(grey==0,"Neutral host rock never emits: "+id);
|
||||
report.append(id).append(',').append(count).append(',').append(mask.length).append(',').append(grey).append('\n');
|
||||
for(int y=0;y<32*6;y++)for(int x=0;x<16*6;x++){
|
||||
int source=(y/6%16)*w/16*w+(x/6)*w/16;int color=pixels[source];if(y>=16*6&&mask[source]==0)color=0xFF16191E;
|
||||
sheet.setPixel((index%5)*16*6+x,(index/5)*32*6+y,color);
|
||||
}
|
||||
}
|
||||
}
|
||||
var root=FabricLoader.getInstance().getGameDir();sheet.writeToFile(root.resolve("bloom130-ore-masks.png"));Files.writeString(root.resolve("bloom130-ore-masks.csv"),report.toString());
|
||||
System.out.println("BLOOM130_MASKS\n"+report);
|
||||
}catch(java.io.IOException e){throw new AssertionError(e);}
|
||||
}
|
||||
private static void frames(ClientGameTestContext c){int before=c.computeOnClient(m->Bloom.renderedFrames());c.waitFor(m->Bloom.applied()&&Bloom.ready()&&Bloom.renderedFrames()>before+10,1200);c.waitTicks(8);}
|
||||
private static Frame emission(ClientGameTestContext c){var result=new CompletableFuture<Frame>();c.runOnClient(m->Screenshot.takeScreenshot(Bloom.emissionTarget(),image->{try(image){result.complete(new Frame(image.getWidth(),image.getHeight(),image.getPixels()));}}));c.waitFor(m->result.isDone(),200);return result.join();}
|
||||
private static int luminous(Frame frame){int count=0;for(int color:frame.pixels())if((color&0xFFFFFF)!=0)count++;return count;}
|
||||
private static double gain(Frame off,Frame on,Frame source){double result=0;for(int i=0;i<off.pixels().length;i++)if(source==null||(source.pixels()[i]&0xFFFFFF)==0)result+=Math.max(0,brightness(on.pixels()[i])-brightness(off.pixels()[i]));return result;}
|
||||
private static int difference(Frame a,Frame b){int n=0;for(int i=0;i<a.pixels().length;i++)if(channelDifference(a.pixels()[i],b.pixels()[i])>3)n++;return n;}
|
||||
}
|
||||
+8
@@ -35,6 +35,14 @@ final class EdgeHighlights129ClientChecks {
|
||||
check(low.brighter>100 && low.stable>low.samples*.55 && low.darker<low.samples*.005,
|
||||
"Highlight brightens local strips, preserves face interiors: "+low);
|
||||
grid(c,off,on);
|
||||
c.runOnClient(m -> VanillaLight.setShadowPixels(8));frames(c);
|
||||
var coarse=measure(off,capture(c,"edges-shared-precision-8"));
|
||||
c.runOnClient(m -> VanillaLight.setShadowPixels(32));frames(c);
|
||||
var fine=measure(off,capture(c,"edges-shared-precision-32"));
|
||||
check(coarse.brighter()>low.brighter()*1.4 && fine.brighter()<low.brighter()*.8,
|
||||
"One precision setting controls the visible highlight width: coarse="+coarse+" middle="+low+" fine="+fine);
|
||||
c.runOnClient(m -> VanillaLight.setShadowPixels(16));frames(c);
|
||||
|
||||
for (int distance : new int[]{16,32,64,128,256}) {
|
||||
c.runOnClient(m -> VanillaLight.setEdgeDistance(distance));
|
||||
frames(c);
|
||||
|
||||
+9
-3
@@ -36,6 +36,9 @@ 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;
|
||||
public PixelShadows122ClientChecks(){this(false);}
|
||||
PixelShadows122ClientChecks(boolean bloomOnly){this.bloomOnly=bloomOnly;}
|
||||
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);
|
||||
@@ -130,6 +133,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(bloomOnly){Bloom130ClientChecks.run(c,server);return;}
|
||||
// Keep VanillaLight enabled for both frames: changing its fog/grade would invalidate this oracle.
|
||||
shadows(c, false);
|
||||
var off = capture(c, "day-off");
|
||||
@@ -236,12 +240,13 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
||||
unchanged(beforeReload, capture(c, "after-reload"), "Resource reload preserves the rendered ground shadow");
|
||||
RealtimeShadows129ClientChecks.run(c, server);
|
||||
EdgeHighlights129ClientChecks.run(c, server);
|
||||
Bloom130ClientChecks.run(c, server);
|
||||
c.getInput().pressKey(InputConstants.KEY_F1);
|
||||
for (String locale : new String[]{"fr_fr", "en_us"}) {
|
||||
c.runOnClient(m -> {
|
||||
m.getLanguageManager().setSelected(locale);
|
||||
m.getLanguageManager().onResourceManagerReload(m.getResourceManager());
|
||||
for (String key : new String[]{"pixel_shadows", "shadow_pixels", "shadow_distance", "edge_highlights", "edge_intensity", "edge_distance"}) {
|
||||
for (String key : new String[]{"pixel_shadows", "shadow_pixels", "shadow_distance", "edge_highlights", "edge_intensity", "edge_distance", "precision", "bloom", "bloom_radius", "ore_emission"}) {
|
||||
String fullKey = "sanctuary.shaders." + key;
|
||||
check(!Component.translatable(fullKey).getString().equals(fullKey), "Translated " + locale + " " + key);
|
||||
}
|
||||
@@ -253,7 +258,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
||||
});
|
||||
});
|
||||
c.waitTicks(3);
|
||||
c.takeScreenshot("sanctuary-beta129-shadows-options-" + locale);
|
||||
c.takeScreenshot("sanctuary-beta130-shader-options-" + locale);
|
||||
}
|
||||
c.runOnClient(m -> m.gui.setScreen(null));
|
||||
}
|
||||
@@ -418,6 +423,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
||||
}
|
||||
|
||||
private static void checkPreferences() {
|
||||
VanillaLight.setBloom(false);
|
||||
VanillaLight.setEnabled(true);
|
||||
VanillaLight.setEdgeHighlights(false);
|
||||
VanillaLight.setSaturation(100);
|
||||
@@ -517,7 +523,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
||||
}
|
||||
|
||||
static Frame capture(ClientGameTestContext c, String name) {
|
||||
if (name != null) c.takeScreenshot("sanctuary-beta129-shadows-" + name);
|
||||
if (name != null) c.takeScreenshot("sanctuary-beta130-shader-" + name);
|
||||
var result = new CompletableFuture<Frame>();
|
||||
c.runOnClient(m -> Screenshot.takeScreenshot(m.gameRenderer.mainRenderTarget(), image -> {
|
||||
try (image) {
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
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 com.mojang.renderpearl.api.textures.GpuTextureView;
|
||||
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;
|
||||
|
||||
/** Selective emissive bloom. No brightness threshold on ordinary world blocks, no scene-copy or readback. */
|
||||
public final class Bloom {
|
||||
private static Identifier id(String path){return Identifier.parse("sanctuary:"+path);}
|
||||
private static final BindGroupLayout SCENE=BindGroupLayout.builder().withUniform("BloomScene",UniformType.UNIFORM_BUFFER)
|
||||
.withUniform("WorldDepth",UniformType.COMBINED_IMAGE_SAMPLER).build();
|
||||
private static final RenderPipeline EMITTERS=RenderPipelines.register(RenderPipeline.builder()
|
||||
.withLocation(id("pipeline/bloom_emitters")).withVertexShader(id("core/bloom_emitters")).withFragmentShader(id("core/bloom_emitters"))
|
||||
.withBindGroupLayout(BindGroupLayouts.DYNAMIC_TRANSFORMS).withBindGroupLayout(SCENE)
|
||||
.withBindGroupLayout(BindGroupLayout.builder().withUniform("BlockAtlas",UniformType.COMBINED_IMAGE_SAMPLER).withUniform("EmissionMasks",UniformType.COMBINED_IMAGE_SAMPLER).build())
|
||||
.withVertexBinding(0,BloomTerrain.FORMAT).withPrimitiveTopology(PrimitiveTopology.QUADS).withCull(false)
|
||||
.withColorTargetState(new ColorTargetState(Optional.empty(),GpuFormat.RGBA8_UNORM,ColorTargetState.WRITE_ALL))
|
||||
.withDepthStencilState(Optional.empty()).build());
|
||||
private static RenderPipeline screen(String name,BlendFunction blend,boolean scene,boolean blur){
|
||||
var layout=BindGroupLayout.builder().withUniform("InputTexture",UniformType.COMBINED_IMAGE_SAMPLER);
|
||||
if(blur)layout.withUniform("BlurParams",UniformType.UNIFORM_BUFFER);
|
||||
var builder=RenderPipeline.builder().withLocation(id("pipeline/"+name)).withVertexShader("core/screenquad").withFragmentShader(id("core/"+name)).withBindGroupLayout(layout.build());
|
||||
if(scene)builder.withBindGroupLayout(SCENE);
|
||||
return RenderPipelines.register(builder.withPrimitiveTopology(PrimitiveTopology.TRIANGLES).withCull(false)
|
||||
.withColorTargetState(new ColorTargetState(Optional.ofNullable(blend),name.equals("bloom_blur")||name.equals("bloom_downsample")?GpuFormat.RGBA16_FLOAT:GpuFormat.RGBA8_UNORM,blend==null?ColorTargetState.WRITE_ALL:ColorTargetState.WRITE_COLOR)).withDepthStencilState(Optional.empty()).build());
|
||||
}
|
||||
private static final RenderPipeline SKY=screen("bloom_sky",null,true,false);
|
||||
private static final RenderPipeline DOWNSAMPLE=screen("bloom_downsample",null,false,false);
|
||||
private static final RenderPipeline BLUR=screen("bloom_blur",null,false,true);
|
||||
private static final RenderPipeline CORE=screen("bloom_core",new BlendFunction(BlendFactor.ONE,BlendFactor.ONE_MINUS_SRC_ALPHA,BlendFactor.ZERO,BlendFactor.ONE),false,false);
|
||||
private static final RenderPipeline APPLY=screen("bloom_apply",new BlendFunction(BlendFactor.ONE,BlendFactor.ONE_MINUS_SRC_COLOR,BlendFactor.ZERO,BlendFactor.ONE),true,false);
|
||||
private static final BloomTerrain TERRAIN=new BloomTerrain();
|
||||
private static final Matrix4f projection=new Matrix4f();
|
||||
private static TextureTarget emission,small,ping;
|
||||
private static MappableRingBuffer scene,horizontal,vertical;
|
||||
private static int frames;
|
||||
private static boolean applied;
|
||||
private Bloom(){}
|
||||
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 render(){
|
||||
applied=false;var mc=Minecraft.getInstance();
|
||||
if(!VanillaLight.active()||!VanillaLight.bloom()||VanillaLight.bloomIntensity()==0||mc.level==null){release();return;}
|
||||
if(PanoramaCapture.rendering())return;
|
||||
var state=mc.gameRenderer.gameRenderState().levelRenderState;var camera=state.cameraRenderState;
|
||||
if(camera.fogType!=FogType.NONE)return;
|
||||
var main=mc.gameRenderer.mainRenderTarget();
|
||||
if(emission==null||emission.width!=main.width||emission.height!=main.height){
|
||||
targetsClose();emission=target("emission",main.width,main.height);small=target("bloom low",Math.max(1,main.width/8),Math.max(1,main.height/8));ping=target("bloom ping",small.width,small.height);
|
||||
}
|
||||
if(scene==null){scene=uniforms("bloom scene",144);horizontal=uniforms("bloom horizontal",16);vertical=uniforms("bloom vertical",16);}
|
||||
var entries=TERRAIN.update();var atlas=TERRAIN.materials.atlas();
|
||||
var vp=new Matrix4f(projection).mul(camera.viewRotationMatrix);var inverse=new Matrix4f(vp).invert();
|
||||
var sun=PixelShadows.sunDirection(state.skyRenderState.sunAngle);var moon=PixelShadows.sunDirection(state.skyRenderState.moonAngle);
|
||||
float celestial=state.skyRenderState.skybox==DimensionType.Skybox.OVERWORLD?state.skyRenderState.rainBrightness:0;
|
||||
var fog=camera.fogData;scene.rotate();
|
||||
try(var mapped=scene.currentBuffer().map(false,true)){
|
||||
Std140Builder.intoBuffer(mapped.data()).putMat4f(inverse)
|
||||
.putVec4(VanillaLight.oreEmission()?VanillaLight.oreIntensity()/100f:0,RenderSystem.getDevice().getDeviceInfo().isZZeroToOne()?1:0,VanillaLight.bloomIntensity()/50f,0)
|
||||
.putVec4(sun.x,sun.y,sun.z,celestial).putVec4(moon.x,moon.y,moon.z,celestial*.35f)
|
||||
.putVec4(fog.renderDistanceStart,fog.renderDistanceEnd,fog.environmentalStart,fog.environmentalEnd).putVec4(fog.color.w,0,0,0);
|
||||
}
|
||||
float radius=.25f+VanillaLight.bloomRadius()*.0075f;
|
||||
horizontal.rotate();try(var mapped=horizontal.currentBuffer().map(false,true)){Std140Builder.intoBuffer(mapped.data()).putVec4(radius/small.width,0,0,0);}
|
||||
vertical.rotate();try(var mapped=vertical.currentBuffer().map(false,true)){Std140Builder.intoBuffer(mapped.data()).putVec4(0,radius/small.height,0,0);}
|
||||
// Gather transforms before opening any pass (the native dynamic ring may resize).
|
||||
var transforms=new ArrayList<GpuBufferSlice>(entries.size());int max=0;
|
||||
for(var entry:entries){var pos=entry.origin();var offset=new Vector3f((float)(pos.getX()-camera.pos.x),(float)(pos.getY()-camera.pos.y),(float)(pos.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 nearest=RenderSystem.getSamplerCache().getClampToEdge(FilterMode.NEAREST);
|
||||
screen(SKY,emission.getColorTextureView(),main.getColorTextureView(),true,null);
|
||||
if(atlas!=null&&!entries.isEmpty())try(var pass=RenderSystem.getDevice().createCommandEncoder().createRenderPass(()->"Sanctuary visible emitters",emission.getColorTextureView(),Optional.empty())){
|
||||
pass.setPipeline(RenderSystem.getCompiledPipeline(EMITTERS));pass.setUniform("BloomScene",scene.currentBuffer());pass.setUniform("WorldDepth",main.getDepthTextureView(),nearest);
|
||||
pass.setUniform("BlockAtlas",mc.getTextureManager().getTexture(TextureAtlas.LOCATION_BLOCKS).getTextureView(),nearest);pass.setUniform("EmissionMasks",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);}
|
||||
}
|
||||
screen(DOWNSAMPLE,small.getColorTextureView(),emission.getColorTextureView(),false,null);
|
||||
screen(BLUR,ping.getColorTextureView(),small.getColorTextureView(),false,horizontal);
|
||||
screen(BLUR,small.getColorTextureView(),ping.getColorTextureView(),false,vertical);
|
||||
screen(CORE,main.getColorTextureView(),emission.getColorTextureView(),false,null);
|
||||
screen(APPLY,main.getColorTextureView(),small.getColorTextureView(),true,null);
|
||||
applied=true;frames++;
|
||||
}
|
||||
private static void screen(RenderPipeline pipeline,GpuTextureView output,GpuTextureView input,boolean useScene,MappableRingBuffer blur){
|
||||
try(var pass=RenderSystem.getDevice().createCommandEncoder().createRenderPass(()->"Sanctuary bloom",output,Optional.empty())){
|
||||
pass.setPipeline(RenderSystem.getCompiledPipeline(pipeline));pass.setUniform("InputTexture",input,RenderSystem.getSamplerCache().getClampToEdge(pipeline==CORE||pipeline==SKY?FilterMode.NEAREST:FilterMode.LINEAR));
|
||||
if(useScene){pass.setUniform("BloomScene",scene.currentBuffer());pass.setUniform("WorldDepth",Minecraft.getInstance().gameRenderer.mainRenderTarget().getDepthTextureView(),RenderSystem.getSamplerCache().getClampToEdge(FilterMode.NEAREST));}
|
||||
if(blur!=null)pass.setUniform("BlurParams",blur.currentBuffer());pass.draw(3,1,0,0);
|
||||
}
|
||||
}
|
||||
private static TextureTarget target(String name,int w,int h){return new TextureTarget("Sanctuary "+name,w,h,name.equals("emission")?GpuFormat.RGBA8_UNORM:GpuFormat.RGBA16_FLOAT,null);}
|
||||
private static MappableRingBuffer uniforms(String name,int size){return new MappableRingBuffer(()->"Sanctuary "+name,GpuBuffer.USAGE_UNIFORM|GpuBuffer.USAGE_MAP_WRITE,size);}
|
||||
private static void targetsClose(){if(emission!=null){emission.destroyBuffers();small.destroyBuffers();ping.destroyBuffers();emission=small=ping=null;}}
|
||||
public static void release(){TERRAIN.close();targetsClose();if(scene!=null){scene.close();horizontal.close();vertical.close();scene=horizontal=vertical=null;}applied=false;}
|
||||
static boolean applied(){return applied;}
|
||||
static boolean allocated(){return emission!=null;}
|
||||
static boolean ready(){return TERRAIN.settled();}
|
||||
static int renderedFrames(){return frames;}
|
||||
static TextureTarget emissionTarget(){return emission;}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import com.mojang.blaze3d.platform.NativeImage;
|
||||
import java.util.*;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.texture.DynamicTexture;
|
||||
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
/** Analyse source texels once per resource reload, never rendered frames or GPU pixels. */
|
||||
final class BloomMaterials implements AutoCloseable {
|
||||
static final int TILE=68, INNER=64, SIZE=TILE*16, MAX_SPRITES=256;
|
||||
record Slot(int x,int y,boolean empty) {
|
||||
float u(float local){return (x+2+Math.clamp(local,0,1)*INNER)/SIZE;}
|
||||
float v(float local){return (y+2+Math.clamp(local,0,1)*INNER)/SIZE;}
|
||||
}
|
||||
private final Map<String,Slot> slots=new HashMap<>();
|
||||
private final Map<BlockState,Boolean> ores=new IdentityHashMap<>();
|
||||
private final Map<Identifier,int[]> hosts=new HashMap<>();
|
||||
private DynamicTexture atlas;
|
||||
private boolean dirty;
|
||||
private static final Set<String> ORES=Set.of("iron","copper","redstone","lapis","diamond","emerald","ruby","sapphire","quartz","gold");
|
||||
|
||||
boolean ore(BlockState state){return ores.computeIfAbsent(state,s->{
|
||||
var id=BuiltInRegistries.BLOCK.getKey(s.getBlock());
|
||||
if(!id.getNamespace().equals("minecraft")&&!id.getNamespace().equals("sanctuary"))return false;
|
||||
String name=id.getPath().replaceFirst("^(deepslate_|nether_)","");
|
||||
return name.endsWith("_ore")&&ORES.contains(name.substring(0,name.length()-4));
|
||||
});}
|
||||
boolean candidate(BlockState state){return ore(state)||state.getLightEmission()>0||state.emissiveRendering();}
|
||||
|
||||
Slot slot(TextureAtlasSprite sprite,boolean ore,boolean fullEmission){
|
||||
String key=sprite.contents().name()+":"+ore+":"+fullEmission;
|
||||
var cached=slots.get(key);if(cached!=null)return cached;
|
||||
if(slots.size()>=MAX_SPRITES)return new Slot(0,0,true);
|
||||
int index=slots.size(),x=(index%16)*TILE,y=(index/16)*TILE;
|
||||
if(atlas==null){atlas=new DynamicTexture("Sanctuary emissive masks",SIZE,SIZE,false);atlas.getPixels().fillRect(0,0,SIZE,SIZE,0);}
|
||||
boolean any=false;
|
||||
var id=sprite.contents().name();
|
||||
try(var image=read(id)){
|
||||
int frameWidth=Math.min(image.getWidth(),sprite.contents().width()),frameHeight=Math.min(image.getHeight(),sprite.contents().height());
|
||||
int w=Math.min(INNER,frameWidth),h=Math.min(INNER,frameHeight);
|
||||
int[] colors=new int[w*h];for(int iy=0;iy<h;iy++)for(int ix=0;ix<w;ix++)colors[iy*w+ix]=image.getPixel(ix*frameWidth/w,iy*frameHeight/h);
|
||||
int[] mask=mask(id,colors,w,h,ore,fullEmission);
|
||||
// Standard optional _e texture takes precedence, including in personal resource packs.
|
||||
var override=Minecraft.getInstance().getResourceManager().getResource(texture(id.withSuffix("_e")));
|
||||
if(override.isPresent())try(var stream=override.get().open();var explicit=NativeImage.read(stream)){
|
||||
for(int iy=0;iy<h;iy++)for(int ix=0;ix<w;ix++){
|
||||
int c=explicit.getPixel(ix*explicit.getWidth()/w,iy*Math.min(explicit.getHeight(),frameHeight)/h);
|
||||
mask[iy*w+ix]=(c>>>24)*Math.max((c>>16)&255,Math.max((c>>8)&255,c&255))/255;
|
||||
}
|
||||
}
|
||||
for(int iy=0;iy<TILE;iy++)for(int ix=0;ix<TILE;ix++){
|
||||
int value=mask[Math.clamp((iy-2)*h/INNER,0,h-1)*w+Math.clamp((ix-2)*w/INNER,0,w-1)];
|
||||
any|=value>0;atlas.getPixels().setPixel(x+ix,y+iy,0xFF000000|value*0x010101);
|
||||
}
|
||||
}catch(java.io.IOException exception){
|
||||
fr.koka.sanctuary.SanctuaryMod.LOGGER.warn("Cannot analyse emissive texture {}",id,exception);
|
||||
}
|
||||
var result=new Slot(x,y,!any);slots.put(key,result);dirty=true;return result;
|
||||
}
|
||||
|
||||
int[] mask(Identifier id,int[] colors,int width,int height,boolean ore,boolean fullEmission){
|
||||
int[] result=new int[colors.length];String name=id.getPath();
|
||||
boolean quartz=name.contains("quartz"),nether=name.contains("nether_gold")||quartz;
|
||||
int[] host=ore?host(nether?"netherrack":name.contains("deepslate")?"deepslate":"stone"):new int[0];
|
||||
boolean structural=!ore&&!fullEmission&&(name.contains("furnace_side")||name.contains("furnace_top")
|
||||
||name.contains("smoker_side")||name.contains("smoker_top")||name.contains("smoker_bottom")
|
||||
||name.contains("campfire_log")&&!name.contains("lit")||name.contains("respawn_anchor_side")
|
||||
||name.contains("respawn_anchor_bottom"));
|
||||
for(int i=0;i<colors.length;i++){
|
||||
int c=colors[i],r=c>>16&255,g=c>>8&255,b=c&255,max=Math.max(r,Math.max(g,b)),min=Math.min(r,Math.min(g,b));
|
||||
if((c>>>24)<128||structural)continue;
|
||||
if(ore){
|
||||
int difference=255;
|
||||
for(int base:host){int br=base>>16&255,bg=base>>8&255,bb=base&255;
|
||||
difference=Math.min(difference,Math.max(Math.abs(r-br),Math.max(Math.abs(g-bg),Math.abs(b-bb))));}
|
||||
boolean inclusion=nether?(quartz?g>r*.65&&b>r*.5&&max>110:r>110&&g>75&&g>b*1.45):max-min>=10;
|
||||
if(inclusion&&difference>=10)result[i]=255;
|
||||
}else{
|
||||
// Luminosity is tied to the block state; cold/off variants never enter this path.
|
||||
// Dark casings/wood are suppressed; live atlas colours and animation stay native.
|
||||
float value=fullEmission?1:Math.clamp((max-100)/110f,0,1);
|
||||
if(name.contains("torch")||name.contains("furnace")||name.contains("smoker")||name.contains("campfire"))
|
||||
value*=Math.clamp((max-min-35)/65f,0,1);
|
||||
result[i]=Math.round(value*255);
|
||||
}
|
||||
}
|
||||
if(ore&&!nether){
|
||||
// Retain pale/white highlights touching coloured inclusions, without growing into grey rock.
|
||||
int[] seed=result.clone();
|
||||
for(int y=0;y<height;y++)for(int x=0;x<width;x++)if(seed[y*width+x]==0){
|
||||
int c=colors[y*width+x],r=c>>16&255,g=c>>8&255,b=c&255;
|
||||
if((c>>>24)<128||Math.min(r,Math.min(g,b))<215)continue;
|
||||
for(int dy=-1;dy<=1;dy++)for(int dx=-1;dx<=1;dx++)if(x+dx>=0&&x+dx<width&&y+dy>=0&&y+dy<height&&seed[(y+dy)*width+x+dx]>0)result[y*width+x]=255;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
private int[] host(String name){return hosts.computeIfAbsent(Identifier.withDefaultNamespace("block/"+name),id->{
|
||||
try(var image=read(id)){var values=new LinkedHashSet<Integer>();for(int y=0;y<Math.min(64,image.getHeight());y++)for(int x=0;x<Math.min(64,image.getWidth());x++)values.add(image.getPixel(x*image.getWidth()/Math.min(64,image.getWidth()),y*image.getHeight()/Math.min(64,image.getHeight())));return values.stream().mapToInt(Integer::intValue).toArray();}
|
||||
catch(java.io.IOException exception){return new int[0];}
|
||||
});}
|
||||
static Identifier texture(Identifier id){return Identifier.fromNamespaceAndPath(id.getNamespace(),"textures/"+id.getPath()+".png");}
|
||||
static NativeImage read(Identifier id)throws java.io.IOException{try(var stream=Minecraft.getInstance().getResourceManager().getResourceOrThrow(texture(id)).open()){return NativeImage.read(stream);}}
|
||||
DynamicTexture atlas(){if(dirty){atlas.upload();dirty=false;}return atlas;}
|
||||
int count(){return slots.size();}
|
||||
public void close(){if(atlas!=null){atlas.close();atlas=null;}slots.clear();ores.clear();hosts.clear();dirty=false;}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
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;
|
||||
|
||||
/** Native visible sections only, palette rejection and bounded incremental emitter meshing. */
|
||||
final class BloomTerrain 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=32L*1024*1024;
|
||||
private final Map<Long,Section> sections=new HashMap<>();
|
||||
final BloomMaterials materials=new BloomMaterials();
|
||||
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;}
|
||||
long frame=mc.level.getGameTime();var active=new ArrayList<Section>();
|
||||
for(var render:mc.levelRenderer.visibleSections()){
|
||||
long key=render.getSectionNode();var pos=render.getRenderOrigin();
|
||||
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();}}
|
||||
Vec3 camera=mc.gameRenderer.gameRenderState().levelRenderState.cameraRenderState.pos;
|
||||
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 bloom emitters",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);boolean ore=materials.ore(state);
|
||||
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,ore,material.lightEmission()>0||state.emissiveRendering()||sprite.isAnimated());
|
||||
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;
|
||||
// Alpha marks ore vs. native emitter; the mask alpha is not a gameplay light value.
|
||||
int light=state.emissiveRendering()?15:Math.max(state.getLightEmission(),material.lightEmission());
|
||||
int alpha=ore?Math.round(light/15f*120):128+Math.round(light/15f*127);
|
||||
int color=(alpha<<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(state.liquid()&&state.getLightEmission()>0)fluid(build,pos,x,y,z,state.getFluidState().getHeight(mc.level,pos));
|
||||
}
|
||||
if((build.cursor&31)==0&&System.nanoTime()>=deadline)return build.cursor==4096;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
private void fluid(Build build,BlockPos pos,int x,int y,int z,float height){
|
||||
var mc=Minecraft.getInstance();var sprite=((TextureAtlas)mc.getTextureManager().getTexture(TextureAtlas.LOCATION_BLOCKS)).getSprite(net.minecraft.resources.Identifier.withDefaultNamespace("block/lava_still"));
|
||||
var slot=materials.slot(sprite,false,true);if(slot.empty())return;
|
||||
// Native world depth retains the actual fluid silhouette and occludes these candidate faces.
|
||||
float[][][] faces={{{0,height,0},{0,height,1},{1,height,1},{1,height,0}},{{0,0,0},{0,height,0},{1,height,0},{1,0,0}},
|
||||
{{0,0,1},{1,0,1},{1,height,1},{0,height,1}},{{0,0,0},{0,0,1},{0,height,1},{0,height,0}},{{1,0,0},{1,height,0},{1,height,1},{1,0,1}}};
|
||||
for(var face:faces){if(build.quads>=16384){build.overflow=true;return;}
|
||||
for(int i=0;i<4;i++){float u=i>=2?1:0,v=i==1||i==2?1:0;var p=face[i];
|
||||
build.out.addVertex(x+p[0],y+p[1],z+p[2]).setColor(0xFFFFFFFF).setUv(sprite.getU(u),sprite.getV(v)).setUv3(slot.u(u),slot.v(v));}
|
||||
build.quads++;
|
||||
}
|
||||
}
|
||||
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();}
|
||||
}
|
||||
}
|
||||
@@ -68,7 +68,7 @@ public final class EdgeHighlights {
|
||||
Std140Builder.intoBuffer(mapped.data()).putMat4f(inverse).putMat4f(viewProjection)
|
||||
.putVec4((float)(camera.pos.x - Math.floor(camera.pos.x)),
|
||||
(float)(camera.pos.y - Math.floor(camera.pos.y)),
|
||||
(float)(camera.pos.z - Math.floor(camera.pos.z)), 16)
|
||||
(float)(camera.pos.z - Math.floor(camera.pos.z)), VanillaLight.shadowPixels())
|
||||
.putVec4(VanillaLight.edgeIntensity() * .004f,
|
||||
RenderSystem.getDevice().getDeviceInfo().isZZeroToOne() ? 1 : 0,
|
||||
fog.renderDistanceStart, fog.renderDistanceEnd)
|
||||
|
||||
+15
-3
@@ -17,6 +17,16 @@ public final class ShaderOptionsScreen extends OptionsSubScreen {
|
||||
list.addBig(new OptionInstance<Integer>("sanctuary.shaders.saturation",OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders.saturation_help")),(caption,value)->Component.translatable("sanctuary.shaders.saturation_value",value),
|
||||
new OptionInstance.IntRange(0,200),VanillaLight.saturation(),VanillaLight::setSaturation));
|
||||
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),Codec.INT),VanillaLight.shadowPixels(),VanillaLight::setShadowPixels));
|
||||
list.addBig(OptionInstance.createBoolean("sanctuary.shaders.bloom",OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders.bloom_help")),VanillaLight.bloom(),VanillaLight::setBloom));
|
||||
slider("bloom_intensity",VanillaLight.bloomIntensity(),VanillaLight::setBloomIntensity);
|
||||
slider("bloom_radius",VanillaLight.bloomRadius(),VanillaLight::setBloomRadius);
|
||||
list.addBig(OptionInstance.createBoolean("sanctuary.shaders.ore_emission",OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders.ore_emission_help")),VanillaLight.oreEmission(),VanillaLight::setOreEmission));
|
||||
slider("ore_intensity",VanillaLight.oreIntensity(),VanillaLight::setOreIntensity);
|
||||
list.addBig(OptionInstance.createBoolean("sanctuary.shaders.edge_highlights",OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders.edge_highlights_help")),
|
||||
VanillaLight.edgeHighlights(),VanillaLight::setEdgeHighlights));
|
||||
@@ -29,11 +39,13 @@ public final class ShaderOptionsScreen extends OptionsSubScreen {
|
||||
list.addBig(OptionInstance.createBoolean("sanctuary.shaders.pixel_shadows",OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders.pixel_shadows_help")),
|
||||
VanillaLight.pixelShadows(),VanillaLight::setPixelShadows));
|
||||
list.addBig(new OptionInstance<Integer>("sanctuary.shaders.shadow_pixels",OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders.shadow_pixels_help")),(caption,value)->Component.translatable("sanctuary.shaders.shadow_pixels_value",value),
|
||||
new OptionInstance.Enum<>(List.of(8,16,32),Codec.INT),VanillaLight.shadowPixels(),VanillaLight::setShadowPixels));
|
||||
list.addBig(new OptionInstance<Integer>("sanctuary.shaders.shadow_distance",OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders.shadow_distance_help")),(caption,value)->Component.translatable("sanctuary.shaders.shadow_distance_value",value),
|
||||
new OptionInstance.Enum<>(List.of(16,32,64,128,256),Codec.INT),VanillaLight.shadowDistance(),VanillaLight::setShadowDistance));
|
||||
}
|
||||
private void slider(String key,int value,java.util.function.Consumer<Integer> setter){
|
||||
list.addBig(new OptionInstance<Integer>("sanctuary.shaders."+key,OptionInstance.cachedConstantTooltip(
|
||||
Component.translatable("sanctuary.shaders."+key+"_help")),(caption,v)->Component.translatable("sanctuary.shaders."+key+"_value",v),
|
||||
new OptionInstance.IntRange(0,100),value,setter::accept));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,12 +17,17 @@ public final class VanillaLight {
|
||||
private static final class Preferences {
|
||||
boolean enabled=true;
|
||||
int saturation=142;
|
||||
boolean pixelShadows=false;
|
||||
boolean pixelShadows=true;
|
||||
boolean bloom=false;
|
||||
int bloomIntensity=35;
|
||||
int bloomRadius=50;
|
||||
boolean oreEmission=true;
|
||||
int oreIntensity=50;
|
||||
boolean edgeHighlights=false;
|
||||
int edgeIntensity=30;
|
||||
int edgeDistance=32;
|
||||
int shadowPixels=16;
|
||||
int shadowDistance=32;
|
||||
int shadowDistance=128;
|
||||
}
|
||||
private static Path path(){return FabricLoader.getInstance().getConfigDir().resolve("sanctuary-shaders.json");}
|
||||
public static boolean enabled(){
|
||||
@@ -30,6 +35,9 @@ 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.bloomIntensity=Math.clamp(preferences.bloomIntensity,0,100);
|
||||
preferences.bloomRadius=Math.clamp(preferences.bloomRadius,0,100);
|
||||
preferences.oreIntensity=Math.clamp(preferences.oreIntensity,0,100);
|
||||
preferences.edgeIntensity=Math.clamp(preferences.edgeIntensity,0,100);
|
||||
preferences.edgeDistance=normalizeShadowDistance(preferences.edgeDistance);
|
||||
preferences.saturation=Math.clamp(preferences.saturation,0,200);
|
||||
@@ -47,6 +55,16 @@ 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 bloom(){enabled();return preferences.bloom;}
|
||||
public static int bloomIntensity(){enabled();return preferences.bloomIntensity;}
|
||||
public static int bloomRadius(){enabled();return preferences.bloomRadius;}
|
||||
public static boolean oreEmission(){enabled();return preferences.oreEmission;}
|
||||
public static int oreIntensity(){enabled();return preferences.oreIntensity;}
|
||||
public static void setBloom(boolean value){enabled();preferences.bloom=value;save();}
|
||||
public static void setBloomIntensity(int value){enabled();preferences.bloomIntensity=Math.clamp(value,0,100);save();}
|
||||
public static void setBloomRadius(int value){enabled();preferences.bloomRadius=Math.clamp(value,0,100);save();}
|
||||
public static void setOreEmission(boolean value){enabled();preferences.oreEmission=value;save();}
|
||||
public static void setOreIntensity(int value){enabled();preferences.oreIntensity=Math.clamp(value,0,100);save();}
|
||||
public static boolean pixelShadows(){enabled();return preferences.pixelShadows;}
|
||||
public static int shadowPixels(){enabled();return preferences.shadowPixels;}
|
||||
public static int shadowDistance(){enabled();return preferences.shadowDistance;}
|
||||
|
||||
+3
-2
@@ -1,6 +1,7 @@
|
||||
package fr.koka.sanctuary.mixin.client;
|
||||
|
||||
import fr.koka.sanctuary.client.shader.PixelShadows;
|
||||
import fr.koka.sanctuary.client.shader.Bloom;
|
||||
import net.minecraft.client.renderer.extract.LevelExtractor;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
@@ -13,8 +14,8 @@ 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);
|
||||
PixelShadows.dirtyBlock(pos); Bloom.dirtyBlock(pos);
|
||||
}
|
||||
@Inject(method="allChanged", at=@At("HEAD"))
|
||||
private void sanctuary$reload(CallbackInfo ci) { PixelShadows.release(); }
|
||||
private void sanctuary$reload(CallbackInfo ci) { PixelShadows.release(); Bloom.release(); }
|
||||
}
|
||||
|
||||
+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.Bloom;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
@@ -15,11 +16,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);
|
||||
if (before != after) {PixelShadows.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()); }
|
||||
private void sanctuary$loaded(ChunkPos pos, CallbackInfo ci) { PixelShadows.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()); }
|
||||
private void sanctuary$unload(LevelChunk chunk, CallbackInfo ci) { PixelShadows.dirtyChunk(chunk.getPos().x(), chunk.getPos().z()); Bloom.dirtyChunk(chunk.getPos().x(), chunk.getPos().z()); }
|
||||
}
|
||||
|
||||
+4
-2
@@ -5,6 +5,7 @@ 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.EdgeHighlights;
|
||||
import fr.koka.sanctuary.client.shader.Bloom;
|
||||
import net.minecraft.client.renderer.GameRenderer;
|
||||
import net.minecraft.client.renderer.ProjectionMatrixBuffer;
|
||||
import org.joml.Matrix4f;
|
||||
@@ -19,10 +20,11 @@ abstract class PixelShadowRendererMixin {
|
||||
private GpuBufferSlice sanctuary$projection(ProjectionMatrixBuffer buffer, Matrix4f matrix, Operation<GpuBufferSlice> original) {
|
||||
PixelShadows.captureProjection(matrix);
|
||||
EdgeHighlights.captureProjection(matrix);
|
||||
Bloom.captureProjection(matrix);
|
||||
return original.call(buffer, matrix);
|
||||
}
|
||||
@Inject(method="renderLevel", at=@At(value="INVOKE", target="Lnet/minecraft/client/renderer/GameRenderer;render3dHud(Lnet/minecraft/client/renderer/state/level/CameraRenderState;Lnet/minecraft/client/renderer/state/level/PlayerRenderState;Lnet/minecraft/client/renderer/state/OptionsRenderState;Z)V"))
|
||||
private void sanctuary$shadows(CallbackInfo ci) { PixelShadows.render(); EdgeHighlights.render(); }
|
||||
private void sanctuary$shadows(CallbackInfo ci) { PixelShadows.render(); EdgeHighlights.render(); Bloom.render(); }
|
||||
@Inject(method={"close", "setLevel"}, at=@At("HEAD"))
|
||||
private void sanctuary$close(CallbackInfo ci) { PixelShadows.release(); EdgeHighlights.release(); }
|
||||
private void sanctuary$close(CallbackInfo ci) { PixelShadows.release(); EdgeHighlights.release(); Bloom.release(); }
|
||||
}
|
||||
|
||||
@@ -2263,7 +2263,7 @@
|
||||
"sanctuary.shaders.edge_intensity_help": "Highlight strength along edges. Independent of cast shadows.",
|
||||
"sanctuary.shaders.edge_intensity_value": "Edge intensity: %s %%",
|
||||
"sanctuary.shaders.pixel_shadows": "Pixel shadows",
|
||||
"sanctuary.shaders.pixel_shadows_help": "Sun shadows and subtle moon shadows on a pixel grid. Dappled foliage and stable contours. Disabled by default; requires Vanilla Light.",
|
||||
"sanctuary.shaders.pixel_shadows_help": "Sun shadows and subtle moon shadows on a pixel grid. Dappled foliage and stable contours. Enabled by default at 128 blocks; requires Vanilla Light.",
|
||||
"sanctuary.shaders.shadow_pixels": "Shadow precision",
|
||||
"sanctuary.shaders.shadow_pixels_value": "%s pixels/block",
|
||||
"sanctuary.shaders.shadow_pixels_help": "Grid precision: 8, 16 or 32 pixels per block. Higher values make shadow pixels finer. Requires Vanilla Light and pixel shadows.",
|
||||
@@ -2272,5 +2272,21 @@
|
||||
"sanctuary.shaders.shadow_distance_help": "Shadow range: 16, 32, 64, 128 or 256 blocks, within loaded terrain. Higher distances cost more performance. Requires Vanilla Light and pixel shadows.",
|
||||
"sanctuary.shaders.edge_distance": "Highlight distance",
|
||||
"sanctuary.shaders.edge_distance_value": "%s blocks",
|
||||
"sanctuary.shaders.edge_distance_help": "Independent range: 16, 32, 64, 128 or 256 blocks. Limited to visible terrain and render distance. Distant contours become naturally finer."
|
||||
"sanctuary.shaders.edge_distance_help": "Independent range: 16, 32, 64, 128 or 256 blocks. Limited to visible terrain and render distance. Distant contours become naturally finer.",
|
||||
"sanctuary.shaders.precision": "Precision",
|
||||
"sanctuary.shaders.precision_help": "Shared grid for shadows and edge highlights: 8, 16 or 32 pixels per block.",
|
||||
"sanctuary.shaders.precision_value": "%s pixels/block",
|
||||
"sanctuary.shaders.bloom": "Bloom",
|
||||
"sanctuary.shaders.bloom_help": "Glow around the sun and luminous surfaces. Ores can also become emissive. Requires Vanilla Light. OFF by default.",
|
||||
"sanctuary.shaders.bloom_intensity": "Bloom strength",
|
||||
"sanctuary.shaders.bloom_intensity_help": "Halo strength. Zero disables bloom and frees its resources.",
|
||||
"sanctuary.shaders.bloom_intensity_value": "Bloom: %s%%",
|
||||
"sanctuary.shaders.bloom_radius": "Bloom spread",
|
||||
"sanctuary.shaders.bloom_radius_help": "Halo size around luminous pixels. Texture details stay sharp.",
|
||||
"sanctuary.shaders.bloom_radius_value": "Spread: %s%%",
|
||||
"sanctuary.shaders.ore_emission": "Emissive ores",
|
||||
"sanctuary.shaders.ore_emission_help": "With bloom: makes ore inclusions glow while preserving their host rock. Visual effect only.",
|
||||
"sanctuary.shaders.ore_intensity": "Ore emission",
|
||||
"sanctuary.shaders.ore_intensity_help": "Inclusion brightness. Does not change world light levels or mob spawning.",
|
||||
"sanctuary.shaders.ore_intensity_value": "Ores: %s%%"
|
||||
}
|
||||
|
||||
@@ -2263,7 +2263,7 @@
|
||||
"sanctuary.shaders.edge_intensity_help": "Force du reflet sur les arêtes. Le réglage est indépendant des ombres portées.",
|
||||
"sanctuary.shaders.edge_intensity_value": "Intensité des arêtes : %s %%",
|
||||
"sanctuary.shaders.pixel_shadows": "Ombres pixelisées",
|
||||
"sanctuary.shaders.pixel_shadows_help": "Ombres du soleil et ombres légères de la lune, sur une grille de pixels. Feuillage irrégulier et contours stables. Désactivées par défaut ; nécessite Vanilla Light.",
|
||||
"sanctuary.shaders.pixel_shadows_help": "Ombres du soleil et ombres légères de la lune, sur une grille de pixels. Feuillage irrégulier et contours stables. Activées par défaut, portée 128 blocs ; nécessite Vanilla Light.",
|
||||
"sanctuary.shaders.shadow_pixels": "Précision des ombres",
|
||||
"sanctuary.shaders.shadow_pixels_value": "%s pixels/bloc",
|
||||
"sanctuary.shaders.shadow_pixels_help": "Précision de la grille : 8, 16 ou 32 pixels par bloc. Plus la valeur est élevée, plus les pixels des ombres sont fins. Nécessite Vanilla Light et les ombres pixelisées.",
|
||||
@@ -2272,5 +2272,21 @@
|
||||
"sanctuary.shaders.shadow_distance_help": "Portée des ombres : 16, 32, 64, 128 ou 256 blocs, dans le terrain chargé. Une distance élevée coûte davantage en performances. Nécessite Vanilla Light et les ombres pixelisées.",
|
||||
"sanctuary.shaders.edge_distance": "Portée des arêtes",
|
||||
"sanctuary.shaders.edge_distance_value": "%s blocs",
|
||||
"sanctuary.shaders.edge_distance_help": "Portée indépendante : 16, 32, 64, 128 ou 256 blocs. Limitée au terrain visible et à la distance de rendu. Les contours lointains s’affinent naturellement."
|
||||
"sanctuary.shaders.edge_distance_help": "Portée indépendante : 16, 32, 64, 128 ou 256 blocs. Limitée au terrain visible et à la distance de rendu. Les contours lointains s’affinent naturellement.",
|
||||
"sanctuary.shaders.precision": "Précision",
|
||||
"sanctuary.shaders.precision_help": "Grille commune aux ombres et aux reflets sur les arêtes : 8, 16 ou 32 pixels par bloc.",
|
||||
"sanctuary.shaders.precision_value": "%s pixels/bloc",
|
||||
"sanctuary.shaders.bloom": "Bloom",
|
||||
"sanctuary.shaders.bloom_help": "Halo du soleil et des surfaces lumineuses. Les minerais peuvent aussi devenir émissifs. Nécessite Vanilla Light. Désactivé par défaut.",
|
||||
"sanctuary.shaders.bloom_intensity": "Intensité du bloom",
|
||||
"sanctuary.shaders.bloom_intensity_help": "Force du halo. Zéro désactive le bloom et libère ses ressources.",
|
||||
"sanctuary.shaders.bloom_intensity_value": "Bloom : %s %%",
|
||||
"sanctuary.shaders.bloom_radius": "Diffusion du bloom",
|
||||
"sanctuary.shaders.bloom_radius_help": "Taille du halo autour des pixels lumineux. Les détails des textures restent nets.",
|
||||
"sanctuary.shaders.bloom_radius_value": "Diffusion : %s %%",
|
||||
"sanctuary.shaders.ore_emission": "Minerais émissifs",
|
||||
"sanctuary.shaders.ore_emission_help": "Avec le bloom : fait briller les inclusions des minerais sans éclairer leur roche. Effet visuel uniquement.",
|
||||
"sanctuary.shaders.ore_intensity": "Émission des minerais",
|
||||
"sanctuary.shaders.ore_intensity_help": "Luminosité des inclusions. Aucun changement de la lumière du monde ou des apparitions de mobs.",
|
||||
"sanctuary.shaders.ore_intensity_value": "Minerais : %s %%"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
#version 330
|
||||
#extension GL_ARB_separate_shader_objects : require
|
||||
#include <sanctuary:bloom_scene.glsl>
|
||||
uniform sampler2D InputTexture;
|
||||
layout(location=0) in vec2 texCoord;
|
||||
layout(location=0) out vec4 fragColor;
|
||||
void main(){vec3 halo=texture(InputTexture,texCoord).rgb;fragColor=vec4(clamp(halo*BloomParams.z,0.0,1.0),0.0);}
|
||||
@@ -0,0 +1,13 @@
|
||||
#version 330
|
||||
#extension GL_ARB_separate_shader_objects : require
|
||||
uniform sampler2D InputTexture;
|
||||
layout(std140) uniform BlurParams { vec4 Step; };
|
||||
layout(location=0) in vec2 texCoord;
|
||||
layout(location=0) out vec4 fragColor;
|
||||
// At most one low-resolution texel between taps: broad halos cannot turn into a sparse grid.
|
||||
void main(){
|
||||
float weights[13]=float[13](0.099908358,0.096834501,0.088168817,0.075414785,0.060597482,0.045741379,0.032435495,0.021606698,0.013521126,0.007948660,0.004389667,0.002277329,0.001109882);
|
||||
vec3 color=texture(InputTexture,texCoord).rgb*weights[0];
|
||||
for(int i=1;i<=12;i++){vec2 d=Step.xy*float(i);color+=(texture(InputTexture,texCoord+d).rgb+texture(InputTexture,texCoord-d).rgb)*weights[i];}
|
||||
fragColor=vec4(color,0.0);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
#version 330
|
||||
#extension GL_ARB_separate_shader_objects : require
|
||||
uniform sampler2D InputTexture;
|
||||
layout(location=0) in vec2 texCoord;
|
||||
layout(location=0) out vec4 fragColor;
|
||||
void main(){vec4 emission=texture(InputTexture,texCoord);fragColor=emission.a>0.0?emission:vec4(0.0);}
|
||||
@@ -0,0 +1,6 @@
|
||||
#version 330
|
||||
#extension GL_ARB_separate_shader_objects : require
|
||||
uniform sampler2D InputTexture;
|
||||
layout(location=0) in vec2 texCoord;
|
||||
layout(location=0) out vec4 fragColor;
|
||||
void main(){vec2 pixel=1.0/vec2(textureSize(InputTexture,0));vec3 sum=vec3(0.0);for(int y=0;y<8;y++)for(int x=0;x<8;x++)sum+=texture(InputTexture,texCoord+(vec2(x,y)-3.5)*pixel).rgb;fragColor=vec4(sum/64.0,0.0);}
|
||||
@@ -0,0 +1,22 @@
|
||||
#version 330
|
||||
#extension GL_ARB_separate_shader_objects : require
|
||||
#include <sanctuary:bloom_scene.glsl>
|
||||
uniform sampler2D BlockAtlas;
|
||||
uniform sampler2D EmissionMasks;
|
||||
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;
|
||||
void main(){
|
||||
vec2 uv=gl_FragCoord.xy/vec2(textureSize(WorldDepth,0));
|
||||
float depth=texture(WorldDepth,uv).r;
|
||||
if(depth>0.0&&length(relativePosition)>length(bloomPosition(uv,depth))+0.02)discard;
|
||||
vec4 source=texture(BlockAtlas,blockUv);
|
||||
float mask=texture(EmissionMasks,maskUv).r;
|
||||
bool ore=tint.a<0.5;
|
||||
float nativeLight=ore?tint.a*(255.0/120.0):max(0.0,(tint.a*255.0-128.0)/127.0);
|
||||
float amount=mask*(ore?max(BloomParams.x,nativeLight):nativeLight)*bloomFog(relativePosition);
|
||||
if(source.a<0.5||amount<0.001)discard;
|
||||
fragColor=vec4(source.rgb*tint.rgb*amount,ore?amount:0.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,15 @@
|
||||
#version 330
|
||||
#extension GL_ARB_separate_shader_objects : require
|
||||
#include <sanctuary:bloom_scene.glsl>
|
||||
uniform sampler2D InputTexture;
|
||||
layout(location=0) in vec2 texCoord;
|
||||
layout(location=0) out vec4 fragColor;
|
||||
void main(){
|
||||
fragColor=vec4(0.0);
|
||||
if(texture(WorldDepth,texCoord).r>0.0)return;
|
||||
vec3 ray=normalize(bloomPosition(texCoord,0.00001));
|
||||
float celestial=max(step(0.955,dot(ray,SunDirection.xyz))*SunDirection.w,step(0.975,dot(ray,MoonDirection.xyz))*MoonDirection.w);
|
||||
vec3 color=texture(InputTexture,texCoord).rgb;
|
||||
float luminance=dot(color,vec3(.2126,.7152,.0722));
|
||||
fragColor=vec4(color*smoothstep(.72,.98,luminance)*celestial,0.0);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
uniform sampler2D WorldDepth;
|
||||
layout(std140) uniform BloomScene {
|
||||
mat4 InverseViewProjection;
|
||||
vec4 BloomParams;
|
||||
vec4 SunDirection;
|
||||
vec4 MoonDirection;
|
||||
vec4 FogDistance;
|
||||
vec4 FogInfo;
|
||||
};
|
||||
vec3 bloomPosition(vec2 uv,float depth) {
|
||||
vec4 p=InverseViewProjection*vec4(uv*2.0-1.0,BloomParams.y>0.5?depth:depth*2.0-1.0,1.0);
|
||||
return p.xyz/p.w;
|
||||
}
|
||||
float bloomFog(vec3 position) {
|
||||
float a=clamp((max(length(position.xz),abs(position.y))-FogDistance.x)/max(.001,FogDistance.y-FogDistance.x),0.0,1.0);
|
||||
float b=clamp((length(position)-FogDistance.z)/max(.001,FogDistance.w-FogDistance.z),0.0,1.0);
|
||||
return 1.0-max(a,b)*FogInfo.x;
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
name = "Sanctuary"
|
||||
author = "KOKA99CAB"
|
||||
version = "beta.129"
|
||||
version = "beta.130"
|
||||
pack-format = "packwiz:1.1.0"
|
||||
|
||||
[index]
|
||||
|
||||
Reference in New Issue
Block a user