Add connected edge highlights and stable shadow projection for beta.129
Build Sanctuary / build (push) Canceled after 0s
Build Sanctuary / build (push) Canceled after 0s
This commit is contained in:
@@ -20,14 +20,13 @@ La [vision complète](docs/vision.md) conserve les intentions ; le
|
|||||||
L’économie, les claims et les dimensions décrits dans la vision restent
|
L’économie, les claims et les dimensions décrits dans la vision restent
|
||||||
à implémenter.
|
à implémenter.
|
||||||
|
|
||||||
La version **beta.128** stabilise les ombres pixélisées, atténue le soleil au
|
La version **beta.129** ajoute les reflets CTM optionnels sur les contours du
|
||||||
zénith et ajoute les ombres légères de la lune et le feuillage irrégulier.
|
relief, avec une portée indépendante de 16 à 256 blocs. Les surfaces plates
|
||||||
**Options → Shaders → Sanctuary · Vanilla Light** : les réglages de finesse et
|
se raccordent entre tous les matériaux. Les ombres gagnent les portées
|
||||||
de portée sont conservés. Le [ticket de livraison](docs/shader-beta128.md)
|
128/256, gardent leur intensité à midi et ne réalignent plus leur grille à
|
||||||
décrit le résultat et ses vérifications. Les gemmes, sculptures et découvertes
|
chaque mouvement du soleil. **Options → Shaders → Sanctuary · Vanilla Light**.
|
||||||
de beta.126 restent inchangées. La release est publiée et l'instance
|
Le [ticket de livraison](docs/shader-beta129.md) précise le résultat et ses
|
||||||
**Sanctuary Beta** synchronisée, sauvegardes et réglages conservés. Packs
|
vérifications. Publication et synchronisation en cours de préparation.
|
||||||
normal/Test et template sont également disponibles dans `build/`.
|
|
||||||
La [livraison précédente](docs/clay-workshop-integration-beta110.md) réunit déjà
|
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**.
|
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.
|
Les [synchronisations antérieures](docs/git-sync-beta092.md) restent documentées.
|
||||||
@@ -38,6 +37,12 @@ La [carte native beta.005](docs/atlas-beta005.md) prend le relais après
|
|||||||
l’étude de Xaero et des alternatives libres.
|
l’étude de Xaero et des alternatives libres.
|
||||||
|
|
||||||
|
|
||||||
|
## beta.129 — Reflets CTM et ombres jusqu'à 256 blocs
|
||||||
|
|
||||||
|
Reflets connectés réglables, cinq portées indépendantes, soleil plein à midi
|
||||||
|
et correction du réalignement cyclique de la grille d'ombres.
|
||||||
|
[Contrat et vérifications](docs/shader-beta129.md).
|
||||||
|
|
||||||
## beta.128 — Ombres stables, lune et feuillage
|
## beta.128 — Ombres stables, lune et feuillage
|
||||||
|
|
||||||
Grille de réception stabilisée, contours nets des blocs, atténuation à midi,
|
Grille de réception stabilisée, contours nets des blocs, atténuation à midi,
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
# SHADER-129 — arêtes connectées et portées étendues
|
||||||
|
|
||||||
|
Demandes du 17 septembre 2026 : highlight optionnel, puis raccordement CTM de
|
||||||
|
tous les blocs coplanaires, portée indépendante 16–256 blocs, ombres à 128/256,
|
||||||
|
suppression de l'atténuation au zénith et du réalignement cyclique des ombres.
|
||||||
|
Branche `codex/edge-highlights-beta129`, socle beta.128, Minecraft 26.3 / Java 25.
|
||||||
|
|
||||||
|
## Résultat
|
||||||
|
|
||||||
|
Dans **Options → Shaders → Sanctuary · Vanilla Light**, les reflets sur les
|
||||||
|
arêtes sont désactivés par défaut. L'intensité va de 0 à 100 ; la valeur 30
|
||||||
|
éclaircit au maximum de 12 %. Leur portée se règle indépendamment des ombres :
|
||||||
|
**16, 32, 64, 128 ou 256 blocs**. Les anciens réglages restent conservés.
|
||||||
|
|
||||||
|
Le highlight raccorde les surfaces plates, y compris entre matériaux différents,
|
||||||
|
sans dessiner les jonctions intérieures. Il suit les contours visibles du relief,
|
||||||
|
y compris les dalles et escaliers. Il multiplie la couleur existante pour garder
|
||||||
|
les pixels des textures et composer avec les ombres. Ce CTM géométrique utilise
|
||||||
|
la profondeur déjà produite par Minecraft : aucune liste de blocs autorisés,
|
||||||
|
aucun pack CTM à fournir, aucune exploration CPU du volume de 256 blocs.
|
||||||
|
Une cassure/repose modifie le contour dès que le terrain natif est réaffiché.
|
||||||
|
|
||||||
|
Le trait proche suit la grille de 1/16 de bloc. À distance, la couverture devient
|
||||||
|
partielle pour limiter le scintillement des traits plus petits qu'un pixel.
|
||||||
|
La portée choisie s'efface progressivement dans son dernier quart et respecte
|
||||||
|
la brume ainsi que la distance de rendu. Une passe plein écran, quatre sondes
|
||||||
|
de profondeur et un tampon uniforme de 176 octets ; aucun retour GPU vers CPU
|
||||||
|
en production. L'arrêt de l'option ou une intensité nulle libèrent ce tampon.
|
||||||
|
|
||||||
|
Ce n'est pas un remplacement des textures par des variantes CTM. Le calcul
|
||||||
|
concerne les faces visibles alignées sur la grille : les faces hors écran,
|
||||||
|
les surfaces transparentes absentes de la profondeur et les géométries
|
||||||
|
arbitrairement inclinées ne sont pas reconstruites. À très grande distance,
|
||||||
|
les détails plus petits qu'un pixel peuvent rester imperceptibles. Aucun
|
||||||
|
chargement de chunk forcé, aucune promesse de FPS.
|
||||||
|
|
||||||
|
Les ombres proposent les cinq mêmes portées. La carte reste plafonnée à
|
||||||
|
4096 × 4096 : les grandes distances réduisent sa précision spatiale. Le cache
|
||||||
|
garde ses limites de 96 Mio, quatre sections et 3 ms souples de construction
|
||||||
|
par image ; la réconciliation des chunks est limitée aux ticks et changements
|
||||||
|
de caméra/terrain plutôt qu'à toutes les images.
|
||||||
|
|
||||||
|
L'intensité solaire reste pleine au zénith. Les contours opaques restent nets,
|
||||||
|
le filtrage du feuillage et les ombres légères de la lune sont conservés.
|
||||||
|
Le réalignement de la grille lumineuse a lieu au changement d'ancrage de la
|
||||||
|
caméra, avec transfert de phase par texels entiers. La rotation du soleil ne
|
||||||
|
réarrondit plus la projection de l'origine mondiale à chaque image, source
|
||||||
|
de sauts périodiques amplifiés aux grandes coordonnées. Le soleil continue
|
||||||
|
d'avancer ; une ombre pixélisée peut naturellement changer de case. Cette
|
||||||
|
correction ne modifie pas l'horloge serveur ni sa cadence native.
|
||||||
|
|
||||||
|
Textes FR/EN. Aucun changement de règles, sauvegardes, génération, gemmes,
|
||||||
|
sculptures ou découvertes.
|
||||||
|
|
||||||
|
## Vérifications et livraison
|
||||||
|
|
||||||
|
Le client natif réussit en **2 min 41 s**, dans un monde neuf plat, graine 122,
|
||||||
|
sur macOS / Apple M1 / OpenGL :
|
||||||
|
|
||||||
|
- CTM : 596 échantillons éclaircis sur le contour et 65 068 conservés ; les
|
||||||
|
20 centres et 20 jonctions coplanaires ciblés restent intacts, y compris
|
||||||
|
au changement de matériau. L'intensité 90 produit environ trois fois le gain.
|
||||||
|
- Ouverture d'un trou : 716 échantillons éclaircis ; la fermeture restitue
|
||||||
|
l'image connectée. Au-delà de 16 blocs, le réglage 16 supprime le contour ;
|
||||||
|
le réglage 64 le rétablit (254 échantillons éclaircis).
|
||||||
|
- Cinq portées CTM et cinq portées d'ombres, persistance/clamp, migration des
|
||||||
|
anciennes préférences, arrêt/libération, rechargement, caméra et vue oblique
|
||||||
|
avec ombres. Captures françaises/anglaises inspectées ; suppression ensuite
|
||||||
|
d'un préfixe redondant dans le libellé de portée.
|
||||||
|
- Soleil, lune, nouvelle lune, ombres opaques nettes à midi et transmission
|
||||||
|
du feuillage restent vérifiés. Casters hors écran et retrait/repose actifs.
|
||||||
|
- 2 000 petits pas solaires aux coordonnées ±100 000 vérifient l'absence de
|
||||||
|
saut de translation de la carte ; changement d'ancrage par texels entiers.
|
||||||
|
La séquence du vrai service realtime suit 294 images : angle avancé de
|
||||||
|
0,000694 radian, aucun rebond dans les 65 664 échantillons de sol suivis.
|
||||||
|
Sur cette courte séquence, aucune case opaque n'a changé ; cela ne constitue
|
||||||
|
pas une mesure de mouvement visible sur une journée entière.
|
||||||
|
|
||||||
|
La fixture initiale capturait parfois le terrain avant la fin de sa construction
|
||||||
|
native : elle attend désormais la réception de toute la plateforme et la fin
|
||||||
|
compilée des sections. Aucun changement de gameplay pour contourner ce problème.
|
||||||
|
|
||||||
|
Commande : `./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true
|
||||||
|
-PsanctuaryPixelShadows122ClientTests=true -PsanctuaryClientNoVsync=true
|
||||||
|
-PsanctuaryQuickTests=true`. Log `build/shader129-client.log`, captures sous
|
||||||
|
`mods/sanctuary/build/run/clientGameTest/screenshots/`.
|
||||||
|
|
||||||
|
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||||
|
réussit en **2 min 17 s**, 126 tâches. Une dernière passe d’assemblage intègre
|
||||||
|
la simplification du libellé de portée (2 min 18 s). Les archives sont vérifiées :
|
||||||
|
**17 entrées de production** diffèrent de beta.128, limitées au shader et à ses
|
||||||
|
interfaces. Même JAR dans normal/Test, sources archivées conformes, ressources
|
||||||
|
du template exactes, assets des gemmes inchangés. Aucun monde ni test embarqué.
|
||||||
|
Le serveur GameTest dédié
|
||||||
|
reste exclu conformément au refus antérieur de son EULA. Aucun monde personnel
|
||||||
|
utilisé pour les essais, aucun résultat Windows/Vulkan revendiqué.
|
||||||
|
|
||||||
|
|
||||||
|
Archives vérifiées dans `build/` et recopiées dans le dossier de livraison
|
||||||
|
habituel `sanctuary-beta/build/` :
|
||||||
|
|
||||||
|
- Normal : 10 353 946 octets, SHA-256
|
||||||
|
`2c7735194faab8a699004e69ad543ce6e8fabdebe297b759bffd31fea2dcc2e8`.
|
||||||
|
- Test : 10 372 872 octets, SHA-256
|
||||||
|
`cca572ee313d1d7e66ed745bb48bac62490998760d9b7bbfc7a1f6d60c430dce`.
|
||||||
|
- JAR Sanctuary :
|
||||||
|
`ca3e2ff4e0717a3a54c1d57478d43aade39d001b71c4eec745d76f010f7def54`.
|
||||||
|
|
||||||
|
Publication et synchronisation à consigner après leur exécution.
|
||||||
+2
-2
@@ -9,8 +9,8 @@ loom_version=1.17.20
|
|||||||
fabric_api_version=0.160.5+26.3
|
fabric_api_version=0.160.5+26.3
|
||||||
|
|
||||||
# Release counter: beta.001, .002, .003, ... (see docs/versioning.md).
|
# Release counter: beta.001, .002, .003, ... (see docs/versioning.md).
|
||||||
mod_version=beta.128
|
mod_version=beta.129
|
||||||
pack_version=beta.128
|
pack_version=beta.129
|
||||||
resource_pack_version=beta.121
|
resource_pack_version=beta.121
|
||||||
maven_group=fr.koka.sanctuary
|
maven_group=fr.koka.sanctuary
|
||||||
jei_version=30.32.0-sanctuary.3
|
jei_version=30.32.0-sanctuary.3
|
||||||
|
|||||||
+206
@@ -0,0 +1,206 @@
|
|||||||
|
package fr.koka.sanctuary.client.shader;
|
||||||
|
|
||||||
|
import static fr.koka.sanctuary.client.shader.PixelShadows122ClientChecks.*;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
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 org.joml.Matrix4f;
|
||||||
|
import org.joml.Vector4f;
|
||||||
|
|
||||||
|
/** Native GPU checks sharing the isolated flat-world fixture and its preference backup. */
|
||||||
|
final class EdgeHighlights129ClientChecks {
|
||||||
|
static void run(ClientGameTestContext c, TestServerContext server) {
|
||||||
|
c.runOnClient(m -> preferences());
|
||||||
|
server.runOnServer(s -> {
|
||||||
|
for (int x=10;x<=14;x++) for (int z=6;z<=10;z++) {
|
||||||
|
s.overworld().setBlockAndUpdate(new BlockPos(x,65,z),
|
||||||
|
(Math.floorMod(x,8)<4 ? Blocks.STONE : Blocks.OAK_PLANKS).defaultBlockState());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
camera(c,server,12.5,72,8.5,60);
|
||||||
|
c.waitTicks(35);
|
||||||
|
shadows(c,false);
|
||||||
|
edge(c,false);
|
||||||
|
var off=capture(c,"edges-flat-off");
|
||||||
|
c.waitTicks(6);
|
||||||
|
unchanged(off,capture(c,null),"Highlight OFF control is stable");
|
||||||
|
edge(c,true);
|
||||||
|
var on=capture(c,"edges-flat-on-30");
|
||||||
|
var low=measure(off,on);
|
||||||
|
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);
|
||||||
|
for (int distance : new int[]{16,32,64,128,256}) {
|
||||||
|
c.runOnClient(m -> VanillaLight.setEdgeDistance(distance));
|
||||||
|
frames(c);
|
||||||
|
check(c.computeOnClient(m -> VanillaLight.edgeDistance())==distance,"All CTM ranges are available");
|
||||||
|
unchanged(on,capture(c,null),"Near CTM contours unchanged at range "+distance);
|
||||||
|
}
|
||||||
|
c.runOnClient(m -> VanillaLight.setEdgeDistance(32));
|
||||||
|
c.runOnClient(m -> {
|
||||||
|
check(PixelShadows.shadowMap()==null,"Highlight works without allocating a cast-shadow map");
|
||||||
|
VanillaLight.setEdgeIntensity(90);
|
||||||
|
});
|
||||||
|
frames(c);
|
||||||
|
var high=measure(off,capture(c,"edges-flat-on-90"));
|
||||||
|
check(high.meanGain>low.meanGain*1.8,"Intensity changes visible brightness: low="+low+" high="+high);
|
||||||
|
c.runOnClient(m -> VanillaLight.setEdgeIntensity(0));
|
||||||
|
c.waitTicks(6);
|
||||||
|
c.runOnClient(m -> check(!EdgeHighlights.applied() && !EdgeHighlights.allocated(),"Zero intensity frees its uniform buffer"));
|
||||||
|
unchanged(off,capture(c,"edges-zero"),"Zero intensity reproduces OFF exactly");
|
||||||
|
c.runOnClient(m -> VanillaLight.setEdgeIntensity(30));
|
||||||
|
frames(c);
|
||||||
|
camera(c,server,11.99999,72,8.5,60);
|
||||||
|
var anchor=capture(c,null);
|
||||||
|
camera(c,server,12.00001,72,8.5,60);
|
||||||
|
unchanged(anchor,capture(c,"edges-anchor-crossing"),"Grid remains stable across an integer camera anchor");
|
||||||
|
camera(c,server,12.5,72,8.5,60);
|
||||||
|
unchanged(on,capture(c,null),"Returning to the camera restores the same grid");
|
||||||
|
var reload=c.computeOnClient(net.minecraft.client.Minecraft::reloadResourcePacks);
|
||||||
|
c.waitFor(m -> reload.isDone() && m.gui.overlay()==null,1200);
|
||||||
|
reload.join();
|
||||||
|
c.runOnClient(m -> {
|
||||||
|
forgetPreferences();
|
||||||
|
check(VanillaLight.edgeHighlights() && VanillaLight.edgeIntensity()==30,"Highlight preferences survive reload and disk reread");
|
||||||
|
});
|
||||||
|
frames(c);
|
||||||
|
unchanged(on,capture(c,"edges-after-reload"),"Resource reload retains the highlight image");
|
||||||
|
edge(c,false);
|
||||||
|
unchanged(off,capture(c,"edges-off-restored"),"Disabling leaves no residual highlight");
|
||||||
|
|
||||||
|
// A new hole creates a connected boundary immediately; filling it removes that boundary.
|
||||||
|
server.runOnServer(s -> s.overworld().setBlockAndUpdate(new BlockPos(12,65,8),Blocks.AIR.defaultBlockState()));
|
||||||
|
c.waitTicks(20);
|
||||||
|
var holeOff=capture(c,null);
|
||||||
|
edge(c,true);
|
||||||
|
var hole=measure(holeOff,capture(c,"edges-ctm-hole"));
|
||||||
|
check(hole.brighter>low.brighter+20,"Removing an interior block creates a new CTM contour");
|
||||||
|
server.runOnServer(s -> s.overworld().setBlockAndUpdate(new BlockPos(12,65,8),Blocks.STONE.defaultBlockState()));
|
||||||
|
c.waitTicks(20);
|
||||||
|
unchanged(on,capture(c,null),"Filling the hole reconnects the flat surface");
|
||||||
|
// The actual radius gates a visible contour, independently of shadow range.
|
||||||
|
camera(c,server,12.5,88,8.5,30);
|
||||||
|
edge(c,false);
|
||||||
|
var farOff=capture(c,null);
|
||||||
|
c.runOnClient(m -> VanillaLight.setEdgeDistance(16));
|
||||||
|
edge(c,true);
|
||||||
|
unchanged(farOff,capture(c,"edges-beyond-16"),"CTM beyond selected 16-block radius is absent");
|
||||||
|
c.runOnClient(m -> {VanillaLight.setEdgeDistance(64);VanillaLight.setEdgeIntensity(90);});
|
||||||
|
frames(c);
|
||||||
|
var far=measure(farOff,capture(c,"edges-inside-64"));
|
||||||
|
check(far.brighter>40,"Extending the CTM radius restores a distant contour: "+far);
|
||||||
|
c.runOnClient(m -> {VanillaLight.setEdgeDistance(32);VanillaLight.setEdgeIntensity(30);});
|
||||||
|
edge(c,false);
|
||||||
|
|
||||||
|
server.runOnServer(s -> {
|
||||||
|
for (int x=9;x<=13;x+=2) for(int y=65;y<=67;y++)
|
||||||
|
s.overworld().setBlockAndUpdate(new BlockPos(x,y,12),
|
||||||
|
(x==9 ? Blocks.CONCRETE.red() : x==11 ? Blocks.STONE_BRICKS : Blocks.OAK_PLANKS).defaultBlockState());
|
||||||
|
s.overworld().setBlockAndUpdate(new BlockPos(10,65,11),Blocks.STONE_SLAB.defaultBlockState());
|
||||||
|
s.overworld().setBlockAndUpdate(new BlockPos(12,65,11),Blocks.OAK_STAIRS.defaultBlockState());
|
||||||
|
});
|
||||||
|
camera(c,server,12.5,72,7.5,70);
|
||||||
|
look(c,server,15,55);
|
||||||
|
shadows(c,true);
|
||||||
|
var reliefOff=capture(c,"edges-relief-with-shadows-off");
|
||||||
|
edge(c,true);
|
||||||
|
var reliefOn=capture(c,"edges-relief-with-shadows-on");
|
||||||
|
var relief=measure(reliefOff,reliefOn);
|
||||||
|
check(relief.brighter>100 && relief.stable>relief.samples*.5 && relief.darker<relief.samples*.01,
|
||||||
|
"Highlight composes with cast shadows and coloured textured blocks: "+relief);
|
||||||
|
look(c,server,15.00002f,55.00002f);
|
||||||
|
unchanged(reliefOn,capture(c,null),"Tiny oblique camera rotation keeps highlights stable");
|
||||||
|
c.runOnClient(m -> VanillaLight.setEnabled(false));
|
||||||
|
c.waitTicks(6);
|
||||||
|
c.runOnClient(m -> check(!EdgeHighlights.applied() && !EdgeHighlights.allocated(),"Main shader OFF also releases highlight resources"));
|
||||||
|
c.runOnClient(m -> VanillaLight.setEnabled(true));
|
||||||
|
frames(c);
|
||||||
|
edge(c,false);
|
||||||
|
System.out.println("EDGE_HIGHLIGHTS129_PASS connected contours and unchanged flat joins; independent shadows; intensity; zero/OFF release; "
|
||||||
|
+"camera anchors; oblique textured scene; resource reload; legacy preference migration; low="+low+" high="+high+" relief="+relief);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void preferences() {
|
||||||
|
var path=FabricLoader.getInstance().getConfigDir().resolve("sanctuary-shaders.json");
|
||||||
|
try {
|
||||||
|
// Real prior-version JSON, without either new field. Preserve its existing settings.
|
||||||
|
Files.writeString(path,"{\"enabled\":true,\"saturation\":100,\"pixelShadows\":true,\"shadowPixels\":16,\"shadowDistance\":32}");
|
||||||
|
forgetPreferences();
|
||||||
|
check(!VanillaLight.edgeHighlights() && VanillaLight.edgeIntensity()==30 && VanillaLight.edgeDistance()==32,"Old configuration defaults highlights OFF at intensity 30");
|
||||||
|
check(VanillaLight.pixelShadows() && VanillaLight.saturation()==100,"Migration retains existing shader choices");
|
||||||
|
for(int value:new int[]{-100,0,30,100,1000}) {
|
||||||
|
VanillaLight.setEdgeIntensity(value);forgetPreferences();
|
||||||
|
check(VanillaLight.edgeIntensity()==Math.clamp(value,0,100),"Saved intensity is bounded");
|
||||||
|
}
|
||||||
|
for(int[] pair:new int[][]{{-1,16},{24,32},{48,64},{96,128},{192,256},{9999,256}}) {
|
||||||
|
VanillaLight.setEdgeDistance(pair[0]);forgetPreferences();
|
||||||
|
check(VanillaLight.edgeDistance()==pair[1],"Saved CTM distance is bounded and normalized");
|
||||||
|
}
|
||||||
|
VanillaLight.setEdgeDistance(32);
|
||||||
|
VanillaLight.setEdgeIntensity(30);
|
||||||
|
VanillaLight.setEdgeHighlights(true);forgetPreferences();
|
||||||
|
check(VanillaLight.edgeHighlights(),"Toggle persists independently");
|
||||||
|
VanillaLight.setEdgeHighlights(false);
|
||||||
|
} catch(java.io.IOException e) { throw new AssertionError(e); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void edge(ClientGameTestContext c,boolean enabled) {
|
||||||
|
c.runOnClient(m -> VanillaLight.setEdgeHighlights(enabled));
|
||||||
|
if(enabled)frames(c);
|
||||||
|
else {
|
||||||
|
c.waitTicks(6);
|
||||||
|
c.runOnClient(m -> check(!EdgeHighlights.applied() && !EdgeHighlights.allocated(),"OFF frees resources"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void frames(ClientGameTestContext c) {
|
||||||
|
int before=c.computeOnClient(m -> EdgeHighlights.renderedFrames());
|
||||||
|
c.waitFor(m -> EdgeHighlights.applied() && EdgeHighlights.renderedFrames()>before+8,200);
|
||||||
|
c.waitTicks(6);
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Difference(int samples,int brighter,int stable,int darker,double meanGain) { }
|
||||||
|
private static Difference measure(Frame off,Frame on) {
|
||||||
|
int samples=0,brighter=0,stable=0,darker=0;double gain=0;
|
||||||
|
for(int y=off.height()/10;y<off.height()*9/10;y+=2) for(int x=off.width()/10;x<off.width()*9/10;x+=2) {
|
||||||
|
int i=y*off.width()+x,a=off.pixels()[i],b=on.pixels()[i];
|
||||||
|
int delta=brightness(b)-brightness(a);samples++;gain+=delta;
|
||||||
|
if(delta>=4)brighter++;
|
||||||
|
if(delta<=-4)darker++;
|
||||||
|
if(channelDifference(a,b)<=2)stable++;
|
||||||
|
}
|
||||||
|
var result=new Difference(samples,brighter,stable,darker,gain/samples);
|
||||||
|
System.out.println("EDGE_HIGHLIGHTS129_PIXELS "+result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void grid(ClientGameTestContext c,Frame off,Frame on) {
|
||||||
|
var samples=c.computeOnClient(m -> {
|
||||||
|
var camera=m.gameRenderer.gameRenderState().levelRenderState.cameraRenderState;
|
||||||
|
var vp=new Matrix4f(camera.projectionMatrix).mul(camera.viewRotationMatrix);
|
||||||
|
var result=new ArrayList<int[]>();
|
||||||
|
for(int x=11;x<=14;x++) for(int z=6;z<=10;z++) for(int edge=0;edge<2;edge++) {
|
||||||
|
var p=vp.transform(new Vector4f((float)(x+(edge==0?.5:1.0/32)-camera.pos.x),
|
||||||
|
(float)(66-camera.pos.y),(float)(z+.5-camera.pos.z),1));
|
||||||
|
int px=(int)((p.x/p.w*.5+.5)*off.width());
|
||||||
|
int py=(int)((.5-p.y/p.w*.5)*off.height());
|
||||||
|
if(px>=10 && px<off.width()-10 && py>=10 && py<off.height()-10)result.add(new int[]{py*off.width()+px,edge});
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
int centers=0,unchanged=0,edges=0,bright=0;
|
||||||
|
for(var sample:samples) {
|
||||||
|
int a=off.pixels()[sample[0]],b=on.pixels()[sample[0]];
|
||||||
|
if(sample[1]==0){centers++;if(channelDifference(a,b)<=2)unchanged++;}
|
||||||
|
else {edges++;if(brightness(b)-brightness(a)>=4)bright++;}
|
||||||
|
}
|
||||||
|
check(centers>=15 && unchanged>centers*.95 && edges>=15 && bright==0,
|
||||||
|
"Connected block centers and coplanar joins remain untouched: centers="+unchanged+"/"+centers+" edges="+bright+"/"+edges);
|
||||||
|
System.out.println("EDGE_HIGHLIGHTS129_GRID centers="+unchanged+"/"+centers+" edges="+bright+"/"+edges);
|
||||||
|
}
|
||||||
|
}
|
||||||
+29
-21
@@ -121,6 +121,11 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
|||||||
c.waitFor(m -> m.level.getBlockState(new BlockPos(0, FLOOR + 8, 0)).is(Blocks.CONCRETE.red())
|
c.waitFor(m -> m.level.getBlockState(new BlockPos(0, FLOOR + 8, 0)).is(Blocks.CONCRETE.red())
|
||||||
&& m.level.getBlockState(SLAB).is(Blocks.RED_SANDSTONE_SLAB)
|
&& m.level.getBlockState(SLAB).is(Blocks.RED_SANDSTONE_SLAB)
|
||||||
&& m.level.getBlockState(STAIR).is(Blocks.RED_SANDSTONE_STAIRS), 200);
|
&& m.level.getBlockState(STAIR).is(Blocks.RED_SANDSTONE_STAIRS), 200);
|
||||||
|
c.waitFor(m -> {
|
||||||
|
for(int x=-24;x<=24;x+=8) for(int z=-24;z<=24;z+=8)
|
||||||
|
if(!m.level.getBlockState(new BlockPos(x,FLOOR,z)).is(Blocks.CONCRETE.white()))return false;
|
||||||
|
return m.levelRenderer.hasRenderedAllSections();
|
||||||
|
},1200);
|
||||||
c.waitTicks(40);
|
c.waitTicks(40);
|
||||||
c.runOnClient(m -> check(VanillaLight.active(), "Native Sanctuary shader is active without an external engine"));
|
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);
|
float morning = c.computeOnClient(m -> m.gameRenderer.gameRenderState().levelRenderState.skyRenderState.sunAngle);
|
||||||
@@ -147,7 +152,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
|||||||
"Live pixel density reallocates the correct depth map"));
|
"Live pixel density reallocates the correct depth map"));
|
||||||
capture(c, "precision-" + pixels);
|
capture(c, "precision-" + pixels);
|
||||||
}
|
}
|
||||||
for (int distance : new int[]{16, 64, 32}) {
|
for (int distance : new int[]{16, 64, 128, 256, 32}) {
|
||||||
c.runOnClient(m -> VanillaLight.setShadowDistance(distance));
|
c.runOnClient(m -> VanillaLight.setShadowDistance(distance));
|
||||||
activeFrames(c);
|
activeFrames(c);
|
||||||
c.runOnClient(m -> check(PixelShadows.shadowMap().width == PixelShadows.mapSize(distance, 16),
|
c.runOnClient(m -> check(PixelShadows.shadowMap().width == PixelShadows.mapSize(distance, 16),
|
||||||
@@ -229,12 +234,14 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
|||||||
});
|
});
|
||||||
activeFrames(c);
|
activeFrames(c);
|
||||||
unchanged(beforeReload, capture(c, "after-reload"), "Resource reload preserves the rendered ground shadow");
|
unchanged(beforeReload, capture(c, "after-reload"), "Resource reload preserves the rendered ground shadow");
|
||||||
|
RealtimeShadows129ClientChecks.run(c, server);
|
||||||
|
EdgeHighlights129ClientChecks.run(c, server);
|
||||||
c.getInput().pressKey(InputConstants.KEY_F1);
|
c.getInput().pressKey(InputConstants.KEY_F1);
|
||||||
for (String locale : new String[]{"fr_fr", "en_us"}) {
|
for (String locale : new String[]{"fr_fr", "en_us"}) {
|
||||||
c.runOnClient(m -> {
|
c.runOnClient(m -> {
|
||||||
m.getLanguageManager().setSelected(locale);
|
m.getLanguageManager().setSelected(locale);
|
||||||
m.getLanguageManager().onResourceManagerReload(m.getResourceManager());
|
m.getLanguageManager().onResourceManagerReload(m.getResourceManager());
|
||||||
for (String key : new String[]{"pixel_shadows", "shadow_pixels", "shadow_distance"}) {
|
for (String key : new String[]{"pixel_shadows", "shadow_pixels", "shadow_distance", "edge_highlights", "edge_intensity", "edge_distance"}) {
|
||||||
String fullKey = "sanctuary.shaders." + key;
|
String fullKey = "sanctuary.shaders." + key;
|
||||||
check(!Component.translatable(fullKey).getString().equals(fullKey), "Translated " + locale + " " + key);
|
check(!Component.translatable(fullKey).getString().equals(fullKey), "Translated " + locale + " " + key);
|
||||||
}
|
}
|
||||||
@@ -246,12 +253,12 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
c.waitTicks(3);
|
c.waitTicks(3);
|
||||||
c.takeScreenshot("sanctuary-beta128-shadows-options-" + locale);
|
c.takeScreenshot("sanctuary-beta129-shadows-options-" + locale);
|
||||||
}
|
}
|
||||||
c.runOnClient(m -> m.gui.setScreen(null));
|
c.runOnClient(m -> m.gui.setScreen(null));
|
||||||
}
|
}
|
||||||
System.out.println("PIXEL_SHADOWS122_CLIENT_PASS: bounded saved preferences; native GPU ON/OFF ground; "
|
System.out.println("PIXEL_SHADOWS122_CLIENT_PASS: bounded saved preferences; native GPU ON/OFF ground; "
|
||||||
+ "offscreen caster; live removal/replacement; subtle moon; noon attenuation; foliage density; camera stability; resource reload; FR/EN screenshots");
|
+ "offscreen caster; live removal/replacement; subtle moon; full-strength noon; foliage density; camera stability; resource reload; FR/EN screenshots");
|
||||||
} finally {
|
} finally {
|
||||||
onboarding.set(false);
|
onboarding.set(false);
|
||||||
c.runOnClient(m -> {
|
c.runOnClient(m -> {
|
||||||
@@ -287,8 +294,8 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
|||||||
var noonOn = capture(c, "noon-on");
|
var noonOn = capture(c, "noon-on");
|
||||||
var noonStats = opacity(noonOff, noonOn);
|
var noonStats = opacity(noonOff, noonOn);
|
||||||
crispEdge(noonOff, noonOn);
|
crispEdge(noonOff, noonOn);
|
||||||
check(noonStats.dark > 100 && noonStats.maximum > .04 && noonStats.maximum < .14,
|
check(noonStats.dark > 100 && noonStats.maximum > .34 && noonStats.maximum < .42,
|
||||||
"Noon shadow is present but attenuated: " + noonStats);
|
"Noon shadow keeps its full daytime strength: " + noonStats);
|
||||||
camera(c, server, 1.00001, FLOOR + 5, 1, 70);
|
camera(c, server, 1.00001, FLOOR + 5, 1, 70);
|
||||||
unchanged(noonOn, capture(c, "noon-camera-subpixel"), "Noon tiny motion across former integer anchor");
|
unchanged(noonOn, capture(c, "noon-camera-subpixel"), "Noon tiny motion across former integer anchor");
|
||||||
camera(c, server, .99999, FLOOR + 5, 1, 70);
|
camera(c, server, .99999, FLOOR + 5, 1, 70);
|
||||||
@@ -379,7 +386,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
|||||||
if (!whiteGround(off.pixels[i])) continue;
|
if (!whiteGround(off.pixels[i])) continue;
|
||||||
samples++;
|
samples++;
|
||||||
double ratio=(brightness(off.pixels[i])-brightness(on.pixels[i]))/(double)brightness(off.pixels[i]);
|
double ratio=(brightness(off.pixels[i])-brightness(on.pixels[i]))/(double)brightness(off.pixels[i]);
|
||||||
if (ratio > .015 && ratio < .070) intermediate++;
|
if (ratio > .015 && ratio < .32) intermediate++;
|
||||||
}
|
}
|
||||||
check(samples > 1000 && intermediate < samples / 200,
|
check(samples > 1000 && intermediate < samples / 200,
|
||||||
"Opaque noon edge stays crisp, no foliage smoothing: " + intermediate + "/" + samples);
|
"Opaque noon edge stays crisp, no foliage smoothing: " + intermediate + "/" + samples);
|
||||||
@@ -412,13 +419,14 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
|||||||
|
|
||||||
private static void checkPreferences() {
|
private static void checkPreferences() {
|
||||||
VanillaLight.setEnabled(true);
|
VanillaLight.setEnabled(true);
|
||||||
|
VanillaLight.setEdgeHighlights(false);
|
||||||
VanillaLight.setSaturation(100);
|
VanillaLight.setSaturation(100);
|
||||||
for (int[] pair : new int[][]{{Integer.MIN_VALUE, 8}, {11, 8}, {12, 16}, {16, 16}, {23, 16}, {24, 32}, {Integer.MAX_VALUE, 32}}) {
|
for (int[] pair : new int[][]{{Integer.MIN_VALUE, 8}, {11, 8}, {12, 16}, {16, 16}, {23, 16}, {24, 32}, {Integer.MAX_VALUE, 32}}) {
|
||||||
VanillaLight.setShadowPixels(pair[0]);
|
VanillaLight.setShadowPixels(pair[0]);
|
||||||
forgetPreferences();
|
forgetPreferences();
|
||||||
check(VanillaLight.shadowPixels() == pair[1], "Persisted pixel density rounds/clamps " + pair[0]);
|
check(VanillaLight.shadowPixels() == pair[1], "Persisted pixel density rounds/clamps " + pair[0]);
|
||||||
}
|
}
|
||||||
for (int[] pair : new int[][]{{Integer.MIN_VALUE, 16}, {23, 16}, {24, 32}, {32, 32}, {47, 32}, {48, 64}, {Integer.MAX_VALUE, 64}}) {
|
for (int[] pair : new int[][]{{Integer.MIN_VALUE, 16}, {23, 16}, {24, 32}, {32, 32}, {47, 32}, {48, 64}, {95,64}, {96,128}, {191,128}, {192,256}, {256,256}, {Integer.MAX_VALUE, 256}}) {
|
||||||
VanillaLight.setShadowDistance(pair[0]);
|
VanillaLight.setShadowDistance(pair[0]);
|
||||||
forgetPreferences();
|
forgetPreferences();
|
||||||
check(VanillaLight.shadowDistance() == pair[1], "Persisted shadow distance rounds/clamps " + pair[0]);
|
check(VanillaLight.shadowDistance() == pair[1], "Persisted shadow distance rounds/clamps " + pair[0]);
|
||||||
@@ -432,7 +440,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
|||||||
VanillaLight.setShadowDistance(32);
|
VanillaLight.setShadowDistance(32);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void forgetPreferences() {
|
static void forgetPreferences() {
|
||||||
try {
|
try {
|
||||||
var field = VanillaLight.class.getDeclaredField("preferences");
|
var field = VanillaLight.class.getDeclaredField("preferences");
|
||||||
field.setAccessible(true);
|
field.setAccessible(true);
|
||||||
@@ -442,7 +450,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void camera(ClientGameTestContext c, TestServerContext server, double x, double y, double z, int fov) {
|
static void camera(ClientGameTestContext c, TestServerContext server, double x, double y, double z, int fov) {
|
||||||
server.runOnServer(s -> {
|
server.runOnServer(s -> {
|
||||||
var p = s.getPlayerList().getPlayers().getFirst();
|
var p = s.getPlayerList().getPlayers().getFirst();
|
||||||
command(s, "tp @a " + x + " " + y + " " + z + " 0 90");
|
command(s, "tp @a " + x + " " + y + " " + z + " 0 90");
|
||||||
@@ -461,7 +469,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
|||||||
c.waitTicks(15);
|
c.waitTicks(15);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void look(ClientGameTestContext c, TestServerContext server, float yaw, float pitch) {
|
static void look(ClientGameTestContext c, TestServerContext server, float yaw, float pitch) {
|
||||||
server.runOnServer(s -> {
|
server.runOnServer(s -> {
|
||||||
var p = s.getPlayerList().getPlayers().getFirst();
|
var p = s.getPlayerList().getPlayers().getFirst();
|
||||||
command(s, "tp @a " + p.getX() + " " + p.getY() + " " + p.getZ() + " " + yaw + " " + pitch);
|
command(s, "tp @a " + p.getX() + " " + p.getY() + " " + p.getZ() + " " + yaw + " " + pitch);
|
||||||
@@ -478,7 +486,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void command(MinecraftServer s, String command) {
|
static void command(MinecraftServer s, String command) {
|
||||||
try {
|
try {
|
||||||
// Dispatcher exceptions fail the fixture instead of leaving an ignored command error in chat.
|
// Dispatcher exceptions fail the fixture instead of leaving an ignored command error in chat.
|
||||||
s.getCommands().getDispatcher().execute(command, s.createCommandSourceStack());
|
s.getCommands().getDispatcher().execute(command, s.createCommandSourceStack());
|
||||||
@@ -487,7 +495,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void shadows(ClientGameTestContext c, boolean enabled) {
|
static void shadows(ClientGameTestContext c, boolean enabled) {
|
||||||
c.runOnClient(m -> VanillaLight.setPixelShadows(enabled));
|
c.runOnClient(m -> VanillaLight.setPixelShadows(enabled));
|
||||||
if (enabled) activeFrames(c);
|
if (enabled) activeFrames(c);
|
||||||
else {
|
else {
|
||||||
@@ -501,15 +509,15 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void activeFrames(ClientGameTestContext c) {
|
static void activeFrames(ClientGameTestContext c) {
|
||||||
int before = c.computeOnClient(m -> PixelShadows.renderedFrames());
|
int before = c.computeOnClient(m -> PixelShadows.renderedFrames());
|
||||||
c.waitFor(m -> PixelShadows.applied() && PixelShadows.ready() && PixelShadows.renderedFrames() > before + 12, 200);
|
c.waitFor(m -> PixelShadows.applied() && PixelShadows.ready() && PixelShadows.renderedFrames() > before + 12, 200);
|
||||||
// Allow the bounded terrain cache and native light/chunk uploads to settle before reading pixels.
|
// Allow the bounded terrain cache and native light/chunk uploads to settle before reading pixels.
|
||||||
c.waitTicks(30);
|
c.waitTicks(30);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Frame capture(ClientGameTestContext c, String name) {
|
static Frame capture(ClientGameTestContext c, String name) {
|
||||||
if (name != null) c.takeScreenshot("sanctuary-beta128-shadows-" + name);
|
if (name != null) c.takeScreenshot("sanctuary-beta129-shadows-" + name);
|
||||||
var result = new CompletableFuture<Frame>();
|
var result = new CompletableFuture<Frame>();
|
||||||
c.runOnClient(m -> Screenshot.takeScreenshot(m.gameRenderer.mainRenderTarget(), image -> {
|
c.runOnClient(m -> Screenshot.takeScreenshot(m.gameRenderer.mainRenderTarget(), image -> {
|
||||||
try (image) {
|
try (image) {
|
||||||
@@ -522,7 +530,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
|||||||
return result.join();
|
return result.join();
|
||||||
}
|
}
|
||||||
|
|
||||||
private record Frame(int width, int height, int[] pixels) { }
|
record Frame(int width, int height, int[] pixels) { }
|
||||||
private record Difference(int sampled, int ground, int dark, int stable, int brighter) { }
|
private record Difference(int sampled, int ground, int dark, int stable, int brighter) { }
|
||||||
|
|
||||||
private static Difference compare(Frame before, Frame after) {
|
private static Difference compare(Frame before, Frame after) {
|
||||||
@@ -552,7 +560,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
|||||||
check(d.brighter < Math.max(12, d.ground / 100), label + ": shadows must not brighten ground: " + d);
|
check(d.brighter < Math.max(12, d.ground / 100), label + ": shadows must not brighten ground: " + d);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void unchanged(Frame before, Frame after, String label) {
|
static void unchanged(Frame before, Frame after, String label) {
|
||||||
sameSize(before, after);
|
sameSize(before, after);
|
||||||
int sampled = 0, changed = 0;
|
int sampled = 0, changed = 0;
|
||||||
for (int y = before.height / 10; y < before.height * 9 / 10; y += 2) {
|
for (int y = before.height / 10; y < before.height * 9 / 10; y += 2) {
|
||||||
@@ -595,11 +603,11 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
|||||||
return Math.min(r, Math.min(g, b)) > 100 && Math.max(r, Math.max(g, b)) - Math.min(r, Math.min(g, b)) < 24;
|
return Math.min(r, Math.min(g, b)) > 100 && Math.max(r, Math.max(g, b)) - Math.min(r, Math.min(g, b)) < 24;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int brightness(int color) {
|
static int brightness(int color) {
|
||||||
return ((color >> 16 & 255) + (color >> 8 & 255) + (color & 255)) / 3;
|
return ((color >> 16 & 255) + (color >> 8 & 255) + (color & 255)) / 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int channelDifference(int a, int b) {
|
static int channelDifference(int a, int b) {
|
||||||
return Math.max(Math.abs((a >> 16 & 255) - (b >> 16 & 255)),
|
return Math.max(Math.abs((a >> 16 & 255) - (b >> 16 & 255)),
|
||||||
Math.max(Math.abs((a >> 8 & 255) - (b >> 8 & 255)), Math.abs((a & 255) - (b & 255))));
|
Math.max(Math.abs((a >> 8 & 255) - (b >> 8 & 255)), Math.abs((a & 255) - (b & 255))));
|
||||||
}
|
}
|
||||||
@@ -630,5 +638,5 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static KeyEvent enter() { return new KeyEvent(InputConstants.KEY_RETURN, 0, 0); }
|
private static KeyEvent enter() { return new KeyEvent(InputConstants.KEY_RETURN, 0, 0); }
|
||||||
private static void check(boolean ok, String message) { if (!ok) throw new AssertionError(message); }
|
static void check(boolean ok, String message) { if (!ok) throw new AssertionError(message); }
|
||||||
}
|
}
|
||||||
|
|||||||
+115
@@ -0,0 +1,115 @@
|
|||||||
|
package fr.koka.sanctuary.client.shader;
|
||||||
|
|
||||||
|
import static fr.koka.sanctuary.client.shader.PixelShadows122ClientChecks.*;
|
||||||
|
|
||||||
|
import fr.koka.sanctuary.realtime.RealtimeService;
|
||||||
|
import java.time.Clock;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||||
|
import net.fabricmc.fabric.api.client.gametest.v1.context.TestServerContext;
|
||||||
|
import net.fabricmc.fabric.api.client.rendering.v1.level.LevelRenderEvents;
|
||||||
|
|
||||||
|
final class RealtimeShadows129ClientChecks {
|
||||||
|
private static void projectionContinuity() {
|
||||||
|
// Far coordinates amplify the old per-frame rounding failure. Moving the sun must not
|
||||||
|
// add a periodic translation to the shadow raster while the camera remains stationary.
|
||||||
|
var grid=new ShadowProjection();
|
||||||
|
float last=0; int jumps=0; float greatest=0;
|
||||||
|
for(int i=0;i<2000;i++) {
|
||||||
|
var matrix=grid.matrix(PixelShadows.sunDirection(.7f+i*.000001f),32,2048,100000,64,-100000,false);
|
||||||
|
if(i>0){float step=Math.abs(matrix.m31()-last);greatest=Math.max(greatest,step);if(step>1e-6f)jumps++;}
|
||||||
|
last=matrix.m31();
|
||||||
|
}
|
||||||
|
check(jumps==0,"Slow light rotation has no cyclical raster translation far from spawn: "+greatest);
|
||||||
|
// Recentring transfers the phase modulo integral texels, preserving the sampled grid.
|
||||||
|
var sun=PixelShadows.sunDirection(.71f);
|
||||||
|
var before=grid.matrix(sun,32,2048,100000,64,-100000,false);
|
||||||
|
var after=grid.matrix(sun,32,2048,100016,64,-100000,false);
|
||||||
|
var a=before.transform(new org.joml.Vector4f(4,1,2,1));
|
||||||
|
var b=after.transform(new org.joml.Vector4f(-12,1,2,1));
|
||||||
|
for(float value:new float[]{(b.x-a.x)*1024,(b.y-a.y)*1024})
|
||||||
|
check(Math.abs(value-Math.rint(value))<.001,"Camera recentering moves only whole shadow texels");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void run(ClientGameTestContext c, TestServerContext server) {
|
||||||
|
projectionContinuity();
|
||||||
|
var enabled=new AtomicBoolean(false);
|
||||||
|
var trace=Collections.synchronizedList(new ArrayList<double[]>());
|
||||||
|
LevelRenderEvents.END_EXTRACTION.register(context -> {
|
||||||
|
if(enabled.get() && trace.size()<4096)trace.add(new double[]{System.nanoTime()/1e9,context.levelState().skyRenderState.sunAngle});
|
||||||
|
});
|
||||||
|
var original=new AtomicReference<Clock>();
|
||||||
|
try {
|
||||||
|
server.runOnServer(s -> {
|
||||||
|
var session=RealtimeService.session(s);
|
||||||
|
check(session!=null,"Isolated world has the actual real-time service");
|
||||||
|
try {
|
||||||
|
var field=session.getClass().getDeclaredField("clock");field.setAccessible(true);
|
||||||
|
original.set((Clock)field.get(session));
|
||||||
|
var morning=session.sample().day().noon().minusSeconds(3*3600);
|
||||||
|
field.set(session,Clock.offset(Clock.systemUTC(),Duration.between(Instant.now(),morning)));
|
||||||
|
check(RealtimeService.setMode(s,true),"Real-time mode enabled through actual server service");
|
||||||
|
} catch(ReflectiveOperationException e) {throw new AssertionError(e);}
|
||||||
|
catch(Exception e) {throw new AssertionError(e);}
|
||||||
|
});
|
||||||
|
c.waitFor(m -> PixelShadows.sunDirection(m.gameRenderer.gameRenderState().levelRenderState.skyRenderState.sunAngle).y>.2,200);
|
||||||
|
c.waitTicks(40);
|
||||||
|
double shadowX=c.computeOnClient(m -> {
|
||||||
|
var sun=PixelShadows.sunDirection(m.gameRenderer.gameRenderState().levelRenderState.skyRenderState.sunAngle);
|
||||||
|
return 1-7*sun.x/sun.y;
|
||||||
|
});
|
||||||
|
camera(c,server,shadowX,69,1,30);
|
||||||
|
shadows(c,false);
|
||||||
|
var off=capture(c,null);
|
||||||
|
shadows(c,true);
|
||||||
|
enabled.set(true);
|
||||||
|
var frames=new ArrayList<Frame>();
|
||||||
|
for(int i=0;i<24;i++) {c.waitTicks(6);frames.add(capture(c,i==0?"realtime-start":i==23?"realtime-end":null));}
|
||||||
|
c.runOnClient(m -> enabled.set(false));
|
||||||
|
double travel=Math.IEEEremainder(trace.getLast()[1]-trace.getFirst()[1],Math.PI*2);
|
||||||
|
int reverse=0;double largest=0;
|
||||||
|
for(int i=1;i<trace.size();i++) {
|
||||||
|
double delta=Math.IEEEremainder(trace.get(i)[1]-trace.get(i-1)[1],Math.PI*2)*Math.signum(travel);
|
||||||
|
if(delta<-.0000001){reverse++;largest=Math.max(largest,-delta);}
|
||||||
|
}
|
||||||
|
int samples=0,changed=0,rebound=0;
|
||||||
|
for(int y=off.height()/10;y<off.height()*9/10;y+=2) for(int x=off.width()/10;x<off.width()*9/10;x+=2) {
|
||||||
|
int index=y*off.width()+x,a=off.pixels()[index];
|
||||||
|
int r=a>>16&255,g=a>>8&255,b=a&255;
|
||||||
|
if(Math.min(r,Math.min(g,b))<100 || Math.max(r,Math.max(g,b))-Math.min(r,Math.min(g,b))>24)continue;
|
||||||
|
samples++;int flips=0;
|
||||||
|
boolean last=brightness(a)-brightness(frames.getFirst().pixels()[index])>=8;
|
||||||
|
for(var frame:frames) {
|
||||||
|
boolean dark=brightness(a)-brightness(frame.pixels()[index])>=8;
|
||||||
|
if(last!=dark)flips++;
|
||||||
|
last=dark;
|
||||||
|
}
|
||||||
|
if(flips>0)changed++;
|
||||||
|
if(flips>1)rebound++;
|
||||||
|
}
|
||||||
|
check(trace.size()>30 && Math.abs(travel)>.00001,"Real-time shadows actually remain dynamic");
|
||||||
|
check(rebound<Math.max(8,samples/1000),"Real-time opaque contours must not repeatedly rebound: "+rebound);
|
||||||
|
System.out.println("REALTIME129_PASS frames="+trace.size()+" rawTravel="+travel+" reversals="+reverse+" largestReverse="+largest
|
||||||
|
+" pixels="+samples+" changed="+changed+" rebound="+rebound);
|
||||||
|
try {
|
||||||
|
var out=new StringBuilder("seconds,raw_angle\n");
|
||||||
|
for(var sample:trace)out.append(sample[0]).append(',').append(sample[1]).append('\n');
|
||||||
|
java.nio.file.Files.writeString(net.fabricmc.loader.api.FabricLoader.getInstance().getGameDir().resolve("shadow129-clock.csv"),out);
|
||||||
|
}catch(java.io.IOException e){throw new AssertionError(e);}
|
||||||
|
} finally {
|
||||||
|
enabled.set(false);
|
||||||
|
server.runOnServer(s -> {
|
||||||
|
try {
|
||||||
|
RealtimeService.setMode(s,false);
|
||||||
|
if(original.get()!=null){var session=RealtimeService.session(s);var field=session.getClass().getDeclaredField("clock");field.setAccessible(true);field.set(session,original.get());}
|
||||||
|
command(s,"time set 3000");
|
||||||
|
}catch(Exception e){throw new AssertionError(e);}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ public final class SanctuaryKnowledgeClient implements ClientModInitializer {
|
|||||||
SanctuaryResourcePack.register();
|
SanctuaryResourcePack.register();
|
||||||
VortexCloudRenderer.register();
|
VortexCloudRenderer.register();
|
||||||
fr.koka.sanctuary.client.shader.PixelShadows.register();
|
fr.koka.sanctuary.client.shader.PixelShadows.register();
|
||||||
|
fr.koka.sanctuary.client.shader.EdgeHighlights.register();
|
||||||
ProgressionClient.register();
|
ProgressionClient.register();
|
||||||
StarClient.register();
|
StarClient.register();
|
||||||
WeatherClient.register();
|
WeatherClient.register();
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package fr.koka.sanctuary.client.shader;
|
||||||
|
|
||||||
|
import com.mojang.blaze3d.buffers.Std140Builder;
|
||||||
|
import com.mojang.blaze3d.systems.RenderSystem;
|
||||||
|
import com.mojang.renderpearl.api.GpuFormat;
|
||||||
|
import com.mojang.renderpearl.api.buffers.GpuBuffer;
|
||||||
|
import com.mojang.renderpearl.api.pipeline.*;
|
||||||
|
import com.mojang.renderpearl.api.textures.FilterMode;
|
||||||
|
import fr.koka.sanctuary.client.panorama.PanoramaCapture;
|
||||||
|
import java.util.Optional;
|
||||||
|
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
|
||||||
|
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents;
|
||||||
|
import net.minecraft.client.Minecraft;
|
||||||
|
import net.minecraft.client.renderer.MappableRingBuffer;
|
||||||
|
import net.minecraft.client.renderer.RenderPipelines;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.world.level.material.FogType;
|
||||||
|
import org.joml.Matrix4f;
|
||||||
|
|
||||||
|
/** Optional, texture-preserving highlights on visible voxel edges. No gameplay light changes. */
|
||||||
|
public final class EdgeHighlights {
|
||||||
|
// Destination * (1 + highlight): retain the block's existing colour and texture, including shadows.
|
||||||
|
private static final RenderPipeline APPLY = RenderPipelines.register(RenderPipeline.builder()
|
||||||
|
.withLocation(Identifier.parse("sanctuary:pipeline/edge_highlights"))
|
||||||
|
.withVertexShader("core/screenquad")
|
||||||
|
.withFragmentShader(Identifier.parse("sanctuary:core/edge_highlights"))
|
||||||
|
.withBindGroupLayout(BindGroupLayout.builder()
|
||||||
|
.withUniform("EdgeHighlights", UniformType.UNIFORM_BUFFER)
|
||||||
|
.withUniform("WorldDepth", UniformType.COMBINED_IMAGE_SAMPLER).build())
|
||||||
|
.withPrimitiveTopology(PrimitiveTopology.TRIANGLES).withCull(false)
|
||||||
|
.withColorTargetState(new ColorTargetState(Optional.of(new BlendFunction(
|
||||||
|
BlendFactor.DST_COLOR, BlendFactor.ONE, BlendFactor.ZERO, BlendFactor.ONE)),
|
||||||
|
GpuFormat.RGBA8_UNORM, ColorTargetState.WRITE_COLOR))
|
||||||
|
.withDepthStencilState(Optional.empty()).build());
|
||||||
|
private static final Matrix4f projection = new Matrix4f();
|
||||||
|
private static MappableRingBuffer uniforms;
|
||||||
|
private static boolean applied;
|
||||||
|
private static int renderedFrames;
|
||||||
|
|
||||||
|
private EdgeHighlights() { }
|
||||||
|
|
||||||
|
public static void register() {
|
||||||
|
ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> release());
|
||||||
|
ClientLifecycleEvents.CLIENT_STOPPING.register(client -> release());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void captureProjection(Matrix4f matrix) { projection.set(matrix); }
|
||||||
|
|
||||||
|
/** Runs after cast shadows and before the hand and HUD; works without the shadow depth map. */
|
||||||
|
public static void render() {
|
||||||
|
applied = false;
|
||||||
|
var client = Minecraft.getInstance();
|
||||||
|
if (!VanillaLight.active() || !VanillaLight.edgeHighlights() || VanillaLight.edgeIntensity() == 0
|
||||||
|
|| client.level == null) {
|
||||||
|
release();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (PanoramaCapture.rendering()) return;
|
||||||
|
var camera = client.gameRenderer.gameRenderState().levelRenderState.cameraRenderState;
|
||||||
|
if (camera.fogType != FogType.NONE) return;
|
||||||
|
if (uniforms == null) uniforms = new MappableRingBuffer(() -> "Sanctuary edge highlights",
|
||||||
|
GpuBuffer.USAGE_UNIFORM | GpuBuffer.USAGE_MAP_WRITE, 176);
|
||||||
|
var viewProjection = new Matrix4f(projection).mul(camera.viewRotationMatrix);
|
||||||
|
var inverse = new Matrix4f(viewProjection).invert();
|
||||||
|
var fog = camera.fogData;
|
||||||
|
uniforms.rotate();
|
||||||
|
try (var mapped = uniforms.currentBuffer().map(false, true)) {
|
||||||
|
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)
|
||||||
|
.putVec4(VanillaLight.edgeIntensity() * .004f,
|
||||||
|
RenderSystem.getDevice().getDeviceInfo().isZZeroToOne() ? 1 : 0,
|
||||||
|
fog.renderDistanceStart, fog.renderDistanceEnd)
|
||||||
|
.putVec4(fog.environmentalStart, fog.environmentalEnd, fog.color.w, VanillaLight.edgeDistance());
|
||||||
|
}
|
||||||
|
var main = client.gameRenderer.mainRenderTarget();
|
||||||
|
var encoder = RenderSystem.getDevice().createCommandEncoder();
|
||||||
|
try (var pass = encoder.createRenderPass(() -> "Sanctuary edge highlights",
|
||||||
|
main.getColorTextureView(), Optional.empty())) {
|
||||||
|
pass.setPipeline(RenderSystem.getCompiledPipeline(APPLY));
|
||||||
|
pass.setUniform("EdgeHighlights", uniforms.currentBuffer());
|
||||||
|
pass.setUniform("WorldDepth", main.getDepthTextureView(),
|
||||||
|
RenderSystem.getSamplerCache().getClampToEdge(FilterMode.NEAREST));
|
||||||
|
pass.draw(3, 1, 0, 0);
|
||||||
|
}
|
||||||
|
renderedFrames++;
|
||||||
|
applied = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void release() {
|
||||||
|
if (uniforms != null) { uniforms.close(); uniforms = null; }
|
||||||
|
applied = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean applied() { return applied; }
|
||||||
|
static boolean allocated() { return uniforms != null; }
|
||||||
|
static int renderedFrames() { return renderedFrames; }
|
||||||
|
}
|
||||||
@@ -51,6 +51,7 @@ public final class PixelShadows {
|
|||||||
.withDepthStencilState(Optional.empty()).build());
|
.withDepthStencilState(Optional.empty()).build());
|
||||||
private static final ShadowTerrain TERRAIN = new ShadowTerrain();
|
private static final ShadowTerrain TERRAIN = new ShadowTerrain();
|
||||||
private static final Matrix4f projection = new Matrix4f();
|
private static final Matrix4f projection = new Matrix4f();
|
||||||
|
private static final ShadowProjection lightGrid = new ShadowProjection();
|
||||||
private static TextureTarget shadowMap;
|
private static TextureTarget shadowMap;
|
||||||
private static MappableRingBuffer uniforms;
|
private static MappableRingBuffer uniforms;
|
||||||
private static int renderedFrames;
|
private static int renderedFrames;
|
||||||
@@ -86,7 +87,7 @@ public final class PixelShadows {
|
|||||||
if (lunar) sun = sunDirection(sky.moonAngle);
|
if (lunar) sun = sunDirection(sky.moonAngle);
|
||||||
if (sky.skybox != DimensionType.Skybox.OVERWORLD || camera.fogType != FogType.NONE || sun.y <= .08f) return;
|
if (sky.skybox != DimensionType.Skybox.OVERWORLD || camera.fogType != FogType.NONE || sun.y <= .08f) return;
|
||||||
float moon = sky.moonPhase == null ? 1 : (float)(.5 + .5 * Math.cos(sky.moonPhase.index() * Math.PI / 4));
|
float moon = sky.moonPhase == null ? 1 : (float)(.5 + .5 * Math.cos(sky.moonPhase.index() * Math.PI / 4));
|
||||||
float strength = (lunar ? .10f * moon : .38f * zenithFade(sun.y))
|
float strength = (lunar ? .10f * moon : .38f)
|
||||||
* Math.clamp((sun.y - .08f) / .16f, 0, 1) * sky.rainBrightness;
|
* Math.clamp((sun.y - .08f) / .16f, 0, 1) * sky.rainBrightness;
|
||||||
if (strength <= .001f) return;
|
if (strength <= .001f) return;
|
||||||
int distance = VanillaLight.shadowDistance(), pixels = VanillaLight.shadowPixels();
|
int distance = VanillaLight.shadowDistance(), pixels = VanillaLight.shadowPixels();
|
||||||
@@ -100,7 +101,7 @@ public final class PixelShadows {
|
|||||||
GpuBuffer.USAGE_UNIFORM | GpuBuffer.USAGE_MAP_WRITE, 192);
|
GpuBuffer.USAGE_UNIFORM | GpuBuffer.USAGE_MAP_WRITE, 192);
|
||||||
double ox = anchor(camera.pos.x), oy = anchor(camera.pos.y), oz = anchor(camera.pos.z);
|
double ox = anchor(camera.pos.x), oy = anchor(camera.pos.y), oz = anchor(camera.pos.z);
|
||||||
boolean zeroToOne = RenderSystem.getDevice().getDeviceInfo().isZZeroToOne();
|
boolean zeroToOne = RenderSystem.getDevice().getDeviceInfo().isZZeroToOne();
|
||||||
var light = lightMatrix(sun, distance, size, ox, oy, oz, zeroToOne);
|
var light = lightGrid.matrix(sun, distance, size, ox, oy, oz, zeroToOne);
|
||||||
var inverse = new Matrix4f(projection).mul(camera.viewRotationMatrix).invert();
|
var inverse = new Matrix4f(projection).mul(camera.viewRotationMatrix).invert();
|
||||||
uniforms.rotate();
|
uniforms.rotate();
|
||||||
var fog = camera.fogData;
|
var fog = camera.fogData;
|
||||||
@@ -152,12 +153,6 @@ public final class PixelShadows {
|
|||||||
applied = true;
|
applied = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Smoothly soften overhead sunlight, without a noon on/off discontinuity. */
|
|
||||||
static float zenithFade(float elevation) {
|
|
||||||
float t = Math.clamp((elevation - .92f) / .08f, 0, 1);
|
|
||||||
return 1 - .75f * t * t * (3 - 2 * t);
|
|
||||||
}
|
|
||||||
|
|
||||||
static double anchor(double coordinate) { return Math.floor(coordinate / 16.0 + .5) * 16.0; }
|
static double anchor(double coordinate) { return Math.floor(coordinate / 16.0 + .5) * 16.0; }
|
||||||
static Vector3f sunDirection(float angle) { return new Vector3f(-(float)Math.sin(angle), (float)Math.cos(angle), 0); }
|
static Vector3f sunDirection(float angle) { return new Vector3f(-(float)Math.sin(angle), (float)Math.cos(angle), 0); }
|
||||||
static int mapSize(int distance, int pixels) {
|
static int mapSize(int distance, int pixels) {
|
||||||
@@ -165,20 +160,9 @@ public final class PixelShadows {
|
|||||||
while (size < 2 * (distance + 16) * pixels && size < 4096) size *= 2;
|
while (size < 2 * (distance + 16) * pixels && size < 4096) size *= 2;
|
||||||
return size;
|
return size;
|
||||||
}
|
}
|
||||||
static Matrix4f lightMatrix(Vector3f sun, int distance, int size, double x, double y, double z, boolean zeroToOne) {
|
|
||||||
float extent = distance + 16, reach = 4 * (distance + 16);
|
|
||||||
// A fixed up axis is nonsingular even at noon. Snap the light-space origin to texels.
|
|
||||||
var view = new Matrix4f().lookAt(sun.x * reach, sun.y * reach, sun.z * reach, 0, 0, 0, 0, 0, 1);
|
|
||||||
double texel = 2.0 * extent / size;
|
|
||||||
double sx = view.m00() * x + view.m10() * y + view.m20() * z;
|
|
||||||
double sy = view.m01() * x + view.m11() * y + view.m21() * z;
|
|
||||||
var result = new Matrix4f().setOrtho(-extent, extent, -extent, extent, .1f, reach * 2, zeroToOne).mul(view);
|
|
||||||
result.m30(result.m30() + (float)((sx - Math.rint(sx / texel) * texel) / extent));
|
|
||||||
result.m31(result.m31() + (float)((sy - Math.rint(sy / texel) * texel) / extent));
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
public static void release() {
|
public static void release() {
|
||||||
TERRAIN.close();
|
TERRAIN.close();
|
||||||
|
lightGrid.reset();
|
||||||
if (shadowMap != null) { shadowMap.destroyBuffers(); shadowMap = null; }
|
if (shadowMap != null) { shadowMap.destroyBuffers(); shadowMap = null; }
|
||||||
if (uniforms != null) { uniforms.close(); uniforms = null; }
|
if (uniforms != null) { uniforms.close(); uniforms = null; }
|
||||||
applied = false;
|
applied = false;
|
||||||
|
|||||||
+10
-1
@@ -17,6 +17,15 @@ public final class ShaderOptionsScreen extends OptionsSubScreen {
|
|||||||
list.addBig(new OptionInstance<Integer>("sanctuary.shaders.saturation",OptionInstance.cachedConstantTooltip(
|
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),
|
Component.translatable("sanctuary.shaders.saturation_help")),(caption,value)->Component.translatable("sanctuary.shaders.saturation_value",value),
|
||||||
new OptionInstance.IntRange(0,200),VanillaLight.saturation(),VanillaLight::setSaturation));
|
new OptionInstance.IntRange(0,200),VanillaLight.saturation(),VanillaLight::setSaturation));
|
||||||
|
list.addBig(OptionInstance.createBoolean("sanctuary.shaders.edge_highlights",OptionInstance.cachedConstantTooltip(
|
||||||
|
Component.translatable("sanctuary.shaders.edge_highlights_help")),
|
||||||
|
VanillaLight.edgeHighlights(),VanillaLight::setEdgeHighlights));
|
||||||
|
list.addBig(new OptionInstance<Integer>("sanctuary.shaders.edge_intensity",OptionInstance.cachedConstantTooltip(
|
||||||
|
Component.translatable("sanctuary.shaders.edge_intensity_help")),(caption,value)->Component.translatable("sanctuary.shaders.edge_intensity_value",value),
|
||||||
|
new OptionInstance.IntRange(0,100),VanillaLight.edgeIntensity(),VanillaLight::setEdgeIntensity));
|
||||||
|
list.addBig(new OptionInstance<Integer>("sanctuary.shaders.edge_distance",OptionInstance.cachedConstantTooltip(
|
||||||
|
Component.translatable("sanctuary.shaders.edge_distance_help")),(caption,value)->Component.translatable("sanctuary.shaders.edge_distance_value",value),
|
||||||
|
new OptionInstance.Enum<>(List.of(16,32,64,128,256),Codec.INT),VanillaLight.edgeDistance(),VanillaLight::setEdgeDistance));
|
||||||
list.addBig(OptionInstance.createBoolean("sanctuary.shaders.pixel_shadows",OptionInstance.cachedConstantTooltip(
|
list.addBig(OptionInstance.createBoolean("sanctuary.shaders.pixel_shadows",OptionInstance.cachedConstantTooltip(
|
||||||
Component.translatable("sanctuary.shaders.pixel_shadows_help")),
|
Component.translatable("sanctuary.shaders.pixel_shadows_help")),
|
||||||
VanillaLight.pixelShadows(),VanillaLight::setPixelShadows));
|
VanillaLight.pixelShadows(),VanillaLight::setPixelShadows));
|
||||||
@@ -25,6 +34,6 @@ public final class ShaderOptionsScreen extends OptionsSubScreen {
|
|||||||
new OptionInstance.Enum<>(List.of(8,16,32),Codec.INT),VanillaLight.shadowPixels(),VanillaLight::setShadowPixels));
|
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(
|
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),
|
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),Codec.INT),VanillaLight.shadowDistance(),VanillaLight::setShadowDistance));
|
new OptionInstance.Enum<>(List.of(16,32,64,128,256),Codec.INT),VanillaLight.shadowDistance(),VanillaLight::setShadowDistance));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package fr.koka.sanctuary.client.shader;
|
||||||
|
|
||||||
|
import org.joml.Matrix4f;
|
||||||
|
import org.joml.Vector3f;
|
||||||
|
|
||||||
|
/** Keeps the raster grid continuous as the sun moves; only camera recentering snaps to texels. */
|
||||||
|
final class ShadowProjection {
|
||||||
|
private boolean initialized;
|
||||||
|
private int distance, size;
|
||||||
|
private double x, y, z, phaseX, phaseY;
|
||||||
|
|
||||||
|
Matrix4f matrix(Vector3f sun, int distance, int size, double x, double y, double z, boolean zeroToOne) {
|
||||||
|
float extent = distance + 16, reach = 4 * (distance + 16);
|
||||||
|
var view = new Matrix4f().lookAt(sun.x * reach, sun.y * reach, sun.z * reach,
|
||||||
|
0, 0, 0, 0, 0, 1);
|
||||||
|
double texel = 2.0 * extent / size;
|
||||||
|
if (!initialized || this.distance != distance || this.size != size) {
|
||||||
|
phaseX = phaseY = 0;
|
||||||
|
} else if (this.x != x || this.y != y || this.z != z) {
|
||||||
|
double dx = x - this.x, dy = y - this.y, dz = z - this.z;
|
||||||
|
phaseX = Math.IEEEremainder(phaseX + view.m00() * dx + view.m10() * dy + view.m20() * dz, texel);
|
||||||
|
phaseY = Math.IEEEremainder(phaseY + view.m01() * dx + view.m11() * dy + view.m21() * dz, texel);
|
||||||
|
}
|
||||||
|
// Re-rounding the projected WORLD origin every frame made a slowly rotating light jump
|
||||||
|
// a raster texel periodically, particularly far from spawn. Retain this phase in between
|
||||||
|
// camera anchor changes: the light can rotate continuously without moving the raster origin.
|
||||||
|
initialized = true;
|
||||||
|
this.distance = distance; this.size = size;
|
||||||
|
this.x = x; this.y = y; this.z = z;
|
||||||
|
var result = new Matrix4f().setOrtho(-extent, extent, -extent, extent, .1f, reach * 2, zeroToOne).mul(view);
|
||||||
|
result.m30(result.m30() + (float)(phaseX / extent));
|
||||||
|
result.m31(result.m31() + (float)(phaseY / extent));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void reset() { initialized = false; }
|
||||||
|
}
|
||||||
@@ -39,8 +39,8 @@ import org.joml.Vector3fc;
|
|||||||
* A different ClientLevel, or update(null, ...), also releases all owned resources.
|
* A different ClientLevel, or update(null, ...), also releases all owned resources.
|
||||||
*/
|
*/
|
||||||
public final class ShadowTerrain implements AutoCloseable {
|
public final class ShadowTerrain implements AutoCloseable {
|
||||||
/** Bounds metadata/work as well as geometry; current shader options use 16, 32 and 64. */
|
/** Bounds metadata/work as well as geometry; shader options reach 256 blocks. */
|
||||||
public static final int MAX_DISTANCE = 128;
|
public static final int MAX_DISTANCE = 256;
|
||||||
private static final int MAX_SECTIONS_PER_UPDATE = 4;
|
private static final int MAX_SECTIONS_PER_UPDATE = 4;
|
||||||
private static final long BUILD_NANOS = 3_000_000L;
|
private static final long BUILD_NANOS = 3_000_000L;
|
||||||
private static final long MAX_GPU_BYTES = 96L * 1024 * 1024;
|
private static final long MAX_GPU_BYTES = 96L * 1024 * 1024;
|
||||||
@@ -61,6 +61,8 @@ public final class ShadowTerrain implements AutoCloseable {
|
|||||||
private long gpuBytes;
|
private long gpuBytes;
|
||||||
private List<Entry> entries = List.of();
|
private List<Entry> entries = List.of();
|
||||||
private boolean entriesChanged;
|
private boolean entriesChanged;
|
||||||
|
private long reconciledTick = Long.MIN_VALUE;
|
||||||
|
private boolean metadataDirty = true;
|
||||||
private int cameraSectionX, cameraSectionY, cameraSectionZ;
|
private int cameraSectionX, cameraSectionY, cameraSectionZ;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -108,7 +110,12 @@ public final class ShadowTerrain implements AutoCloseable {
|
|||||||
cameraSectionX = cx;
|
cameraSectionX = cx;
|
||||||
cameraSectionY = cy;
|
cameraSectionY = cy;
|
||||||
cameraSectionZ = cz;
|
cameraSectionZ = cz;
|
||||||
reconcile();
|
// Large ranges must not rescan loaded chunk metadata at the display's frame rate.
|
||||||
|
if (moved || metadataDirty || reconciledTick != level.getGameTime()) {
|
||||||
|
reconcile();
|
||||||
|
reconciledTick = level.getGameTime();
|
||||||
|
metadataDirty = false;
|
||||||
|
}
|
||||||
if (moved) {
|
if (moved) {
|
||||||
retryDeferred();
|
retryDeferred();
|
||||||
}
|
}
|
||||||
@@ -172,11 +179,13 @@ public final class ShadowTerrain implements AutoCloseable {
|
|||||||
RenderSystem.assertOnRenderThread();
|
RenderSystem.assertOnRenderThread();
|
||||||
Section section = sections.get(SectionPos.asLong(sx, sy, sz));
|
Section section = sections.get(SectionPos.asLong(sx, sy, sz));
|
||||||
if (section != null) dirty(section);
|
if (section != null) dirty(section);
|
||||||
|
else metadataDirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Coordinates are chunk coordinates. Caller also invalidates adjacent chunks as needed. */
|
/** Coordinates are chunk coordinates. Caller also invalidates adjacent chunks as needed. */
|
||||||
public void dirtyChunk(int cx, int cz) {
|
public void dirtyChunk(int cx, int cz) {
|
||||||
RenderSystem.assertOnRenderThread();
|
RenderSystem.assertOnRenderThread();
|
||||||
|
metadataDirty = true;
|
||||||
for (Section section : sections.values()) {
|
for (Section section : sections.values()) {
|
||||||
if (section.x == cx && section.z == cz) dirty(section);
|
if (section.x == cx && section.z == cz) dirty(section);
|
||||||
}
|
}
|
||||||
@@ -406,6 +415,8 @@ public final class ShadowTerrain implements AutoCloseable {
|
|||||||
for (Section section : sections.values()) release(section);
|
for (Section section : sections.values()) release(section);
|
||||||
sections.clear();
|
sections.clear();
|
||||||
chunks.clear();
|
chunks.clear();
|
||||||
|
reconciledTick = Long.MIN_VALUE;
|
||||||
|
metadataDirty = true;
|
||||||
shapes.clear();
|
shapes.clear();
|
||||||
entries = List.of();
|
entries = List.of();
|
||||||
entriesChanged = false;
|
entriesChanged = false;
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ public final class VanillaLight {
|
|||||||
boolean enabled=true;
|
boolean enabled=true;
|
||||||
int saturation=142;
|
int saturation=142;
|
||||||
boolean pixelShadows=false;
|
boolean pixelShadows=false;
|
||||||
|
boolean edgeHighlights=false;
|
||||||
|
int edgeIntensity=30;
|
||||||
|
int edgeDistance=32;
|
||||||
int shadowPixels=16;
|
int shadowPixels=16;
|
||||||
int shadowDistance=32;
|
int shadowDistance=32;
|
||||||
}
|
}
|
||||||
@@ -27,22 +30,30 @@ public final class VanillaLight {
|
|||||||
try{if(Files.isRegularFile(path()))preferences=new GsonBuilder().create().fromJson(Files.readString(path()),Preferences.class);}
|
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);}
|
catch(Exception e){SanctuaryMod.LOGGER.warn("Cannot read Sanctuary shader preference",e);}
|
||||||
if(preferences==null)preferences=new Preferences();
|
if(preferences==null)preferences=new Preferences();
|
||||||
|
preferences.edgeIntensity=Math.clamp(preferences.edgeIntensity,0,100);
|
||||||
|
preferences.edgeDistance=normalizeShadowDistance(preferences.edgeDistance);
|
||||||
preferences.saturation=Math.clamp(preferences.saturation,0,200);
|
preferences.saturation=Math.clamp(preferences.saturation,0,200);
|
||||||
preferences.shadowPixels=normalizeShadowValue(preferences.shadowPixels,8,16,32);
|
preferences.shadowPixels=normalizeShadowValue(preferences.shadowPixels,8,16,32);
|
||||||
preferences.shadowDistance=normalizeShadowValue(preferences.shadowDistance,16,32,64);
|
preferences.shadowDistance=normalizeShadowDistance(preferences.shadowDistance);
|
||||||
}
|
}
|
||||||
return preferences.enabled;
|
return preferences.enabled;
|
||||||
}
|
}
|
||||||
public static boolean externalEngine(){return EXTERNAL_ENGINE;}
|
public static boolean externalEngine(){return EXTERNAL_ENGINE;}
|
||||||
public static boolean active(){return ProgressionClient.active()&&!EXTERNAL_ENGINE&&enabled();}
|
public static boolean active(){return ProgressionClient.active()&&!EXTERNAL_ENGINE&&enabled();}
|
||||||
public static int saturation(){enabled();return preferences.saturation;}
|
public static int saturation(){enabled();return preferences.saturation;}
|
||||||
|
public static boolean edgeHighlights(){enabled();return preferences.edgeHighlights;}
|
||||||
|
public static int edgeDistance(){enabled();return preferences.edgeDistance;}
|
||||||
|
public static void setEdgeDistance(int value){enabled();preferences.edgeDistance=normalizeShadowDistance(value);save();}
|
||||||
|
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 pixelShadows(){enabled();return preferences.pixelShadows;}
|
public static boolean pixelShadows(){enabled();return preferences.pixelShadows;}
|
||||||
public static int shadowPixels(){enabled();return preferences.shadowPixels;}
|
public static int shadowPixels(){enabled();return preferences.shadowPixels;}
|
||||||
public static int shadowDistance(){enabled();return preferences.shadowDistance;}
|
public static int shadowDistance(){enabled();return preferences.shadowDistance;}
|
||||||
public static void setSaturation(int value){enabled();preferences.saturation=Math.clamp(value,0,200);save();}
|
public static void setSaturation(int value){enabled();preferences.saturation=Math.clamp(value,0,200);save();}
|
||||||
public static void setPixelShadows(boolean value){enabled();preferences.pixelShadows=value;save();}
|
public static void setPixelShadows(boolean value){enabled();preferences.pixelShadows=value;save();}
|
||||||
public static void setShadowPixels(int value){enabled();preferences.shadowPixels=normalizeShadowValue(value,8,16,32);save();}
|
public static void setShadowPixels(int value){enabled();preferences.shadowPixels=normalizeShadowValue(value,8,16,32);save();}
|
||||||
public static void setShadowDistance(int value){enabled();preferences.shadowDistance=normalizeShadowValue(value,16,32,64);save();}
|
public static void setShadowDistance(int value){enabled();preferences.shadowDistance=normalizeShadowDistance(value);save();}
|
||||||
public static void setEnabled(boolean value){
|
public static void setEnabled(boolean value){
|
||||||
enabled();preferences.enabled=value;
|
enabled();preferences.enabled=value;
|
||||||
save();
|
save();
|
||||||
@@ -52,6 +63,12 @@ public final class VanillaLight {
|
|||||||
if(value<(low+middle)/2)return low;
|
if(value<(low+middle)/2)return low;
|
||||||
return value<(middle+high)/2?middle:high;
|
return value<(middle+high)/2?middle:high;
|
||||||
}
|
}
|
||||||
|
private static int normalizeShadowDistance(int value){
|
||||||
|
if(value<24)return 16;
|
||||||
|
if(value<48)return 32;
|
||||||
|
if(value<96)return 64;
|
||||||
|
return value<192?128:256;
|
||||||
|
}
|
||||||
private static void save(){
|
private static void save(){
|
||||||
try{
|
try{
|
||||||
Files.createDirectories(path().getParent());var temporary=path().resolveSibling(path().getFileName()+".tmp");
|
Files.createDirectories(path().getParent());var temporary=path().resolveSibling(path().getFileName()+".tmp");
|
||||||
|
|||||||
+4
-2
@@ -4,6 +4,7 @@ import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
|
|||||||
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
|
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
|
||||||
import com.mojang.renderpearl.api.buffers.GpuBufferSlice;
|
import com.mojang.renderpearl.api.buffers.GpuBufferSlice;
|
||||||
import fr.koka.sanctuary.client.shader.PixelShadows;
|
import fr.koka.sanctuary.client.shader.PixelShadows;
|
||||||
|
import fr.koka.sanctuary.client.shader.EdgeHighlights;
|
||||||
import net.minecraft.client.renderer.GameRenderer;
|
import net.minecraft.client.renderer.GameRenderer;
|
||||||
import net.minecraft.client.renderer.ProjectionMatrixBuffer;
|
import net.minecraft.client.renderer.ProjectionMatrixBuffer;
|
||||||
import org.joml.Matrix4f;
|
import org.joml.Matrix4f;
|
||||||
@@ -17,10 +18,11 @@ 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;"))
|
@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) {
|
private GpuBufferSlice sanctuary$projection(ProjectionMatrixBuffer buffer, Matrix4f matrix, Operation<GpuBufferSlice> original) {
|
||||||
PixelShadows.captureProjection(matrix);
|
PixelShadows.captureProjection(matrix);
|
||||||
|
EdgeHighlights.captureProjection(matrix);
|
||||||
return original.call(buffer, 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"))
|
@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(); }
|
private void sanctuary$shadows(CallbackInfo ci) { PixelShadows.render(); EdgeHighlights.render(); }
|
||||||
@Inject(method={"close", "setLevel"}, at=@At("HEAD"))
|
@Inject(method={"close", "setLevel"}, at=@At("HEAD"))
|
||||||
private void sanctuary$close(CallbackInfo ci) { PixelShadows.release(); }
|
private void sanctuary$close(CallbackInfo ci) { PixelShadows.release(); EdgeHighlights.release(); }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2257,12 +2257,20 @@
|
|||||||
"block.sanctuary.deepslate_sapphire_ore": "Deepslate Sapphire Ore",
|
"block.sanctuary.deepslate_sapphire_ore": "Deepslate Sapphire Ore",
|
||||||
"sanctuary.discovery.granted": "Discoveries added for %s: %s blocks, %s items, %s species. Linked recipes updated; reopen catalogues.",
|
"sanctuary.discovery.granted": "Discoveries added for %s: %s blocks, %s items, %s species. Linked recipes updated; reopen catalogues.",
|
||||||
"sanctuary.discovery.invalid": "Choose a discoverable block, item or species.",
|
"sanctuary.discovery.invalid": "Choose a discoverable block, item or species.",
|
||||||
|
"sanctuary.shaders.edge_highlights": "Edge highlights",
|
||||||
|
"sanctuary.shaders.edge_highlights_help": "Connected highlights along terrain contours. Flat surfaces connect, even across different blocks. Preserves textures. OFF by default; requires Vanilla Light.",
|
||||||
|
"sanctuary.shaders.edge_intensity": "Edge intensity",
|
||||||
|
"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": "Pixel shadows",
|
||||||
"sanctuary.shaders.pixel_shadows_help": "Sun shadows and subtle moon shadows on a pixel grid. Dappled foliage and softer shadows at noon. 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. Disabled by default; requires Vanilla Light.",
|
||||||
"sanctuary.shaders.shadow_pixels": "Shadow precision",
|
"sanctuary.shaders.shadow_pixels": "Shadow precision",
|
||||||
"sanctuary.shaders.shadow_pixels_value": "%s pixels/block",
|
"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.",
|
"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.",
|
||||||
"sanctuary.shaders.shadow_distance": "Shadow distance",
|
"sanctuary.shaders.shadow_distance": "Shadow distance",
|
||||||
"sanctuary.shaders.shadow_distance_value": "%s blocks",
|
"sanctuary.shaders.shadow_distance_value": "%s blocks",
|
||||||
"sanctuary.shaders.shadow_distance_help": "Shadow range: 16, 32 or 64 blocks. Higher distances cost more performance. Requires Vanilla Light and pixel shadows."
|
"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."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2257,12 +2257,20 @@
|
|||||||
"block.sanctuary.deepslate_sapphire_ore": "Minerai de saphir des abîmes",
|
"block.sanctuary.deepslate_sapphire_ore": "Minerai de saphir des abîmes",
|
||||||
"sanctuary.discovery.granted": "Découvertes ajoutées pour %s : %s blocs, %s objets, %s espèces. Recettes liées actualisées ; rouvre les catalogues.",
|
"sanctuary.discovery.granted": "Découvertes ajoutées pour %s : %s blocs, %s objets, %s espèces. Recettes liées actualisées ; rouvre les catalogues.",
|
||||||
"sanctuary.discovery.invalid": "Choisis un bloc, un objet ou une espèce découvrable.",
|
"sanctuary.discovery.invalid": "Choisis un bloc, un objet ou une espèce découvrable.",
|
||||||
|
"sanctuary.shaders.edge_highlights": "Reflets sur les arêtes",
|
||||||
|
"sanctuary.shaders.edge_highlights_help": "Reflets connectés sur les contours du relief. Les surfaces plates se raccordent, même entre blocs différents. Préserve les textures. Désactivé par défaut ; nécessite Vanilla Light.",
|
||||||
|
"sanctuary.shaders.edge_intensity": "Intensité des arêtes",
|
||||||
|
"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": "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 ombres atténuées à midi. 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. Désactivées par défaut ; nécessite Vanilla Light.",
|
||||||
"sanctuary.shaders.shadow_pixels": "Précision des ombres",
|
"sanctuary.shaders.shadow_pixels": "Précision des ombres",
|
||||||
"sanctuary.shaders.shadow_pixels_value": "%s pixels/bloc",
|
"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.",
|
"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.",
|
||||||
"sanctuary.shaders.shadow_distance": "Distance des ombres",
|
"sanctuary.shaders.shadow_distance": "Distance des ombres",
|
||||||
"sanctuary.shaders.shadow_distance_value": "%s blocs",
|
"sanctuary.shaders.shadow_distance_value": "%s blocs",
|
||||||
"sanctuary.shaders.shadow_distance_help": "Portée des ombres : 16, 32 ou 64 blocs. Une distance plus élevée coûte davantage en performances. Nécessite Vanilla Light et les ombres pixelisées."
|
"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."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
#version 330
|
||||||
|
#extension GL_ARB_separate_shader_objects : require
|
||||||
|
uniform sampler2D WorldDepth;
|
||||||
|
layout(std140) uniform EdgeHighlights {
|
||||||
|
mat4 InverseViewProjection;
|
||||||
|
mat4 ViewProjection;
|
||||||
|
vec4 CameraGrid;
|
||||||
|
vec4 StrengthClipFog;
|
||||||
|
vec4 EnvironmentFog;
|
||||||
|
};
|
||||||
|
layout(location = 0) in vec2 texCoord;
|
||||||
|
layout(location = 0) out vec4 fragColor;
|
||||||
|
|
||||||
|
vec3 positionAt(vec2 uv, float depth) {
|
||||||
|
float z = StrengthClipFog.y > 0.5 ? depth : depth * 2.0 - 1.0;
|
||||||
|
vec4 p = InverseViewProjection * vec4(uv * 2.0 - 1.0, z, 1.0);
|
||||||
|
return p.xyz / p.w;
|
||||||
|
}
|
||||||
|
|
||||||
|
float exposed(vec3 probe, vec3 surface, vec3 normal) {
|
||||||
|
vec4 clip = ViewProjection * vec4(probe, 1.0);
|
||||||
|
if (clip.w <= 0.0) return 0.0;
|
||||||
|
vec2 uv = clip.xy / clip.w * 0.5 + 0.5;
|
||||||
|
if (any(lessThanEqual(uv, vec2(0.0))) || any(greaterThanEqual(uv, vec2(1.0)))) return 0.0;
|
||||||
|
// Reconstruct at the sampled depth texel's centre, not at a different subpixel position.
|
||||||
|
vec2 size = vec2(textureSize(WorldDepth, 0));
|
||||||
|
uv = (floor(uv * size) + 0.5) / size;
|
||||||
|
float depth = texture(WorldDepth, uv).r;
|
||||||
|
if (depth <= 0.0) return 1.0;
|
||||||
|
float behind = dot(positionAt(uv, depth) - surface, normal);
|
||||||
|
// A coplanar neighbouring block contributes zero. Concave corners are not glowing seams.
|
||||||
|
return 1.0 - smoothstep(-0.045, -0.020, behind);
|
||||||
|
}
|
||||||
|
|
||||||
|
float fogAmount(float distance, float start, float end) {
|
||||||
|
return clamp((distance - start) / max(end - start, 0.001), 0.0, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
float depth = texture(WorldDepth, texCoord).r;
|
||||||
|
vec3 relative = positionAt(texCoord, depth);
|
||||||
|
vec3 dx = dFdx(relative), dy = dFdy(relative);
|
||||||
|
vec3 normal = normalize(cross(dx, dy));
|
||||||
|
if (dot(normal, -relative) < 0.0) normal = -normal;
|
||||||
|
vec3 an = abs(normal);
|
||||||
|
if (depth <= 0.0 || length(relative) >= EnvironmentFog.w || max(max(an.x, an.y), an.z) < 0.995) discard;
|
||||||
|
vec3 axis = an.x > an.y && an.x > an.z ? vec3(1,0,0) : an.y > an.z ? vec3(0,1,0) : vec3(0,0,1);
|
||||||
|
normal = axis * sign(dot(normal, axis));
|
||||||
|
vec3 tangent = axis.x > 0.5 ? vec3(0,0,1) : vec3(1,0,0);
|
||||||
|
vec3 bitangent = abs(cross(axis, tangent));
|
||||||
|
float width = 1.0 / CameraGrid.w;
|
||||||
|
vec3 p = relative + CameraGrid.xyz;
|
||||||
|
float plane = floor(dot(p, axis) * CameraGrid.w + 0.5) * width;
|
||||||
|
// Restrict to voxel planes; don't trace arbitrary moving models with a voxel-grid effect.
|
||||||
|
if (abs(dot(p, axis) - plane) > 0.003) discard;
|
||||||
|
vec3 q = (floor(p * CameraGrid.w + 0.0001) + 0.5) * width;
|
||||||
|
q += axis * (plane - dot(q, axis));
|
||||||
|
q -= CameraGrid.xyz;
|
||||||
|
float probeWidth = max(width, max(length(dx), length(dy)));
|
||||||
|
float edge = max(max(exposed(q + tangent * probeWidth, q, normal), exposed(q - tangent * probeWidth, q, normal)),
|
||||||
|
max(exposed(q + bitangent * probeWidth, q, normal), exposed(q - bitangent * probeWidth, q, normal)));
|
||||||
|
// Connected surfaces share their outline, irrespective of the block texture.
|
||||||
|
// Integrate subpixel strips as coverage rather than deleting distant CTM outlines.
|
||||||
|
float footprint = max(length(dx), length(dy));
|
||||||
|
float fade = min(1.0, width / max(footprint, width));
|
||||||
|
fade *= 1.0 - smoothstep(EnvironmentFog.w * 0.75, EnvironmentFog.w, length(relative));
|
||||||
|
float fog = max(fogAmount(length(relative), EnvironmentFog.x, EnvironmentFog.y),
|
||||||
|
fogAmount(max(length(relative.xz), abs(relative.y)), StrengthClipFog.z, StrengthClipFog.w));
|
||||||
|
float highlight = edge * fade * StrengthClipFog.x * (1.0 - fog * EnvironmentFog.z);
|
||||||
|
fragColor = vec4(vec3(highlight), 0.0);
|
||||||
|
}
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
name = "Sanctuary"
|
name = "Sanctuary"
|
||||||
author = "KOKA99CAB"
|
author = "KOKA99CAB"
|
||||||
version = "beta.128"
|
version = "beta.129"
|
||||||
pack-format = "packwiz:1.1.0"
|
pack-format = "packwiz:1.1.0"
|
||||||
|
|
||||||
[index]
|
[index]
|
||||||
|
|||||||
Reference in New Issue
Block a user