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:
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
|
||||
|
||||
@@ -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++)
|
||||
|
||||
Reference in new issue
Block a user