render cat/wolf collars, charged creeper and wither armor energy swirl (additive), fox/dolphin mouth items

This commit is contained in:
MineTec committed 2026-11-04 14:45:57 +01:00
1 parent fbea9dfe08
commit c66ebaa942
11 files changed
+231 -20

No files matched your search

@@ -13,10 +13,16 @@ public final class EntityCube {
public final Face[] faces; // by Direction.ordinal() public final Face[] faces; // by Direction.ordinal()
public final Affine toWorld; public final Affine toWorld;
public final Affine toLocal; // inverse public final Affine toLocal; // inverse
public final boolean additive; // emissive, added to what lies behind (energy swirl) instead of blended
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};
public EntityCube(double[] from, double[] to, Face[] faces, Affine toWorld) { public EntityCube(double[] from, double[] to, Face[] faces, Affine toWorld) {
this(from, to, faces, toWorld, false);
}
public EntityCube(double[] from, double[] to, Face[] faces, Affine toWorld, boolean additive) {
this.additive = additive;
this.from = from; this.from = from;
this.to = to; this.to = to;
this.faces = faces; this.faces = faces;
@@ -103,7 +103,7 @@ public final class EntityIntersector {
Vector normal = new Vector(n[0], n[1], n[2]).normalize(); Vector normal = new Vector(n[0], n[1], n[2]).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, color, -1); return new FaceHit(t, world, normal, color, -1, cube.additive);
} }
private static double frac(double v, double lo, double hi) { private static double frac(double v, double lo, double hi) {
@@ -35,7 +35,10 @@ public record EntityState(
boolean passenger, // riding something (vanilla sitting pose) boolean passenger, // riding something (vanilla sitting pose)
ItemSpec rightHand, // item in the RIGHT hand (main or off hand, resolved by handedness); null = empty ItemSpec rightHand, // item in the RIGHT hand (main or off hand, resolved by handedness); null = empty
ItemSpec leftHand, // item in the LEFT hand; null = empty ItemSpec leftHand, // item in the LEFT hand; null = empty
ItemSpec headItem // non-armor item worn in the head slot (pumpkin, skull, block, …); null = none ItemSpec headItem, // non-armor item worn in the head slot (pumpkin, skull, block, …); null = none
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
ItemSpec mouthItem // item carried in the mouth (fox/dolphin main hand); null = none
) { ) {
/** Worn armor (4 slots) of a humanoid wearer; any field may be null. */ /** Worn armor (4 slots) of a humanoid wearer; any field may be null. */
@@ -110,6 +110,10 @@ public final class CemBaker implements EntityBaker<EntityState> {
String m = "entity/horse/horse_markings_" + s.markings(); String m = "entity/horse/horse_markings_" + s.markings();
overlayIfPresent(tex, ch != null && ch.babyModel ? m + "_baby" : m); overlayIfPresent(tex, ch != null && ch.babyModel ? m + "_baby" : m);
} }
if (s.collar() != 0 && tex != null && layerModels(base).stream().noneMatch(n -> n.contains("CollarLayer."))) {
// Collar drawn on the body model itself (WolfCollarLayer: parent model, same UV) -> composite it.
tex = withCollar(tex, base, ch != null && ch.babyModel, s.collar());
}
if (s.typeKey().equals("tropical_fish") && tex != null && s.tint() != 0) { if (s.typeKey().equals("tropical_fish") && tex != null && s.tint() != 0) {
tex = TextureOps.deepCopy(tex); tex = TextureOps.deepCopy(tex);
TextureOps.tint(tex, s.tint()); // white base body tinted with the body colour TextureOps.tint(tex, s.tint()); // white base body tinted with the body colour
@@ -160,15 +164,16 @@ public final class CemBaker implements EntityBaker<EntityState> {
: List.of(); : List.of();
List<CemGeometry.Baked> baked = new ArrayList<>(); List<CemGeometry.Baked> baked = new ArrayList<>();
List<CemGeometry.Baked> glow = new ArrayList<>(); // additive energy-swirl layers
if (!invisible) { if (!invisible) {
baked.addAll(body); baked.addAll(body);
if (vanilla) addVanillaLayers(s, ch, pose, show, tex, baked); if (vanilla) addVanillaLayers(s, ch, pose, show, tex, baked, glow);
} }
// Humanoid armor / elytra: drawn even when invisible (vanilla "floating armor"). // Humanoid armor / elytra: drawn even when invisible (vanilla "floating armor").
if (vanilla && s.equipment() != null) addArmorLayers(s, ch, pose, baked); if (vanilla && s.equipment() != null) addArmorLayers(s, ch, pose, baked);
List<EntityCube> itemCubes = new ArrayList<>(); List<EntityCube> itemCubes = new ArrayList<>();
if (vanilla && items != null) addItems(s, frames, isIllager(info), itemCubes); if (vanilla && items != null) addItems(s, frames, isIllager(info), itemCubes);
if (baked.isEmpty() && itemCubes.isEmpty()) return invisible ? null : fallbackBox(s, tex); if (baked.isEmpty() && glow.isEmpty() && itemCubes.isEmpty()) return invisible ? null : fallbackBox(s, tex);
// Legacy CEM models: snap the lowest point to the ground (their rest geometry has no reliable origin). // Legacy CEM models: snap the lowest point to the ground (their rest geometry has no reliable origin).
if (place == null) { if (place == null) {
@@ -182,6 +187,9 @@ public final class CemBaker implements EntityBaker<EntityState> {
List<EntityCube> cubes = new ArrayList<>(baked.size()); List<EntityCube> cubes = new ArrayList<>(baked.size());
for (CemGeometry.Baked b : baked) cubes.add(new EntityCube(b.from(), b.to(), b.faces(), place.mul(b.world()))); for (CemGeometry.Baked b : baked) cubes.add(new EntityCube(b.from(), b.to(), b.faces(), place.mul(b.world())));
for (CemGeometry.Baked b : 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(new EntityCube(c.from, c.to, c.faces, place.mul(c.toWorld)));
return RenderedEntity.of(cubes); return RenderedEntity.of(cubes);
} }
@@ -260,6 +268,7 @@ public final class CemBaker implements EntityBaker<EntityState> {
items.bake(item, "ground", false, x, out); items.bake(item, "ground", false, x, out);
} }
} }
addMouthItem(s, frames, out);
ItemSpec head = s.headItem(); ItemSpec head = s.headItem();
Affine h = frames.get("head"); Affine h = frames.get("head");
if (head != null && h != null) { if (head != null && h != null) {
@@ -280,6 +289,27 @@ public final class CemBaker implements EntityBaker<EntityState> {
} }
} }
/**
* Item carried in the mouth, rendered in the "ground" context like vanilla (HoldingEntityRenderState).
* Fox (FoxHeldItemLayer, awake): head pivot · [baby: S(0.75)] · head rotation · T(0.06, 0.27 [baby 0.26],
* -0.5) · rotX(90) — the head frame carries exactly that pivot + rotation. Dolphin (DolphinCarryingItemLayer)
* at the model root · T(0, 1, -1) (its xRot-dependent shift is 0: PixelPics dolphins aren't pitched).
*/
private void addMouthItem(EntityState s, Map<String, Affine> frames, List<EntityCube> out) {
ItemSpec item = s.mouthItem();
if (item == null) return;
boolean baby = s.baby();
Affine x = null;
if (s.typeKey().equals("fox") && frames.containsKey("head")) {
x = frames.get("head").mul(CemGeometry.JAVA_LOCAL);
if (baby) x = x.mul(Affine.scale(0.75));
x = x.mul(Affine.translation(0.06, baby ? 0.26 : 0.27, -0.5)).mul(Affine.rotX(Math.PI / 2));
} else if (s.typeKey().equals("dolphin") && frames.containsKey("root")) {
x = frames.get("root").mul(CemGeometry.JAVA_LOCAL).mul(Affine.translation(0, 1, -1));
}
if (x != null) items.bake(item, "ground", false, x, out);
}
private static boolean isIllager(CemModelLoader.EntityInfo info) { private static boolean isIllager(CemModelLoader.EntityInfo info) {
return info != null && info.poses().keySet().stream().anyMatch(k -> k.startsWith("arms_")); return info != null && info.poses().keySet().stream().anyMatch(k -> k.startsWith("arms_"));
} }
@@ -373,7 +403,13 @@ public final class CemBaker implements EntityBaker<EntityState> {
// --- vanilla render layers ----------------------------------------------------------------------- // --- vanilla render layers -----------------------------------------------------------------------
private void addVanillaLayers( private void addVanillaLayers(
EntityState s, Choice ch, Pose pose, Set<String> show, int[][] bodyTex, List<CemGeometry.Baked> baked) { EntityState s,
Choice ch,
Pose pose,
Set<String> show,
int[][] bodyTex,
List<CemGeometry.Baked> baked,
List<CemGeometry.Baked> glow) {
String base = ch.base; String base = ch.base;
boolean baby = ch.babyModel; boolean baby = ch.babyModel;
for (String n : layerModels(base)) { for (String n : layerModels(base)) {
@@ -392,6 +428,21 @@ public final class CemBaker implements EntityBaker<EntityState> {
baked.addAll(pose.bake(lm, t, show)); baked.addAll(pose.bake(lm, t, show));
} }
case "TropicalFishPatternLayer" -> addTropicalPattern(s, n, lm, pose, baked); case "TropicalFishPatternLayer" -> addTropicalPattern(s, n, lm, pose, baked);
case "CatCollarLayer" -> {
// Own collar model (inflated by 0.01), collar texture tinted with the collar's dye colour.
if (s.collar() == 0 || lm.texture() == null) continue;
int[][] t = texture(lm.texture());
if (t == null) continue;
t = TextureOps.deepCopy(t);
TextureOps.tint(t, s.collar());
baked.addAll(pose.bake(lm, t, show));
}
case "CreeperPowerLayer", "WitherArmorLayer" -> {
// EnergySwirlLayer: charged creeper / wither below half health; drawn additively.
if (!s.powered() || lm.texture() == null) continue;
int[][] t = energySwirl(lm.texture(), layerClass.equals("WitherArmorLayer"));
if (t != null) glow.addAll(pose.bake(lm, t, show));
}
case "SlimeOuterLayer" -> { case "SlimeOuterLayer" -> {
// Translucent outer cube on the body texture (alpha < 255 there; blended by the tracer). // Translucent outer cube on the body texture (alpha < 255 there; blended by the tracer).
int[][] t = lm.texture() != null ? texture(lm.texture()) : bodyTex; int[][] t = lm.texture() != null ? texture(lm.texture()) : bodyTex;
@@ -475,6 +526,49 @@ public final class CemBaker implements EntityBaker<EntityState> {
return out; return out;
} }
// --- energy swirl & collars ----------------------------------------------------------------------
// A photo has no time: the swirl's scrolling texture is frozen at this age (ticks). Chosen so the sparse
// wither_armor pattern covers every wither part (at e.g. 42 the side heads land on empty rows).
private static final float SWIRL_AGE = 59f;
/**
* Energy-swirl texture (vanilla EnergySwirlLayer / RenderTypes.energySwirl): the texture matrix shifts the UV
* by ({@code xOffset(age) % 1}, {@code age·0.01 % 1}) on a repeating texture — baked here as a wrapped copy
* of the texture rolled by that offset — and the vertex colour 0xFF808080 halves the RGB. The result is
* drawn additively (BlendFunction.ADDITIVE), so alpha only cuts out (shader ALPHA_CUTOUT 0.1).
* xOffset: creeper {@code age·0.01}, wither {@code cos(age·0.02)·3}.
*/
private int[][] energySwirl(String path, boolean wither) {
int[][] src = texture(path);
if (src == null) return null;
double u = wither ? Math.cos(SWIRL_AGE * 0.02) * 3 : SWIRL_AGE * 0.01;
double v = SWIRL_AGE * 0.01;
int h = src.length, w = src[0].length;
int du = Math.floorMod((int) Math.round((u % 1) * w), w);
int dv = Math.floorMod((int) Math.round((v % 1) * h), h);
int[][] out = new int[h][w];
for (int y = 0; y < h; y++) {
for (int x = 0; x < w; x++) {
int p = src[(y + dv) % h][(x + du) % w];
out[y][x] = (p & 0xFF000000) | ((p >>> 1) & 0x7F7F7F);
}
}
return out;
}
/** Body texture with the dye-tinted {@code entity/<base>/<base>_collar[_baby]} composited over it. */
private int[][] withCollar(int[][] tex, String base, boolean baby, int dye) {
String path = "entity/" + base + "/" + base + "_collar" + (baby ? "_baby" : "");
int[][] collar = texture(path);
if (collar == null || collar.length != tex.length || collar[0].length != tex[0].length) return tex;
int[][] tinted = TextureOps.deepCopy(collar);
TextureOps.tint(tinted, dye);
int[][] out = TextureOps.deepCopy(tex);
TextureOps.overlay(out, tinted);
return out;
}
// --- texture resolution -------------------------------------------------------------------------- // --- texture resolution --------------------------------------------------------------------------
private int[][] texture(String path) { private int[][] texture(String path) {
@@ -501,6 +595,12 @@ public final class CemBaker implements EntityBaker<EntityState> {
Math.min(adult.size(), EntityModels.specificCandidateCount(s.typeKey(), s.variant())), Math.min(adult.size(), EntityModels.specificCandidateCount(s.typeKey(), s.variant())),
info.texture()); info.texture());
} }
if (s.collar() != 0) {
// Tamed: vanilla switches to the "_tame" texture where the variant has one (wolves).
List<String> tame = new ArrayList<>();
for (String a : adult) tame.add(a + "_tame");
adult.addAll(0, tame);
}
List<String> order = new ArrayList<>(); List<String> order = new ArrayList<>();
if (ch != null && ch.babyModel) { if (ch != null && ch.babyModel) {
for (String a : adult) order.add(a + "_baby"); for (String a : adult) order.add(a + "_baby");
@@ -81,7 +81,7 @@ public final class ElementIntersector {
Vector world = new Vector(bx + ox + dx * c.t, by + oy + dy * c.t, bz + oz + dz * c.t); Vector world = new Vector(bx + ox + dx * c.t, by + oy + dy * c.t, bz + oz + dz * c.t);
Vector normal = new Vector(c.normal[0], c.normal[1], c.normal[2]); Vector normal = new Vector(c.normal[0], c.normal[1], c.normal[2]);
return new FaceHit(c.t, world, normal, color, face.tintIndex); return new FaceHit(c.t, world, normal, color, face.tintIndex, false);
} }
return null; return null;
} }
@@ -4,6 +4,7 @@ import org.bukkit.util.Vector;
/** /**
* The result of intersecting a ray with a block's geometry: the world-space hit point and normal, * The result of intersecting a ray with a block's geometry: the world-space hit point and normal,
* the sampled ARGB color (before shading/tinting) and the face's tint index ({@code -1} = none). * the sampled ARGB color (before shading/tinting), the face's tint index ({@code -1} = none) and whether the
* surface is drawn additively (emissive energy-swirl layers: its colour is added to what lies behind).
*/ */
public record FaceHit(double t, Vector point, Vector normal, int color, int tintIndex) {} public record FaceHit(double t, Vector point, Vector normal, int color, int tintIndex, boolean additive) {}
@@ -42,6 +42,8 @@ public final class SnapshotRaytracer {
private static final int MAX_TRANSLUCENT_LAYERS = 6; private static final int MAX_TRANSLUCENT_LAYERS = 6;
private static final double MIN_TRANSMITTANCE = 0.03; private static final double MIN_TRANSMITTANCE = 0.03;
private static final double LAYER_EPS = 1e-5; private static final double LAYER_EPS = 1e-5;
// layerAlpha marker of an additive (emissive) layer: its colour is added to what lies behind.
private static final double ADDITIVE = -1;
private final BlockModelRegistry registry; private final BlockModelRegistry registry;
private final BiomeTintProvider tintProvider; private final BiomeTintProvider tintProvider;
@@ -78,7 +80,8 @@ public final class SnapshotRaytracer {
BlockCursor blocks = new BlockCursor(snapshot, origin, direction, sky, scene, depth); BlockCursor blocks = new BlockCursor(snapshot, origin, direction, sky, scene, depth);
boolean entities = scene != null && !scene.isEmpty(); boolean entities = scene != null && !scene.isEmpty();
// Translucent surfaces in front of the final one, front to back (shaded colour, alpha 0..1, ray t). // Translucent surfaces in front of the final one, front to back (shaded colour, alpha 0..1 or ADDITIVE,
// ray t).
int[] layerColor = null; int[] layerColor = null;
double[] layerAlpha = null, layerT = null; double[] layerAlpha = null, layerT = null;
int layers = 0; int layers = 0;
@@ -97,8 +100,11 @@ public final class SnapshotRaytracer {
FaceHit eh = entities ? scene.nearestHit(ox, oy, oz, dx, dy, dz, minT, limit) : null; FaceHit eh = entities ? scene.nearestHit(ox, oy, oz, dx, dy, dz, minT, limit) : null;
int color; int color;
double t; double t;
boolean additive = false;
if (eh != null) { if (eh != null) {
color = ColorUtil.shade(eh.color(), shadeFactor(eh.normal())); // Emissive additive surfaces (energy swirl) are unshaded: vanilla NO_CARDINAL_LIGHTING + EMISSIVE.
additive = eh.additive();
color = additive ? eh.color() : ColorUtil.shade(eh.color(), shadeFactor(eh.normal()));
t = eh.t(); t = eh.t();
} else if (pending != null) { } else if (pending != null) {
color = pending.color; color = pending.color;
@@ -109,7 +115,14 @@ public final class SnapshotRaytracer {
} }
int alpha = ColorUtil.alpha(color); int alpha = ColorUtil.alpha(color);
double a = alpha / 255.0; double a = alpha / 255.0;
if (alpha >= OPAQUE_ALPHA if (additive) {
// Additive light never hides what lies behind it: it neither ends the ray nor dims it. Beyond the
// layer budget it is dropped.
if (layers == MAX_TRANSLUCENT_LAYERS) {
minT = t + LAYER_EPS;
continue;
}
} else if (alpha >= OPAQUE_ALPHA
|| layers == MAX_TRANSLUCENT_LAYERS || layers == MAX_TRANSLUCENT_LAYERS
|| transmittance * (1 - a) < MIN_TRANSMITTANCE) { || transmittance * (1 - a) < MIN_TRANSMITTANCE) {
// Opaque, or what lies behind would contribute next to nothing: this surface ends the ray. // Opaque, or what lies behind would contribute next to nothing: this surface ends the ray.
@@ -123,10 +136,10 @@ public final class SnapshotRaytracer {
layerT = new double[MAX_TRANSLUCENT_LAYERS]; layerT = new double[MAX_TRANSLUCENT_LAYERS];
} }
layerColor[layers] = color; layerColor[layers] = color;
layerAlpha[layers] = a; layerAlpha[layers] = additive ? ADDITIVE : a;
layerT[layers] = t; layerT[layers] = t;
layers++; layers++;
transmittance *= 1 - a; if (!additive) transmittance *= 1 - a;
minT = t + LAYER_EPS; minT = t + LAYER_EPS;
} }
@@ -138,7 +151,12 @@ public final class SnapshotRaytracer {
color = blocks.applyLiquid(color, finalT, skyColor); color = blocks.applyLiquid(color, finalT, skyColor);
liquidPending = false; liquidPending = false;
} }
color = ColorUtil.mix(fog(layerColor[i], layerT[i], skyColor), color, layerAlpha[i], 1 - layerAlpha[i]); if (layerAlpha[i] == ADDITIVE) {
// Added light fades with distance like the surface it would otherwise be.
color = ColorUtil.add(color, ColorUtil.shade(layerColor[i], 1 - fogFactor(layerT[i])));
} else {
color = ColorUtil.mix(fog(layerColor[i], layerT[i], skyColor), color, layerAlpha[i], 1 - layerAlpha[i]);
}
} }
if (liquidPending) color = blocks.applyLiquid(color, finalT, skyColor); if (liquidPending) color = blocks.applyLiquid(color, finalT, skyColor);
return color & 0xFFFFFF; return color & 0xFFFFFF;
@@ -19,6 +19,8 @@ import java.util.function.DoubleSupplier;
import org.bukkit.Keyed; import org.bukkit.Keyed;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.attribute.Attribute;
import org.bukkit.attribute.AttributeInstance;
import org.bukkit.entity.AbstractArrow; import org.bukkit.entity.AbstractArrow;
import org.bukkit.entity.AbstractHorse; import org.bukkit.entity.AbstractHorse;
import org.bukkit.entity.Ageable; import org.bukkit.entity.Ageable;
@@ -28,6 +30,7 @@ import org.bukkit.entity.Cat;
import org.bukkit.entity.ChestedHorse; import org.bukkit.entity.ChestedHorse;
import org.bukkit.entity.Chicken; import org.bukkit.entity.Chicken;
import org.bukkit.entity.Cow; import org.bukkit.entity.Cow;
import org.bukkit.entity.Creeper;
import org.bukkit.entity.Entity; import org.bukkit.entity.Entity;
import org.bukkit.entity.Fox; import org.bukkit.entity.Fox;
import org.bukkit.entity.Frog; import org.bukkit.entity.Frog;
@@ -51,6 +54,7 @@ import org.bukkit.entity.TraderLlama;
import org.bukkit.entity.Trident; import org.bukkit.entity.Trident;
import org.bukkit.entity.TropicalFish; import org.bukkit.entity.TropicalFish;
import org.bukkit.entity.Villager; import org.bukkit.entity.Villager;
import org.bukkit.entity.Wither;
import org.bukkit.entity.Wolf; import org.bukkit.entity.Wolf;
import org.bukkit.entity.ZombieVillager; import org.bukkit.entity.ZombieVillager;
import org.bukkit.inventory.EntityEquipment; import org.bukkit.inventory.EntityEquipment;
@@ -114,6 +118,9 @@ public final class EntitySnapshotBuilder {
"piglin", "piglin",
"piglin_brute"); "piglin_brute");
// Mobs whose main-hand item is drawn in the mouth (FoxHeldItemLayer, DolphinCarryingItemLayer).
private static final Set<String> MOUTH_CARRIERS = Set.of("fox", "dolphin");
/** {@code nearby}: {@link FrustumBounds#nearbyEntities} of this photo. */ /** {@code nearby}: {@link FrustumBounds#nearbyEntities} of this photo. */
public static List<EntityState> build(Collection<Entity> nearby, UUID shooter) { public static List<EntityState> build(Collection<Entity> nearby, UUID shooter) {
List<EntityState> states = new ArrayList<>(); List<EntityState> states = new ArrayList<>();
@@ -169,6 +176,8 @@ public final class EntitySnapshotBuilder {
String bodyEquip = null; String bodyEquip = null;
int bodyDye = 0; int bodyDye = 0;
int tint2 = 0; int tint2 = 0;
int collar = 0;
boolean powered = false;
try { try {
// Slime & magma cube (MagmaCube extends Slime) scale their model by size (1/2/4). // Slime & magma cube (MagmaCube extends Slime) scale their model by size (1/2/4).
if (e instanceof Slime sl) sizeScale = sl.getSize(); if (e instanceof Slime sl) sizeScale = sl.getSize();
@@ -187,8 +196,16 @@ public final class EntitySnapshotBuilder {
default -> "big";}; default -> "big";};
} else if (e instanceof Cat c) { } else if (e instanceof Cat c) {
variant = keyOf(c.getCatType()); variant = keyOf(c.getCatType());
if (c.isTamed()) collar = ColorUtil.dyeArgb(c.getCollarColor(), 0);
} else if (e instanceof Wolf w) { } else if (e instanceof Wolf w) {
variant = keyOf(w.getVariant()); variant = keyOf(w.getVariant());
if (w.isTamed()) collar = ColorUtil.dyeArgb(w.getCollarColor(), 0);
} else if (e instanceof Creeper cr) {
powered = cr.isPowered();
} else if (e instanceof Wither wi) {
// WitherBoss.isPowered(): health <= max health / 2 (wither armor layer).
AttributeInstance max = wi.getAttribute(Attribute.MAX_HEALTH);
powered = max != null && wi.getHealth() <= max.getValue() / 2;
} else if (e instanceof Axolotl a) { } else if (e instanceof Axolotl a) {
variant = keyOf(a.getVariant()); variant = keyOf(a.getVariant());
} else if (e instanceof Parrot p) { } else if (e instanceof Parrot p) {
@@ -245,7 +262,7 @@ public final class EntitySnapshotBuilder {
// Equipment slots: saddle + BODY equipment (horse armor, llama carpet, harness, wolf/nautilus armor, …) // Equipment slots: saddle + BODY equipment (horse armor, llama carpet, harness, wolf/nautilus armor, …)
// for every mount — including ones added in newer versions — plus held items (by hand side, like // for every mount — including ones added in newer versions — plus held items (by hand side, like
// vanilla: a left-handed mob holds its main-hand item left) and a non-armor head item. // vanilla: a left-handed mob holds its main-hand item left) and a non-armor head item.
ItemSpec rightHand = null, leftHand = null, headItem = null; ItemSpec rightHand = null, leftHand = null, headItem = null, mouthItem = null;
EntityEquipment eq = e instanceof LivingEntity le ? le.getEquipment() : null; EntityEquipment eq = e instanceof LivingEntity le ? le.getEquipment() : null;
if (eq != null) { if (eq != null) {
try { try {
@@ -263,8 +280,12 @@ public final class EntitySnapshotBuilder {
|| (e instanceof HumanEntity he && he.getMainHand() == MainHand.LEFT); || (e instanceof HumanEntity he && he.getMainHand() == MainHand.LEFT);
ItemSpec main = ItemSpecs.of(eq.getItemInMainHand()); ItemSpec main = ItemSpecs.of(eq.getItemInMainHand());
ItemSpec off = ItemSpecs.of(eq.getItemInOffHand()); ItemSpec off = ItemSpecs.of(eq.getItemInOffHand());
rightHand = leftHanded ? off : main; if (MOUTH_CARRIERS.contains(type)) {
leftHand = leftHanded ? main : off; mouthItem = main; // fox/dolphin carry their main-hand item in the mouth, not in a hand
} else {
rightHand = leftHanded ? off : main;
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.getType()) == null) {
headItem = ItemSpecs.of(helmet); headItem = ItemSpecs.of(helmet);
@@ -312,7 +333,10 @@ public final class EntitySnapshotBuilder {
e.isInsideVehicle(), e.isInsideVehicle(),
rightHand, rightHand,
leftHand, leftHand,
headItem); headItem,
collar,
powered,
mouthItem);
} }
/** Worn armor (4 slots) from a humanoid wearer; null when nothing is equipped. */ /** Worn armor (4 slots) from a humanoid wearer; null when nothing is equipped. */
@@ -92,7 +92,6 @@ public final class ColorUtil {
return argb(a, r, g, b); return argb(a, r, g, b);
} }
/** Scales the RGB channels by {@code factor} (0..1), keeping alpha. Used for directional face shading. */
/** Texels with alpha at or below this are holes (alpha cutout) for every intersector and the extrusion. */ /** Texels with alpha at or below this are holes (alpha cutout) for every intersector and the extrusion. */
public static final int ALPHA_CUTOUT = 16; public static final int ALPHA_CUTOUT = 16;
@@ -101,6 +100,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. */
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));
@@ -109,6 +109,13 @@ public final class ColorUtil {
return argb(a, r, g, b); return argb(a, r, g, b);
} }
/** Additive blend (vanilla BlendFunction.ADDITIVE, ONE/ONE): RGB of {@code src} added to {@code dst}, clamped. */
public static int add(int dst, int src) {
return (clamp(red(dst) + red(src)) << 16)
| (clamp(green(dst) + green(src)) << 8)
| clamp(blue(dst) + blue(src));
}
private static int clamp(int v) { private static int clamp(int v) {
return v < 0 ? 0 : Math.min(v, 255); return v < 0 ? 0 : Math.min(v, 255);
} }
+1 -1
View File
@@ -185,7 +185,7 @@ public class EntityTestRender {
EntityState s = new EntityState(key, 0, 0, 0, yaw, baby, 0.8, 1.0, EntityState s = new EntityState(key, 0, 0, 0, yaw, baby, 0.8, 1.0,
isPlayer, null, false, VAR.get(key), tint, 1.0, PROF.get(key), LVL.getOrDefault(key, 0), isPlayer, null, false, VAR.get(key), tint, 1.0, PROF.get(key), LVL.getOrDefault(key, 0),
MARK.get(key), SADDLE.contains(key), CHEST.contains(key), EQUIP.get(key), baby ? null : ARMOR.get(key), MARK.get(key), SADDLE.contains(key), CHEST.contains(key), EQUIP.get(key), baby ? null : ARMOR.get(key),
!baby && INVISIBLE.contains(key), 0, 0, tint2, false, null, null, null); !baby && INVISIBLE.contains(key), 0, 0, tint2, false, null, null, null, 0, false, null);
RenderedEntity re = baker.bake(s); RenderedEntity re = baker.bake(s);
if (re == null || re.cubes.isEmpty()) { // invisible with no equipment -> nothing renders if (re == null || re.cubes.isEmpty()) { // invisible with no equipment -> nothing renders
return new BufferedImage(TW, TH, BufferedImage.TYPE_INT_RGB); return new BufferedImage(TW, TH, BufferedImage.TYPE_INT_RGB);
+52
View File
@@ -79,6 +79,18 @@ public class PPTest extends JavaPlugin {
scenes.put("proj_stuck_close_t", new Scene("proj_stuck_close_t", this::projStuck, 18.5, 2.6, -3, 13.5, 1.6, 1)); scenes.put("proj_stuck_close_t", new Scene("proj_stuck_close_t", this::projStuck, 18.5, 2.6, -3, 13.5, 1.6, 1));
// Rendered 4 ticks after the shots: freshly stuck arrows still wobble (vanilla shakeTime -> pitch offset). // Rendered 4 ticks after the shots: freshly stuck arrows still wobble (vanilla shakeTime -> pitch offset).
scenes.put("proj_shake", new Scene("proj_shake", this::projStuck, -2.5, 2.4, -2.5, 2.5, 1.6, 1)); scenes.put("proj_shake", new Scene("proj_shake", this::projStuck, -2.5, 2.4, -2.5, 2.5, 1.6, 1));
// Conditional vanilla layers: collars (tamed only, dye-tinted), charged creeper / wither armor (additive
// energy swirl), fox/dolphin mouth items.
scenes.put("layers", new Scene("layers", this::layers, 4.8, 2.4, -5.5, 4.8, 0.6, 1.5));
scenes.put("layers_side", new Scene("layers_side", this::layers, 12.5, 2.2, 5.5, 5, 0.5, 2.5));
scenes.put("layers_close", new Scene("layers_close", this::layers, 5.5, 1.3, 0.3, 5.5, 0.4, 2.5));
scenes.put("layers_pets", new Scene("layers_pets", this::layers, 4.5, 1.6, -3.2, 4.5, 0.3, 0));
scenes.put("layers_dolphin", new Scene("layers_dolphin", this::layers, 10, 1.0, 1.0, 7.5, 0.4, 2.0));
scenes.put("layers_cats", new Scene("layers_cats", this::layers, 1.7, 1.1, -1.6, 1.7, 0.3, 0));
scenes.put("layers_cats_side", new Scene("layers_cats_side", this::layers, 1.7, 1.6, 1.4, 1.7, 0.2, 0));
scenes.put("layers_top", new Scene("layers_top", this::layers, 5.5, 6, 2.0, 5.5, 0, 2.5));
scenes.put("layers_wither", new Scene("layers_wither", this::withers, 3.5, 4, -9, 3.5, 2.2, 1));
scenes.put("layers_wither_close", new Scene("layers_wither_close", this::withers, 6, 3.4, -3.5, 6, 2.6, 1));
scenes.put("proj_stuck_sw", new Scene("proj_stuck_sw", this::projStuck, -3, 6, 10, 8, 1.5, 2)); scenes.put("proj_stuck_sw", new Scene("proj_stuck_sw", this::projStuck, -3, 6, 10, 8, 1.5, 2));
} }
@@ -613,6 +625,46 @@ public class PPTest extends JavaPlugin {
t.setBillboard(Display.Billboard.CENTER); t.setBillboard(Display.Billboard.CENTER);
} }
// --- conditional layers ----------------------------------------------------------------------
private void layers(Location o) {
tame(spawn(o, 0, 0, 0, 180, Cat.class), DyeColor.RED);
tame(spawn(o, 1.2, 0, 0, 180, Cat.class), DyeColor.BLUE);
spawn(o, 2.4, 0, 0, 180, Cat.class); // untamed: no collar
Cat kitten = spawn(o, 3.4, 0, 0, 180, Cat.class);
kitten.setBaby();
tame(kitten, DyeColor.YELLOW);
tame(spawn(o, 5, 0, 0, 180, Wolf.class), DyeColor.RED);
tame(spawn(o, 6.4, 0, 0, 180, Wolf.class), DyeColor.LIME);
spawn(o, 7.8, 0, 0, 180, Wolf.class); // untamed: wild texture, no collar
Wolf pup = spawn(o, 9.0, 0, 0, 180, Wolf.class);
pup.setBaby();
tame(pup, DyeColor.MAGENTA);
spawn(o, 0.5, 0, 2.5, 180, Creeper.class);
spawn(o, 2.2, 0, 2.5, 180, Creeper.class).setPowered(true);
hands(spawn(o, 4.2, 0, 2.5, 180, Fox.class), Material.EMERALD, null);
Fox kit = spawn(o, 5.5, 0, 2.5, 180, Fox.class);
kit.setBaby();
hands(kit, Material.SWEET_BERRIES, null);
hands(spawn(o, 7.5, 0, 2.5, 180, Dolphin.class), Material.COD, null);
// Spawned mobs get a random body yaw: face them all to the camera (north).
for (Entity e : w.getNearbyEntities(o.clone().add(5, 0, 1), 7, 3, 4))
if (e instanceof LivingEntity le && !(e instanceof Player)) le.setBodyYaw(180);
}
private void tame(Tameable t, DyeColor collar) {
t.setTamed(true);
if (t instanceof Cat c) c.setCollarColor(collar);
if (t instanceof Wolf wo) wo.setCollarColor(collar);
}
private void withers(Location o) {
spawn(o, 1, 0, 1, 180, Wither.class); // full health: no armor
Wither half = spawn(o, 6, 0, 1, 180, Wither.class);
half.setHealth(half.getAttribute(org.bukkit.attribute.Attribute.MAX_HEALTH).getValue() / 2 - 1);
for (Entity e : w.getNearbyEntities(o, 12, 8, 8)) if (e instanceof Wither wi) wi.setBodyYaw(180);
}
// --- projectiles ----------------------------------------------------------------------------- // --- projectiles -----------------------------------------------------------------------------
/** Spawns a projectile at p moving along dir, rotated like vanilla Projectile#shoot (yaw atan2(x,z), pitch up). */ /** Spawns a projectile at p moving along dir, rotated like vanilla Projectile#shoot (yaw atan2(x,z), pitch up). */