Add colored dynamic lights and smooth hue-preserving fades for beta.140
Build Sanctuary / build (push) Canceled after 0s
Build Sanctuary / build (push) Canceled after 0s
This commit is contained in:
@@ -20,12 +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
|
L’économie, les claims et les dimensions décrits dans la vision restent
|
||||||
à implémenter.
|
à implémenter.
|
||||||
|
|
||||||
La version **beta.139** ajoute les **lumières colorées** optionnelles : les vrais
|
La version **beta.140** relie les lumières colorées aux objets en main et sur
|
||||||
blocs lumineux teintent les surfaces voisines, même quand la source est hors
|
la tête, renforce leur teinte et ajoute une distance réglable de 8 à 64 blocs
|
||||||
champ. Intensité indépendante, réglée à 35 % ; obstacles natifs et cache local.
|
avec apparition progressive. Activation par défaut à 30 % ; torches natives,
|
||||||
Le [ticket](docs/colored-lights-beta139.md) décrit les limites et vérifications.
|
option « Torches chaudes » désactivée, cuivre vert et redstone rouge saturé.
|
||||||
Release publiée et même instance Prism synchronisée ; sauvegardes et réglages conservés.
|
Les frontières de saturation des lumières bleues s’atténuent progressivement.
|
||||||
La [vérification Vulkan antérieure](docs/shader-vulkan-beta138.md) reste disponible.
|
Le [ticket](docs/colored-dynamic-beta140.md) décrit le rendu et les vérifications.
|
||||||
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.
|
||||||
@@ -36,6 +36,14 @@ 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.140 — Lumières portées et fondus
|
||||||
|
|
||||||
|
Mains et tête, distances 8/16/32/64, fondus, cuivre vert, redstone rouge et
|
||||||
|
option pour la chaleur des torches. Lumières colorées activées à 30 % par défaut.
|
||||||
|
[Contrat et vérifications](docs/colored-dynamic-beta140.md) ·
|
||||||
|
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.140/Sanctuary-beta.140.mrpack) ·
|
||||||
|
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.140/Sanctuary-Test-beta.140.mrpack).
|
||||||
|
|
||||||
## beta.139 — Lumières colorées
|
## beta.139 — Lumières colorées
|
||||||
|
|
||||||
Teintes chaudes, sources des âmes bleues, redstone rouge, froglights et matériaux lumineux.
|
Teintes chaudes, sources des âmes bleues, redstone rouge, froglights et matériaux lumineux.
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
# LIGHT-140 — Lumières colorées portées et distance
|
||||||
|
|
||||||
|
Socle beta.139 `c05c338`, branche `codex/colored-dynamic-beta140`.
|
||||||
|
Minecraft 26.3 / Java 25. Livraison beta.140.
|
||||||
|
|
||||||
|
## Contrat
|
||||||
|
|
||||||
|
Relier les couleurs aux vraies sources de lumière dynamique : mains, tête,
|
||||||
|
cosmétiques, objets au sol et feu, selon le système existant. Renforcer la
|
||||||
|
teinte et activer les lumières colorées par défaut à 30 %. Ajouter une distance
|
||||||
|
d'affichage de 8, 16, 32 ou 64 blocs (16 par défaut), indépendante de la portée
|
||||||
|
native de chaque source. Augmenter la distance coûte davantage en mémoire et
|
||||||
|
calcul ; aucun chargement forcé de chunk. Les choix sauvegardés sont conservés.
|
||||||
|
|
||||||
|
Sources portées actualisées avec le système dynamique, sans reconstruire tout
|
||||||
|
le champ statique à chaque mouvement. Aucun changement de monde ou de règle
|
||||||
|
serveur. Vérifications natives OpenGL et Vulkan, puis construction et archives.
|
||||||
|
|
||||||
|
## Couleurs et fondus
|
||||||
|
|
||||||
|
Torches ordinaires : température native par défaut ; option « Torches chaudes »
|
||||||
|
pour retrouver l'orangé. Glowstone jaune, lave jaune-rouge, redstone rouge saturé,
|
||||||
|
cuivre vert (posé, en main ou sur la tête). Le cuivre est reconnu avant les
|
||||||
|
familles génériques de torches et lanternes. Les palettes explicites restent
|
||||||
|
indépendantes des packs ; le repli des autres matériaux suit leurs textures.
|
||||||
|
|
||||||
|
Le rendu s'atténue sur le dernier quart de la distance choisie. Le champ précédent
|
||||||
|
reste disponible pendant un fondu de 300 ms vers le nouveau champ ; l'activation
|
||||||
|
et les sources dynamiques ont aussi un fondu. Le changement de distance est
|
||||||
|
interpolé. Les sources mobiles utilisent la même sélection bornée et les mêmes
|
||||||
|
intensités que la lumière dynamique existante, avec interpolation visuelle des
|
||||||
|
positions et couleurs. Elles héritent de sa propagation radiale, y compris de
|
||||||
|
ses limites d'occultation ; les blocs posés conservent leur propagation avec murs.
|
||||||
|
|
||||||
|
Le volume varie réellement selon la distance (56, 72, 104 ou 168 blocs de côté,
|
||||||
|
avec marge de propagation). Les grandes distances augmentent la mémoire et le
|
||||||
|
délai de reconstruction ; la construction reste répartie entre les images.
|
||||||
|
Deux champs coexistent brièvement pour le fondu. Seuls les chunks déjà chargés
|
||||||
|
sont lus. La portée lumineuse native des torches n'est pas allongée.
|
||||||
|
|
||||||
|
## Frontières de saturation
|
||||||
|
|
||||||
|
Le stockage beta.139 tronquait les canaux faibles séparément en fin de portée :
|
||||||
|
un bleu pouvait perdre son rouge avant son vert, puis devenir plus saturé sur
|
||||||
|
une frontière visible. Le stockage logarithmique conserve maintenant une marge
|
||||||
|
sous le niveau lumineux natif zéro. Le shader applique la coupure à l'énergie
|
||||||
|
globale en conservant les rapports RGB. La réponse devient linéaire près du noir,
|
||||||
|
sans amplification par racine quatrième des très faibles contributions.
|
||||||
|
|
||||||
|
La transition des champs interpole le résultat colorimétrique rendu, pas une
|
||||||
|
énergie ensuite amplifiée. Les sources mobiles ont une atténuation de fondu
|
||||||
|
compensée pour la même réponse. Le test contrôle les rapports rouge/bleu et
|
||||||
|
vert/bleu d'une torche des âmes de 1 à 9 blocs, jusqu'au bord de sa portée.
|
||||||
|
|
||||||
|
## Vérifications natives
|
||||||
|
|
||||||
|
Minecraft 26.3 / Java 25 sur Apple M1, mondes de test neufs de graine 122 :
|
||||||
|
|
||||||
|
- OpenGL 4.1 Metal : **2 min 40 s**, `build/colored140-opengl.log`.
|
||||||
|
- Vulkan / MoltenVK 1.4.2, profondeur 0–1 et transparence améliorée :
|
||||||
|
**2 min 26 s**, `build/colored140-vulkan.log`.
|
||||||
|
|
||||||
|
Les deux exécutions passent `COLORED139_PASS` et `COLORED140_PASS` :
|
||||||
|
propagation avec murs, sources hors champ, lampes allumées/éteintes, retrait,
|
||||||
|
rechargement des ressources, cache, déplacement/retour de caméra, OFF/zéro,
|
||||||
|
préférences et traductions FR/EN. Les nouveaux contrôles couvrent la teinte
|
||||||
|
bleue jusqu'à neuf blocs, la torche native inchangée, l'option chaude, le cuivre
|
||||||
|
vert et le rouge saturé, les deux mains et le cosmétique de tête, la position
|
||||||
|
en troisième personne, les quatre distances et un fondu sans dépassement.
|
||||||
|
|
||||||
|
Dans la même scène contrôlée, la somme d'écarts OFF/torche chaude à 30 % vaut
|
||||||
|
3 926 093 / 3 926 092 sur OpenGL/Vulkan, contre 2 025 577 en beta.139 à 35 %.
|
||||||
|
Cette mesure vérifie l'accentuation demandée, pas la performance. Le test fige
|
||||||
|
uniquement le scintillement de la lightmap native ; cette sonde et les tests
|
||||||
|
client ne sont pas distribués. Les captures finales sont conservées sous
|
||||||
|
`build/colored140-opengl-screenshots/` et `build/colored140-vulkan-screenshots/`.
|
||||||
|
Comparatif autonome : `build/Shader-beta.140-comparaison.html`.
|
||||||
|
|
||||||
|
Pas de validation des pilotes Windows de l'utilisateur revendiquée. Les
|
||||||
|
limites de beta.139 pour le rendu sous l'eau/lave et les panoramas restent
|
||||||
|
ouvertes. Les sources dynamiques gardent l'occultation du système existant.
|
||||||
|
Aucun monde personnel ouvert ni modifié.
|
||||||
|
|
||||||
|
## Construction et archives
|
||||||
|
|
||||||
|
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||||
|
réussit en **2 min 22 s**, 126 tâches. Le GameTest serveur dédié reste exclu
|
||||||
|
conformément au refus EULA antérieur ; les tests clients ci-dessus sont exécutés
|
||||||
|
séparément. Log : `build/shader140-build.log`.
|
||||||
|
|
||||||
|
La comparaison avec beta.139 trouve **18 entrées de production modifiées**,
|
||||||
|
limitées aux lumières dynamiques/colorées, options et aides FR/EN. Les sources
|
||||||
|
des JAR correspondent aux fichiers du dépôt. Les archives normale/Test contiennent
|
||||||
|
le même JAR Sanctuary ; la version, les dépendances exactes, les textures de
|
||||||
|
gemmes et de la clé de Steve, l'absence de classes de test et le template complet
|
||||||
|
sont vérifiés. Reçu : `build/shader140-artifact.json`.
|
||||||
|
|
||||||
|
SHA-256 du JAR : `853284c1a61c7fce12edd2f7763717321eef56977290eb0e7c5db5e30bff442f`.
|
||||||
|
|
||||||
|
- Sanctuary-beta.140.mrpack : `1b366746e4d6bebfbe0115c849524bdeb81c14eee89a2510f9dbe18808974d6a`.
|
||||||
|
- Sanctuary-Test-beta.140.mrpack : `50c3cdf759117bb5286d41b4535fde15d9d84deaac833b7f8f61c4a0d29b72e7`.
|
||||||
+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.139
|
mod_version=beta.140
|
||||||
pack_version=beta.139
|
pack_version=beta.140
|
||||||
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
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ if (clientRenderTests) {
|
|||||||
if (improvedTransparency.isPresent()) {
|
if (improvedTransparency.isPresent()) {
|
||||||
systemProperty('sanctuary.test.improvedTransparency', improvedTransparency.get())
|
systemProperty('sanctuary.test.improvedTransparency', improvedTransparency.get())
|
||||||
}
|
}
|
||||||
|
systemProperty('sanctuary.test.colored140', providers.gradleProperty('sanctuaryColored140ClientTests').getOrElse('false'))
|
||||||
systemProperty('sanctuary.test.noVsync', providers.gradleProperty('sanctuaryClientNoVsync').getOrElse('false'))
|
systemProperty('sanctuary.test.noVsync', providers.gradleProperty('sanctuaryClientNoVsync').getOrElse('false'))
|
||||||
systemProperty('sanctuary.test.quick', providers.gradleProperty('sanctuaryQuickTests').getOrElse('false'))
|
systemProperty('sanctuary.test.quick', providers.gradleProperty('sanctuaryQuickTests').getOrElse('false'))
|
||||||
systemProperty('sanctuary.test.introVisualOnly', providers.gradleProperty('sanctuaryIntroVisualOnly').getOrElse('false'))
|
systemProperty('sanctuary.test.introVisualOnly', providers.gradleProperty('sanctuaryIntroVisualOnly').getOrElse('false'))
|
||||||
|
|||||||
+9
-3
@@ -20,7 +20,7 @@ public final class ColoredLights139ClientChecks implements FabricClientGameTest
|
|||||||
finally{System.clearProperty("sanctuary.test.freezeBlockFlicker");}
|
finally{System.clearProperty("sanctuary.test.freezeBlockFlicker");}
|
||||||
}
|
}
|
||||||
static void run(ClientGameTestContext c,TestServerContext server){
|
static void run(ClientGameTestContext c,TestServerContext server){
|
||||||
c.runOnClient(m->{preferences();m.options.particles().set(net.minecraft.server.level.ParticleStatus.MINIMAL);VanillaLight.setEnabled(true);VanillaLight.setSaturation(100);VanillaLight.setPbr(false);VanillaLight.setSsgi(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);VanillaLight.setLensFlare(false);VanillaLight.setEdgeHighlights(false);VanillaLight.setPixelShadows(false);VanillaLight.setSunshafts(false);VanillaLight.setColoredLights(false);VanillaLight.setColoredLightIntensity(35);});
|
c.runOnClient(m->{preferences();VanillaLight.setWarmTorches(true);m.options.particles().set(net.minecraft.server.level.ParticleStatus.MINIMAL);VanillaLight.setEnabled(true);VanillaLight.setSaturation(100);VanillaLight.setPbr(false);VanillaLight.setSsgi(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);VanillaLight.setLensFlare(false);VanillaLight.setEdgeHighlights(false);VanillaLight.setPixelShadows(false);VanillaLight.setSunshafts(false);VanillaLight.setColoredLights(false);VanillaLight.setColoredLightIntensity(35);});
|
||||||
camera(c,server,100.5,101,104.5,70);c.waitTicks(30);
|
camera(c,server,100.5,101,104.5,70);c.waitTicks(30);
|
||||||
server.runOnServer(s->{command(s,"time set 18000");
|
server.runOnServer(s->{command(s,"time set 18000");
|
||||||
for(int x=88;x<=112;x++)for(int z=92;z<=116;z++)for(int y=100;y<=109;y++)
|
for(int x=88;x<=112;x++)for(int z=92;z<=116;z++)for(int y=100;y<=109;y++)
|
||||||
@@ -36,6 +36,12 @@ public final class ColoredLights139ClientChecks implements FabricClientGameTest
|
|||||||
int r=color>>16&255,g=color>>8&255,b=color&255;
|
int r=color>>16&255,g=color>>8&255,b=color&255;
|
||||||
if(block==Blocks.TORCH){check(r>g&&g>b,"Torch is warm");warm=delta;}
|
if(block==Blocks.TORCH){check(r>g&&g>b,"Torch is warm");warm=delta;}
|
||||||
if(block==Blocks.SOUL_TORCH)check(b>g&&g>r,"Soul source is blue");
|
if(block==Blocks.SOUL_TORCH)check(b>g&&g>r,"Soul source is blue");
|
||||||
|
if(block==Blocks.SOUL_TORCH)for(int step=1;step<=9;step++){
|
||||||
|
final int distance=step;int sample=c.computeOnClient(m->ColoredLights.sample(SOURCE.offset(distance,0,0)));
|
||||||
|
int sr=sample>>16&255,sg=sample>>8&255,sb=sample&255;
|
||||||
|
double rr=sr==0?0:Math.pow(.8,(sb-sr)/8.0),gg=sg==0?0:Math.pow(.8,(sb-sg)/8.0);
|
||||||
|
check(Math.abs(rr-85/255.)<.02&&Math.abs(gg-189/255.)<.02,"Blue hue remains consistent at native distance "+step+" "+rr+" / "+gg);
|
||||||
|
}
|
||||||
if(block==Blocks.VERDANT_FROGLIGHT)check(g>r&&g>b,"Verdant froglight is green");
|
if(block==Blocks.VERDANT_FROGLIGHT)check(g>r&&g>b,"Verdant froglight is green");
|
||||||
if(block==Blocks.PEARLESCENT_FROGLIGHT)check(r>g&&b>g,"Pearlescent froglight is pink");
|
if(block==Blocks.PEARLESCENT_FROGLIGHT)check(r>g&&b>g,"Pearlescent froglight is pink");
|
||||||
if(block==Blocks.OCHRE_FROGLIGHT)check(r>g&&g>b,"Ochre froglight is warm");
|
if(block==Blocks.OCHRE_FROGLIGHT)check(r>g&&g>b,"Ochre froglight is warm");
|
||||||
@@ -73,8 +79,8 @@ public final class ColoredLights139ClientChecks implements FabricClientGameTest
|
|||||||
private static void settle(ClientGameTestContext c){int before=c.computeOnClient(m->ColoredLights.renderedFrames());c.waitFor(m->ColoredLights.applied()&&ColoredLights.settled()&&ColoredLights.renderedFrames()>before+8,1600);c.waitTicks(8);}
|
private static void settle(ClientGameTestContext c){int before=c.computeOnClient(m->ColoredLights.renderedFrames());c.waitFor(m->ColoredLights.applied()&&ColoredLights.settled()&&ColoredLights.renderedFrames()>before+8,1600);c.waitTicks(8);}
|
||||||
private static long difference(Frame a,Frame b){long sum=0;for(int i=0;i<a.pixels().length;i++)sum+=channelDifference(a.pixels()[i],b.pixels()[i]);return sum;}
|
private static long difference(Frame a,Frame b){long sum=0;for(int i=0;i<a.pixels().length;i++)sum+=channelDifference(a.pixels()[i],b.pixels()[i]);return sum;}
|
||||||
private static void preferences(){try{
|
private static void preferences(){try{
|
||||||
var path=FabricLoader.getInstance().getConfigDir().resolve("sanctuary-shaders.json");Files.deleteIfExists(path);forgetPreferences();check(!VanillaLight.coloredLights()&&VanillaLight.coloredLightIntensity()==35,"New option is opt-in, subtle at 35 percent");
|
var path=FabricLoader.getInstance().getConfigDir().resolve("sanctuary-shaders.json");Files.deleteIfExists(path);forgetPreferences();check(VanillaLight.coloredLights()&&VanillaLight.coloredLightIntensity()==30,"New defaults are enabled at 30 percent");
|
||||||
Files.writeString(path,"{\"bloomIntensity\":20,\"ssgi\":false}");forgetPreferences();check(!VanillaLight.coloredLights()&&VanillaLight.coloredLightIntensity()==35&&!VanillaLight.ssgi()&&VanillaLight.bloomIntensity()==20,"Old preferences keep their choices");
|
Files.writeString(path,"{\"bloomIntensity\":20,\"ssgi\":false}");forgetPreferences();check(VanillaLight.coloredLights()&&VanillaLight.coloredLightIntensity()==30&&!VanillaLight.ssgi()&&VanillaLight.bloomIntensity()==20,"Old preferences keep their choices");
|
||||||
for(int n:new int[]{-1,0,35,100,400}){VanillaLight.setColoredLights(true);VanillaLight.setColoredLightIntensity(n);forgetPreferences();check(VanillaLight.coloredLights()&&VanillaLight.coloredLightIntensity()==Math.clamp(n,0,100),"Bounded persistent controls");}
|
for(int n:new int[]{-1,0,35,100,400}){VanillaLight.setColoredLights(true);VanillaLight.setColoredLightIntensity(n);forgetPreferences();check(VanillaLight.coloredLights()&&VanillaLight.coloredLightIntensity()==Math.clamp(n,0,100),"Bounded persistent controls");}
|
||||||
}catch(java.io.IOException e){throw new AssertionError(e);}}
|
}catch(java.io.IOException e){throw new AssertionError(e);}}
|
||||||
}
|
}
|
||||||
|
|||||||
+59
@@ -0,0 +1,59 @@
|
|||||||
|
package fr.koka.sanctuary.client.shader;
|
||||||
|
|
||||||
|
import static fr.koka.sanctuary.client.shader.PixelShadows122ClientChecks.*;
|
||||||
|
import fr.koka.sanctuary.client.DynamicLightClient;
|
||||||
|
import fr.koka.sanctuary.inventory.*;
|
||||||
|
import fr.koka.sanctuary.cosmetics.AccessoryService;
|
||||||
|
import net.fabricmc.fabric.api.client.gametest.v1.context.*;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.registries.BuiltInRegistries;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.world.entity.EquipmentSlot;
|
||||||
|
import net.minecraft.world.item.*;
|
||||||
|
import net.minecraft.world.level.block.*;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
|
||||||
|
final class ColoredLights140ClientChecks {
|
||||||
|
private static final BlockPos SOURCE=new BlockPos(98,101,102),PROBE=new BlockPos(99,101,106);
|
||||||
|
static void run(ClientGameTestContext c,TestServerContext server){
|
||||||
|
c.runOnClient(m->{VanillaLight.setPbr(false);VanillaLight.setSsgi(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);VanillaLight.setLensFlare(false);VanillaLight.setEdgeHighlights(false);VanillaLight.setPixelShadows(false);VanillaLight.setSunshafts(false);VanillaLight.setColoredLightIntensity(30);VanillaLight.setWarmTorches(false);VanillaLight.setColoredLightDistance(16);});
|
||||||
|
source(c,server,Blocks.TORCH);enable(c,false);var nativeTorch=capture(c,"torch-native-off");enable(c,true);unchanged(nativeTorch,capture(c,"torch-native-on"),"Normal torch preserves vanilla colour");
|
||||||
|
c.runOnClient(m->VanillaLight.setWarmTorches(true));settle(c);var warm=capture(c,"torch-warm-30");long accent=difference(nativeTorch,warm);check(accent>2_025_577,"30 percent exceeds beta139 warm tint at 35 percent: "+accent);
|
||||||
|
c.runOnClient(m->VanillaLight.setWarmTorches(false));settle(c);unchanged(nativeTorch,capture(c,null),"Warm torch option can restore native tint");
|
||||||
|
for(String name:new String[]{"copper_torch","copper_lantern"}){
|
||||||
|
var copper=BuiltInRegistries.BLOCK.getValue(Identifier.parse("minecraft:"+name));check(copper!=Blocks.AIR&&copper.defaultBlockState().getLightEmission()>0,"Native copper source exists "+name);
|
||||||
|
source(c,server,copper);int value=c.computeOnClient(m->ColoredLights.sample(PROBE));check((value>>8&255)>(value>>16&255)&&(value>>8&255)>(value&255),"Copper emits green");capture(c,name+"-green");
|
||||||
|
}
|
||||||
|
server.runOnServer(s->{s.overworld().setBlockAndUpdate(SOURCE,Blocks.REDSTONE_LAMP.defaultBlockState());s.overworld().setBlockAndUpdate(SOURCE.below(),Blocks.REDSTONE_BLOCK.defaultBlockState());});c.waitTicks(40);settle(c);
|
||||||
|
int red=c.computeOnClient(m->ColoredLights.sample(PROBE));check(Math.pow(.8,((red>>16&255)-(red>>8&255))/8.)<.04,"Redstone uses saturated red");capture(c,"redstone-red-30");
|
||||||
|
source(c,server,Blocks.AIR);server.runOnServer(s->s.overworld().setBlockAndUpdate(SOURCE.below(),Blocks.CONCRETE.white().defaultBlockState()));
|
||||||
|
// Actual carried sources, synchronized through native equipment and accessory packets.
|
||||||
|
server.runOnServer(s->{command(s,"gamemode creative @a");var p=s.getPlayerList().getPlayers().getFirst();p.setItemSlot(EquipmentSlot.MAINHAND,new ItemStack(Items.SOUL_TORCH));});
|
||||||
|
c.waitFor(m->DynamicLightClient.snapshot(m.level).size()==1,200);c.waitTicks(45);enable(c,false);var carriedOff=capture(c,"held-soul-off");enable(c,true);c.waitTicks(25);var carriedOn=capture(c,"held-soul-blue");check(difference(carriedOff,carriedOn)>1000&&c.computeOnClient(m->ColoredLights.dynamicCount())==1,"Held soul torch is visibly blue");
|
||||||
|
c.runOnClient(m->{var l=DynamicLightClient.snapshot(m.level).sources().iterator().next();check((l.color()&255)>(l.color()>>16&255),"Dynamic source has actual blue colour");});
|
||||||
|
c.runOnClient(m->m.options.setCameraType(net.minecraft.client.CameraType.THIRD_PERSON_BACK));c.waitTicks(25);
|
||||||
|
c.runOnClient(m->{var pos=ColoredLights.dynamicPosition(m.player.getId());check(pos!=null&&pos.distanceToSqr(m.player.getEyePosition())<1,"Third-person light follows player, not camera");});capture(c,"held-third-person");
|
||||||
|
c.runOnClient(m->m.options.setCameraType(net.minecraft.client.CameraType.FIRST_PERSON));c.waitTicks(20);settle(c);
|
||||||
|
int builds=c.computeOnClient(m->ColoredLights.builds());
|
||||||
|
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.setItemSlot(EquipmentSlot.MAINHAND,ItemStack.EMPTY);p.setItemSlot(EquipmentSlot.OFFHAND,new ItemStack(Items.SOUL_TORCH));});c.waitTicks(35);settle(c);check(c.computeOnClient(m->ColoredLights.builds())==builds,"Moving a light between hands does not rebuild static volume");capture(c,"offhand-soul-blue");
|
||||||
|
var copperItem=BuiltInRegistries.ITEM.getValue(Identifier.parse("minecraft:copper_torch"));
|
||||||
|
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.setItemSlot(EquipmentSlot.OFFHAND,ItemStack.EMPTY);AccessoryAccess.of(p).setItem(AccessoryContainer.HEAD,new ItemStack(copperItem));AccessoryService.publish(p);});c.waitTicks(45);settle(c);
|
||||||
|
c.runOnClient(m->{var l=DynamicLightClient.snapshot(m.level).sources().iterator().next();check((l.color()>>8&255)>(l.color()>>16&255)&&(l.color()>>8&255)>(l.color()&255),"Head cosmetic carries copper green");});capture(c,"head-copper-green");check(c.computeOnClient(m->ColoredLights.builds())==builds,"Dynamic colour updates preserve static cache");
|
||||||
|
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();AccessoryAccess.of(p).setItem(AccessoryContainer.HEAD,ItemStack.EMPTY);AccessoryService.publish(p);});c.waitFor(m->DynamicLightClient.snapshot(m.level).size()==0&&ColoredLights.dynamicCount()==0,200);c.waitTicks(25);
|
||||||
|
server.runOnServer(s->command(s,"gamemode spectator @a"));
|
||||||
|
camera(c,server,100.5,108,104.5,70);look(c,server,0,18);c.waitTicks(25);
|
||||||
|
server.runOnServer(s->{for(int x=88;x<=112;x++)for(int z=117;z<=155;z++)s.overworld().setBlockAndUpdate(new BlockPos(x,100,z),Blocks.CONCRETE.white().defaultBlockState());
|
||||||
|
for(int x=88;x<=112;x++)for(int y=101;y<=109;y++)s.overworld().setBlockAndUpdate(new BlockPos(x,y,116),Blocks.AIR.defaultBlockState());
|
||||||
|
s.overworld().setBlockAndUpdate(new BlockPos(100,101,140),Blocks.VERDANT_FROGLIGHT.defaultBlockState());});c.waitTicks(50);
|
||||||
|
c.runOnClient(m->VanillaLight.setColoredLightDistance(8));settle(c);var shortRange=capture(c,"distance-8");int small=c.computeOnClient(m->ColoredLights.volumeSide());check(c.computeOnClient(m->ColoredLights.sample(new BlockPos(100,101,141)))==0,"Far field omitted at short distance");
|
||||||
|
int before=c.computeOnClient(m->ColoredLights.builds());c.runOnClient(m->VanillaLight.setColoredLightDistance(64));c.waitFor(m->ColoredLights.builds()>before&&ColoredLights.blend()>0&&ColoredLights.blend()<1,1600);var transition=capture(c,"distance-fade");settle(c);var longRange=capture(c,"distance-64");
|
||||||
|
check(c.computeOnClient(m->ColoredLights.volumeSide())>small,"Distance changes actual field allocation");check((c.computeOnClient(m->ColoredLights.sample(new BlockPos(100,101,141)))&0xffffff)!=0,"Far light retained at long distance");check(difference(shortRange,longRange)>1000,"Far light is visibly rendered");check(difference(transition,longRange)>100&&difference(shortRange,transition)<difference(shortRange,longRange),"Transition is an intermediate rendered frame without overshoot");
|
||||||
|
for(int distance:new int[]{8,16,32,64}){c.runOnClient(m->{VanillaLight.setColoredLightDistance(distance);forgetPreferences();check(VanillaLight.coloredLightDistance()==distance,"Persisted distance "+distance);});settle(c);}
|
||||||
|
c.runOnClient(m->{for(String locale:new String[]{"fr_fr","en_us"}){m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());for(String key:new String[]{"warm_torches","warm_torches_help","colored_light_distance","colored_light_distance_help","colored_light_distance_value"}){String full="sanctuary.shaders."+key;check(!Component.translatable(full,16).getString().equals(full),"Translated "+locale+" "+key);}}});
|
||||||
|
System.out.println("COLORED140_PASS accent="+accent+" native/warm torches, copper green, saturated red, main/offhand/head, dynamic cache, 8/16/32/64 distance, fade and FR/EN");
|
||||||
|
}
|
||||||
|
private static void source(ClientGameTestContext c,TestServerContext s,Block b){s.runOnServer(server->server.overworld().setBlockAndUpdate(SOURCE,b.defaultBlockState()));c.waitTicks(35);settle(c);}
|
||||||
|
private static void enable(ClientGameTestContext c,boolean value){c.runOnClient(m->VanillaLight.setColoredLights(value));if(value)settle(c);else c.waitTicks(10);}
|
||||||
|
private static void settle(ClientGameTestContext c){c.waitFor(m->ColoredLights.applied()&&ColoredLights.settled(),2000);c.waitTicks(12);}
|
||||||
|
private static long difference(Frame a,Frame b){long total=0;for(int i=0;i<a.pixels().length;i++)total+=channelDifference(a.pixels()[i],b.pixels()[i]);return total;}
|
||||||
|
}
|
||||||
+3
-3
@@ -149,7 +149,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
|||||||
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);
|
||||||
|
|
||||||
if(coloredOnly){ColoredLights139ClientChecks.run(c,server);return;}
|
if(coloredOnly){ColoredLights139ClientChecks.run(c,server);if(Boolean.getBoolean("sanctuary.test.colored140"))ColoredLights140ClientChecks.run(c,server);return;}
|
||||||
if(sunshaftOnly){Sunshafts137ClientChecks.run(c,server);return;}
|
if(sunshaftOnly){Sunshafts137ClientChecks.run(c,server);return;}
|
||||||
if(closeupOnly){SsgiCloseup135ClientChecks.run(c,server);Pbr134ClientChecks.run(c,server);return;}
|
if(closeupOnly){SsgiCloseup135ClientChecks.run(c,server);Pbr134ClientChecks.run(c,server);return;}
|
||||||
if(pbrOnly){Pbr134ClientChecks.run(c,server);return;}
|
if(pbrOnly){Pbr134ClientChecks.run(c,server);return;}
|
||||||
@@ -281,7 +281,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
c.waitTicks(3);
|
c.waitTicks(3);
|
||||||
c.takeScreenshot("sanctuary-beta139-shader-options-" + locale);
|
c.takeScreenshot("sanctuary-beta140-shader-options-" + locale);
|
||||||
}
|
}
|
||||||
c.runOnClient(m -> m.gui.setScreen(null));
|
c.runOnClient(m -> m.gui.setScreen(null));
|
||||||
}
|
}
|
||||||
@@ -565,7 +565,7 @@ public final class PixelShadows122ClientChecks implements FabricClientGameTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static Frame capture(ClientGameTestContext c, String name) {
|
static Frame capture(ClientGameTestContext c, String name) {
|
||||||
if (name != null) c.takeScreenshot("sanctuary-beta139-shader-" + name);
|
if (name != null) c.takeScreenshot("sanctuary-beta140-shader-" + 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) {
|
||||||
|
|||||||
@@ -27,6 +27,15 @@ public final class DynamicLightClient {
|
|||||||
private static DynamicLightConfig config;
|
private static DynamicLightConfig config;
|
||||||
private static final Map<Item,Integer> overrides=new IdentityHashMap<>();
|
private static final Map<Item,Integer> overrides=new IdentityHashMap<>();
|
||||||
private static int ticks;
|
private static int ticks;
|
||||||
|
private static boolean invalidatingSections;
|
||||||
|
private static final fr.koka.sanctuary.client.shader.ColoredLightMaterials colors=new fr.koka.sanctuary.client.shader.ColoredLightMaterials();
|
||||||
|
private static Object models;
|
||||||
|
public static boolean invalidatingSections(){return invalidatingSections;}
|
||||||
|
private static final class ColorMix {
|
||||||
|
double r,g,b;
|
||||||
|
void add(int light,int color){if(light<=0)return;double energy=Math.pow(.8,15-light);r=Math.max(r,(color>>16&255)*energy);g=Math.max(g,(color>>8&255)*energy);b=Math.max(b,(color&255)*energy);}
|
||||||
|
int result(){double m=Math.max(1e-8,Math.max(r,Math.max(g,b)));return (int)Math.round(r/m*255)<<16|(int)Math.round(g/m*255)<<8|(int)Math.round(b/m*255);}
|
||||||
|
}
|
||||||
private DynamicLightClient() {}
|
private DynamicLightClient() {}
|
||||||
public static void register(KeyMapping.Category category) {
|
public static void register(KeyMapping.Category category) {
|
||||||
config=DynamicLightConfig.load();
|
config=DynamicLightConfig.load();
|
||||||
@@ -50,7 +59,7 @@ public final class DynamicLightClient {
|
|||||||
}
|
}
|
||||||
public static int pendingSections() {return field.pendingCount();}
|
public static int pendingSections() {return field.pendingCount();}
|
||||||
private static void reset(ClientLevel level) {
|
private static void reset(ClientLevel level) {
|
||||||
field=new DynamicLightField();view=new View(level,field.snapshot());ticks=0;
|
field=new DynamicLightField();view=new View(level,field.snapshot());ticks=0;colors.clear();models=null;
|
||||||
}
|
}
|
||||||
private static int emission(ItemStack stack) {return overrides.getOrDefault(stack.getItem(),-1)>=0?overrides.get(stack.getItem()):ItemLightEmission.of(stack);}
|
private static int emission(ItemStack stack) {return overrides.getOrDefault(stack.getItem(),-1)>=0?overrides.get(stack.getItem()):ItemLightEmission.of(stack);}
|
||||||
private static void tick(Minecraft m) {
|
private static void tick(Minecraft m) {
|
||||||
@@ -59,6 +68,8 @@ public final class DynamicLightClient {
|
|||||||
if(ticks++%config.updateIntervalTicks==0) {
|
if(ticks++%config.updateIntervalTicks==0) {
|
||||||
List<DynamicLightField.Source> sources=List.of();
|
List<DynamicLightField.Source> sources=List.of();
|
||||||
if(config.enabled) {
|
if(config.enabled) {
|
||||||
|
var currentModels=m.getModelManager().getBlockStateModelSet();if(models!=currentModels){colors.clear();models=currentModels;}
|
||||||
|
boolean tinted=fr.koka.sanctuary.client.shader.VanillaLight.coloredLights();
|
||||||
var eye=m.getCameraEntity().getEyePosition();
|
var eye=m.getCameraEntity().getEyePosition();
|
||||||
var collect=new DynamicLightField.Collector(config.dropClusterSize);
|
var collect=new DynamicLightField.Collector(config.dropClusterSize);
|
||||||
double distance=config.range*config.range;
|
double distance=config.range*config.range;
|
||||||
@@ -66,23 +77,26 @@ public final class DynamicLightClient {
|
|||||||
if(entity.isRemoved() || entity.distanceToSqr(eye)>distance)continue;
|
if(entity.isRemoved() || entity.distanceToSqr(eye)>distance)continue;
|
||||||
boolean drop=entity instanceof ItemEntity;
|
boolean drop=entity instanceof ItemEntity;
|
||||||
int light=entity.isOnFire()?15:0;
|
int light=entity.isOnFire()?15:0;
|
||||||
|
var mix=new ColorMix();if(light>0)mix.add(light,0xFFAB55);
|
||||||
double y=entity.getY()+entity.getBbHeight()*.5;
|
double y=entity.getY()+entity.getBbHeight()*.5;
|
||||||
if(entity instanceof ItemEntity item)light=Math.max(light,emission(item.getItem()));
|
if(entity instanceof ItemEntity item){int emitted=emission(item.getItem());light=Math.max(light,emitted);if(emitted>0&&tinted)mix.add(emitted,colors.color(item.getItem()));}
|
||||||
else if(entity instanceof LivingEntity living) {
|
else if(entity instanceof LivingEntity living) {
|
||||||
if(living instanceof Player p && p.isSpectator())continue;
|
if(living instanceof Player p && p.isSpectator())continue;
|
||||||
light=Math.max(light,Math.max(emission(living.getMainHandItem()),emission(living.getOffhandItem())));
|
for(var stack:new ItemStack[]{living.getMainHandItem(),living.getOffhandItem(),living.getItemBySlot(EquipmentSlot.HEAD),HeadCosmeticService.appearance(living)}){
|
||||||
light=Math.max(light,emission(living.getItemBySlot(EquipmentSlot.HEAD)));
|
int emitted=emission(stack);light=Math.max(light,emitted);if(emitted>0&&tinted)mix.add(emitted,colors.color(stack));
|
||||||
light=Math.max(light,emission(HeadCosmeticService.appearance(living)));
|
}
|
||||||
y=entity.getEyeY();
|
y=entity.getEyeY();
|
||||||
}
|
}
|
||||||
if(light>0)collect.add(entity.getId(),drop,entity.getX(),y,entity.getZ(),light);
|
if(light>0)collect.add(entity.getId(),drop,entity.getX(),y,entity.getZ(),light,tinted?mix.result():0xFFFFFF);
|
||||||
}
|
}
|
||||||
sources=collect.finish(config.maxSources,eye.x,eye.y,eye.z,m.player==null?-1:m.player.getId());
|
sources=collect.finish(config.maxSources,eye.x,eye.y,eye.z,m.player==null?-1:m.player.getId());
|
||||||
}
|
}
|
||||||
if(field.update(sources))view=new View(m.level,field.snapshot());
|
if(field.update(sources))view=new View(m.level,field.snapshot());
|
||||||
}
|
}
|
||||||
// Native renderer marks only resident sections; it neither loads chunks nor rebuilds the whole world.
|
// Native renderer marks only resident sections; it neither loads chunks nor rebuilds the whole world.
|
||||||
field.drain(config.sectionUpdatesPerTick,key->m.levelExtractor.setSectionDirty(
|
invalidatingSections=true;
|
||||||
DynamicLightField.sectionX(key),DynamicLightField.sectionY(key),DynamicLightField.sectionZ(key)));
|
try{field.drain(config.sectionUpdatesPerTick,key->m.levelExtractor.setSectionDirty(
|
||||||
|
DynamicLightField.sectionX(key),DynamicLightField.sectionY(key),DynamicLightField.sectionZ(key)));}
|
||||||
|
finally{invalidatingSections=false;}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
package fr.koka.sanctuary.client.shader;
|
||||||
|
|
||||||
|
import fr.koka.sanctuary.client.DynamicLightClient;
|
||||||
|
import fr.koka.sanctuary.lighting.DynamicLightField;
|
||||||
|
import java.util.*;
|
||||||
|
import net.minecraft.client.Minecraft;
|
||||||
|
import net.minecraft.world.phys.Vec3;
|
||||||
|
|
||||||
|
/** Same bounded sources as native dynamic lighting, with visual interpolation only. */
|
||||||
|
final class ColoredDynamicLights {
|
||||||
|
static final int LIMIT=128;
|
||||||
|
static final class Light {
|
||||||
|
double x,y,z,r,g,b,visibility;int level;boolean seen;
|
||||||
|
Light(DynamicLightField.Source s){x=s.x();y=s.y();z=s.z();level=s.light();r=(s.color()>>16&255)/255.;g=(s.color()>>8&255)/255.;b=(s.color()&255)/255.;}
|
||||||
|
}
|
||||||
|
private final Map<DynamicLightField.Id,Light> lights=new HashMap<>();
|
||||||
|
private long last;
|
||||||
|
List<Light> update(Vec3 camera){
|
||||||
|
long now=System.nanoTime();double dt=last==0?0:Math.min(.1,(now-last)/1e9);last=now;
|
||||||
|
double ease=1-Math.exp(-dt*14),fade=dt/.3;
|
||||||
|
lights.values().forEach(l->l.seen=false);
|
||||||
|
var mc=Minecraft.getInstance();
|
||||||
|
for(var s:DynamicLightClient.snapshot(mc.level).sources()){
|
||||||
|
Light l=lights.computeIfAbsent(s.id(),id->new Light(s));l.seen=true;l.level=s.light();
|
||||||
|
boolean own=!s.id().drop()&&mc.player!=null&&s.id().value()==mc.player.getId()
|
||||||
|
&&mc.getCameraEntity()==mc.player&&mc.options.getCameraType().isFirstPerson();
|
||||||
|
double x=own?camera.x:s.x(),y=own?camera.y:s.y(),z=own?camera.z:s.z();
|
||||||
|
double step=own?1:ease;l.x+=(x-l.x)*step;l.y+=(y-l.y)*step;l.z+=(z-l.z)*step;
|
||||||
|
l.r+=((s.color()>>16&255)/255.-l.r)*ease;l.g+=((s.color()>>8&255)/255.-l.g)*ease;l.b+=((s.color()&255)/255.-l.b)*ease;
|
||||||
|
}
|
||||||
|
for(var it=lights.values().iterator();it.hasNext();){var l=it.next();l.visibility=Math.clamp(l.visibility+(l.seen?fade:-fade),0,1);if(!l.seen&&l.visibility<=0)it.remove();}
|
||||||
|
return lights.values().stream().filter(l->camera.distanceToSqr(l.x,l.y,l.z)<Math.pow(VanillaLight.coloredLightDistance()+l.level,2))
|
||||||
|
.sorted(Comparator.comparingDouble(l->camera.distanceToSqr(l.x,l.y,l.z))).limit(LIMIT).toList();
|
||||||
|
}
|
||||||
|
Vec3 position(long entity){var l=lights.get(new DynamicLightField.Id(false,entity));return l==null?null:new Vec3(l.x,l.y,l.z);}
|
||||||
|
void clear(){lights.clear();last=0;}
|
||||||
|
}
|
||||||
+19
-4
@@ -10,15 +10,30 @@ import net.minecraft.world.level.block.state.BlockState;
|
|||||||
import net.minecraft.client.renderer.block.dispatch.BlockStateModelPart;
|
import net.minecraft.client.renderer.block.dispatch.BlockStateModelPart;
|
||||||
|
|
||||||
/** Colours are analysed once per state/resource reload, never from rendered GPU pixels. */
|
/** Colours are analysed once per state/resource reload, never from rendered GPU pixels. */
|
||||||
final class ColoredLightMaterials {
|
public final class ColoredLightMaterials {
|
||||||
private final Map<BlockState,Integer> colors=new IdentityHashMap<>();
|
private final Map<BlockState,Integer> colors=new IdentityHashMap<>();
|
||||||
private final Map<Identifier,Integer> sprites=new HashMap<>();
|
private final Map<Identifier,Integer> sprites=new HashMap<>();
|
||||||
int color(BlockState state){return colors.computeIfAbsent(state,this::analyse);}
|
private boolean warm;
|
||||||
|
public int color(BlockState state){if(warm!=VanillaLight.warmTorches()){warm=VanillaLight.warmTorches();clear();}return colors.computeIfAbsent(state,this::analyse);}
|
||||||
|
public int color(net.minecraft.world.item.ItemStack stack){
|
||||||
|
var state=fr.koka.sanctuary.cosmetics.HeadCosmeticData.state(stack);
|
||||||
|
if(state!=null)return color(state);
|
||||||
|
var item=stack.getItem();
|
||||||
|
if(item==net.minecraft.world.item.Items.LAVA_BUCKET)return 0xFF6020;
|
||||||
|
if(item==net.minecraft.world.item.Items.BLAZE_ROD||item==net.minecraft.world.item.Items.BLAZE_POWDER)return 0xFFAB55;
|
||||||
|
if(item==net.minecraft.world.item.Items.GLOWSTONE_DUST)return color(net.minecraft.world.level.block.Blocks.GLOWSTONE.defaultBlockState());
|
||||||
|
if(item==net.minecraft.world.item.Items.GLOW_BERRIES)return 0xFFBD52;
|
||||||
|
if(item==net.minecraft.world.item.Items.GLOW_INK_SAC||item==net.minecraft.world.item.Items.GLOW_ITEM_FRAME)return 0x66EADD;
|
||||||
|
return 0xFFFFFF;
|
||||||
|
}
|
||||||
private int analyse(BlockState state){
|
private int analyse(BlockState state){
|
||||||
var id=BuiltInRegistries.BLOCK.getKey(state.getBlock());String name=id.getPath();
|
var id=BuiltInRegistries.BLOCK.getKey(state.getBlock());String name=id.getPath();
|
||||||
if(id.getNamespace().equals("minecraft")){
|
if(id.getNamespace().equals("minecraft")){
|
||||||
|
if(name.contains("copper"))return 0x62FFA0;
|
||||||
if(name.startsWith("soul_"))return 0x55BDFF;
|
if(name.startsWith("soul_"))return 0x55BDFF;
|
||||||
if(name.contains("redstone"))return 0xFF3020;
|
if(name.contains("redstone"))return 0xFF0602;
|
||||||
|
if(name.equals("torch")||name.equals("wall_torch"))return VanillaLight.warmTorches()?0xFFAB55:0xFFFFFF;
|
||||||
|
if(name.equals("lava"))return 0xFF6020;
|
||||||
if(name.equals("pearlescent_froglight"))return 0xFFC0EC;
|
if(name.equals("pearlescent_froglight"))return 0xFFC0EC;
|
||||||
if(name.equals("verdant_froglight"))return 0xB4FF82;
|
if(name.equals("verdant_froglight"))return 0xB4FF82;
|
||||||
if(name.equals("ochre_froglight"))return 0xFFCD72;
|
if(name.equals("ochre_froglight"))return 0xFFCD72;
|
||||||
@@ -53,6 +68,6 @@ final class ColoredLightMaterials {
|
|||||||
float max=Math.max(1,Math.max(r,Math.max(g,b)));
|
float max=Math.max(1,Math.max(r,Math.max(g,b)));
|
||||||
return Math.round(r/max*255)<<16|Math.round(g/max*255)<<8|Math.round(b/max*255);
|
return Math.round(r/max*255)<<16|Math.round(g/max*255)<<8|Math.round(b/max*255);
|
||||||
}
|
}
|
||||||
void clear(){colors.clear();sprites.clear();}
|
public void clear(){colors.clear();sprites.clear();}
|
||||||
int cachedStates(){return colors.size();}
|
int cachedStates(){return colors.size();}
|
||||||
}
|
}
|
||||||
|
|||||||
+59
-41
@@ -15,93 +15,111 @@ import net.minecraft.world.level.chunk.status.ChunkStatus;
|
|||||||
|
|
||||||
/** Client-only RGB propagation. Snapshot, flood and packing share a bounded per-frame work budget. */
|
/** Client-only RGB propagation. Snapshot, flood and packing share a bounded per-frame work budget. */
|
||||||
final class ColoredLightVolume implements AutoCloseable {
|
final class ColoredLightVolume implements AutoCloseable {
|
||||||
static final int SIDE=64,COUNT=SIDE*SIDE*SIDE,ATLAS=512;
|
static int sideFor(int distance){return (distance+20)*2;}
|
||||||
|
static int columns(int side){return (int)Math.ceil(Math.sqrt(side));}
|
||||||
private static final Direction[] DIRECTIONS=Direction.values();
|
private static final Direction[] DIRECTIONS=Direction.values();
|
||||||
private final ColoredLightMaterials materials=new ColoredLightMaterials();
|
private final ColoredLightMaterials materials=new ColoredLightMaterials();
|
||||||
private ClientLevel level;
|
private ClientLevel level;
|
||||||
private Object models;
|
private Object models;
|
||||||
private Build build;
|
private Build build;
|
||||||
private DynamicTexture texture;
|
private DynamicTexture texture,previous;
|
||||||
|
private BlockPos previousOrigin;
|
||||||
|
private int side,previousSide;
|
||||||
|
private long transitionStarted;
|
||||||
|
float blend(){return Math.clamp((System.nanoTime()-transitionStarted)/300_000_000f,0,1);}
|
||||||
|
DynamicTexture previous(){return previous;}
|
||||||
|
BlockPos previousOrigin(){return previousOrigin;}
|
||||||
|
int previousSide(){return previousSide;}
|
||||||
|
int side(){return side;}
|
||||||
private BlockPos origin;
|
private BlockPos origin;
|
||||||
private long revision,published=-1,lastStart;
|
private long revision,published=-1,lastStart;
|
||||||
private int completed;
|
private int completed;
|
||||||
|
private boolean warm;
|
||||||
void update(){
|
void update(){
|
||||||
var mc=Minecraft.getInstance();var modelSet=mc.getModelManager().getBlockStateModelSet();
|
var mc=Minecraft.getInstance();var modelSet=mc.getModelManager().getBlockStateModelSet();
|
||||||
if(level!=mc.level||models!=modelSet){close();level=mc.level;models=modelSet;}
|
if(level!=mc.level||models!=modelSet){close();level=mc.level;models=modelSet;}
|
||||||
|
if(warm!=VanillaLight.warmTorches()){warm=VanillaLight.warmTorches();materials.clear();revision++;if(build!=null){build.close();build=null;}}
|
||||||
var p=mc.gameRenderer.gameRenderState().levelRenderState.cameraRenderState.pos;
|
var p=mc.gameRenderer.gameRenderState().levelRenderState.cameraRenderState.pos;
|
||||||
var wanted=new BlockPos(anchor(p.x),anchor(p.y),anchor(p.z));
|
int wantedSide=sideFor(VanillaLight.coloredLightDistance());
|
||||||
|
var wanted=new BlockPos(anchor(p.x,wantedSide),anchor(p.y,wantedSide),anchor(p.z,wantedSide));
|
||||||
long now=System.nanoTime();
|
long now=System.nanoTime();
|
||||||
|
if(previous!=null&&blend()>=1){previous.close();previous=null;previousOrigin=null;}
|
||||||
// Complete a snapshot even if the world keeps changing, then catch up; moving
|
// Complete a snapshot even if the world keeps changing, then catch up; moving
|
||||||
// beyond it cancels immediately. Static worlds do no further scanning or flooding.
|
// beyond it cancels immediately. Static worlds do no further scanning or flooding.
|
||||||
if(build!=null&&!inside(build.origin,BlockPos.containing(p))){build.close();build=null;}
|
if(build!=null&&(build.side!=wantedSide||!inside(build.origin,BlockPos.containing(p),build.side))){build.close();build=null;}
|
||||||
if(build==null&&(texture==null||!wanted.equals(origin)||published!=revision)&&now-lastStart>=100_000_000L){
|
if(build==null&&previous==null&&(texture==null||side!=wantedSide||!wanted.equals(origin)||published!=revision)&&now-lastStart>=100_000_000L){
|
||||||
build=new Build(level,wanted,revision,materials);lastStart=now;
|
build=new Build(level,wanted,wantedSide,revision,materials);lastStart=now;
|
||||||
}
|
}
|
||||||
if(build!=null&&build.advance(now+2_000_000L)){
|
if(build!=null&&build.advance(now+2_000_000L)){
|
||||||
if(texture==null)texture=new DynamicTexture(()->"Sanctuary coloured light volume",build.takeImage());
|
previous=texture;previousOrigin=origin;previousSide=side;
|
||||||
else {texture.setPixels(build.takeImage());texture.upload();}
|
texture=new DynamicTexture(()->"Sanctuary coloured light volume",build.takeImage());
|
||||||
|
side=build.side;transitionStarted=System.nanoTime();
|
||||||
origin=build.origin;published=build.revision;build.close();build=null;completed++;
|
origin=build.origin;published=build.revision;build.close();build=null;completed++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private static int anchor(double p){return Math.floorDiv((int)Math.floor(p),4)*4-SIDE/2;}
|
private static int anchor(double p,int side){return Math.floorDiv((int)Math.floor(p),4)*4-side/2;}
|
||||||
private static boolean inside(BlockPos base,BlockPos p){return p.getX()>=base.getX()&&p.getX()<base.getX()+SIDE&&p.getY()>=base.getY()&&p.getY()<base.getY()+SIDE&&p.getZ()>=base.getZ()&&p.getZ()<base.getZ()+SIDE;}
|
private static boolean inside(BlockPos base,BlockPos p,int side){return p.getX()>=base.getX()&&p.getX()<base.getX()+side&&p.getY()>=base.getY()&&p.getY()<base.getY()+side&&p.getZ()>=base.getZ()&&p.getZ()<base.getZ()+side;}
|
||||||
void dirty(BlockPos p){if(origin!=null&&inside(origin,p)||build!=null&&inside(build.origin,p))revision++;}
|
void dirty(BlockPos p){if(origin!=null&&inside(origin,p,side)||build!=null&&inside(build.origin,p,build.side))revision++;}
|
||||||
void dirtyChunk(int x,int z){if(overlaps(origin,x,z)||build!=null&&overlaps(build.origin,x,z))revision++;}
|
void dirtyChunk(int x,int z){if(overlaps(origin,x,z,side)||build!=null&&overlaps(build.origin,x,z,build.side))revision++;}
|
||||||
void dirtySection(int x,int y,int z){if(overlapsSection(origin,x,y,z)||build!=null&&overlapsSection(build.origin,x,y,z))revision++;}
|
void dirtySection(int x,int y,int z){if(overlapsSection(origin,x,y,z,side)||build!=null&&overlapsSection(build.origin,x,y,z,build.side))revision++;}
|
||||||
private static boolean overlapsSection(BlockPos o,int x,int y,int z){return overlaps(o,x,z)&&y>=(o.getY()>>4)&&y<=((o.getY()+SIDE-1)>>4);}
|
private static boolean overlapsSection(BlockPos o,int x,int y,int z,int side){return overlaps(o,x,z,side)&&y>=(o.getY()>>4)&&y<=((o.getY()+side-1)>>4);}
|
||||||
private static boolean overlaps(BlockPos o,int x,int z){return o!=null&&x>=(o.getX()>>4)&&x<=((o.getX()+SIDE-1)>>4)&&z>=(o.getZ()>>4)&&z<=((o.getZ()+SIDE-1)>>4);}
|
private static boolean overlaps(BlockPos o,int x,int z,int side){return o!=null&&x>=(o.getX()>>4)&&x<=((o.getX()+side-1)>>4)&&z>=(o.getZ()>>4)&&z<=((o.getZ()+side-1)>>4);}
|
||||||
DynamicTexture texture(){return texture;}
|
DynamicTexture texture(){return texture;}
|
||||||
BlockPos origin(){return origin;}
|
BlockPos origin(){return origin;}
|
||||||
boolean settled(){return texture!=null&&build==null&&revision==published;}
|
boolean settled(){return texture!=null&&build==null&&revision==published&&blend()>=1;}
|
||||||
int completed(){return completed;}
|
int completed(){return completed;}
|
||||||
int cachedStates(){return materials.cachedStates();}
|
int cachedStates(){return materials.cachedStates();}
|
||||||
int sample(BlockPos p){if(texture==null||!inside(origin,p))return 0;int x=p.getX()-origin.getX(),y=p.getY()-origin.getY(),z=p.getZ()-origin.getZ();return texture.getPixels().getPixel(x+(y&7)*SIDE,z+(y>>3)*SIDE);}
|
int sample(BlockPos p){if(texture==null||!inside(origin,p,side))return 0;int x=p.getX()-origin.getX(),y=p.getY()-origin.getY(),z=p.getZ()-origin.getZ(),cols=columns(side);return texture.getPixels().getPixel(x+(y%cols)*side,z+(y/cols)*side);}
|
||||||
public void close(){if(build!=null){build.close();build=null;}if(texture!=null){texture.close();texture=null;}materials.clear();level=null;models=null;origin=null;published=-1;revision=0;lastStart=0;}
|
public void close(){if(build!=null){build.close();build=null;}if(texture!=null){texture.close();texture=null;}if(previous!=null){previous.close();previous=null;}materials.clear();level=null;models=null;origin=null;previousOrigin=null;side=previousSide=0;published=-1;revision=0;lastStart=0;}
|
||||||
|
|
||||||
private static final class Build implements AutoCloseable {
|
private static final class Build implements AutoCloseable {
|
||||||
|
final int side,count,cols,chunkSide;
|
||||||
final ClientLevel level;final BlockPos origin;final long revision;final ColoredLightMaterials materials;
|
final ClientLevel level;final BlockPos origin;final long revision;final ColoredLightMaterials materials;
|
||||||
final BlockState[] states=new BlockState[COUNT];
|
final BlockState[] states;
|
||||||
final byte[] red=new byte[COUNT],green=new byte[COUNT],blue=new byte[COUNT],sky=new byte[COUNT];
|
final byte[] red,green,blue,sky;
|
||||||
final boolean[] queued=new boolean[COUNT];final IntArrayFIFOQueue queue=new IntArrayFIFOQueue();
|
final boolean[] queued;final IntArrayFIFOQueue queue=new IntArrayFIFOQueue();
|
||||||
final LevelChunk[] chunks=new LevelChunk[25];final int cx,cz;
|
final LevelChunk[] chunks;final int cx,cz;
|
||||||
final BlockPos.MutableBlockPos pos=new BlockPos.MutableBlockPos();
|
final BlockPos.MutableBlockPos pos=new BlockPos.MutableBlockPos();
|
||||||
NativeImage image=new NativeImage(ATLAS,ATLAS,false);int scan,packed;
|
NativeImage image;int scan,packed;
|
||||||
Build(ClientLevel level,BlockPos origin,long revision,ColoredLightMaterials materials){
|
Build(ClientLevel level,BlockPos origin,int side,long revision,ColoredLightMaterials materials){
|
||||||
|
this.side=side;count=side*side*side;cols=columns(side);chunkSide=(side+15)/16+1;
|
||||||
|
states=new BlockState[count];red=new byte[count];green=new byte[count];blue=new byte[count];sky=new byte[count];queued=new boolean[count];chunks=new LevelChunk[chunkSide*chunkSide];
|
||||||
|
image=new NativeImage(cols*side,((side+cols-1)/cols)*side,false);
|
||||||
this.level=level;this.origin=origin;this.revision=revision;this.materials=materials;
|
this.level=level;this.origin=origin;this.revision=revision;this.materials=materials;
|
||||||
cx=origin.getX()>>4;cz=origin.getZ()>>4;
|
cx=origin.getX()>>4;cz=origin.getZ()>>4;
|
||||||
for(int z=0;z<5;z++)for(int x=0;x<5;x++)chunks[x+z*5]=level.getChunkSource().getChunk(cx+x,cz+z,ChunkStatus.FULL,false);
|
for(int z=0;z<chunkSide;z++)for(int x=0;x<chunkSide;x++)chunks[x+z*chunkSide]=level.getChunkSource().getChunk(cx+x,cz+z,ChunkStatus.FULL,false);
|
||||||
}
|
}
|
||||||
boolean advance(long deadline){
|
boolean advance(long deadline){
|
||||||
while(scan<COUNT){scan(scan++);if((scan&127)==0&&System.nanoTime()>=deadline)return false;}
|
while(scan<count){scan(scan++);if((scan&127)==0&&System.nanoTime()>=deadline)return false;}
|
||||||
int work=0;
|
int work=0;
|
||||||
while(!queue.isEmpty()){
|
while(!queue.isEmpty()){
|
||||||
int i=queue.dequeueInt();queued[i]=false;spread(i);
|
int i=queue.dequeueInt();queued[i]=false;spread(i);
|
||||||
if((++work&127)==0&&System.nanoTime()>=deadline)return false;
|
if((++work&127)==0&&System.nanoTime()>=deadline)return false;
|
||||||
}
|
}
|
||||||
while(packed<COUNT){
|
while(packed<count){
|
||||||
int i=packed++,x=i&63,z=i>>6&63,y=i>>12;
|
int i=packed++,x=i%side,z=i/side%side,y=i/(side*side);
|
||||||
image.setPixel(x+(y&7)*SIDE,z+(y>>3)*SIDE,(sky[i]&255)<<24|(red[i]&255)<<16|(green[i]&255)<<8|(blue[i]&255));
|
image.setPixel(x+(y%cols)*side,z+(y/cols)*side,(sky[i]&255)<<24|(red[i]&255)<<16|(green[i]&255)<<8|(blue[i]&255));
|
||||||
if((packed&511)==0&&System.nanoTime()>=deadline)return false;
|
if((packed&511)==0&&System.nanoTime()>=deadline)return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
void scan(int i){
|
void scan(int i){
|
||||||
int x=origin.getX()+(i&63),z=origin.getZ()+(i>>6&63),y=origin.getY()+(i>>12);pos.set(x,y,z);
|
int x=origin.getX()+(i%side),z=origin.getZ()+(i/side%side),y=origin.getY()+(i/(side*side));pos.set(x,y,z);
|
||||||
var chunk=chunks[((x>>4)-cx)+((z>>4)-cz)*5];
|
var chunk=chunks[((x>>4)-cx)+((z>>4)-cz)*chunkSide];
|
||||||
var state=chunk==null||level.isOutsideBuildHeight(y)?Blocks.BEDROCK.defaultBlockState():chunk.getBlockState(pos);states[i]=state;
|
var state=chunk==null||level.isOutsideBuildHeight(y)?Blocks.BEDROCK.defaultBlockState():chunk.getBlockState(pos);states[i]=state;
|
||||||
if(chunk!=null&&!level.isOutsideBuildHeight(y))sky[i]=(byte)(level.getBrightness(LightLayer.SKY,pos)*17);
|
if(chunk!=null&&!level.isOutsideBuildHeight(y))sky[i]=(byte)(level.getBrightness(LightLayer.SKY,pos)*17);
|
||||||
int emission=state.getLightEmission();if(emission<=0)return;
|
int emission=state.getLightEmission();if(emission<=0)return;
|
||||||
int c=materials.color(state);red[i]=(byte)encode(emission,c>>16&255);green[i]=(byte)encode(emission,c>>8&255);blue[i]=(byte)encode(emission,c&255);enqueue(i);
|
int c=materials.color(state);red[i]=(byte)encode(emission,c>>16&255);green[i]=(byte)encode(emission,c>>8&255);blue[i]=(byte)encode(emission,c&255);enqueue(i);
|
||||||
}
|
}
|
||||||
void spread(int i){
|
void spread(int i){
|
||||||
int r=red[i]&255,g=green[i]&255,b=blue[i]&255;if(Math.max(r,Math.max(g,b))<=16)return;
|
int r=red[i]&255,g=green[i]&255,b=blue[i]&255;if(Math.max(r,Math.max(g,b))<=136)return;
|
||||||
int x=i&63,z=i>>6&63,y=i>>12;
|
int x=i%side,z=i/side%side,y=i/(side*side);
|
||||||
for(var d:DIRECTIONS){
|
for(var d:DIRECTIONS){
|
||||||
int nx=x+d.getStepX(),ny=y+d.getStepY(),nz=z+d.getStepZ();if(nx<0||nx>=SIDE||ny<0||ny>=SIDE||nz<0||nz>=SIDE)continue;
|
int nx=x+d.getStepX(),ny=y+d.getStepY(),nz=z+d.getStepZ();if(nx<0||nx>=side||ny<0||ny>=side||nz<0||nz>=side)continue;
|
||||||
int n=nx+(nz<<6)+(ny<<12);
|
int n=nx+nz*side+ny*side*side;
|
||||||
if(r-16<=(red[n]&255)&&g-16<=(green[n]&255)&&b-16<=(blue[n]&255))continue;
|
if(r-8<=(red[n]&255)&&g-8<=(green[n]&255)&&b-8<=(blue[n]&255))continue;
|
||||||
int loss=net.minecraft.world.level.lighting.LightEngine.getLightDampeningInto(states[i],states[n],d,Math.max(1,states[n].getLightDampening()))*16;
|
int loss=net.minecraft.world.level.lighting.LightEngine.getLightDampeningInto(states[i],states[n],d,Math.max(1,states[n].getLightDampening()))*8;
|
||||||
int nr=Math.max(0,r-loss),ng=Math.max(0,g-loss),nb=Math.max(0,b-loss);boolean changed=false;
|
int nr=Math.max(0,r-loss),ng=Math.max(0,g-loss),nb=Math.max(0,b-loss);if(Math.max(nr,Math.max(ng,nb))<=128)continue;boolean changed=false;
|
||||||
if(nr>(red[n]&255)){red[n]=(byte)nr;changed=true;}if(ng>(green[n]&255)){green[n]=(byte)ng;changed=true;}if(nb>(blue[n]&255)){blue[n]=(byte)nb;changed=true;}
|
if(nr>(red[n]&255)){red[n]=(byte)nr;changed=true;}if(ng>(green[n]&255)){green[n]=(byte)ng;changed=true;}if(nb>(blue[n]&255)){blue[n]=(byte)nb;changed=true;}
|
||||||
if(changed)enqueue(n);
|
if(changed)enqueue(n);
|
||||||
}
|
}
|
||||||
@@ -110,7 +128,7 @@ final class ColoredLightVolume implements AutoCloseable {
|
|||||||
NativeImage takeImage(){var result=image;image=null;return result;}
|
NativeImage takeImage(){var result=image;image=null;return result;}
|
||||||
public void close(){if(image!=null){image.close();image=null;}}
|
public void close(){if(image!=null){image.close();image=null;}}
|
||||||
}
|
}
|
||||||
// Logarithmic channels keep the source hue as light loses one native level per
|
// An offset preserves dim colour channels below native level zero. The shader
|
||||||
// block. Independent maxima mix overlapping colours without summing unbounded light.
|
// applies the native cutoff to overall energy, never to each channel separately.
|
||||||
static int encode(int emission,int channel){return channel==0?0:Math.clamp((int)Math.round((emission+Math.log(channel/255.0)/Math.log(1.25))*16),0,240);}
|
static int encode(int emission,int channel){return channel==0?0:Math.clamp((int)Math.round((emission+16+Math.log(channel/255.0)/Math.log(1.25))*8),0,248);}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,13 +25,19 @@ public final class ColoredLights {
|
|||||||
.withBindGroupLayout(BindGroupLayout.builder().withUniform("ColoredLightScene",UniformType.UNIFORM_BUFFER)
|
.withBindGroupLayout(BindGroupLayout.builder().withUniform("ColoredLightScene",UniformType.UNIFORM_BUFFER)
|
||||||
.withUniform("WorldDepth",UniformType.COMBINED_IMAGE_SAMPLER)
|
.withUniform("WorldDepth",UniformType.COMBINED_IMAGE_SAMPLER)
|
||||||
.withUniform("SceneColor",UniformType.COMBINED_IMAGE_SAMPLER)
|
.withUniform("SceneColor",UniformType.COMBINED_IMAGE_SAMPLER)
|
||||||
.withUniform("LightVolume",UniformType.COMBINED_IMAGE_SAMPLER).build())
|
.withUniform("LightVolume",UniformType.COMBINED_IMAGE_SAMPLER)
|
||||||
|
.withUniform("PreviousVolume",UniformType.COMBINED_IMAGE_SAMPLER).build())
|
||||||
.withPrimitiveTopology(PrimitiveTopology.TRIANGLES).withCull(false)
|
.withPrimitiveTopology(PrimitiveTopology.TRIANGLES).withCull(false)
|
||||||
.withColorTargetState(new ColorTargetState(Optional.of(new BlendFunction(BlendFactor.ONE,BlendFactor.ZERO,BlendFactor.ZERO,BlendFactor.ONE)),GpuFormat.RGBA8_UNORM,ColorTargetState.WRITE_COLOR))
|
.withColorTargetState(new ColorTargetState(Optional.of(new BlendFunction(BlendFactor.ONE,BlendFactor.ZERO,BlendFactor.ZERO,BlendFactor.ONE)),GpuFormat.RGBA8_UNORM,ColorTargetState.WRITE_COLOR))
|
||||||
.withDepthStencilState(Optional.empty()).build());
|
.withDepthStencilState(Optional.empty()).build());
|
||||||
private static final ColoredLightVolume VOLUME=new ColoredLightVolume();
|
private static final ColoredLightVolume VOLUME=new ColoredLightVolume();
|
||||||
private static final Matrix4f projection=new Matrix4f();
|
private static final Matrix4f projection=new Matrix4f();
|
||||||
private static TextureTarget source;
|
private static TextureTarget source;
|
||||||
|
private static net.minecraft.client.renderer.texture.DynamicTexture black;
|
||||||
|
private static final ColoredDynamicLights DYNAMIC=new ColoredDynamicLights();
|
||||||
|
private static long activated,lastRender;
|
||||||
|
private static float displayedDistance;
|
||||||
|
private static int dynamicCount;
|
||||||
private static MappableRingBuffer uniforms;
|
private static MappableRingBuffer uniforms;
|
||||||
private static boolean applied;
|
private static boolean applied;
|
||||||
private static int frames;
|
private static int frames;
|
||||||
@@ -39,40 +45,58 @@ public final class ColoredLights {
|
|||||||
public static void captureProjection(Matrix4f matrix){projection.set(matrix);}
|
public static void captureProjection(Matrix4f matrix){projection.set(matrix);}
|
||||||
public static void dirtyBlock(BlockPos pos){VOLUME.dirty(pos);}
|
public static void dirtyBlock(BlockPos pos){VOLUME.dirty(pos);}
|
||||||
public static void dirtyChunk(int x,int z){VOLUME.dirtyChunk(x,z);}
|
public static void dirtyChunk(int x,int z){VOLUME.dirtyChunk(x,z);}
|
||||||
public static void dirtySection(int x,int y,int z){VOLUME.dirtySection(x,y,z);}
|
public static void dirtySection(int x,int y,int z){if(!fr.koka.sanctuary.client.DynamicLightClient.invalidatingSections())VOLUME.dirtySection(x,y,z);}
|
||||||
public static void render(){
|
public static void render(){
|
||||||
applied=false;var mc=Minecraft.getInstance();
|
applied=false;var mc=Minecraft.getInstance();
|
||||||
if(!VanillaLight.active()||!VanillaLight.coloredLights()||VanillaLight.coloredLightIntensity()==0||mc.level==null){release();return;}
|
if(!VanillaLight.active()||!VanillaLight.coloredLights()||VanillaLight.coloredLightIntensity()==0||mc.level==null){release();return;}
|
||||||
if(PanoramaCapture.rendering())return;
|
if(PanoramaCapture.rendering())return;
|
||||||
var camera=mc.gameRenderer.gameRenderState().levelRenderState.cameraRenderState;
|
var camera=mc.gameRenderer.gameRenderState().levelRenderState.cameraRenderState;
|
||||||
if(camera.fogType!=FogType.NONE)return;
|
if(camera.fogType!=FogType.NONE)return;
|
||||||
VOLUME.update();if(VOLUME.texture()==null)return;
|
long now=System.nanoTime();if(activated==0){activated=now;displayedDistance=VanillaLight.coloredLightDistance();}
|
||||||
|
double elapsed=lastRender==0?0:Math.min(.1,(now-lastRender)/1e9);lastRender=now;
|
||||||
|
displayedDistance+=(VanillaLight.coloredLightDistance()-displayedDistance)*(float)(1-Math.exp(-elapsed*10));
|
||||||
|
VOLUME.update();
|
||||||
|
if(black==null){var image=new com.mojang.blaze3d.platform.NativeImage(1,1,false);image.setPixel(0,0,0);black=new net.minecraft.client.renderer.texture.DynamicTexture(()->"Empty coloured light",image);}
|
||||||
|
var lights=DYNAMIC.update(camera.pos);dynamicCount=lights.size();
|
||||||
|
var current=VOLUME.texture()==null?black:VOLUME.texture();
|
||||||
|
var previous=VOLUME.previous()==null?black:VOLUME.previous();
|
||||||
var main=mc.gameRenderer.mainRenderTarget();
|
var main=mc.gameRenderer.mainRenderTarget();
|
||||||
if(source==null||source.width!=main.width||source.height!=main.height){if(source!=null)source.destroyBuffers();source=new TextureTarget("Sanctuary coloured light source",main.width,main.height,GpuFormat.RGBA8_UNORM,null);}
|
if(source==null||source.width!=main.width||source.height!=main.height){if(source!=null)source.destroyBuffers();source=new TextureTarget("Sanctuary coloured light source",main.width,main.height,GpuFormat.RGBA8_UNORM,null);}
|
||||||
if(uniforms==null)uniforms=new MappableRingBuffer(()->"Sanctuary coloured light scene",GpuBuffer.USAGE_UNIFORM|GpuBuffer.USAGE_MAP_WRITE,128);
|
if(uniforms==null)uniforms=new MappableRingBuffer(()->"Sanctuary coloured light scene",GpuBuffer.USAGE_UNIFORM|GpuBuffer.USAGE_MAP_WRITE,176+ColoredDynamicLights.LIMIT*32);
|
||||||
var base=VOLUME.origin();var fog=camera.fogData;
|
var base=VOLUME.origin()==null?BlockPos.containing(camera.pos):VOLUME.origin();var fog=camera.fogData;
|
||||||
|
var old=VOLUME.previousOrigin()==null?base:VOLUME.previousOrigin();
|
||||||
|
int side=VOLUME.texture()==null?1:VOLUME.side(),oldSide=VOLUME.previous()==null?1:VOLUME.previousSide();
|
||||||
uniforms.rotate();try(var mapped=uniforms.currentBuffer().map(false,true)){
|
uniforms.rotate();try(var mapped=uniforms.currentBuffer().map(false,true)){
|
||||||
Std140Builder.intoBuffer(mapped.data()).putMat4f(new Matrix4f(projection).mul(camera.viewRotationMatrix).invert())
|
var data=Std140Builder.intoBuffer(mapped.data()).putMat4f(new Matrix4f(projection).mul(camera.viewRotationMatrix).invert())
|
||||||
.putVec4((float)(camera.pos.x-base.getX()),(float)(camera.pos.y-base.getY()),(float)(camera.pos.z-base.getZ()),VanillaLight.coloredLightIntensity()/100f)
|
.putVec4((float)(camera.pos.x-base.getX()),(float)(camera.pos.y-base.getY()),(float)(camera.pos.z-base.getZ()),VanillaLight.coloredLightIntensity()/100f)
|
||||||
.putVec4(RenderSystem.getDevice().getDeviceInfo().isZZeroToOne()?1:0,Math.clamp((15-mc.level.getSkyDarken())/15f,0,1),fog.color.w,0)
|
.putVec4(RenderSystem.getDevice().getDeviceInfo().isZZeroToOne()?1:0,Math.clamp((15-mc.level.getSkyDarken())/15f,0,1),fog.color.w,0)
|
||||||
.putVec4(fog.renderDistanceStart,fog.renderDistanceEnd,fog.environmentalStart,fog.environmentalEnd)
|
.putVec4(fog.renderDistanceStart,fog.renderDistanceEnd,fog.environmentalStart,fog.environmentalEnd)
|
||||||
.putVec4(ColoredLightVolume.SIDE,14,4,0);
|
.putVec4(displayedDistance,VOLUME.texture()==null?0:VOLUME.blend(),Math.clamp((now-activated)/300_000_000f,0,1),lights.size())
|
||||||
|
.putVec4(side,ColoredLightVolume.columns(side),current.getPixels().getWidth(),current.getPixels().getHeight())
|
||||||
|
.putVec4((float)(camera.pos.x-old.getX()),(float)(camera.pos.y-old.getY()),(float)(camera.pos.z-old.getZ()),VOLUME.previous()==null?0:1)
|
||||||
|
.putVec4(oldSide,ColoredLightVolume.columns(oldSide),previous.getPixels().getWidth(),previous.getPixels().getHeight());
|
||||||
|
for(int i=0;i<ColoredDynamicLights.LIMIT;i++){if(i<lights.size()){var l=lights.get(i);data.putVec4((float)(l.x-camera.pos.x),(float)(l.y-camera.pos.y),(float)(l.z-camera.pos.z),l.level);}else data.putVec4(0,0,0,0);}
|
||||||
|
for(int i=0;i<ColoredDynamicLights.LIMIT;i++){if(i<lights.size()){var l=lights.get(i);data.putVec4((float)l.r,(float)l.g,(float)l.b,(float)l.visibility);}else data.putVec4(0,0,0,0);}
|
||||||
}
|
}
|
||||||
var encoder=RenderSystem.getDevice().createCommandEncoder();
|
var encoder=RenderSystem.getDevice().createCommandEncoder();
|
||||||
encoder.copyTextureToTexture(main.getColorTexture(),source.getColorTexture(),0,0,0,0,0,main.width,main.height);
|
encoder.copyTextureToTexture(main.getColorTexture(),source.getColorTexture(),0,0,0,0,0,main.width,main.height);
|
||||||
var nearest=RenderSystem.getSamplerCache().getClampToEdge(FilterMode.NEAREST);
|
var nearest=RenderSystem.getSamplerCache().getClampToEdge(FilterMode.NEAREST);
|
||||||
try(var pass=encoder.createRenderPass(()->"Sanctuary coloured light",main.getColorTextureView(),Optional.empty())){
|
try(var pass=encoder.createRenderPass(()->"Sanctuary coloured light",main.getColorTextureView(),Optional.empty())){
|
||||||
pass.setPipeline(RenderSystem.getCompiledPipeline(APPLY));pass.setUniform("ColoredLightScene",uniforms.currentBuffer());
|
pass.setPipeline(RenderSystem.getCompiledPipeline(APPLY));pass.setUniform("ColoredLightScene",uniforms.currentBuffer());
|
||||||
pass.setUniform("WorldDepth",main.getDepthTextureView(),nearest);pass.setUniform("SceneColor",source.getColorTextureView(),nearest);pass.setUniform("LightVolume",VOLUME.texture().getTextureView(),nearest);pass.draw(3,1,0,0);
|
pass.setUniform("WorldDepth",main.getDepthTextureView(),nearest);pass.setUniform("SceneColor",source.getColorTextureView(),nearest);pass.setUniform("LightVolume",current.getTextureView(),nearest);pass.setUniform("PreviousVolume",previous.getTextureView(),nearest);pass.draw(3,1,0,0);
|
||||||
}
|
}
|
||||||
applied=true;frames++;
|
applied=true;frames++;
|
||||||
}
|
}
|
||||||
public static void release(){VOLUME.close();if(source!=null){source.destroyBuffers();source=null;}if(uniforms!=null){uniforms.close();uniforms=null;}applied=false;}
|
public static void release(){VOLUME.close();DYNAMIC.clear();activated=lastRender=0;dynamicCount=0;if(black!=null){black.close();black=null;}if(source!=null){source.destroyBuffers();source=null;}if(uniforms!=null){uniforms.close();uniforms=null;}applied=false;}
|
||||||
static boolean applied(){return applied;}
|
static boolean applied(){return applied;}
|
||||||
static boolean allocated(){return source!=null||VOLUME.texture()!=null;}
|
static boolean allocated(){return source!=null||VOLUME.texture()!=null;}
|
||||||
static boolean settled(){return VOLUME.settled();}
|
static boolean settled(){return VOLUME.settled();}
|
||||||
static int renderedFrames(){return frames;}
|
static int renderedFrames(){return frames;}
|
||||||
static int builds(){return VOLUME.completed();}
|
static int builds(){return VOLUME.completed();}
|
||||||
static int sample(BlockPos pos){return VOLUME.sample(pos);}
|
static int sample(BlockPos pos){return VOLUME.sample(pos);}
|
||||||
|
static net.minecraft.world.phys.Vec3 dynamicPosition(long entity){return DYNAMIC.position(entity);}
|
||||||
|
static int dynamicCount(){return dynamicCount;}
|
||||||
|
static float blend(){return VOLUME.blend();}
|
||||||
|
static int volumeSide(){return VOLUME.side();}
|
||||||
static int materials(){return VOLUME.cachedStates();}
|
static int materials(){return VOLUME.cachedStates();}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,12 @@ public final class ShaderOptionsScreen extends OptionsSubScreen {
|
|||||||
slider("pbr_intensity",VanillaLight.pbrIntensity(),VanillaLight::setPbrIntensity);
|
slider("pbr_intensity",VanillaLight.pbrIntensity(),VanillaLight::setPbrIntensity);
|
||||||
list.addBig(OptionInstance.createBoolean("sanctuary.shaders.colored_lights",OptionInstance.cachedConstantTooltip(
|
list.addBig(OptionInstance.createBoolean("sanctuary.shaders.colored_lights",OptionInstance.cachedConstantTooltip(
|
||||||
Component.translatable("sanctuary.shaders.colored_lights_help")),VanillaLight.coloredLights(),VanillaLight::setColoredLights));
|
Component.translatable("sanctuary.shaders.colored_lights_help")),VanillaLight.coloredLights(),VanillaLight::setColoredLights));
|
||||||
|
list.addBig(OptionInstance.createBoolean("sanctuary.shaders.warm_torches",OptionInstance.cachedConstantTooltip(
|
||||||
|
Component.translatable("sanctuary.shaders.warm_torches_help")),VanillaLight.warmTorches(),VanillaLight::setWarmTorches));
|
||||||
slider("colored_light_intensity",VanillaLight.coloredLightIntensity(),VanillaLight::setColoredLightIntensity);
|
slider("colored_light_intensity",VanillaLight.coloredLightIntensity(),VanillaLight::setColoredLightIntensity);
|
||||||
|
list.addBig(new OptionInstance<Integer>("sanctuary.shaders.colored_light_distance",OptionInstance.cachedConstantTooltip(
|
||||||
|
Component.translatable("sanctuary.shaders.colored_light_distance_help")),(caption,value)->Component.translatable("sanctuary.shaders.colored_light_distance_value",value),
|
||||||
|
new OptionInstance.Enum<>(List.of(8,16,32,64),Codec.INT),VanillaLight.coloredLightDistance(),VanillaLight::setColoredLightDistance));
|
||||||
list.addBig(OptionInstance.createBoolean("sanctuary.shaders.ssgi",OptionInstance.cachedConstantTooltip(
|
list.addBig(OptionInstance.createBoolean("sanctuary.shaders.ssgi",OptionInstance.cachedConstantTooltip(
|
||||||
Component.translatable("sanctuary.shaders.ssgi_help")),VanillaLight.ssgi(),VanillaLight::setSsgi));
|
Component.translatable("sanctuary.shaders.ssgi_help")),VanillaLight.ssgi(),VanillaLight::setSsgi));
|
||||||
slider("ssgi_intensity",VanillaLight.ssgiIntensity(),VanillaLight::setSsgiIntensity);
|
slider("ssgi_intensity",VanillaLight.ssgiIntensity(),VanillaLight::setSsgiIntensity);
|
||||||
|
|||||||
@@ -26,8 +26,10 @@ public final class VanillaLight {
|
|||||||
boolean pbr=true;
|
boolean pbr=true;
|
||||||
int pbrIntensity=50;
|
int pbrIntensity=50;
|
||||||
boolean ssgi=true;
|
boolean ssgi=true;
|
||||||
boolean coloredLights=false;
|
boolean coloredLights=true;
|
||||||
int coloredLightIntensity=35;
|
boolean warmTorches=false;
|
||||||
|
int coloredLightIntensity=30;
|
||||||
|
int coloredLightDistance=16;
|
||||||
int ssgiIntensity=35;
|
int ssgiIntensity=35;
|
||||||
int bloomIntensity=20;
|
int bloomIntensity=20;
|
||||||
int bloomRadius=50;
|
int bloomRadius=50;
|
||||||
@@ -49,6 +51,7 @@ public final class VanillaLight {
|
|||||||
preferences.lensFlareIntensity=Math.clamp(preferences.lensFlareIntensity,0,100);
|
preferences.lensFlareIntensity=Math.clamp(preferences.lensFlareIntensity,0,100);
|
||||||
preferences.pbrIntensity=Math.clamp(preferences.pbrIntensity,0,100);
|
preferences.pbrIntensity=Math.clamp(preferences.pbrIntensity,0,100);
|
||||||
preferences.coloredLightIntensity=Math.clamp(preferences.coloredLightIntensity,0,100);
|
preferences.coloredLightIntensity=Math.clamp(preferences.coloredLightIntensity,0,100);
|
||||||
|
preferences.coloredLightDistance=normalizeColoredLightDistance(preferences.coloredLightDistance);
|
||||||
preferences.ssgiIntensity=Math.clamp(preferences.ssgiIntensity,0,100);
|
preferences.ssgiIntensity=Math.clamp(preferences.ssgiIntensity,0,100);
|
||||||
preferences.bloomIntensity=Math.clamp(preferences.bloomIntensity,0,100);
|
preferences.bloomIntensity=Math.clamp(preferences.bloomIntensity,0,100);
|
||||||
preferences.bloomRadius=Math.clamp(preferences.bloomRadius,0,100);
|
preferences.bloomRadius=Math.clamp(preferences.bloomRadius,0,100);
|
||||||
@@ -82,10 +85,15 @@ public final class VanillaLight {
|
|||||||
public static int pbrIntensity(){enabled();return preferences.pbrIntensity;}
|
public static int pbrIntensity(){enabled();return preferences.pbrIntensity;}
|
||||||
public static void setPbr(boolean value){enabled();preferences.pbr=value;save();}
|
public static void setPbr(boolean value){enabled();preferences.pbr=value;save();}
|
||||||
public static void setPbrIntensity(int value){enabled();preferences.pbrIntensity=Math.clamp(value,0,100);save();}
|
public static void setPbrIntensity(int value){enabled();preferences.pbrIntensity=Math.clamp(value,0,100);save();}
|
||||||
|
public static boolean warmTorches(){enabled();return preferences.warmTorches;}
|
||||||
|
public static void setWarmTorches(boolean value){enabled();preferences.warmTorches=value;save();}
|
||||||
public static boolean coloredLights(){enabled();return preferences.coloredLights;}
|
public static boolean coloredLights(){enabled();return preferences.coloredLights;}
|
||||||
public static int coloredLightIntensity(){enabled();return preferences.coloredLightIntensity;}
|
public static int coloredLightIntensity(){enabled();return preferences.coloredLightIntensity;}
|
||||||
public static void setColoredLights(boolean value){enabled();preferences.coloredLights=value;save();}
|
public static void setColoredLights(boolean value){enabled();preferences.coloredLights=value;save();}
|
||||||
public static void setColoredLightIntensity(int value){enabled();preferences.coloredLightIntensity=Math.clamp(value,0,100);save();}
|
public static void setColoredLightIntensity(int value){enabled();preferences.coloredLightIntensity=Math.clamp(value,0,100);save();}
|
||||||
|
public static int coloredLightDistance(){enabled();return preferences.coloredLightDistance;}
|
||||||
|
public static void setColoredLightDistance(int value){enabled();preferences.coloredLightDistance=normalizeColoredLightDistance(value);save();}
|
||||||
|
static int normalizeColoredLightDistance(int value){return value<12?8:value<24?16:value<48?32:64;}
|
||||||
public static boolean ssgi(){enabled();return preferences.ssgi;}
|
public static boolean ssgi(){enabled();return preferences.ssgi;}
|
||||||
public static int ssgiIntensity(){enabled();return preferences.ssgiIntensity;}
|
public static int ssgiIntensity(){enabled();return preferences.ssgiIntensity;}
|
||||||
public static void setSsgi(boolean value){enabled();preferences.ssgi=value;save();}
|
public static void setSsgi(boolean value){enabled();preferences.ssgi=value;save();}
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import java.util.function.LongConsumer;
|
|||||||
/** Immutable render snapshots; entity collection and the dirty queue belong to the client tick. */
|
/** Immutable render snapshots; entity collection and the dirty queue belong to the client tick. */
|
||||||
public final class DynamicLightField {
|
public final class DynamicLightField {
|
||||||
public record Id(boolean drop, long value) {}
|
public record Id(boolean drop, long value) {}
|
||||||
public record Source(Id id, double x, double y, double z, int light) {
|
public record Source(Id id, double x, double y, double z, int light, int color) {
|
||||||
|
public Source(Id id,double x,double y,double z,int light){this(id,x,y,z,light,0xFFFFFF);}
|
||||||
public Source {
|
public Source {
|
||||||
if (!Double.isFinite(x+y+z) || light<1 || light>15) throw new IllegalArgumentException("light source");
|
if (!Double.isFinite(x+y+z) || light<1 || light>15) throw new IllegalArgumentException("light source");
|
||||||
}
|
}
|
||||||
@@ -20,9 +21,12 @@ public final class DynamicLightField {
|
|||||||
private final int cellSize;
|
private final int cellSize;
|
||||||
public Collector(int cellSize) {this.cellSize=Math.max(1,cellSize);}
|
public Collector(int cellSize) {this.cellSize=Math.max(1,cellSize);}
|
||||||
public void add(long entity,boolean drop,double x,double y,double z,int light) {
|
public void add(long entity,boolean drop,double x,double y,double z,int light) {
|
||||||
|
add(entity,drop,x,y,z,light,0xFFFFFF);
|
||||||
|
}
|
||||||
|
public void add(long entity,boolean drop,double x,double y,double z,int light,int color) {
|
||||||
if(light<=0)return;
|
if(light<=0)return;
|
||||||
Id id=new Id(drop,drop ? sectionKey(floor(x/cellSize),floor(y/cellSize),floor(z/cellSize)) : entity);
|
Id id=new Id(drop,drop ? sectionKey(floor(x/cellSize),floor(y/cellSize),floor(z/cellSize)) : entity);
|
||||||
Source candidate=new Source(id,snap(x),snap(y),snap(z),Math.min(15,light));
|
Source candidate=new Source(id,snap(x),snap(y),snap(z),Math.min(15,light),color);
|
||||||
sources.merge(id,candidate,(a,b)->compareRepresentative(a,b)<=0?a:b);
|
sources.merge(id,candidate,(a,b)->compareRepresentative(a,b)<=0?a:b);
|
||||||
}
|
}
|
||||||
public List<Source> finish(int limit,double x,double y,double z,long priorityEntity) {
|
public List<Source> finish(int limit,double x,double y,double z,long priorityEntity) {
|
||||||
@@ -53,6 +57,7 @@ public final class DynamicLightField {
|
|||||||
lists.long2ObjectEntrySet().forEach(e->bins.put(e.getLongKey(),e.getValue().toArray(Source[]::new)));
|
lists.long2ObjectEntrySet().forEach(e->bins.put(e.getLongKey(),e.getValue().toArray(Source[]::new)));
|
||||||
}
|
}
|
||||||
public int size() {return sources.size();}
|
public int size() {return sources.size();}
|
||||||
|
public Collection<Source> sources(){return sources.values();}
|
||||||
public int sectionCount() {return bins.size();}
|
public int sectionCount() {return bins.size();}
|
||||||
public int sampleCount(double x,double y,double z) {
|
public int sampleCount(double x,double y,double z) {
|
||||||
Source[] near=bins.get(sectionKey(floor(x)>>4,floor(y)>>4,floor(z)>>4));return near==null?0:near.length;
|
Source[] near=bins.get(sectionKey(floor(x)>>4,floor(y)>>4,floor(z)>>4));return near==null?0:near.length;
|
||||||
|
|||||||
@@ -2310,8 +2310,13 @@
|
|||||||
"sanctuary.shaders.sunshaft_intensity_help": "Strength of light scattered in the air. Beams automatically thicken with fog and precipitation. Zero disables the effect.",
|
"sanctuary.shaders.sunshaft_intensity_help": "Strength of light scattered in the air. Beams automatically thicken with fog and precipitation. Zero disables the effect.",
|
||||||
"sanctuary.shaders.sunshaft_intensity_value": "Sunshafts: %s %%",
|
"sanctuary.shaders.sunshaft_intensity_value": "Sunshafts: %s %%",
|
||||||
"sanctuary.shaders.colored_lights": "Colored lights",
|
"sanctuary.shaders.colored_lights": "Colored lights",
|
||||||
"sanctuary.shaders.colored_lights_help": "Light-emitting blocks tint their surroundings, even off screen. Local propagation respects obstacles and does not change gameplay light levels.",
|
"sanctuary.shaders.colored_lights_help": "Light-emitting blocks and carried items tint their surroundings, even off screen. Smooth appearance without changing gameplay light levels.",
|
||||||
"sanctuary.shaders.colored_light_intensity": "Colored light intensity",
|
"sanctuary.shaders.colored_light_intensity": "Colored light intensity",
|
||||||
"sanctuary.shaders.colored_light_intensity_value": "Colored lights: %s %%",
|
"sanctuary.shaders.colored_light_intensity_value": "Colored lights: %s %%",
|
||||||
"sanctuary.shaders.colored_light_intensity_help": "Subtle tint at 35%. Zero disables the calculation. Source changes update progressively."
|
"sanctuary.shaders.colored_light_intensity_help": "Enabled at 30% by default. Zero disables computation. Changes fade in smoothly.",
|
||||||
|
"sanctuary.shaders.colored_light_distance": "Colored light distance",
|
||||||
|
"sanctuary.shaders.colored_light_distance_value": "Colored lights: %s blocks",
|
||||||
|
"sanctuary.shaders.colored_light_distance_help": "View distance: 8 to 64 blocks. Longer distances use more memory and computation. Smooth range fade; each source keeps its native reach.",
|
||||||
|
"sanctuary.shaders.warm_torches": "Warm torches",
|
||||||
|
"sanctuary.shaders.warm_torches_help": "Off: ordinary torches keep the native color temperature. On: orange tint. Other sources retain their own colors."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2310,8 +2310,13 @@
|
|||||||
"sanctuary.shaders.sunshaft_intensity_help": "Force de la lumière dans l’air. Les rayons s’épaississent automatiquement avec le brouillard et les précipitations. Zéro désactive le calcul.",
|
"sanctuary.shaders.sunshaft_intensity_help": "Force de la lumière dans l’air. Les rayons s’épaississent automatiquement avec le brouillard et les précipitations. Zéro désactive le calcul.",
|
||||||
"sanctuary.shaders.sunshaft_intensity_value": "Rayons : %s %%",
|
"sanctuary.shaders.sunshaft_intensity_value": "Rayons : %s %%",
|
||||||
"sanctuary.shaders.colored_lights": "Lumières colorées",
|
"sanctuary.shaders.colored_lights": "Lumières colorées",
|
||||||
"sanctuary.shaders.colored_lights_help": "Les blocs lumineux teintent leurs environs, même hors écran. Propagation locale avec obstacles, sans changer les règles de lumière du jeu.",
|
"sanctuary.shaders.colored_lights_help": "Les blocs lumineux et les objets portés teintent leurs environs, même hors écran. Apparition progressive, sans changer les règles de lumière du jeu.",
|
||||||
"sanctuary.shaders.colored_light_intensity": "Intensité des lumières colorées",
|
"sanctuary.shaders.colored_light_intensity": "Intensité des lumières colorées",
|
||||||
"sanctuary.shaders.colored_light_intensity_value": "Lumières colorées : %s %%",
|
"sanctuary.shaders.colored_light_intensity_value": "Lumières colorées : %s %%",
|
||||||
"sanctuary.shaders.colored_light_intensity_help": "Teinte douce à 35 %. Zéro désactive le calcul. Les changements de sources sont actualisés progressivement."
|
"sanctuary.shaders.colored_light_intensity_help": "Activé par défaut à 30 %. Zéro désactive le calcul. Les changements apparaissent progressivement.",
|
||||||
|
"sanctuary.shaders.colored_light_distance": "Distance des lumières colorées",
|
||||||
|
"sanctuary.shaders.colored_light_distance_value": "Lumières colorées : %s blocs",
|
||||||
|
"sanctuary.shaders.colored_light_distance_help": "Distance d'affichage : 8 à 64 blocs. Les grandes distances utilisent plus de mémoire et de calcul. Fondu aux limites ; la portée de chaque source reste native.",
|
||||||
|
"sanctuary.shaders.warm_torches": "Torches chaudes",
|
||||||
|
"sanctuary.shaders.warm_torches_help": "Désactivé : les torches ordinaires conservent la température de couleur native. Activé : teinte orangée. Les autres sources gardent leurs propres couleurs."
|
||||||
}
|
}
|
||||||
|
|||||||
+46
-16
@@ -3,12 +3,18 @@
|
|||||||
uniform sampler2D WorldDepth;
|
uniform sampler2D WorldDepth;
|
||||||
uniform sampler2D SceneColor;
|
uniform sampler2D SceneColor;
|
||||||
uniform sampler2D LightVolume;
|
uniform sampler2D LightVolume;
|
||||||
|
uniform sampler2D PreviousVolume;
|
||||||
layout(std140) uniform ColoredLightScene {
|
layout(std140) uniform ColoredLightScene {
|
||||||
mat4 InverseViewProjection;
|
mat4 InverseViewProjection;
|
||||||
vec4 CameraStrength;
|
vec4 CameraStrength;
|
||||||
vec4 ClipSkyFog;
|
vec4 ClipSkyFog;
|
||||||
vec4 FogDistance;
|
vec4 FogDistance;
|
||||||
vec4 VolumeSize;
|
vec4 VolumeSize;
|
||||||
|
vec4 Atlas;
|
||||||
|
vec4 PreviousCamera;
|
||||||
|
vec4 PreviousAtlas;
|
||||||
|
vec4 DynamicPosition[128];
|
||||||
|
vec4 DynamicColor[128];
|
||||||
};
|
};
|
||||||
layout(location=0) in vec2 texCoord;
|
layout(location=0) in vec2 texCoord;
|
||||||
layout(location=0) out vec4 fragColor;
|
layout(location=0) out vec4 fragColor;
|
||||||
@@ -27,20 +33,32 @@ vec3 normalAt(vec3 p){
|
|||||||
vec3 n=cross(dx*inversesqrt(xx),dy*inversesqrt(yy));float nn=dot(n,n);if(nn<1e-12)return vec3(0);
|
vec3 n=cross(dx*inversesqrt(xx),dy*inversesqrt(yy));float nn=dot(n,n);if(nn<1e-12)return vec3(0);
|
||||||
n*=inversesqrt(nn);return dot(n,-p)<0.0?-n:n;
|
n*=inversesqrt(nn);return dot(n,-p)<0.0?-n:n;
|
||||||
}
|
}
|
||||||
vec4 cell(ivec3 p){
|
vec4 cell(sampler2D volume,ivec3 p,vec4 atlas){
|
||||||
p=clamp(p,ivec3(0),ivec3(63));
|
int side=int(atlas.x),cols=int(atlas.y);
|
||||||
vec4 v=texelFetch(LightVolume,ivec2(p.x+(p.y%8)*64,p.z+(p.y/8)*64),0);
|
p=clamp(p,ivec3(0),ivec3(side-1));
|
||||||
// Decode Q4.4 logarithmic RGB. Black stays exactly black.
|
vec4 v=texelFetch(volume,ivec2(p.x+(p.y%cols)*side,p.z+(p.y/cols)*side),0);
|
||||||
v.rgb=max(vec3(0),(pow(vec3(.8),vec3(15)-v.rgb*(255.0/16.0))-vec3(pow(.8,15.0)))/(1.0-pow(.8,15.0)));
|
vec3 spectrum=pow(vec3(.8),vec3(31)-v.rgb*(255.0/8.0))*step(vec3(.5/255.0),v.rgb);
|
||||||
|
float peak=max(spectrum.r,max(spectrum.g,spectrum.b));
|
||||||
|
float energy=max(0.0,(peak-pow(.8,15.0))/(1.0-pow(.8,15.0)));
|
||||||
|
v.rgb=spectrum*(energy/max(peak,1e-12));
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
vec4 field(vec3 p){
|
vec4 field(sampler2D volume,vec3 p,vec4 atlas){
|
||||||
|
vec3 edges=min(p,vec3(atlas.x)-p);
|
||||||
|
float margin=smoothstep(14.0,18.0,min(edges.x,min(edges.y,edges.z)));if(margin<=0.0)return vec4(0);
|
||||||
vec3 at=p-.5;ivec3 base=ivec3(floor(at));vec3 f=fract(at);vec4 result=vec4(0);
|
vec3 at=p-.5;ivec3 base=ivec3(floor(at));vec3 f=fract(at);vec4 result=vec4(0);
|
||||||
for(int y=0;y<2;y++)for(int z=0;z<2;z++)for(int x=0;x<2;x++){
|
for(int y=0;y<2;y++)for(int z=0;z<2;z++)for(int x=0;x<2;x++){
|
||||||
float w=(x==0?1.0-f.x:f.x)*(y==0?1.0-f.y:f.y)*(z==0?1.0-f.z:f.z);
|
float w=(x==0?1.0-f.x:f.x)*(y==0?1.0-f.y:f.y)*(z==0?1.0-f.z:f.z);
|
||||||
result+=cell(base+ivec3(x,y,z))*w;
|
result+=cell(volume,base+ivec3(x,y,z),atlas)*w;
|
||||||
}
|
}
|
||||||
return result;
|
result.rgb*=margin;return result;
|
||||||
|
}
|
||||||
|
vec3 tintGain(vec4 light,float amount){
|
||||||
|
float energy=max(light.r,max(light.g,light.b));if(energy<1e-12)return vec3(1);
|
||||||
|
vec3 tint=clamp(light.rgb/max(dot(light.rgb,vec3(.2126,.7152,.0722)),1e-12),vec3(.02),vec3(3));
|
||||||
|
// Linear close to darkness: no fourth-root halo at the edge of a light pool.
|
||||||
|
float response=energy/(energy+.08);
|
||||||
|
return pow(tint,vec3(clamp(amount*response*(1.0-.8*light.a*ClipSkyFog.y),0.0,.95)));
|
||||||
}
|
}
|
||||||
void main(){
|
void main(){
|
||||||
vec3 scene=texture(SceneColor,texCoord).rgb;fragColor=vec4(scene,0);
|
vec3 scene=texture(SceneColor,texCoord).rgb;fragColor=vec4(scene,0);
|
||||||
@@ -49,13 +67,25 @@ void main(){
|
|||||||
vec3 a=abs(n);float axis=max(a.x,max(a.y,a.z));
|
vec3 a=abs(n);float axis=max(a.x,max(a.y,a.z));
|
||||||
// Sample outside opaque block faces. Bilinear atlas filtering would leak
|
// Sample outside opaque block faces. Bilinear atlas filtering would leak
|
||||||
// across unrelated slices, so the eight neighbours are explicitly addressed.
|
// across unrelated slices, so the eight neighbours are explicitly addressed.
|
||||||
vec3 local=p+CameraStrength.xyz+n*(axis>.995?.51:.06);
|
vec3 offset=n*(axis>.995?.51:.06);
|
||||||
vec3 edge=min(local,vec3(VolumeSize.x)-local);
|
float border=1.0-smoothstep(VolumeSize.x*.75,VolumeSize.x,length(p));if(border<=0.0)return;
|
||||||
float border=smoothstep(VolumeSize.y,VolumeSize.y+VolumeSize.z,min(edge.x,min(edge.y,edge.z)));if(border<=0.0)return;
|
vec4 light=field(LightVolume,p+CameraStrength.xyz+offset,Atlas);
|
||||||
vec4 light=field(local);float energy=max(light.r,max(light.g,light.b));if(energy<.0001)return;
|
vec4 before=VolumeSize.y<1.0&&PreviousCamera.w>.5?field(PreviousVolume,p+PreviousCamera.xyz+offset,PreviousAtlas):vec4(0);
|
||||||
float luminance=dot(light.rgb,vec3(.2126,.7152,.0722));
|
vec3 dynamic=vec3(0);
|
||||||
vec3 tint=clamp(light.rgb/max(luminance,.0001),vec3(.15),vec3(2));
|
for(int i=0;i<int(VolumeSize.w);i++){
|
||||||
|
vec3 delta=p-DynamicPosition[i].xyz;float square=dot(delta,delta),radius=DynamicPosition[i].w;
|
||||||
|
if(square>=radius*radius)continue;
|
||||||
|
float remaining=radius-sqrt(square);
|
||||||
|
float power=max(0.0,(pow(.8,15.0-remaining)-pow(.8,15.0))/(1.0-pow(.8,15.0)));
|
||||||
|
float response=power/(power+.08)*smoothstep(0.0,1.0,DynamicColor[i].a);
|
||||||
|
float fadedPower=.08*response/max(1e-6,1.0-response);
|
||||||
|
dynamic=max(dynamic,DynamicColor[i].rgb*fadedPower);
|
||||||
|
}
|
||||||
|
light.rgb=max(light.rgb,dynamic);before.rgb=max(before.rgb,dynamic);
|
||||||
float fog=max(clamp((max(length(p.xz),abs(p.y))-FogDistance.x)/max(.001,FogDistance.y-FogDistance.x),0,1),clamp((length(p)-FogDistance.z)/max(.001,FogDistance.w-FogDistance.z),0,1));
|
float fog=max(clamp((max(length(p.xz),abs(p.y))-FogDistance.x)/max(.001,FogDistance.y-FogDistance.x),0,1),clamp((length(p)-FogDistance.z)/max(.001,FogDistance.w-FogDistance.z),0,1));
|
||||||
float strength=CameraStrength.w*.8*sqrt(energy)*border*(1.0-.8*light.a*ClipSkyFog.y)*(1.0-fog*ClipSkyFog.z);
|
float amount=CameraStrength.w*1.35*border*(1.0-fog*ClipSkyFog.z)*smoothstep(0.0,1.0,VolumeSize.z);
|
||||||
fragColor=vec4(scene*mix(vec3(1),tint,strength),0);
|
vec3 gain=tintGain(light,amount);
|
||||||
|
// Fade the rendered response, preserving colour and avoiding a bright first frame.
|
||||||
|
if(VolumeSize.y<1.0)gain=mix(tintGain(before,amount),gain,smoothstep(0.0,1.0,VolumeSize.y));
|
||||||
|
fragColor=vec4(scene*gain,0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,13 @@ public final class Dynamic030Smoke {
|
|||||||
for(var light:lights)expected=Math.max(expected,(int)Math.max(0,(light.light()-Math.sqrt(light.distanceSquared(x,y,z)))*16));
|
for(var light:lights)expected=Math.max(expected,(int)Math.max(0,(light.light()-Math.sqrt(light.distanceSquared(x,y,z)))*16));
|
||||||
require(indexed.apply(nativeSky,x,y,z)==(nativeSky|expected),"Indexed samples agree with brute-force oracle");
|
require(indexed.apply(nativeSky,x,y,z)==(nativeSky|expected),"Indexed samples agree with brute-force oracle");
|
||||||
}
|
}
|
||||||
|
var colored=new DynamicLightField.Collector(4);colored.add(42,false,2,3,4,10,0x55BDFF);
|
||||||
|
var blue=colored.finish(32,0,0,0,42);var blueField=new DynamicLightField.Snapshot(blue);
|
||||||
|
require(blueField.sources().iterator().next().color()==0x55BDFF,"Snapshot retains source colour");
|
||||||
|
require((blueField.apply(nativeSky,2,3,4)&255)==160,"RGB metadata preserves native intensity");
|
||||||
|
var changed=new DynamicLightField.Collector(4);changed.add(42,false,2,3,4,10,0x62FFA0);
|
||||||
|
field.update(blue);require(field.update(changed.finish(32,0,0,0,42)),"Same intensity can change colour");
|
||||||
|
require(blueField.sources().iterator().next().color()==0x55BDFF,"Older coloured snapshot remains immutable");
|
||||||
long begin=System.nanoTime();long sum=0;
|
long begin=System.nanoTime();long sum=0;
|
||||||
for(int i=0;i<1000000;i++)sum+=stress.apply(0,i%80,2,(i/80)%80);
|
for(int i=0;i<1000000;i++)sum+=stress.apply(0,i%80,2,(i/80)%80);
|
||||||
System.out.printf("LIGHT-01 passed: 10,000-drop clustering, 100,000 oracle samples; 1M cached queries %.1f ms (checksum %d)%n",(System.nanoTime()-begin)/1e6,sum);
|
System.out.printf("LIGHT-01 passed: 10,000-drop clustering, 100,000 oracle samples; 1M cached queries %.1f ms (checksum %d)%n",(System.nanoTime()-begin)/1e6,sum);
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
name = "Sanctuary"
|
name = "Sanctuary"
|
||||||
author = "KOKA99CAB"
|
author = "KOKA99CAB"
|
||||||
version = "beta.139"
|
version = "beta.140"
|
||||||
pack-format = "packwiz:1.1.0"
|
pack-format = "packwiz:1.1.0"
|
||||||
|
|
||||||
[index]
|
[index]
|
||||||
|
|||||||
Reference in New Issue
Block a user