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