rendering fixes: rebaked cubes keep unshaded/additive (end portal/gateway in displays), piston heads no longer skulls (shared block-entity material source), armor/horse armor/carpets/saddles from the equippable component (custom assets, trader llama carpet), inverse-transpose normals for non-uniformly scaled displays, later layer wins on equal depth like vanilla, block tints exactly like vanilla blockcolors (redstone table, stems by age, untinted unknown blocks), pose visibility not forced, pptest scenes tint_blocks/equip_assets/display_fixes

This commit is contained in:
MineTec committed 2026-11-05 13:18:04 +01:00
1 parent 4be03afa3f
commit 291d51891f
13 files changed
+321 -140

No files matched your search

@@ -0,0 +1,94 @@
package eu.mhsl.minecraft.pixelpics.assets;
import java.util.EnumSet;
import org.bukkit.Material;
import org.bukkit.block.data.type.Chest;
import org.bukkit.block.data.type.CopperGolemStatue;
import org.bukkit.block.data.type.HangingSign;
import org.bukkit.block.data.type.Sign;
import org.bukkit.block.data.type.Skull;
import org.bukkit.block.data.type.WallHangingSign;
import org.bukkit.block.data.type.WallSign;
import org.bukkit.block.data.type.WallSkull;
/**
* The block families drawn by block-entity renderers, shared by the block-model registry and the block-entity
* snapshot. Matched by the material's BlockData type ({@link Material#data}, needs no live registry) where it is
* unique, otherwise by name (banners, shulker boxes).
*/
public final class BlockEntityBlocks {
private BlockEntityBlocks() {}
// No block-model geometry, drawn only by the entity scene (classify also covers kinds that keep their model).
private static final EnumSet<Material> NO_BLOCK_MODEL = EnumSet.noneOf(Material.class);
static {
for (Material m : Material.values()) {
if (isSign(m)
|| isBanner(m)
|| isShulkerBox(m)
|| isHead(m)
|| isChest(m)
|| m == Material.ENDER_CHEST
|| m == Material.CONDUIT
|| m == Material.DECORATED_POT
|| m == Material.BELL
|| isCopperGolemStatue(m)
|| m == Material.END_PORTAL
|| m == Material.END_GATEWAY) {
NO_BLOCK_MODEL.add(m);
}
}
}
public static boolean hasNoBlockModel(Material m) {
return NO_BLOCK_MODEL.contains(m);
}
/** Chest, trapped chest and the copper chests (not the ender chest). */
public static boolean isChest(Material m) {
return m.data == Chest.class;
}
public static boolean isCopperChest(Material m) {
return isChest(m) && m.name().endsWith("COPPER_CHEST");
}
/** Mob heads and skulls, standing or on a wall (not the piston head). */
public static boolean isHead(Material m) {
return m.data == Skull.class || isWallHead(m);
}
public static boolean isWallHead(Material m) {
return m.data == WallSkull.class;
}
public static boolean isSign(Material m) {
return m.data == Sign.class || m.data == WallSign.class || isHangingSign(m);
}
public static boolean isWallSign(Material m) {
return m.data == WallSign.class;
}
public static boolean isHangingSign(Material m) {
return m.data == HangingSign.class || m.data == WallHangingSign.class;
}
public static boolean isBanner(Material m) {
return m.name().endsWith("_BANNER");
}
public static boolean isWallBanner(Material m) {
return m.name().endsWith("_WALL_BANNER");
}
public static boolean isShulkerBox(Material m) {
return m.name().endsWith("SHULKER_BOX");
}
public static boolean isCopperGolemStatue(Material m) {
return m.data == CopperGolemStatue.class;
}
}
@@ -31,33 +31,6 @@ public final class BlockModelRegistry {
Material.LAVA, Material.LAVA,
Material.BUBBLE_COLUMN); Material.BUBBLE_COLUMN);
// Drawn by the block-entity renderer (entity scene) and without block-model geometry. Matched by name, not
// Material#isBlock (needs a live registry); only queried with placed block materials. Keep in sync with
// render.snapshot.BlockEntitySnapshotBuilder#classify (which also covers kinds that keep their block model).
private static final EnumSet<Material> BLOCK_ENTITY_MATERIALS = EnumSet.noneOf(Material.class);
static {
for (Material m : Material.values()) {
String n = m.name();
if (n.endsWith("_SIGN")
|| n.endsWith("_BANNER")
|| n.endsWith("SHULKER_BOX")
|| ((n.endsWith("_SKULL") || n.endsWith("_HEAD")) && m != Material.PISTON_HEAD)
|| m == Material.CHEST
|| m == Material.TRAPPED_CHEST
|| m == Material.ENDER_CHEST
|| n.endsWith("COPPER_CHEST")
|| m == Material.CONDUIT
|| m == Material.DECORATED_POT
|| m == Material.BELL
|| n.endsWith("COPPER_GOLEM_STATUE")
|| m == Material.END_PORTAL
|| m == Material.END_GATEWAY) {
BLOCK_ENTITY_MATERIALS.add(m);
}
}
}
private final AssetReader reader; private final AssetReader reader;
private final TextureCache textures; private final TextureCache textures;
private final BlockStateResolver blockStateResolver; private final BlockStateResolver blockStateResolver;
@@ -119,7 +92,7 @@ public final class BlockModelRegistry {
if (!elements.isEmpty()) return new ResolvedModel(elements, true); if (!elements.isEmpty()) return new ResolvedModel(elements, true);
// Block entities (builtin/entity models) are drawn by the entity scene: no grey cube in front of them. // Block entities (builtin/entity models) are drawn by the entity scene: no grey cube in front of them.
if (BLOCK_ENTITY_MATERIALS.contains(material)) return ResolvedModel.EMPTY; if (BlockEntityBlocks.hasNoBlockModel(material)) return ResolvedModel.EMPTY;
FlatModel last = variants.isEmpty() FlatModel last = variants.isEmpty()
? null ? null
: modelResolver.resolve(variants.getLast().model()); : modelResolver.resolve(variants.getLast().model());
@@ -17,6 +17,10 @@ public final class EntityCube {
// 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.
public final boolean inverted, mirrored; public final boolean inverted, mirrored;
// Row-major map of the face normals: toWorld's linear part while it is a rotation with a uniform scale (same
// direction), else its inverse-transpose (vanilla PoseStack normal matrix for non-uniform scales, e.g. block
// displays scaled (2, 1, 0.5) and rotated).
public final double[] normalMatrix;
public final double[] aabbMin = {Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE}; public final double[] aabbMin = {Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE};
public final double[] aabbMax = {-Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE}; public final double[] aabbMax = {-Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE};
@@ -28,6 +32,27 @@ public final class EntityCube {
this(from, to, faces, toWorld, additive, false); this(from, to, faces, toWorld, additive, false);
} }
/** Whether the columns of {@code r} are orthogonal and of equal length (rotation, reflection, uniform scale). */
private static boolean conformal(double[] r) {
double xx = r[0] * r[0] + r[3] * r[3] + r[6] * r[6];
double yy = r[1] * r[1] + r[4] * r[4] + r[7] * r[7];
double zz = r[2] * r[2] + r[5] * r[5] + r[8] * r[8];
double xy = r[0] * r[1] + r[3] * r[4] + r[6] * r[7];
double xz = r[0] * r[2] + r[3] * r[5] + r[6] * r[8];
double yz = r[1] * r[2] + r[4] * r[5] + r[7] * r[8];
double tol = 1e-9 * Math.max(xx, Math.max(yy, zz));
return Math.abs(xx - yy) <= tol
&& Math.abs(xx - zz) <= tol
&& Math.abs(xy) <= tol
&& Math.abs(xz) <= tol
&& Math.abs(yz) <= tol;
}
/** This cube placed by {@code m} (applied after its own map), keeping its render flags. */
public EntityCube transformed(Affine m) {
return new EntityCube(from, to, faces, m.mul(toWorld), additive, unshaded);
}
public EntityCube(double[] from, double[] to, Face[] faces, Affine toWorld, boolean additive, boolean unshaded) { public EntityCube(double[] from, double[] to, Face[] faces, Affine toWorld, boolean additive, boolean unshaded) {
this.additive = additive; this.additive = additive;
this.unshaded = unshaded; this.unshaded = unshaded;
@@ -38,6 +63,9 @@ public final class EntityCube {
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];
this.mirrored = toWorld.determinant() < 0; this.mirrored = toWorld.determinant() < 0;
double[] l = toLocal.r;
this.normalMatrix =
conformal(toWorld.r) ? toWorld.r : new double[] {l[0], l[3], l[6], l[1], l[4], l[7], l[2], l[5], l[8]};
double[] r = toWorld.r, t = toWorld.t; double[] r = toWorld.r, t = toWorld.t;
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];
@@ -124,7 +124,7 @@ public final class EntityIntersector {
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 = h.dir; Direction dir = h.dir;
double[] r = cube.toWorld.r; double[] r = cube.normalMatrix;
Vector normal = new Vector( Vector normal = new Vector(
r[0] * dir.nx + r[1] * dir.ny + r[2] * dir.nz, 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[3] * dir.nx + r[4] * dir.ny + r[5] * dir.nz,
@@ -11,8 +11,8 @@ import java.util.logging.Logger;
/** /**
* All baked entities (mobs, block-entities, decorations, objects) of one render, as one bounding volume hierarchy * All baked entities (mobs, block-entities, decorations, objects) of one render, as one bounding volume hierarchy
* over the cubes' world AABBs (binned SAH, flat arrays), traversed front to back with the best hit as far bound. * over the cubes' world AABBs (binned SAH, flat arrays), traversed front to back with the best hit as far bound.
* Near-ties (coplanar faces of different cubes) are decided in bake order (see {@link #nearestHit}). Immutable after * Near-ties (coplanar faces of different cubes) are decided in bake order, the later cube winning (see
* construction; the per-ray scratch is per thread. * {@link #nearestHit}). Immutable after construction; the per-ray scratch is per thread.
*/ */
public final class EntityScene { public final class EntityScene {
@@ -184,8 +184,9 @@ public final class EntityScene {
if (n == 0) return null; if (n == 0) return null;
// Pass 2, the tie-break: the near candidates in bake order; a cube counts only if its AABB passes with the // Pass 2, the tie-break: the near candidates in bake order; a cube counts only if its AABB passes with the
// best t so far as far bound and it is strictly nearer. Coplanar faces of different cubes (equal t up to // best t so far as far bound and it is not farther. On equal t the later-baked cube wins, like vanilla's
// rounding) depend on this order, so it must not change. // LEQUAL depth test for later-drawn quads (block-model overlay elements in displays/items, layers on the
// body mesh). Coplanar faces of different cubes 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;
@@ -205,7 +206,7 @@ public final class EntityScene {
int k = order[j]; int k = order[j];
double t = tr.cand[k].t; double t = tr.cand[k].t;
double e = entry(cb, candCube[k], ox, oy, oz, ix, iy, iz, px, py, pz, t0, bestT); double e = entry(cb, candCube[k], ox, oy, oz, ix, iy, iz, px, py, pz, t0, bestT);
if (e == e && t < bestT) { if (e == e && t <= bestT) {
bestT = t; bestT = t;
best = k; best = k;
} }
@@ -185,9 +185,7 @@ public final class BlockEntityBaker implements EntityBaker<BlockEntityState> {
.mul(Affine.rotX(Math.toRadians(-30))) .mul(Affine.rotX(Math.toRadians(-30)))
.mul(Affine.scale(scale)) .mul(Affine.scale(scale))
.mul(Affine.translation(-e.x(), -e.y(), -e.z())); .mul(Affine.translation(-e.x(), -e.y(), -e.z()));
for (EntityCube c : mob.cubes) { for (EntityCube c : mob.cubes) cubes.add(c.transformed(m));
cubes.add(new EntityCube(c.from, c.to, c.faces, m.mul(c.toWorld), c.additive, c.unshaded));
}
} }
// BeaconRenderer: sections up to MAX_RENDER_Y (the last one), solid core radius 0.2 rotated by // BeaconRenderer: sections up to MAX_RENDER_Y (the last one), solid core radius 0.2 rotated by
@@ -128,7 +128,7 @@ public final class CemBaker implements EntityBaker<EntityState> {
for (CemGeometry.Baked b : out.glow) { for (CemGeometry.Baked b : out.glow) {
cubes.add(new EntityCube(b.from(), b.to(), b.faces(), place.mul(b.world()), true)); cubes.add(new EntityCube(b.from(), b.to(), b.faces(), place.mul(b.world()), true));
} }
for (EntityCube c : itemCubes) cubes.add(new EntityCube(c.from, c.to, c.faces, place.mul(c.toWorld))); for (EntityCube c : itemCubes) cubes.add(c.transformed(place));
return RenderedEntity.of(cubes); return RenderedEntity.of(cubes);
} }
@@ -886,14 +886,13 @@ public final class CemBaker implements EntityBaker<EntityState> {
// the adult layer scaled, adult UVs). // the adult layer scaled, adult UVs).
boolean babyTextures = baby && !s.typeKey().equals("armor_stand"); boolean babyTextures = baby && !s.typeKey().equals("armor_stand");
String folder = babyTextures ? "humanoid_baby" : "humanoid"; String folder = babyTextures ? "humanoid_baby" : "humanoid";
bakeArmorPiece(eq.head(), set + "head", folder, pose, out); // Vanilla draw order chest, legs, feet, head (coplanar ties: the later piece wins), then the WingsLayer.
if (eq.chest() != null && eq.chest().asset().equals("elytra")) { boolean elytra = eq.chest() != null && eq.chest().asset().equals("elytra");
bakeElytra(eq.chest(), base, baby, pose, out); if (!elytra) bakeArmorPiece(eq.chest(), set + "chest", folder, pose, out);
} else {
bakeArmorPiece(eq.chest(), set + "chest", folder, pose, out);
}
bakeArmorPiece(eq.legs(), set + "legs", babyTextures ? "humanoid_baby" : "humanoid_leggings", pose, out); bakeArmorPiece(eq.legs(), set + "legs", babyTextures ? "humanoid_baby" : "humanoid_leggings", pose, out);
bakeArmorPiece(eq.feet(), set + "feet", folder, pose, out); bakeArmorPiece(eq.feet(), set + "feet", folder, pose, out);
bakeArmorPiece(eq.head(), set + "head", folder, pose, out);
if (elytra) bakeElytra(eq.chest(), base, baby, pose, out);
} }
private void bakeArmorPiece( private void bakeArmorPiece(
@@ -395,7 +395,7 @@ public final class CemModelLoader {
new PartPose( new PartPose(
po.has("pivot") ? arr3(po, "pivot") : null, po.has("pivot") ? arr3(po, "pivot") : null,
po.has("rotate") ? arr3(po, "rotate") : null, po.has("rotate") ? arr3(po, "rotate") : null,
!po.has("visible") || po.get("visible").getAsBoolean(), po.has("visible") ? po.get("visible").getAsBoolean() : null,
po.has("scale") ? arr3(po, "scale") : null, po.has("scale") ? arr3(po, "scale") : null,
null)); null));
} }
@@ -151,7 +151,7 @@ public final class ItemGeometry {
if (state == null || blockEntities == null) return; if (state == null || blockEntities == null) return;
RenderedEntity r = blockEntities.bake(state); RenderedEntity r = blockEntities.bake(state);
if (r == null) return; if (r == null) return;
for (EntityCube c : r.cubes) out.add(new EntityCube(c.from, c.to, c.faces, toWorld.mul(c.toWorld))); for (EntityCube c : r.cubes) out.add(c.transformed(toWorld));
} }
/** A block entity's bare models (no block pose) mapped by {@code toWorld}. */ /** A block entity's bare models (no block pose) mapped by {@code toWorld}. */
@@ -1,5 +1,6 @@
package eu.mhsl.minecraft.pixelpics.render.snapshot; package eu.mhsl.minecraft.pixelpics.render.snapshot;
import eu.mhsl.minecraft.pixelpics.assets.BlockEntityBlocks;
import eu.mhsl.minecraft.pixelpics.render.entity.BlockEntityState; import eu.mhsl.minecraft.pixelpics.render.entity.BlockEntityState;
import eu.mhsl.minecraft.pixelpics.render.entity.BlockEntityState.BannerPattern; import eu.mhsl.minecraft.pixelpics.render.entity.BlockEntityState.BannerPattern;
import eu.mhsl.minecraft.pixelpics.render.entity.BlockEntityState.ChestKind; import eu.mhsl.minecraft.pixelpics.render.entity.BlockEntityState.ChestKind;
@@ -169,7 +170,7 @@ public final class BlockEntitySnapshotBuilder {
Material mat = data.getMaterial(); Material mat = data.getMaterial();
String n = mat.name(); String n = mat.name();
Kind chest = n.endsWith("COPPER_CHEST") Kind chest = BlockEntityBlocks.isCopperChest(mat)
? Kind.COPPER_CHEST ? Kind.COPPER_CHEST
: switch (mat) { : switch (mat) {
case CHEST -> Kind.CHEST; case CHEST -> Kind.CHEST;
@@ -185,7 +186,7 @@ public final class BlockEntitySnapshotBuilder {
return b.build(); return b.build();
} }
if (n.endsWith("SHULKER_BOX")) { if (BlockEntityBlocks.isShulkerBox(mat)) {
String color = String color =
n.equals("SHULKER_BOX") ? null : prefix(n, "_SHULKER_BOX").toLowerCase(Locale.ROOT); n.equals("SHULKER_BOX") ? null : prefix(n, "_SHULKER_BOX").toLowerCase(Locale.ROOT);
String dir = data instanceof Directional d ? d.getFacing().name().toLowerCase(Locale.ROOT) : "up"; String dir = data instanceof Directional d ? d.getFacing().name().toLowerCase(Locale.ROOT) : "up";
@@ -195,8 +196,8 @@ public final class BlockEntitySnapshotBuilder {
.build(); .build();
} }
if (n.endsWith("_BANNER")) { if (BlockEntityBlocks.isBanner(mat)) {
boolean wall = n.endsWith("_WALL_BANNER"); boolean wall = BlockEntityBlocks.isWallBanner(mat);
Builder b = base(wall ? Kind.WALL_BANNER : Kind.BANNER, bx, by, bz, facingYaw(data)); Builder b = base(wall ? Kind.WALL_BANNER : Kind.BANNER, bx, by, bz, facingYaw(data));
if (ts instanceof Banner banner) { if (ts instanceof Banner banner) {
b.baseColorArgb(ColorUtil.dyeArgb(banner.getBaseColor(), 0xFFFFFFFF)); b.baseColorArgb(ColorUtil.dyeArgb(banner.getBaseColor(), 0xFFFFFFFF));
@@ -208,10 +209,10 @@ public final class BlockEntitySnapshotBuilder {
return b.build(); return b.build();
} }
if (n.endsWith("_SIGN")) { if (BlockEntityBlocks.isSign(mat)) {
Kind kind = n.endsWith("_WALL_SIGN") Kind kind = BlockEntityBlocks.isWallSign(mat)
? Kind.WALL_SIGN ? Kind.WALL_SIGN
: n.endsWith("_HANGING_SIGN") ? Kind.HANGING_SIGN : Kind.SIGN; : BlockEntityBlocks.isHangingSign(mat) ? Kind.HANGING_SIGN : Kind.SIGN;
Builder b = base(kind, bx, by, bz, facingYaw(data)); Builder b = base(kind, bx, by, bz, facingYaw(data));
if (ts instanceof Sign sign) { if (ts instanceof Sign sign) {
b.frontText(signText(sign.getSide(Side.FRONT))); b.frontText(signText(sign.getSide(Side.FRONT)));
@@ -220,8 +221,8 @@ public final class BlockEntitySnapshotBuilder {
return b.build(); return b.build();
} }
if (n.endsWith("_SKULL") || n.endsWith("_HEAD")) { if (BlockEntityBlocks.isHead(mat)) {
boolean wall = n.contains("_WALL_"); boolean wall = BlockEntityBlocks.isWallHead(mat);
String headType = headType(n); String headType = headType(n);
Builder b = base(wall ? Kind.WALL_HEAD : Kind.HEAD, bx, by, bz, facingYaw(data)) Builder b = base(wall ? Kind.WALL_HEAD : Kind.HEAD, bx, by, bz, facingYaw(data))
.headType(headType); .headType(headType);
@@ -35,7 +35,6 @@ import org.bukkit.attribute.AttributeInstance;
import org.bukkit.block.BlockFace; import org.bukkit.block.BlockFace;
import org.bukkit.block.data.type.Bed; import org.bukkit.block.data.type.Bed;
import org.bukkit.entity.AbstractArrow; import org.bukkit.entity.AbstractArrow;
import org.bukkit.entity.AbstractHorse;
import org.bukkit.entity.Ageable; import org.bukkit.entity.Ageable;
import org.bukkit.entity.ArmorStand; import org.bukkit.entity.ArmorStand;
import org.bukkit.entity.Arrow; import org.bukkit.entity.Arrow;
@@ -213,17 +212,11 @@ public final class EntitySnapshotBuilder {
} else if (e instanceof Horse h) { } else if (e instanceof Horse h) {
variant = keyOf(h.getColor()); variant = keyOf(h.getColor());
markings = markingsKey(h.getStyle()); markings = markingsKey(h.getStyle());
saddle = isSaddled(h);
bodyEquip = horseArmorKey(h);
} else if (e instanceof Llama l) { } else if (e instanceof Llama l) {
variant = keyOf(l.getColor()); variant = keyOf(l.getColor());
chest = l.isCarryingChest(); chest = l.isCarryingChest();
bodyEquip = (e instanceof TraderLlama) ? "trader_llama" : carpetKey(l);
} else if (e instanceof ChestedHorse ch) { } else if (e instanceof ChestedHorse ch) {
chest = ch.isCarryingChest(); chest = ch.isCarryingChest();
saddle = isSaddled(ch);
} else if (e instanceof AbstractHorse ah) {
saddle = isSaddled(ah);
} else if (e instanceof Fox f) { } else if (e instanceof Fox f) {
variant = keyOf(f.getFoxType()); variant = keyOf(f.getFoxType());
} else if (e instanceof MushroomCow mc) { } else if (e instanceof MushroomCow mc) {
@@ -257,12 +250,14 @@ public final class EntitySnapshotBuilder {
ItemStack body = eq.getItem(EquipmentSlot.BODY); ItemStack body = eq.getItem(EquipmentSlot.BODY);
String asset = equipAsset(body); String asset = equipAsset(body);
if (asset != null) { if (asset != null) {
if (bodyEquip == null) bodyEquip = asset; bodyEquip = asset;
bodyDye = dyeOf(body); bodyDye = dyeOf(body);
} }
} catch (Throwable ignored) { } catch (Throwable ignored) {
// slot not supported by this entity // slot not supported by this entity
} }
// LlamaDecorLayer: the trader llama's own decor unless it wears a carpet.
if (bodyEquip == null && e instanceof TraderLlama) bodyEquip = "trader_llama";
try { try {
boolean leftHanded = (e instanceof Mob mob && mob.isLeftHanded()) boolean leftHanded = (e instanceof Mob mob && mob.isLeftHanded())
|| (e instanceof HumanEntity he && he.getMainHand() == MainHand.LEFT); || (e instanceof HumanEntity he && he.getMainHand() == MainHand.LEFT);
@@ -275,7 +270,7 @@ public final class EntitySnapshotBuilder {
leftHand = leftHanded ? main : off; leftHand = leftHanded ? main : off;
} }
ItemStack helmet = eq.getHelmet(); ItemStack helmet = eq.getHelmet();
if (helmet != null && !helmet.getType().isAir() && armorAsset(helmet.getType()) == null) { if (helmet != null && !helmet.getType().isAir() && armorAsset(helmet, EquipmentSlot.HEAD) == null) {
headItem = ItemSpecs.of(helmet); headItem = ItemSpecs.of(helmet);
} }
// Snow golems wear a carved pumpkin unless sheared (vanilla SnowGolemHeadLayer). // Snow golems wear a carved pumpkin unless sheared (vanilla SnowGolemHeadLayer).
@@ -603,8 +598,8 @@ public final class EntitySnapshotBuilder {
private static EntityState.Equipment captureEquipment(EntityEquipment eq) { private static EntityState.Equipment captureEquipment(EntityEquipment eq) {
try { try {
EntityState.Equipment equip = new EntityState.Equipment( EntityState.Equipment equip = new EntityState.Equipment(
armorPiece(eq.getHelmet()), armorPiece(eq.getChestplate()), armorPiece(eq.getHelmet(), EquipmentSlot.HEAD), armorPiece(eq.getChestplate(), EquipmentSlot.CHEST),
armorPiece(eq.getLeggings()), armorPiece(eq.getBoots())); armorPiece(eq.getLeggings(), EquipmentSlot.LEGS), armorPiece(eq.getBoots(), EquipmentSlot.FEET));
return equip.isEmpty() ? null : equip; return equip.isEmpty() ? null : equip;
} catch (Throwable t) { } catch (Throwable t) {
return null; return null;
@@ -612,9 +607,9 @@ public final class EntitySnapshotBuilder {
} }
/** Null for empty / non-armor items. */ /** Null for empty / non-armor items. */
private static EntityState.EquipPiece armorPiece(ItemStack it) { private static EntityState.EquipPiece armorPiece(ItemStack it, EquipmentSlot slot) {
if (it == null || it.getType().isAir()) return null; if (it == null || it.getType().isAir()) return null;
String asset = armorAsset(it.getType()); String asset = armorAsset(it, slot);
if (asset == null) return null; // not a humanoid-armor item (e.g. a mob head / pumpkin) if (asset == null) return null; // not a humanoid-armor item (e.g. a mob head / pumpkin)
int dye = 0; int dye = 0;
String trimMat = null, trimPat = null; String trimMat = null, trimPat = null;
@@ -633,8 +628,25 @@ public final class EntitySnapshotBuilder {
private static final String[] ARMOR_SUFFIXES = {"_helmet", "_chestplate", "_leggings", "_boots"}; private static final String[] ARMOR_SUFFIXES = {"_helmet", "_chestplate", "_leggings", "_boots"};
/** Item Material -> equipment asset id (= texture name): strips the slot suffix; null if not armor. */ /**
private static String armorAsset(Material m) { * Equipment asset id (= texture name) a humanoid armor layer draws for the item worn in {@code slot}: the
* EQUIPPABLE asset when its slot matches (vanilla HumanoidArmorLayer.shouldRender, custom armor included);
* from the item name without the component. Null if none.
*/
private static String armorAsset(ItemStack it, EquipmentSlot slot) {
try {
var comp = it.getData(DataComponentTypes.EQUIPPABLE);
if (comp != null)
return comp.slot() == slot && comp.assetId() != null
? comp.assetId().value()
: null;
} catch (Throwable ignored) {
}
return armorAssetByName(it.getType());
}
/** Item Material -> equipment asset id: strips the slot suffix; null if not armor. */
private static String armorAssetByName(Material m) {
String key = m.getKey().getKey(); String key = m.getKey().getKey();
if (key.equals("elytra")) return "elytra"; if (key.equals("elytra")) return "elytra";
if (key.equals("turtle_helmet")) return "turtle_scute"; if (key.equals("turtle_helmet")) return "turtle_scute";
@@ -655,21 +667,6 @@ public final class EntitySnapshotBuilder {
return style.name().toLowerCase(Locale.ROOT).replace("_", ""); // WHITE_DOTS -> whitedots return style.name().toLowerCase(Locale.ROOT).replace("_", ""); // WHITE_DOTS -> whitedots
} }
/** Horse armor material -> equipment/horse_body texture key (golden uses the "gold" file); null if none. */
private static String horseArmorKey(Horse h) {
ItemStack a = h.getInventory().getArmor();
if (a == null || a.getType().isAir()) return null;
return assetMaterial(a.getType().getKey().getKey().replace("_horse_armor", ""));
}
/** Llama carpet decor -> equipment/llama_body colour key; null if none. */
private static String carpetKey(Llama l) {
ItemStack d = l.getInventory().getDecor();
if (d == null || d.getType().isAir()) return null;
String k = d.getType().getKey().getKey();
return k.endsWith("_carpet") ? k.substring(0, k.length() - "_carpet".length()) : null;
}
/** Equipment asset id (= equipment/<asset> texture name) from an item's EQUIPPABLE component; null if none. */ /** Equipment asset id (= equipment/<asset> texture name) from an item's EQUIPPABLE component; null if none. */
private static String equipAsset(ItemStack it) { private static String equipAsset(ItemStack it) {
if (it == null || it.getType().isAir()) return null; if (it == null || it.getType().isAir()) return null;
@@ -723,11 +720,6 @@ public final class EntitySnapshotBuilder {
} }
} }
private static boolean isSaddled(AbstractHorse h) {
ItemStack st = h.getInventory().getSaddle();
return st != null && !st.getType().isAir();
}
/** /**
* Registry/Keyed values yield their key path ({@code "<ns>:<path>"} outside the minecraft namespace, e.g. * Registry/Keyed values yield their key path ({@code "<ns>:<path>"} outside the minecraft namespace, e.g.
* datapack variants); plain enums yield their lower-case name. * datapack variants); plain enums yield their lower-case name.
@@ -1,68 +1,76 @@
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.Material;
import org.bukkit.block.data.Ageable;
import org.bukkit.block.data.BlockData; import org.bukkit.block.data.BlockData;
import org.bukkit.block.data.type.RedstoneWire; 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 (block + tintindex) to its in-world tint colour like vanilla BlockColors.createDefault (26.3):
* a biome channel or a constant. Blocks without a registered tint source (bamboo, cherry and pale oak leaves, the
* lava/powder-snow cauldron, the stonecutter, …) and tint layers beyond the registered ones are not tinted.
*/
public final class TintResolver { public final class TintResolver {
private static final int BIRCH = 0xFF80A755;
private static final int SPRUCE = 0xFF619961; private static final int SPRUCE = 0xFF619961;
private static final int LILY_PAD = 0xFF208030; private static final int BIRCH = 0xFF80A755;
private static final int STEM = 0xFF60A017; private static final int LILY_PAD_IN_WORLD = 0xFF208030;
private static final int ATTACHED_STEM = 0xFFE0C71C;
private enum Channel { /** RedstoneWireBlock.COLORS: the wire colour per power level, with vanilla's float arithmetic. */
BIRCH, private static final int[] REDSTONE = new int[16];
SPRUCE,
FOLIAGE, static {
LILY_PAD, for (int i = 0; i <= 15; i++) {
WATER, float power = i / 15.0F;
REDSTONE, float red = power * 0.6F + (power > 0.0F ? 0.4F : 0.3F);
STEM, float green = clamp(power * power * 0.7F - 0.5F);
GRASS float blue = clamp(power * power * 0.6F - 0.7F);
REDSTONE[i] = 0xFF000000 | channel8(red) << 16 | channel8(green) << 8 | channel8(blue);
}
} }
// 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; Material m = data.getMaterial();
Material material = data.getMaterial(); // Pink petals and wildflowers: layer 0 (the flowers) is blank, layer 1 (the stems) grass.
Channel channel = CHANNELS[material.ordinal()]; if (m == Material.PINK_PETALS || m == Material.WILDFLOWERS) return tintIndex == 1 ? biomeTint.grass() : -1;
if (channel == null) if (tintIndex != 0) return -1; // every other source has one layer
CHANNELS[material.ordinal()] = channel = channel(material.name().toLowerCase(Locale.ROOT)); return switch (m) {
return switch (channel) { case GRASS_BLOCK, SHORT_GRASS, FERN, POTTED_FERN -> biomeTint.grass();
case BIRCH -> BIRCH; case BUSH, TALL_GRASS, LARGE_FERN, SUGAR_CANE -> biomeTint.grass();
case SPRUCE -> SPRUCE; case OAK_LEAVES, JUNGLE_LEAVES, ACACIA_LEAVES -> biomeTint.foliage();
case FOLIAGE -> biomeTint.foliage(); case DARK_OAK_LEAVES, MANGROVE_LEAVES, VINE -> biomeTint.foliage();
case LILY_PAD -> LILY_PAD; case SPRUCE_LEAVES -> SPRUCE;
case WATER -> biomeTint.water(); case BIRCH_LEAVES -> BIRCH;
case REDSTONE -> redstone(data); case LEAF_LITTER -> biomeTint.dryFoliage();
case STEM -> STEM; case WATER_CAULDRON -> biomeTint.water();
case GRASS -> biomeTint.grass(); case REDSTONE_WIRE -> redstone(data);
case MELON_STEM, PUMPKIN_STEM -> stem(data);
case ATTACHED_MELON_STEM, ATTACHED_PUMPKIN_STEM -> ATTACHED_STEM;
case LILY_PAD -> LILY_PAD_IN_WORLD;
default -> -1;
}; };
} }
private static Channel channel(String name) { private static int redstone(BlockData data) {
if (name.equals("birch_leaves")) return Channel.BIRCH; return REDSTONE[data instanceof RedstoneWire wire ? Math.clamp(wire.getPower(), 0, 15) : 0];
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. */ /** BlockTintSources.stem: {@code ARGB.color(age·32, 255 - age·8, age·4)}. */
private static int redstone(BlockData data) { private static int stem(BlockData data) {
int power = data instanceof RedstoneWire wire ? wire.getPower() : 0; int age = data instanceof Ageable a ? a.getAge() : 0;
return 0xFF000000 | (Math.min(255, 75 + power * 12) << 16); return 0xFF000000 | age * 32 << 16 | (255 - age * 8) << 8 | age * 4;
}
private static float clamp(float v) {
return v < 0.0F ? 0.0F : Math.min(v, 1.0F);
}
/** ARGB.as8BitChannel: {@code floor(v·255)}. */
private static int channel8(float v) {
return (int) Math.floor(v * 255.0F);
} }
} }
+92 -5
View File
@@ -275,6 +275,97 @@ public class PPTest extends JavaPlugin {
scene("liq_under_warm_top", o -> liqUnder(o, Biome.WARM_OCEAN), -3, 4, -10, 6, -6, 2); scene("liq_under_warm_top", o -> liqUnder(o, Biome.WARM_OCEAN), -3, 4, -10, 6, -6, 2);
scene("liq_lava", this::liqLava, 4, 4.5, -6, 9, 0, 1); scene("liq_lava", this::liqLava, 4, 4.5, -6, 9, 0, 1);
scene("liq_lava_in", this::liqLava, 3.5, -0.5, 1.5, 9, -0.5, 1.5); scene("liq_lava_in", this::liqLava, 3.5, -0.5, 1.5, 9, -0.5, 1.5);
// Block tints like vanilla BlockColors: untinted tintindex blocks (cherry/pale oak leaves, bamboo,
// stonecutter, lava/powder-snow cauldron), leaf litter (dry foliage), petals/wildflowers (stems only), melon
// stems age 0-7 + attached, redstone wire power 0-15 (RedStoneWireBlock.COLORS).
scene("tint_blocks", this::tintBlocks, 7.5, 6, -4.5, 7.5, 0, 2.5);
scene("tint_blocks_close", this::tintBlocks, 4, 2.6, -1.6, 4, 0, 1);
// Equipment from the EQUIPPABLE asset: custom-asset armor/horse armor, elytra on the head (an item, not
// armor), carpets on llamas and on a trader llama, saddles on horse/donkey/pig.
scene("equip_assets", this::equipAssets, 5, 2.8, -5.5, 5, 0.9, 1.5);
// Block displays: end portal/gateway (unshaded), piston head (no skull), a non-uniformly scaled + rotated
// log (normals), grass block (side overlay); shulkers without and with colour.
scene("display_fixes", this::displayFixes, 4.5, 3.2, -5, 4.5, 0.8, 1.5);
}
private void tintBlocks(Location o) {
String[] row0 = {"grass_block", "oak_leaves[persistent=true]", "cherry_leaves[persistent=true]",
"pale_oak_leaves[persistent=true]", "bamboo[age=0,leaves=small,stage=0]", "stonecutter[facing=north]",
"lava_cauldron", "powder_snow_cauldron[level=3]", "water_cauldron[level=3]",
"leaf_litter[facing=north,segment_amount=4]", "pink_petals[facing=north,flower_amount=4]",
"wildflowers[facing=north,flower_amount=4]", "fern", "sugar_cane", "vine[north=false,south=true]"};
for (int i = 0; i < row0.length; i++) put(o, i, 0, 0, row0[i]);
for (int a = 0; a < 8; a++) put(o, a, 0, 2, "melon_stem[age=" + a + "]");
put(o, 8, 0, 2, "attached_melon_stem[facing=south]");
put(o, 9, 0, 2, "pumpkin_stem[age=7]");
put(o, 10, 0, 2, "attached_pumpkin_stem[facing=north]");
for (int p = 0; p < 16; p++)
put(o, p, 0, 4, "redstone_wire[power=" + p + ",north=side,south=side,east=none,west=none]");
}
private static ItemStack withAsset(Material m, EquipmentSlot slot, String asset) {
ItemStack s = it(m);
s.setData(io.papermc.paper.datacomponent.DataComponentTypes.EQUIPPABLE,
io.papermc.paper.datacomponent.item.Equippable.equippable(slot)
.assetId(net.kyori.adventure.key.Key.key("minecraft", asset))
.build());
return s;
}
private void equipAssets(Location o) {
Zombie z = rfSpawn(o, 0, 0, 0, 180, Zombie.class);
z.getEquipment().setChestplate(withAsset(Material.DIAMOND_CHESTPLATE, EquipmentSlot.CHEST, "gold"));
z.getEquipment().setLeggings(withAsset(Material.LEATHER_LEGGINGS, EquipmentSlot.LEGS, "netherite"));
Zombie z2 = rfSpawn(o, 1.3, 0, 0, 180, Zombie.class);
z2.getEquipment().setHelmet(it(Material.ELYTRA));
z2.getEquipment().setChestplate(it(Material.IRON_CHESTPLATE));
Horse h = rfSpawn(o, 3, 0, 0, 180, Horse.class);
h.setColor(Horse.Color.WHITE);
h.setStyle(Horse.Style.NONE);
h.getInventory().setArmor(withAsset(Material.IRON_HORSE_ARMOR, EquipmentSlot.BODY, "diamond"));
h.getInventory().setSaddle(it(Material.SADDLE));
Horse h2 = rfSpawn(o, 5, 0, 0, 180, Horse.class);
h2.setColor(Horse.Color.BROWN);
h2.setStyle(Horse.Style.NONE);
h2.getInventory().setArmor(it(Material.GOLDEN_HORSE_ARMOR));
h2.getInventory().setSaddle(it(Material.SADDLE));
Donkey d = rfSpawn(o, 7, 0, 0, 180, Donkey.class);
d.getInventory().setSaddle(it(Material.SADDLE));
Pig pig = rfSpawn(o, 8.6, 0, 0, 180, Pig.class);
pig.getEquipment().setItem(EquipmentSlot.SADDLE, it(Material.SADDLE));
Llama l = rfSpawn(o, 0.5, 0, 2.5, 180, Llama.class);
l.setColor(Llama.Color.CREAMY);
l.getInventory().setDecor(it(Material.WHITE_CARPET));
Llama l2 = rfSpawn(o, 2.5, 0, 2.5, 180, Llama.class);
l2.setColor(Llama.Color.GRAY);
l2.getInventory().setDecor(it(Material.LIME_CARPET));
TraderLlama t = rfSpawn(o, 4.5, 0, 2.5, 180, TraderLlama.class);
t.setColor(Llama.Color.CREAMY);
t.getInventory().setDecor(it(Material.RED_CARPET));
TraderLlama t2 = rfSpawn(o, 6.5, 0, 2.5, 180, TraderLlama.class);
t2.setColor(Llama.Color.BROWN);
faceNorth(o);
}
private void displayFixes(Location o) {
String[] blocks = {"end_portal", "end_gateway", "piston_head[facing=up,short=false,type=normal]",
"piston_head[facing=north,short=false,type=sticky]", "grass_block[snowy=false]"};
for (int i = 0; i < blocks.length; i++) {
BlockDisplay b = spawn(o, i * 1.6, 0, 0, 0, BlockDisplay.class);
b.setBlock(Bukkit.createBlockData("minecraft:" + blocks[i]));
}
// Non-uniform scale + rotation: the normals need the inverse-transpose (face shading).
Quaternionf rot = new Quaternionf().rotateY((float) Math.toRadians(35)).rotateX((float) Math.toRadians(25));
BlockDisplay n1 = spawn(o, 0.5, 0.2, 2.5, 0, BlockDisplay.class);
n1.setBlock(Bukkit.createBlockData("minecraft:crafting_table"));
n1.setTransformation(new Transformation(new Vector3f(), rot, new Vector3f(2f, 0.6f, 0.4f), new Quaternionf()));
BlockDisplay n2 = spawn(o, 4, 0.2, 2.5, 0, BlockDisplay.class);
n2.setBlock(Bukkit.createBlockData("minecraft:crafting_table"));
n2.setTransformation(new Transformation(new Vector3f(), rot, new Vector3f(0.8f), new Quaternionf()));
Shulker s1 = rfSpawn(o, 6.5, 0, 2.5, 180, Shulker.class);
s1.setColor(null);
Shulker s2 = rfSpawn(o, 8, 0, 2.5, 180, Shulker.class);
s2.setColor(DyeColor.PURPLE);
} }
private static ItemStack shield(DyeColor base, Object... patterns) { private static ItemStack shield(DyeColor base, Object... patterns) {
@@ -1491,11 +1582,7 @@ public class PPTest extends JavaPlugin {
} }
private Entity rfSpawn(Location o, double x, double y, double z, float yaw, EntityType type) { private Entity rfSpawn(Location o, double x, double y, double z, float yaw, EntityType type) {
Location l = o.clone().add(x, y, z); return rfSpawn(o, x, y, z, yaw, type.getEntityClass());
l.setYaw(yaw);
Entity e = w.spawnEntity(l, type, false);
if (e instanceof Mob m) m.setAI(false);
return e;
} }
private void faceNorth(Location o) { private void faceNorth(Location o) {