primitive chunk table in world snapshot instead of colliding long-keyed hashmap, bounded cache for tinted/composited mob and banner textures, vanilla dump with nested model json, village benchmark and perf test scenes

This commit is contained in:
MineTec committed 2026-11-04 16:59:42 +01:00
1 parent b5baffebbd
commit 88bae41a03
10 files changed
+306 -38

No files matched your search

@@ -116,10 +116,11 @@ public final class Main extends JavaPlugin {
try (InputStream in =
dump.isFile() ? new java.io.FileInputStream(dump) : getResource("cem/vanilla_entity_models.json")) {
if (in != null) {
long t0 = System.nanoTime();
int n = cemLoader.load(in, getLogger());
getLogger()
.info("Loaded vanilla entity models" + (dump.isFile() ? " from " + dump.getName() : "") + " ("
+ n + " models total).");
+ n + " models total, " + (System.nanoTime() - t0) / 1_000_000 + " ms).");
}
} catch (Exception e) {
getLogger().severe("Failed to load vanilla entity models: " + e.getMessage());
@@ -0,0 +1,56 @@
package eu.mhsl.minecraft.pixelpics.render.entity;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
/**
* Bounded, thread-safe LRU cache of DERIVED textures — tinted / composited copies the bakers build per entity
* (sheep wool colours, villager type + profession + level overlays, dyed leather armor, collars, banner
* flags, …) — so identical mobs share one texture instead of re-tinting it on every bake.
*
* <p>Keys are built with {@link #key}: a value list of the inputs that determine the result. Source pixel
* grids may be part of the key; arrays compare by identity, which is exact because no grid is mutated once
* it is built (texture-cache grids live for the plugin's lifetime). Cached grids are shared and must not be
* mutated by callers.
* The computation runs outside the lock; concurrent misses on the same key may compute twice, the first
* stored result wins.
*/
public final class DerivedTextureCache {
private static final int[][] NONE = new int[0][0]; // cached "no texture"
private final Map<List<Object>, int[][]> map;
public DerivedTextureCache(int capacity) {
this.map = new LinkedHashMap<>(64, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<List<Object>, int[][]> eldest) {
return size() > capacity;
}
};
}
/** A cache key from the inputs (nulls allowed; arrays compare by identity). */
public static List<Object> key(Object... parts) {
return Arrays.asList(parts);
}
/** The cached texture for {@code key}, computing (and caching) it on a miss; null results are cached too. */
public int[][] get(List<Object> key, Supplier<int[][]> compute) {
int[][] v;
synchronized (map) {
v = map.get(key);
}
if (v == null) {
int[][] computed = compute.get();
synchronized (map) {
v = map.putIfAbsent(key, computed == null ? NONE : computed);
}
if (v == null) return computed;
}
return v == NONE ? null : v;
}
}
@@ -9,6 +9,7 @@ import eu.mhsl.minecraft.pixelpics.assets.model.Face;
import eu.mhsl.minecraft.pixelpics.render.entity.Affine;
import eu.mhsl.minecraft.pixelpics.render.entity.BlockEntityModels;
import eu.mhsl.minecraft.pixelpics.render.entity.BlockEntityState;
import eu.mhsl.minecraft.pixelpics.render.entity.DerivedTextureCache;
import eu.mhsl.minecraft.pixelpics.render.entity.EntityBaker;
import eu.mhsl.minecraft.pixelpics.render.entity.EntityCube;
import eu.mhsl.minecraft.pixelpics.render.entity.RenderedEntity;
@@ -37,6 +38,8 @@ public final class BlockEntityBaker implements EntityBaker<BlockEntityState> {
private final TextureCache textures;
private final SkinCache skins;
private final BitmapFont font;
// Composited banner flags (base + dyed pattern masks), shared by identical banners.
private final DerivedTextureCache derived = new DerivedTextureCache(256);
/** The sign/text font (shared with text-display rendering). */
public BitmapFont font() {
@@ -104,9 +107,17 @@ public final class BlockEntityBaker implements EntityBaker<BlockEntityState> {
* (one geometry, no coplanar layers).
*/
private int[][] bannerFlag(int[][] base, BlockEntityState s) {
int baseColor = s.baseColorArgb();
List<BlockEntityState.BannerPattern> patterns = List.copyOf(s.patterns());
return derived.get(
DerivedTextureCache.key("banner", base, baseColor, patterns),
() -> buildBannerFlag(base, baseColor, patterns));
}
private int[][] buildBannerFlag(int[][] base, int baseColor, List<BlockEntityState.BannerPattern> patterns) {
int[][] out = TextureOps.deepCopy(base);
if (s.baseColorArgb() != 0) overlayMask(out, "entity/banner/base", s.baseColorArgb());
for (BlockEntityState.BannerPattern pat : s.patterns()) {
if (baseColor != 0) overlayMask(out, "entity/banner/base", baseColor);
for (BlockEntityState.BannerPattern pat : patterns) {
overlayMask(out, "entity/banner/" + pat.patternKey(), pat.colorArgb());
}
return out;
@@ -7,6 +7,7 @@ import eu.mhsl.minecraft.pixelpics.assets.model.Face;
import eu.mhsl.minecraft.pixelpics.render.entity.Affine;
import eu.mhsl.minecraft.pixelpics.render.entity.BlockEntityState;
import eu.mhsl.minecraft.pixelpics.render.entity.BoxUv;
import eu.mhsl.minecraft.pixelpics.render.entity.DerivedTextureCache;
import eu.mhsl.minecraft.pixelpics.render.entity.EntityBaker;
import eu.mhsl.minecraft.pixelpics.render.entity.EntityCube;
import eu.mhsl.minecraft.pixelpics.render.entity.EntityModels;
@@ -48,6 +49,8 @@ public final class CemBaker implements EntityBaker<EntityState> {
private final TextureCache textures;
private final SkinCache skins;
private final Map<String, List<String>> subModelCache = new ConcurrentHashMap<>();
// Tinted / composited per-entity textures (wool, villager overlays, armor, collars, …), shared by identical mobs.
private final DerivedTextureCache derived = new DerivedTextureCache(512);
private volatile ItemGeometry items; // held/head items; null = not drawn
@@ -84,17 +87,21 @@ public final class CemBaker implements EntityBaker<EntityState> {
if (base.equals("villager") || base.equals("zombie_villager")) {
tex = compositeVillager(s, tex, base, ch != null && ch.babyModel);
} else if (s.markings() != null && tex != null) {
tex = TextureOps.deepCopy(tex); // horse coat markings
String m = "entity/horse/horse_markings_" + s.markings();
overlayIfPresent(tex, ch != null && ch.babyModel ? m + "_baby" : m);
String m = "entity/horse/horse_markings_" + s.markings(); // horse coat markings
String path = ch != null && ch.babyModel ? m + "_baby" : m;
int[][] src = tex;
tex = derived.get(DerivedTextureCache.key("markings", src, path), () -> {
int[][] out = TextureOps.deepCopy(src);
overlayIfPresent(out, path);
return out;
});
}
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
tex = tinted(tex, s.tint()); // white base body tinted with the body colour
}
boolean invisible = s.invisible();
@@ -401,9 +408,7 @@ public final class CemBaker implements EntityBaker<EntityState> {
if ("sheared".equals(s.variant()) || lm.texture() == null) continue;
int[][] t = texture(lm.texture());
if (t == null) continue;
t = TextureOps.deepCopy(t);
if (s.tint() != 0) TextureOps.tint(t, s.tint());
baked.addAll(pose.bake(lm, t));
baked.addAll(pose.bake(lm, tinted(t, s.tint())));
}
case "TropicalFishPatternLayer" -> addTropicalPattern(s, n, lm, pose, baked);
case "CatCollarLayer" -> {
@@ -411,9 +416,7 @@ public final class CemBaker implements EntityBaker<EntityState> {
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));
baked.addAll(pose.bake(lm, tinted(t, s.collar())));
}
case "CreeperPowerLayer", "WitherArmorLayer" -> {
// EnergySwirlLayer: charged creeper / wither below half health; drawn additively.
@@ -477,13 +480,17 @@ public final class CemBaker implements EntityBaker<EntityState> {
if (name.toLowerCase(Locale.ROOT).contains("small") != small) return;
int[][] t = texture("entity/fish/tropical_" + (small ? "a" : "b") + "_pattern_" + (idx % 6 + 1));
if (t == null) return;
t = TextureOps.deepCopy(t);
if (s.tint2() != 0) TextureOps.tint(t, s.tint2());
baked.addAll(pose.bake(lm, t));
baked.addAll(pose.bake(lm, tinted(t, s.tint2())));
}
/** entity/equipment/<layerType>/<asset> (+ a "_carpet"-stripped name; leather dye + overlay). */
/** entity/equipment/<layerType>/<asset> (+ a "_carpet"-stripped name; leather dye + overlay); cached. */
private int[][] equipmentTexture(String layerType, String asset, boolean baby, int dye) {
return derived.get(
DerivedTextureCache.key("equip", layerType, asset, baby, dye),
() -> buildEquipmentTexture(layerType, asset, baby, dye));
}
private int[][] buildEquipmentTexture(String layerType, String asset, boolean baby, int dye) {
String folder = "entity/equipment/" + layerType + "/";
int[][] t = null;
List<String> names = new ArrayList<>();
@@ -520,6 +527,10 @@ public final class CemBaker implements EntityBaker<EntityState> {
* xOffset: creeper {@code age·0.01}, wither {@code cos(age·0.02)·3}.
*/
private int[][] energySwirl(String path, boolean wither) {
return derived.get(DerivedTextureCache.key("swirl", path, wither), () -> buildEnergySwirl(path, wither));
}
private int[][] buildEnergySwirl(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;
@@ -539,16 +550,29 @@ public final class CemBaker implements EntityBaker<EntityState> {
/** 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) {
return derived.get(
DerivedTextureCache.key("collar", tex, base, baby, dye), () -> buildWithCollar(tex, base, baby, dye));
}
private int[][] buildWithCollar(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);
TextureOps.overlay(out, tinted(collar, dye));
return out;
}
/** {@code src} multiplied by an ARGB tint (cached; {@code src} itself for no tint). */
private int[][] tinted(int[][] src, int argb) {
if (argb == 0) return src;
return derived.get(DerivedTextureCache.key("tint", src, argb), () -> {
int[][] out = TextureOps.deepCopy(src);
TextureOps.tint(out, argb);
return out;
});
}
// --- texture resolution --------------------------------------------------------------------------
private int[][] texture(String path) {
@@ -602,6 +626,15 @@ public final class CemBaker implements EntityBaker<EntityState> {
private int[][] compositeVillager(EntityState s, int[][] base, String folder, boolean babyModel) {
if (base == null) return null;
// Only the inputs the composite reads: babies ignore profession and level.
String prof = babyModel ? null : s.profession();
int level = babyModel ? 0 : s.villagerLevel();
return derived.get(
DerivedTextureCache.key("villager", base, folder, babyModel, s.variant(), prof, level),
() -> buildVillager(s, base, folder, babyModel));
}
private int[][] buildVillager(EntityState s, int[][] base, String folder, boolean babyModel) {
int[][] out = TextureOps.deepCopy(base);
if (babyModel) {
// Baby villagers: their own biome-type clothing set; no profession.
@@ -658,7 +691,8 @@ public final class CemBaker implements EntityBaker<EntityState> {
if (piece == null) return;
CemModelLoader.CemModel model = models.vanilla(modelName);
if (model == null) return;
int[][] tex = buildArmorTexture(piece, layerFolder);
int[][] tex = derived.get(
DerivedTextureCache.key("armor", piece, layerFolder), () -> buildArmorTexture(piece, layerFolder));
if (tex != null) baked.addAll(pose.bake(model, tex));
}
@@ -713,9 +747,14 @@ public final class CemBaker implements EntityBaker<EntityState> {
if (model == null) return; // renderer without a WingsLayer
int[][] tex = texture("entity/equipment/wings/elytra");
if (tex == null) return;
int[][] out = TextureOps.deepCopy(tex);
if (piece.glint()) applyGlint(out);
baked.addAll(pose.bake(model, out));
if (piece.glint()) {
tex = derived.get(DerivedTextureCache.key("elytra_glint", tex), () -> {
int[][] out = TextureOps.deepCopy(texture("entity/equipment/wings/elytra"));
applyGlint(out);
return out;
});
}
baked.addAll(pose.bake(model, tex));
}
private RenderedEntity fallbackBox(EntityState s, int[][] tex) {
@@ -211,8 +211,12 @@ public final class CemModelLoader {
try {
JsonObject entry = e.getValue().getAsJsonObject();
if (!entry.has("model")) continue;
JsonObject model =
JsonParser.parseString(entry.get("model").getAsString()).getAsJsonObject();
// The vanilla dump nests the jem as an object; the CEM template set (and dumps written before
// 2026-11) embed it as a JSON string that needs a second parse.
JsonElement m = entry.get("model");
JsonObject model = m.isJsonObject()
? m.getAsJsonObject()
: JsonParser.parseString(m.getAsString()).getAsJsonObject();
int tw = model.getAsJsonArray("textureSize").get(0).getAsInt();
int th = model.getAsJsonArray("textureSize").get(1).getAsInt();
List<CemPart> parts = new ArrayList<>();
@@ -8,35 +8,75 @@ import org.bukkit.block.data.BlockData;
/**
* An immutable, thread-safe view of a bounded region of the world, backed by {@link ChunkSnapshot}s.
* Block/biome lookups outside the captured region return air/null so rays simply terminate there.
*
* <p>The chunks live in a primitive open-addressing table (linear probing, Fibonacci hashing of the full
* {@link #chunkKey}), not a {@code HashMap<Long, …>}: {@code Long.hashCode} of the key is {@code cx ^ cz}, so
* every diagonal of the captured rectangle collided into treeified bins and each block lookup paid for
* boxing, {@code TreeNode.find} and its reflective {@code comparableClassFor}. Keys are compared in full,
* so lookups stay exact.
*/
public final class WorldSnapshot {
private final Map<Long, ChunkSnapshot> chunks;
private static final long GOLDEN = 0x9E3779B97F4A7C15L;
private final long[] keys;
private final ChunkSnapshot[] values; // null = empty slot
private final int mask;
private final int shift;
private final int minY;
private final int maxY; // exclusive
private final BlockData air;
public WorldSnapshot(Map<Long, ChunkSnapshot> chunks, int minY, int maxY, BlockData air) {
this.chunks = chunks;
// Power-of-two capacity >= 2·size (load factor <= 0.5), at least 2 so the shift stays below 64.
int cap = Math.max(2, Integer.highestOneBit(Math.max(1, chunks.size()) * 2 - 1) << 1);
this.keys = new long[cap];
this.values = new ChunkSnapshot[cap];
this.mask = cap - 1;
this.shift = 64 - Integer.numberOfTrailingZeros(cap);
for (Map.Entry<Long, ChunkSnapshot> e : chunks.entrySet()) {
if (e.getValue() == null) continue;
long key = e.getKey();
int i = slot(key);
while (values[i] != null) i = (i + 1) & mask;
keys[i] = key;
values[i] = e.getValue();
}
this.minY = minY;
this.maxY = maxY;
this.air = air;
}
/** Unique key of a chunk position (x in the high, z in the low 32 bits). */
public static long chunkKey(int chunkX, int chunkZ) {
return ((long) chunkX << 32) ^ (chunkZ & 0xFFFFFFFFL);
}
private int slot(long key) {
return (int) ((key * GOLDEN) >>> shift);
}
private ChunkSnapshot chunk(int chunkX, int chunkZ) {
long key = chunkKey(chunkX, chunkZ);
int i = slot(key);
ChunkSnapshot cs;
while ((cs = values[i]) != null) {
if (keys[i] == key) return cs;
i = (i + 1) & mask;
}
return null;
}
public BlockData getBlockData(int x, int y, int z) {
if (y < minY || y >= maxY) return air;
ChunkSnapshot cs = chunks.get(chunkKey(x >> 4, z >> 4));
ChunkSnapshot cs = chunk(x >> 4, z >> 4);
if (cs == null) return air;
return cs.getBlockData(x & 15, y, z & 15);
}
public Biome getBiome(int x, int y, int z) {
if (y < minY || y >= maxY) return null;
ChunkSnapshot cs = chunks.get(chunkKey(x >> 4, z >> 4));
ChunkSnapshot cs = chunk(x >> 4, z >> 4);
if (cs == null) return null;
return cs.getBiome(x & 15, y, z & 15);
}
@@ -1 +1 @@
{"version":"26.3","models":{"acacia_boat":{"model":"{\"textureSize\":[128,64],\"absolute\":true,\"layer\":\"boat/acacia#main\",\"models\":[{\"part\":\"root\",\"pivot\":[0.0,24.0,0.0],\"rotate\":[0.0,0.0,0.0],\"submodels\":[{\"part\":\"right_paddle\",\"pivot\":[-3.0,29.0,-9.0],\"rotate\":[37.5,-142.1347,11.25],\"boxes\":[{\"coordinates\":[-4.0,27.0,-14.0,2.0,2.0,18.0],\"textureOffset\":[62.0,20.0]},{\"coordinates\":[-4.001,26.0,-1.0,1.0,6.0,7.0],\"textureOffset\":[62.0,20.0]}]},{\"part\":\"left_paddle\",\"pivot\":[-3.0,29.0,9.0],\"rotate\":[37.5,-37.8653,11.25],\"boxes\":[{\"coordinates\":[-4.0,27.0,4.0,2.0,2.0,18.0],\"textureOffset\":[62.0,0.0]},{\"coordinates\":[-2.999,26.0,17.0,1.0,6.0,7.0],\"textureOffset\":[62.0,0.0]}]},{\"part\":\"left\",\"pivot\":[0.0,20.0,9.0],\"rotate\":[0.0,0.0,0.0],\"boxes\":[{\"coordinates\":[-14.0,21.0,8.0,28.0,6.0,2.0],\"textureOffset\":[0.0,43.0]}]},{\"part\":\"bottom\",\"pivot\":[0.0,21.0,1.0],\"rotate\":[-90.0,0.0,0.0],\"boxes\":[{\"coordinates\":[-14.0,14.0,-2.0,28.0,16.0,3.0],\"textureOffset\":[0.0,0.0]}]},{\"part\":\"back\",\"pivot\":[15.0,20.0,4.0],\"rotate\":[0.0,-270.0,0.0],\"boxes\":[{\"coordinates\":[10.0,21.0,3.0,18.0,6.0,2.0],\"textureOffset\":[0.0,19.0]}]},{\"part\":\"front\",\"pivot\":[-15.0,20.0,0.0],\"rotate\":[0.0,-90.0,0.0],\"boxes\":[{\"coordinates\":[-23.0,21.0,-1.0,16.0,6.0,2.0],\"textureOffset\":[0.0,27.0]}]},{\"part\":\"right\",\"pivot\":[0.0,20.0,-9.0],\"rotate\":[0.0,-180.0,0.0],\"boxes\":[{\"coordinates\":[-14.0,21.0,-10.0,28.0,6.0,2.0],\"textureOffset\":[0.0,35.0]}]}]}]}"},"acacia_boat/waterPatchModel":{"model":"{\"textureSize\":[0,0],\"absolute\":true,\"layer\":\"boat#water_patch\",\"models\":[{\"part\":\"root\",\"pivot\":[0.0,24.0,0.0],\"rotate\":[0.0,0.0,0.0],\"submodels\":[{\"part\":\"water_patch\",\"pivot\":[0.0,27.0,1.0],\"rotate\":[-90.0,0.0,0.0],\"boxes\":[{\"coordinates\":[-14.0,20.0,-2.0,28.0,16.0,3.0],\"textureOffset\":[0.0,0.0]}]}]}]}"},"acacia_chest_boat":{"model":"{\"textureSize\":[128,128],\"absolute\":true,\"layer\":\"chest_boat/acacia#main\",\"models\":[{\"part\":\"root\",\"pivot\":[0.0,24.0,0.0],\"rotate\":[0.0,0.0,0.0],\"submodels\":[{\"part\":\"chest_bottom\",\"pivot\":[2.0,29.0,-6.0],\"rotate\":[0.0,90.0,0.0],\"boxes\":[{\"coordinates\":[-10.0,21.0,-6.0,12.0,8.0,12.0],\"textureOffset\":[0.0,76.0]}]},{\"part\":\"right_paddle\",\"pivot\":[-3.0,29.0,-9.0],\"rotate\":[37.5,-142.1347,11.25],\"boxes\":[{\"coordinates\":[-4.0,27.0,-14.0,2.0,2.0,18.0],\"textureOffset\":[62.0,20.0]},{\"coordinates\":[-4.001,26.0,-1.0,1.0,6.0,7.0],\"textureOffset\":[62.0,20.0]}]},{\"part\":\"left_paddle\",\"pivot\":[-3.0,29.0,9.0],\"rotate\":[37.5,-37.8653,11.25],\"boxes\":[{\"coordinates\":[-4.0,27.0,4.0,2.0,2.0,18.0],\"textureOffset\":[62.0,0.0]},{\"coordinates\":[-2.999,26.0,17.0,1.0,6.0,7.0],\"textureOffset\":[62.0,0.0]}]},{\"part\":\"left\",\"pivot\":[0.0,20.0,9.0],\"rotate\":[0.0,0.0,0.0],\"boxes\":[{\"coordinates\":[-14.0,21.0,8.0,28.0,6.0,2.0],\"textureOffset\":[0.0,43.0]}]},{\"part\":\"bottom\",\"pivot\":[0.0,21.0,1.0],\"rotate\":[-90.0,0.0,0.0],\"boxes\":[{\"coordinates\":[-14.0,14.0,-2.0,28.0,16.0,3.0],\"textureOffset\":[0.0,0.0]}]},{\"part\":\"back\",\"pivot\":[15.0,20.0,4.0],\"rotate\":[0.0,-270.0,0.0],\"boxes\":[{\"coordinates\":[10.0,21.0,3.0,18.0,6.0,2.0],\"textureOffset\":[0.0,19.0]}]},{\"part\":\"chest_lid\",\"pivot\":[2.0,33.0,-6.0],\"rotate\":[0.0,90.0,0.0],\"boxes\":[{\"coordinates\":[-10.0,29.0,-6.0,12.0,4.0,12.0],\"textureOffset\":[0.0,59.0]}]},{\"part\":\"front\",\"pivot\":[-15.0,20.0,0.0],\"rotate\":[0.0,-90.0,0.0],\"boxes\":[{\"coordinates\":[-23.0,21.0,-1.0,16.0,6.0,2.0],\"textureOffset\":[0.0,27.0]}]},{\"part\":\"right\",\"pivot\":[0.0,20.0,-9.0],\"rotate\":[0.0,-180.0,0.0],\"boxes\":[{\"coordinates\":[-14.0,21.0,-10.0,28.0,6.0,2.0],\"textureOffset\":[0.0,35.0]}]},{\"part\":\"chest_lock\",\"pivot\":[1.0,30.0,-1.0],\"rotate\":[0.0,90.0,0.0],\"boxes\":[{\"coordinates\":[-1.0,26.0,-1.0,2.0,4.0,1.0],\"textureOffset\":[0.0,59.0]}]}]}]}"},"acacia_chest_boat/waterPatchModel":{"model":"{\"textureSize\":[0,0],\"absolute\":true,\"layer\":\"boat#water_patch\",\"models\":[{\"part\":\"root\",\"pivot\":[0.0,24.0,0.0],\"rotate\":[0.0,0.0,0.0],\"submodels\":[{\"part\":\"water_patch\",\"pivot\":[0.0,27.0,1.0],\"rotate\":[-90.0,0.0,0.0],\"boxes\":[{\"coordinates\":[-14.0,20.0,-2.0,28.0,16.0,3.0],\"textureOffset\":[0.0,0.0]}]}]}]}"},"allay":{"model":"{\"textureSize\":[32,32],\"absolute\":true,\"layer\":\"allay#main\",\"models\":[{\"part\":\"root\",\"pivot\":[0.0,0.25,0.0],\"rotate\":[0.0,0.0,0.0],\"submodels\":[{\"part\":\"head\",\"pivot\":[0.0,4.24,0.0],\"rotate\":[0.0,0.0,0.0],\"boxes\":[{\"coordinates\":[-2.5,4.24,-2.5,5.0,5.0,5.0],\"textureOffset\":[0.0,0.0]}]},{\"part\":\"body\",\"pivot\":[0.0,4.25,0.0],\"rotate\":[0.0,0.0,0.0],\"boxes\":[{\"coordinates\":[-1.5,0.25,-1.0,3.0,4.0,2.0],\"textureOffset\":[0.0,10.0]},{\"coordinates\":[-1.5,-0.75,-1.0,3.0,5.0,2.0],\"textureOffset\":[0.0,16.0],\"sizeAdd\":-0.2}],\"submodels\":[{\"part\":\"right_arLine truncated
{"version":"26.3","models":{"acacia_boat":{"model":{"textureSize":[128,64],"absolute":true,"layer":"boat/acacia#main","models":[{"part":"root","pivot":[0.0,24.0,0.0],"rotate":[0.0,0.0,0.0],"submodels":[{"part":"right_paddle","pivot":[-3.0,29.0,-9.0],"rotate":[37.5,-142.1347,11.25],"boxes":[{"coordinates":[-4.0,27.0,-14.0,2.0,2.0,18.0],"textureOffset":[62.0,20.0]},{"coordinates":[-4.001,26.0,-1.0,1.0,6.0,7.0],"textureOffset":[62.0,20.0]}]},{"part":"left_paddle","pivot":[-3.0,29.0,9.0],"rotate":[37.5,-37.8653,11.25],"boxes":[{"coordinates":[-4.0,27.0,4.0,2.0,2.0,18.0],"textureOffset":[62.0,0.0]},{"coordinates":[-2.999,26.0,17.0,1.0,6.0,7.0],"textureOffset":[62.0,0.0]}]},{"part":"left","pivot":[0.0,20.0,9.0],"rotate":[0.0,0.0,0.0],"boxes":[{"coordinates":[-14.0,21.0,8.0,28.0,6.0,2.0],"textureOffset":[0.0,43.0]}]},{"part":"bottom","pivot":[0.0,21.0,1.0],"rotate":[-90.0,0.0,0.0],"boxes":[{"coordinates":[-14.0,14.0,-2.0,28.0,16.0,3.0],"textureOffset":[0.0,0.0]}]},{"part":"back","pivot":[15.0,20.0,4.0],"rotate":[0.0,-270.0,0.0],"boxes":[{"coordinates":[10.0,21.0,3.0,18.0,6.0,2.0],"textureOffset":[0.0,19.0]}]},{"part":"front","pivot":[-15.0,20.0,0.0],"rotate":[0.0,-90.0,0.0],"boxes":[{"coordinates":[-23.0,21.0,-1.0,16.0,6.0,2.0],"textureOffset":[0.0,27.0]}]},{"part":"right","pivot":[0.0,20.0,-9.0],"rotate":[0.0,-180.0,0.0],"boxes":[{"coordinates":[-14.0,21.0,-10.0,28.0,6.0,2.0],"textureOffset":[0.0,35.0]}]}]}]}},"acacia_boat/waterPatchModel":{"model":{"textureSize":[0,0],"absolute":true,"layer":"boat#water_patch","models":[{"part":"root","pivot":[0.0,24.0,0.0],"rotate":[0.0,0.0,0.0],"submodels":[{"part":"water_patch","pivot":[0.0,27.0,1.0],"rotate":[-90.0,0.0,0.0],"boxes":[{"coordinates":[-14.0,20.0,-2.0,28.0,16.0,3.0],"textureOffset":[0.0,0.0]}]}]}]}},"acacia_chest_boat":{"model":{"textureSize":[128,128],"absolute":true,"layer":"chest_boat/acacia#main","models":[{"part":"root","pivot":[0.0,24.0,0.0],"rotate":[0.0,0.0,0.0],"submodels":[{"part":"chest_bottom","pivot":[2.0,29.0,-6.0],"rotate":[0.0,90.0,0.0],"boxes":[{"coordinates":[-10.0,21.0,-6.0,12.0,8.0,12.0],"textureOffset":[0.0,76.0]}]},{"part":"right_paddle","pivot":[-3.0,29.0,-9.0],"rotate":[37.5,-142.1347,11.25],"boxes":[{"coordinates":[-4.0,27.0,-14.0,2.0,2.0,18.0],"textureOffset":[62.0,20.0]},{"coordinates":[-4.001,26.0,-1.0,1.0,6.0,7.0],"textureOffset":[62.0,20.0]}]},{"part":"left_paddle","pivot":[-3.0,29.0,9.0],"rotate":[37.5,-37.8653,11.25],"boxes":[{"coordinates":[-4.0,27.0,4.0,2.0,2.0,18.0],"textureOffset":[62.0,0.0]},{"coordinates":[-2.999,26.0,17.0,1.0,6.0,7.0],"textureOffset":[62.0,0.0]}]},{"part":"left","pivot":[0.0,20.0,9.0],"rotate":[0.0,0.0,0.0],"boxes":[{"coordinates":[-14.0,21.0,8.0,28.0,6.0,2.0],"textureOffset":[0.0,43.0]}]},{"part":"bottom","pivot":[0.0,21.0,1.0],"rotate":[-90.0,0.0,0.0],"boxes":[{"coordinates":[-14.0,14.0,-2.0,28.0,16.0,3.0],"textureOffset":[0.0,0.0]}]},{"part":"back","pivot":[15.0,20.0,4.0],"rotate":[0.0,-270.0,0.0],"boxes":[{"coordinates":[10.0,21.0,3.0,18.0,6.0,2.0],"textureOffset":[0.0,19.0]}]},{"part":"chest_lid","pivot":[2.0,33.0,-6.0],"rotate":[0.0,90.0,0.0],"boxes":[{"coordinates":[-10.0,29.0,-6.0,12.0,4.0,12.0],"textureOffset":[0.0,59.0]}]},{"part":"front","pivot":[-15.0,20.0,0.0],"rotate":[0.0,-90.0,0.0],"boxes":[{"coordinates":[-23.0,21.0,-1.0,16.0,6.0,2.0],"textureOffset":[0.0,27.0]}]},{"part":"right","pivot":[0.0,20.0,-9.0],"rotate":[0.0,-180.0,0.0],"boxes":[{"coordinates":[-14.0,21.0,-10.0,28.0,6.0,2.0],"textureOffset":[0.0,35.0]}]},{"part":"chest_lock","pivot":[1.0,30.0,-1.0],"rotate":[0.0,90.0,0.0],"boxes":[{"coordinates":[-1.0,26.0,-1.0,2.0,4.0,1.0],"textureOffset":[0.0,59.0]}]}]}]}},"acacia_chest_boat/waterPatchModel":{"model":{"textureSize":[0,0],"absolute":true,"layer":"boat#water_patch","models":[{"part":"root","pivot":[0.0,24.0,0.0],"rotate":[0.0,0.0,0.0],"submodels":[{"part":"water_patch","pivot":[0.0,27.0,1.0],"rotate":[-90.0,0.0,0.0],"boxes":[{"coordinates":[-14.0,20.0,-2.0,28.0,16.0,3.0],"textureOffset":[0.0,0.0]}]}]}]}},"allay":{"model":{"textureSize":[32,32],"absolute":true,"layer":"allay#main","models":[{"part":"root","pivot":[0.0,0.25,0.0],"rotate":[0.0,0.0,0.0],"submodels":[{"part":"head","pivot":[0.0,4.24,0.0],"rotate":[0.0,0.0,0.0],"boxes":[{"coordinates":[-2.5,4.24,-2.5,5.0,5.0,5.0],"textureOffset":[0.0,0.0]}]},{"part":"body","pivot":[0.0,4.25,0.0],"rotate":[0.0,0.0,0.0],"boxes":[{"coordinates":[-1.5,0.25,-1.0,3.0,4.0,2.0],"textureOffset":[0.0,10.0]},{"coordinates":[-1.5,-0.75,-1.0,3.0,5.0,2.0],"textureOffset":[0.0,16.0],"sizeAdd":-0.2}],"submodels":[{"part":"right_arm","pivot":[1.75,3.75,0.0],"rotate":[0.0,0.0,25.0],"boxes":[{"coordinates":[1.5,0.25,-1.0,1.0,4.0,2.0],"textureOffset":[23.0,0.0],"sizeAdd":-0.01}]},{"part":"left_arm","pivot":[-1.75,3.75,0.0],"rotate":[0.0,0.0,-25.0],"boxes":[{"coordinates":[-2.5,0.25,-1.0,1.0,4.0,2.0],"textureOffset":[23.0,6.0],"sizeAdd":-0.01}]},{"part":"right_wing","pivot":[0.5,4.25,0.6],"rotate":[-25.0,18.0,0.0],"boxes":[{"coordinates":[0.5,-1.75,0.6,0.0,Line truncated
+49 -1
View File
@@ -24,7 +24,7 @@ import java.util.logging.Logger;
* times (after {@code -Dwarmup} runs), prints the median wall time and writes one PNG per scene to
* {@code -Dout} (default ~/Dokumente/PixelPics-Entity-Renders/bench) for before/after pixel diffs.
* Args: scene names to run (default all): farm_mobs, farm_empty (same view, no entities), farm_mobs_close,
* farm_items, normal. {@code -Djava.util.concurrent.ForkJoinPool.common.parallelism=N} pins the thread count.
* farm_items, village, normal. {@code -Djava.util.concurrent.ForkJoinPool.common.parallelism=N} pins the thread count.
*/
public class EntityBenchmark {
@@ -114,6 +114,33 @@ public class EntityBenchmark {
}
out.add(new Scene("farm_items", new Vector(0, 7, -1), new Vector(7, 1, 7), List.of(), items));
// Village: 300 mobs with per-mob texture compositing (villager type/profession/level overlays, dyed
// sheep wool, dyed leather armor, tinted wolf collars) — exercises the tinted-texture cache.
List<EntityState> village = new ArrayList<>();
String[] vTypes = {"plains", "desert", "jungle", "savanna", "snow", "swamp", "taiga"};
String[] profs = {"armorer", "butcher", "cartographer", "cleric", "farmer", "fisherman", "fletcher",
"leatherworker", "librarian", "mason", "shepherd", "toolsmith", "weaponsmith", "nitwit", "none"};
Random rv = new Random(7);
for (int i = 0; i < 15; i++) {
for (int j = 0; j < 20; j++) {
double x = i * 1.4 + rv.nextDouble() * 0.3, z = j * 1.4 + rv.nextDouble() * 0.3;
float yaw = rv.nextFloat() * 360;
int dye = DYES[rv.nextInt(DYES.length)];
EntityState m = switch (rv.nextInt(4)) {
case 0 -> villager(x, z, yaw, vTypes[rv.nextInt(vTypes.length)], profs[rv.nextInt(profs.length)],
1 + rv.nextInt(5), rv.nextInt(6) == 0);
case 1 -> with(mob("sheep", x, 1, z, yaw, rv.nextInt(6) == 0), dye, null, 0);
case 2 -> {
EntityState.EquipPiece le = new EntityState.EquipPiece("leather", dye, null, null, false);
yield with(mob("zombie", x, 1, z, yaw, false), 0, new EntityState.Equipment(le, le, le, le), 0);
}
default -> with(mob("wolf", x, 1, z, yaw, rv.nextInt(6) == 0), 0, null, dye);
};
village.add(m);
}
}
out.add(new Scene("village", new Vector(-5, 10, -5), new Vector(10, 1, 13), village, List.of()));
// Normal scene: a handful of mobs incl. translucent (slime) and additive (charged creeper) layers,
// a text display with the default translucent background and two items.
List<EntityState> few = new ArrayList<>();
@@ -142,11 +169,32 @@ public class EntityBenchmark {
false, 0, 0, 0, false, null, null, null, 0, false, null);
}
/** Vanilla DyeColor texture colours (white … black). */
static final int[] DYES = {0xFFF9FFFE, 0xFFF9801D, 0xFFC74EBD, 0xFF3AB3DA, 0xFFFED83D, 0xFF80C71F, 0xFFF38BAA,
0xFF474F52, 0xFF9D9D97, 0xFF169C9C, 0xFF8932B8, 0xFF3C44AA, 0xFF835432, 0xFF5E7C16, 0xFFB02E26, 0xFF1D1D21};
static EntityState villager(double x, double z, float yaw, String type, String prof, int level, boolean baby) {
return new EntityState("villager", x, 1, z, yaw, baby, 0.6, 1.95,
false, null, false, type, 0, 1.0, prof, level,
null, false, false, false, null, null,
false, 0, 0, 0, false, null, null, null, 0, false, null);
}
/** {@code s} with wool tint, humanoid armor and collar dye replaced. */
static EntityState with(EntityState s, int tint, EntityState.Equipment eq, int collar) {
return new EntityState(s.typeKey(), s.x(), s.y(), s.z(), s.bodyYaw(), s.baby(), s.width(), s.height(),
s.player(), s.skinUrl(), s.slim(), s.variant(), tint, s.sizeScale(), s.profession(), s.villagerLevel(),
s.markings(), s.saddle(), s.chest(), s.arms(), s.bodyEquip(), eq, s.invisible(), s.pitch(),
s.bodyDye(), s.tint2(), s.passenger(), s.rightHand(), s.leftHand(), s.headItem(), collar, s.powered(),
s.mouthItem());
}
/** The variant the game always supplies (cow/pig/chicken have no texture without one). */
static String variant(String type) {
return switch (type) {
case "villager" -> "plains";
case "cow", "pig", "chicken" -> "temperate";
case "wolf" -> "pale";
default -> null;
};
}
+5 -5
View File
@@ -1,4 +1,3 @@
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
@@ -31,7 +30,8 @@ import net.minecraft.world.entity.EntityType;
/**
* Dumps the vanilla Java entity models straight from an (unobfuscated, 26.x+) Minecraft CLIENT jar into
* the CEM-template JSON layout PixelPics loads ({@code {"models": {name: {"model": "<jem json>"}}}}).
* the CEM-template JSON layout PixelPics loads ({@code {"models": {name: {"model": {<jem>}}}}}; the jem is a
* nested object, not the template set's embedded JSON string, so the plugin parses the file only once).
*
* <p>Unlike the third-party CEM template set (rest geometry only), every entity model is instantiated
* through its real renderer and posed by its real {@code setupAnim} with a default (idle, adult, not
@@ -152,7 +152,7 @@ public class VanillaModelDump {
parts.add(part("root", rootDef, baked, 0, 0, 0));
jem.add("models", parts);
JsonObject entry = new JsonObject();
entry.addProperty("model", new Gson().toJson(jem));
entry.add("model", jem); // nested object, not a JSON string: parsed once at plugin start
out.putIfAbsent(name, entry);
} catch (Throwable t) {
failed.add(name + " (layer: " + t + ")");
@@ -315,7 +315,7 @@ public class VanillaModelDump {
JsonObject jem = dump(factory.apply(set.bakeLayer(loc)), state, name, failed);
if (jem == null) return;
JsonObject entry = new JsonObject();
entry.addProperty("model", new Gson().toJson(jem));
entry.add("model", jem);
out.put(name, entry);
} catch (Throwable t) {
failed.add(name + " (" + t + ")");
@@ -368,7 +368,7 @@ public class VanillaModelDump {
String tex = textures.get(f);
if (tex != null) jem.addProperty("texture", tex);
JsonObject entry = new JsonObject();
entry.addProperty("model", new Gson().toJson(jem));
entry.add("model", jem);
out.put(name, entry);
}
}
+69
View File
@@ -141,6 +141,10 @@ public class PPTest extends JavaPlugin {
scenes.put("perf_farm", new Scene("perf_farm", this::perfFarm, -6, 9, -13, 4, 0, 0));
scenes.put("perf_farm_close", new Scene("perf_farm_close", this::perfFarm, 0.5, 3, -11.5, 5, 0.5, -4));
scenes.put("perf_items", new Scene("perf_items", this::perfItems, 0.5, 5, -10, 5, 0, -2));
// Per-mob texture compositing (tinted-texture cache): villager type/profession/level, sheep colours, dyed
// leather armor, wolf collars and patterned banners, all from a seeded RNG (deterministic).
scenes.put("perf_village", new Scene("perf_village", this::perfVillage, -6, 9, -13, 4, 0, 0));
scenes.put("perf_village_close", new Scene("perf_village_close", this::perfVillage, 0.5, 3, -11.5, 5, 0.5, -4));
}
private void perfFarm(Location o) {
@@ -156,6 +160,71 @@ public class PPTest extends JavaPlugin {
}
}
private void perfVillage(Location o) {
Random r = new Random(11);
List<Villager.Type> vTypes = registry(io.papermc.paper.registry.RegistryKey.VILLAGER_TYPE);
List<Villager.Profession> profs = registry(io.papermc.paper.registry.RegistryKey.VILLAGER_PROFESSION);
DyeColor[] dyes = DyeColor.values();
for (int i = 0; i < 14; i++)
for (int j = 0; j < 16; j++) {
double x = -5 + i * 1.2, z = -10 + j * 1.2;
DyeColor dye = dyes[r.nextInt(dyes.length)];
LivingEntity e;
switch (r.nextInt(4)) {
case 0 -> {
Villager v = rfSpawn(o, x, 0, z, 0, Villager.class);
v.setVillagerType(vTypes.get(r.nextInt(vTypes.size())));
if (r.nextInt(6) == 0) v.setBaby();
else {
v.setProfession(profs.get(r.nextInt(profs.size())));
v.setVillagerLevel(1 + r.nextInt(5));
}
e = v;
}
case 1 -> {
Sheep sh = rfSpawn(o, x, 0, z, 0, Sheep.class);
sh.setColor(dye);
if (r.nextInt(6) == 0) sh.setBaby();
e = sh;
}
case 2 -> {
Zombie zo = rfSpawn(o, x, 0, z, 0, Zombie.class);
Material[] pieces = {Material.LEATHER_HELMET, Material.LEATHER_CHESTPLATE,
Material.LEATHER_LEGGINGS, Material.LEATHER_BOOTS};
ItemStack[] armor = new ItemStack[4];
for (int k = 0; k < 4; k++) {
armor[k] = it(pieces[k]);
org.bukkit.inventory.meta.LeatherArmorMeta m =
(org.bukkit.inventory.meta.LeatherArmorMeta) armor[k].getItemMeta();
m.setColor(dye.getColor());
armor[k].setItemMeta(m);
}
zo.getEquipment().setHelmet(armor[0]);
zo.getEquipment().setChestplate(armor[1]);
zo.getEquipment().setLeggings(armor[2]);
zo.getEquipment().setBoots(armor[3]);
e = zo;
}
default -> {
Wolf wo = rfSpawn(o, x, 0, z, 0, Wolf.class);
tame(wo, dye);
if (r.nextInt(6) == 0) wo.setBaby();
e = wo;
}
}
e.setBodyYaw(r.nextInt(8) * 45);
}
for (int b = 0; b < 10; b++) {
int x = -4 + b * 2;
put(o, x, 0, 10, dyes[r.nextInt(dyes.length)].name().toLowerCase() + "_banner[rotation=" + r.nextInt(16) + "]");
if (o.clone().add(x, 0, 10).getBlock().getState() instanceof org.bukkit.block.Banner bn) {
bn.addPattern(new org.bukkit.block.banner.Pattern(dyes[r.nextInt(dyes.length)], org.bukkit.block.banner.PatternType.STRIPE_TOP));
bn.addPattern(new org.bukkit.block.banner.Pattern(dyes[r.nextInt(dyes.length)], org.bukkit.block.banner.PatternType.CREEPER));
bn.update(true, false);
}
}
}
private void perfItems(Location o) {
Random r = new Random(7);
Material[] mats = {Material.WHEAT, Material.CARROT, Material.EGG, Material.BONE, Material.ROTTEN_FLESH,