render cat/wolf collars, charged creeper and wither armor energy swirl (additive), fox/dolphin mouth items
This commit is contained in:
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 Affine toWorld;
|
||||
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[] aabbMax = {-Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE};
|
||||
|
||||
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.to = to;
|
||||
this.faces = faces;
|
||||
|
||||
@@ -103,7 +103,7 @@ public final class EntityIntersector {
|
||||
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.
|
||||
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) {
|
||||
|
||||
@@ -35,7 +35,10 @@ public record EntityState(
|
||||
boolean passenger, // riding something (vanilla sitting pose)
|
||||
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 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. */
|
||||
|
||||
@@ -110,6 +110,10 @@ public final class CemBaker implements EntityBaker<EntityState> {
|
||||
String m = "entity/horse/horse_markings_" + s.markings();
|
||||
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) {
|
||||
tex = TextureOps.deepCopy(tex);
|
||||
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<CemGeometry.Baked> baked = new ArrayList<>();
|
||||
List<CemGeometry.Baked> glow = new ArrayList<>(); // additive energy-swirl layers
|
||||
if (!invisible) {
|
||||
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").
|
||||
if (vanilla && s.equipment() != null) addArmorLayers(s, ch, pose, baked);
|
||||
List<EntityCube> itemCubes = new ArrayList<>();
|
||||
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).
|
||||
if (place == null) {
|
||||
@@ -182,6 +187,9 @@ public final class CemBaker implements EntityBaker<EntityState> {
|
||||
|
||||
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 : 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)));
|
||||
return RenderedEntity.of(cubes);
|
||||
}
|
||||
@@ -260,6 +268,7 @@ public final class CemBaker implements EntityBaker<EntityState> {
|
||||
items.bake(item, "ground", false, x, out);
|
||||
}
|
||||
}
|
||||
addMouthItem(s, frames, out);
|
||||
ItemSpec head = s.headItem();
|
||||
Affine h = frames.get("head");
|
||||
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) {
|
||||
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 -----------------------------------------------------------------------
|
||||
|
||||
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;
|
||||
boolean baby = ch.babyModel;
|
||||
for (String n : layerModels(base)) {
|
||||
@@ -392,6 +428,21 @@ public final class CemBaker implements EntityBaker<EntityState> {
|
||||
baked.addAll(pose.bake(lm, t, show));
|
||||
}
|
||||
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" -> {
|
||||
// Translucent outer cube on the body texture (alpha < 255 there; blended by the tracer).
|
||||
int[][] t = lm.texture() != null ? texture(lm.texture()) : bodyTex;
|
||||
@@ -475,6 +526,49 @@ public final class CemBaker implements EntityBaker<EntityState> {
|
||||
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 --------------------------------------------------------------------------
|
||||
|
||||
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())),
|
||||
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<>();
|
||||
if (ch != null && ch.babyModel) {
|
||||
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 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;
|
||||
}
|
||||
|
||||
@@ -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 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 double MIN_TRANSMITTANCE = 0.03;
|
||||
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 BiomeTintProvider tintProvider;
|
||||
@@ -78,7 +80,8 @@ public final class SnapshotRaytracer {
|
||||
BlockCursor blocks = new BlockCursor(snapshot, origin, direction, sky, scene, depth);
|
||||
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;
|
||||
double[] layerAlpha = null, layerT = null;
|
||||
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;
|
||||
int color;
|
||||
double t;
|
||||
boolean additive = false;
|
||||
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();
|
||||
} else if (pending != null) {
|
||||
color = pending.color;
|
||||
@@ -109,7 +115,14 @@ public final class SnapshotRaytracer {
|
||||
}
|
||||
int alpha = ColorUtil.alpha(color);
|
||||
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
|
||||
|| transmittance * (1 - a) < MIN_TRANSMITTANCE) {
|
||||
// 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];
|
||||
}
|
||||
layerColor[layers] = color;
|
||||
layerAlpha[layers] = a;
|
||||
layerAlpha[layers] = additive ? ADDITIVE : a;
|
||||
layerT[layers] = t;
|
||||
layers++;
|
||||
transmittance *= 1 - a;
|
||||
if (!additive) transmittance *= 1 - a;
|
||||
minT = t + LAYER_EPS;
|
||||
}
|
||||
|
||||
@@ -138,7 +151,12 @@ public final class SnapshotRaytracer {
|
||||
color = blocks.applyLiquid(color, finalT, skyColor);
|
||||
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);
|
||||
return color & 0xFFFFFF;
|
||||
|
||||
+28
-4
@@ -19,6 +19,8 @@ import java.util.function.DoubleSupplier;
|
||||
import org.bukkit.Keyed;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.attribute.Attribute;
|
||||
import org.bukkit.attribute.AttributeInstance;
|
||||
import org.bukkit.entity.AbstractArrow;
|
||||
import org.bukkit.entity.AbstractHorse;
|
||||
import org.bukkit.entity.Ageable;
|
||||
@@ -28,6 +30,7 @@ import org.bukkit.entity.Cat;
|
||||
import org.bukkit.entity.ChestedHorse;
|
||||
import org.bukkit.entity.Chicken;
|
||||
import org.bukkit.entity.Cow;
|
||||
import org.bukkit.entity.Creeper;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Fox;
|
||||
import org.bukkit.entity.Frog;
|
||||
@@ -51,6 +54,7 @@ import org.bukkit.entity.TraderLlama;
|
||||
import org.bukkit.entity.Trident;
|
||||
import org.bukkit.entity.TropicalFish;
|
||||
import org.bukkit.entity.Villager;
|
||||
import org.bukkit.entity.Wither;
|
||||
import org.bukkit.entity.Wolf;
|
||||
import org.bukkit.entity.ZombieVillager;
|
||||
import org.bukkit.inventory.EntityEquipment;
|
||||
@@ -114,6 +118,9 @@ public final class EntitySnapshotBuilder {
|
||||
"piglin",
|
||||
"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. */
|
||||
public static List<EntityState> build(Collection<Entity> nearby, UUID shooter) {
|
||||
List<EntityState> states = new ArrayList<>();
|
||||
@@ -169,6 +176,8 @@ public final class EntitySnapshotBuilder {
|
||||
String bodyEquip = null;
|
||||
int bodyDye = 0;
|
||||
int tint2 = 0;
|
||||
int collar = 0;
|
||||
boolean powered = false;
|
||||
try {
|
||||
// Slime & magma cube (MagmaCube extends Slime) scale their model by size (1/2/4).
|
||||
if (e instanceof Slime sl) sizeScale = sl.getSize();
|
||||
@@ -187,8 +196,16 @@ public final class EntitySnapshotBuilder {
|
||||
default -> "big";};
|
||||
} else if (e instanceof Cat c) {
|
||||
variant = keyOf(c.getCatType());
|
||||
if (c.isTamed()) collar = ColorUtil.dyeArgb(c.getCollarColor(), 0);
|
||||
} else if (e instanceof Wolf w) {
|
||||
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) {
|
||||
variant = keyOf(a.getVariant());
|
||||
} 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, …)
|
||||
// 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.
|
||||
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;
|
||||
if (eq != null) {
|
||||
try {
|
||||
@@ -263,8 +280,12 @@ public final class EntitySnapshotBuilder {
|
||||
|| (e instanceof HumanEntity he && he.getMainHand() == MainHand.LEFT);
|
||||
ItemSpec main = ItemSpecs.of(eq.getItemInMainHand());
|
||||
ItemSpec off = ItemSpecs.of(eq.getItemInOffHand());
|
||||
rightHand = leftHanded ? off : main;
|
||||
leftHand = leftHanded ? main : off;
|
||||
if (MOUTH_CARRIERS.contains(type)) {
|
||||
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();
|
||||
if (helmet != null && !helmet.getType().isAir() && armorAsset(helmet.getType()) == null) {
|
||||
headItem = ItemSpecs.of(helmet);
|
||||
@@ -312,7 +333,10 @@ public final class EntitySnapshotBuilder {
|
||||
e.isInsideVehicle(),
|
||||
rightHand,
|
||||
leftHand,
|
||||
headItem);
|
||||
headItem,
|
||||
collar,
|
||||
powered,
|
||||
mouthItem);
|
||||
}
|
||||
|
||||
/** 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);
|
||||
}
|
||||
|
||||
/** 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. */
|
||||
public static final int ALPHA_CUTOUT = 16;
|
||||
|
||||
@@ -101,6 +100,7 @@ public final class ColorUtil {
|
||||
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) {
|
||||
int a = alpha(argb);
|
||||
int r = clamp((int) (red(argb) * factor));
|
||||
@@ -109,6 +109,13 @@ public final class ColorUtil {
|
||||
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) {
|
||||
return v < 0 ? 0 : Math.min(v, 255);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user