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 b28ae8a..9c7d9f5 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 @@ -8,35 +8,39 @@ import org.bukkit.block.Biome; import org.bukkit.util.Vector; /** - * The fluid the camera is in and its fog, like vanilla 26.3 {@code FogRenderer} with {@code WaterFogEnvironment} / - * {@code LavaFogEnvironment}: linear fog from {@code fogStart} to {@code fogEnd} (spherical distance from the - * camera) towards {@code fogColor}, applied to every surface; the sky is the fogged sky disc. + * What the camera looks through and its distance fog. In water/lava like vanilla 26.3 {@code FogRenderer} with + * {@code WaterFogEnvironment} / {@code LavaFogEnvironment}: linear fog from {@code fogStart} to {@code fogEnd} + * (spherical distance) towards {@code fogColor}; the sky is the fogged sky disc. In air: PixelPics' own + * atmospheric fog towards the sky colour (not vanilla). * *

Water: start/end/colour are the biome attributes {@code water_fog_start_distance} (-8), - * {@code water_fog_end_distance} (96, swamps x0.85) and {@code water_fog_color} (default #050533), each blended - * over the biomes around the camera with vanilla's {@code GaussianSampler} (6x6x6 quart cells, kernel 1-4-6-4-1). - * The player's water vision is taken as fully adapted (1: after 30 s under water; {@code LocalPlayer - * .getWaterVision}), 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). + * {@code water_fog_end_distance} (96, swamps x0.85) and {@code water_fog_color} (default #050533), blended over + * 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). */ public record CameraMedium(int fluid, int fogColor, double fogStart, double fogEnd) { public static final CameraMedium AIR = new CameraMedium(FluidRenderer.NONE, 0, 0, 0); - private static final double WATER_VISION = 1.0; private static final double[] KERNEL = {0, 1, 4, 6, 4, 1, 0}; + // Atmospheric fog in air. + 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; + // Vanilla sky disc (SkyRenderer.buildSkyDisc): a fan at height 16 around the camera, rim octagon at 512. private static final double DISC_HEIGHT = 16; private static final double DISC_RADIUS = 512; private static final double DISC_APOTHEM = DISC_RADIUS * Math.cos(Math.PI / 8); + private static final double DISC_RIM_DISTANCE = Math.hypot(DISC_RADIUS, DISC_HEIGHT); public boolean inFluid() { return fluid != FluidRenderer.NONE; } - /** The medium at {@code eye} in {@code snapshot}. */ - public static CameraMedium at(WorldSnapshot snapshot, Vector eye, BiomeTintProvider tints) { + static CameraMedium at(WorldSnapshot snapshot, Vector eye, BiomeTintProvider tints) { double x = eye.getX(), y = eye.getY(), z = eye.getZ(); int fluid = FluidRenderer.cameraFluid(snapshot::getBlockData, x, y, z); if (fluid == FluidRenderer.LAVA) return new CameraMedium(fluid, 0x991A00, 0.25, 1.0); @@ -60,9 +64,9 @@ public record CameraMedium(int fluid, int fogColor, double fogStart, double fogE Biome biome = snapshot.getBiome((ix - 2 + i) * 4, by, (iz - 2 + k) * 4); BiomeClimate.Climate c = biome == null ? BiomeClimate.DEFAULT : tints.climate(biome); int fog = c.waterFogColor(); - r += w * ((fog >> 16) & 0xFF) / 255.0; - g += w * ((fog >> 8) & 0xFF) / 255.0; - b += w * (fog & 0xFF) / 255.0; + r += w * ColorUtil.red(fog) / 255.0; + g += w * ColorUtil.green(fog) / 255.0; + b += w * ColorUtil.blue(fog) / 255.0; start += w * c.waterFogStart(); end += w * c.waterFogEnd(); total += w; @@ -74,14 +78,13 @@ public record CameraMedium(int fluid, int fogColor, double fogStart, double fogE b /= total; start /= total; end /= total; - // FogRenderer.computeFogColor: brighten by water vision towards a maximum channel of 1. + // FogRenderer.computeFogColor with water vision 1: brightened to a maximum channel of 1. if (r != 0 && g != 0 && b != 0) { double scale = 1 / Math.max(r, Math.max(g, b)); - r = r + (r * scale - r) * WATER_VISION; - g = g + (g * scale - g) * WATER_VISION; - b = b + (b * scale - b) * WATER_VISION; + r = r + (r * scale - r); + g = g + (g * scale - g); + b = b + (b * scale - b); } - end *= Math.max(0.25, WATER_VISION); int color = ColorUtil.argb(255, channel(r), channel(g), channel(b)) & 0xFFFFFF; return new CameraMedium(fluid, color, start, end); } @@ -94,26 +97,36 @@ public record CameraMedium(int fluid, int fogColor, double fogStart, double fogE return Math.clamp((int) Math.round(v * 255), 0, 255); } - /** fog.glsl linear_fog_value at distance {@code d}. */ - public double fogValue(double d) { + /** 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); } - /** {@code color} (RGB) fogged at distance {@code d}. */ - public int fog(int color, double d) { + /** {@code color} fogged at distance {@code d}; air fogs towards {@code skyColor}. */ + int fog(int color, double d, int skyColor) { double f = fogValue(d); - return f > 0 ? ColorUtil.mix(color, fogColor, 1 - f, f) : color & 0xFFFFFF; + return f > 0 ? ColorUtil.mix(color, inFluid() ? 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); } /** - * The sky seen in direction {@code (dx, dy, dz)} (unit) from inside the fluid: vanilla draws the sky disc with - * fog over {@code [0, skyEnd = fogEnd]} using the vertex distances interpolated across the fan triangles - * (centre 16, rim 512, plus the cylindrical distance beyond skyEnd = fully fogged); below the horizon and - * beyond the disc only the clear colour (= fog colour) remains. + * The sky seen in direction {@code (dx, dy, dz)} (unit). From inside a fluid vanilla draws the sky disc with fog + * over {@code [0, skyEnd = fogEnd]} using the vertex distances interpolated across the fan triangles (centre 16, + * rim 512, plus the cylindrical distance beyond skyEnd = fully fogged); below the horizon and beyond the disc + * only the clear colour (= fog colour) remains. */ - public int sky(int skyColor, double dx, double dy, double dz) { + int sky(int skyColor, double dx, double dy, double dz) { + if (!inFluid()) return skyColor; if (fluid == FluidRenderer.LAVA || dy <= 1e-9) return fogColor; double horizontal = Math.sqrt(dx * dx + dz * dz); double radius = DISC_HEIGHT * horizontal / dy; @@ -125,7 +138,7 @@ public record CameraMedium(int fluid, int fogColor, double fogStart, double fogE lambda = radius * Math.cos(rel - sector / 2) / DISC_APOTHEM; } if (lambda >= 1) return fogColor; - double spherical = DISC_HEIGHT + (Math.hypot(DISC_RADIUS, DISC_HEIGHT) - DISC_HEIGHT) * lambda; + double spherical = DISC_HEIGHT + (DISC_RIM_DISTANCE - DISC_HEIGHT) * lambda; double cylindrical = DISC_HEIGHT + (DISC_RADIUS - DISC_HEIGHT) * lambda; double f = cylindrical > fogEnd ? 1 : Math.clamp(spherical / fogEnd, 0, 1); return ColorUtil.mix(skyColor, fogColor, 1 - f, f); diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/raytrace/ElementIntersector.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/raytrace/ElementIntersector.java index 715a47a..78f0441 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/raytrace/ElementIntersector.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/raytrace/ElementIntersector.java @@ -11,11 +11,9 @@ import java.util.List; import org.bukkit.util.Vector; /** - * Intersects a ray (in block coordinates) with a {@link ResolvedModel}'s element boxes and returns - * the nearest drawn face hit. Axis-aligned boxes use a slab test; rotated elements are handled as - * oriented boxes by transforming the ray into the element's local frame. Fully transparent texels - * (alpha ≤ threshold) are treated as holes so the ray passes through (cutout for plants, glass); - * partially transparent texels are returned with their alpha, the caller blends them (stained glass, ice). + * Intersects a ray (in block coordinates) with a {@link ResolvedModel}'s element boxes and returns the nearest drawn + * face hit. Rotated elements are slab-tested in their local frame. Cut-out texels are holes the ray passes through; + * partially transparent texels are returned with their alpha for the caller to blend. */ public final class ElementIntersector { @@ -24,10 +22,7 @@ public final class ElementIntersector { private ElementIntersector() {} - /** - * Per-thread scratch of one {@link #intersect} call: the candidate faces as parallel primitive arrays, so - * the hot path allocates nothing per element test (the tracer runs rays in parallel, hence per thread). - */ + /** Per-thread scratch of one {@link #intersect} call: candidate faces as parallel arrays, so nothing is allocated. */ private static final class Scratch { int n; Element[] element = new Element[16]; @@ -44,8 +39,8 @@ public final class ElementIntersector { double hitT, hitS, hitT2, hitNx, hitNy, hitNz; Direction hitDir; - // Same ordering as before (depth; coplanar within 1e-4: later elements first) over candidate indices. - // Sorted with the same TimSort as the former List.sort, so ties resolve identically. + // Depth; coplanar within 1e-4: later elements first. Not transitive, so the tie order depends on the sort + // algorithm: keep Arrays.sort over objects (TimSort). final Comparator byDepth = (a, b) -> { if (Math.abs(t[a] - t[b]) > 1e-4) return Double.compare(t[a], t[b]); return Integer.compare(order[b], order[a]); @@ -94,29 +89,29 @@ public final class ElementIntersector { double[] v = sc.v; for (int i = 0; i < count; i++) { Element element = elements.get(i); - boolean hit; - Element e; + Element e = element; + double eox = ox, eoy = oy, eoz = oz, edx = dx, edy = dy, edz = dz; if (element.unrotated != null) { // Blockstate rotation: hit the model-space element with the inverse-rotated ray (same t: rigid - // transform), so faces and UVs stay bound to the model like in vanilla (uvlock: see Face#uvLock). + // transform), so faces and UVs stay bound to the model like vanilla's FaceBakery. e = element.unrotated; unrotate(v, ox, oy, oz, element.xSteps, element.ySteps, true); - double mox = v[0], moy = v[1], moz = v[2]; + eox = v[0]; + eoy = v[1]; + eoz = v[2]; unrotate(v, dx, dy, dz, element.xSteps, element.ySteps, false); - hit = e.isAxisAligned() - ? slab(sc, mox, moy, moz, v[0], v[1], v[2], e.from, e.to, null) - : intersectObb(sc, e, mox, moy, moz, v[0], v[1], v[2]); - if (hit) { - rotate(v, sc.hitNx, sc.hitNy, sc.hitNz, element.xSteps, element.ySteps, false); - sc.hitNx = v[0]; - sc.hitNy = v[1]; - sc.hitNz = v[2]; - } - } else { - e = element; - hit = e.isAxisAligned() - ? slab(sc, ox, oy, oz, dx, dy, dz, e.from, e.to, null) - : intersectObb(sc, e, ox, oy, oz, dx, dy, dz); + edx = v[0]; + edy = v[1]; + edz = v[2]; + } + boolean hit = e.isAxisAligned() + ? slab(sc, eox, eoy, eoz, edx, edy, edz, e.from, e.to, null) + : intersectObb(sc, e, eox, eoy, eoz, edx, edy, edz); + if (hit && element.unrotated != null) { + rotate(v, sc.hitNx, sc.hitNy, sc.hitNz, element.xSteps, element.ySteps, false); + sc.hitNx = v[0]; + sc.hitNy = v[1]; + sc.hitNz = v[2]; } if (!hit || !(sc.hitT > minT)) continue; int k = sc.n; @@ -135,9 +130,8 @@ public final class ElementIntersector { int n = sc.n; if (n == 0) return null; Integer[] idx = sc.idx; - for (int k = 0; k < n; k++) idx[k] = k; // autoboxing hits the Integer cache for k < 128 - // Sort by depth; for coplanar faces (equal t) render later elements first, matching vanilla's - // draw order so overlays (e.g. the tinted grass side overlay) sit on top of the base face. + for (int k = 0; k < n; k++) idx[k] = k; // Integer cache for k < 128 + // Coplanar faces: later elements first, like vanilla's draw order (overlays such as the grass side tint). if (n > 1) Arrays.sort(idx, 0, n, sc.byDepth); for (int j = 0; j < n; j++) { @@ -156,9 +150,8 @@ public final class ElementIntersector { } /** - * Rotated element: transform the ray into the element's local (unrotated, unscaled) frame, then slab-test. - * Vanilla bakes {@code origin + R·S·(p - origin)} (S = rescale, see {@link Element#invScale}); the inverse is - * affine, so the ray parameter t stays the block-space one. + * Rotated element: slab test in the element's local (unrotated, unscaled) frame. Vanilla bakes + * {@code origin + R·S·(p - origin)} (S = rescale); the inverse is affine, so t stays the block-space one. */ private static boolean intersectObb( Scratch sc, Element e, double ox, double oy, double oz, double dx, double dy, double dz) { @@ -192,7 +185,10 @@ public final class ElementIntersector { } } - /** Slab test; on a hit the entry (or, from inside, exit) face is left in the scratch's hit fields. */ + /** + * Slab test; on a hit the entry (or, from inside, exit) face is left in the scratch's hit fields. {@code obb}: + * the rotated element whose local frame the ray is in (normal rotated back), else null. + */ private static boolean slab( Scratch sc, double ox, @@ -204,14 +200,6 @@ public final class ElementIntersector { double[] from, double[] to, Element obb) { - if (from[0] > to[0] || from[1] > to[1] || from[2] > to[2]) { - return invertedBox(sc, ox, oy, oz, dx, dy, dz, from, to, obb); - } - double tmin = Double.NEGATIVE_INFINITY; - double tmax = Double.POSITIVE_INFINITY; - int axis = -1; - boolean negFace = false; // entered through the low-coordinate face - double[] o = sc.o, d = sc.d; o[0] = ox; o[1] = oy; @@ -219,6 +207,12 @@ public final class ElementIntersector { d[0] = dx; d[1] = dy; d[2] = dz; + if (from[0] > to[0] || from[1] > to[1] || from[2] > to[2]) return invertedBox(sc, from, to, obb); + double tmin = Double.NEGATIVE_INFINITY; + double tmax = Double.POSITIVE_INFINITY; + int axis = -1; + boolean negFace = false; // entered through the low-coordinate face + for (int a = 0; a < 3; a++) { if (Math.abs(d[a]) < EPS) { if (o[a] < from[a] - EPS || o[a] > to[a] + EPS) return false; @@ -247,7 +241,7 @@ public final class ElementIntersector { double tEntry = tmin; if (tEntry < EPS) { - // origin inside the box (e.g. camera within a block): use exit point instead + // Origin inside the box (camera within a block): the exit face. tEntry = tmax; if (tEntry < EPS) return false; } @@ -257,31 +251,10 @@ public final class ElementIntersector { double pz = o[2] + d[2] * tEntry; Direction dir = faceFor(axis, negFace); - if (obb != null) { - // rotate the normal back into block space - // (rescale is diagonal in the local frame, so an axis normal keeps its direction: n = R·n_local) - double[] v = sc.v; - double[] m = obb.rotMatrix; - if (m != null) { - v[0] = m[0] * dir.nx + m[1] * dir.ny + m[2] * dir.nz; - v[1] = m[3] * dir.nx + m[4] * dir.ny + m[5] * dir.nz; - v[2] = m[6] * dir.nx + m[7] * dir.ny + m[8] * dir.nz; - } else { - rotate(v, dir.nx, dir.ny, dir.nz, obb.rotAxis, obb.rotCos, obb.rotSin); - } - sc.hitNx = v[0]; - sc.hitNy = v[1]; - sc.hitNz = v[2]; - } else { - sc.hitNx = dir.nx; - sc.hitNy = dir.ny; - sc.hitNz = dir.nz; - } - + setNormal(sc, obb, dir.nx, dir.ny, dir.nz); double fracX = frac(px, from[0], to[0]); double fracY = frac(py, from[1], to[1]); double fracZ = frac(pz, from[2], to[2]); - sc.hitT = tEntry; sc.hitDir = dir; sc.hitS = faceS(dir, fracX, fracY, fracZ); @@ -294,26 +267,11 @@ public final class ElementIntersector { * bottom): vanilla places the WEST/DOWN/NORTH face at {@code from} and EAST/UP/SOUTH at {@code to} of its axis, * and every inverted OTHER axis mirrors the quad, flipping its winding. So these faces point into the box and * are seen from inside (through the cage: the far wall). Each face is tested as its own back-face-culled quad; - * the nearest visible one wins. Texture coordinates follow the mirrored corners (signed span). + * the nearest visible one wins. Texture coordinates follow the mirrored corners (signed span). The ray is in + * the scratch's {@code o}/{@code d}. */ - private static boolean invertedBox( - Scratch sc, - double ox, - double oy, - double oz, - double dx, - double dy, - double dz, - double[] from, - double[] to, - Element obb) { + private static boolean invertedBox(Scratch sc, double[] from, double[] to, Element obb) { double[] o = sc.o, d = sc.d; - o[0] = ox; - o[1] = oy; - o[2] = oz; - d[0] = dx; - d[1] = dy; - d[2] = dz; double best = Double.POSITIVE_INFINITY; Direction bestDir = null; double bestSign = 0; @@ -335,36 +293,44 @@ public final class ElementIntersector { } if (bestDir == null) return false; int a = bestDir.nx != 0 ? 0 : bestDir.ny != 0 ? 1 : 2; - double nx = a == 0 ? bestSign : 0, ny = a == 1 ? bestSign : 0, nz = a == 2 ? bestSign : 0; - if (obb != null) { - double[] v = sc.v; - double[] m = obb.rotMatrix; - if (m != null) { - v[0] = m[0] * nx + m[1] * ny + m[2] * nz; - v[1] = m[3] * nx + m[4] * ny + m[5] * nz; - v[2] = m[6] * nx + m[7] * ny + m[8] * nz; - } else { - rotate(v, nx, ny, nz, obb.rotAxis, obb.rotCos, obb.rotSin); - } - nx = v[0]; - ny = v[1]; - nz = v[2]; - } - double fx = signedFrac(o[0] + d[0] * best, from[0], to[0]); - double fy = signedFrac(o[1] + d[1] * best, from[1], to[1]); - double fz = signedFrac(o[2] + d[2] * best, from[2], to[2]); + setNormal(sc, obb, a == 0 ? bestSign : 0, a == 1 ? bestSign : 0, a == 2 ? bestSign : 0); + double fx = frac(o[0] + d[0] * best, from[0], to[0]); + double fy = frac(o[1] + d[1] * best, from[1], to[1]); + double fz = frac(o[2] + d[2] * best, from[2], to[2]); sc.hitT = best; sc.hitDir = bestDir; - sc.hitNx = nx; - sc.hitNy = ny; - sc.hitNz = nz; sc.hitS = faceS(bestDir, fx, fy, fz); sc.hitT2 = faceT(bestDir, fy, fz); return true; } + /** + * The hit normal (local axis normal of the element's frame) in block space: rotated back for {@code obb}. The + * rescale is diagonal in the local frame, so an axis normal keeps its direction (n = R·n_local). + */ + private static void setNormal(Scratch sc, Element obb, double nx, double ny, double nz) { + if (obb == null) { + sc.hitNx = nx; + sc.hitNy = ny; + sc.hitNz = nz; + return; + } + double[] m = obb.rotMatrix; + if (m != null) { + sc.hitNx = m[0] * nx + m[1] * ny + m[2] * nz; + sc.hitNy = m[3] * nx + m[4] * ny + m[5] * nz; + sc.hitNz = m[6] * nx + m[7] * ny + m[8] * nz; + } else { + double[] v = sc.v; + rotate(v, nx, ny, nz, obb.rotAxis, obb.rotCos, obb.rotSin); + sc.hitNx = v[0]; + sc.hitNy = v[1]; + sc.hitNz = v[2]; + } + } + /** Position between {@code from} (0) and {@code to} (1), also when {@code from > to}; clamped to 0..1. */ - private static double signedFrac(double v, double from, double to) { + private static double frac(double v, double from, double to) { double span = to - from; if (Math.abs(span) < 1e-6) return 0; double f = (v - from) / span; @@ -372,10 +338,9 @@ public final class ElementIntersector { } /** - * Face-local horizontal coordinate s in 0..1 for a hit at block fractions (fx, fy, fz), matching vanilla's - * {@code BlockElement.uvsByFace}: a face's UV rect {@code [u1,v1,u2,v2]} is lerped with (s, t), so north/east - * faces run their horizontal axis towards -x/-z (left-to-right seen from outside) and down runs v towards - * north. Shared with the entity intersector (entity box-UV uses the same orientation). + * Face-local horizontal coordinate s in 0..1 for a hit at box fractions (fx, fy, fz), like vanilla's + * {@code BlockElement.uvsByFace}: north/east faces run towards -x/-z (left to right seen from outside), down runs + * v towards north. Shared with the entity intersector (box UVs have the same orientation). */ public static double faceS(Direction dir, double fx, double fy, double fz) { return switch (dir) { @@ -403,14 +368,7 @@ public final class ElementIntersector { }; } - private static double frac(double v, double lo, double hi) { - double span = hi - lo; - if (span < 1e-6) return 0; - double f = (v - lo) / span; - return f < 0 ? 0 : Math.min(f, 1); - } - - /** Rotate (x,y,z) around the given axis (0=x,1=y,2=z) by the angle with the given cos/sin, into out. */ + /** Rotates (x, y, z) about axis 0/1/2 (x/y/z) by the angle with the given cos/sin into out. */ private static void rotate(double[] out, double x, double y, double z, int axis, double cos, double sin) { switch (axis) { case 0 -> { @@ -431,7 +389,7 @@ public final class ElementIntersector { } } - /** {@link Element#rotate} into out (same arithmetic, no allocation). */ + /** {@link Element#rotate} into out (same arithmetic, without allocating). */ private static void rotate(double[] out, double x, double y, double z, int xSteps, int ySteps, boolean point) { double c = point ? 0.5 : 0; double dx = x - c, dy = y - c, dz = z - c; @@ -450,7 +408,7 @@ public final class ElementIntersector { out[2] = dz + c; } - /** {@link Element#unrotate} into out (same arithmetic, no allocation). */ + /** {@link Element#unrotate} into out (same arithmetic, without allocating). */ private static void unrotate(double[] out, double x, double y, double z, int xSteps, int ySteps, boolean point) { rotate(out, x, y, z, 0, (4 - ySteps) % 4, point); rotate(out, out[0], out[1], out[2], (4 - xSteps) % 4, 0, point); diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/raytrace/FaceHit.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/raytrace/FaceHit.java index e4686b7..5958c8d 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/raytrace/FaceHit.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/raytrace/FaceHit.java @@ -3,10 +3,9 @@ package eu.mhsl.minecraft.pixelpics.render.raytrace; import org.bukkit.util.Vector; /** - * The result of intersecting a ray with a block's geometry: the world-space hit point and normal, - * the sampled ARGB color (before shading/tinting), the face's tint index ({@code -1} = none), whether the - * surface is drawn additively (emissive energy-swirl layers: its colour is added to what lies behind) and whether - * it skips directional face shading (entity layers without cardinal lighting: eyes, breeze wind). + * A ray hit on block or entity geometry: world-space point and normal, sampled ARGB texel (before shading/tint), + * tint index (-1 = none), additive (emissive energy swirl, added to what lies behind) and unshaded (no cardinal + * lighting: eyes, breeze wind). */ public record FaceHit( double t, Vector point, Vector normal, int color, int tintIndex, boolean additive, boolean unshaded) {} diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/raytrace/FluidRenderer.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/raytrace/FluidRenderer.java index 8fe88a6..172e0bd 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/raytrace/FluidRenderer.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/raytrace/FluidRenderer.java @@ -18,26 +18,21 @@ import org.bukkit.block.data.Levelled; import org.bukkit.block.data.Waterlogged; /** - * Water and lava like vanilla 26.3 {@code FluidRenderer} (decompiled client): fluids are not block models but - * polygons built per cell from the fluid state and its neighbours: a top surface whose four corner heights come - * from the cell's level (amount / 9, source 8/9, full when the same fluid lies above) averaged with the - * neighbours ({@code calculateAverageHeight}, split into the two triangles NW-SW-SE / NW-SE-NE), side faces - * against every neighbour that is not the same fluid and does not cover the face, and a bottom face. Top and - * side faces are double-sided (the top only when {@code shouldRenderBackwardUpFace}); side faces against - * {@code HalfTransparentBlock}/{@code LeavesBlock} use the water overlay texture and are one-sided; the bottom is - * one-sided (seen from below). Waterlogged blocks and seagrass/kelp/bubble columns hold a water source. - * - *

The tracer asks for the nearest drawn fluid polygon a ray crosses inside one cell ({@link #nearest}); the - * caller tints/lights it and blends it as a translucent layer (water texture alpha 180) or ends the ray on it - * (lava, opaque). + * Water and lava like vanilla 26.3 {@code FluidRenderer} (decompiled client): not block models but polygons built + * per cell from the fluid state and its neighbours. The top surface has four corner heights (amount / 9, full + * under the same fluid, averaged with the neighbours like {@code calculateAverageHeight}) split into the triangles + * NW-SW-SE / NW-SE-NE; side faces against every neighbour that is not the same fluid and does not cover the face; + * a bottom face. Top and sides are double-sided (the top only if {@code shouldRenderBackwardUpFace}), except side + * faces against {@code HalfTransparentBlock}/{@code LeavesBlock}, which use the overlay texture one-sided; the + * bottom is seen from below only. Waterlogged blocks and seagrass/kelp/bubble columns hold a water source. */ public final class FluidRenderer { public static final int NONE = 0; - public static final int WATER = 1; - public static final int LAVA = 2; + static final int WATER = 1; + static final int LAVA = 2; - // Vanilla FluidRenderer insets: faces 0.001 inside the cell, the top 0.001 below the computed height. + // Vanilla insets: faces 0.001 inside the cell, the top 0.001 below the computed height. private static final float OFFS = 0.001F; private static final double EPS = 1e-9; @@ -47,13 +42,31 @@ public final class FluidRenderer { BlockData at(int x, int y, int z); } - /** Blocks whose fluid state is always a water source (LiquidBlockContainer without a waterlogged property). */ + // LiquidBlockContainers without a waterlogged property: always a water source. private static final EnumSet ALWAYS_WATER = materials("SEAGRASS", "TALL_SEAGRASS", "KELP", "KELP_PLANT", "BUBBLE_COLUMN"); - // Vanilla HalfTransparentBlock / LeavesBlock: water draws its side faces against them with the overlay texture, - // one-sided (FluidRenderer isOverlay). Blue ice is one too but occludes (its face is culled anyway). - private static final EnumSet OVERLAY_NEIGHBOURS = buildOverlayNeighbours(); + // Vanilla HalfTransparentBlock / LeavesBlock (FluidRenderer isOverlay). Blue ice too, but it occludes anyway. + private static final EnumSet OVERLAY_NEIGHBOURS = EnumSet.noneOf(Material.class); + // Vanilla tag blocks_fluid_flow = #blocks_motion (Material.isSolid) + #all_signs. + private static final EnumSet SIGNS = EnumSet.noneOf(Material.class); + + static { + for (Material m : Material.values()) { + if (m.isLegacy()) continue; + String n = m.name(); + if (n.equals("GLASS") + || n.equals("TINTED_GLASS") + || n.endsWith("_STAINED_GLASS") + || n.equals("ICE") + || n.equals("FROSTED_ICE") + || n.equals("BLUE_ICE") + || n.equals("SLIME_BLOCK") + || n.equals("HONEY_BLOCK") + || n.endsWith("_LEAVES")) OVERLAY_NEIGHBOURS.add(m); + if (n.endsWith("_SIGN")) SIGNS.add(m); + } + } private static EnumSet materials(String... names) { EnumSet set = EnumSet.noneOf(Material.class); @@ -67,30 +80,12 @@ public final class FluidRenderer { return set; } - private static EnumSet buildOverlayNeighbours() { - EnumSet set = EnumSet.noneOf(Material.class); - for (Material m : Material.values()) { - if (m.isLegacy()) continue; - String n = m.name(); - if (n.equals("GLASS") - || n.equals("TINTED_GLASS") - || n.endsWith("_STAINED_GLASS") - || n.equals("ICE") - || n.equals("FROSTED_ICE") - || n.equals("BLUE_ICE") - || n.equals("SLIME_BLOCK") - || n.equals("HONEY_BLOCK") - || n.endsWith("_LEAVES")) set.add(m); - } - return set; - } - // --- fluid state: type (bits 0-1), amount 1..8 (bits 2-5), falling (bit 6), source (bit 7) ------------- private static final int WATER_SOURCE = WATER | 8 << 2 | 1 << 7; /** The fluid state of a block (vanilla {@code BlockState.getFluidState}), packed; 0 = no fluid. */ - public static int state(BlockData data) { + static int state(BlockData data) { Material m = data.getMaterial(); if (m == Material.AIR) return 0; if (m == Material.WATER || m == Material.LAVA) { @@ -106,7 +101,7 @@ public final class FluidRenderer { return 0; } - public static int type(int state) { + static int type(int state) { return state & 3; } @@ -125,8 +120,8 @@ public final class FluidRenderer { /** * Which fluid the camera at {@code (x, y, z)} is in (vanilla {@code Camera.getFluidInCamera}): the eye block's - * fluid, when the eye is below its camera height ({@code FluidState.getHeightForCamera}: 1 for a source under a - * block with a sturdy bottom face or with the same fluid above, else amount / 9). + * fluid when the eye is below its {@code FluidState.getHeightForCamera} (1 under the same fluid or for a source + * under a sturdy bottom face, else amount / 9). */ public static int cameraFluid(BlockLookup level, double x, double y, double z) { int bx = (int) Math.floor(x), by = (int) Math.floor(y), bz = (int) Math.floor(z); @@ -134,14 +129,7 @@ public final class FluidRenderer { int type = type(s); if (type == NONE) return NONE; BlockData above = level.at(bx, by + 1, bz); - float h; - if (type(state(above)) == type) { - h = 1; - } else if (source(s) && sturdyBottom(above)) { - h = 1; - } else { - h = ownHeight(s); - } + float h = type(state(above)) == type || (source(s) && sturdyBottom(above)) ? 1 : ownHeight(s); double rel = y - by; // Water: strictly below the surface; lava (near-plane check): at or below it. return type == WATER ? (rel < h ? WATER : NONE) : (rel <= h ? LAVA : NONE); @@ -159,11 +147,11 @@ public final class FluidRenderer { // --- sky light under water ---------------------------------------------------------------------------- /** - * Vanilla sky light level of the cell (x, y, z) when water lies above it: 15 minus one level per block from - * this cell up to the topmost water block of the column (water costs one level, non-occluding blocks below the - * surface such as glass at least one). Cells without water above them (in the next 15 blocks) get 15. + * Vanilla sky light level of the cell (x, y, z) under water: 15 minus one per block from this cell up to the + * topmost water block of the column (water and non-occluding blocks below the surface cost one level each). + * 15 without water in the next 15 blocks. */ - public static int skyLevel(BlockLookup level, int x, int y, int z) { + static int skyLevel(BlockLookup level, int x, int y, int z) { int water = 0; for (int i = 0; i < 15; i++) { BlockData d = level.at(x, y + i, z); @@ -181,12 +169,13 @@ public final class FluidRenderer { private final BlockModelRegistry registry; private final int[][] waterStill, waterFlow, waterOverlay, lavaStill, lavaFlow; - // Bit per Direction: the block model covers that whole cell face with opaque texels (vanilla face occlusion - // shape of blocks that can occlude). Identity keys (ResolvedModel is cached per BlockData). + // Bit per Direction: the model covers that whole cell face with opaque texels (approximates vanilla's face + // occlusion shape). Identity keys: ResolvedModel is cached per BlockData. private final Map fullFaces = new ConcurrentHashMap<>(); - public FluidRenderer(BlockModelRegistry registry, TextureCache textures) { + FluidRenderer(BlockModelRegistry registry) { this.registry = registry; + TextureCache textures = registry.textures(); this.waterStill = texture(textures, "block/water_still", 0xB4B1B1B1); this.waterFlow = texture(textures, "block/water_flow", 0xB4AAAAAA); this.waterOverlay = texture(textures, "block/water_overlay", 0xB4A5A5A5); @@ -210,9 +199,12 @@ public final class FluidRenderer { // --- face occlusion ------------------------------------------------------------------------------------ private int fullFaces(BlockData data) { - Material m = data.getMaterial(); - if (m.isAir()) return 0; - return fullFaces.computeIfAbsent(registry.get(data), FluidRenderer::computeFullFaces); + if (data.getMaterial().isAir()) return 0; + return fullFaces(registry.get(data)); + } + + private int fullFaces(ResolvedModel model) { + return fullFaces.computeIfAbsent(model, FluidRenderer::computeFullFaces); } /** Faces fully covered by opaque, unrotated element faces on the cell boundary (8x8 sample grid). */ @@ -253,14 +245,18 @@ public final class FluidRenderer { return true; } - /** FluidRenderer.isFaceOccludedByState for a neighbour/self block whose face points at the fluid. */ + /** FluidRenderer.isFaceOccludedByState for a neighbour block whose face points at the fluid. */ private boolean occludes(BlockData block, Direction face) { - return (fullFaces(block) & 1 << face.ordinal()) != 0; + return covers(fullFaces(block), face); + } + + private static boolean covers(int fullFaces, Direction face) { + return (fullFaces & 1 << face.ordinal()) != 0; } // --- heights ------------------------------------------------------------------------------------------- - /** FluidRenderer.getHeight: 1 under the same fluid, own height in it, 0 next to air etc., -1 next to solids. */ + /** FluidRenderer.getHeight: 1 under the same fluid, own height in it, 0 for non-solid blocks, -1 for solids. */ private static float height(BlockLookup level, int type, int x, int y, int z, BlockData data, int state) { if (type(state) == type) { return type(state(level.at(x, y + 1, z))) == type ? 1.0F : ownHeight(state); @@ -273,42 +269,36 @@ public final class FluidRenderer { return height(level, type, x, y, z, d, state(d)); } + /** FluidRenderer.calculateAverageHeight (weights: 10 for heights >= 0.8, 1 below, solids (-1) none). */ private static float averageHeight( BlockLookup level, int type, float self, float h2, float h1, int cx, int cy, int cz) { if (h1 >= 1.0F || h2 >= 1.0F) return 1.0F; - float[] w = new float[2]; + float corner = -1.0F; if (h1 > 0.0F || h2 > 0.0F) { - float corner = height(level, type, cx, cy, cz); + corner = height(level, type, cx, cy, cz); if (corner >= 1.0F) return 1.0F; - addWeighted(w, corner); } - addWeighted(w, self); - addWeighted(w, h1); - addWeighted(w, h2); - return w[0] / w[1]; + float wc = weight(corner), ws = weight(self), w1 = weight(h1), w2 = weight(h2); + // Same summation order as vanilla's running sums. + return (0.0F + corner * wc + self * ws + h1 * w1 + h2 * w2) / (0.0F + wc + ws + w1 + w2); } - private static void addWeighted(float[] w, float h) { - if (h >= 0.8F) { - w[0] += h * 10.0F; - w[1] += 10.0F; - } else if (h >= 0.0F) { - w[0] += h; - w[1]++; - } + private static float weight(float h) { + return h >= 0.8F ? 10.0F : h >= 0.0F ? 1.0F : 0.0F; } // --- flow (top texture direction) --------------------------------------------------------------------- + private static final int[][] HORIZONTAL = {{0, -1}, {1, 0}, {0, 1}, {-1, 0}}; + /** - * FlowingFluid.getFlow, horizontal part ({x, z}, not normalised; the falling tweak only adds a y component, - * which does not change the texture angle). {0, 0} = still texture. + * Texture angle of the flowing top sprite from FlowingFluid.getFlow (horizontal part; the falling tweak only adds + * y), or NaN for the still texture. */ - private static double[] flow(BlockLookup level, int type, int x, int y, int z, int state) { + private static float flowAngle(BlockLookup level, int type, int x, int y, int z, int state) { double fx = 0, fz = 0; float own = ownHeight(state); - int[][] dirs = {{0, -1}, {1, 0}, {0, 1}, {-1, 0}}; - for (int[] d : dirs) { + for (int[] d : HORIZONTAL) { BlockData nd = level.at(x + d[0], y, z + d[1]); int ns = state(nd); if (!affectsFlow(type, ns)) continue; @@ -331,8 +321,8 @@ public final class FluidRenderer { } } // Vec3.normalize: below 1e-5 the vector is zero (still texture). - if (Math.sqrt(fx * fx + fz * fz) < 1.0E-5) return new double[] {0, 0}; - return new double[] {fx, fz}; + if (Math.sqrt(fx * fx + fz * fz) < 1.0E-5) return Float.NaN; + return (float) Math.atan2(fz, fx) - (float) (Math.PI / 2); } private static boolean affectsFlow(int type, int neighbourState) { @@ -340,10 +330,9 @@ public final class FluidRenderer { return t == NONE || t == type; } - /** Vanilla tag blocks_fluid_flow (= #blocks_motion + #all_signs). */ private static boolean blocksFluidFlow(BlockData data) { Material m = data.getMaterial(); - return m.isSolid() || m.name().endsWith("_SIGN"); + return m.isSolid() || SIGNS.contains(m); } /** FluidState.shouldRenderBackwardUpFace: some block of the 3x3 above is neither the fluid nor solid-render. */ @@ -359,27 +348,25 @@ public final class FluidRenderer { // --- ray crossing -------------------------------------------------------------------------------------- - /** A drawn fluid polygon crossed by a ray: texel (ARGB), shade, face, outward normal and side. */ - public static final class Crossing { - public double t; - public int texel; - public double shade; - public int type; - public boolean entering; // the ray comes from outside the fluid (front side) - public double nx, ny, nz; // outward normal (unit) - public int lightX, lightY, lightZ; // cell whose light (max with the one above) lights the face + /** A drawn fluid polygon crossed by a ray. */ + static final class Crossing { + double t; + int texel; + boolean entering; // the ray comes from outside the fluid (front side) + double nx, ny, nz; // outward unit normal + boolean bottom; // the bottom face, lit by the cell below } /** - * The nearest fluid polygon of cell (bx, by, bz) (holding {@code data} with fluid {@code state}) crossed by - * the ray {@code o + t·d} with {@code t > minT}, or null. + * The nearest drawn fluid polygon of cell (bx, by, bz) (block model {@code model}, fluid {@code state}) crossed + * by the ray {@code o + t·d} with {@code t > minT}, or null. */ - public Crossing nearest( + Crossing nearest( BlockLookup level, int bx, int by, int bz, - BlockData data, + ResolvedModel model, int state, double ox, double oy, @@ -392,22 +379,22 @@ public final class FluidRenderer { BlockData down = level.at(bx, by - 1, bz), up = level.at(bx, by + 1, bz); BlockData north = level.at(bx, by, bz - 1), south = level.at(bx, by, bz + 1); BlockData west = level.at(bx - 1, by, bz), east = level.at(bx + 1, by, bz); - int sDown = state(down), sUp = state(up), sNorth = state(north), sSouth = state(south); - int sWest = state(west), sEast = state(east); - boolean sameUp = type(sUp) == type, sameDown = type(sDown) == type; + int sNorth = state(north), sSouth = state(south), sWest = state(west), sEast = state(east); + boolean sameUp = type(state(up)) == type, sameDown = type(state(down)) == type; boolean sameN = type(sNorth) == type, sameS = type(sSouth) == type; boolean sameW = type(sWest) == type, sameE = type(sEast) == type; - if (sameUp && sameDown && sameN && sameS && sameW && sameE) return null; // inside a body of fluid + // Own faces covering the cell faces; only looked up when a face other than the top may be drawn. + int selfFaces = sameDown && sameN && sameS && sameW && sameE ? 0 : fullFaces(model); boolean renderUp = !sameUp; - boolean renderDown = !sameDown && !occludes(data, Direction.DOWN) && !occludes(down, Direction.UP); - boolean renderN = !sameN && !occludes(data, Direction.NORTH); - boolean renderS = !sameS && !occludes(data, Direction.SOUTH); - boolean renderW = !sameW && !occludes(data, Direction.WEST); - boolean renderE = !sameE && !occludes(data, Direction.EAST); + boolean renderDown = !sameDown && !covers(selfFaces, Direction.DOWN) && !occludes(down, Direction.UP); + boolean renderN = !sameN && !covers(selfFaces, Direction.NORTH); + boolean renderS = !sameS && !covers(selfFaces, Direction.SOUTH); + boolean renderW = !sameW && !covers(selfFaces, Direction.WEST); + boolean renderE = !sameE && !covers(selfFaces, Direction.EAST); if (!(renderUp || renderDown || renderN || renderS || renderW || renderE)) return null; - float self = height(level, type, bx, by, bz, data, state); + float self = sameUp ? 1.0F : ownHeight(state); float hNE, hNW, hSE, hSW; if (self >= 1.0F) { hNE = hNW = hSE = hSW = 1.0F; @@ -461,7 +448,7 @@ public final class FluidRenderer { double x = lx + dx * t, z = lz + dz * t; if (x < -EPS || x > 1 + EPS || z < -EPS || z > 1 + EPS) continue; if (tri == 0 ? z < x - EPS : z >= x + EPS) continue; - boolean entering = denom < 0; // dot(d, n) = dy - b·dx - c·dz + boolean entering = denom < 0; if (!entering) { if (backUp == null) backUp = backwardUpFace(level, type, bx, by + 1, bz); if (!backUp) continue; @@ -479,7 +466,6 @@ public final class FluidRenderer { } } if (renderDown && dy > 0) { - // One-sided, seen from below only. double t = (bottom - ly) / dy; if (t > minT && t < bestT) { double x = lx + dx * t, z = lz + dz * t; @@ -496,73 +482,60 @@ public final class FluidRenderer { } } } - // Side faces: plane, outward normal, edge heights from vertex 0 to vertex 1 (vanilla order) along s. + // Side faces: the neighbour's face towards the fluid, plane, outward sign and the edge heights from vertex 0 + // to vertex 1 in vanilla's order (N: west->east, S: east->west, W: south->north, E: north->south). for (int face = 2; face <= 5; face++) { boolean render; BlockData neighbour; - double plane; + Direction neighbourFace; float h0, h1; switch (face) { case 2 -> { render = renderN; neighbour = north; - plane = OFFS; + neighbourFace = Direction.SOUTH; h0 = hNW; h1 = hNE; } case 3 -> { render = renderS; neighbour = south; - plane = 1 - OFFS; + neighbourFace = Direction.NORTH; h0 = hSE; h1 = hSW; } case 4 -> { render = renderW; neighbour = west; - plane = OFFS; + neighbourFace = Direction.EAST; h0 = hSW; h1 = hNW; } default -> { render = renderE; neighbour = east; - plane = 1 - OFFS; + neighbourFace = Direction.WEST; h0 = hNE; h1 = hSE; } } if (!render) continue; boolean alongX = face == 2 || face == 3; + boolean negative = face == 2 || face == 4; + double plane = negative ? OFFS : 1 - OFFS; double dAxis = alongX ? dz : dx; if (Math.abs(dAxis) < 1e-12) continue; double t = (plane - (alongX ? lz : lx)) / dAxis; if (!(t > minT) || t >= bestT) continue; - double outward = face == 2 || face == 4 ? -1 : 1; + double outward = negative ? -1 : 1; boolean entering = dAxis * outward < 0; - Direction toNeighbour = - switch (face) { - case 2 -> Direction.NORTH; - case 3 -> Direction.SOUTH; - case 4 -> Direction.WEST; - default -> Direction.EAST; - }; - // Covered by the neighbour's face towards the fluid (full block face; partial shapes never cover the - // whole fluid face here). - if (occludes(neighbour, opposite(toNeighbour))) continue; + if (occludes(neighbour, neighbourFace)) continue; boolean overlay = type == WATER && OVERLAY_NEIGHBOURS.contains(neighbour.getMaterial()); if (!entering && overlay) continue; double x = lx + dx * t, y = ly + dy * t, z = lz + dz * t; double along = alongX ? x : z; if (along < -EPS || along > 1 + EPS) continue; - // s: position from vertex 0 to vertex 1 (N: west->east, S: east->west, W: south->north, E: north->south) - double s = - switch (face) { - case 2 -> x; - case 3 -> 1 - x; - case 4 -> 1 - z; - default -> z; - }; + double s = alongX ? (negative ? x : 1 - x) : (negative ? 1 - z : z); double top = h0 + (h1 - h0) * s; if (y < bottom - EPS || y > top + EPS) continue; bestT = t; @@ -581,51 +554,29 @@ public final class FluidRenderer { Crossing best = new Crossing(); best.t = bestT; - best.type = type; best.entering = bestEntering; best.nx = bestNx; best.ny = bestNy; best.nz = bestNz; - best.lightX = bx; - best.lightY = bestFace == 1 ? by - 1 : by; - best.lightZ = bz; + best.bottom = bestFace == 1; boolean water = type == WATER; - switch (bestFace) { - case 0 -> { - double[] f = flow(level, type, bx, by, bz, state); - if (f[0] == 0 && f[1] == 0) { - best.texel = sample(water ? waterStill : lavaStill, hitX, hitZ); - } else { - // Flowing sprite turned to the flow (vanilla UVs at the corners; affine over the cell). - float angle = (float) Math.atan2(f[1], f[0]) - (float) (Math.PI / 2); - double sn = Math.sin(angle) * 0.25F, cs = Math.cos(angle) * 0.25F; - double u = 0.5 - cs - sn + 2 * cs * hitX + 2 * sn * hitZ; - double v = 0.5 - cs + sn - 2 * sn * hitX + 2 * cs * hitZ; - best.texel = sample(water ? waterFlow : lavaFlow, u, v); - } - best.shade = 1.0; - } - case 1 -> { - best.texel = sample(water ? waterStill : lavaStill, hitX, hitZ); - best.shade = 0.5; - } - default -> { - int[][] sprite = sideOverlay ? waterOverlay : water ? waterFlow : lavaFlow; - best.texel = sample(sprite, sideS * 0.5, (1 - hitY) * 0.5); - best.shade = bestFace <= 3 ? 0.8 : 0.6; + int[][] still = water ? waterStill : lavaStill, flow = water ? waterFlow : lavaFlow; + if (bestFace == 0) { + float angle = flowAngle(level, type, bx, by, bz, state); + if (Float.isNaN(angle)) { + best.texel = sample(still, hitX, hitZ); + } else { + // Flowing sprite turned to the flow (vanilla UVs at the corners; affine over the cell). + double sn = Math.sin(angle) * 0.25F, cs = Math.cos(angle) * 0.25F; + double u = 0.5 - cs - sn + 2 * cs * hitX + 2 * sn * hitZ; + double v = 0.5 - cs + sn - 2 * sn * hitX + 2 * cs * hitZ; + best.texel = sample(flow, u, v); } + } else if (bestFace == 1) { + best.texel = sample(still, hitX, hitZ); + } else { + best.texel = sample(sideOverlay ? waterOverlay : flow, sideS * 0.5, (1 - hitY) * 0.5); } return best; } - - private static Direction opposite(Direction d) { - return switch (d) { - case NORTH -> Direction.SOUTH; - case SOUTH -> Direction.NORTH; - case WEST -> Direction.EAST; - case EAST -> Direction.WEST; - case UP -> Direction.DOWN; - case DOWN -> Direction.UP; - }; - } } 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 8772c28..8e56685 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 @@ -19,25 +19,19 @@ import org.bukkit.block.data.BlockData; import org.bukkit.util.Vector; /** - * Traces a single ray against a {@link WorldSnapshot}, sampling block models via the - * {@link ElementIntersector} and applying biome tint, directional face shading, alpha blending of - * translucent surfaces, liquid transparency and reflection. Contains no Bukkit world access, so it is safe to invoke from worker threads. + * Traces a single ray against a {@link WorldSnapshot}: block models ({@link ElementIntersector}), fluid polygons + * ({@link FluidRenderer}) and entities, with biome tint, face shading, ambient occlusion, alpha blending of + * translucent surfaces, fog and water reflection. No Bukkit world access, so it runs on worker threads. */ public final class SnapshotRaytracer { private static final int BIOME_BLEND_RADIUS = 2; - // Distance fog (atmospheric perspective). - private static final double FOG_START = 128; - private static final double FOG_END = 256; - private static final double FOG_MAX = 0.75; - - // Vanilla-style ambient occlusion brightness per occlusion level (0=most occluded .. 3=open). Kept subtle. + // Vanilla-style ambient occlusion brightness per occlusion level (0 = most occluded .. 3 = open), kept subtle. private static final double[] AO_BRIGHTNESS = {0.55, 0.70, 0.85, 1.0}; - // Alpha blending: texels with alpha >= OPAQUE_ALPHA end the ray; translucent ones (stained glass, ice, - // slime outer layer, text-display backgrounds) are blended over what lies behind them. Bounded so a - // stack of glass costs at most a few extra intersection rounds per ray. + // Texels with alpha >= OPAQUE_ALPHA end the ray; translucent ones are blended over what lies behind them. + // Bounded so a stack of glass costs at most a few extra intersection rounds per ray. private static final int OPAQUE_ALPHA = 250; private static final int MAX_TRANSLUCENT_LAYERS = 6; private static final double MIN_TRANSMITTANCE = 0.03; @@ -45,14 +39,12 @@ public final class SnapshotRaytracer { // layerAlpha marker of an additive (emissive) layer: its colour is added to what lies behind. private static final double ADDITIVE = -1; - // Water reflection (not vanilla; kept as before): weight of the reflected ray on the first water surface hit - // from above (rays starting in the air only). + // Water reflection (not vanilla): weight of the reflected ray on the first water surface entered from the air. private static final double WATER_REFLECTION = 0.10; - // Brightness by sky light level (index): vanilla lightmap (lightmap.fsh: level / (4 - 3·level), mixed halfway - // with notGamma = default brightness option 0.5, overworld ambient 0). Sky light loses one level per water - // block (water's light block = 1), so surfaces under water get darker with VERTICAL depth (FluidRenderer - // .skyLevel); PixelPics has no other light model, so this only applies under water. + // Brightness by sky light level: vanilla lightmap.fsh (level / (4 - 3·level), mixed halfway with notGamma at the + // default brightness 0.5, overworld ambient 0). PixelPics has no other light model, so it only matters under + // water, where sky light drops by vertical water depth (FluidRenderer.skyLevel). SKY_LIGHT[15] == 1. private static final double[] SKY_LIGHT = new double[16]; static { @@ -80,7 +72,7 @@ public final class SnapshotRaytracer { double maxDistance, int reflectionDepth) { this.registry = registry; - this.fluids = new FluidRenderer(registry, registry.textures()); + this.fluids = new FluidRenderer(registry); this.tintProvider = tintProvider; this.skyRenderer = skyRenderer; this.maxDistance = maxDistance; @@ -88,16 +80,11 @@ public final class SnapshotRaytracer { this.maxSteps = (int) (maxDistance * 3) + 3; } - /** The fluid the camera at {@code origin} is in and its fog (once per photo, see {@link CameraMedium}). */ + /** The medium (air or fluid with its fog) the camera at {@code origin} is in; once per photo. */ public CameraMedium cameraMedium(WorldSnapshot snapshot, Vector origin) { return CameraMedium.at(snapshot, origin, tintProvider); } - public int trace(WorldSnapshot snapshot, Vector origin, Vector direction, SkyContext sky, EntityScene scene) { - return trace(snapshot, origin, direction, sky, scene, cameraMedium(snapshot, origin)); - } - - /** As {@link #trace(WorldSnapshot, Vector, Vector, SkyContext, EntityScene)} with the camera medium given. */ public int trace( WorldSnapshot snapshot, Vector origin, @@ -119,26 +106,25 @@ public final class SnapshotRaytracer { double ox = origin.getX(), oy = origin.getY(), oz = origin.getZ(); double dx = direction.getX(), dy = direction.getY(), dz = direction.getZ(); - int skyColor = skyRenderer.colorFor(direction, origin, sky); BlockCursor blocks = new BlockCursor(snapshot, origin, direction, sky, scene, depth, medium); boolean entities = scene != null && !scene.isEmpty(); // Translucent surfaces in front of the final one, front to back (shaded colour, alpha 0..1 or ADDITIVE, - // ray t, already lit: fluid faces carry their own sky light). + // ray t, already lit). int[] layerColor = null; double[] layerAlpha = null, layerT = null; boolean[] layerLit = null; int layers = 0; double transmittance = 1; - int finalColor = skyColor; + int finalColor = 0; double finalT = Double.POSITIVE_INFINITY; // sky boolean finalLit = false; double minT = 0; BlockHit pending = null; // nearest block surface not consumed yet - // Walk the surfaces along the ray in depth order (blocks, fluid faces and entities interleaved); - // translucent ones are collected as layers and the ray continues behind them, the first opaque one ends it. + // Surfaces in depth order (blocks, fluid faces and entities interleaved): translucent ones become layers, + // the first opaque one ends the ray. while (true) { if (pending == null) pending = blocks.next(minT); double limit = pending != null ? pending.t : maxDistance; @@ -148,8 +134,7 @@ public final class SnapshotRaytracer { boolean additive = false; boolean lit = false; if (eh != null) { - // Emissive additive surfaces (energy swirl) and layers without cardinal lighting (eyes, breeze - // wind) are unshaded: vanilla NO_CARDINAL_LIGHTING (+ EMISSIVE). + // Vanilla NO_CARDINAL_LIGHTING: additive energy swirl, eyes, breeze wind. additive = eh.additive(); boolean flat = additive || eh.unshaded(); color = flat ? eh.color() : ColorUtil.shade(eh.color(), shadeFactor(eh.normal())); @@ -165,8 +150,7 @@ public final class SnapshotRaytracer { int alpha = ColorUtil.alpha(color); double a = alpha / 255.0; if (additive) { - // Additive light never hides what lies behind it: it neither ends the ray nor dims it. Beyond the - // layer budget it is dropped. + // Additive light neither ends nor dims the ray; beyond the layer budget it is dropped. if (layers == MAX_TRANSLUCENT_LAYERS) { minT = t + LAYER_EPS; continue; @@ -174,7 +158,6 @@ public final class SnapshotRaytracer { } else if (alpha >= OPAQUE_ALPHA || layers == MAX_TRANSLUCENT_LAYERS || transmittance * (1 - a) < MIN_TRANSMITTANCE) { - // Opaque, or what lies behind would contribute next to nothing: this surface ends the ray. finalColor = color; finalT = t; finalLit = lit; @@ -195,51 +178,35 @@ public final class SnapshotRaytracer { minT = t + LAYER_EPS; } - // Compose back to front: final surface, translucent layers (fluid faces among them), each fogged. - // Surfaces under water are dimmed by their depth like vanilla sky light. + // Compose back to front, each surface fogged; surfaces under water dimmed by their sky light. The sky + // colour is only computed when something reads it (most rays end on a near surface). + int skyColor = medium.needsSkyColor(finalT) ? skyRenderer.colorFor(direction, origin, sky) : 0; int color; if (Double.isInfinite(finalT)) { - color = medium.inFluid() ? medium.sky(finalColor, dx, dy, dz) : finalColor; + color = medium.sky(skyColor, dx, dy, dz); } else { int lit = finalLit ? finalColor : blocks.underwaterLight(finalColor, finalT); - color = fog(lit, finalT, skyColor, medium); + color = medium.fog(lit, finalT, skyColor); } for (int i = layers - 1; i >= 0; i--) { if (layerAlpha[i] == ADDITIVE) { - // Added light fades with distance like the surface it would otherwise be. - double f = medium.inFluid() ? medium.fogValue(layerT[i]) : fogFactor(layerT[i]); + double f = medium.fogValue(layerT[i]); color = ColorUtil.add(color, ColorUtil.shade(layerColor[i], 1 - f)); } else { int layer = layerLit[i] ? layerColor[i] : blocks.underwaterLight(layerColor[i], layerT[i]); - color = ColorUtil.mix(fog(layer, layerT[i], skyColor, medium), color, layerAlpha[i], 1 - layerAlpha[i]); + color = ColorUtil.mix(medium.fog(layer, layerT[i], skyColor), color, layerAlpha[i], 1 - layerAlpha[i]); } } return color & 0xFFFFFF; } - /** Distance fog: the camera fluid's fog inside water/lava, the atmospheric one otherwise. */ - private int fog(int color, double distance, int skyColor, CameraMedium medium) { - if (medium.inFluid()) return medium.fog(color, distance); - return fog(color, distance, skyColor); - } - - private int fog(int color, double distance, int skyColor) { - double f = fogFactor(distance); - return f > 0 ? ColorUtil.mix(color, skyColor, 1 - f, f) : color; - } - - /** - * A block surface found by the {@link BlockCursor}: shaded/tinted ARGB colour, ray parameter and whether it is - * already lit (fluid faces carry their own sky light). - */ + /** A surface found by the {@link BlockCursor}; {@code lit}: carries its own sky light (fluid faces). */ private record BlockHit(int color, double t, boolean lit) {} /** - * Walks the voxel grid along one ray and hands out block surfaces in depth order. It stays on the voxel - * of the last returned hit, so a later call (with {@code minT} behind that hit) also finds surfaces - * further inside the same block (slime/honey block cores, the far side of a pane, the water polygons of a - * waterlogged block). Fluids are polygons like vanilla's FluidRenderer ({@link FluidRenderer}): every drawn - * water face the ray crosses is a translucent layer (texture alpha), lava faces are opaque. + * Walks the voxel grid along one ray and hands out block surfaces in depth order. It stays on the voxel of the + * last returned hit, so a later call (with {@code minT} behind that hit) also finds surfaces further inside the + * same block (slime/honey cores, the far side of a pane, the water polygons of a waterlogged block). */ private final class BlockCursor { private final WorldSnapshot snapshot; @@ -249,11 +216,13 @@ public final class SnapshotRaytracer { private final int depth; private final CameraMedium medium; private final double ox, oy, oz, dx, dy, dz; + private final FluidRenderer.BlockLookup blocks; private final VoxelDDA dda; private int steps; private boolean done; // The ray met water (a water cell, or the camera is in water): surfaces may need the underwater light. + // Checked when composing, after the whole walk. private boolean sawWater; private boolean reflected; @@ -277,6 +246,7 @@ public final class SnapshotRaytracer { this.dx = direction.getX(); this.dy = direction.getY(); this.dz = direction.getZ(); + this.blocks = snapshot::getBlockData; this.dda = new VoxelDDA(ox, oy, oz, dx, dy, dz); this.sawWater = medium.fluid() == FluidRenderer.WATER; } @@ -300,21 +270,22 @@ public final class SnapshotRaytracer { ElementIntersector.intersect(model, ox - bx, oy - by, oz - bz, dx, dy, dz, bx, by, bz, minT); int fluidState = FluidRenderer.state(data); if (fluidState != 0) { - if (FluidRenderer.type(fluidState) == FluidRenderer.WATER) sawWater = true; - FluidRenderer.Crossing c = fluids.nearest( - snapshot::getBlockData, bx, by, bz, data, fluidState, ox, oy, oz, dx, dy, dz, minT); - if (c != null && (hit == null || c.t < hit.t())) return fluidHit(c, bx, by, bz); + int type = FluidRenderer.type(fluidState); + if (type == FluidRenderer.WATER) sawWater = true; + FluidRenderer.Crossing c = + fluids.nearest(blocks, bx, by, bz, model, fluidState, ox, oy, oz, dx, dy, dz, minT); + if (c != null && (hit == null || c.t < hit.t())) return fluidHit(c, type, bx, by, bz); } if (hit == null) { advance(); continue; } - int color = shadeAndTint(hit, data, snapshot, bx, by, bz); - if (ColorUtil.alpha(color) < OPAQUE_ALPHA && culledByNeighbour(hit, data, bx, by, bz)) { - minT = hit.t(); // inner face between two blocks of the same translucent kind: not drawn + // Shading and tint keep the texel alpha, so the culling test can run first. + if (ColorUtil.alpha(hit.color()) < OPAQUE_ALPHA && culledByNeighbour(hit, data, bx, by, bz)) { + minT = hit.t(); continue; } - return new BlockHit(color, hit.t(), false); + return new BlockHit(shadeAndTint(hit, data, snapshot, bx, by, bz), hit.t(), false); } return null; } @@ -325,23 +296,23 @@ public final class SnapshotRaytracer { } /** - * A crossed fluid polygon as a surface: texel × biome water colour (water) × face shade × sky light of the - * face's cell (max with the one above, like vanilla's light coords; water only, lava glows). The first - * water surface entered from the air also reflects (folded into the layer colour/alpha). + * A crossed fluid polygon as a surface: texel × biome water colour × face shade × sky light of the face's + * cell (the one below for the bottom face; max with the cell above, like vanilla's light coords). Lava is + * neither tinted nor dimmed. The first water surface entered from the air also reflects. */ - private BlockHit fluidHit(FluidRenderer.Crossing c, int bx, int by, int bz) { + private BlockHit fluidHit(FluidRenderer.Crossing c, int type, int bx, int by, int bz) { int color = c.texel; - boolean water = c.type == FluidRenderer.WATER; + boolean water = type == FluidRenderer.WATER; double light = 1; if (water) { BiomeTint tint = blendedTint(snapshot, bx, by, bz); if (tint != null) color = ColorUtil.multiply(color, tint.water()); + int y = c.bottom ? by - 1 : by; int level = Math.max( - FluidRenderer.skyLevel(snapshot::getBlockData, c.lightX, c.lightY, c.lightZ), - FluidRenderer.skyLevel(snapshot::getBlockData, c.lightX, c.lightY + 1, c.lightZ)); + FluidRenderer.skyLevel(blocks, bx, y, bz), FluidRenderer.skyLevel(blocks, bx, y + 1, bz)); if (level < 15) light = SKY_LIGHT[level]; } - color = ColorUtil.shade(color, c.shade * light); + color = ColorUtil.shade(color, shadeFactor(c.nx, c.ny, c.nz) * light); if (water && c.entering && !reflected && depth > 0 && !medium.inFluid()) { reflected = true; double px = ox + dx * c.t, py = oy + dy * c.t, pz = oz + dz * c.t; @@ -349,7 +320,7 @@ public final class SnapshotRaytracer { Vector reflectDir = MathUtil.reflectVector(direction, normal); Vector reflectStart = new Vector(px, py, pz).add(normal.clone().multiply(1e-3)); int reflection = trace(snapshot, reflectStart, reflectDir, sky, scene, depth - 1, CameraMedium.AIR); - // mix(layer over behind, reflection): a layer with alpha a' = 1 - (1-a)(1-r). + // mix(layer over behind, reflection) as one layer with alpha a' = 1 - (1-a)(1-r). double a = ColorUtil.alpha(color) / 255.0, r = WATER_REFLECTION; double a2 = 1 - (1 - a) * (1 - r); double wl = a * (1 - r) / a2, wr = r / a2; @@ -364,25 +335,22 @@ public final class SnapshotRaytracer { } /** - * {@code color} of a surface at ray parameter {@code t} dimmed like vanilla sky light under water: by the - * light level of the block in front of the surface (see {@link FluidRenderer#skyLevel}). Unchanged when the - * ray never met water. + * {@code color} of a surface at ray parameter {@code t} dimmed by the sky light of the block in front of it + * (back along the ray), when the ray met water. */ int underwaterLight(int color, double t) { - if (!sawWater || Double.isInfinite(t)) return color; - // A point just in front of the surface (back along the ray), i.e. in the block that lights it. + if (!sawWater) return color; double back = 1e-4 / Math.sqrt(dx * dx + dy * dy + dz * dz); int bx = (int) Math.floor(ox + dx * (t - back)); int by = (int) Math.floor(oy + dy * (t - back)); int bz = (int) Math.floor(oz + dz * (t - back)); - int level = FluidRenderer.skyLevel(snapshot::getBlockData, bx, by, bz); + int level = FluidRenderer.skyLevel(blocks, bx, by, bz); return level >= 15 ? color : ColorUtil.shade(color, SKY_LIGHT[level]); } /** - * Vanilla skips the faces between two translucent blocks of the same kind (stained glass walls, ice, - * connected panes): true when the hit lies on the block boundary and the neighbour across it is the - * same block type. + * Vanilla skips the faces between two translucent blocks of the same kind (HalfTransparentBlock: stained + * glass walls, ice, connected panes): the hit lies on the block boundary and the neighbour is the same type. */ private boolean culledByNeighbour(FaceHit hit, BlockData data, int bx, int by, int bz) { Vector n = hit.normal(); @@ -420,93 +388,64 @@ public final class SnapshotRaytracer { return ColorUtil.shade(color, light); } - private double fogFactor(double distance) { - if (distance <= FOG_START) return 0; - double f = (distance - FOG_START) / (FOG_END - FOG_START); - return Math.clamp(f, 0, FOG_MAX); - } - /** - * Vanilla-style smooth ambient occlusion: darkens face corners by how many of the three blocks - * touching that corner (in the layer just outside the face) are solid, bilinearly interpolated - * across the face. Only applied to axis-aligned faces. + * Smooth ambient occlusion like vanilla: each face corner is darkened by how many of the three blocks touching + * it (in the layer just outside the face) are solid, bilinearly interpolated across the face. Axis-aligned + * faces only. */ private double ambientOcclusion(FaceHit hit, WorldSnapshot snapshot, int bx, int by, int bz) { double nx = hit.normal().getX(), ny = hit.normal().getY(), nz = hit.normal().getZ(); double ax = Math.abs(nx), ay = Math.abs(ny), az = Math.abs(nz); - if (Math.max(ax, Math.max(ay, az)) < 0.99) return 1.0; // skip rotated/diagonal faces + if (Math.max(ax, Math.max(ay, az)) < 0.99) return 1.0; double lx = hit.point().getX() - bx; double ly = hit.point().getY() - by; double lz = hit.point().getZ() - bz; - // Offset to the layer just outside the face, plus the two in-plane unit axes and face coords. - int ofx = (int) Math.round(nx), ofy = (int) Math.round(ny), ofz = (int) Math.round(nz); - int ux, uy, uz, vx, vy, vz; + // In-plane axes u = (ux, 0, uz), v = (0, vy, vz) and the face coordinates along them. + int ux, uz, vy, vz; double su, sv; if (ay > 0.5) { // up/down ux = 1; - uy = 0; uz = 0; - vx = 0; vy = 0; vz = 1; su = lx; sv = lz; } else if (ax > 0.5) { // east/west ux = 0; - uy = 0; uz = 1; - vx = 0; vy = 1; vz = 0; su = lz; sv = ly; } else { // north/south ux = 1; - uy = 0; uz = 0; - vx = 0; vy = 1; vz = 0; su = lx; sv = ly; } - - double b00 = aoCorner(snapshot, bx, by, bz, ofx, ofy, ofz, ux, uy, uz, vx, vy, vz, -1, -1); - double b10 = aoCorner(snapshot, bx, by, bz, ofx, ofy, ofz, ux, uy, uz, vx, vy, vz, +1, -1); - double b01 = aoCorner(snapshot, bx, by, bz, ofx, ofy, ofz, ux, uy, uz, vx, vy, vz, -1, +1); - double b11 = aoCorner(snapshot, bx, by, bz, ofx, ofy, ofz, ux, uy, uz, vx, vy, vz, +1, +1); + // The cell just outside the face. + int x = bx + (int) Math.round(nx), y = by + (int) Math.round(ny), z = bz + (int) Math.round(nz); + boolean uMinus = solid(snapshot, x - ux, y, z - uz), uPlus = solid(snapshot, x + ux, y, z + uz); + boolean vMinus = solid(snapshot, x, y - vy, z - vz), vPlus = solid(snapshot, x, y + vy, z + vz); + double b00 = aoBrightness(uMinus, vMinus, solid(snapshot, x - ux, y - vy, z - uz - vz)); + double b10 = aoBrightness(uPlus, vMinus, solid(snapshot, x + ux, y - vy, z + uz - vz)); + double b01 = aoBrightness(uMinus, vPlus, solid(snapshot, x - ux, y + vy, z - uz + vz)); + double b11 = aoBrightness(uPlus, vPlus, solid(snapshot, x + ux, y + vy, z + uz + vz)); double top = b00 + (b10 - b00) * su; double bottom = b01 + (b11 - b01) * su; return top + (bottom - top) * sv; } - private double aoCorner( - WorldSnapshot snapshot, - int bx, - int by, - int bz, - int ofx, - int ofy, - int ofz, - int ux, - int uy, - int uz, - int vx, - int vy, - int vz, - int du, - int dv) { - boolean side1 = solid(snapshot, bx + ofx + du * ux, by + ofy + du * uy, bz + ofz + du * uz); - boolean side2 = solid(snapshot, bx + ofx + dv * vx, by + ofy + dv * vy, bz + ofz + dv * vz); - boolean corner = solid( - snapshot, bx + ofx + du * ux + dv * vx, by + ofy + du * uy + dv * vy, bz + ofz + du * uz + dv * vz); + private static double aoBrightness(boolean side1, boolean side2, boolean corner) { int level = (side1 && side2) ? 0 : 3 - (side1 ? 1 : 0) - (side2 ? 1 : 0) - (corner ? 1 : 0); - return AO_BRIGHTNESS[Math.clamp(level, 0, 3)]; + return AO_BRIGHTNESS[level]; } private boolean solid(WorldSnapshot snapshot, int x, int y, int z) { @@ -514,11 +453,7 @@ public final class SnapshotRaytracer { return m != Material.AIR && m.isOccluding(); } - /** - * Biome-blended tint: averages the per-biome tint over a {@code (2r+1)x(2r+1)} neighbourhood in - * X/Z (vanilla biome blend radius, default 2), giving smooth grass/foliage gradients across biome - * borders instead of hard edges. Cached per column. - */ + /** Biome tint averaged over the (2r+1)² columns around the block (vanilla biome blend, radius 2); cached per column. */ private BiomeTint blendedTint(WorldSnapshot snapshot, int bx, int by, int bz) { long key = (((long) bx) & 0xFFFFFFFFL) | (((long) bz) << 32); BiomeTint cached = tintCache.get(key); @@ -545,21 +480,23 @@ public final class SnapshotRaytracer { } private static void accumulate(long[] acc, int argb) { - acc[0] += (argb >> 16) & 0xFF; - acc[1] += (argb >> 8) & 0xFF; - acc[2] += argb & 0xFF; + acc[0] += ColorUtil.red(argb); + acc[1] += ColorUtil.green(argb); + acc[2] += ColorUtil.blue(argb); } private static int average(long[] acc, int n) { - return 0xFF000000 | (((int) (acc[0] / n)) << 16) | (((int) (acc[1] / n)) << 8) | ((int) (acc[2] / n)); + return ColorUtil.argb(0xFF, (int) (acc[0] / n), (int) (acc[1] / n), (int) (acc[2] / n)); } - /** Vanilla-style directional shading: top 1.0, north/south 0.8, east/west 0.6, bottom 0.5. */ - private double shadeFactor(Vector normal) { - double ax = Math.abs(normal.getX()); - double ay = Math.abs(normal.getY()); - double az = Math.abs(normal.getZ()); - if (ay >= ax && ay >= az) return normal.getY() >= 0 ? 1.0 : 0.5; + private static double shadeFactor(Vector normal) { + return shadeFactor(normal.getX(), normal.getY(), normal.getZ()); + } + + /** Vanilla directional shading by the dominant normal axis: up 1.0, down 0.5, north/south 0.8, east/west 0.6. */ + private static double shadeFactor(double nx, double ny, double nz) { + double ax = Math.abs(nx), ay = Math.abs(ny), az = Math.abs(nz); + if (ay >= ax && ay >= az) return ny >= 0 ? 1.0 : 0.5; if (az >= ax) return 0.8; return 0.6; } diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/raytrace/VoxelDDA.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/raytrace/VoxelDDA.java index 447bd1f..ca4fc53 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/raytrace/VoxelDDA.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/raytrace/VoxelDDA.java @@ -1,23 +1,19 @@ package eu.mhsl.minecraft.pixelpics.render.raytrace; -/** - * Amanatides-Woo voxel traversal: walks the integer block grid a ray passes through, in order, - * without any Bukkit world access (safe to run off the main thread). - */ -public final class VoxelDDA { +/** Amanatides-Woo voxel traversal: the integer block cells a ray passes through, in order. */ +final class VoxelDDA { - public int x, y, z; - public double tCurrent; // ray parameter at which the current voxel was entered + int x, y, z; + double tCurrent; // ray parameter at which the current voxel was entered private final int stepX, stepY, stepZ; private final double tDeltaX, tDeltaY, tDeltaZ; private double tMaxX, tMaxY, tMaxZ; - public VoxelDDA(double ox, double oy, double oz, double dx, double dy, double dz) { + VoxelDDA(double ox, double oy, double oz, double dx, double dy, double dz) { this.x = (int) Math.floor(ox); this.y = (int) Math.floor(oy); this.z = (int) Math.floor(oz); - this.tCurrent = 0; this.stepX = dx > 0 ? 1 : (dx < 0 ? -1 : 0); this.stepY = dy > 0 ? 1 : (dy < 0 ? -1 : 0); @@ -38,8 +34,7 @@ public final class VoxelDDA { return (next - origin) / dir; } - /** Advance to the next voxel along the ray, updating {@link #tCurrent}. */ - public void advance() { + void advance() { if (tMaxX < tMaxY) { if (tMaxX < tMaxZ) { x += stepX; 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 b777e5f..9e6cd28 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 @@ -45,12 +45,9 @@ import org.bukkit.util.Vector; * Renders the scene by capturing a world snapshot on the main thread ({@link #prepare}) and then * tracing one ray per pixel in parallel against that snapshot ({@link #execute}). */ -public class DefaultScreenRenderer implements Renderer { +public class DefaultScreenRenderer { - /** - * Vertical field of view in degrees (vanilla's default FOV option, which is vertical too); the - * horizontal FOV follows from the image aspect ratio. Square photos: 70° both ways. - */ + /** Vertical field of view in degrees (vanilla's default FOV option is vertical too). */ public static final double V_FOV_DEG = 70; // Camera.getFov in water/lava: fov · lerp(fovEffectScale = 1, 1, 6/7). private static final double FLUID_FOV_SCALE = 6.0 / 7.0; @@ -58,7 +55,7 @@ public class DefaultScreenRenderer implements Renderer { private static final double MAX_DISTANCE = 256; private static final int REFLECTION_DEPTH = 4; - /** Supersampling factor: SSAA x SSAA rays per output pixel, downsampled gamma-correctly. */ + /** SSAA x SSAA rays per output pixel, downsampled gamma-correctly. */ private static final int SSAA = 3; private final SnapshotRaytracer raytracer; @@ -109,22 +106,14 @@ public class DefaultScreenRenderer implements Renderer { this.tracePool = tracePool; } - /** Convenience: prepare and execute in one call (must run on the main thread). */ - @Override - public BufferedImage render(Location eyeLocation, Resolution resolution) { - return execute(prepare(eyeLocation, resolution, null)); - } - /** Builds the (supersampled) ray map and captures world + entities. MUST run on the main thread. */ public RenderJob prepare(Location eyeLocation, Resolution resolution, UUID shooter) { - int superW = resolution.getWidth() * SSAA; - int superH = resolution.getHeight() * SSAA; - // Vanilla Camera.getFov: in water or lava the FOV shrinks to 6/7 (FOV effects slider at its default 100%). - World eyeWorld = eyeLocation.getWorld(); - boolean inFluid = eyeWorld != null - && FluidRenderer.cameraFluid( - eyeWorld::getBlockData, eyeLocation.getX(), eyeLocation.getY(), eyeLocation.getZ()) - != FluidRenderer.NONE; + int superW = resolution.width() * SSAA; + int superH = resolution.height() * SSAA; + World world = eyeLocation.getWorld(); + boolean inFluid = FluidRenderer.cameraFluid( + world::getBlockData, eyeLocation.getX(), eyeLocation.getY(), eyeLocation.getZ()) + != FluidRenderer.NONE; List rayMap = buildRayMap(eyeLocation, superW, superH, inFluid ? V_FOV_DEG * FLUID_FOV_SCALE : V_FOV_DEG); WorldSnapshot snapshot = SnapshotBuilder.build(eyeLocation, rayMap, MAX_DISTANCE, logger); @@ -134,7 +123,6 @@ public class DefaultScreenRenderer implements Renderer { List decorations = DecorationSnapshotBuilder.build(nearby); List objects = ObjectSnapshotBuilder.build(nearby, eyeLocation); - World world = eyeLocation.getWorld(); long dayTime = world.getTime(); long fullTime = world.getFullTime(); int moonPhase = (int) (fullTime / 24000L % 8L); @@ -144,8 +132,8 @@ public class DefaultScreenRenderer implements Renderer { snapshot, rayMap, eyeLocation.toVector(), - resolution.getWidth(), - resolution.getHeight(), + resolution.width(), + resolution.height(), sky, entities, blockEntities, @@ -159,9 +147,8 @@ public class DefaultScreenRenderer implements Renderer { } /** - * As {@link #execute(RenderJob)}, but cooperatively abortable: once {@code cancelled} is set, the - * parallel loops short-circuit (remaining work becomes a no-op) so a stuck/overlong render drains - * out quickly. The returned image is then partial and should be discarded by the caller. + * As {@link #execute(RenderJob)}, but abortable: once {@code cancelled} is set the remaining work is skipped and + * the returned image is partial (to be discarded). */ public BufferedImage execute(RenderJob job, AtomicBoolean cancelled) { int finalW = job.width(); @@ -207,9 +194,8 @@ public class DefaultScreenRenderer implements Renderer { } /** - * Runs a parallel-stream task. With a {@link #tracePool} the work is confined to that bounded, - * low-priority pool (parallel streams adopt the pool of the running fork-join worker); without one - * it runs inline on the common pool (used by the offline render tools). + * Runs a parallel-stream task in the {@link #tracePool} (parallel streams adopt the pool of the running fork-join + * worker), or on the common pool without one (offline render tools). */ private void runParallel(Runnable task) { if (tracePool == null) { @@ -220,12 +206,9 @@ public class DefaultScreenRenderer implements Renderer { } /** - * Pinhole camera like vanilla's projection: the rays go through a flat image plane one unit in front - * of the eye, {@code ray = forward + u·tan(fovX/2)·right + v·tan(fovY/2)·up} with u, v ∈ [-1, 1] at - * the pixel centres, row 0 = top. The vertical FOV is fixed ({@link #V_FOV_DEG}, like vanilla's - * default 70°), the horizontal one follows from the aspect ratio ({@code tan(fovX/2) = tan(fovY/2)·w/h}), - * so one pixel covers the same angle (at the centre) both ways and squares stay square. Right/up come - * from the yaw/pitch of {@code eye} (no roll), so looking straight up or down keeps the yaw orientation. + * Pinhole camera like vanilla's projection: {@code ray = forward + u·tan(fovX/2)·right + v·tan(fovY/2)·up} with + * u, v ∈ [-1, 1] at the pixel centres, row 0 = top; {@code tan(fovX/2) = tan(fovY/2)·w/h}. Right/up come from + * the yaw of {@code eye} (no roll), so looking straight up or down keeps the yaw orientation. */ public static List buildRayMap(Location eye, int width, int height) { return buildRayMap(eye, width, height, V_FOV_DEG); diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/render/RenderJob.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/render/RenderJob.java index 1a6137d..9960846 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/render/RenderJob.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/render/RenderJob.java @@ -10,9 +10,8 @@ import java.util.List; import org.bukkit.util.Vector; /** - * A prepared render: the world snapshot (captured on the main thread) plus the ray map, camera origin, - * the sky context (time of day / moon phase) and the captured entity, block-entity and decoration - * (painting / item frame) states. {@link DefaultScreenRenderer#execute} can run this off the main thread. + * A prepared render: everything captured on the main thread (world snapshot, ray map, camera origin, sky state, + * entity/block-entity/decoration/object states). {@link DefaultScreenRenderer#execute} runs it off the main thread. */ public record RenderJob( WorldSnapshot snapshot, @@ -24,44 +23,4 @@ public record RenderJob( List entities, List blockEntities, List decorations, - List objects) { - - /** Without display/item/falling-block objects. */ - public RenderJob( - WorldSnapshot snapshot, - List rayMap, - Vector origin, - int width, - int height, - SkyContext sky, - List entities, - List blockEntities, - List decorations) { - this(snapshot, rayMap, origin, width, height, sky, entities, blockEntities, decorations, List.of()); - } - - /** Backwards-compatible constructor (no block-entities/decorations), used by the standalone harness. */ - public RenderJob( - WorldSnapshot snapshot, - List rayMap, - Vector origin, - int width, - int height, - SkyContext sky, - List entities) { - this(snapshot, rayMap, origin, width, height, sky, entities, List.of(), List.of()); - } - - /** Convenience for callers that supply entities + block-entities but no decorations. */ - public RenderJob( - WorldSnapshot snapshot, - List rayMap, - Vector origin, - int width, - int height, - SkyContext sky, - List entities, - List blockEntities) { - this(snapshot, rayMap, origin, width, height, sky, entities, blockEntities, List.of()); - } -} + List objects) {} diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/render/Renderer.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/render/Renderer.java deleted file mode 100644 index 018f3c5..0000000 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/render/Renderer.java +++ /dev/null @@ -1,8 +0,0 @@ -package eu.mhsl.minecraft.pixelpics.render.render; - -import java.awt.image.BufferedImage; -import org.bukkit.Location; - -public interface Renderer { - BufferedImage render(Location eyeLocation, Resolution resolution); -} diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/render/Resolution.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/render/Resolution.java index 9d2b7c9..f9d7a77 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/render/Resolution.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/render/Resolution.java @@ -2,54 +2,11 @@ package eu.mhsl.minecraft.pixelpics.render.render; import com.google.common.base.Preconditions; -public final class Resolution { +/** Output size of a photo in pixels. */ +public record Resolution(int width, int height) { - private final int width; - private final int height; - - public Resolution(Pixels pixels, AspectRatio aspectRatio) { - this( - (int) Math.round( - Preconditions.checkNotNull(pixels).height * Preconditions.checkNotNull(aspectRatio).ratio), - pixels.height); - } - - public Resolution(int width, int height) { + public Resolution { Preconditions.checkArgument(width > 0); Preconditions.checkArgument(height > 0); - - this.width = width; - this.height = height; - } - - public int getWidth() { - return width; - } - - public int getHeight() { - return height; - } - - public enum Pixels { - _128P(128), - _256P(256); - - private final int height; - - Pixels(int height) { - this.height = height; - } - } - - public enum AspectRatio { - _1_1(1), - _2_1(2), - _3_2(3 / 2.0); - - private final double ratio; - - AspectRatio(double ratio) { - this.ratio = ratio; - } } } 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 fb7bdf1..15de5d8 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,9 +6,8 @@ import eu.mhsl.minecraft.pixelpics.render.util.ColorUtil; import org.bukkit.util.Vector; /** - * Computes a time-of-day dependent sky color for rays that escape the world: a day/night gradient - * with twilight glow, the sun and moon (with phase), stars at night and a procedural cloud layer. - * All inputs are immutable ({@link SkyContext} + captured textures), so it is thread safe. + * 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. */ public final class SkyRenderer { @@ -17,8 +16,9 @@ public final class SkyRenderer { private static final double CLOUD_CELL = 12.0; // world blocks per cloud texel private static final double CLOUD_SPEED = 0.03; // blocks per tick, drift along +X - private static final double SUN_HALF = 0.085; // angular half-size (radians) - private static final double MOON_HALF = 0.075; + // sin of the angular half-size. + private static final double SUN_SIN_HALF = Math.sin(0.085); + private static final double MOON_SIN_HALF = Math.sin(0.075); // Gradient endpoints (RGB). private static final int DAY_ZENITH = rgb(86, 138, 252); @@ -29,13 +29,10 @@ public final class SkyRenderer { private static final int SUNSET_RED = rgb(205, 70, 60); private static final int TWI_PURPLE = rgb(80, 42, 92); - private final int[][] sunTexture; private final int[][] moonTexture; private final int[][] cloudTexture; public SkyRenderer(TextureCache textures) { - this.sunTexture = - textures.get(ResourceLocation.parse("environment/sun")).orElse(null); this.moonTexture = textures.get(ResourceLocation.parse("environment/moon_phases")).orElse(null); this.cloudTexture = @@ -50,9 +47,8 @@ public final class SkyRenderer { dy /= len; dz /= len; - // Sun/moon position, derived exactly from Minecraft's sky transforms: - // celestialAngle ca = getTimeOfDay(dayTime); the sun is rotated by ca*360deg about the X axis - // (after a -90deg Y rotation), giving sunDir = (-sin(2*pi*ca), cos(2*pi*ca), 0) in world space. + // Vanilla sun direction: rotated by celestialAngle·360° about X (after -90° about Y) + // = (-sin(2π·ca), cos(2π·ca), 0). double ca = celestialAngle(ctx.dayTime()); double ang = ca * 2 * Math.PI; double sunX = -Math.sin(ang), sunY = Math.cos(ang); @@ -64,8 +60,8 @@ public final class SkyRenderer { int nightColor = lerp(NIGHT_HORIZON, NIGHT_ZENITH, up); int color = lerp(nightColor, dayColor, dayFactor); - // Sunrise/sunset: a full-sky warm wash (orange at the horizon -> red -> purple at the zenith), - // strongest while the sun is near the horizon and warmer toward its azimuth. Matches vanilla. + // Sunrise/sunset: a full-sky warm wash (orange at the horizon -> red -> purple at the zenith), strongest while + // the sun is near the horizon and warmer toward its azimuth. double twilight = Math.clamp(1 - Math.abs(sunY) / 0.45, 0, 1); if (twilight > 0) { double az = Math.clamp(dx * Math.signum(sunX) * 0.5 + 0.5, 0, 1); // 1 toward sun .. 0 away @@ -83,7 +79,7 @@ public final class SkyRenderer { double star = starField(dx, dy, dz); if (star > 0) { int s = (int) (star * 255 * visibility); - color = add(color, s, s, s); + color = ColorUtil.add(color, rgb(s, s, s)); } } } @@ -97,19 +93,14 @@ public final class SkyRenderer { } } - // Sun disc (soft glowing disc, texture used only as a shape mask). if (sunY > -0.15) { - color = overlayDisc(color, dx, dy, dz, sunX, sunY, 0, SUN_HALF, sunTexture, rgb(255, 244, 214), -1); + color = overlayDisc(color, dx, dy, dz, sunX, sunY, 0, SUN_SIN_HALF, rgb(255, 244, 214), -1); } - // Moon disc (phase shape from the texture's alpha). - if (-sunY > -0.15) { - color = overlayDisc( - color, dx, dy, dz, -sunX, -sunY, 0, MOON_HALF, moonTexture, rgb(228, 228, 238), ctx.moonPhase()); + if (sunY < 0.15) { + color = overlayDisc(color, dx, dy, dz, -sunX, -sunY, 0, MOON_SIN_HALF, rgb(228, 228, 238), ctx.moonPhase()); } - // Cloud layer: the ray crosses the cloud plane at y = CLOUD_HEIGHT; the world hit point is - // mapped to a clouds.png texel exactly as vanilla does (see clouds()). Horizontal drift uses - // the world time (fullTime * CLOUD_SPEED along +X). + // Cloud layer at y = CLOUD_HEIGHT, drifting along +X with the world time. if (dy > 0.02 && origin.getY() < CLOUD_HEIGHT) { double t = (CLOUD_HEIGHT - origin.getY()) / dy; double cx = origin.getX() + dx * t + ctx.fullTime() * CLOUD_SPEED; @@ -126,7 +117,11 @@ public final class SkyRenderer { return color & 0xFFFFFF; } - /** Draws a sun/moon disc, sampling a texture when available (moonPhase ≥ 0 picks the phase tile). */ + /** + * Draws the sun or moon (a square billboard like vanilla) in colour {@code solid} around the body direction + * {@code c}. The sun ({@code moonPhase < 0}) is a solid square, the moon takes its phase shape from the alpha of + * the moon texture's tile (only a mask, so its dark transparent texels never bleed in as a rim). + */ private int overlayDisc( int base, double dx, @@ -135,15 +130,12 @@ public final class SkyRenderer { double cx, double cy, double cz, - double half, - int[][] texture, + double sinHalf, int solid, int moonPhase) { double cos = dx * cx + dy * cy + dz * cz; if (cos <= 0) return base; - double sinHalf = Math.sin(half); - // Local disc coordinates: project the direction onto the plane around the body axis. - // right = normalize(body x worldUp); discUp = right x body + // Disc coordinates: right = normalize(body x worldUp), discUp = right x body. double crx = cz, cry = 0, crz = -cx; double crl = Math.sqrt(crx * crx + crz * crz); if (crl < 1e-6) { @@ -155,25 +147,20 @@ public final class SkyRenderer { crx /= crl; cry /= crl; crz /= crl; - // discUp = right cross body double ux = cry * cz - crz * cy; double uy = crz * cx - crx * cz; double uz = crx * cy - cry * cx; double u = dx * crx + dy * cry + dz * crz; double v = dx * ux + dy * uy + dz * uz; - // The sun and moon are flat SQUARE billboards in Minecraft, not round discs. double m = Math.max(Math.abs(u), Math.abs(v)) / sinHalf; // 0 center .. 1 square edge if (m > 1) return base; double su = u / sinHalf * 0.5 + 0.5; double sv = v / sinHalf * 0.5 + 0.5; - - // The texture is used only as a shape/phase mask; the body color is always `solid` so the - // texture's black transparent texels never bleed in as a dark rim. double alpha; - if (moonPhase >= 0 && texture != null && texture.length > 0) { - alpha = bodyAlpha(texture, su, sv, moonPhase) > 80 ? 1.0 : 0.0; // phase shape + if (moonPhase >= 0 && moonTexture != null && moonTexture.length > 0) { + alpha = bodyAlpha(moonTexture, su, sv, moonPhase) > 80 ? 1.0 : 0.0; } else { alpha = 1 - smoothstep(0.92, 1.0, m); // solid square, faint edge softening } @@ -181,17 +168,17 @@ public final class SkyRenderer { return lerp(base, solid, alpha); } - /** Alpha of the body texture at the disc coordinate; moonPhase≥0 selects a tile in the 4x2 grid. */ + /** Alpha of the texture at the disc coordinate in the moon phase's tile of the 4x2 grid. */ private int bodyAlpha(int[][] texture, double su, double sv, int moonPhase) { int h = texture.length; int w = texture[0].length; double u = su, v = 1 - sv; // texture v is top-down int col = moonPhase % 4; - int row = (moonPhase / 4) % 2; + int row = moonPhase / 4; u = (col + u) / 4.0; v = (row + v) / 2.0; - int px = clamp((int) (u * w), 0, w - 1); - int py = clamp((int) (v * h), 0, h - 1); + int px = Math.clamp((int) (u * w), 0, w - 1); + int py = Math.clamp((int) (v * h), 0, h - 1); return ColorUtil.alpha(texture[py][px]); } @@ -206,13 +193,8 @@ public final class SkyRenderer { } /** - * Exact vanilla cloud coverage. Minecraft tiles {@code clouds.png} (256x256) over the world with - * each texel covering a {@link #CLOUD_CELL} (=12) block square, so the pattern repeats every - * 256*12 = 3072 blocks. A world position maps to texel - * {@code (col = floorMod(floor(x/12), 256), row = floorMod(floor(z/12), 256))} with the texture's - * U axis along world X and V axis along world Z; a texel is a cloud where its alpha > 0. This - * reproduces the blocky cloud shapes and their world alignment exactly. Falls back to value noise - * only when the texture is missing from the pack. + * Vanilla cloud coverage: {@code clouds.png} tiled over the world, one texel per {@link #CLOUD_CELL} block square + * (U along X, V along Z); a texel with alpha is a cloud. Value noise only when the pack lacks the texture. */ private double clouds(double x, double z) { if (cloudTexture != null && cloudTexture.length > 0) { @@ -250,8 +232,6 @@ public final class SkyRenderer { return h ^ (h >>> 16); } - // --- small color/math helpers --- - private static int rgb(int r, int g, int b) { return (r << 16) | (g << 8) | b; } @@ -261,13 +241,6 @@ public final class SkyRenderer { return ColorUtil.mix(a, b, 1 - t, t); } - private static int add(int c, int r, int g, int b) { - int cr = Math.min(255, ((c >> 16) & 0xFF) + r); - int cg = Math.min(255, ((c >> 8) & 0xFF) + g); - int cb = Math.min(255, (c & 0xFF) + b); - return rgb(cr, cg, cb); - } - /** Minecraft's {@code Level.getTimeOfDay}: the celestial angle as a fraction [0,1). */ private static double celestialAngle(long dayTime) { double d = frac(dayTime / TICKS_PER_DAY - 0.25); @@ -283,8 +256,4 @@ public final class SkyRenderer { double t = Math.clamp((x - edge0) / (edge1 - edge0), 0, 1); return t * t * (3 - 2 * t); } - - private static int clamp(int v, int lo, int hi) { - return v < lo ? lo : Math.min(v, hi); - } } diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/survival/PhotoService.java b/src/main/java/eu/mhsl/minecraft/pixelpics/survival/PhotoService.java index a53bcfb..1b2a55c 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/survival/PhotoService.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/survival/PhotoService.java @@ -66,7 +66,7 @@ public final class PhotoService { boolean dispatched = false; try { - Resolution resolution = new Resolution(Resolution.Pixels._128P, Resolution.AspectRatio._1_1); + Resolution resolution = new Resolution(128, 128); // Capture the world snapshot on the main thread (the moment of the shot). RenderJob job = renderer.prepare(player.getEyeLocation(), resolution, user); diff --git a/tools/BlockEntityTestRender.java b/tools/BlockEntityTestRender.java index 87aea1d..41ac684 100644 --- a/tools/BlockEntityTestRender.java +++ b/tools/BlockEntityTestRender.java @@ -377,7 +377,7 @@ public class BlockEntityTestRender { Location loc = new Location(null, cam.getX(), cam.getY(), cam.getZ()); loc.setDirection(center.clone().subtract(cam)); List rayMap = buildRayMap(loc, TW * SSAA, TH * SSAA); - RenderJob job = new RenderJob(world, rayMap, cam, TW, TH, sky, List.of(), states); + RenderJob job = new RenderJob(world, rayMap, cam, TW, TH, sky, List.of(), states, List.of(), List.of()); return renderer.execute(job); } @@ -403,7 +403,7 @@ public class BlockEntityTestRender { Location loc = new Location(null, cam.getX(), cam.getY(), cam.getZ()); loc.setDirection(center.clone().subtract(cam)); List rayMap = buildRayMap(loc, TW * SSAA, TH * SSAA); - RenderJob job = new RenderJob(world, rayMap, cam, TW, TH, sky, List.of(), List.of(), states); + RenderJob job = new RenderJob(world, rayMap, cam, TW, TH, sky, List.of(), List.of(), states, List.of()); return renderer.execute(job); } diff --git a/tools/EntityTestRender.java b/tools/EntityTestRender.java index 3440606..97420e3 100644 --- a/tools/EntityTestRender.java +++ b/tools/EntityTestRender.java @@ -250,7 +250,7 @@ public class EntityTestRender { loc.setDirection(center.clone().subtract(cam)); List rayMap = buildRayMap(loc, TW * SSAA, TH * SSAA); - RenderJob job = new RenderJob(world, rayMap, cam, TW, TH, sky, List.of(s)); + RenderJob job = new RenderJob(world, rayMap, cam, TW, TH, sky, List.of(s), List.of(), List.of(), List.of()); return renderer.execute(job); }