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:
1 parent
4be03afa3f
commit
291d51891f
14 files changed
+322
-141
No files matched your search
+1
-1
@@ -169,4 +169,4 @@ gradle-app.setting
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/java,intellij,gradle
|
||||
|
||||
/local.gradle
|
||||
/local.gradle
|
||||
@@ -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.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 TextureCache textures;
|
||||
private final BlockStateResolver blockStateResolver;
|
||||
@@ -119,7 +92,7 @@ public final class BlockModelRegistry {
|
||||
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.
|
||||
if (BLOCK_ENTITY_MATERIALS.contains(material)) return ResolvedModel.EMPTY;
|
||||
if (BlockEntityBlocks.hasNoBlockModel(material)) return ResolvedModel.EMPTY;
|
||||
FlatModel last = variants.isEmpty()
|
||||
? null
|
||||
: 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
|
||||
// and are back-face culled like vanilla quads (see EntityIntersector); mirrored = toWorld flips handedness.
|
||||
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[] 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);
|
||||
}
|
||||
|
||||
/** 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) {
|
||||
this.additive = additive;
|
||||
this.unshaded = unshaded;
|
||||
@@ -38,6 +63,9 @@ public final class EntityCube {
|
||||
this.toLocal = toWorld.inverse();
|
||||
this.inverted = from[0] > to[0] || from[1] > to[1] || from[2] > to[2];
|
||||
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;
|
||||
for (int i = 0; i < 8; i++) {
|
||||
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) {
|
||||
double t = h.t;
|
||||
Direction dir = h.dir;
|
||||
double[] r = cube.toWorld.r;
|
||||
double[] r = cube.normalMatrix;
|
||||
Vector normal = new Vector(
|
||||
r[0] * dir.nx + r[1] * dir.ny + r[2] * 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
|
||||
* 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
|
||||
* construction; the per-ray scratch is per thread.
|
||||
* Near-ties (coplanar faces of different cubes) are decided in bake order, the later cube winning (see
|
||||
* {@link #nearestHit}). Immutable after construction; the per-ray scratch is per thread.
|
||||
*/
|
||||
public final class EntityScene {
|
||||
|
||||
@@ -184,8 +184,9 @@ public final class EntityScene {
|
||||
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
|
||||
// best t so far as far bound and it is strictly nearer. Coplanar faces of different cubes (equal t up to
|
||||
// rounding) depend on this order, so it must not change.
|
||||
// best t so far as far bound and it is not farther. On equal t the later-baked cube wins, like vanilla's
|
||||
// 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[] order = tr.order;
|
||||
int m = 0;
|
||||
@@ -205,7 +206,7 @@ public final class EntityScene {
|
||||
int k = order[j];
|
||||
double t = tr.cand[k].t;
|
||||
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;
|
||||
best = k;
|
||||
}
|
||||
|
||||
@@ -185,9 +185,7 @@ public final class BlockEntityBaker implements EntityBaker<BlockEntityState> {
|
||||
.mul(Affine.rotX(Math.toRadians(-30)))
|
||||
.mul(Affine.scale(scale))
|
||||
.mul(Affine.translation(-e.x(), -e.y(), -e.z()));
|
||||
for (EntityCube c : mob.cubes) {
|
||||
cubes.add(new EntityCube(c.from, c.to, c.faces, m.mul(c.toWorld), c.additive, c.unshaded));
|
||||
}
|
||||
for (EntityCube c : mob.cubes) cubes.add(c.transformed(m));
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -886,14 +886,13 @@ public final class CemBaker implements EntityBaker<EntityState> {
|
||||
// the adult layer scaled, adult UVs).
|
||||
boolean babyTextures = baby && !s.typeKey().equals("armor_stand");
|
||||
String folder = babyTextures ? "humanoid_baby" : "humanoid";
|
||||
bakeArmorPiece(eq.head(), set + "head", folder, pose, out);
|
||||
if (eq.chest() != null && eq.chest().asset().equals("elytra")) {
|
||||
bakeElytra(eq.chest(), base, baby, pose, out);
|
||||
} else {
|
||||
bakeArmorPiece(eq.chest(), set + "chest", folder, pose, out);
|
||||
}
|
||||
// Vanilla draw order chest, legs, feet, head (coplanar ties: the later piece wins), then the WingsLayer.
|
||||
boolean elytra = eq.chest() != null && eq.chest().asset().equals("elytra");
|
||||
if (!elytra) bakeArmorPiece(eq.chest(), set + "chest", folder, pose, out);
|
||||
bakeArmorPiece(eq.legs(), set + "legs", babyTextures ? "humanoid_baby" : "humanoid_leggings", 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(
|
||||
|
||||
@@ -395,7 +395,7 @@ public final class CemModelLoader {
|
||||
new PartPose(
|
||||
po.has("pivot") ? arr3(po, "pivot") : 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,
|
||||
null));
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ public final class ItemGeometry {
|
||||
if (state == null || blockEntities == null) return;
|
||||
RenderedEntity r = blockEntities.bake(state);
|
||||
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}. */
|
||||
|
||||
+10
-9
@@ -1,5 +1,6 @@
|
||||
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.BannerPattern;
|
||||
import eu.mhsl.minecraft.pixelpics.render.entity.BlockEntityState.ChestKind;
|
||||
@@ -169,7 +170,7 @@ public final class BlockEntitySnapshotBuilder {
|
||||
Material mat = data.getMaterial();
|
||||
String n = mat.name();
|
||||
|
||||
Kind chest = n.endsWith("COPPER_CHEST")
|
||||
Kind chest = BlockEntityBlocks.isCopperChest(mat)
|
||||
? Kind.COPPER_CHEST
|
||||
: switch (mat) {
|
||||
case CHEST -> Kind.CHEST;
|
||||
@@ -185,7 +186,7 @@ public final class BlockEntitySnapshotBuilder {
|
||||
return b.build();
|
||||
}
|
||||
|
||||
if (n.endsWith("SHULKER_BOX")) {
|
||||
if (BlockEntityBlocks.isShulkerBox(mat)) {
|
||||
String color =
|
||||
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";
|
||||
@@ -195,8 +196,8 @@ public final class BlockEntitySnapshotBuilder {
|
||||
.build();
|
||||
}
|
||||
|
||||
if (n.endsWith("_BANNER")) {
|
||||
boolean wall = n.endsWith("_WALL_BANNER");
|
||||
if (BlockEntityBlocks.isBanner(mat)) {
|
||||
boolean wall = BlockEntityBlocks.isWallBanner(mat);
|
||||
Builder b = base(wall ? Kind.WALL_BANNER : Kind.BANNER, bx, by, bz, facingYaw(data));
|
||||
if (ts instanceof Banner banner) {
|
||||
b.baseColorArgb(ColorUtil.dyeArgb(banner.getBaseColor(), 0xFFFFFFFF));
|
||||
@@ -208,10 +209,10 @@ public final class BlockEntitySnapshotBuilder {
|
||||
return b.build();
|
||||
}
|
||||
|
||||
if (n.endsWith("_SIGN")) {
|
||||
Kind kind = n.endsWith("_WALL_SIGN")
|
||||
if (BlockEntityBlocks.isSign(mat)) {
|
||||
Kind kind = BlockEntityBlocks.isWallSign(mat)
|
||||
? 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));
|
||||
if (ts instanceof Sign sign) {
|
||||
b.frontText(signText(sign.getSide(Side.FRONT)));
|
||||
@@ -220,8 +221,8 @@ public final class BlockEntitySnapshotBuilder {
|
||||
return b.build();
|
||||
}
|
||||
|
||||
if (n.endsWith("_SKULL") || n.endsWith("_HEAD")) {
|
||||
boolean wall = n.contains("_WALL_");
|
||||
if (BlockEntityBlocks.isHead(mat)) {
|
||||
boolean wall = BlockEntityBlocks.isWallHead(mat);
|
||||
String headType = headType(n);
|
||||
Builder b = base(wall ? Kind.WALL_HEAD : Kind.HEAD, bx, by, bz, facingYaw(data))
|
||||
.headType(headType);
|
||||
|
||||
+27
-35
@@ -35,7 +35,6 @@ import org.bukkit.attribute.AttributeInstance;
|
||||
import org.bukkit.block.BlockFace;
|
||||
import org.bukkit.block.data.type.Bed;
|
||||
import org.bukkit.entity.AbstractArrow;
|
||||
import org.bukkit.entity.AbstractHorse;
|
||||
import org.bukkit.entity.Ageable;
|
||||
import org.bukkit.entity.ArmorStand;
|
||||
import org.bukkit.entity.Arrow;
|
||||
@@ -213,17 +212,11 @@ public final class EntitySnapshotBuilder {
|
||||
} else if (e instanceof Horse h) {
|
||||
variant = keyOf(h.getColor());
|
||||
markings = markingsKey(h.getStyle());
|
||||
saddle = isSaddled(h);
|
||||
bodyEquip = horseArmorKey(h);
|
||||
} else if (e instanceof Llama l) {
|
||||
variant = keyOf(l.getColor());
|
||||
chest = l.isCarryingChest();
|
||||
bodyEquip = (e instanceof TraderLlama) ? "trader_llama" : carpetKey(l);
|
||||
} else if (e instanceof ChestedHorse ch) {
|
||||
chest = ch.isCarryingChest();
|
||||
saddle = isSaddled(ch);
|
||||
} else if (e instanceof AbstractHorse ah) {
|
||||
saddle = isSaddled(ah);
|
||||
} else if (e instanceof Fox f) {
|
||||
variant = keyOf(f.getFoxType());
|
||||
} else if (e instanceof MushroomCow mc) {
|
||||
@@ -257,12 +250,14 @@ public final class EntitySnapshotBuilder {
|
||||
ItemStack body = eq.getItem(EquipmentSlot.BODY);
|
||||
String asset = equipAsset(body);
|
||||
if (asset != null) {
|
||||
if (bodyEquip == null) bodyEquip = asset;
|
||||
bodyEquip = asset;
|
||||
bodyDye = dyeOf(body);
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
// 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 {
|
||||
boolean leftHanded = (e instanceof Mob mob && mob.isLeftHanded())
|
||||
|| (e instanceof HumanEntity he && he.getMainHand() == MainHand.LEFT);
|
||||
@@ -275,7 +270,7 @@ public final class EntitySnapshotBuilder {
|
||||
leftHand = leftHanded ? main : off;
|
||||
}
|
||||
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);
|
||||
}
|
||||
// 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) {
|
||||
try {
|
||||
EntityState.Equipment equip = new EntityState.Equipment(
|
||||
armorPiece(eq.getHelmet()), armorPiece(eq.getChestplate()),
|
||||
armorPiece(eq.getLeggings()), armorPiece(eq.getBoots()));
|
||||
armorPiece(eq.getHelmet(), EquipmentSlot.HEAD), armorPiece(eq.getChestplate(), EquipmentSlot.CHEST),
|
||||
armorPiece(eq.getLeggings(), EquipmentSlot.LEGS), armorPiece(eq.getBoots(), EquipmentSlot.FEET));
|
||||
return equip.isEmpty() ? null : equip;
|
||||
} catch (Throwable t) {
|
||||
return null;
|
||||
@@ -612,9 +607,9 @@ public final class EntitySnapshotBuilder {
|
||||
}
|
||||
|
||||
/** 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;
|
||||
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)
|
||||
int dye = 0;
|
||||
String trimMat = null, trimPat = null;
|
||||
@@ -633,8 +628,25 @@ public final class EntitySnapshotBuilder {
|
||||
|
||||
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();
|
||||
if (key.equals("elytra")) return "elytra";
|
||||
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
|
||||
}
|
||||
|
||||
/** 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. */
|
||||
private static String equipAsset(ItemStack it) {
|
||||
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.
|
||||
* datapack variants); plain enums yield their lower-case name.
|
||||
|
||||
@@ -1,68 +1,76 @@
|
||||
package eu.mhsl.minecraft.pixelpics.render.tint;
|
||||
|
||||
import java.util.Locale;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.data.Ageable;
|
||||
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 (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 {
|
||||
|
||||
private static final int BIRCH = 0xFF80A755;
|
||||
private static final int SPRUCE = 0xFF619961;
|
||||
private static final int LILY_PAD = 0xFF208030;
|
||||
private static final int STEM = 0xFF60A017;
|
||||
private static final int BIRCH = 0xFF80A755;
|
||||
private static final int LILY_PAD_IN_WORLD = 0xFF208030;
|
||||
private static final int ATTACHED_STEM = 0xFFE0C71C;
|
||||
|
||||
private enum Channel {
|
||||
BIRCH,
|
||||
SPRUCE,
|
||||
FOLIAGE,
|
||||
LILY_PAD,
|
||||
WATER,
|
||||
REDSTONE,
|
||||
STEM,
|
||||
GRASS
|
||||
/** RedstoneWireBlock.COLORS: the wire colour per power level, with vanilla's float arithmetic. */
|
||||
private static final int[] REDSTONE = new int[16];
|
||||
|
||||
static {
|
||||
for (int i = 0; i <= 15; i++) {
|
||||
float power = i / 15.0F;
|
||||
float red = power * 0.6F + (power > 0.0F ? 0.4F : 0.3F);
|
||||
float green = clamp(power * power * 0.7F - 0.5F);
|
||||
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() {}
|
||||
|
||||
/** 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) {
|
||||
if (tintIndex < 0) return -1;
|
||||
Material material = data.getMaterial();
|
||||
Channel channel = CHANNELS[material.ordinal()];
|
||||
if (channel == null)
|
||||
CHANNELS[material.ordinal()] = channel = channel(material.name().toLowerCase(Locale.ROOT));
|
||||
return switch (channel) {
|
||||
case BIRCH -> BIRCH;
|
||||
case SPRUCE -> SPRUCE;
|
||||
case FOLIAGE -> biomeTint.foliage();
|
||||
case LILY_PAD -> LILY_PAD;
|
||||
case WATER -> biomeTint.water();
|
||||
case REDSTONE -> redstone(data);
|
||||
case STEM -> STEM;
|
||||
case GRASS -> biomeTint.grass();
|
||||
Material m = data.getMaterial();
|
||||
// Pink petals and wildflowers: layer 0 (the flowers) is blank, layer 1 (the stems) grass.
|
||||
if (m == Material.PINK_PETALS || m == Material.WILDFLOWERS) return tintIndex == 1 ? biomeTint.grass() : -1;
|
||||
if (tintIndex != 0) return -1; // every other source has one layer
|
||||
return switch (m) {
|
||||
case GRASS_BLOCK, SHORT_GRASS, FERN, POTTED_FERN -> biomeTint.grass();
|
||||
case BUSH, TALL_GRASS, LARGE_FERN, SUGAR_CANE -> biomeTint.grass();
|
||||
case OAK_LEAVES, JUNGLE_LEAVES, ACACIA_LEAVES -> biomeTint.foliage();
|
||||
case DARK_OAK_LEAVES, MANGROVE_LEAVES, VINE -> biomeTint.foliage();
|
||||
case SPRUCE_LEAVES -> SPRUCE;
|
||||
case BIRCH_LEAVES -> BIRCH;
|
||||
case LEAF_LITTER -> biomeTint.dryFoliage();
|
||||
case WATER_CAULDRON -> biomeTint.water();
|
||||
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) {
|
||||
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;
|
||||
private static int redstone(BlockData data) {
|
||||
return REDSTONE[data instanceof RedstoneWire wire ? Math.clamp(wire.getPower(), 0, 15) : 0];
|
||||
}
|
||||
|
||||
/** Approximation of the redstone wire colour: red rising with the power level. */
|
||||
private static int redstone(BlockData data) {
|
||||
int power = data instanceof RedstoneWire wire ? wire.getPower() : 0;
|
||||
return 0xFF000000 | (Math.min(255, 75 + power * 12) << 16);
|
||||
/** BlockTintSources.stem: {@code ARGB.color(age·32, 255 - age·8, age·4)}. */
|
||||
private static int stem(BlockData data) {
|
||||
int age = data instanceof Ageable a ? a.getAge() : 0;
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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_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);
|
||||
// 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) {
|
||||
@@ -1491,11 +1582,7 @@ public class PPTest extends JavaPlugin {
|
||||
}
|
||||
|
||||
private Entity rfSpawn(Location o, double x, double y, double z, float yaw, EntityType type) {
|
||||
Location l = o.clone().add(x, y, z);
|
||||
l.setYaw(yaw);
|
||||
Entity e = w.spawnEntity(l, type, false);
|
||||
if (e instanceof Mob m) m.setAI(false);
|
||||
return e;
|
||||
return rfSpawn(o, x, y, z, yaw, type.getEntityClass());
|
||||
}
|
||||
|
||||
private void faceNorth(Location o) {
|
||||
|
||||
Reference in new issue
Block a user