simplify entity, tint and util code: shared direction helpers (axis, of, opposite), one inverted-face test and frac for block and entity intersectors, element rotate/unrotate without allocating copies, entity scene without the per-entity list and with a single child push, cube corners and hit normal without temp arrays, tint channel cached per material, redstone power from the block data, shared leash faces and frame cards, texture ops on colour helpers, simpler text and chest/shulker/statue texture code, dead helper and dead null checks removed, comments trimmed
This commit is contained in:
1 parent
2a3db3d7aa
commit
cbc859b565
28 files changed
+466
-615
No files matched your search
@@ -12,11 +12,34 @@ public enum Direction {
|
|||||||
WEST(-1, 0, 0),
|
WEST(-1, 0, 0),
|
||||||
EAST(1, 0, 0);
|
EAST(1, 0, 0);
|
||||||
public final int nx, ny, nz;
|
public final int nx, ny, nz;
|
||||||
|
/** The axis of the normal: 0 = x, 1 = y, 2 = z. */
|
||||||
|
public final int axis;
|
||||||
|
|
||||||
Direction(int nx, int ny, int nz) {
|
Direction(int nx, int ny, int nz) {
|
||||||
this.nx = nx;
|
this.nx = nx;
|
||||||
this.ny = ny;
|
this.ny = ny;
|
||||||
this.nz = nz;
|
this.nz = nz;
|
||||||
|
this.axis = nx != 0 ? 0 : ny != 0 ? 1 : 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The face of a box on {@code axis}: the low-coordinate one (WEST/DOWN/NORTH) when {@code negative}. */
|
||||||
|
public static Direction of(int axis, boolean negative) {
|
||||||
|
return switch (axis) {
|
||||||
|
case 0 -> negative ? WEST : EAST;
|
||||||
|
case 1 -> negative ? DOWN : UP;
|
||||||
|
default -> negative ? NORTH : SOUTH;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public Direction opposite() {
|
||||||
|
return switch (this) {
|
||||||
|
case DOWN -> UP;
|
||||||
|
case UP -> DOWN;
|
||||||
|
case NORTH -> SOUTH;
|
||||||
|
case SOUTH -> NORTH;
|
||||||
|
case WEST -> EAST;
|
||||||
|
case EAST -> WEST;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Direction fromName(String name) {
|
public static Direction fromName(String name) {
|
||||||
|
|||||||
@@ -147,6 +147,13 @@ public final class Element {
|
|||||||
|
|
||||||
/** Model space -> world: vanilla blockstate rotation (x steps, then y steps; clockwise about the centre). */
|
/** Model space -> world: vanilla blockstate rotation (x steps, then y steps; clockwise about the centre). */
|
||||||
public static double[] rotate(double x, double y, double z, int xSteps, int ySteps, boolean point) {
|
public static double[] rotate(double x, double y, double z, int xSteps, int ySteps, boolean point) {
|
||||||
|
double[] out = new double[3];
|
||||||
|
rotate(out, x, y, z, xSteps, ySteps, point);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@link #rotate} into {@code out} (allocation-free, for the intersectors). */
|
||||||
|
public static void rotate(double[] out, double x, double y, double z, int xSteps, int ySteps, boolean point) {
|
||||||
double c = point ? 0.5 : 0;
|
double c = point ? 0.5 : 0;
|
||||||
double dx = x - c, dy = y - c, dz = z - c;
|
double dx = x - c, dy = y - c, dz = z - c;
|
||||||
for (int i = 0; i < xSteps; i++) {
|
for (int i = 0; i < xSteps; i++) {
|
||||||
@@ -159,13 +166,22 @@ public final class Element {
|
|||||||
dx = nx;
|
dx = nx;
|
||||||
dz = nz;
|
dz = nz;
|
||||||
}
|
}
|
||||||
return new double[] {dx + c, dy + c, dz + c};
|
out[0] = dx + c;
|
||||||
|
out[1] = dy + c;
|
||||||
|
out[2] = dz + c;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** World -> model space: the inverse of {@link #rotate}. */
|
/** World -> model space: the inverse of {@link #rotate}. */
|
||||||
public static double[] unrotate(double x, double y, double z, int xSteps, int ySteps, boolean point) {
|
public static double[] unrotate(double x, double y, double z, int xSteps, int ySteps, boolean point) {
|
||||||
double[] p = rotate(x, y, z, 0, (4 - ySteps) % 4, point);
|
double[] out = new double[3];
|
||||||
return rotate(p[0], p[1], p[2], (4 - xSteps) % 4, 0, point);
|
unrotate(out, x, y, z, xSteps, ySteps, point);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@link #unrotate} into {@code out}. */
|
||||||
|
public 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isAxisAligned() {
|
public boolean isAxisAligned() {
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
package eu.mhsl.minecraft.pixelpics.render.entity;
|
package eu.mhsl.minecraft.pixelpics.render.entity;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A 3x3 linear transform plus translation (affine), used to compose entity bone hierarchies and to
|
* A 3x3 linear transform (row-major) plus translation. Ray parameters {@code t} are preserved under affine maps, so
|
||||||
* map a cube's local space to world space. The {@code apply} parameter {@code t} of a ray is
|
* distances stay consistent between world and local space.
|
||||||
* preserved under affine maps, so ray distances stay consistent between world and local space.
|
|
||||||
*/
|
*/
|
||||||
public final class Affine {
|
public final class Affine {
|
||||||
|
|
||||||
// row-major 3x3
|
|
||||||
public final double[] r;
|
public final double[] r;
|
||||||
public final double[] t;
|
public final double[] t;
|
||||||
|
|
||||||
@@ -16,8 +14,13 @@ public final class Affine {
|
|||||||
this.t = t;
|
this.t = t;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A linear map (no translation) from its row-major matrix. */
|
||||||
|
private static Affine linear(double... r) {
|
||||||
|
return new Affine(r, new double[] {0, 0, 0});
|
||||||
|
}
|
||||||
|
|
||||||
public static Affine identity() {
|
public static Affine identity() {
|
||||||
return new Affine(new double[] {1, 0, 0, 0, 1, 0, 0, 0, 1}, new double[] {0, 0, 0});
|
return linear(1, 0, 0, 0, 1, 0, 0, 0, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Affine translation(double x, double y, double z) {
|
public static Affine translation(double x, double y, double z) {
|
||||||
@@ -39,63 +42,62 @@ public final class Affine {
|
|||||||
y /= n;
|
y /= n;
|
||||||
z /= n;
|
z /= n;
|
||||||
w /= n;
|
w /= n;
|
||||||
return new Affine(
|
return linear(
|
||||||
new double[] {
|
1 - 2 * (y * y + z * z),
|
||||||
1 - 2 * (y * y + z * z),
|
2 * (x * y - z * w),
|
||||||
2 * (x * y - z * w),
|
2 * (x * z + y * w),
|
||||||
2 * (x * z + y * w),
|
2 * (x * y + z * w),
|
||||||
2 * (x * y + z * w),
|
1 - 2 * (x * x + z * z),
|
||||||
1 - 2 * (x * x + z * z),
|
2 * (y * z - x * w),
|
||||||
2 * (y * z - x * w),
|
2 * (x * z - y * w),
|
||||||
2 * (x * z - y * w),
|
2 * (y * z + x * w),
|
||||||
2 * (y * z + x * w),
|
1 - 2 * (x * x + y * y));
|
||||||
1 - 2 * (x * x + y * y)
|
|
||||||
},
|
|
||||||
new double[] {0, 0, 0});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Right-handed rotation by {@code a} radians about the unit axis {@code (x, y, z)} (Rodrigues). */
|
/** Right-handed rotation by {@code a} radians about the unit axis {@code (x, y, z)} (Rodrigues). */
|
||||||
public static Affine rotAxis(double x, double y, double z, double a) {
|
public static Affine rotAxis(double x, double y, double z, double a) {
|
||||||
double c = Math.cos(a), s = Math.sin(a), k = 1 - c;
|
double c = Math.cos(a), s = Math.sin(a), k = 1 - c;
|
||||||
return new Affine(
|
return linear(
|
||||||
new double[] {
|
c + x * x * k,
|
||||||
c + x * x * k,
|
x * y * k - z * s,
|
||||||
x * y * k - z * s,
|
x * z * k + y * s,
|
||||||
x * z * k + y * s,
|
y * x * k + z * s,
|
||||||
y * x * k + z * s,
|
c + y * y * k,
|
||||||
c + y * y * k,
|
y * z * k - x * s,
|
||||||
y * z * k - x * s,
|
z * x * k - y * s,
|
||||||
z * x * k - y * s,
|
z * y * k + x * s,
|
||||||
z * y * k + x * s,
|
c + z * z * k);
|
||||||
c + z * z * k
|
|
||||||
},
|
|
||||||
new double[] {0, 0, 0});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Affine scale(double s) {
|
public static Affine scale(double s) {
|
||||||
return new Affine(new double[] {s, 0, 0, 0, s, 0, 0, 0, s}, new double[] {0, 0, 0});
|
return linear(s, 0, 0, 0, s, 0, 0, 0, s);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Affine scale(double sx, double sy, double sz) {
|
public static Affine scale(double sx, double sy, double sz) {
|
||||||
return new Affine(new double[] {sx, 0, 0, 0, sy, 0, 0, 0, sz}, new double[] {0, 0, 0});
|
return linear(sx, 0, 0, 0, sy, 0, 0, 0, sz);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Affine rotX(double a) {
|
public static Affine rotX(double a) {
|
||||||
double c = Math.cos(a), s = Math.sin(a);
|
double c = Math.cos(a), s = Math.sin(a);
|
||||||
return new Affine(new double[] {1, 0, 0, 0, c, -s, 0, s, c}, new double[] {0, 0, 0});
|
return linear(1, 0, 0, 0, c, -s, 0, s, c);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Affine rotY(double a) {
|
public static Affine rotY(double a) {
|
||||||
double c = Math.cos(a), s = Math.sin(a);
|
double c = Math.cos(a), s = Math.sin(a);
|
||||||
return new Affine(new double[] {c, 0, s, 0, 1, 0, -s, 0, c}, new double[] {0, 0, 0});
|
return linear(c, 0, s, 0, 1, 0, -s, 0, c);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Affine rotZ(double a) {
|
public static Affine rotZ(double a) {
|
||||||
double c = Math.cos(a), s = Math.sin(a);
|
double c = Math.cos(a), s = Math.sin(a);
|
||||||
return new Affine(new double[] {c, -s, 0, s, c, 0, 0, 0, 1}, new double[] {0, 0, 0});
|
return linear(c, -s, 0, s, c, 0, 0, 0, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** this ∘ o (apply o first, then this). */
|
/** JOML {@code rotationAround}: {@code T(c) · rot · T(-c)}. */
|
||||||
|
public static Affine rotationAround(Affine rot, double cx, double cy, double cz) {
|
||||||
|
return translation(cx, cy, cz).mul(rot).mul(translation(-cx, -cy, -cz));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** this ∘ o (apply o first, then this). */
|
||||||
public Affine mul(Affine o) {
|
public Affine mul(Affine o) {
|
||||||
double[] a = this.r, b = o.r;
|
double[] a = this.r, b = o.r;
|
||||||
double[] nr = new double[9];
|
double[] nr = new double[9];
|
||||||
@@ -121,17 +123,16 @@ public final class Affine {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Linear part only (for directions). */
|
public double determinant() {
|
||||||
public double[] applyLinear(double x, double y, double z) {
|
return r[0] * (r[4] * r[8] - r[5] * r[7])
|
||||||
return new double[] {
|
- r[1] * (r[3] * r[8] - r[5] * r[6])
|
||||||
r[0] * x + r[1] * y + r[2] * z, r[3] * x + r[4] * y + r[5] * z, r[6] * x + r[7] * y + r[8] * z
|
+ r[2] * (r[3] * r[7] - r[4] * r[6]);
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** General affine inverse (3x3 inverse + translation). */
|
/** Affine inverse; a singular matrix yields a zero linear part. */
|
||||||
public Affine inverse() {
|
public Affine inverse() {
|
||||||
double a = r[0], b = r[1], c = r[2], d = r[3], e = r[4], f = r[5], g = r[6], h = r[7], i = r[8];
|
double a = r[0], b = r[1], c = r[2], d = r[3], e = r[4], f = r[5], g = r[6], h = r[7], i = r[8];
|
||||||
double det = a * (e * i - f * h) - b * (d * i - f * g) + c * (d * h - e * g);
|
double det = determinant();
|
||||||
double inv = Math.abs(det) < 1e-12 ? 0 : 1.0 / det;
|
double inv = Math.abs(det) < 1e-12 ? 0 : 1.0 / det;
|
||||||
double[] ir = new double[] {
|
double[] ir = new double[] {
|
||||||
(e * i - f * h) * inv, (c * h - b * i) * inv, (b * f - c * e) * inv,
|
(e * i - f * h) * inv, (c * h - b * i) * inv, (b * f - c * e) * inv,
|
||||||
|
|||||||
@@ -6,16 +6,10 @@ import java.util.List;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maps a {@link BlockEntityState} to the models its vanilla BlockEntityRenderer draws (from the client
|
* Maps a {@link BlockEntityState} to the dumped models its vanilla BlockEntityRenderer draws (see
|
||||||
* dump, see {@code tools/VanillaModelDump.java}) and to the renderer's PoseStack. Every pose below is
|
* {@code tools/VanillaModelDump.java}) and to the renderer's PoseStack. Every pose is transcribed from the decompiled
|
||||||
* transcribed from the decompiled 26.3 client renderers (ChestRenderer, SkullBlockRenderer,
|
* 26.3 client renderers, without calibration offsets. Beds and sign boards are block models since 26.x; only the sign
|
||||||
* BannerRenderer, ShulkerBoxRenderer, DecoratedPotRenderer, BellRenderer, ConduitRenderer,
|
* text is added here.
|
||||||
* LecternRenderer, EnchantTableRenderer, CopperGolemStatueBlockRenderer, Standing/HangingSignRenderer) — no
|
|
||||||
* calibration offsets. Mirrors the role
|
|
||||||
* {@link EntityModels} plays for mobs.
|
|
||||||
*
|
|
||||||
* <p>Beds and sign boards are plain block models since 26.x (no renderer model): the block-model
|
|
||||||
* pipeline draws them; only the sign TEXT is added here.
|
|
||||||
*/
|
*/
|
||||||
public final class BlockEntityModels {
|
public final class BlockEntityModels {
|
||||||
|
|
||||||
@@ -38,7 +32,11 @@ public final class BlockEntityModels {
|
|||||||
*/
|
*/
|
||||||
public record Part(String model, List<String> textures, Set<String> hidden, Paint paint) {
|
public record Part(String model, List<String> textures, Set<String> hidden, Paint paint) {
|
||||||
static Part of(String model, String texture) {
|
static Part of(String model, String texture) {
|
||||||
return new Part(model, List.of(texture), Set.of(), Paint.PLAIN);
|
return of(model, List.of(texture));
|
||||||
|
}
|
||||||
|
|
||||||
|
static Part of(String model, List<String> textures) {
|
||||||
|
return new Part(model, textures, Set.of(), Paint.PLAIN);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,8 +61,8 @@ public final class BlockEntityModels {
|
|||||||
case SINGLE -> "layer/chest#main";
|
case SINGLE -> "layer/chest#main";
|
||||||
};
|
};
|
||||||
yield new VanillaModel(
|
yield new VanillaModel(
|
||||||
rotationAround(Affine.rotY(Math.toRadians(-deg)), 0.5, 0, 0.5),
|
Affine.rotationAround(Affine.rotY(Math.toRadians(-deg)), 0.5, 0, 0.5),
|
||||||
List.of(new Part(layer, chestTextures(s), Set.of(), Paint.PLAIN)));
|
List.of(Part.of(layer, chestTextures(s))));
|
||||||
}
|
}
|
||||||
// SkullBlockRenderer ground: T(0.5,0,0.5)·rotY(-segment·22.5)·S(-1,-1,1); wall: T(0.5 - step·0.25,
|
// SkullBlockRenderer ground: T(0.5,0,0.5)·rotY(-segment·22.5)·S(-1,-1,1); wall: T(0.5 - step·0.25,
|
||||||
// 0.25, …)·rotY(-opposite.toYRot())·S(-1,-1,1).
|
// 0.25, …)·rotY(-opposite.toYRot())·S(-1,-1,1).
|
||||||
@@ -105,17 +103,12 @@ public final class BlockEntityModels {
|
|||||||
.mul(directionRotation(s.direction()))
|
.mul(directionRotation(s.direction()))
|
||||||
.mul(Affine.scale(1, -1, -1))
|
.mul(Affine.scale(1, -1, -1))
|
||||||
.mul(Affine.translation(0, -1, 0)),
|
.mul(Affine.translation(0, -1, 0)),
|
||||||
List.of(new Part(
|
List.of(Part.of(
|
||||||
"layer/shulker_box#main",
|
"layer/shulker_box#main", variantThenBase("entity/shulker/shulker", s.colorName()))));
|
||||||
s.colorName() == null
|
|
||||||
? List.of("entity/shulker/shulker")
|
|
||||||
: List.of("entity/shulker/shulker_" + s.colorName(), "entity/shulker/shulker"),
|
|
||||||
Set.of(),
|
|
||||||
Paint.PLAIN)));
|
|
||||||
// DecoratedPotRenderer: rotateAround(Y, 180 - facing.toYRot(), .5,.5,.5); neck/top/bottom from the
|
// DecoratedPotRenderer: rotateAround(Y, 180 - facing.toYRot(), .5,.5,.5); neck/top/bottom from the
|
||||||
// base sheet, each side its sherd pattern (or the blank side).
|
// base sheet, each side its sherd pattern (or the blank side).
|
||||||
case DECORATED_POT -> new VanillaModel(
|
case DECORATED_POT -> new VanillaModel(
|
||||||
rotationAround(Affine.rotY(Math.toRadians(180 - deg)), 0.5, 0.5, 0.5), potParts(s));
|
Affine.rotationAround(Affine.rotY(Math.toRadians(180 - deg)), 0.5, 0.5, 0.5), potParts(s));
|
||||||
// BellRenderer: the model as is (pivot 8,12,8 in block px); the frame is the block model.
|
// BellRenderer: the model as is (pivot 8,12,8 in block px); the frame is the block model.
|
||||||
case BELL -> new VanillaModel(
|
case BELL -> new VanillaModel(
|
||||||
Affine.identity(), List.of(Part.of("layer/bell#main", "entity/bell/bell_body")));
|
Affine.identity(), List.of(Part.of("layer/bell#main", "entity/bell/bell_body")));
|
||||||
@@ -151,15 +144,9 @@ public final class BlockEntityModels {
|
|||||||
// (CopperGolemStatueModel.setupAnim: root.y 0, root.zRot π), texture per weathering stage.
|
// (CopperGolemStatueModel.setupAnim: root.y 0, root.zRot π), texture per weathering stage.
|
||||||
case COPPER_GOLEM_STATUE -> new VanillaModel(
|
case COPPER_GOLEM_STATUE -> new VanillaModel(
|
||||||
Affine.translation(0.5, 0, 0.5).mul(Affine.rotY(Math.toRadians(-(deg + 180)))),
|
Affine.translation(0.5, 0, 0.5).mul(Affine.rotY(Math.toRadians(-(deg + 180)))),
|
||||||
List.of(new Part(
|
List.of(Part.of(
|
||||||
"posed/copper_golem_statue_" + (s.pose() == null ? "standing" : s.pose()) + "#main",
|
"posed/copper_golem_statue_" + (s.pose() == null ? "standing" : s.pose()) + "#main",
|
||||||
s.colorName() == null
|
variantThenBase("entity/copper_golem/copper_golem", s.colorName()))));
|
||||||
? List.of("entity/copper_golem/copper_golem")
|
|
||||||
: List.of(
|
|
||||||
"entity/copper_golem/copper_golem_" + s.colorName(),
|
|
||||||
"entity/copper_golem/copper_golem"),
|
|
||||||
Set.of(),
|
|
||||||
Paint.PLAIN)));
|
|
||||||
// Signs: text only (BlockEntityBaker). Items, spawner mobs, beams and portals are not entity models
|
// Signs: text only (BlockEntityBaker). Items, spawner mobs, beams and portals are not entity models
|
||||||
// (BlockEntityBaker.addContents).
|
// (BlockEntityBaker.addContents).
|
||||||
case SIGN,
|
case SIGN,
|
||||||
@@ -176,10 +163,9 @@ public final class BlockEntityModels {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The sign renderer's text transformation (Standing/HangingSignRenderer.textTransformation) WITHOUT
|
* Standing/HangingSignRenderer.textTransformation WITHOUT its final {@code scale(s, -s, s)}: the frame whose
|
||||||
* its final {@code scale(s, -s, s)}: returns the frame whose origin is the text anchor (centre of the
|
* origin is the text anchor (centre of the four lines), x right / y up / +z towards the reader; the font-pixel
|
||||||
* four lines), x right / y up / +z towards the reader, and the font-pixel size {@code s} in blocks via
|
* size {@code s} in blocks goes to {@code pxSize[0]}. Back text is the front transform turned by 180°.
|
||||||
* {@code out[0]}. Back text is the front transform turned by 180°.
|
|
||||||
*/
|
*/
|
||||||
public static Affine signTextFrame(BlockEntityState s, boolean front, double[] pxSize) {
|
public static Affine signTextFrame(BlockEntityState s, boolean front, double[] pxSize) {
|
||||||
double deg = s.facingDeg();
|
double deg = s.facingDeg();
|
||||||
@@ -243,15 +229,14 @@ public final class BlockEntityModels {
|
|||||||
* trapped chests between 24 and 26 December ({@link SpecialDates#isExtendedChristmas}), else normal/trapped.
|
* trapped chests between 24 and 26 December ({@link SpecialDates#isExtendedChristmas}), else normal/trapped.
|
||||||
*/
|
*/
|
||||||
private static List<String> chestTextures(BlockEntityState s) {
|
private static List<String> chestTextures(BlockEntityState s) {
|
||||||
|
if (s.kind() == BlockEntityState.Kind.ENDER_CHEST) return List.of("entity/chest/ender");
|
||||||
boolean xmas = SpecialDates.isExtendedChristmas();
|
boolean xmas = SpecialDates.isExtendedChristmas();
|
||||||
String base =
|
String base =
|
||||||
switch (s.kind()) {
|
switch (s.kind()) {
|
||||||
case TRAPPED_CHEST -> xmas ? "christmas" : "trapped";
|
case TRAPPED_CHEST -> xmas ? "christmas" : "trapped";
|
||||||
case ENDER_CHEST -> "ender";
|
|
||||||
case COPPER_CHEST -> s.colorName() == null ? "copper" : "copper_" + s.colorName();
|
case COPPER_CHEST -> s.colorName() == null ? "copper" : "copper_" + s.colorName();
|
||||||
default -> xmas ? "christmas" : "normal";
|
default -> xmas ? "christmas" : "normal";
|
||||||
};
|
};
|
||||||
if (s.kind() == BlockEntityState.Kind.ENDER_CHEST) return List.of("entity/chest/ender");
|
|
||||||
return switch (chestKind(s)) {
|
return switch (chestKind(s)) {
|
||||||
case LEFT -> List.of("entity/chest/" + base + "_left", "entity/chest/" + base);
|
case LEFT -> List.of("entity/chest/" + base + "_left", "entity/chest/" + base);
|
||||||
case RIGHT -> List.of("entity/chest/" + base + "_right", "entity/chest/" + base);
|
case RIGHT -> List.of("entity/chest/" + base + "_right", "entity/chest/" + base);
|
||||||
@@ -259,9 +244,9 @@ public final class BlockEntityModels {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** JOML {@code rotationAround}: {@code T(c) · rot · T(-c)}. */
|
/** {@code <base>_<variant>} first, then the base texture; just the base without a variant. */
|
||||||
private static Affine rotationAround(Affine rot, double cx, double cy, double cz) {
|
private static List<String> variantThenBase(String base, String variant) {
|
||||||
return Affine.translation(cx, cy, cz).mul(rot).mul(Affine.translation(-cx, -cy, -cz));
|
return variant == null ? List.of(base) : List.of(base + "_" + variant, base);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Minecraft {@code Direction.getRotation()} (JOML rotationXYZ = Rx·Ry·Rz); null/unknown = UP. */
|
/** Minecraft {@code Direction.getRotation()} (JOML rotationXYZ = Rx·Ry·Rz); null/unknown = UP. */
|
||||||
|
|||||||
@@ -3,14 +3,9 @@ package eu.mhsl.minecraft.pixelpics.render.entity;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Immutable snapshot of one block-entity captured on the main thread, sufficient to bake and place it
|
* Bukkit-free snapshot of one block-entity, captured on the main thread and baked off-thread through the entity path
|
||||||
* off-thread. Block-entities drawn by a vanilla BlockEntityRenderer (chests, banners, heads, shulker
|
* (see {@link BlockEntityModels}). {@code facingDeg} is the vanilla yaw of the facing/rotation (0 = south, 90 = west;
|
||||||
* boxes, …) have no block-model geometry; their shape is the renderer's entity model (dumped from the
|
* 16-step rotations are {@code segment · 22.5}). Type-specific fields are null/0/false/empty when unused.
|
||||||
* client, see {@link BlockEntityModels}), rendered through the same baking/intersection path as mobs.
|
|
||||||
* Bukkit-free so the baker needs no world access.
|
|
||||||
*
|
|
||||||
* <p>{@code facingDeg} is the vanilla yaw of the block's facing/rotation (0 = south, 90 = west; a
|
|
||||||
* 16-step rotation is {@code segment · 22.5}). Type-specific fields are null/0/false/empty when unused.
|
|
||||||
*/
|
*/
|
||||||
public record BlockEntityState(
|
public record BlockEntityState(
|
||||||
Kind kind,
|
Kind kind,
|
||||||
@@ -35,7 +30,7 @@ public record BlockEntityState(
|
|||||||
String pose // copper golem statue pose ("standing", "running", "sitting", "star"); null otherwise
|
String pose // copper golem statue pose ("standing", "running", "sitting", "star"); null otherwise
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/** The pre-contents layout (no items/entity/beam/pose): chests, signs, banners, heads, … */
|
/** Without contents (items, display entity, beam, pose); used by the test harnesses. */
|
||||||
public BlockEntityState(
|
public BlockEntityState(
|
||||||
Kind kind,
|
Kind kind,
|
||||||
int bx,
|
int bx,
|
||||||
|
|||||||
@@ -4,18 +4,16 @@ import eu.mhsl.minecraft.pixelpics.assets.model.Direction;
|
|||||||
import eu.mhsl.minecraft.pixelpics.assets.model.Face;
|
import eu.mhsl.minecraft.pixelpics.assets.model.Face;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unwraps a {@link ModelCube} into six {@link Face}s using the standard Minecraft/Bedrock box-UV
|
* Unwraps a {@link ModelCube} into six {@link Face}s with the Java box-UV layout (the cross around the {@code uv}
|
||||||
* layout (faces packed in the canonical cross around the {@code uv} offset). Mirror swaps the
|
* offset). Mirror swaps the left/right faces and flips each face horizontally.
|
||||||
* left/right faces and flips each face horizontally.
|
|
||||||
*/
|
*/
|
||||||
public final class BoxUv {
|
public final class BoxUv {
|
||||||
|
|
||||||
private BoxUv() {}
|
private BoxUv() {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns Faces indexed by {@link Direction#ordinal()}. UVs are normalized by the model's DECLARED
|
* Faces by {@link Direction#ordinal()}. UVs are normalised by the model's DECLARED texture size, so a
|
||||||
* texel size (so a higher-res pack texture — e.g. a 128x128 sheet for a model authored at 64x64 —
|
* higher-resolution pack texture maps proportionally; the actual size is used when undeclared.
|
||||||
* still maps proportionally, same layout). Falls back to the actual texture size if undeclared.
|
|
||||||
*/
|
*/
|
||||||
public static Face[] build(ModelCube cube, int[][] texture, int declaredW, int declaredH) {
|
public static Face[] build(ModelCube cube, int[][] texture, int declaredW, int declaredH) {
|
||||||
int texW = texture.length > 0 ? texture[0].length : 64;
|
int texW = texture.length > 0 ? texture[0].length : 64;
|
||||||
@@ -23,7 +21,6 @@ public final class BoxUv {
|
|||||||
int nW = declaredW > 0 ? declaredW : texW;
|
int nW = declaredW > 0 ? declaredW : texW;
|
||||||
int nH = declaredH > 0 ? declaredH : texH;
|
int nH = declaredH > 0 ? declaredH : texH;
|
||||||
|
|
||||||
// Modern per-face UV: each face carries its own {u, v, w, h} rect directly.
|
|
||||||
if (cube.faceUv != null) {
|
if (cube.faceUv != null) {
|
||||||
Face[] faces = new Face[6];
|
Face[] faces = new Face[6];
|
||||||
for (int i = 0; i < 6; i++) {
|
for (int i = 0; i < 6; i++) {
|
||||||
@@ -35,9 +32,8 @@ public final class BoxUv {
|
|||||||
double dx = cube.size[0], dy = cube.size[1], dz = cube.size[2];
|
double dx = cube.size[0], dy = cube.size[1], dz = cube.size[2];
|
||||||
double u = cube.uv[0], v = cube.uv[1];
|
double u = cube.uv[0], v = cube.uv[1];
|
||||||
|
|
||||||
// rect = {x, y, w, h} in texels, SIGNED — a negative width/height flips that axis. These match
|
// {x, y, w, h} texels, SIGNED (negative = flipped axis): the Blockbench/OptiFine box-UV layout (up/down
|
||||||
// the OptiFine/Blockbench box-UV layout EXACTLY (up/down are flipped), paired with the (s,t) the
|
// flipped). Must stay in sync with the face (s, t) orientation of ElementIntersector.faceS/faceT.
|
||||||
// EntityIntersector feeds in, so every face's texture orientation matches vanilla Java.
|
|
||||||
double[] east = {u, v + dz, dz, dy};
|
double[] east = {u, v + dz, dz, dy};
|
||||||
double[] west = {u + dz + dx, v + dz, dz, dy};
|
double[] west = {u + dz + dx, v + dz, dz, dy};
|
||||||
double[] north = {u + dz, v + dz, dx, dy};
|
double[] north = {u + dz, v + dz, dx, dy};
|
||||||
|
|||||||
@@ -10,9 +10,8 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bakes wall decorations into world-space {@link EntityCube}s: paintings as flat textured quads built from the
|
* Bakes wall decorations: paintings as a thin box over the captured world bounding box, item frames as their vanilla
|
||||||
* captured world bounding box, item frames as their vanilla frame block model plus the framed item (item
|
* frame block model plus the framed item or map.
|
||||||
* model geometry) or map. Added to the {@link EntityScene} like any other entity.
|
|
||||||
*/
|
*/
|
||||||
public final class DecorationBaker implements EntityBaker<DecorationState> {
|
public final class DecorationBaker implements EntityBaker<DecorationState> {
|
||||||
|
|
||||||
@@ -44,9 +43,9 @@ public final class DecorationBaker implements EntityBaker<DecorationState> {
|
|||||||
thicken(from, to, s.facing());
|
thicken(from, to, s.facing());
|
||||||
|
|
||||||
Face[] faces = new Face[6];
|
Face[] faces = new Face[6];
|
||||||
Direction front = direction(s.facing());
|
Direction front = Direction.valueOf(s.facing().name());
|
||||||
faces[front.ordinal()] = frontFace(art, s.facing());
|
faces[front.ordinal()] = frontFace(art, s.facing());
|
||||||
faces[opposite(front).ordinal()] = new Face(back, 0, 0, 1, 1, 0, -1);
|
faces[front.opposite().ordinal()] = new Face(back, 0, 0, 1, 1, 0, -1);
|
||||||
EntityCube cube = new EntityCube(from, to, faces, Affine.identity());
|
EntityCube cube = new EntityCube(from, to, faces, Affine.identity());
|
||||||
return RenderedEntity.of(List.of(cube));
|
return RenderedEntity.of(List.of(cube));
|
||||||
}
|
}
|
||||||
@@ -81,7 +80,7 @@ public final class DecorationBaker implements EntityBaker<DecorationState> {
|
|||||||
|
|
||||||
List<EntityCube> cubes = new ArrayList<>();
|
List<EntityCube> cubes = new ArrayList<>();
|
||||||
boolean map = s.mapPixels() != null;
|
boolean map = s.mapPixels() != null;
|
||||||
if (!s.invisible() && registry != null && items != null) {
|
if (!s.invisible()) {
|
||||||
String model = "block/" + (s.glow() ? "glow_item_frame" : "item_frame") + (map ? "_map" : "");
|
String model = "block/" + (s.glow() ? "glow_item_frame" : "item_frame") + (map ? "_map" : "");
|
||||||
items.bakeModel(
|
items.bakeModel(
|
||||||
registry.model(ResourceLocation.parse(model)),
|
registry.model(ResourceLocation.parse(model)),
|
||||||
@@ -95,13 +94,13 @@ public final class DecorationBaker implements EntityBaker<DecorationState> {
|
|||||||
.mul(Affine.rotZ(Math.PI))
|
.mul(Affine.rotZ(Math.PI))
|
||||||
.mul(Affine.scale(1 / 128.0))
|
.mul(Affine.scale(1 / 128.0))
|
||||||
.mul(Affine.translation(-64, -64, -1));
|
.mul(Affine.translation(-64, -64, -1));
|
||||||
// Quad (0,0)..(128,128) at z −0.01 facing local −Z (away from the wall); u along +x, v along +y.
|
// Quad (0,0)..(128,128) facing local −Z (away from the wall); u along +x, v along +y.
|
||||||
Face[] faces = new Face[6];
|
Face[] faces = new Face[6];
|
||||||
faces[Direction.NORTH.ordinal()] = new Face(s.mapPixels(), 1, 1, 0, 0, 0, -1);
|
faces[Direction.NORTH.ordinal()] = new Face(s.mapPixels(), 1, 1, 0, 0, 0, -1);
|
||||||
faces[Direction.SOUTH.ordinal()] = new Face(s.mapPixels(), 0, 1, 1, 0, 0, -1);
|
faces[Direction.SOUTH.ordinal()] = new Face(s.mapPixels(), 0, 1, 1, 0, 0, -1);
|
||||||
cubes.add(new EntityCube(new double[] {0, 0, -0.01}, new double[] {128, 128, -0.01}, faces, m));
|
cubes.add(new EntityCube(new double[] {0, 0, -0.01}, new double[] {128, 128, -0.01}, faces, m));
|
||||||
addMapMarkers(s, m, cubes);
|
addMapMarkers(s, m, cubes);
|
||||||
} else if (s.item() != null && items != null) {
|
} else if (s.item() != null) {
|
||||||
Affine m =
|
Affine m =
|
||||||
content.mul(Affine.rotZ(Math.toRadians(s.rotation() * 45))).mul(Affine.scale(0.5));
|
content.mul(Affine.rotZ(Math.toRadians(s.rotation() * 45))).mul(Affine.scale(0.5));
|
||||||
items.bake(s.item(), "fixed", false, m, cubes);
|
items.bake(s.item(), "fixed", false, m, cubes);
|
||||||
@@ -115,22 +114,18 @@ public final class DecorationBaker implements EntityBaker<DecorationState> {
|
|||||||
* (-1,1)..(1,-1) at z = n·-0.001 (n-th drawn decoration), {@code map/decorations/<asset>} with v0 at +y.
|
* (-1,1)..(1,-1) at z = n·-0.001 (n-th drawn decoration), {@code map/decorations/<asset>} with v0 at +y.
|
||||||
*/
|
*/
|
||||||
private void addMapMarkers(DecorationState s, Affine map, List<EntityCube> cubes) {
|
private void addMapMarkers(DecorationState s, Affine map, List<EntityCube> cubes) {
|
||||||
int count = 0;
|
List<DecorationState.MapMarker> markers = s.mapMarkers();
|
||||||
for (DecorationState.MapMarker mk : s.mapMarkers()) {
|
for (int n = 0; n < markers.size(); n++) {
|
||||||
|
DecorationState.MapMarker mk = markers.get(n);
|
||||||
int[][] sprite = textures.get(ResourceLocation.parse("map/decorations/" + mk.asset()))
|
int[][] sprite = textures.get(ResourceLocation.parse("map/decorations/" + mk.asset()))
|
||||||
.orElse(null);
|
.orElse(null);
|
||||||
if (sprite != null) {
|
if (sprite == null) continue;
|
||||||
Affine m = map.mul(Affine.translation(mk.x() / 2.0 + 64, mk.y() / 2.0 + 64, -0.02))
|
Affine m = map.mul(Affine.translation(mk.x() / 2.0 + 64, mk.y() / 2.0 + 64, -0.02))
|
||||||
.mul(Affine.rotZ(Math.toRadians(mk.rot() * 360 / 16.0)))
|
.mul(Affine.rotZ(Math.toRadians(mk.rot() * 360 / 16.0)))
|
||||||
.mul(Affine.scale(4, 4, 3))
|
.mul(Affine.scale(4, 4, 3))
|
||||||
.mul(Affine.translation(-0.125, 0.125, 0));
|
.mul(Affine.translation(-0.125, 0.125, 0));
|
||||||
double z = count * -0.001;
|
double z = n * -0.001;
|
||||||
Face[] faces = new Face[6];
|
cubes.add(new EntityCube(new double[] {-1, -1, z}, new double[] {1, 1, z}, ObjectBaker.card(sprite), m));
|
||||||
faces[Direction.SOUTH.ordinal()] = new Face(sprite, 0, 0, 1, 1, 0, -1);
|
|
||||||
faces[Direction.NORTH.ordinal()] = new Face(sprite, 1, 0, 0, 1, 0, -1);
|
|
||||||
cubes.add(new EntityCube(new double[] {-1, -1, z}, new double[] {1, 1, z}, faces, m));
|
|
||||||
}
|
|
||||||
count++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,38 +140,16 @@ public final class DecorationBaker implements EntityBaker<DecorationState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Front face with the per-facing horizontal flip so the texture reads upright and unmirrored. */
|
/** Front face with the per-facing horizontal flip so the texture reads upright and unmirrored. */
|
||||||
private Face frontFace(int[][] tex, DecorationState.Facing facing) {
|
private static Face frontFace(int[][] tex, DecorationState.Facing facing) {
|
||||||
boolean flipH = facing == DecorationState.Facing.SOUTH || facing == DecorationState.Facing.WEST;
|
boolean flipH = facing == DecorationState.Facing.SOUTH || facing == DecorationState.Facing.WEST;
|
||||||
double u1 = flipH ? 1 : 0, u2 = flipH ? 0 : 1;
|
double u1 = flipH ? 1 : 0, u2 = flipH ? 0 : 1;
|
||||||
return new Face(tex, u1, 0, u2, 1, 0, -1);
|
return new Face(tex, u1, 0, u2, 1, 0, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void thicken(double[] from, double[] to, DecorationState.Facing facing) {
|
private static void thicken(double[] from, double[] to, DecorationState.Facing facing) {
|
||||||
int a = facing.axis();
|
int a = facing.axis();
|
||||||
if (to[a] - from[a] >= MIN_THICKNESS) return;
|
if (to[a] - from[a] >= MIN_THICKNESS) return;
|
||||||
if (facing.sign() > 0) from[a] = to[a] - MIN_THICKNESS;
|
if (facing.sign() > 0) from[a] = to[a] - MIN_THICKNESS;
|
||||||
else to[a] = from[a] + MIN_THICKNESS;
|
else to[a] = from[a] + MIN_THICKNESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Direction direction(DecorationState.Facing f) {
|
|
||||||
return switch (f) {
|
|
||||||
case NORTH -> Direction.NORTH;
|
|
||||||
case SOUTH -> Direction.SOUTH;
|
|
||||||
case EAST -> Direction.EAST;
|
|
||||||
case WEST -> Direction.WEST;
|
|
||||||
case UP -> Direction.UP;
|
|
||||||
case DOWN -> Direction.DOWN;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Direction opposite(Direction d) {
|
|
||||||
return switch (d) {
|
|
||||||
case NORTH -> Direction.SOUTH;
|
|
||||||
case SOUTH -> Direction.NORTH;
|
|
||||||
case EAST -> Direction.WEST;
|
|
||||||
case WEST -> Direction.EAST;
|
|
||||||
case UP -> Direction.DOWN;
|
|
||||||
case DOWN -> Direction.UP;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -3,11 +3,8 @@ package eu.mhsl.minecraft.pixelpics.render.entity;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Immutable snapshot of a flat wall decoration (painting or item frame). These are Bukkit
|
* Snapshot of a wall decoration (painting or item frame). The world bounding box is captured directly: it already
|
||||||
* {@code Hanging} entities, not block-entities, but render as flat textured quads rather than CEM
|
* encodes vanilla's multi-block painting offset, and its centre is the item frame's entity position.
|
||||||
* models, so they have their own state/baker. The world-space bounding box is captured directly (it
|
|
||||||
* already encodes vanilla's awkward multi-block painting offset), avoiding placement math. Item frames use
|
|
||||||
* its centre (the vanilla entity position).
|
|
||||||
*/
|
*/
|
||||||
public record DecorationState(
|
public record DecorationState(
|
||||||
Kind kind,
|
Kind kind,
|
||||||
@@ -27,7 +24,7 @@ public record DecorationState(
|
|||||||
List<MapMarker> mapMarkers // framed map's decorations shown on item frames (banners, frames, …); may be empty
|
List<MapMarker> mapMarkers // framed map's decorations shown on item frames (banners, frames, …); may be empty
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/** The layout without map markers (paintings, frames without a map or with an undecorated one). */
|
/** Without map markers; used by the test harnesses. */
|
||||||
public DecorationState(
|
public DecorationState(
|
||||||
Kind kind,
|
Kind kind,
|
||||||
double minX,
|
double minX,
|
||||||
|
|||||||
@@ -7,16 +7,11 @@ import java.util.Map;
|
|||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bounded, thread-safe LRU cache of DERIVED textures — tinted / composited copies the bakers build per entity
|
* Bounded, thread-safe LRU cache of derived (tinted/composited) textures, so identical mobs share one texture.
|
||||||
* (sheep wool colours, villager type + profession + level overlays, dyed leather armor, collars, banner
|
|
||||||
* flags, …) — so identical mobs share one texture instead of re-tinting it on every bake.
|
|
||||||
*
|
*
|
||||||
* <p>Keys are built with {@link #key}: a value list of the inputs that determine the result. Source pixel
|
* <p>Keys ({@link #key}) list the inputs that determine the result; source grids compare by identity, which is exact
|
||||||
* grids may be part of the key; arrays compare by identity, which is exact because no grid is mutated once
|
* because no grid is mutated once built. Cached grids are shared: callers must not mutate them. The computation runs
|
||||||
* it is built (texture-cache grids live for the plugin's lifetime). Cached grids are shared and must not be
|
* outside the lock; concurrent misses may compute twice, the first stored result wins.
|
||||||
* mutated by callers.
|
|
||||||
* The computation runs outside the lock; concurrent misses on the same key may compute twice, the first
|
|
||||||
* stored result wins.
|
|
||||||
*/
|
*/
|
||||||
public final class DerivedTextureCache {
|
public final class DerivedTextureCache {
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
package eu.mhsl.minecraft.pixelpics.render.entity;
|
package eu.mhsl.minecraft.pixelpics.render.entity;
|
||||||
|
|
||||||
/**
|
/** Bakes a captured state into a {@link RenderedEntity}, or null when it has no geometry. */
|
||||||
* Bakes a captured state {@code S} (mob, block-entity or decoration) into a {@link RenderedEntity},
|
|
||||||
* or returns {@code null} when it has no renderable geometry. Lets {@link EntityScene} treat all three
|
|
||||||
* render paths uniformly.
|
|
||||||
*/
|
|
||||||
public interface EntityBaker<S> {
|
public interface EntityBaker<S> {
|
||||||
RenderedEntity bake(S state);
|
RenderedEntity bake(S state);
|
||||||
}
|
}
|
||||||
@@ -3,17 +3,16 @@ package eu.mhsl.minecraft.pixelpics.render.entity;
|
|||||||
import eu.mhsl.minecraft.pixelpics.assets.model.Face;
|
import eu.mhsl.minecraft.pixelpics.assets.model.Face;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A baked entity cube in world space: a local box (model pixels) plus the affine transform mapping it
|
* A baked entity cube: a local box, the affine map into the world, its six faces (by {@code Direction.ordinal()},
|
||||||
* into the world, its six faces, the precomputed inverse transform and a world-space AABB for
|
* null = not drawn), the inverse map and the world-space AABB.
|
||||||
* broad-phase culling.
|
|
||||||
*/
|
*/
|
||||||
public final class EntityCube {
|
public final class EntityCube {
|
||||||
public final double[] from; // local min (px, inflated)
|
public final double[] from; // local min (inflated)
|
||||||
public final double[] to; // local max
|
public final double[] to; // local max
|
||||||
public final Face[] faces; // by Direction.ordinal()
|
public final Face[] faces;
|
||||||
public final Affine toWorld;
|
public final Affine toWorld;
|
||||||
public final Affine toLocal; // inverse
|
public final Affine toLocal;
|
||||||
public final boolean additive; // emissive, added to what lies behind (energy swirl) instead of blended
|
public final boolean additive; // added to what lies behind (energy swirl) instead of blended
|
||||||
public final boolean unshaded; // no directional face shading (vanilla NO_CARDINAL_LIGHTING: eyes, breeze wind)
|
public final boolean unshaded; // no directional face shading (vanilla NO_CARDINAL_LIGHTING: eyes, breeze wind)
|
||||||
// Block-model element with from > to on some axis (spawner/vault inner faces): its faces point INTO the box
|
// Block-model element with from > to on some axis (spawner/vault inner faces): its faces point INTO the box
|
||||||
// and are back-face culled like vanilla quads (see EntityIntersector); mirrored = toWorld flips handedness.
|
// and are back-face culled like vanilla quads (see EntityIntersector); mirrored = toWorld flips handedness.
|
||||||
@@ -38,19 +37,16 @@ public final class EntityCube {
|
|||||||
this.toWorld = toWorld;
|
this.toWorld = toWorld;
|
||||||
this.toLocal = toWorld.inverse();
|
this.toLocal = toWorld.inverse();
|
||||||
this.inverted = from[0] > to[0] || from[1] > to[1] || from[2] > to[2];
|
this.inverted = from[0] > to[0] || from[1] > to[1] || from[2] > to[2];
|
||||||
double[] r = toWorld.r;
|
this.mirrored = toWorld.determinant() < 0;
|
||||||
double det = r[0] * (r[4] * r[8] - r[5] * r[7])
|
double[] r = toWorld.r, t = toWorld.t;
|
||||||
- r[1] * (r[3] * r[8] - r[5] * r[6])
|
|
||||||
+ r[2] * (r[3] * r[7] - r[4] * r[6]);
|
|
||||||
this.mirrored = det < 0;
|
|
||||||
for (int i = 0; i < 8; i++) {
|
for (int i = 0; i < 8; i++) {
|
||||||
double x = (i & 1) == 0 ? from[0] : to[0];
|
double x = (i & 1) == 0 ? from[0] : to[0];
|
||||||
double y = (i & 2) == 0 ? from[1] : to[1];
|
double y = (i & 2) == 0 ? from[1] : to[1];
|
||||||
double z = (i & 4) == 0 ? from[2] : to[2];
|
double z = (i & 4) == 0 ? from[2] : to[2];
|
||||||
double[] w = toWorld.apply(x, y, z);
|
|
||||||
for (int a = 0; a < 3; a++) {
|
for (int a = 0; a < 3; a++) {
|
||||||
if (w[a] < aabbMin[a]) aabbMin[a] = w[a];
|
double w = r[a * 3] * x + r[a * 3 + 1] * y + r[a * 3 + 2] * z + t[a];
|
||||||
if (w[a] > aabbMax[a]) aabbMax[a] = w[a];
|
if (w < aabbMin[a]) aabbMin[a] = w;
|
||||||
|
if (w > aabbMax[a]) aabbMax[a] = w;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,11 +8,10 @@ import eu.mhsl.minecraft.pixelpics.render.util.ColorUtil;
|
|||||||
import org.bukkit.util.Vector;
|
import org.bukkit.util.Vector;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Intersects a world-space ray with a single {@link EntityCube} (oriented box). The ray is mapped
|
* Intersects a world-space ray with a single {@link EntityCube}: the ray is mapped into the cube's local frame and
|
||||||
* into the cube's local frame, slab-tested, and the entry face is sampled. Fully transparent texels
|
* slab-tested (t stays the world-space one). Cut-out texels are holes; partially transparent ones keep their alpha
|
||||||
* are treated as holes (alpha cutout); partially transparent ones keep their alpha for the tracer to blend.
|
* for the tracer to blend. The test writes into a caller-owned {@link Hit} and allocates nothing; {@link #toFaceHit}
|
||||||
* The {@code t} is a world-space distance. The test writes into a caller-owned {@link Hit} and allocates
|
* builds the result only for the hit the caller keeps.
|
||||||
* nothing; {@link #toFaceHit} builds the result object only for the hit the caller finally keeps.
|
|
||||||
*/
|
*/
|
||||||
public final class EntityIntersector {
|
public final class EntityIntersector {
|
||||||
|
|
||||||
@@ -25,16 +24,13 @@ public final class EntityIntersector {
|
|||||||
public static final class Hit {
|
public static final class Hit {
|
||||||
public double t;
|
public double t;
|
||||||
public int color;
|
public int color;
|
||||||
int axis;
|
Direction dir;
|
||||||
boolean neg;
|
|
||||||
final double[] o = new double[3], d = new double[3];
|
final double[] o = new double[3], d = new double[3];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Nearest drawn face of the cube with {@code t > minT} (entry face first, else the far face); {@code minT}
|
* Nearest drawn face with {@code t > minT} (entry face first, else the far face). Cubes entered beyond
|
||||||
* lets the tracer continue behind a translucent hit. Cubes whose entry lies beyond {@code maxT} are
|
* {@code maxT} are rejected before any texture lookup; a hit at exactly {@code maxT} still counts.
|
||||||
* rejected early (no texture lookup); a hit at exactly {@code maxT} is still reported. On a hit the face
|
|
||||||
* is left in {@code out}.
|
|
||||||
*/
|
*/
|
||||||
public static boolean intersect(
|
public static boolean intersect(
|
||||||
EntityCube cube,
|
EntityCube cube,
|
||||||
@@ -55,9 +51,10 @@ public final class EntityIntersector {
|
|||||||
d[0] = r[0] * dx + r[1] * dy + r[2] * dz;
|
d[0] = r[0] * dx + r[1] * dy + r[2] * dz;
|
||||||
d[1] = r[3] * dx + r[4] * dy + r[5] * dz;
|
d[1] = r[3] * dx + r[4] * dy + r[5] * dz;
|
||||||
d[2] = r[6] * dx + r[7] * dy + r[8] * dz;
|
d[2] = r[6] * dx + r[7] * dy + r[8] * dz;
|
||||||
double[] from = cube.from, to = cube.to;
|
double lo = Math.max(EPS, minT);
|
||||||
if (cube.inverted) return intersectInverted(cube, o, d, Math.max(EPS, minT), maxT, out);
|
if (cube.inverted) return intersectInverted(cube, o, d, lo, maxT, out);
|
||||||
|
|
||||||
|
double[] from = cube.from, to = cube.to;
|
||||||
double tmin = Double.NEGATIVE_INFINITY, tmax = Double.POSITIVE_INFINITY;
|
double tmin = Double.NEGATIVE_INFINITY, tmax = Double.POSITIVE_INFINITY;
|
||||||
int inAxis = -1, outAxis = -1;
|
int inAxis = -1, outAxis = -1;
|
||||||
boolean inNeg = false, outNeg = false;
|
boolean inNeg = false, outNeg = false;
|
||||||
@@ -67,9 +64,7 @@ public final class EntityIntersector {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
double inv = 1.0 / d[a];
|
double inv = 1.0 / d[a];
|
||||||
// Moving towards +a the ray enters through the min ("negative") face and leaves through the
|
// Entry side from the direction, not from t1 > t2: both are equal for zero-thickness boxes (wings, fins).
|
||||||
// max face. Decided by the direction, not by comparing t1/t2: for a zero-thickness box
|
|
||||||
// (wing membranes, fins) both are equal and the comparison can't tell the sides apart.
|
|
||||||
boolean forward = inv > 0;
|
boolean forward = inv > 0;
|
||||||
double tNear = ((forward ? from[a] : to[a]) - o[a]) * inv;
|
double tNear = ((forward ? from[a] : to[a]) - o[a]) * inv;
|
||||||
double tFar = ((forward ? to[a] : from[a]) - o[a]) * inv;
|
double tFar = ((forward ? to[a] : from[a]) - o[a]) * inv;
|
||||||
@@ -85,105 +80,65 @@ public final class EntityIntersector {
|
|||||||
}
|
}
|
||||||
if (tmin > tmax + EPS) return false;
|
if (tmin > tmax + EPS) return false;
|
||||||
}
|
}
|
||||||
double lo = Math.max(EPS, minT);
|
|
||||||
if (inAxis < 0 || tmax <= lo || tmin > maxT) return false;
|
if (inAxis < 0 || tmax <= lo || tmin > maxT) return false;
|
||||||
|
|
||||||
// Vanilla draws entity models without back-face culling: a ray looking through a transparent
|
// Vanilla draws entity models without back-face culling: through a transparent texel of the entry face the
|
||||||
// texel of the entry face still sees the inside of the far face (and both sides of a plane).
|
// ray sees the inside of the far face (and both sides of a plane).
|
||||||
if (tmin > lo && sample(cube, o, d, tmin, inAxis, inNeg, out)) return true;
|
if (tmin > lo && sample(cube, o, d, tmin, Direction.of(inAxis, inNeg), out)) return true;
|
||||||
return outAxis >= 0 && sample(cube, o, d, tmax, outAxis, outNeg, out);
|
return outAxis >= 0 && sample(cube, o, d, tmax, Direction.of(outAxis, outNeg), out);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean sample(EntityCube cube, double[] o, double[] d, double t, int axis, boolean neg, Hit out) {
|
private static boolean sample(EntityCube cube, double[] o, double[] d, double t, Direction dir, Hit out) {
|
||||||
Direction dir = direction(axis, neg);
|
|
||||||
Face face = cube.faces[dir.ordinal()];
|
Face face = cube.faces[dir.ordinal()];
|
||||||
if (face == null) return false;
|
if (face == null) return false;
|
||||||
double px = o[0] + d[0] * t, py = o[1] + d[1] * t, pz = o[2] + d[2] * t;
|
double fx = ElementIntersector.frac(o[0] + d[0] * t, cube.from[0], cube.to[0]);
|
||||||
double fx = frac(px, cube.from[0], cube.to[0]);
|
double fy = ElementIntersector.frac(o[1] + d[1] * t, cube.from[1], cube.to[1]);
|
||||||
double fy = frac(py, cube.from[1], cube.to[1]);
|
double fz = ElementIntersector.frac(o[2] + d[2] * t, cube.from[2], cube.to[2]);
|
||||||
double fz = frac(pz, cube.from[2], cube.to[2]);
|
|
||||||
// Vanilla face-UV orientation, shared by block-model faces and entity box-UV (see BoxUv).
|
|
||||||
int color = face.sample(ElementIntersector.faceS(dir, fx, fy, fz), ElementIntersector.faceT(dir, fy, fz));
|
int color = face.sample(ElementIntersector.faceS(dir, fx, fy, fz), ElementIntersector.faceT(dir, fy, fz));
|
||||||
if (!ColorUtil.opaque(color)) return false;
|
if (!ColorUtil.opaque(color)) return false;
|
||||||
out.t = t;
|
out.t = t;
|
||||||
out.color = color;
|
out.color = color;
|
||||||
out.axis = axis;
|
out.dir = dir;
|
||||||
out.neg = neg;
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A block-model element with {@code from > to} on some axis (spawner/vault/trial-spawner inner faces) baked as
|
* A block-model element with {@code from > to} on some axis (spawner/vault inner faces) baked as an item or
|
||||||
* an item or display: like {@code ElementIntersector.invertedBox}, vanilla puts the WEST/DOWN/NORTH face at
|
* display: back-face-culled inward quads like {@link ElementIntersector} (a mirroring {@code toWorld} flips their
|
||||||
* {@code from} and EAST/UP/SOUTH at {@code to} of its axis, and each inverted OTHER axis mirrors the quad and
|
* winding too). The nearest opaque face in {@code (lo, maxT]} wins.
|
||||||
* flips its winding — the faces point into the box and are back-face culled (seen through the cage: the far
|
|
||||||
* wall's inside). Nearest visible opaque face in {@code (lo, maxT]} wins; UVs follow the mirrored corners.
|
|
||||||
*/
|
*/
|
||||||
private static boolean intersectInverted(EntityCube cube, double[] o, double[] d, double lo, double maxT, Hit out) {
|
private static boolean intersectInverted(EntityCube cube, double[] o, double[] d, double lo, double maxT, Hit out) {
|
||||||
double[] from = cube.from, to = cube.to;
|
double[] from = cube.from, to = cube.to;
|
||||||
double best = Double.POSITIVE_INFINITY;
|
double best = Double.POSITIVE_INFINITY;
|
||||||
for (Direction dir : DIRECTIONS) {
|
for (Direction dir : DIRECTIONS) {
|
||||||
int a = dir.nx != 0 ? 0 : dir.ny != 0 ? 1 : 2;
|
if (Math.abs(d[dir.axis]) < EPS) continue;
|
||||||
if (Math.abs(d[a]) < EPS) continue;
|
double t = ElementIntersector.invertedFaceT(dir, from, to, o, d, cube.mirrored);
|
||||||
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) * (cube.mirrored ? -1 : 1);
|
|
||||||
if (sign * d[a] >= 0) continue; // back face
|
|
||||||
double t = ((neg ? from[a] : to[a]) - o[a]) / d[a];
|
|
||||||
if (!(t > lo) || t > maxT || t >= best) continue;
|
if (!(t > lo) || t > maxT || t >= best) continue;
|
||||||
double pb = o[b] + d[b] * t, pc = o[c] + d[c] * t;
|
if (sample(cube, o, d, t, dir, out)) best = 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;
|
|
||||||
Face face = cube.faces[dir.ordinal()];
|
|
||||||
if (face == null) continue;
|
|
||||||
double fx = signedFrac(o[0] + d[0] * t, from[0], to[0]);
|
|
||||||
double fy = signedFrac(o[1] + d[1] * t, from[1], to[1]);
|
|
||||||
double fz = signedFrac(o[2] + d[2] * t, from[2], to[2]);
|
|
||||||
int color = face.sample(ElementIntersector.faceS(dir, fx, fy, fz), ElementIntersector.faceT(dir, fy, fz));
|
|
||||||
if (!ColorUtil.opaque(color)) continue;
|
|
||||||
best = t;
|
|
||||||
out.t = t;
|
|
||||||
out.color = color;
|
|
||||||
out.axis = a;
|
|
||||||
out.neg = neg;
|
|
||||||
}
|
}
|
||||||
return best < Double.POSITIVE_INFINITY;
|
return best < Double.POSITIVE_INFINITY;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 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-9) return 0;
|
|
||||||
double f = (v - from) / span;
|
|
||||||
return f < 0 ? 0 : Math.min(f, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The result object for a hit of {@code cube} recorded by {@link #intersect} (world point + normal). */
|
/** The result object for a hit of {@code cube} recorded by {@link #intersect} (world point + normal). */
|
||||||
public static FaceHit toFaceHit(
|
public static FaceHit toFaceHit(
|
||||||
EntityCube cube, Hit h, double ox, double oy, double oz, double dx, double dy, double dz) {
|
EntityCube cube, Hit h, double ox, double oy, double oz, double dx, double dy, double dz) {
|
||||||
double t = h.t;
|
double t = h.t;
|
||||||
Direction dir = direction(h.axis, h.neg);
|
Direction dir = h.dir;
|
||||||
Vector world = new Vector(ox + dx * t, oy + dy * t, oz + dz * t);
|
double[] r = cube.toWorld.r;
|
||||||
double[] n = cube.toWorld.applyLinear(dir.nx, dir.ny, dir.nz);
|
Vector normal = new Vector(
|
||||||
Vector normal = new Vector(n[0], n[1], n[2]).normalize();
|
r[0] * dir.nx + r[1] * dir.ny + r[2] * dir.nz,
|
||||||
|
r[3] * dir.nx + r[4] * dir.ny + r[5] * dir.nz,
|
||||||
|
r[6] * dir.nx + r[7] * dir.ny + r[8] * dir.nz)
|
||||||
|
.normalize();
|
||||||
// A far/back face is seen from inside: shade it as facing the viewer.
|
// A far/back face is seen from inside: shade it as facing the viewer.
|
||||||
if (normal.getX() * dx + normal.getY() * dy + normal.getZ() * dz > 0) normal.multiply(-1);
|
if (normal.getX() * dx + normal.getY() * dy + normal.getZ() * dz > 0) normal.multiply(-1);
|
||||||
return new FaceHit(t, world, normal, h.color, -1, cube.additive, cube.unshaded);
|
return new FaceHit(
|
||||||
}
|
t,
|
||||||
|
new Vector(ox + dx * t, oy + dy * t, oz + dz * t),
|
||||||
private static Direction direction(int axis, boolean neg) {
|
normal,
|
||||||
return switch (axis) {
|
h.color,
|
||||||
case 0 -> neg ? Direction.WEST : Direction.EAST;
|
-1,
|
||||||
case 1 -> neg ? Direction.DOWN : Direction.UP;
|
cube.additive,
|
||||||
default -> neg ? Direction.NORTH : Direction.SOUTH;
|
cube.unshaded);
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,16 +6,14 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maps an entity type to its CEM ({@code .jem}) Java model name and candidate texture paths. Most types
|
* Entity type -> CEM template model name (legacy fallback) and candidate texture paths. Most types use the type key
|
||||||
* use the type key directly for both; small override maps handle the exceptions. The models are vanilla
|
* directly; small override maps handle the exceptions.
|
||||||
* Java models (already posed); variant-specific textures (cow/sheep colour, etc.) are handled here.
|
|
||||||
*/
|
*/
|
||||||
public final class EntityModels {
|
public final class EntityModels {
|
||||||
|
|
||||||
private EntityModels() {}
|
private EntityModels() {}
|
||||||
|
|
||||||
// Type key -> CEM (.jem) model name. Default is the type key itself; these are the exceptions
|
// Exceptions to "model name = type key": reused models and renamed/version-suffixed CEM names.
|
||||||
// (mob reuses another mob's model, or the CEM set only ships a version-suffixed/renamed name).
|
|
||||||
private static final Map<String, String> CEM_OVERRIDE = Map.ofEntries(
|
private static final Map<String, String> CEM_OVERRIDE = Map.ofEntries(
|
||||||
Map.entry("husk", "zombie"),
|
Map.entry("husk", "zombie"),
|
||||||
Map.entry("giant", "zombie"),
|
Map.entry("giant", "zombie"),
|
||||||
@@ -46,7 +44,7 @@ public final class EntityModels {
|
|||||||
|
|
||||||
/** The CEM model name for an entity type (boats/rafts share the boat hull). */
|
/** The CEM model name for an entity type (boats/rafts share the boat hull). */
|
||||||
public static String cemModel(String typeKey) {
|
public static String cemModel(String typeKey) {
|
||||||
if (typeKey.endsWith("_boat") || typeKey.endsWith("_raft")) return "boat";
|
if (isBoat(typeKey)) return "boat";
|
||||||
return CEM_OVERRIDE.getOrDefault(typeKey, typeKey);
|
return CEM_OVERRIDE.getOrDefault(typeKey, typeKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,7 +52,7 @@ public final class EntityModels {
|
|||||||
// or not the right default: variant-only mobs whose renderer default is a variant/"missingno", the dump's
|
// or not the right default: variant-only mobs whose renderer default is a variant/"missingno", the dump's
|
||||||
// piglin_brute (= piglin), players/mannequins, and non-mob entities whose renderer texture isn't captured.
|
// piglin_brute (= piglin), players/mannequins, and non-mob entities whose renderer texture isn't captured.
|
||||||
// Everything else comes from the dump (see CemBaker#resolveTexture).
|
// Everything else comes from the dump (see CemBaker#resolveTexture).
|
||||||
private static final Map<String, String> TEX_OVERRIDE = Map.<String, String>ofEntries(
|
private static final Map<String, String> TEX_OVERRIDE = Map.ofEntries(
|
||||||
Map.entry("cow", "entity/cow/cow_temperate"),
|
Map.entry("cow", "entity/cow/cow_temperate"),
|
||||||
Map.entry("player", "entity/player/wide/steve"),
|
Map.entry("player", "entity/player/wide/steve"),
|
||||||
Map.entry("ender_dragon", "entity/enderdragon/dragon"),
|
Map.entry("ender_dragon", "entity/enderdragon/dragon"),
|
||||||
@@ -80,7 +78,7 @@ public final class EntityModels {
|
|||||||
/** Ordered texture-path candidates; the baker uses the first that loads. */
|
/** Ordered texture-path candidates; the baker uses the first that loads. */
|
||||||
public static List<ResourceLocation> textureCandidates(String typeKey, String variant) {
|
public static List<ResourceLocation> textureCandidates(String typeKey, String variant) {
|
||||||
List<ResourceLocation> list = new ArrayList<>();
|
List<ResourceLocation> list = new ArrayList<>();
|
||||||
if (typeKey.endsWith("_boat") || typeKey.endsWith("_raft")) {
|
if (isBoat(typeKey)) {
|
||||||
String wood = typeKey.substring(0, typeKey.length() - 5);
|
String wood = typeKey.substring(0, typeKey.length() - 5);
|
||||||
boolean chest = wood.endsWith("_chest");
|
boolean chest = wood.endsWith("_chest");
|
||||||
if (chest) wood = wood.substring(0, wood.length() - "_chest".length());
|
if (chest) wood = wood.substring(0, wood.length() - "_chest".length());
|
||||||
@@ -100,17 +98,15 @@ public final class EntityModels {
|
|||||||
|
|
||||||
/** How many leading {@link #textureCandidates} entries are type/variant-specific (not generic guesses). */
|
/** How many leading {@link #textureCandidates} entries are type/variant-specific (not generic guesses). */
|
||||||
public static int specificCandidateCount(String typeKey, String variant) {
|
public static int specificCandidateCount(String typeKey, String variant) {
|
||||||
if (typeKey.endsWith("_boat") || typeKey.endsWith("_raft")) return 1;
|
if (isBoat(typeKey)) return 1;
|
||||||
return (variant != null ? variantPaths(typeKey, variant).size() : 0)
|
return (variant != null ? variantPaths(typeKey, variant).size() : 0)
|
||||||
+ (TEX_OVERRIDE.containsKey(typeKey) ? 1 : 0);
|
+ (TEX_OVERRIDE.containsKey(typeKey) ? 1 : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Variant-specific texture paths of the ENUM variants vanilla hardcodes in its renderers (modern pack
|
* Texture paths of the ENUM variants vanilla hardcodes in its renderers ("entity/<folder>/<entity>_<variant>",
|
||||||
* naming is "entity/<folder>/<entity>_<variant>", with a handful of mismatches the small maps below
|
* mismatches normalised by the maps below). Registry variants (cat, wolf, frog, cow, …) come from their
|
||||||
* normalise). Data-driven variants (cat, wolf, frog, cow, pig, chicken, zombie nautilus) are resolved from
|
* {@code asset_id} in the dump instead. Tried before the generic fallbacks.
|
||||||
* their {@code asset_id} in the vanilla dump instead. Returned paths are tried before the generic
|
|
||||||
* fallbacks, so an unknown variant still degrades to the base texture.
|
|
||||||
*/
|
*/
|
||||||
private static List<String> variantPaths(String typeKey, String v) {
|
private static List<String> variantPaths(String typeKey, String v) {
|
||||||
return switch (typeKey) {
|
return switch (typeKey) {
|
||||||
@@ -135,6 +131,10 @@ public final class EntityModels {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean isBoat(String typeKey) {
|
||||||
|
return typeKey.endsWith("_boat") || typeKey.endsWith("_raft");
|
||||||
|
}
|
||||||
|
|
||||||
/** Vanilla tropical-fish patterns in id order: 0-5 use body shape A (small), 6-11 shape B (large). */
|
/** Vanilla tropical-fish patterns in id order: 0-5 use body shape A (small), 6-11 shape B (large). */
|
||||||
private static final List<String> TROPICAL_PATTERNS = List.of(
|
private static final List<String> TROPICAL_PATTERNS = List.of(
|
||||||
"kob",
|
"kob",
|
||||||
|
|||||||
@@ -4,16 +4,15 @@ import eu.mhsl.minecraft.pixelpics.render.entity.cem.BlockEntityBaker;
|
|||||||
import eu.mhsl.minecraft.pixelpics.render.entity.cem.CemBaker;
|
import eu.mhsl.minecraft.pixelpics.render.entity.cem.CemBaker;
|
||||||
import eu.mhsl.minecraft.pixelpics.render.raytrace.FaceHit;
|
import eu.mhsl.minecraft.pixelpics.render.raytrace.FaceHit;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* All baked entities (mobs + block-entities + decorations + objects) for one render. Provides the nearest hit
|
* All baked entities (mobs, block-entities, decorations, objects) of one render, as one bounding volume hierarchy
|
||||||
* along a ray. Every entity cube goes into one bounding volume hierarchy over the cubes' world AABBs (binned
|
* over the cubes' world AABBs (binned SAH, flat arrays), traversed front to back with the best hit as far bound.
|
||||||
* SAH, flat arrays), traversed front to back with the current best hit as the far bound, so a ray only tests
|
* Near-ties (coplanar faces of different cubes) are decided in bake order (see {@link #nearestHit}). Immutable after
|
||||||
* the cubes near it instead of every entity — farms with hundreds of mobs/items cost about as much as a few.
|
* construction; the per-ray scratch is per thread.
|
||||||
* Near-ties (coplanar faces of different cubes) are decided by replaying the former linear scan over the few
|
|
||||||
* candidates, so images stay pixel-identical. Immutable after construction; the per-ray scratch is per thread
|
|
||||||
* → safe for the parallel tracer.
|
|
||||||
*/
|
*/
|
||||||
public final class EntityScene {
|
public final class EntityScene {
|
||||||
|
|
||||||
@@ -23,8 +22,6 @@ public final class EntityScene {
|
|||||||
private static final int MAX_SAH_DEPTH = 40;
|
private static final int MAX_SAH_DEPTH = 40;
|
||||||
private static volatile boolean warned;
|
private static volatile boolean warned;
|
||||||
|
|
||||||
private final List<RenderedEntity> entities;
|
|
||||||
|
|
||||||
// Cubes in BVH leaf order, their bake-order rank (tie-break) and world AABBs (minX..maxZ, 6 per cube).
|
// Cubes in BVH leaf order, their bake-order rank (tie-break) and world AABBs (minX..maxZ, 6 per cube).
|
||||||
private final EntityCube[] cubes;
|
private final EntityCube[] cubes;
|
||||||
private final int[] cubeRank;
|
private final int[] cubeRank;
|
||||||
@@ -52,11 +49,11 @@ public final class EntityScene {
|
|||||||
EntityIntersector.Hit candidate(int k) {
|
EntityIntersector.Hit candidate(int k) {
|
||||||
if (k == cand.length) {
|
if (k == cand.length) {
|
||||||
int len = Math.max(8, k * 2);
|
int len = Math.max(8, k * 2);
|
||||||
EntityIntersector.Hit[] c = java.util.Arrays.copyOf(cand, len);
|
EntityIntersector.Hit[] c = Arrays.copyOf(cand, len);
|
||||||
for (int i = k; i < len; i++) c[i] = new EntityIntersector.Hit();
|
for (int i = k; i < len; i++) c[i] = new EntityIntersector.Hit();
|
||||||
cand = c;
|
cand = c;
|
||||||
candCube = java.util.Arrays.copyOf(candCube, len);
|
candCube = Arrays.copyOf(candCube, len);
|
||||||
order = java.util.Arrays.copyOf(order, len);
|
order = Arrays.copyOf(order, len);
|
||||||
}
|
}
|
||||||
return cand[k];
|
return cand[k];
|
||||||
}
|
}
|
||||||
@@ -73,17 +70,13 @@ public final class EntityScene {
|
|||||||
DecorationBaker decoBaker,
|
DecorationBaker decoBaker,
|
||||||
List<ObjectState> objects,
|
List<ObjectState> objects,
|
||||||
ObjectBaker objectBaker) {
|
ObjectBaker objectBaker) {
|
||||||
this.entities = new ArrayList<>(states.size() + blockEntities.size() + decorations.size() + objects.size());
|
List<EntityCube> baked = new ArrayList<>();
|
||||||
addAll(states, baker);
|
addAll(states, baker, baked);
|
||||||
addAll(blockEntities, beBaker);
|
addAll(blockEntities, beBaker, baked);
|
||||||
addAll(decorations, decoBaker);
|
addAll(decorations, decoBaker, baked);
|
||||||
addAll(objects, objectBaker);
|
addAll(objects, objectBaker, baked);
|
||||||
|
EntityCube[] all = baked.toArray(new EntityCube[0]);
|
||||||
int n = 0;
|
int n = all.length;
|
||||||
for (RenderedEntity e : entities) n += e.cubes.size();
|
|
||||||
EntityCube[] all = new EntityCube[n];
|
|
||||||
int k = 0;
|
|
||||||
for (RenderedEntity e : entities) for (EntityCube c : e.cubes) all[k++] = c;
|
|
||||||
|
|
||||||
cubes = new EntityCube[n];
|
cubes = new EntityCube[n];
|
||||||
cubeRank = new int[n];
|
cubeRank = new int[n];
|
||||||
@@ -91,28 +84,27 @@ public final class EntityScene {
|
|||||||
maxDepth = build(all);
|
maxDepth = build(all);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Bakes each state and keeps the ones with geometry. A null baker (path disabled) is skipped. */
|
/** Bakes each state into {@code out} (in bake order = tie-break rank). A null baker (path disabled) is skipped. */
|
||||||
private <S> void addAll(List<S> states, EntityBaker<S> baker) {
|
private static <S> void addAll(List<S> states, EntityBaker<S> baker, List<EntityCube> out) {
|
||||||
if (baker == null) return;
|
if (baker == null) return;
|
||||||
for (S s : states) {
|
for (S s : states) {
|
||||||
RenderedEntity e;
|
RenderedEntity e;
|
||||||
try {
|
try {
|
||||||
e = baker.bake(s);
|
e = baker.bake(s);
|
||||||
} catch (RuntimeException ex) {
|
} catch (RuntimeException ex) {
|
||||||
// One unexpected entity/asset must never fail the whole photo: skip just this one.
|
// One bad entity/asset must never fail the whole photo.
|
||||||
if (!warned) {
|
if (!warned) {
|
||||||
warned = true;
|
warned = true;
|
||||||
java.util.logging.Logger.getLogger("PixelPics")
|
Logger.getLogger("PixelPics").warning("PixelPics: skipped an entity that failed to bake: " + ex);
|
||||||
.warning("PixelPics: skipped an entity that failed to bake: " + ex);
|
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (e != null && !e.cubes.isEmpty()) entities.add(e);
|
if (e != null) out.addAll(e.cubes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isEmpty() {
|
public boolean isEmpty() {
|
||||||
return entities.isEmpty();
|
return cubes.length == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Nearest entity hit with {@code minT < t < maxT}, or null. */
|
/** Nearest entity hit with {@code minT < t < maxT}, or null. */
|
||||||
@@ -164,34 +156,30 @@ public final class EntityScene {
|
|||||||
int l = node + 1, r = nodeRight[node];
|
int l = node + 1, r = nodeRight[node];
|
||||||
double tl = entry(nb, l, ox, oy, oz, ix, iy, iz, px, py, pz, t0, bound);
|
double tl = entry(nb, l, ox, oy, oz, ix, iy, iz, px, py, pz, t0, bound);
|
||||||
double trr = entry(nb, r, ox, oy, oz, ix, iy, iz, px, py, pz, t0, bound);
|
double trr = entry(nb, r, ox, oy, oz, ix, iy, iz, px, py, pz, t0, bound);
|
||||||
boolean hl = tl == tl, hr = trr == trr;
|
// Near child on top of the stack: visited first, so the far one is usually culled. NaN = missed.
|
||||||
if (hl && hr) {
|
int near = l, far = r;
|
||||||
// Near child on top of the stack: visited first, so the far one is usually culled.
|
double tn = tl, tf = trr;
|
||||||
if (tl <= trr) {
|
if (!(tl <= trr)) {
|
||||||
stack[sp] = r;
|
near = r;
|
||||||
stackT[sp++] = trr;
|
far = l;
|
||||||
stack[sp] = l;
|
tn = trr;
|
||||||
stackT[sp++] = tl;
|
tf = tl;
|
||||||
} else {
|
}
|
||||||
stack[sp] = l;
|
if (tf == tf) {
|
||||||
stackT[sp++] = tl;
|
stack[sp] = far;
|
||||||
stack[sp] = r;
|
stackT[sp++] = tf;
|
||||||
stackT[sp++] = trr;
|
}
|
||||||
}
|
if (tn == tn) {
|
||||||
} else if (hl) {
|
stack[sp] = near;
|
||||||
stack[sp] = l;
|
stackT[sp++] = tn;
|
||||||
stackT[sp++] = tl;
|
|
||||||
} else if (hr) {
|
|
||||||
stack[sp] = r;
|
|
||||||
stackT[sp++] = trr;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (n == 0) return null;
|
if (n == 0) return null;
|
||||||
|
|
||||||
// Pass 2: replay the former linear scan over the near candidates in bake order — a cube counts only if
|
// Pass 2, the tie-break: the near candidates in bake order; a cube counts only if its AABB passes with the
|
||||||
// its AABB passes with the best t so far as far bound and it is strictly nearer. Coplanar faces of
|
// best t so far as far bound and it is strictly nearer. Coplanar faces of different cubes (equal t up to
|
||||||
// different cubes (equal t up to rounding) thus resolve exactly as before: pixel-identical images.
|
// rounding) depend on this order, so it must not change.
|
||||||
int[] candCube = tr.candCube;
|
int[] candCube = tr.candCube;
|
||||||
int[] order = tr.order;
|
int[] order = tr.order;
|
||||||
int m = 0;
|
int m = 0;
|
||||||
@@ -221,8 +209,7 @@ public final class EntityScene {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Slab test of box {@code i} of {@code boxes} (6 doubles per box) against the ray within {@code [t0, tMax]};
|
* Slab test of box {@code i} of {@code boxes} (6 doubles per box) against the ray within {@code [t0, tMax]};
|
||||||
* returns the entry t, or NaN on a miss. Same comparisons as the former per-entity/per-cube AABB check
|
* returns the entry t, or NaN on a miss (a box touching the interval at {@code tMax} still counts).
|
||||||
* (a box touching the interval at {@code tMax} still counts).
|
|
||||||
*/
|
*/
|
||||||
private static double entry(
|
private static double entry(
|
||||||
double[] boxes,
|
double[] boxes,
|
||||||
@@ -352,7 +339,7 @@ public final class EntityScene {
|
|||||||
}
|
}
|
||||||
if (mid < 0) {
|
if (mid < 0) {
|
||||||
// Leaf: cubes in bake order inside the leaf (ascending original index).
|
// Leaf: cubes in bake order inside the leaf (ascending original index).
|
||||||
java.util.Arrays.sort(perm, lo, hi);
|
Arrays.sort(perm, lo, hi);
|
||||||
nodeStart[node] = filled;
|
nodeStart[node] = filled;
|
||||||
nodeCount[node] = count;
|
nodeCount[node] = count;
|
||||||
for (int i = lo; i < hi; i++) {
|
for (int i = lo; i < hi; i++) {
|
||||||
@@ -410,7 +397,7 @@ public final class EntityScene {
|
|||||||
for (int b = 0; b < SAH_BINS - 1; b++) {
|
for (int b = 0; b < SAH_BINS - 1; b++) {
|
||||||
grow(acc, binBox, b);
|
grow(acc, binBox, b);
|
||||||
cnt += binCount[b];
|
cnt += binCount[b];
|
||||||
leftArea[b] = area(acc);
|
leftArea[b] = area(acc, 0);
|
||||||
leftCount[b] = cnt;
|
leftCount[b] = cnt;
|
||||||
}
|
}
|
||||||
acc[0] = acc[1] = acc[2] = Double.MAX_VALUE;
|
acc[0] = acc[1] = acc[2] = Double.MAX_VALUE;
|
||||||
@@ -423,7 +410,7 @@ public final class EntityScene {
|
|||||||
cnt += binCount[b];
|
cnt += binCount[b];
|
||||||
int left = leftCount[b - 1];
|
int left = leftCount[b - 1];
|
||||||
if (left == 0 || cnt == 0) continue;
|
if (left == 0 || cnt == 0) continue;
|
||||||
double cost = leftArea[b - 1] * left + area(acc) * cnt;
|
double cost = leftArea[b - 1] * left + area(acc, 0) * cnt;
|
||||||
if (cost < bestCost) {
|
if (cost < bestCost) {
|
||||||
bestCost = cost;
|
bestCost = cost;
|
||||||
bestSplit = b;
|
bestSplit = b;
|
||||||
@@ -459,7 +446,7 @@ public final class EntityScene {
|
|||||||
private void sortByCentroid(int lo, int hi, int axis) {
|
private void sortByCentroid(int lo, int hi, int axis) {
|
||||||
Integer[] tmp = new Integer[hi - lo];
|
Integer[] tmp = new Integer[hi - lo];
|
||||||
for (int i = lo; i < hi; i++) tmp[i - lo] = perm[i];
|
for (int i = lo; i < hi; i++) tmp[i - lo] = perm[i];
|
||||||
java.util.Arrays.sort(tmp, (a, b) -> Double.compare(centroid[a * 3 + axis], centroid[b * 3 + axis]));
|
Arrays.sort(tmp, (a, b) -> Double.compare(centroid[a * 3 + axis], centroid[b * 3 + axis]));
|
||||||
for (int i = lo; i < hi; i++) perm[i] = tmp[i - lo];
|
for (int i = lo; i < hi; i++) perm[i] = tmp[i - lo];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -470,10 +457,6 @@ public final class EntityScene {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static double area(double[] box) {
|
|
||||||
return area(box, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static double area(double[] box, int o) {
|
private static double area(double[] box, int o) {
|
||||||
double x = box[o + 3] - box[o], y = box[o + 4] - box[o + 1], z = box[o + 5] - box[o + 2];
|
double x = box[o + 3] - box[o], y = box[o + 4] - box[o + 1], z = box[o + 5] - box[o + 2];
|
||||||
if (!(x >= 0) || !(y >= 0) || !(z >= 0)) return 0;
|
if (!(x >= 0) || !(y >= 0) || !(z >= 0)) return 0;
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
package eu.mhsl.minecraft.pixelpics.render.entity;
|
package eu.mhsl.minecraft.pixelpics.render.entity;
|
||||||
|
|
||||||
/**
|
import java.util.Map;
|
||||||
* Immutable snapshot of one entity captured on the main thread, sufficient to bake and pose it
|
|
||||||
* off-thread. Angles are in degrees (Minecraft convention).
|
/** Snapshot of one entity captured on the main thread, enough to bake it off-thread. Angles in degrees. */
|
||||||
*/
|
|
||||||
public record EntityState(
|
public record EntityState(
|
||||||
String typeKey, // e.g. "cow", "zombie", "player"
|
String typeKey, // e.g. "cow", "zombie", "player"
|
||||||
double x,
|
double x,
|
||||||
@@ -16,8 +15,7 @@ public record EntityState(
|
|||||||
boolean player,
|
boolean player,
|
||||||
String skinUrl,
|
String skinUrl,
|
||||||
boolean slim,
|
boolean slim,
|
||||||
String variant, // variant key (e.g. "ashen", "warm", "tabby"; "<ns>:<key>" outside minecraft); for villagers
|
String variant, // variant key ("ashen", "tabby"; "<ns>:<key>" outside minecraft; villagers: biome type)
|
||||||
// the biome type, or null
|
|
||||||
int tint, // ARGB multiplier for tintable layers (sheep wool); 0 = none
|
int tint, // ARGB multiplier for tintable layers (sheep wool); 0 = none
|
||||||
double sizeScale, // extra model scale (slime/magma-cube size); 1.0 = default
|
double sizeScale, // extra model scale (slime/magma-cube size); 1.0 = default
|
||||||
String profession, // villager profession key (e.g. "farmer", "librarian", "none"), or null
|
String profession, // villager profession key (e.g. "farmer", "librarian", "none"), or null
|
||||||
@@ -26,11 +24,10 @@ public record EntityState(
|
|||||||
boolean saddle, // horse/donkey/mule is saddled
|
boolean saddle, // horse/donkey/mule is saddled
|
||||||
boolean chest, // donkey/mule/llama is carrying a chest (pose flag "chest")
|
boolean chest, // donkey/mule/llama is carrying a chest (pose flag "chest")
|
||||||
boolean arms, // armor stand shows its arms (pose flag "arms")
|
boolean arms, // armor stand shows its arms (pose flag "arms")
|
||||||
String bodyEquip, // BODY-slot equipment asset (horse armor material, llama carpet, harness, wolf armor,
|
String bodyEquip, // BODY-slot equipment asset (horse armor, llama carpet, harness, …) or "trader_llama"
|
||||||
// nautilus armor) or "trader_llama"; null = none
|
|
||||||
Equipment equipment, // worn humanoid armor (players, armor stands, zombies, …); null = none/not a wearer
|
Equipment equipment, // worn humanoid armor (players, armor stands, zombies, …); null = none/not a wearer
|
||||||
boolean invisible, // entity is invisible -> render only its equipment (like vanilla), not the body
|
boolean invisible, // entity is invisible -> render only its equipment (like vanilla), not the body
|
||||||
float pitch, // entity pitch (deg) — orients projectiles (arrows, tridents, skulls)
|
float pitch, // orients projectiles and pitched bodies
|
||||||
int bodyDye, // ARGB dye of dyeable BODY equipment (leather horse armor, wolf armor); 0 = none
|
int bodyDye, // ARGB dye of dyeable BODY equipment (leather horse armor, wolf armor); 0 = none
|
||||||
int tint2, // second tint (tropical-fish pattern colour); 0 = none
|
int tint2, // second tint (tropical-fish pattern colour); 0 = none
|
||||||
boolean passenger, // riding something (vanilla sitting pose)
|
boolean passenger, // riding something (vanilla sitting pose)
|
||||||
@@ -40,10 +37,9 @@ public record EntityState(
|
|||||||
int collar, // ARGB dye of a tamed cat's/wolf's collar; 0 = no collar (untamed)
|
int collar, // ARGB dye of a tamed cat's/wolf's collar; 0 = no collar (untamed)
|
||||||
boolean powered, // energy-swirl layer: charged creeper, wither below half health
|
boolean powered, // energy-swirl layer: charged creeper, wither below half health
|
||||||
ItemSpec mouthItem, // item carried in the mouth (fox/dolphin main hand); null = none
|
ItemSpec mouthItem, // item carried in the mouth (fox/dolphin main hand); null = none
|
||||||
String pose, // state pose flags, sorted and "+"-joined ("sitting", "lying+head_up", "sleeping", "in_water",
|
String pose, // "+"-joined state pose/render flags (the dump's STATE_POSES names, e.g. "lying+head_up"); null =
|
||||||
// …; the dump's STATE_POSES names, unknown ones are ignored per type); null = none (idle)
|
// idle
|
||||||
java.util.Map<String, float[]> partPoses, // continuous part poses by vanilla render-state field (armor stand
|
Map<String, float[]> partPoses, // armor-stand part rotations by render-state field ("headPose", …), x/y/z deg
|
||||||
// "headPose"/"bodyPose"/"leftArmPose"/…: x/y/z degrees); null = defaults
|
|
||||||
String shoulderLeft, // parrot variant on a player's left shoulder (e.g. "red"); null = none
|
String shoulderLeft, // parrot variant on a player's left shoulder (e.g. "red"); null = none
|
||||||
String shoulderRight // parrot variant on the right shoulder; null = none
|
String shoulderRight // parrot variant on the right shoulder; null = none
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -3,9 +3,7 @@ package eu.mhsl.minecraft.pixelpics.render.entity;
|
|||||||
import org.bukkit.block.data.BlockData;
|
import org.bukkit.block.data.BlockData;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An item to render (held, worn on the head, in an item display, dropped): its item key, the block data
|
* An item to render (held, worn, displayed, dropped): item key, the block it places (null if none), that block's
|
||||||
* when the item places a block (block-model geometry), the block-entity state when that block is
|
* block-entity state when it is block-entity rendered, and whether it is enchanted.
|
||||||
* block-entity rendered (chests, heads, banners, …), and whether it is enchanted. Captured on the main
|
|
||||||
* thread; immutable afterwards.
|
|
||||||
*/
|
*/
|
||||||
public record ItemSpec(String id, BlockData block, BlockEntityState blockEntity, boolean glint) {}
|
public record ItemSpec(String id, BlockData block, BlockEntityState blockEntity, boolean glint) {}
|
||||||
@@ -1,16 +1,13 @@
|
|||||||
package eu.mhsl.minecraft.pixelpics.render.entity;
|
package eu.mhsl.minecraft.pixelpics.render.entity;
|
||||||
|
|
||||||
/**
|
/** One box of an entity model in model pixels: min corner, size, inflation and box-UV offset (or per-face UVs). */
|
||||||
* A single box of an entity model, in Bedrock model-pixel coordinates (16 px = 1 block).
|
|
||||||
* {@code origin} is the minimum corner, {@code uv} is the box-UV texture offset.
|
|
||||||
*/
|
|
||||||
public final class ModelCube {
|
public final class ModelCube {
|
||||||
public final double[] origin; // 3, min corner (px)
|
public final double[] origin;
|
||||||
public final double[] size; // 3 (px)
|
public final double[] size;
|
||||||
public final double inflate; // px, expands the box on all sides (overlay layers)
|
public final double inflate; // grows the box on all sides (overlay layers)
|
||||||
public final double[] uv; // 2, box-UV offset (texels)
|
public final double[] uv; // box-UV offset (texels)
|
||||||
public final boolean mirror;
|
public final boolean mirror;
|
||||||
/** Optional modern per-face UV, indexed by {@code Direction.ordinal()}: {u, v, w, h} texels (h/w may be negative for flips). Null = use box-UV. */
|
// Per-face {u, v, w, h} texels by Direction.ordinal() (negative w/h flip); null = box-UV.
|
||||||
public final double[][] faceUv;
|
public final double[][] faceUv;
|
||||||
|
|
||||||
public ModelCube(double[] origin, double[] size, double inflate, double[] uv, boolean mirror) {
|
public ModelCube(double[] origin, double[] size, double inflate, double[] uv, boolean mirror) {
|
||||||
|
|||||||
@@ -7,18 +7,17 @@ import eu.mhsl.minecraft.pixelpics.render.entity.cem.ItemGeometry;
|
|||||||
import eu.mhsl.minecraft.pixelpics.render.entity.cem.SignTextRasterizer;
|
import eu.mhsl.minecraft.pixelpics.render.entity.cem.SignTextRasterizer;
|
||||||
import eu.mhsl.minecraft.pixelpics.render.util.ColorUtil;
|
import eu.mhsl.minecraft.pixelpics.render.util.ColorUtil;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bakes {@link ObjectState}s (display entities, dropped items, falling blocks, primed TNT, minecart
|
* Bakes {@link ObjectState}s (display entities, dropped items, falling blocks, primed TNT, minecart contents,
|
||||||
* contents) into world-space {@link EntityCube}s: block content reuses the resolved BLOCK MODEL elements
|
* leashes): blocks and items through {@link ItemGeometry}, text rasterized with the pack's bitmap font.
|
||||||
* (any block, any version — straight from the resource pack), items use their block model or a flat
|
|
||||||
* sprite, text is rasterized with the pack's bitmap font (as for signs).
|
|
||||||
*/
|
*/
|
||||||
public final class ObjectBaker implements EntityBaker<ObjectState> {
|
public final class ObjectBaker implements EntityBaker<ObjectState> {
|
||||||
|
|
||||||
// Vanilla leash colours (LeashRenderer): brown, alternating with a darker shade per segment.
|
// Vanilla LeashRenderer colours, alternating per segment.
|
||||||
private static final int LEASH_LIGHT = 0xFF7F6650, LEASH_DARK = 0xFF59473A;
|
private static final Face[] LEASH_LIGHT = solidFaces(0xFF7F6650), LEASH_DARK = solidFaces(0xFF59473A);
|
||||||
private static final int LEASH_SEGMENTS = 24;
|
private static final int LEASH_SEGMENTS = 24;
|
||||||
private static final double LEASH_HALF_WIDTH = 0.025;
|
private static final double LEASH_HALF_WIDTH = 0.025;
|
||||||
|
|
||||||
@@ -35,8 +34,7 @@ public final class ObjectBaker implements EntityBaker<ObjectState> {
|
|||||||
List<EntityCube> cubes = new ArrayList<>();
|
List<EntityCube> cubes = new ArrayList<>();
|
||||||
switch (s.kind()) {
|
switch (s.kind()) {
|
||||||
case BLOCK -> {
|
case BLOCK -> {
|
||||||
// The block model plus what its block-entity renderer adds (chest/banner/… bodies, the lectern's
|
// Block model plus the block-entity renderer's parts (chest bodies, lectern book, bell, …).
|
||||||
// book, the bell, the enchanting-table book): block-entity blocks draw no block-model geometry.
|
|
||||||
items.bakeBlock(s.block(), s.toWorld(), cubes);
|
items.bakeBlock(s.block(), s.toWorld(), cubes);
|
||||||
items.bakeBlockEntity(s.blockEntity(), s.toWorld(), cubes);
|
items.bakeBlockEntity(s.blockEntity(), s.toWorld(), cubes);
|
||||||
}
|
}
|
||||||
@@ -70,15 +68,14 @@ public final class ObjectBaker implements EntityBaker<ObjectState> {
|
|||||||
for (int i = 1; i <= LEASH_SEGMENTS; i++) {
|
for (int i = 1; i <= LEASH_SEGMENTS; i++) {
|
||||||
double t = (double) i / LEASH_SEGMENTS;
|
double t = (double) i / LEASH_SEGMENTS;
|
||||||
double[] cur = {p[0] + dx * t, p[1] + dy * (t * t + t) * 0.5, p[2] + dz * t};
|
double[] cur = {p[0] + dx * t, p[1] + dy * (t * t + t) * 0.5, p[2] + dz * t};
|
||||||
int color = (i % 2 == 0) ? LEASH_LIGHT : LEASH_DARK;
|
EntityCube seg = segment(prev, cur, i % 2 == 0 ? LEASH_LIGHT : LEASH_DARK);
|
||||||
EntityCube seg = segment(prev, cur, color);
|
|
||||||
if (seg != null) cubes.add(seg);
|
if (seg != null) cubes.add(seg);
|
||||||
prev = cur;
|
prev = cur;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A thin square rope box from a to b. */
|
/** A thin square rope box from a to b. */
|
||||||
private static EntityCube segment(double[] a, double[] b, int argb) {
|
private static EntityCube segment(double[] a, double[] b, Face[] faces) {
|
||||||
double[] d = {b[0] - a[0], b[1] - a[1], b[2] - a[2]};
|
double[] d = {b[0] - a[0], b[1] - a[1], b[2] - a[2]};
|
||||||
double len = Math.sqrt(d[0] * d[0] + d[1] * d[1] + d[2] * d[2]);
|
double len = Math.sqrt(d[0] * d[0] + d[1] * d[1] + d[2] * d[2]);
|
||||||
if (len < 1e-6) return null;
|
if (len < 1e-6) return null;
|
||||||
@@ -89,11 +86,14 @@ public final class ObjectBaker implements EntityBaker<ObjectState> {
|
|||||||
double[] v = cross(d, u);
|
double[] v = cross(d, u);
|
||||||
Affine frame = new Affine(
|
Affine frame = new Affine(
|
||||||
new double[] {u[0], v[0], d[0], u[1], v[1], d[1], u[2], v[2], d[2]}, new double[] {a[0], a[1], a[2]});
|
new double[] {u[0], v[0], d[0], u[1], v[1], d[1], u[2], v[2], d[2]}, new double[] {a[0], a[1], a[2]});
|
||||||
int[][] solid = {{argb}};
|
|
||||||
Face[] f = new Face[6];
|
|
||||||
for (int i = 0; i < 6; i++) f[i] = new Face(solid, 0, 0, 1, 1, 0, -1);
|
|
||||||
double w = LEASH_HALF_WIDTH;
|
double w = LEASH_HALF_WIDTH;
|
||||||
return new EntityCube(new double[] {-w, -w, 0}, new double[] {w, w, len + w}, f, frame);
|
return new EntityCube(new double[] {-w, -w, 0}, new double[] {w, w, len + w}, faces, frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Face[] solidFaces(int argb) {
|
||||||
|
Face[] f = new Face[6];
|
||||||
|
Arrays.fill(f, new Face(new int[][] {{argb}}, 0, 0, 1, 1, 0, -1));
|
||||||
|
return f;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static double[] cross(double[] a, double[] b) {
|
private static double[] cross(double[] a, double[] b) {
|
||||||
@@ -110,21 +110,23 @@ public final class ObjectBaker implements EntityBaker<ObjectState> {
|
|||||||
private void addText(ObjectState s, List<EntityCube> cubes) {
|
private void addText(ObjectState s, List<EntityCube> cubes) {
|
||||||
if (font == null || font.isEmpty()) return;
|
if (font == null || font.isEmpty()) return;
|
||||||
List<String> lines = wrap(s.lines(), Math.max(1, s.lineWidth()));
|
List<String> lines = wrap(s.lines(), Math.max(1, s.lineWidth()));
|
||||||
boolean blank = lines.stream().allMatch(String::isBlank);
|
int w = 0, h = 0;
|
||||||
int[][] text = blank ? null : SignTextRasterizer.rasterize(lines, font, s.textArgb(), 0, false);
|
if (!lines.stream().allMatch(String::isBlank)) {
|
||||||
int w = text == null ? 0 : text[0].length, h = text == null ? 0 : text.length;
|
int[][] text = SignTextRasterizer.rasterize(lines, font, s.textArgb(), 0, false);
|
||||||
if (text != null && s.shadow()) text = withShadow(text, s.textArgb());
|
w = text[0].length;
|
||||||
|
h = text.length;
|
||||||
// Text block: bottom edge on the origin, horizontally centred, front facing +Z.
|
if (s.shadow()) text = withShadow(text, s.textArgb());
|
||||||
double x0 = -w / 2.0, x1 = x0 + (text == null ? 0 : text[0].length);
|
// Bottom edge on the origin (the shadow hangs below), horizontally centred, front facing +Z.
|
||||||
double y1 = h, y0 = text == null ? 0 : h - text.length;
|
double x0 = -w / 2.0;
|
||||||
if (text != null) {
|
cubes.add(new EntityCube(
|
||||||
cubes.add(
|
new double[] {x0, h - text.length, 0.05},
|
||||||
new EntityCube(new double[] {x0, y0, 0.05}, new double[] {x1, y1, 0.05}, card(text), s.toWorld()));
|
new double[] {x0 + text[0].length, h, 0.05},
|
||||||
|
card(text),
|
||||||
|
s.toWorld()));
|
||||||
}
|
}
|
||||||
int bg = s.backgroundArgb();
|
int bg = s.backgroundArgb();
|
||||||
// Translucent backgrounds (vanilla default: 25% black) keep their alpha; the tracer blends them.
|
// Translucent backgrounds (vanilla default: 25% black) keep their alpha; the tracer blends them.
|
||||||
if (ColorUtil.opaque(bg) && (w > 0 || !s.lines().isEmpty())) {
|
if (ColorUtil.opaque(bg) && !s.lines().isEmpty()) {
|
||||||
int[][] solid = {{bg}};
|
int[][] solid = {{bg}};
|
||||||
cubes.add(new EntityCube(
|
cubes.add(new EntityCube(
|
||||||
new double[] {-w / 2.0 - 1, -1, 0}, new double[] {w / 2.0 + 1, h, 0}, card(solid), s.toWorld()));
|
new double[] {-w / 2.0 - 1, -1, 0}, new double[] {w / 2.0 + 1, h, 0}, card(solid), s.toWorld()));
|
||||||
@@ -135,7 +137,7 @@ public final class ObjectBaker implements EntityBaker<ObjectState> {
|
|||||||
* Faces of a planar (z-thickness 0) card: front (+Z) and back show the SAME texel at each point (the
|
* Faces of a planar (z-thickness 0) card: front (+Z) and back show the SAME texel at each point (the
|
||||||
* back is seen mirrored, as in vanilla), so a ray through a transparent texel misses both sides.
|
* back is seen mirrored, as in vanilla), so a ray through a transparent texel misses both sides.
|
||||||
*/
|
*/
|
||||||
private static Face[] card(int[][] tex) {
|
static Face[] card(int[][] tex) {
|
||||||
Face[] f = new Face[6];
|
Face[] f = new Face[6];
|
||||||
f[Direction.SOUTH.ordinal()] = new Face(tex, 0, 0, 1, 1, 0, -1);
|
f[Direction.SOUTH.ordinal()] = new Face(tex, 0, 0, 1, 1, 0, -1);
|
||||||
f[Direction.NORTH.ordinal()] = new Face(tex, 1, 0, 0, 1, 0, -1);
|
f[Direction.NORTH.ordinal()] = new Face(tex, 1, 0, 0, 1, 0, -1);
|
||||||
@@ -171,8 +173,10 @@ public final class ObjectBaker implements EntityBaker<ObjectState> {
|
|||||||
int h = text.length, w = text[0].length;
|
int h = text.length, w = text[0].length;
|
||||||
int[][] out = new int[h + 1][w + 1];
|
int[][] out = new int[h + 1][w + 1];
|
||||||
int shadow = ColorUtil.shade(argb, 0.25);
|
int shadow = ColorUtil.shade(argb, 0.25);
|
||||||
for (int y = 0; y < h; y++) for (int x = 0; x < w; x++) if ((text[y][x] >>> 24) > 0) out[y + 1][x + 1] = shadow;
|
for (int y = 0; y < h; y++)
|
||||||
for (int y = 0; y < h; y++) for (int x = 0; x < w; x++) if ((text[y][x] >>> 24) > 0) out[y][x] = text[y][x];
|
for (int x = 0; x < w; x++) if (ColorUtil.alpha(text[y][x]) > 0) out[y + 1][x + 1] = shadow;
|
||||||
|
for (int y = 0; y < h; y++)
|
||||||
|
for (int x = 0; x < w; x++) if (ColorUtil.alpha(text[y][x]) > 0) out[y][x] = text[y][x];
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,10 +4,9 @@ import java.util.List;
|
|||||||
import org.bukkit.block.data.BlockData;
|
import org.bukkit.block.data.BlockData;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Snapshot of a non-mob object that renders block/item/text content instead of an entity model:
|
* Snapshot of a non-mob object that renders block/item/text content instead of an entity model: block/item/text
|
||||||
* block/item/text DISPLAY entities, dropped items, falling blocks, primed TNT and the block inside a
|
* display entities, dropped items, falling blocks, primed TNT, minecart blocks and leash ropes. The local→world
|
||||||
* minecart, and leash ropes. Captured on the main thread with its complete local→world transform already resolved
|
* transform is resolved at capture (position, yaw/pitch or billboard, display transformation).
|
||||||
* (entity position, yaw/pitch or billboard facing, display transformation), so baking is pure geometry.
|
|
||||||
*
|
*
|
||||||
* <p>Local frames: BLOCK = the block cube {@code [0,1]³}; ITEM = an item centred on the origin
|
* <p>Local frames: BLOCK = the block cube {@code [0,1]³}; ITEM = an item centred on the origin
|
||||||
* ({@code [-0.5,0.5]³}); TEXT = font pixels (x right, y up, text front facing +Z), already scaled.
|
* ({@code [-0.5,0.5]³}); TEXT = font pixels (x right, y up, text front facing +Z), already scaled.
|
||||||
@@ -19,7 +18,7 @@ public record ObjectState(
|
|||||||
BlockEntityState blockEntity, // BLOCK: set when the block is block-entity rendered (chest, banner, …)
|
BlockEntityState blockEntity, // BLOCK: set when the block is block-entity rendered (chest, banner, …)
|
||||||
ItemSpec item, // ITEM: the item
|
ItemSpec item, // ITEM: the item
|
||||||
String context, // ITEM: display context ("none", "fixed", "ground", "head", …)
|
String context, // ITEM: display context ("none", "fixed", "ground", "head", …)
|
||||||
boolean dropped, // ITEM: a dropped item entity (vanilla lifts it by 0.1 + 0.25·groundScaleY)
|
boolean dropped, // ITEM: a dropped item entity (lifted above the ground by the baker)
|
||||||
double[] leash, // LEASH: rope points {fromX,fromY,fromZ, toX,toY,toZ} (entity -> holder)
|
double[] leash, // LEASH: rope points {fromX,fromY,fromZ, toX,toY,toZ} (entity -> holder)
|
||||||
List<String> lines, // TEXT: explicit lines (wrapped by the baker to lineWidth)
|
List<String> lines, // TEXT: explicit lines (wrapped by the baker to lineWidth)
|
||||||
int lineWidth, // TEXT: wrap width in font px
|
int lineWidth, // TEXT: wrap width in font px
|
||||||
|
|||||||
@@ -2,19 +2,18 @@ package eu.mhsl.minecraft.pixelpics.render.entity;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/** A baked entity: its world-space cubes and overall AABB (for broad-phase culling). */
|
/** A baked entity: its world-space cubes and their union AABB (framing in the test harnesses). */
|
||||||
public final class RenderedEntity {
|
public final class RenderedEntity {
|
||||||
public final List<EntityCube> cubes;
|
public final List<EntityCube> cubes;
|
||||||
public final double[] aabbMin;
|
public final double[] aabbMin;
|
||||||
public final double[] aabbMax;
|
public final double[] aabbMax;
|
||||||
|
|
||||||
public RenderedEntity(List<EntityCube> cubes, double[] aabbMin, double[] aabbMax) {
|
private RenderedEntity(List<EntityCube> cubes, double[] aabbMin, double[] aabbMax) {
|
||||||
this.cubes = cubes;
|
this.cubes = cubes;
|
||||||
this.aabbMin = aabbMin;
|
this.aabbMin = aabbMin;
|
||||||
this.aabbMax = aabbMax;
|
this.aabbMax = aabbMax;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Wraps baked world-space cubes, computing the overall broad-phase AABB from their per-cube AABBs. */
|
|
||||||
public static RenderedEntity of(List<EntityCube> cubes) {
|
public static RenderedEntity of(List<EntityCube> cubes) {
|
||||||
double[] min = {Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE};
|
double[] min = {Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE};
|
||||||
double[] max = {-Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE};
|
double[] max = {-Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE};
|
||||||
|
|||||||
@@ -1,66 +1,55 @@
|
|||||||
package eu.mhsl.minecraft.pixelpics.render.entity;
|
package eu.mhsl.minecraft.pixelpics.render.entity;
|
||||||
|
|
||||||
/**
|
import eu.mhsl.minecraft.pixelpics.render.util.ColorUtil;
|
||||||
* In-place ARGB pixel-grid compositing shared by the entity/block-entity bakers: deep copy, tint and
|
|
||||||
* src-over alpha overlay. Grids are indexed {@code [y][x]}; all operations assume the ARGB layout used
|
/** In-place ARGB pixel-grid ({@code [y][x]}) compositing shared by the entity and block-entity bakers. */
|
||||||
* throughout the renderer.
|
|
||||||
*/
|
|
||||||
public final class TextureOps {
|
public final class TextureOps {
|
||||||
|
|
||||||
private TextureOps() {}
|
private TextureOps() {}
|
||||||
|
|
||||||
/** A row-by-row copy so callers can tint/overlay without mutating the cached source texture. */
|
/** A row-by-row copy, so callers can tint/overlay without mutating a cached source texture. */
|
||||||
public static int[][] deepCopy(int[][] src) {
|
public static int[][] deepCopy(int[][] src) {
|
||||||
int[][] out = new int[src.length][];
|
int[][] out = new int[src.length][];
|
||||||
for (int y = 0; y < src.length; y++) out[y] = src[y].clone();
|
for (int y = 0; y < src.length; y++) out[y] = src[y].clone();
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Multiplies every non-transparent texel by an ARGB tint (RGB channels only), in place. */
|
/** Multiplies the RGB of every non-transparent texel by {@code argb}, in place. */
|
||||||
public static void tint(int[][] tex, int argb) {
|
public static void tint(int[][] tex, int argb) {
|
||||||
int tr = (argb >> 16) & 0xFF, tg = (argb >> 8) & 0xFF, tb = argb & 0xFF;
|
|
||||||
for (int[] row : tex) {
|
for (int[] row : tex) {
|
||||||
for (int x = 0; x < row.length; x++) {
|
for (int x = 0; x < row.length; x++) {
|
||||||
int p = row[x];
|
if (ColorUtil.alpha(row[x]) != 0) row[x] = ColorUtil.multiply(row[x], argb);
|
||||||
int a = (p >>> 24) & 0xFF;
|
|
||||||
if (a == 0) continue;
|
|
||||||
int r = ((p >> 16) & 0xFF) * tr / 255, g = ((p >> 8) & 0xFF) * tg / 255, b = (p & 0xFF) * tb / 255;
|
|
||||||
row[x] = (a << 24) | (r << 16) | (g << 8) | b;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Standard src-over alpha composite of {@code src} onto {@code dst} (clipped to the overlap), in place. */
|
/** Src-over alpha composite of {@code src} onto {@code dst} (clipped to the overlap), in place. */
|
||||||
public static void overlay(int[][] dst, int[][] src) {
|
public static void overlay(int[][] dst, int[][] src) {
|
||||||
int h = Math.min(dst.length, src.length);
|
int h = Math.min(dst.length, src.length);
|
||||||
for (int y = 0; y < h; y++) {
|
for (int y = 0; y < h; y++) {
|
||||||
int w = Math.min(dst[y].length, src[y].length);
|
int w = Math.min(dst[y].length, src[y].length);
|
||||||
for (int x = 0; x < w; x++) {
|
for (int x = 0; x < w; x++) {
|
||||||
int sp = src[y][x];
|
int sp = src[y][x];
|
||||||
int sa = (sp >>> 24) & 0xFF;
|
int sa = ColorUtil.alpha(sp);
|
||||||
if (sa == 0) continue;
|
if (sa == 0) continue;
|
||||||
if (sa == 255) {
|
if (sa == 255) {
|
||||||
dst[y][x] = sp;
|
dst[y][x] = sp;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
int dp = dst[y][x];
|
int dp = dst[y][x];
|
||||||
int da = (dp >>> 24) & 0xFF;
|
int da = ColorUtil.alpha(dp);
|
||||||
int outA = sa + da * (255 - sa) / 255;
|
int outA = sa + da * (255 - sa) / 255;
|
||||||
int sr = (sp >> 16) & 0xFF, sg = (sp >> 8) & 0xFF, sb = sp & 0xFF;
|
int r = (ColorUtil.red(sp) * sa + ColorUtil.red(dp) * da * (255 - sa) / 255) / Math.max(1, outA);
|
||||||
int dr = (dp >> 16) & 0xFF, dg = (dp >> 8) & 0xFF, db = dp & 0xFF;
|
int g = (ColorUtil.green(sp) * sa + ColorUtil.green(dp) * da * (255 - sa) / 255) / Math.max(1, outA);
|
||||||
int r = (sr * sa + dr * da * (255 - sa) / 255) / Math.max(1, outA);
|
int b = (ColorUtil.blue(sp) * sa + ColorUtil.blue(dp) * da * (255 - sa) / 255) / Math.max(1, outA);
|
||||||
int g = (sg * sa + dg * da * (255 - sa) / 255) / Math.max(1, outA);
|
dst[y][x] = ColorUtil.argb(outA, r, g, b);
|
||||||
int b = (sb * sa + db * da * (255 - sa) / 255) / Math.max(1, outA);
|
|
||||||
dst[y][x] = (outA << 24) | (r << 16) | (g << 8) | b;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Palette-swap recolouring used for armor trims: each (non-transparent) texel of {@code tex} is matched
|
* Armor-trim palette swap: each non-transparent texel is replaced by the {@code to} colour whose index is the
|
||||||
* to its nearest entry in the grayscale source palette {@code from} (8-colour key, RGB distance) and
|
* nearest (RGB distance) {@code from} key shade, keeping its alpha. Vanilla trim patterns use exactly those keys.
|
||||||
* replaced by the same-index colour from the material palette {@code to}, preserving alpha. The vanilla
|
|
||||||
* trim patterns are authored in those 8 key shades, so this reproduces the runtime palette swap.
|
|
||||||
*/
|
*/
|
||||||
public static void paletteSwap(int[][] tex, int[] from, int[] to) {
|
public static void paletteSwap(int[][] tex, int[] from, int[] to) {
|
||||||
int n = Math.min(from.length, to.length);
|
int n = Math.min(from.length, to.length);
|
||||||
@@ -68,13 +57,14 @@ public final class TextureOps {
|
|||||||
for (int[] row : tex) {
|
for (int[] row : tex) {
|
||||||
for (int x = 0; x < row.length; x++) {
|
for (int x = 0; x < row.length; x++) {
|
||||||
int p = row[x];
|
int p = row[x];
|
||||||
int a = (p >>> 24) & 0xFF;
|
int a = ColorUtil.alpha(p);
|
||||||
if (a == 0) continue;
|
if (a == 0) continue;
|
||||||
int pr = (p >> 16) & 0xFF, pg = (p >> 8) & 0xFF, pb = p & 0xFF;
|
|
||||||
int best = 0, bestD = Integer.MAX_VALUE;
|
int best = 0, bestD = Integer.MAX_VALUE;
|
||||||
for (int i = 0; i < n; i++) {
|
for (int i = 0; i < n; i++) {
|
||||||
int c = from[i];
|
int c = from[i];
|
||||||
int dr = pr - ((c >> 16) & 0xFF), dg = pg - ((c >> 8) & 0xFF), db = pb - (c & 0xFF);
|
int dr = ColorUtil.red(p) - ColorUtil.red(c);
|
||||||
|
int dg = ColorUtil.green(p) - ColorUtil.green(c);
|
||||||
|
int db = ColorUtil.blue(p) - ColorUtil.blue(c);
|
||||||
int d = dr * dr + dg * dg + db * db;
|
int d = dr * dr + dg * dg + db * db;
|
||||||
if (d < bestD) {
|
if (d < bestD) {
|
||||||
bestD = d;
|
bestD = d;
|
||||||
@@ -87,34 +77,27 @@ public final class TextureOps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Static approximation of the (animated) enchantment glint: a single glint frame is tiled over the
|
* Static approximation of the animated enchantment glint: one glint frame tiled over the texture and added
|
||||||
* texture and additively blended — tinted by {@code glintColor} and scaled by {@code strength} and the
|
* (tinted by {@code glintColor}, scaled by {@code strength}, frame alpha and luminance) to its opaque texels.
|
||||||
* frame's luminance — onto the texture's opaque texels only (so it sheens the armor, not the
|
|
||||||
* transparent background). Not animated; just a frozen purple shimmer.
|
|
||||||
*/
|
*/
|
||||||
public static void addGlint(int[][] tex, int[][] glint, int glintColor, double strength) {
|
public static void addGlint(int[][] tex, int[][] glint, int glintColor, double strength) {
|
||||||
if (glint == null || glint.length == 0 || glint[0].length == 0) return;
|
if (glint == null || glint.length == 0 || glint[0].length == 0) return;
|
||||||
int gh = glint.length, gw = glint[0].length;
|
int gh = glint.length, gw = glint[0].length;
|
||||||
int cr = (glintColor >> 16) & 0xFF, cg = (glintColor >> 8) & 0xFF, cb = glintColor & 0xFF;
|
int cr = ColorUtil.red(glintColor), cg = ColorUtil.green(glintColor), cb = ColorUtil.blue(glintColor);
|
||||||
for (int y = 0; y < tex.length; y++) {
|
for (int y = 0; y < tex.length; y++) {
|
||||||
for (int x = 0; x < tex[y].length; x++) {
|
for (int x = 0; x < tex[y].length; x++) {
|
||||||
int p = tex[y][x];
|
int p = tex[y][x];
|
||||||
int a = (p >>> 24) & 0xFF;
|
int a = ColorUtil.alpha(p);
|
||||||
if (a == 0) continue; // only sheen actual armor pixels
|
if (a == 0) continue;
|
||||||
int gp = glint[y % gh][x % gw];
|
int gp = glint[y % gh][x % gw];
|
||||||
int ga = (gp >>> 24) & 0xFF;
|
int gl = Math.max(ColorUtil.red(gp), Math.max(ColorUtil.green(gp), ColorUtil.blue(gp)));
|
||||||
int gl = Math.max((gp >> 16) & 0xFF, Math.max((gp >> 8) & 0xFF, gp & 0xFF));
|
double inten = strength * (ColorUtil.alpha(gp) / 255.0) * (gl / 255.0);
|
||||||
double inten = strength * (ga / 255.0) * (gl / 255.0);
|
|
||||||
if (inten <= 0) continue;
|
if (inten <= 0) continue;
|
||||||
int r = clamp(((p >> 16) & 0xFF) + (int) (cr * inten));
|
int r = ColorUtil.clamp(ColorUtil.red(p) + (int) (cr * inten));
|
||||||
int g = clamp(((p >> 8) & 0xFF) + (int) (cg * inten));
|
int g = ColorUtil.clamp(ColorUtil.green(p) + (int) (cg * inten));
|
||||||
int b = clamp((p & 0xFF) + (int) (cb * inten));
|
int b = ColorUtil.clamp(ColorUtil.blue(p) + (int) (cb * inten));
|
||||||
tex[y][x] = (a << 24) | (r << 16) | (g << 8) | b;
|
tex[y][x] = ColorUtil.argb(a, r, g, b);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int clamp(int v) {
|
|
||||||
return v < 0 ? 0 : (Math.min(v, 255));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -95,11 +95,11 @@ public final class ElementIntersector {
|
|||||||
// Blockstate rotation: hit the model-space element with the inverse-rotated ray (same t: rigid
|
// 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 vanilla's FaceBakery.
|
// transform), so faces and UVs stay bound to the model like vanilla's FaceBakery.
|
||||||
e = element.unrotated;
|
e = element.unrotated;
|
||||||
unrotate(v, ox, oy, oz, element.xSteps, element.ySteps, true);
|
Element.unrotate(v, ox, oy, oz, element.xSteps, element.ySteps, true);
|
||||||
eox = v[0];
|
eox = v[0];
|
||||||
eoy = v[1];
|
eoy = v[1];
|
||||||
eoz = v[2];
|
eoz = v[2];
|
||||||
unrotate(v, dx, dy, dz, element.xSteps, element.ySteps, false);
|
Element.unrotate(v, dx, dy, dz, element.xSteps, element.ySteps, false);
|
||||||
edx = v[0];
|
edx = v[0];
|
||||||
edy = v[1];
|
edy = v[1];
|
||||||
edz = v[2];
|
edz = v[2];
|
||||||
@@ -108,7 +108,7 @@ public final class ElementIntersector {
|
|||||||
? slab(sc, eox, eoy, eoz, edx, edy, edz, e.from, e.to, null)
|
? slab(sc, eox, eoy, eoz, edx, edy, edz, e.from, e.to, null)
|
||||||
: intersectObb(sc, e, eox, eoy, eoz, edx, edy, edz);
|
: intersectObb(sc, e, eox, eoy, eoz, edx, edy, edz);
|
||||||
if (hit && element.unrotated != null) {
|
if (hit && element.unrotated != null) {
|
||||||
rotate(v, sc.hitNx, sc.hitNy, sc.hitNz, element.xSteps, element.ySteps, false);
|
Element.rotate(v, sc.hitNx, sc.hitNy, sc.hitNz, element.xSteps, element.ySteps, false);
|
||||||
sc.hitNx = v[0];
|
sc.hitNx = v[0];
|
||||||
sc.hitNy = v[1];
|
sc.hitNy = v[1];
|
||||||
sc.hitNz = v[2];
|
sc.hitNz = v[2];
|
||||||
@@ -250,7 +250,7 @@ public final class ElementIntersector {
|
|||||||
double py = o[1] + d[1] * tEntry;
|
double py = o[1] + d[1] * tEntry;
|
||||||
double pz = o[2] + d[2] * tEntry;
|
double pz = o[2] + d[2] * tEntry;
|
||||||
|
|
||||||
Direction dir = faceFor(axis, negFace);
|
Direction dir = Direction.of(axis, negFace);
|
||||||
setNormal(sc, obb, dir.nx, dir.ny, dir.nz);
|
setNormal(sc, obb, dir.nx, dir.ny, dir.nz);
|
||||||
double fracX = frac(px, from[0], to[0]);
|
double fracX = frac(px, from[0], to[0]);
|
||||||
double fracY = frac(py, from[1], to[1]);
|
double fracY = frac(py, from[1], to[1]);
|
||||||
@@ -274,26 +274,16 @@ public final class ElementIntersector {
|
|||||||
double[] o = sc.o, d = sc.d;
|
double[] o = sc.o, d = sc.d;
|
||||||
double best = Double.POSITIVE_INFINITY;
|
double best = Double.POSITIVE_INFINITY;
|
||||||
Direction bestDir = null;
|
Direction bestDir = null;
|
||||||
double bestSign = 0;
|
|
||||||
for (Direction dir : DIRECTIONS) {
|
for (Direction dir : DIRECTIONS) {
|
||||||
int a = dir.nx != 0 ? 0 : dir.ny != 0 ? 1 : 2;
|
double t = invertedFaceT(dir, from, to, o, d, false);
|
||||||
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;
|
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;
|
best = t;
|
||||||
bestDir = dir;
|
bestDir = dir;
|
||||||
bestSign = sign;
|
|
||||||
}
|
}
|
||||||
if (bestDir == null) return false;
|
if (bestDir == null) return false;
|
||||||
int a = bestDir.nx != 0 ? 0 : bestDir.ny != 0 ? 1 : 2;
|
int a = bestDir.axis;
|
||||||
setNormal(sc, obb, a == 0 ? bestSign : 0, a == 1 ? bestSign : 0, a == 2 ? bestSign : 0);
|
double sign = invertedFaceSign(bestDir, from, to, false);
|
||||||
|
setNormal(sc, obb, a == 0 ? sign : 0, a == 1 ? sign : 0, a == 2 ? sign : 0);
|
||||||
double fx = frac(o[0] + d[0] * best, from[0], to[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 fy = frac(o[1] + d[1] * best, from[1], to[1]);
|
||||||
double fz = frac(o[2] + d[2] * best, from[2], to[2]);
|
double fz = frac(o[2] + d[2] * best, from[2], to[2]);
|
||||||
@@ -304,6 +294,34 @@ public final class ElementIntersector {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Outward normal sign of face {@code dir} of an inverted box along its axis: each inverted OTHER axis flips the
|
||||||
|
* winding, as does a mirroring transform ({@code mirrored}).
|
||||||
|
*/
|
||||||
|
private static double invertedFaceSign(Direction dir, double[] from, double[] to, boolean mirrored) {
|
||||||
|
int b = (dir.axis + 1) % 3, c = (dir.axis + 2) % 3;
|
||||||
|
int flips = (from[b] > to[b] ? 1 : 0) + (from[c] > to[c] ? 1 : 0);
|
||||||
|
boolean neg = dir.nx + dir.ny + dir.nz < 0;
|
||||||
|
return (neg ? -1 : 1) * (flips % 2 == 0 ? 1 : -1) * (mirrored ? -1 : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ray parameter where the ray {@code o + t·d} meets face {@code dir} of an inverted box (see
|
||||||
|
* {@link #invertedBox}) from its visible side inside the face rectangle; NaN for a back face, a parallel ray or
|
||||||
|
* a miss. Shared with the entity intersector (inverted elements baked as items/displays).
|
||||||
|
*/
|
||||||
|
public static double invertedFaceT(
|
||||||
|
Direction dir, double[] from, double[] to, double[] o, double[] d, boolean mirrored) {
|
||||||
|
int a = dir.axis, b = (a + 1) % 3, c = (a + 2) % 3;
|
||||||
|
if (invertedFaceSign(dir, from, to, mirrored) * d[a] >= 0) return Double.NaN;
|
||||||
|
boolean neg = dir.nx + dir.ny + dir.nz < 0;
|
||||||
|
double t = ((neg ? from[a] : to[a]) - o[a]) / d[a];
|
||||||
|
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) return Double.NaN;
|
||||||
|
if (pc < Math.min(from[c], to[c]) - EPS || pc > Math.max(from[c], to[c]) + EPS) return Double.NaN;
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The hit normal (local axis normal of the element's frame) in block space: rotated back for {@code obb}. The
|
* 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).
|
* rescale is diagonal in the local frame, so an axis normal keeps its direction (n = R·n_local).
|
||||||
@@ -330,7 +348,7 @@ public final class ElementIntersector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Position between {@code from} (0) and {@code to} (1), also when {@code from > to}; clamped to 0..1. */
|
/** Position between {@code from} (0) and {@code to} (1), also when {@code from > to}; clamped to 0..1. */
|
||||||
private static double frac(double v, double from, double to) {
|
public static double frac(double v, double from, double to) {
|
||||||
double span = to - from;
|
double span = to - from;
|
||||||
if (Math.abs(span) < 1e-6) return 0;
|
if (Math.abs(span) < 1e-6) return 0;
|
||||||
double f = (v - from) / span;
|
double f = (v - from) / span;
|
||||||
@@ -360,14 +378,6 @@ public final class ElementIntersector {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Direction faceFor(int axis, boolean negFace) {
|
|
||||||
return switch (axis) {
|
|
||||||
case 0 -> negFace ? Direction.WEST : Direction.EAST;
|
|
||||||
case 1 -> negFace ? Direction.DOWN : Direction.UP;
|
|
||||||
default -> negFace ? Direction.NORTH : Direction.SOUTH;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Rotates (x, y, z) about axis 0/1/2 (x/y/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) {
|
private static void rotate(double[] out, double x, double y, double z, int axis, double cos, double sin) {
|
||||||
switch (axis) {
|
switch (axis) {
|
||||||
@@ -388,29 +398,4 @@ public final class ElementIntersector {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** {@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;
|
|
||||||
for (int i = 0; i < xSteps; i++) {
|
|
||||||
double ny = dz, nz = -dy; // (dy, dz) -> (dz, -dy)
|
|
||||||
dy = ny;
|
|
||||||
dz = nz;
|
|
||||||
}
|
|
||||||
for (int i = 0; i < ySteps; i++) {
|
|
||||||
double nx = -dz, nz = dx; // (dx, dz) -> (-dz, dx)
|
|
||||||
dx = nx;
|
|
||||||
dz = nz;
|
|
||||||
}
|
|
||||||
out[0] = dx + c;
|
|
||||||
out[1] = dy + c;
|
|
||||||
out[2] = dz + c;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** {@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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -212,7 +212,7 @@ public final class FluidRenderer {
|
|||||||
int mask = 0;
|
int mask = 0;
|
||||||
if (model.elements.isEmpty()) return 0;
|
if (model.elements.isEmpty()) return 0;
|
||||||
for (Direction d : Direction.values()) {
|
for (Direction d : Direction.values()) {
|
||||||
int axis = d.nx != 0 ? 0 : d.ny != 0 ? 1 : 2;
|
int axis = d.axis;
|
||||||
int a1 = axis == 0 ? 1 : 0, a2 = axis == 2 ? 1 : 2;
|
int a1 = axis == 0 ? 1 : 0, a2 = axis == 2 ? 1 : 2;
|
||||||
boolean positive = d.nx + d.ny + d.nz > 0;
|
boolean positive = d.nx + d.ny + d.nz > 0;
|
||||||
boolean covered = true;
|
boolean covered = true;
|
||||||
|
|||||||
@@ -83,11 +83,11 @@ public final class BiomeClimate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final int DEFAULT_WATER = 0x3F76E4;
|
private static final int DEFAULT_WATER = 0x3F76E4;
|
||||||
// Vanilla 26.3 EnvironmentAttributes defaults (no dimension type sets them).
|
// Vanilla 26.3 EnvironmentAttributes defaults (no dimension type sets them).
|
||||||
public static final int DEFAULT_WATER_FOG_COLOR = 0x050533;
|
private static final int DEFAULT_WATER_FOG_COLOR = 0x050533;
|
||||||
public static final double DEFAULT_WATER_FOG_START = -8;
|
private static final double DEFAULT_WATER_FOG_START = -8;
|
||||||
public static final double DEFAULT_WATER_FOG_END = 96;
|
private static final double DEFAULT_WATER_FOG_END = 96;
|
||||||
public static final Climate DEFAULT = new Climate(0.8, 0.4, DEFAULT_WATER, NONE, NONE, NONE, null);
|
public static final Climate DEFAULT = new Climate(0.8, 0.4, DEFAULT_WATER, NONE, NONE, NONE, null);
|
||||||
|
|
||||||
private static final String DATA_PATH = "data/minecraft/worldgen/biome/";
|
private static final String DATA_PATH = "data/minecraft/worldgen/biome/";
|
||||||
@@ -189,7 +189,7 @@ public final class BiomeClimate {
|
|||||||
|
|
||||||
public static Climate forKey(String biomePath) {
|
public static Climate forKey(String biomePath) {
|
||||||
Optional<Climate> loaded = LOADED.computeIfAbsent(biomePath, BiomeClimate::loadFromServerData);
|
Optional<Climate> loaded = LOADED.computeIfAbsent(biomePath, BiomeClimate::loadFromServerData);
|
||||||
return loaded.orElseGet(() -> FALLBACK.getOrDefault(biomePath, DEFAULT));
|
return loaded.isPresent() ? loaded.get() : FALLBACK.getOrDefault(biomePath, DEFAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Optional<Climate> loadFromServerData(String biomePath) {
|
private static Optional<Climate> loadFromServerData(String biomePath) {
|
||||||
@@ -247,8 +247,8 @@ public final class BiomeClimate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Colors are either "#rrggbb" strings or packed integers, depending on the data version. */
|
/** Colors are either "#rrggbb" strings or packed integers, depending on the data version. */
|
||||||
private static int color(JsonObject effects, String key, int fallback) {
|
private static int color(JsonObject obj, String key, int fallback) {
|
||||||
JsonElement el = effects.get(key);
|
JsonElement el = obj.get(key);
|
||||||
if (el == null || !el.isJsonPrimitive()) return fallback;
|
if (el == null || !el.isJsonPrimitive()) return fallback;
|
||||||
if (el.getAsJsonPrimitive().isNumber()) return el.getAsInt() & 0xFFFFFF;
|
if (el.getAsJsonPrimitive().isNumber()) return el.getAsInt() & 0xFFFFFF;
|
||||||
String s = el.getAsString().trim();
|
String s = el.getAsString().trim();
|
||||||
|
|||||||
@@ -6,10 +6,7 @@ import java.util.Map;
|
|||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import org.bukkit.block.Biome;
|
import org.bukkit.block.Biome;
|
||||||
|
|
||||||
/**
|
/** Per-biome grass/foliage/water tints: explicit biome colours or the pack's colormaps, cached per biome. */
|
||||||
* Computes per-biome grass/foliage tint colors by sampling the resource pack's colormaps using the
|
|
||||||
* vanilla temperature/downfall formula, plus a per-biome water color. Results are cached per biome.
|
|
||||||
*/
|
|
||||||
public final class BiomeTintProvider {
|
public final class BiomeTintProvider {
|
||||||
|
|
||||||
private final int[][] grassMap;
|
private final int[][] grassMap;
|
||||||
@@ -29,7 +26,7 @@ public final class BiomeTintProvider {
|
|||||||
return cache.computeIfAbsent(keyOf(biome), this::compute);
|
return cache.computeIfAbsent(keyOf(biome), this::compute);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The biome's climate/effects/attribute data (water fog etc.), see {@link BiomeClimate}. */
|
/** The biome's climate/effects/attribute data (water fog etc.). */
|
||||||
public BiomeClimate.Climate climate(Biome biome) {
|
public BiomeClimate.Climate climate(Biome biome) {
|
||||||
return BiomeClimate.forKey(keyOf(biome));
|
return BiomeClimate.forKey(keyOf(biome));
|
||||||
}
|
}
|
||||||
@@ -44,18 +41,11 @@ public final class BiomeTintProvider {
|
|||||||
|
|
||||||
private BiomeTint compute(String key) {
|
private BiomeTint compute(String key) {
|
||||||
BiomeClimate.Climate climate = BiomeClimate.forKey(key);
|
BiomeClimate.Climate climate = BiomeClimate.forKey(key);
|
||||||
// Explicit biome colors (biome "effects") win over the colormap, like in vanilla.
|
int grass = channel(climate.grass(), grassMap, climate, 0xFF91BD59);
|
||||||
int grass = climate.grass() != BiomeClimate.NONE
|
int foliage = channel(climate.foliage(), foliageMap, climate, 0xFF77AB2F);
|
||||||
? 0xFF000000 | climate.grass()
|
int dry = channel(climate.dryFoliage(), dryFoliageMap, climate, 0xFFA9A05B);
|
||||||
: sample(grassMap, climate.temperature(), climate.downfall(), 0xFF91BD59);
|
|
||||||
int foliage = climate.foliage() != BiomeClimate.NONE
|
|
||||||
? 0xFF000000 | climate.foliage()
|
|
||||||
: sample(foliageMap, climate.temperature(), climate.downfall(), 0xFF77AB2F);
|
|
||||||
int dry = climate.dryFoliage() != BiomeClimate.NONE
|
|
||||||
? 0xFF000000 | climate.dryFoliage()
|
|
||||||
: sample(dryFoliageMap, climate.temperature(), climate.downfall(), 0xFFA9A05B);
|
|
||||||
|
|
||||||
// Vanilla grass_color_modifier (applies to grass only).
|
// Vanilla grass_color_modifier (grass only).
|
||||||
if ("swamp".equals(climate.grassModifier())) {
|
if ("swamp".equals(climate.grassModifier())) {
|
||||||
// Vanilla picks 0x4C763C/0x6A7039 via position noise; use the dominant color.
|
// Vanilla picks 0x4C763C/0x6A7039 via position noise; use the dominant color.
|
||||||
grass = 0xFF6A7039;
|
grass = 0xFF6A7039;
|
||||||
@@ -65,11 +55,16 @@ public final class BiomeTintProvider {
|
|||||||
return new BiomeTint(grass, foliage, dry, 0xFF000000 | climate.water());
|
return new BiomeTint(grass, foliage, dry, 0xFF000000 | climate.water());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** An explicit biome colour wins over the colormap, like in vanilla. */
|
||||||
|
private static int channel(int explicit, int[][] colormap, BiomeClimate.Climate climate, int fallback) {
|
||||||
|
return explicit != BiomeClimate.NONE ? 0xFF000000 | explicit : sample(colormap, climate, fallback);
|
||||||
|
}
|
||||||
|
|
||||||
/** Vanilla colormap lookup: x = (1-temp)*255, y = (1-downfall*temp)*255. */
|
/** Vanilla colormap lookup: x = (1-temp)*255, y = (1-downfall*temp)*255. */
|
||||||
private int sample(int[][] colormap, double temperature, double downfall, int fallback) {
|
private static int sample(int[][] colormap, BiomeClimate.Climate climate, int fallback) {
|
||||||
if (colormap == null || colormap.length == 0) return fallback;
|
if (colormap == null || colormap.length == 0) return fallback;
|
||||||
double temp = Math.clamp(temperature, 0, 1);
|
double temp = Math.clamp(climate.temperature(), 0, 1);
|
||||||
double down = Math.clamp(downfall, 0, 1) * temp;
|
double down = Math.clamp(climate.downfall(), 0, 1) * temp;
|
||||||
int x = (int) ((1.0 - temp) * 255.0);
|
int x = (int) ((1.0 - temp) * 255.0);
|
||||||
int y = (int) ((1.0 - down) * 255.0);
|
int y = (int) ((1.0 - down) * 255.0);
|
||||||
int h = colormap.length;
|
int h = colormap.length;
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package eu.mhsl.minecraft.pixelpics.render.tint;
|
package eu.mhsl.minecraft.pixelpics.render.tint;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
import org.bukkit.Material;
|
||||||
import org.bukkit.block.data.BlockData;
|
import org.bukkit.block.data.BlockData;
|
||||||
|
import org.bukkit.block.data.type.RedstoneWire;
|
||||||
|
|
||||||
/**
|
/** Maps a tinted face (material + tintindex) to its tint colour: a biome channel or a vanilla constant. */
|
||||||
* Maps a tinted face (material + tintindex) to the concrete tint color, choosing between the
|
|
||||||
* biome-driven channels and a handful of vanilla constants.
|
|
||||||
*/
|
|
||||||
public final class TintResolver {
|
public final class TintResolver {
|
||||||
|
|
||||||
private static final int BIRCH = 0xFF80A755;
|
private static final int BIRCH = 0xFF80A755;
|
||||||
@@ -13,51 +13,56 @@ public final class TintResolver {
|
|||||||
private static final int LILY_PAD = 0xFF208030;
|
private static final int LILY_PAD = 0xFF208030;
|
||||||
private static final int STEM = 0xFF60A017;
|
private static final int STEM = 0xFF60A017;
|
||||||
|
|
||||||
|
private enum Channel {
|
||||||
|
BIRCH,
|
||||||
|
SPRUCE,
|
||||||
|
FOLIAGE,
|
||||||
|
LILY_PAD,
|
||||||
|
WATER,
|
||||||
|
REDSTONE,
|
||||||
|
STEM,
|
||||||
|
GRASS
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per Material.ordinal(), filled lazily (idempotent, so the benign race is harmless); called for every tinted hit.
|
||||||
|
private static final Channel[] CHANNELS = new Channel[Material.values().length];
|
||||||
|
|
||||||
private TintResolver() {}
|
private TintResolver() {}
|
||||||
|
|
||||||
/** Returns the ARGB tint to multiply with, or {@code -1} when the face should not be tinted. */
|
/** Returns the ARGB tint to multiply with, or {@code -1} when the face should not be tinted. */
|
||||||
public static int resolve(BlockData data, int tintIndex, BiomeTint biomeTint) {
|
public static int resolve(BlockData data, int tintIndex, BiomeTint biomeTint) {
|
||||||
if (tintIndex < 0) return -1;
|
if (tintIndex < 0) return -1;
|
||||||
String name = data.getMaterial().name().toLowerCase();
|
Material material = data.getMaterial();
|
||||||
|
Channel channel = CHANNELS[material.ordinal()];
|
||||||
if (name.equals("birch_leaves")) return BIRCH;
|
if (channel == null)
|
||||||
if (name.equals("spruce_leaves")) return SPRUCE;
|
CHANNELS[material.ordinal()] = channel = channel(material.name().toLowerCase(Locale.ROOT));
|
||||||
if (name.endsWith("leaves") || name.equals("vine")) return biomeTint.foliage();
|
return switch (channel) {
|
||||||
if (name.equals("lily_pad")) return LILY_PAD;
|
case BIRCH -> BIRCH;
|
||||||
|
case SPRUCE -> SPRUCE;
|
||||||
if (name.equals("water") || name.equals("water_cauldron") || name.equals("bubble_column")) {
|
case FOLIAGE -> biomeTint.foliage();
|
||||||
return biomeTint.water();
|
case LILY_PAD -> LILY_PAD;
|
||||||
}
|
case WATER -> biomeTint.water();
|
||||||
if (name.equals("redstone_wire")) return redstone(data);
|
case REDSTONE -> redstone(data);
|
||||||
if (name.endsWith("stem")) return STEM;
|
case STEM -> STEM;
|
||||||
|
case GRASS -> biomeTint.grass();
|
||||||
// grass_block (top/overlay), short_grass, tall_grass, fern, large_fern, sugar_cane, ...
|
};
|
||||||
if (name.contains("grass")
|
|
||||||
|| name.equals("fern")
|
|
||||||
|| name.equals("large_fern")
|
|
||||||
|| name.equals("sugar_cane")
|
|
||||||
|| name.equals("potted_fern")) {
|
|
||||||
return biomeTint.grass();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default for unknown tinted faces: grass channel (the most common tintindex 0 use).
|
|
||||||
return biomeTint.grass();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Channel channel(String name) {
|
||||||
|
if (name.equals("birch_leaves")) return Channel.BIRCH;
|
||||||
|
if (name.equals("spruce_leaves")) return Channel.SPRUCE;
|
||||||
|
if (name.endsWith("leaves") || name.equals("vine")) return Channel.FOLIAGE;
|
||||||
|
if (name.equals("lily_pad")) return Channel.LILY_PAD;
|
||||||
|
if (name.equals("water") || name.equals("water_cauldron") || name.equals("bubble_column")) return Channel.WATER;
|
||||||
|
if (name.equals("redstone_wire")) return Channel.REDSTONE;
|
||||||
|
if (name.endsWith("stem")) return Channel.STEM;
|
||||||
|
// grass_block, short/tall grass, ferns, sugar cane, … and any other tinted face (tintindex 0 is mostly grass).
|
||||||
|
return Channel.GRASS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Approximation of the redstone wire colour: red rising with the power level. */
|
||||||
private static int redstone(BlockData data) {
|
private static int redstone(BlockData data) {
|
||||||
int power = 0;
|
int power = data instanceof RedstoneWire wire ? wire.getPower() : 0;
|
||||||
String s = data.getAsString(false);
|
return 0xFF000000 | (Math.min(255, 75 + power * 12) << 16);
|
||||||
int idx = s.indexOf("power=");
|
|
||||||
if (idx >= 0) {
|
|
||||||
int end = idx + 6;
|
|
||||||
int e = end;
|
|
||||||
while (e < s.length() && Character.isDigit(s.charAt(e))) e++;
|
|
||||||
try {
|
|
||||||
power = Integer.parseInt(s.substring(end, e));
|
|
||||||
} catch (NumberFormatException ignored) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
int r = Math.min(255, 75 + power * 12);
|
|
||||||
return 0xFF000000 | (r << 16);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,9 +3,7 @@ package eu.mhsl.minecraft.pixelpics.render.util;
|
|||||||
import org.bukkit.Color;
|
import org.bukkit.Color;
|
||||||
import org.bukkit.DyeColor;
|
import org.bukkit.DyeColor;
|
||||||
|
|
||||||
/**
|
/** Helpers for packed ARGB colours. */
|
||||||
* Helpers for packed ARGB integer colors.
|
|
||||||
*/
|
|
||||||
public final class ColorUtil {
|
public final class ColorUtil {
|
||||||
|
|
||||||
private ColorUtil() {}
|
private ColorUtil() {}
|
||||||
@@ -37,11 +35,8 @@ public final class ColorUtil {
|
|||||||
return argb(0xFF, c.getRed(), c.getGreen(), c.getBlue());
|
return argb(0xFF, c.getRed(), c.getGreen(), c.getBlue());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Vanilla {@code DyeColor.getTextColor()} (not the cloth colour) as opaque ARGB; null = black (default ink). */
|
||||||
* The vanilla sign-text colour for a dye (opaque ARGB), from Mojang's {@code DyeColor.getTextColor()}
|
private static int signTextColor(DyeColor dye) {
|
||||||
* table (the firework/text colours, NOT the cloth colours). Null = black (the default sign ink).
|
|
||||||
*/
|
|
||||||
public static int signTextColor(DyeColor dye) {
|
|
||||||
int rgb =
|
int rgb =
|
||||||
switch (dye == null ? DyeColor.BLACK : dye) {
|
switch (dye == null ? DyeColor.BLACK : dye) {
|
||||||
case WHITE -> 0xF9FFFE;
|
case WHITE -> 0xF9FFFE;
|
||||||
@@ -64,23 +59,16 @@ public final class ColorUtil {
|
|||||||
return 0xFF000000 | rgb;
|
return 0xFF000000 | rgb;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Sign text fill (vanilla SignRenderer): the dye's text colour, darkened to 40 % unless glowing. */
|
||||||
* The fill colour for sign text: glowing text uses the full dye colour; non-glowing text is the dye
|
|
||||||
* colour darkened to 40% (matching vanilla {@code SignRenderer}, which is why normal ink looks dim).
|
|
||||||
*/
|
|
||||||
public static int signFillArgb(DyeColor dye, boolean glowing) {
|
public static int signFillArgb(DyeColor dye, boolean glowing) {
|
||||||
int base = signTextColor(dye);
|
int base = signTextColor(dye);
|
||||||
return glowing ? base : (0xFF000000 | (shade(base, 0.4) & 0xFFFFFF));
|
return glowing ? base : shade(base, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Outline of glowing sign text (vanilla {@code getDarkColor}): text colour at 40 %, black ink a light cream. */
|
||||||
* The 8-directional outline colour drawn around glowing sign text (vanilla {@code getDarkColor}):
|
|
||||||
* the dye colour darkened to 40%, except glowing BLACK ink which gets a light cream outline so it
|
|
||||||
* stays readable.
|
|
||||||
*/
|
|
||||||
public static int signOutlineArgb(DyeColor dye) {
|
public static int signOutlineArgb(DyeColor dye) {
|
||||||
if ((dye == null ? DyeColor.BLACK : dye) == DyeColor.BLACK) return 0xFFF0EBCC;
|
if (dye == null || dye == DyeColor.BLACK) return 0xFFF0EBCC;
|
||||||
return 0xFF000000 | (shade(signTextColor(dye), 0.4) & 0xFFFFFF);
|
return shade(signTextColor(dye), 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Multiplies the RGB channels of {@code base} by {@code tint} (per-channel, 0..255), keeping base alpha. */
|
/** Multiplies the RGB channels of {@code base} by {@code tint} (per-channel, 0..255), keeping base alpha. */
|
||||||
@@ -100,7 +88,7 @@ public final class ColorUtil {
|
|||||||
return alpha(argb) > ALPHA_CUTOUT;
|
return alpha(argb) > ALPHA_CUTOUT;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Scales the RGB channels by {@code factor} (0..1), keeping alpha. Used for directional face shading. */
|
/** Scales the RGB channels by {@code factor}, keeping alpha. */
|
||||||
public static int shade(int argb, double factor) {
|
public static int shade(int argb, double factor) {
|
||||||
int a = alpha(argb);
|
int a = alpha(argb);
|
||||||
int r = clamp((int) (red(argb) * factor));
|
int r = clamp((int) (red(argb) * factor));
|
||||||
@@ -116,14 +104,12 @@ public final class ColorUtil {
|
|||||||
| clamp(blue(dst) + blue(src));
|
| clamp(blue(dst) + blue(src));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int clamp(int v) {
|
/** Clamps a channel value to 0..255. */
|
||||||
|
public static int clamp(int v) {
|
||||||
return v < 0 ? 0 : Math.min(v, 255);
|
return v < 0 ? 0 : Math.min(v, 255);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Weighted average of two RGB colours (weights normalised by their sum); alpha ignored, result RGB only. */
|
||||||
* Normalized weighted average of two RGB colors (alpha ignored, result opaque-RGB in the low 24 bits).
|
|
||||||
* Weights need not sum to 1; they are normalized by their total.
|
|
||||||
*/
|
|
||||||
public static int mix(int rgbA, int rgbB, double weightA, double weightB) {
|
public static int mix(int rgbA, int rgbB, double weightA, double weightB) {
|
||||||
double total = weightA + weightB;
|
double total = weightA + weightB;
|
||||||
int r = (int) ((red(rgbA) * weightA + red(rgbB) * weightB) / total);
|
int r = (int) ((red(rgbA) * weightA + red(rgbB) * weightB) / total);
|
||||||
@@ -132,8 +118,6 @@ public final class ColorUtil {
|
|||||||
return (r << 16) | (g << 8) | b;
|
return (r << 16) | (g << 8) | b;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Gamma-correct (linear-light) averaging ---
|
|
||||||
|
|
||||||
private static final float[] SRGB_TO_LINEAR = new float[256];
|
private static final float[] SRGB_TO_LINEAR = new float[256];
|
||||||
|
|
||||||
static {
|
static {
|
||||||
@@ -147,7 +131,7 @@ public final class ColorUtil {
|
|||||||
return SRGB_TO_LINEAR[channel & 0xFF];
|
return SRGB_TO_LINEAR[channel & 0xFF];
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int toSrgb(double linear) {
|
private static int toSrgb(double linear) {
|
||||||
double c = linear <= 0.0031308 ? linear * 12.92 : 1.055 * Math.pow(linear, 1 / 2.4) - 0.055;
|
double c = linear <= 0.0031308 ? linear * 12.92 : 1.055 * Math.pow(linear, 1 / 2.4) - 0.055;
|
||||||
return clamp((int) Math.round(c * 255.0));
|
return clamp((int) Math.round(c * 255.0));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
package eu.mhsl.minecraft.pixelpics.render.util;
|
package eu.mhsl.minecraft.pixelpics.render.util;
|
||||||
|
|
||||||
import org.bukkit.block.BlockFace;
|
|
||||||
import org.bukkit.util.Vector;
|
import org.bukkit.util.Vector;
|
||||||
|
|
||||||
public class MathUtil {
|
public final class MathUtil {
|
||||||
|
|
||||||
private MathUtil() {}
|
private MathUtil() {}
|
||||||
|
|
||||||
@@ -11,8 +10,4 @@ public class MathUtil {
|
|||||||
public static Vector reflectVector(Vector direction, Vector normal) {
|
public static Vector reflectVector(Vector direction, Vector normal) {
|
||||||
return direction.clone().subtract(normal.clone().multiply(2 * direction.dot(normal)));
|
return direction.clone().subtract(normal.clone().multiply(2 * direction.dot(normal)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Vector toVector(BlockFace face) {
|
|
||||||
return new Vector(face.getModX(), face.getModY(), face.getModZ());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user