simplify raytracer, fluid, sky and render code: air fog moved into the camera medium, sky colour computed only when read, single ao neighbour lookups, culling test before tint and ao, allocation-free fluid heights and flow angle, one fluid model lookup per cell, shared normal rotation and face fractions in the element intersector, unified side-face table, dead overloads/interface/enums and convenience constructors removed, comments trimmed
This commit is contained in:
1 parent
4348b5fca0
commit
bc07631fec
14 files changed
+398
-685
No files matched your search
@@ -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).
|
||||
*
|
||||
* <p>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);
|
||||
|
||||
+79
-121
@@ -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<Candidate>.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<Integer> 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);
|
||||
|
||||
@@ -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) {}
|
||||
@@ -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.
|
||||
*
|
||||
* <p>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<Material> 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<Material> OVERLAY_NEIGHBOURS = buildOverlayNeighbours();
|
||||
// Vanilla HalfTransparentBlock / LeavesBlock (FluidRenderer isOverlay). Blue ice too, but it occludes anyway.
|
||||
private static final EnumSet<Material> OVERLAY_NEIGHBOURS = EnumSet.noneOf(Material.class);
|
||||
// Vanilla tag blocks_fluid_flow = #blocks_motion (Material.isSolid) + #all_signs.
|
||||
private static final EnumSet<Material> 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<Material> materials(String... names) {
|
||||
EnumSet<Material> set = EnumSet.noneOf(Material.class);
|
||||
@@ -67,30 +80,12 @@ public final class FluidRenderer {
|
||||
return set;
|
||||
}
|
||||
|
||||
private static EnumSet<Material> buildOverlayNeighbours() {
|
||||
EnumSet<Material> 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<ResolvedModel, Integer> 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;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
+18
-35
@@ -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<Vector> 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<DecorationState> decorations = DecorationSnapshotBuilder.build(nearby);
|
||||
List<ObjectState> 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<Vector> buildRayMap(Location eye, int width, int height) {
|
||||
return buildRayMap(eye, width, height, V_FOV_DEG);
|
||||
|
||||
@@ -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<EntityState> entities,
|
||||
List<BlockEntityState> blockEntities,
|
||||
List<DecorationState> decorations,
|
||||
List<ObjectState> objects) {
|
||||
|
||||
/** Without display/item/falling-block objects. */
|
||||
public RenderJob(
|
||||
WorldSnapshot snapshot,
|
||||
List<Vector> rayMap,
|
||||
Vector origin,
|
||||
int width,
|
||||
int height,
|
||||
SkyContext sky,
|
||||
List<EntityState> entities,
|
||||
List<BlockEntityState> blockEntities,
|
||||
List<DecorationState> 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<Vector> rayMap,
|
||||
Vector origin,
|
||||
int width,
|
||||
int height,
|
||||
SkyContext sky,
|
||||
List<EntityState> 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<Vector> rayMap,
|
||||
Vector origin,
|
||||
int width,
|
||||
int height,
|
||||
SkyContext sky,
|
||||
List<EntityState> entities,
|
||||
List<BlockEntityState> blockEntities) {
|
||||
this(snapshot, rayMap, origin, width, height, sky, entities, blockEntities, List.of());
|
||||
}
|
||||
}
|
||||
List<ObjectState> objects) {}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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<Vector> 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<Vector> 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -250,7 +250,7 @@ public class EntityTestRender {
|
||||
loc.setDirection(center.clone().subtract(cam));
|
||||
|
||||
List<Vector> 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);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user