From faeda0e0da4befaee9b1c980027693faf16859c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Thu, 5 Nov 2026 16:57:44 +0100 Subject: [PATCH] nether and end skies: the nether has no sky (camera biome fog_color) and fogs like vanilla (10 to 96 blocks), the end shows vanilla's textured sky box and fogs towards its fog colour (both showed the overworld day sky with sun and clouds) --- .../render/raytrace/CameraMedium.java | 45 ++++++++---- .../render/raytrace/SnapshotRaytracer.java | 4 +- .../render/render/DefaultScreenRenderer.java | 19 ++++- .../pixelpics/render/sky/SkyContext.java | 22 +++++- .../pixelpics/render/sky/SkyRenderer.java | 71 ++++++++++++++++++- .../pixelpics/render/tint/BiomeClimate.java | 33 +++++++++ 6 files changed, 170 insertions(+), 24 deletions(-) diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/raytrace/CameraMedium.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/raytrace/CameraMedium.java index 3860b6a..eea8377 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/raytrace/CameraMedium.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/raytrace/CameraMedium.java @@ -1,5 +1,6 @@ package eu.mhsl.minecraft.pixelpics.render.raytrace; +import eu.mhsl.minecraft.pixelpics.render.sky.SkyContext; import eu.mhsl.minecraft.pixelpics.render.snapshot.WorldSnapshot; import eu.mhsl.minecraft.pixelpics.render.tint.BiomeClimate; import eu.mhsl.minecraft.pixelpics.render.tint.BiomeTintProvider; @@ -18,10 +19,19 @@ import org.bukkit.util.Vector; * the biomes around the camera with vanilla's {@code GaussianSampler} (6x6x6 quart cells, kernel 1-4-6-4-1). * Water vision ({@code LocalPlayer.getWaterVision}) is taken as fully adapted (1), which keeps the end distance and * brightens the colour to a maximum channel of 1. Lava: #991A00 from 0.25 to 1 block (no fire resistance). + * + *

Air outside the overworld: the Nether fogs like vanilla's environmental fog (dimension attributes + * {@code fog_start_distance} 10, {@code fog_end_distance} 96) towards the sky context's fog colour (the camera biome's + * {@code fog_color}); the End uses the atmospheric fog towards its fog colour (#181318) instead of the sky. + * + * @param fogColor in air: the fixed colour distant surfaces fog towards, or -1 for the sky colour in their direction + * @param fogMax the strongest fog (1 = opaque) */ -public record CameraMedium(int fluid, int fogColor, double fogStart, double fogEnd) { +public record CameraMedium(int fluid, int fogColor, double fogStart, double fogEnd, double fogMax) { - public static final CameraMedium AIR = new CameraMedium(FluidRenderer.NONE, 0, 0, 0); + /** Overworld air: PixelPics' atmospheric fog towards the sky. */ + public static final CameraMedium AIR = + new CameraMedium(FluidRenderer.NONE, -1, CameraMedium.AIR_FOG_START, CameraMedium.AIR_FOG_END, 0.75); private static final double[] KERNEL = {0, 1, 4, 6, 4, 1, 0}; @@ -29,6 +39,9 @@ public record CameraMedium(int fluid, int fogColor, double fogStart, double fogE private static final double AIR_FOG_START = 128; private static final double AIR_FOG_END = 256; private static final double AIR_FOG_MAX = 0.75; + // Nether: dimension type attributes minecraft:visual/fog_start_distance / fog_end_distance (vanilla 26.3). + private static final double NETHER_FOG_START = 10; + private static final double NETHER_FOG_END = 96; // Vanilla sky disc (SkyRenderer.buildSkyDisc): a fan at height 16 around the camera, rim octagon at 512. private static final double DISC_HEIGHT = 16; @@ -40,10 +53,16 @@ public record CameraMedium(int fluid, int fogColor, double fogStart, double fogE return fluid != FluidRenderer.NONE; } - static CameraMedium at(WorldSnapshot snapshot, Vector eye, int fluid, BiomeTintProvider tints) { + static CameraMedium at(WorldSnapshot snapshot, Vector eye, int fluid, BiomeTintProvider tints, SkyContext sky) { double x = eye.getX(), y = eye.getY(), z = eye.getZ(); - if (fluid == FluidRenderer.LAVA) return new CameraMedium(fluid, 0x991A00, 0.25, 1.0); - if (fluid != FluidRenderer.WATER) return AIR; + if (fluid == FluidRenderer.LAVA) return new CameraMedium(fluid, 0x991A00, 0.25, 1.0, 1); + if (fluid != FluidRenderer.WATER) { + return switch (sky.skybox()) { + case OVERWORLD -> AIR; + case NONE -> new CameraMedium(fluid, sky.fogColor(), NETHER_FOG_START, NETHER_FOG_END, 1); + case END -> new CameraMedium(fluid, sky.fogColor(), AIR_FOG_START, AIR_FOG_END, AIR_FOG_MAX); + }; + } // GaussianSampler.sample(position / 4) over the noise biomes (quart positions). double px = x * 0.25 - 0.5, py = y * 0.25 - 0.5, pz = z * 0.25 - 0.5; @@ -85,7 +104,7 @@ public record CameraMedium(int fluid, int fogColor, double fogStart, double fogE b = b + (b * scale - b); } int color = ColorUtil.argb(255, channel(r), channel(g), channel(b)) & 0xFFFFFF; - return new CameraMedium(fluid, color, start, end); + return new CameraMedium(fluid, color, start, end, 1); } private static double lerp(double a, double from, double to) { @@ -98,24 +117,20 @@ public record CameraMedium(int fluid, int fogColor, double fogStart, double fogE /** Fog fraction at distance {@code d} (in fluids fog.glsl {@code linear_fog_value}). */ double fogValue(double d) { - if (!inFluid()) { - if (d <= AIR_FOG_START) return 0; - return Math.clamp((d - AIR_FOG_START) / (AIR_FOG_END - AIR_FOG_START), 0, AIR_FOG_MAX); - } if (d <= fogStart) return 0; - if (d >= fogEnd) return 1; - return (d - fogStart) / (fogEnd - fogStart); + if (d >= fogEnd) return fogMax; + return Math.min((d - fogStart) / (fogEnd - fogStart), fogMax); } - /** {@code color} fogged at distance {@code d}; air fogs towards {@code skyColor}. */ + /** {@code color} fogged at distance {@code d}; overworld air fogs towards {@code skyColor}. */ int fog(int color, double d, int skyColor) { double f = fogValue(d); - return f > 0 ? ColorUtil.mix(color, inFluid() ? fogColor : skyColor, 1 - f, f) : color; + return f > 0 ? ColorUtil.mix(color, fogColor >= 0 ? fogColor : skyColor, 1 - f, f) : color; } /** Whether {@link #fog}/{@link #sky} of a surface at {@code t} (infinite = sky) read the sky colour. */ boolean needsSkyColor(double t) { - return Double.isInfinite(t) || (!inFluid() && t > AIR_FOG_START); + return Double.isInfinite(t) || (fogColor < 0 && t > fogStart); } /** diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/raytrace/SnapshotRaytracer.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/raytrace/SnapshotRaytracer.java index c3d3c4a..a81feaa 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/raytrace/SnapshotRaytracer.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/raytrace/SnapshotRaytracer.java @@ -86,8 +86,8 @@ public final class SnapshotRaytracer { * The medium (air or {@code fluid} with its fog) of the camera at {@code origin}; once per photo. {@code fluid} = * {@link FluidRenderer#cameraFluid} (determined once per photo, also for the FOV). */ - public CameraMedium cameraMedium(WorldSnapshot snapshot, Vector origin, int fluid) { - return CameraMedium.at(snapshot, origin, fluid, tintProvider); + public CameraMedium cameraMedium(WorldSnapshot snapshot, Vector origin, int fluid, SkyContext sky) { + return CameraMedium.at(snapshot, origin, fluid, tintProvider, sky); } public int trace( diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/render/DefaultScreenRenderer.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/render/DefaultScreenRenderer.java index 0dbbadd..2f3f09a 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/render/DefaultScreenRenderer.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/render/DefaultScreenRenderer.java @@ -25,6 +25,7 @@ import eu.mhsl.minecraft.pixelpics.render.snapshot.FrustumBounds; import eu.mhsl.minecraft.pixelpics.render.snapshot.ObjectSnapshotBuilder; import eu.mhsl.minecraft.pixelpics.render.snapshot.SnapshotBuilder; import eu.mhsl.minecraft.pixelpics.render.snapshot.WorldSnapshot; +import eu.mhsl.minecraft.pixelpics.render.tint.BiomeClimate; import eu.mhsl.minecraft.pixelpics.render.tint.BiomeTintProvider; import eu.mhsl.minecraft.pixelpics.render.util.ColorUtil; import eu.mhsl.minecraft.pixelpics.render.util.SpecialDates; @@ -54,6 +55,9 @@ public class DefaultScreenRenderer { private static final double FLUID_FOV_SCALE = 6.0 / 7.0; private static final double MAX_DISTANCE = 256; + // Fog colours of the vanilla 26.3 dimension types / nether_wastes (the default of a nether biome without one). + private static final int NETHER_FOG_COLOR = 0x330808; + private static final int END_FOG_COLOR = 0x181318; private static final int REFLECTION_DEPTH = 4; /** SSAA x SSAA rays per output pixel, downsampled gamma-correctly. */ @@ -126,7 +130,18 @@ public class DefaultScreenRenderer { long dayTime = world.getTime(); long fullTime = world.getFullTime(); int moonPhase = (int) (fullTime / 24000L % 8L); - SkyContext sky = new SkyContext(dayTime, moonPhase, fullTime); + SkyContext sky = + switch (world.getEnvironment()) { + case NETHER -> new SkyContext( + dayTime, + moonPhase, + fullTime, + SkyContext.Skybox.NONE, + BiomeClimate.fogColor( + world.getBiome(eyeLocation).getKey().getKey(), NETHER_FOG_COLOR)); + case THE_END -> new SkyContext(dayTime, moonPhase, fullTime, SkyContext.Skybox.END, END_FOG_COLOR); + default -> new SkyContext(dayTime, moonPhase, fullTime); + }; return new RenderJob( snapshot, @@ -191,7 +206,7 @@ public class DefaultScreenRenderer { job.objects(), objectBaker)); - CameraMedium medium = raytracer.cameraMedium(snapshot, origin, job.cameraFluid()); + CameraMedium medium = raytracer.cameraMedium(snapshot, origin, job.cameraFluid(), sky); BiomeBlend tints = raytracer.biomeBlend(snapshot); int[] superBuf = new int[rayMap.size()]; runParallel(() -> IntStream.range(0, rayMap.size()).parallel().forEach(i -> { diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/sky/SkyContext.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/sky/SkyContext.java index a4d02db..ea4826c 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/sky/SkyContext.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/sky/SkyContext.java @@ -2,7 +2,23 @@ package eu.mhsl.minecraft.pixelpics.render.sky; /** * Per-render sky state captured on the main thread: the world time of day (0..24000), the moon phase - * (0..7) and the absolute world time (for continuous cloud drift). Immutable so it can be read from - * worker threads. + * (0..7), the absolute world time (for continuous cloud drift), the dimension's sky box and its fog colour (Nether: + * the camera biome's {@code fog_color}, End: the dimension's). Immutable so it can be read from worker threads. */ -public record SkyContext(long dayTime, int moonPhase, long fullTime) {} +public record SkyContext(long dayTime, int moonPhase, long fullTime, Skybox skybox, int fogColor) { + + /** The dimension's sky like vanilla's {@code DimensionType.skybox}. */ + public enum Skybox { + /** Overworld sky: gradient, sun, moon, stars and clouds. */ + OVERWORLD, + /** No sky (the Nether): only the fog colour. */ + NONE, + /** The End's textured sky box. */ + END + } + + /** An overworld sky. */ + public SkyContext(long dayTime, int moonPhase, long fullTime) { + this(dayTime, moonPhase, fullTime, Skybox.OVERWORLD, 0); + } +} diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/sky/SkyRenderer.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/sky/SkyRenderer.java index 15de5d8..508eeda 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/sky/SkyRenderer.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/sky/SkyRenderer.java @@ -6,8 +6,9 @@ import eu.mhsl.minecraft.pixelpics.render.util.ColorUtil; import org.bukkit.util.Vector; /** - * Sky colour of rays that escape the world (not vanilla's sky renderer): day/night gradient with twilight glow, sun - * and moon (with phase), stars and vanilla's cloud layer. Immutable, so thread safe. + * Sky colour of rays that escape the world. Overworld (not vanilla's sky renderer): day/night gradient with twilight + * glow, sun and moon (with phase), stars and vanilla's cloud layer. The Nether has no sky (fog colour) and the End + * vanilla's textured sky box ({@code SkyRenderer.buildEndSky}). Immutable, so thread safe. */ public final class SkyRenderer { @@ -29,14 +30,71 @@ public final class SkyRenderer { private static final int SUNSET_RED = rgb(205, 70, 60); private static final int TWI_PURPLE = rgb(80, 42, 92); + // End sky box (vanilla SkyRenderer.buildEndSky): 6 quads at distance 100, the texture repeated 16 times per face, + // vertex colour #282828. + private static final double END_SKY_REPEAT = 16; + private static final int END_SKY_TINT = 0x28; + // Face poses of the end sky quads: identity, rotX(90°), rotX(-90°), rotX(180°), rotZ(90°), rotZ(-90°). + private static final double[][] END_FACES = { + {0, 0}, {1, Math.PI / 2}, {1, -Math.PI / 2}, {1, Math.PI}, {2, Math.PI / 2}, {2, -Math.PI / 2} + }; + private final int[][] moonTexture; private final int[][] cloudTexture; + private final int[][] endSkyTexture; public SkyRenderer(TextureCache textures) { this.moonTexture = textures.get(ResourceLocation.parse("environment/moon_phases")).orElse(null); this.cloudTexture = textures.get(ResourceLocation.parse("environment/clouds")).orElse(null); + this.endSkyTexture = + textures.get(ResourceLocation.parse("environment/end_sky")).orElse(null); + } + + /** + * The end sky in a direction: the quad of vanilla's sky box hit by the ray, its texel (nearest, repeating) times + * the vertex colour. + */ + private int endSky(double dx, double dy, double dz) { + int[][] tex = endSkyTexture; + if (tex == null || tex.length == 0) return rgb(END_SKY_TINT, END_SKY_TINT, END_SKY_TINT); + // The quad whose pose maps its plane y = -100 onto the ray: q = pose^T d with the most negative q.y. + double bestY = 0, qx = 0, qz = 0; + for (double[] face : END_FACES) { + double c = Math.cos(face[1]), sn = Math.sin(face[1]); + double x, y, z; + if (face[0] == 1) { // rotX(a)^T + x = dx; + y = dy * c + dz * sn; + z = -dy * sn + dz * c; + } else if (face[0] == 2) { // rotZ(a)^T + x = dx * c + dy * sn; + y = -dx * sn + dy * c; + z = dz; + } else { + x = dx; + y = dy; + z = dz; + } + if (y < bestY) { + bestY = y; + qx = x; + qz = z; + } + } + if (bestY >= 0) return rgb(END_SKY_TINT, END_SKY_TINT, END_SKY_TINT); + double t = -100 / bestY; + double u = (qx * t + 100) / 200 * END_SKY_REPEAT; + double v = (qz * t + 100) / 200 * END_SKY_REPEAT; + int h = tex.length, w = tex[0].length; + int px = Math.floorMod((int) Math.floor(u * w), w); + int py = Math.floorMod((int) Math.floor(v * h), h); + int texel = tex[py][px]; + return rgb( + ColorUtil.red(texel) * END_SKY_TINT / 255, + ColorUtil.green(texel) * END_SKY_TINT / 255, + ColorUtil.blue(texel) * END_SKY_TINT / 255); } public int colorFor(Vector direction, Vector origin, SkyContext ctx) { @@ -46,6 +104,15 @@ public final class SkyRenderer { dx /= len; dy /= len; dz /= len; + switch (ctx.skybox()) { + case NONE -> { + return ctx.fogColor() & 0xFFFFFF; + } + case END -> { + return endSky(dx, dy, dz); + } + case OVERWORLD -> {} + } // Vanilla sun direction: rotated by celestialAngle·360° about X (after -90° about Y) // = (-sin(2π·ca), cos(2π·ca), 0). diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/tint/BiomeClimate.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/tint/BiomeClimate.java index 1021d1c..ff38e6b 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/tint/BiomeClimate.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/tint/BiomeClimate.java @@ -188,6 +188,39 @@ public final class BiomeClimate { private BiomeClimate() {} + // Biome attribute minecraft:visual/fog_color of the vanilla 26.3 nether biomes (fallback without server data). + private static final Map FOG_FALLBACK = Map.of( + "nether_wastes", 0x330808, + "basalt_deltas", 0x685F70, + "crimson_forest", 0x330303, + "warped_forest", 0x1A051A, + "soul_sand_valley", 0x1B4745); + private static final Map FOG_LOADED = new ConcurrentHashMap<>(); + + /** The biome's {@code minecraft:visual/fog_color} attribute applied to {@code base}. */ + public static int fogColor(String biomePath, int base) { + int loaded = FOG_LOADED.computeIfAbsent(biomePath, BiomeClimate::loadFogColor); + if (loaded != NONE) return loaded == Integer.MIN_VALUE ? base : loaded; + return FOG_FALLBACK.getOrDefault(biomePath, base); + } + + /** The fog colour of the biome file (MIN_VALUE: file without one), NONE without server data. */ + private static int loadFogColor(String biomePath) { + if (!biomePath.matches("[a-z0-9_./-]+")) return NONE; + ClassLoader loader = BiomeClimate.class.getClassLoader(); + if (loader == null) return NONE; + try (InputStream in = loader.getResourceAsStream(DATA_PATH + biomePath + ".json")) { + if (in == null) return NONE; + JsonObject root = JsonParser.parseReader(new InputStreamReader(in, StandardCharsets.UTF_8)) + .getAsJsonObject(); + JsonObject attributes = root.has("attributes") ? root.getAsJsonObject("attributes") : new JsonObject(); + if (!attributes.has("minecraft:visual/fog_color")) return Integer.MIN_VALUE; + return rgbAttribute(attributes, "minecraft:visual/fog_color", 0); + } catch (Exception e) { + return NONE; + } + } + public static Climate forKey(String biomePath) { Optional loaded = LOADED.computeIfAbsent(biomePath, BiomeClimate::loadFromServerData); return loaded.isPresent() ? loaded.get() : FALLBACK.getOrDefault(biomePath, DEFAULT);