rescale rotated block elements like vanilla (cross plants, amethyst, campfire, fire, raised rails), euler x/y/z element rotations (hanging sign chains), inverted inner-face elements (spawner, vault), vanilla-like water: single surface blend with depth sky light instead of path-length density, water faces against glass/ice/leaves, block geometry and water test scenes

This commit is contained in:
MineTec committed 2026-11-04 19:25:17 +01:00
1 parent 2c2e9e2e32
commit 318a309580
7 files changed
+510 -38

No files matched your search

@@ -58,17 +58,26 @@ public final class ModelBaker {
double[] rotOrigin = null;
int rotAxis = -1;
double rotAngle = 0;
double[] rotMatrix = null;
boolean rescale = false;
if (dto.rotation != null && dto.rotation.angle != 0 && dto.rotation.origin != null) {
rotOrigin = new double[] {
dto.rotation.origin[0] / 16.0, dto.rotation.origin[1] / 16.0, dto.rotation.origin[2] / 16.0
};
rotAxis = axisIndex(dto.rotation.axis);
rotAngle = Math.toRadians(dto.rotation.angle);
rescale = dto.rotation.rescale;
ModelFileDto.RotationDto rot = dto.rotation;
if (rot != null && rot.origin != null) {
if (rot.axis == null && (rot.x != null || rot.y != null || rot.z != null)) {
rotMatrix = eulerXyz(rot.x, rot.y, rot.z);
if (rotMatrix != null) rotAxis = Element.AXIS_MATRIX;
} else if (rot.angle != 0) {
rotAxis = axisIndex(rot.axis);
rotAngle = Math.toRadians(rot.angle);
}
if (rotAxis >= 0) {
rotOrigin = new double[] {rot.origin[0] / 16.0, rot.origin[1] / 16.0, rot.origin[2] / 16.0};
rescale = rot.rescale;
}
}
Element modelElement = new Element(from, to, faces, rotOrigin, rotAxis, rotAngle, rescale);
Element modelElement = rotMatrix != null
? new Element(from, to, faces, rotOrigin, rotMatrix, rescale, null, 0, 0)
: new Element(from, to, faces, rotOrigin, rotAxis, rotAngle, rescale);
// Apply blockstate variant rotation (90-degree steps) to box + faces + element rotation.
for (int i = 0; i < xSteps; i++) {
@@ -76,8 +85,8 @@ public final class ModelBaker {
from = r[0];
to = r[1];
faces = rotateFacesX(faces);
if (rotAxis >= 0) {
rotOrigin = rotatePointX(rotOrigin);
if (rotAxis >= 0) rotOrigin = rotatePointX(rotOrigin);
if (rotAxis >= 0 && rotAxis < Element.AXIS_MATRIX) {
AxisRotation na = rotateAxisX(rotAxis);
rotAxis = na.axis();
if (na.flip()) rotAngle = -rotAngle;
@@ -88,8 +97,8 @@ public final class ModelBaker {
from = r[0];
to = r[1];
faces = rotateFacesY(faces);
if (rotAxis >= 0) {
rotOrigin = rotatePointY(rotOrigin);
if (rotAxis >= 0) rotOrigin = rotatePointY(rotOrigin);
if (rotAxis >= 0 && rotAxis < Element.AXIS_MATRIX) {
AxisRotation na = rotateAxisY(rotAxis);
rotAxis = na.axis();
if (na.flip()) rotAngle = -rotAngle;
@@ -103,7 +112,20 @@ public final class ModelBaker {
if (xSteps != 0 || ySteps != 0) {
unrotated = variant.uvlock() ? uvLocked(modelElement, xSteps, ySteps) : modelElement;
}
baked.add(new Element(from, to, faces, rotOrigin, rotAxis, rotAngle, rescale, unrotated, xSteps, ySteps));
baked.add(
rotMatrix != null
? new Element(
from,
to,
faces,
rotOrigin,
conjugate(rotMatrix, xSteps, ySteps),
rescale,
unrotated,
xSteps,
ySteps)
: new Element(
from, to, faces, rotOrigin, rotAxis, rotAngle, rescale, unrotated, xSteps, ySteps));
// Accumulate average color from the element's face textures.
for (Face f : faces) {
@@ -153,6 +175,48 @@ public final class ModelBaker {
};
}
/**
* Vanilla {@code EulerXYZRotation}: {@code Matrix4f.rotationZYX(z, y, x)} = Rz·Ry·Rx (degrees), row-major;
* null when it is the identity (vanilla then skips the rotation and its rescale).
*/
static double[] eulerXyz(Double xDeg, Double yDeg, Double zDeg) {
double x = Math.toRadians(xDeg == null ? 0 : xDeg);
double y = Math.toRadians(yDeg == null ? 0 : yDeg);
double z = Math.toRadians(zDeg == null ? 0 : zDeg);
if (x == 0 && y == 0 && z == 0) return null;
double cx = Math.cos(x),
sx = Math.sin(x),
cy = Math.cos(y),
sy = Math.sin(y),
cz = Math.cos(z),
sz = Math.sin(z);
double[] rz = {cz, -sz, 0, sz, cz, 0, 0, 0, 1};
double[] ry = {cy, 0, sy, 0, 1, 0, -sy, 0, cy};
double[] rx = {1, 0, 0, 0, cx, -sx, 0, sx, cx};
return mul(mul(rz, ry), rx);
}
/** The element rotation as seen after the blockstate rotation: V·M·Vᵀ (V = Element#rotate of the axes). */
private static double[] conjugate(double[] m, int xSteps, int ySteps) {
double[] v = new double[9];
for (int a = 0; a < 3; a++) {
double[] col = Element.rotate(a == 0 ? 1 : 0, a == 1 ? 1 : 0, a == 2 ? 1 : 0, xSteps, ySteps, false);
v[a] = col[0];
v[3 + a] = col[1];
v[6 + a] = col[2];
}
double[] vt = {v[0], v[3], v[6], v[1], v[4], v[7], v[2], v[5], v[8]};
return mul(mul(v, m), vt);
}
private static double[] mul(double[] a, double[] b) {
double[] r = new double[9];
for (int i = 0; i < 3; i++)
for (int j = 0; j < 3; j++)
r[3 * i + j] = a[3 * i] * b[j] + a[3 * i + 1] * b[3 + j] + a[3 * i + 2] * b[6 + j];
return r;
}
/** The model-space element with every face's UVs locked to the world under the given rotation. */
private static Element uvLocked(Element e, int xSteps, int ySteps) {
Face[] locked = new Face[6];
@@ -160,6 +224,8 @@ public final class ModelBaker {
Face f = e.faces[d.ordinal()];
if (f != null) locked[d.ordinal()] = f.withUvLock(uvLockTransform(d, xSteps, ySteps));
}
if (e.rotMatrix != null)
return new Element(e.from, e.to, locked, e.rotOrigin, e.rotMatrix, e.rescale, null, 0, 0);
return new Element(e.from, e.to, locked, e.rotOrigin, e.rotAxis, e.rotAngleRad, e.rescale);
}
@@ -32,7 +32,10 @@ public class ModelFileDto {
public static class RotationDto {
public double[] origin; // 0..16
public String axis; // "x" | "y" | "z"
public double angle; // -45..45 in 22.5 steps
public double angle; // degrees (vanilla models: 22.5 steps)
// Since 26.x alternatively Euler angles in degrees instead of axis/angle (CuboidRotation.EulerXYZRotation,
// matrix Rz·Ry·Rx), e.g. the chains of template_hanging_sign_rot_3.
public Double x, y, z;
public boolean rescale;
}
}
@@ -15,14 +15,26 @@ public final class Element {
// Element rotation (0..1 origin), null/zero when axis-aligned.
public final double[] rotOrigin; // length 3, 0..1, may be null
public final int rotAxis; // 0=x,1=y,2=z, -1 when none
public final double rotAngleRad; // radians
public final int rotAxis; // 0=x,1=y,2=z, AXIS_MATRIX (Euler x/y/z rotation in rotMatrix), -1 when none
public final double rotAngleRad; // radians (single-axis rotations only)
public final boolean rescale;
/** {@link #rotAxis} of an element rotated by a general (Euler x/y/z) rotation held in {@link #rotMatrix}. */
public static final int AXIS_MATRIX = 3;
// cos/sin of the element rotation (+angle: local -> block) and of its inverse (-angle: block -> local),
// precomputed for the intersector's hot path.
public final double rotCos, rotSin, invRotCos, invRotSin;
// General rotation (rotAxis == AXIS_MATRIX): row-major 3x3 local -> block and its inverse (transpose); null
// otherwise.
public final double[] rotMatrix, invRotMatrix;
// Vanilla rescale (CuboidRotation.computeRescale): the element is scaled per LOCAL axis by
// 1 / max|component of R·axis| before the rotation (1/cos 22.5° or 1/cos 45° on the axes the rotation tilts),
// so a rotated cross plane still spans the whole block. Reciprocals (block -> local); null without rescale.
public final double[] invScale;
// Blockstate rotation: the model-space element and the 90° steps (x first, then y) that turn it into this
// world-space one. Intersectors hit the unrotated element with the inverse-rotated ray, so textures stay
// bound to the model (vanilla); under uvlock its faces carry a UV transform keeping them world-aligned.
@@ -42,6 +54,20 @@ public final class Element {
this(from, to, faces, rotOrigin, rotAxis, rotAngleRad, rescale, null, 0, 0);
}
/** An element rotated by a general rotation matrix (row-major 3x3, local -> block, e.g. Euler x/y/z). */
public Element(
double[] from,
double[] to,
Face[] faces,
double[] rotOrigin,
double[] rotMatrix,
boolean rescale,
Element unrotated,
int xSteps,
int ySteps) {
this(from, to, faces, rotOrigin, AXIS_MATRIX, 0, rescale, unrotated, xSteps, ySteps, rotMatrix);
}
public Element(
double[] from,
double[] to,
@@ -53,6 +79,21 @@ public final class Element {
Element unrotated,
int xSteps,
int ySteps) {
this(from, to, faces, rotOrigin, rotAxis, rotAngleRad, rescale, unrotated, xSteps, ySteps, null);
}
private Element(
double[] from,
double[] to,
Face[] faces,
double[] rotOrigin,
int rotAxis,
double rotAngleRad,
boolean rescale,
Element unrotated,
int xSteps,
int ySteps,
double[] rotMatrix) {
this.unrotated = unrotated;
this.xSteps = xSteps;
this.ySteps = ySteps;
@@ -67,6 +108,41 @@ public final class Element {
this.rotSin = Math.sin(rotAngleRad);
this.invRotCos = Math.cos(-rotAngleRad);
this.invRotSin = Math.sin(-rotAngleRad);
this.rotMatrix = rotAxis == AXIS_MATRIX ? rotMatrix : null;
this.invRotMatrix = this.rotMatrix == null ? null : transpose(this.rotMatrix);
this.invScale = rescale && !isAxisAligned() ? inverseRescale(linearRotation()) : null;
}
/** The rotation as a row-major 3x3 (local -> block), identity when axis-aligned. */
public double[] linearRotation() {
if (rotMatrix != null) return rotMatrix.clone();
double c = rotCos, s = rotSin;
return switch (isAxisAligned() ? -1 : rotAxis) {
case 0 -> new double[] {1, 0, 0, 0, c, -s, 0, s, c};
case 1 -> new double[] {c, 0, s, 0, 1, 0, -s, 0, c};
case 2 -> new double[] {c, -s, 0, s, c, 0, 0, 0, 1};
default -> new double[] {1, 0, 0, 0, 1, 0, 0, 0, 1};
};
}
/** Vanilla rescale factors per local axis: 1 / max|component of R·axis| (column of R); as reciprocals. */
private static double[] inverseRescale(double[] r) {
double[] inv = new double[3];
for (int a = 0; a < 3; a++) {
double max = Math.max(Math.max(Math.abs(r[a]), Math.abs(r[3 + a])), Math.abs(r[6 + a]));
inv[a] = max; // 1 / (1 / max)
}
return inv;
}
/** The rescale factors per local axis (local -> block), {1,1,1} without rescale. */
public double[] rescaleFactors() {
if (invScale == null) return new double[] {1, 1, 1};
return new double[] {1 / invScale[0], 1 / invScale[1], 1 / invScale[2]};
}
private static double[] transpose(double[] m) {
return new double[] {m[0], m[3], m[6], m[1], m[4], m[7], m[2], m[5], m[8]};
}
/** Model space -> world: vanilla blockstate rotation (x steps, then y steps; clockwise about the centre). */
@@ -93,6 +169,6 @@ public final class Element {
}
public boolean isAxisAligned() {
return rotAxis < 0 || rotAngleRad == 0.0;
return rotAxis < 0 || (rotAxis != AXIS_MATRIX && rotAngleRad == 0.0);
}
}
@@ -460,11 +460,13 @@ public final class ItemGeometry {
switch (e.rotAxis) {
case 0 -> Affine.rotX(e.rotAngleRad);
case 1 -> Affine.rotY(e.rotAngleRad);
default -> Affine.rotZ(e.rotAngleRad);
case 2 -> Affine.rotZ(e.rotAngleRad);
default -> new Affine(e.linearRotation(), new double[] {0, 0, 0});
};
if (e.rescale) {
double f = 1 / Math.cos(e.rotAngleRad);
r = r.mul(Affine.scale(e.rotAxis == 0 ? 1 : f, e.rotAxis == 1 ? 1 : f, e.rotAxis == 2 ? 1 : f));
if (e.invScale != null) {
// Vanilla CuboidRotation rescale: R·S, S = 1 / max|component of R·axis| per local axis.
double[] f = e.rescaleFactors();
r = r.mul(Affine.scale(f[0], f[1], f[2]));
}
return Affine.translation(o[0], o[1], o[2]).mul(r).mul(Affine.translation(-o[0], -o[1], -o[2]));
}
@@ -20,6 +20,7 @@ import org.bukkit.util.Vector;
public final class ElementIntersector {
private static final double EPS = 1e-7;
private static final Direction[] DIRECTIONS = Direction.values();
private ElementIntersector() {}
@@ -154,14 +155,41 @@ public final class ElementIntersector {
return null;
}
/** Rotated element: transform the ray into the element's local (unrotated) frame, then slab-test. */
/**
* 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.
*/
private static boolean intersectObb(
Scratch sc, Element e, double ox, double oy, double oz, double dx, double dy, double dz) {
double[] v = sc.v;
rotate(v, ox - e.rotOrigin[0], oy - e.rotOrigin[1], oz - e.rotOrigin[2], e.rotAxis, e.invRotCos, e.invRotSin);
double lox = v[0] + e.rotOrigin[0], loy = v[1] + e.rotOrigin[1], loz = v[2] + e.rotOrigin[2];
rotate(v, dx, dy, dz, e.rotAxis, e.invRotCos, e.invRotSin);
return slab(sc, lox, loy, loz, v[0], v[1], v[2], e.from, e.to, e);
double[] o = e.rotOrigin;
unrotateElement(v, e, ox - o[0], oy - o[1], oz - o[2]);
double lox = v[0], loy = v[1], loz = v[2];
unrotateElement(v, e, dx, dy, dz);
double ldx = v[0], ldy = v[1], ldz = v[2];
double[] is = e.invScale;
if (is != null) {
lox *= is[0];
loy *= is[1];
loz *= is[2];
ldx *= is[0];
ldy *= is[1];
ldz *= is[2];
}
return slab(sc, lox + o[0], loy + o[1], loz + o[2], ldx, ldy, ldz, e.from, e.to, e);
}
/** Inverse element rotation (block -> local) of a vector into out, without the rescale. */
private static void unrotateElement(double[] out, Element e, double x, double y, double z) {
double[] m = e.invRotMatrix;
if (m != null) {
out[0] = m[0] * x + m[1] * y + m[2] * z;
out[1] = m[3] * x + m[4] * y + m[5] * z;
out[2] = m[6] * x + m[7] * y + m[8] * z;
} else {
rotate(out, x, y, z, e.rotAxis, e.invRotCos, e.invRotSin);
}
}
/** Slab test; on a hit the entry (or, from inside, exit) face is left in the scratch's hit fields. */
@@ -176,6 +204,9 @@ 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;
@@ -228,8 +259,16 @@ public final class ElementIntersector {
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;
rotate(v, dir.nx, dir.ny, dir.nz, obb.rotAxis, obb.rotCos, obb.rotSin);
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];
@@ -250,6 +289,88 @@ public final class ElementIntersector {
return true;
}
/**
* Element with {@code from > to} on some axis (spawner/vault/trial-spawner inner faces, snow layer 2's inner
* 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).
*/
private static boolean invertedBox(
Scratch sc,
double ox,
double oy,
double oz,
double dx,
double dy,
double dz,
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;
for (Direction dir : DIRECTIONS) {
int a = dir.nx != 0 ? 0 : dir.ny != 0 ? 1 : 2;
boolean neg = dir.nx + dir.ny + dir.nz < 0;
int b = (a + 1) % 3, c = (a + 2) % 3;
int flips = (from[b] > to[b] ? 1 : 0) + (from[c] > to[c] ? 1 : 0);
double sign = (neg ? -1 : 1) * (flips % 2 == 0 ? 1 : -1);
if (sign * d[a] >= 0) continue; // back face (or parallel)
double t = ((neg ? from[a] : to[a]) - o[a]) / d[a];
if (!(t >= EPS) || t >= best) continue;
double pb = o[b] + d[b] * t, pc = o[c] + d[c] * t;
if (pb < Math.min(from[b], to[b]) - EPS || pb > Math.max(from[b], to[b]) + EPS) continue;
if (pc < Math.min(from[c], to[c]) - EPS || pc > Math.max(from[c], to[c]) + EPS) continue;
best = t;
bestDir = dir;
bestSign = sign;
}
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]);
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;
}
/** 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) {
double span = to - from;
if (Math.abs(span) < 1e-6) return 0;
double f = (v - from) / span;
return f < 0 ? 0 : Math.min(f, 1);
}
/**
* 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
@@ -11,6 +11,7 @@ import eu.mhsl.minecraft.pixelpics.render.tint.BiomeTintProvider;
import eu.mhsl.minecraft.pixelpics.render.tint.TintResolver;
import eu.mhsl.minecraft.pixelpics.render.util.ColorUtil;
import eu.mhsl.minecraft.pixelpics.render.util.MathUtil;
import java.util.EnumSet;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.bukkit.Material;
@@ -45,6 +46,43 @@ 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;
// Brightness of a surface under water seen from above, 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 above the face (water's light block = 1), so the floor
// gets darker with VERTICAL depth; the path length through the water plays no role in vanilla when the camera
// is above the surface (no water fog there): one translucent surface layer over the dimmed scene.
private static final double[] SKY_LIGHT = new double[16];
static {
for (int level = 0; level < 16; level++) {
double l = level / 15.0;
double b = l / (4 - 3 * l);
double inv = 1 - b;
SKY_LIGHT[level] = (b + (1 - inv * inv * inv * inv)) / 2;
}
}
// Vanilla HalfTransparentBlock / LeavesBlock that do not occlude: water renders its faces against them with the
// water overlay texture (FluidRenderer). Blue ice is a HalfTransparentBlock too but occludes, packed ice is not.
private static final EnumSet<Material> WATER_OVERLAY_NEIGHBOURS = buildWaterOverlayNeighbours();
private static EnumSet<Material> buildWaterOverlayNeighbours() {
EnumSet<Material> set = EnumSet.noneOf(Material.class);
for (Material m : Material.values()) {
String n = m.name();
if (m.isLegacy()) continue;
if (n.equals("GLASS")
|| n.equals("TINTED_GLASS")
|| n.endsWith("_STAINED_GLASS")
|| n.equals("ICE")
|| n.equals("FROSTED_ICE")
|| n.equals("SLIME_BLOCK")
|| n.equals("HONEY_BLOCK")
|| n.endsWith("_LEAVES")) set.add(m);
}
return set;
}
private final BlockModelRegistry registry;
private final BiomeTintProvider tintProvider;
private final SkyRenderer skyRenderer;
@@ -146,7 +184,10 @@ public final class SnapshotRaytracer {
}
// Compose back to front: final surface (fogged), the liquid volume where it lies, translucent layers.
int color = Double.isInfinite(finalT) ? finalColor : fog(finalColor, finalT, skyColor);
// Surfaces under water (seen from above it) are dimmed by their depth like vanilla sky light.
int color = Double.isInfinite(finalT)
? finalColor
: fog(blocks.underwaterLight(finalColor, finalT), finalT, skyColor);
boolean liquidPending = blocks.liquidT >= 0 && blocks.liquidT < finalT;
for (int i = layers - 1; i >= 0; i--) {
if (liquidPending && layerT[i] < blocks.liquidT) {
@@ -157,7 +198,8 @@ public final class SnapshotRaytracer {
// Added light fades with distance like the surface it would otherwise be.
color = ColorUtil.add(color, ColorUtil.shade(layerColor[i], 1 - fogFactor(layerT[i])));
} else {
color = ColorUtil.mix(fog(layerColor[i], layerT[i], skyColor), color, layerAlpha[i], 1 - layerAlpha[i]);
int layer = blocks.underwaterLight(layerColor[i], layerT[i]);
color = ColorUtil.mix(fog(layer, layerT[i], skyColor), color, layerAlpha[i], 1 - layerAlpha[i]);
}
}
if (liquidPending) color = blocks.applyLiquid(color, finalT, skyColor);
@@ -191,6 +233,9 @@ public final class SnapshotRaytracer {
private boolean done;
double liquidT = -1;
// The ray starts inside a liquid (underwater photo): the liquid acts as a fog along the path instead.
private final boolean startsInLiquid;
private Material left; // material of the voxel the ray passed through last
private int liquidColor;
private double liquidFactor;
private boolean reflected;
@@ -211,6 +256,8 @@ public final class SnapshotRaytracer {
this.dy = direction.getY();
this.dz = direction.getZ();
this.dda = new VoxelDDA(ox, oy, oz, dx, dy, dz);
BlockData start = snapshot.getBlockData((int) Math.floor(ox), (int) Math.floor(oy), (int) Math.floor(oz));
this.startsInLiquid = start.getMaterial() != Material.AIR && registry.get(start).transparency > 0;
}
/** The next block surface with {@code t > minT}, or null when the ray leaves the render distance. */
@@ -222,20 +269,31 @@ public final class SnapshotRaytracer {
}
int bx = dda.x, by = dda.y, bz = dda.z;
BlockData data = snapshot.getBlockData(bx, by, bz);
if (data.getMaterial() == Material.AIR) {
advance();
Material material = data.getMaterial();
if (material == Material.AIR) {
advance(material);
continue;
}
ResolvedModel model = registry.get(data);
FaceHit hit =
ElementIntersector.intersect(model, ox - bx, oy - by, oz - bz, dx, dy, dz, bx, by, bz, minT);
if (hit == null) {
advance();
advance(material);
continue;
}
if (model.transparency > 0) {
if (liquidT >= 0 && material == Material.WATER && WATER_OVERLAY_NEIGHBOURS.contains(left)) {
// Under water, leaving glass/ice/slime/honey/leaves into water again: vanilla FluidRenderer
// draws the water's faces against such non-occluding blocks (side faces with the water
// overlay texture), facing into the block, so from inside it the far wall is a water layer.
int c = shadeAndTint(hit, data, snapshot, bx, by, bz);
int a = (int) Math.round((1 - model.transparency) * 255);
advance(material);
return new BlockHit(
ColorUtil.argb(a, ColorUtil.red(c), ColorUtil.green(c), ColorUtil.blue(c)), hit.t());
}
recordLiquid(model, hit, data, bx, by, bz);
advance();
advance(material);
continue;
}
int color = shadeAndTint(hit, data, snapshot, bx, by, bz);
@@ -248,7 +306,9 @@ public final class SnapshotRaytracer {
return null;
}
private void advance() {
/** Steps to the next voxel; {@code material} is the one of the voxel being left. */
private void advance(Material material) {
left = material;
dda.advance();
steps++;
}
@@ -269,15 +329,50 @@ public final class SnapshotRaytracer {
}
}
/** Liquid volume over {@code behind}: denser with depth down to the final surface, plus reflection. */
/**
* The liquid over {@code behind} (everything beyond its surface, already dimmed by
* {@link #underwaterLight}): seen from above, one translucent surface layer like vanilla, independent of
* the path length through the liquid (before, the surface weight grew with the path, so blocks a few
* blocks deep and anything seen at a shallow angle vanished in the water colour). From inside the liquid
* the path-length model stays (a fog). Plus reflection.
*/
int applyLiquid(int behind, double finalT, int skyColor) {
double depthIn = Double.isInfinite(finalT) ? 0 : finalT - liquidT;
int surface = fog(liquidColor, liquidT, skyColor);
int c = ColorUtil.mix(behind, surface, liquidFactor, (1 - liquidFactor) * (1 + depthIn / 5.0));
double surfaceWeight = 1 - liquidFactor;
if (startsInLiquid) {
double depthIn = Double.isInfinite(finalT) ? 0 : finalT - liquidT;
surfaceWeight *= 1 + depthIn / 5.0;
}
int c = ColorUtil.mix(behind, surface, liquidFactor, surfaceWeight);
if (reflected) c = ColorUtil.mix(c, reflectionColor, 1 - reflectionFactor, reflectionFactor);
return c;
}
/**
* {@code color} of a surface at ray parameter {@code t} dimmed like vanilla sky light under water, when it
* lies beyond the water surface and the ray started above it: level 15 minus the blocks from the block in
* front of the surface up to the topmost water block of that column (water, and non-occluding blocks below
* it such as glass, cost one level each). Unchanged without water on the ray.
*/
int underwaterLight(int color, double t) {
if (liquidT < 0 || startsInLiquid || !(t > liquidT) || 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.
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 water = 0;
for (int i = 0; i < 15; i++) {
Material m = snapshot.getBlockData(bx, by + i, bz).getMaterial();
if (m == Material.WATER) {
water = i + 1;
} else if (m == Material.AIR || m == Material.CAVE_AIR || m == Material.VOID_AIR || m.isOccluding()) {
break;
}
}
return water == 0 ? color : ColorUtil.shade(color, SKY_LIGHT[15 - water]);
}
/**
* 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
+110 -1
View File
@@ -176,6 +176,19 @@ public class PPTest extends JavaPlugin {
scenes.put("rf_vlayers_close", new Scene("rf_vlayers_close", this::rfVisualLayers, 3.2, 1.5, -2.4, 3.2, 0.5, 1));
scenes.put("rf_vstates", new Scene("rf_vstates", this::rfVisualStates, 5.5, 3.4, -6, 5.5, 0.9, 2.5));
scenes.put("rf_vstates_close", new Scene("rf_vstates_close", this::rfVisualStates, 3.2, 1.7, -2.6, 3.2, 0.9, 0.5));
// Block geometry (vanilla FaceBakery): rescaled element rotations (cross plants, amethyst, campfire, fire,
// raised rails, tripwire hooks, coral wall fans, dripleaf, stems, sea pickles), Euler x/y/z element
// rotations (hanging sign rot 3/7/11/15 chains), inverted elements (spawner/vault inner faces), 22.5° steps.
scenes.put("geo_plants", new Scene("geo_plants", this::geoPlants, 5.5, 2.6, -3.2, 5.5, 0.4, 2.2));
scenes.put("geo_plants_close", new Scene("geo_plants_close", this::geoPlants, 2.5, 1.1, -0.9, 2.5, 0.5, 1));
scenes.put("geo_misc", new Scene("geo_misc", this::geoMisc, 6, 3.4, -4.5, 6, 0.5, 3));
scenes.put("geo_misc_close", new Scene("geo_misc_close", this::geoMisc, 3, 1.6, -1.6, 3, 0.4, 1));
scenes.put("geo_misc_signs", new Scene("geo_misc_signs", this::geoMisc, 4, 2.4, 3.5, 4, 2.2, 7));
// Water depth vs. translucency: a pool with columns 1-6 blocks deep (x pairs), each with a glass block,
// a gold block and white stained glass on the sand bottom; seen from the shore, from above and from inside.
scenes.put("geo_water", new Scene("geo_water", this::geoWater, 6, 3.5, -5, 6, -3, 2.5));
scenes.put("geo_water_top", new Scene("geo_water_top", this::geoWater, 6, 9, 2.4, 6, -4, 2.5));
scenes.put("geo_water_under", new Scene("geo_water_under", this::geoWater, 11.5, -3.3, 0.3, 4, -5, 3));
}
private void rfVisualLayers(Location o) {
@@ -587,7 +600,7 @@ public class PPTest extends JavaPlugin {
Location eye = o.clone().add(sc.camX, sc.camY, sc.camZ);
Vector look = o.clone().add(sc.lookX, sc.lookY, sc.lookZ).toVector().subtract(eye.toVector());
eye.setDirection(look);
RenderJob job = r.prepare(eye, sc.name.endsWith("_sq") ? new Resolution(384, 384) : sc.name.startsWith("proj_") || sc.name.startsWith("rf_") || sc.name.startsWith("pose_") || sc.name.startsWith("frames") ? new Resolution(960, 640) : new Resolution(480, 320), null);
RenderJob job = r.prepare(eye, sc.name.endsWith("_sq") ? new Resolution(384, 384) : sc.name.startsWith("proj_") || sc.name.startsWith("rf_") || sc.name.startsWith("pose_") || sc.name.startsWith("frames") || sc.name.startsWith("geo_") ? new Resolution(960, 640) : new Resolution(480, 320), null);
if (sc.name.equals("dbg")) {
try {
java.lang.reflect.Field f = DefaultScreenRenderer.class.getDeclaredField("objectBaker");
@@ -1588,6 +1601,102 @@ public class PPTest extends JavaPlugin {
frame(o, 5, 5, 5, BlockFace.NORTH, false, synthMap(), 0, true);
}
/** Rows of cross/rescaled plants on grass (z=0..3), potted plants and stems; see the geo_* scenes. */
private void geoPlants(Location o) {
for (int x = -1; x <= 12; x++) for (int z = -1; z <= 5; z++) put(o, x, -1, z, "grass_block");
String[] row0 = {"short_grass", "fern", "dandelion", "poppy", "azure_bluet", "oxeye_daisy", "cornflower",
"oak_sapling", "birch_sapling", "cherry_sapling", "dead_bush", "brown_mushroom"};
String[] row1 = {"sweet_berry_bush[age=3]", "sugar_cane", "cobweb", "red_tulip", "wither_rose", "bush",
"short_dry_grass", "firefly_bush", "mangrove_propagule[hanging=false,stage=1]", "lily_of_the_valley",
"pumpkin_stem[age=7]", "crimson_roots"};
String[] row3 = {"potted_fern", "potted_oak_sapling", "potted_red_tulip", "potted_dead_bush",
"potted_azalea_bush", "potted_mangrove_propagule", "big_dripleaf_stem[facing=north]",
"sea_pickle[pickles=4,waterlogged=false]", "sea_pickle[pickles=1,waterlogged=false]", "azalea",
"flowering_azalea", "pointed_dripstone[thickness=tip,vertical_direction=up]"};
for (int x = 0; x < 12; x++) {
put(o, x, 0, 0, row0[x]);
put(o, x, 0, 1, row1[x]);
put(o, x, 0, 3, row3[x]);
}
for (int x = 0; x < 12; x += 3) {
put(o, x, 0, 2, "tall_grass[half=lower]");
put(o, x, 1, 2, "tall_grass[half=upper]");
put(o, x + 1, 0, 2, "sunflower[half=lower]");
put(o, x + 1, 1, 2, "sunflower[half=upper]");
}
put(o, 2, 0, 2, "big_dripleaf[facing=north,tilt=none]");
put(o, 5, 0, 2, "melon_stem[age=4]");
put(o, 8, 0, 2, "attached_pumpkin_stem[facing=east]");
put(o, 9, 0, 2, "pumpkin");
put(o, 11, 0, 2, "large_fern[half=lower]");
put(o, 11, 1, 2, "large_fern[half=upper]");
}
/**
* Row z=0: amethyst cluster/buds (up/north/east), campfires, fire; z=2: raised rails, tripwire hooks, coral wall
* fans, spawner/vault/trial spawner (inverted inner faces), snow layer 2; z=6/7: hanging signs rot 0-15 under a
* stone ceiling (rot 3/7/11/15 = Euler chain rotation).
*/
private void geoMisc(Location o) {
put(o, 0, 0, 0, "amethyst_cluster[facing=up]");
put(o, 1, 0, 0, "large_amethyst_bud[facing=up]");
put(o, 2, 0, 0, "medium_amethyst_bud[facing=up]");
put(o, 3, 0, 0, "small_amethyst_bud[facing=up]");
put(o, 4, 0, 1, "amethyst_block");
put(o, 4, 0, 0, "amethyst_cluster[facing=north]");
put(o, 5, 0, 1, "amethyst_block");
put(o, 5, 1, 1, "large_amethyst_bud[facing=up]");
put(o, 6, 0, 0, "campfire[lit=true,facing=south]");
put(o, 7, 0, 0, "campfire[lit=false,facing=east]");
put(o, 8, 0, 0, "soul_campfire[lit=true,facing=south]");
put(o, 9, 0, 0, "netherrack");
put(o, 9, 1, 0, "fire");
put(o, 10, 0, 1, "stone");
put(o, 10, 0, 0, "fire[north=true]");
put(o, 11, 0, 0, "soul_fire");
put(o, 11, -1, 0, "soul_sand");
put(o, 0, 0, 2, "rail[shape=ascending_east]");
put(o, 1, 0, 2, "stone");
put(o, 2, 0, 2, "powered_rail[shape=ascending_north,powered=true]");
put(o, 2, 0, 1, "stone");
put(o, 3, 0, 2, "detector_rail[shape=ascending_west]");
put(o, 2, 0, 3, "stone");
put(o, 4, 0, 3, "stone");
put(o, 4, 0, 2, "tripwire_hook[attached=true,facing=north,powered=false]");
put(o, 5, 0, 3, "stone");
put(o, 5, 0, 2, "tripwire_hook[attached=true,facing=north,powered=true]");
put(o, 6, 0, 3, "stone");
put(o, 6, 0, 2, "tube_coral_wall_fan[facing=north,waterlogged=false]");
put(o, 7, 0, 3, "stone");
put(o, 7, 0, 2, "fire_coral_wall_fan[facing=north,waterlogged=false]");
put(o, 8, 0, 2, "spawner");
put(o, 9, 0, 2, "vault[facing=north]");
put(o, 10, 0, 2, "trial_spawner");
put(o, 11, 0, 2, "snow[layers=2]");
for (int x = -1; x <= 9; x++) for (int z = 5; z <= 8; z++) put(o, x, 4, z, "stone");
for (int r = 0; r < 16; r++) put(o, r % 8, 3, 6 + r / 8, "oak_hanging_sign[rotation=" + r + ",attached=false]");
}
/** Pool: water columns 1..6 deep (two blocks wide each) on sand, with glass/gold/white stained glass at the bottom. */
private void geoWater(Location o) {
for (int x = -1; x <= 12; x++)
for (int z = -1; z <= 5; z++)
for (int y = -8; y <= -1; y++) {
boolean wall = x == -1 || x == 12 || z == -1 || z == 5;
int depth = x < 0 || x > 11 ? 0 : x / 2 + 1;
String b = wall ? "smooth_stone" : y >= -depth ? "water" : y == -depth - 1 ? "sand" : "stone";
put(o, x, y, z, b);
}
for (int x = 0; x < 12; x += 2) {
int y = -(x / 2 + 1);
put(o, x, y, 1, "glass");
put(o, x + 1, y, 1, "glass");
put(o, x, y, 2, "gold_block");
put(o, x + 1, y, 3, "white_stained_glass");
}
for (int x = -1; x <= 12; x++) put(o, x, 0, 6, "stone");
}
/** Black wall z=6 with a white vertical stripe (x=5) and a white horizontal stripe (y=5). */
private void camStripes(Location o) {
for (int a = -6; a < 18; a++)