simplify tools: shared harness setup (pipeline, block proxy, render job, framing, record copy, label cells, contact sheets) for the standalone renderers and the benchmark, block-entity scenes as name/yaw records, one dump pipeline for mob and avatar renderers with a shared submit helper, transform record and jem/entry/subset helpers, posed block entities from direct layer constants, pptest scenes registered once with settle-tick and resolution tables and put() for all blocks, jdk detection and full error log in the dump script, pptest jar rebuilt instead of updated, gradle task printing the tool classpath, comments trimmed

This commit is contained in:
MineTec committed 2026-11-05 03:24:30 +01:00
1 parent b242adb6d2
commit 02e052787c
11 files changed
+1449 -1686

No files matched your search

+194 -321
View File
@@ -1,35 +1,38 @@
import eu.mhsl.minecraft.pixelpics.assets.*;
import eu.mhsl.minecraft.pixelpics.render.entity.*;
import eu.mhsl.minecraft.pixelpics.render.entity.BlockEntityState.*;
import eu.mhsl.minecraft.pixelpics.render.entity.cem.*;
import eu.mhsl.minecraft.pixelpics.render.render.*;
import eu.mhsl.minecraft.pixelpics.render.sky.SkyContext;
import eu.mhsl.minecraft.pixelpics.render.render.RenderJob;
import eu.mhsl.minecraft.pixelpics.render.snapshot.WorldSnapshot;
import eu.mhsl.minecraft.pixelpics.render.tint.BiomeTintProvider;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.data.BlockData;
import eu.mhsl.minecraft.pixelpics.render.util.ColorUtil;
import org.bukkit.DyeColor;
import org.bukkit.util.Vector;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.lang.reflect.Proxy;
import java.util.*;
import java.util.List;
import java.util.logging.Logger;
/** Standalone (no server) block-entity renderer: each scene framed and labelled into a contact sheet. */
/**
* Standalone (no server, no block models: sign boards, tables, campfires, cages are absent) block-entity and
* decoration renderer: each scene framed and labelled -> {@code <scene>.png} + {@code sheet.png} in {@code -Dout}
* (default ~/Dokumente/PixelPics-BlockEntity-Renders). Christmas chests: {@code -Dpixelpics.testDate=2026-12-25}.
* Build: see {@link Harness}.
*/
public class BlockEntityTestRender {
static final String ROOT = "/home/elias/Dokumente/mcTestServer/plugins/PixelPics";
static final double V_FOV_HALF = Math.toRadians(eu.mhsl.minecraft.pixelpics.render.render.DefaultScreenRenderer.V_FOV_DEG / 2);
static final int SSAA = 3;
static final int TW = 220, TH = 240;
static final String[] D = {"S", "W", "N", "E"}; // facing yaw i·90
static BlockEntityBaker beBaker;
static DecorationBaker decoBaker;
/** Block entities, viewed from the front ({@code frontYaw}: facing yaw, null = fixed diagonal view). */
record Scene(List<BlockEntityState> states, Float frontYaw) {}
static final Map<String, Scene> scenes = new LinkedHashMap<>();
static final Map<String, List<DecorationState>> decoScenes = new LinkedHashMap<>();
static void scene(String name, Float frontYaw, BlockEntityState... states) {
scenes.put(name, new Scene(List.of(states), frontYaw));
}
/** A block-entity state at (bx, 0, bz); unused fields default. */
static BlockEntityState st(Kind kind, int bx, int bz, float yaw, String direction, boolean active, String color,
@@ -42,224 +45,177 @@ public class BlockEntityTestRender {
return st(kind, 0, 0, yaw, null, false, null, 0, null, List.of(), List.of());
}
static BlockEntityState head(Kind kind, String type, float yaw) {
return st(kind, 0, 0, yaw, null, false, null, 0, type, List.of(), List.of());
}
static BlockEntityState chest(Kind kind, int bx, int bz, float yaw, ChestKind ck, String variant) {
return new BlockEntityState(kind, bx, 0, bz, yaw, null, false, ck, 0, variant, null, null,
List.of(), List.of(), null, null);
}
static BlockEntityState sign(Kind kind, float yaw, SignText front, SignText back) {
return new BlockEntityState(kind, 0, 0, 0, yaw, null, false, null, 0, null, null, null,
List.of(), List.of(), front, back);
}
static SignText txt(org.bukkit.DyeColor dye, boolean glow, String... lines) {
return new SignText(List.of(lines),
eu.mhsl.minecraft.pixelpics.render.util.ColorUtil.signFillArgb(dye, glow),
eu.mhsl.minecraft.pixelpics.render.util.ColorUtil.signOutlineArgb(dye), glow);
static SignText txt(DyeColor dye, boolean glow, String... lines) {
return new SignText(List.of(lines), ColorUtil.signFillArgb(dye, glow), ColorUtil.signOutlineArgb(dye), glow);
}
public static void main(String[] args) throws Exception {
Logger log = Logger.getLogger("test");
ResourcePack pack = ResourcePackLoader.load(new File(ROOT, "resourcepack"), log).orElseThrow();
AssetReader reader = new AssetReader(pack);
TextureCache textures = new TextureCache(pack);
BlockModelRegistry registry = new BlockModelRegistry(reader, textures);
BiomeTintProvider tint = new BiomeTintProvider(textures);
CemModelLoader geo = new CemModelLoader();
geo.load(new java.io.FileInputStream("src/main/resources/cem/cem_template_models.json"), log); // entity fallback only
geo.load(new java.io.FileInputStream("src/main/resources/cem/vanilla_entity_models.json"), log);
SkinCache skins = new SkinCache();
eu.mhsl.minecraft.pixelpics.assets.font.BitmapFont font =
eu.mhsl.minecraft.pixelpics.assets.font.FontLoader.load(pack, textures, log);
log.info("font glyphs loaded: " + (font.isEmpty() ? "NONE" : "ok"));
CemBaker baker = new CemBaker(geo, textures, skins);
beBaker = new BlockEntityBaker(geo, textures, skins, font);
decoBaker = new DecorationBaker(textures, registry,
new eu.mhsl.minecraft.pixelpics.render.entity.cem.ItemGeometry(reader, registry, textures, geo, beBaker));
DefaultScreenRenderer renderer = new DefaultScreenRenderer(registry, tint, textures, baker, beBaker, log);
/** A contents-bearing block entity at the origin (items / spawner mob / beacon beam / statue pose). */
static BlockEntityState contents(Kind kind, float yaw, List<ItemSpec> items, EntityState mob, Beam beam,
String pose, String color) {
return new BlockEntityState(kind, 0, 0, 0, yaw, null, false, null, 0, color, null, null,
List.of(), List.of(), null, null, items, mob, beam, pose);
}
BlockData air = (BlockData) Proxy.newProxyInstance(BlockEntityTestRender.class.getClassLoader(),
new Class<?>[]{BlockData.class}, (p, m, a) -> {
switch (m.getName()) {
case "getMaterial": return Material.AIR;
case "equals": return p == a[0];
case "hashCode": return System.identityHashCode(p);
case "toString": return "air";
}
Class<?> rt = m.getReturnType();
if (rt == boolean.class) return false;
if (rt.isPrimitive()) return 0;
return null;
});
WorldSnapshot empty = new WorldSnapshot(Map.of(), -64, 320, air);
SkyContext sky = new SkyContext(6000, 0, 6000);
static List<ItemSpec> items(String... ids) {
List<ItemSpec> out = new ArrayList<>();
for (String id : ids) out.add(id == null ? null : Harness.item(id));
return out;
}
// Scenes: label -> list of block-entity states.
LinkedHashMap<String, List<BlockEntityState>> scenes = new LinkedHashMap<>();
// Chests: single + double (all four facings), trapped/ender/copper. A double chest's LEFT half has
// its partner (RIGHT) on facing.getClockWise(): S→west, W→north, N→east, E→south.
String[] dirs = {"S", "W", "N", "E"};
/** EntityTestRender's default state of a mob with its real bounding box (spawner scale depends on it). */
static EntityState spawnerMob(String type) throws Exception {
double[] dims = switch (type) {
case "zombie" -> new double[] {0.6, 1.95};
case "pig" -> new double[] {0.9, 0.9};
case "spider" -> new double[] {1.4, 0.9};
default -> new double[] {0.6, 1.8};
};
return Harness.copy(EntityTestRender.state(type, 0), Map.of("width", dims[0], "height", dims[1]));
}
/** Item frame on the south side of the block at the origin. */
static DecorationState southFrame(ItemSpec item) {
return new DecorationState(DecorationState.Kind.ITEM_FRAME, 0.0625, 0.0625, 0, 0.9375, 0.9375, 0.0625,
DecorationState.Facing.SOUTH, null, item, null, 0, false, false);
}
static void buildScenes() throws Exception {
// Chests: single + double (all four facings), trapped/ender/copper. A double chest's LEFT half has its
// partner (RIGHT) on facing.getClockWise(): S→west, W→north, N→east, E→south.
int[][] partner = {{-1, 0}, {0, -1}, {1, 0}, {0, 1}};
for (int i = 0; i < 4; i++) {
float yaw = i * 90;
scenes.put("chest_" + dirs[i], List.of(chest(Kind.CHEST, 0, 0, yaw, ChestKind.SINGLE, null)));
frontCam.put("chest_" + dirs[i], yaw);
scenes.put("double_chest_" + dirs[i], List.of(
chest(Kind.CHEST, 0, 0, yaw, ChestKind.LEFT, null),
chest(Kind.CHEST, partner[i][0], partner[i][1], yaw, ChestKind.RIGHT, null)));
frontCam.put("double_chest_" + dirs[i], yaw);
scene("chest_" + D[i], yaw, chest(Kind.CHEST, 0, 0, yaw, ChestKind.SINGLE, null));
scene("double_chest_" + D[i], yaw, chest(Kind.CHEST, 0, 0, yaw, ChestKind.LEFT, null),
chest(Kind.CHEST, partner[i][0], partner[i][1], yaw, ChestKind.RIGHT, null));
}
scenes.put("double_chest_back", scenes.get("double_chest_S"));
frontCam.put("double_chest_back", 180f);
scenes.put("trapped_double", List.of(
chest(Kind.TRAPPED_CHEST, 0, 0, 0, ChestKind.LEFT, null),
chest(Kind.TRAPPED_CHEST, -1, 0, 0, ChestKind.RIGHT, null)));
frontCam.put("trapped_double", 0f);
scenes.put("ender_chest", List.of(chest(Kind.ENDER_CHEST, 0, 0, 0, ChestKind.SINGLE, null)));
frontCam.put("ender_chest", 0f);
scenes.put("copper_chest", List.of(chest(Kind.COPPER_CHEST, 0, 0, 0, ChestKind.SINGLE, null)));
frontCam.put("copper_chest", 0f);
scenes.put("copper_oxidized_double", List.of(
chest(Kind.COPPER_CHEST, 0, 0, 0, ChestKind.LEFT, "oxidized"),
chest(Kind.COPPER_CHEST, -1, 0, 0, ChestKind.RIGHT, "oxidized")));
frontCam.put("copper_oxidized_double", 0f);
// --- sign text (boards are block models, not drawn here) --- facingDeg 180 → front faces north.
org.bukkit.DyeColor B = org.bukkit.DyeColor.BLACK;
String[] D = {"S", "W", "N", "E"};
scenes.put("double_chest_back", new Scene(scenes.get("double_chest_S").states(), 180f));
scene("trapped_double", 0f, chest(Kind.TRAPPED_CHEST, 0, 0, 0, ChestKind.LEFT, null),
chest(Kind.TRAPPED_CHEST, -1, 0, 0, ChestKind.RIGHT, null));
scene("ender_chest", 0f, chest(Kind.ENDER_CHEST, 0, 0, 0, ChestKind.SINGLE, null));
scene("copper_chest", 0f, chest(Kind.COPPER_CHEST, 0, 0, 0, ChestKind.SINGLE, null));
scene("copper_oxidized_double", 0f, chest(Kind.COPPER_CHEST, 0, 0, 0, ChestKind.LEFT, "oxidized"),
chest(Kind.COPPER_CHEST, -1, 0, 0, ChestKind.RIGHT, "oxidized"));
// Sign text (boards are block models, not drawn here); facing 180 → front faces north.
DyeColor b = DyeColor.BLACK;
for (int i = 0; i < 4; i++) {
float yaw = i * 90;
scenes.put("sign_text_" + D[i], List.of(sign(Kind.SIGN, yaw, txt(B, false, "Hello World", "zweite Zeile", "Gruesse: AÖÜ", "ßäöü 12345"), null)));
frontCam.put("sign_text_" + D[i], yaw);
scene("sign_text_" + D[i], i * 90f,
sign(Kind.SIGN, i * 90, txt(b, false, "Hello World", "zweite Zeile", "Gruesse: AÖÜ", "ßäöü 12345"), null));
}
scenes.put("sign_text_diag", List.of(sign(Kind.SIGN, 22.5f * 3, txt(B, false, "rot 3", "67.5 deg", "", ""), null)));
frontCam.put("sign_text_diag", 22.5f * 3);
scenes.put("wall_sign_text", List.of(sign(Kind.WALL_SIGN, 180, txt(B, false, "Wall Sign", "Line 2", "Line 3", "Line 4"), null)));
frontCam.put("wall_sign_text", 180f);
scenes.put("wall_sign_text_E", List.of(sign(Kind.WALL_SIGN, 270, txt(B, false, "Wall East", "Line 2", "", ""), null)));
frontCam.put("wall_sign_text_E", 270f);
scenes.put("hanging_sign_text", List.of(sign(Kind.HANGING_SIGN, 180, txt(B, false, "Hanging", "sign text", "row 3", "row 4"), null)));
frontCam.put("hanging_sign_text", 180f);
scenes.put("sign_text_red", List.of(sign(Kind.SIGN, 180, txt(org.bukkit.DyeColor.RED, false, "RED INK", "colored", "sign text", ""), null)));
frontCam.put("sign_text_red", 180f);
scenes.put("sign_text_glow", List.of(sign(Kind.SIGN, 180, txt(org.bukkit.DyeColor.LIME, true, "GLOWING", "lime glow", "outline!", ""), null)));
frontCam.put("sign_text_glow", 180f);
// Back side viewed head-on: the camera looks at the side opposite the front.
scenes.put("sign_view_back", List.of(sign(Kind.SIGN, 180, txt(B, false, "FRONTXY", "f2", "", ""), txt(org.bukkit.DyeColor.BLUE, false, "BACKXY", "b2", "", ""))));
frontCam.put("sign_view_back", 0f);
scenes.put("sign_1line", List.of(sign(Kind.SIGN, 180, txt(B, false, "", "SHOP", "", ""), null)));
frontCam.put("sign_1line", 180f);
scenes.put("sign_long", List.of(sign(Kind.SIGN, 180, txt(B, false, "a very long line here", "", "", ""), null)));
frontCam.put("sign_long", 180f);
// --- banners: standing (rotation 0/4/6), wall (4 facings), patterned ---
List<BannerPattern> pats = List.of(new BannerPattern("stripe_bottom", 0xFFCC2020), new BannerPattern("cross", 0xFF2040CC), new BannerPattern("border", 0xFF20A020));
scene("sign_text_diag", 22.5f * 3, sign(Kind.SIGN, 22.5f * 3, txt(b, false, "rot 3", "67.5 deg", "", ""), null));
scene("wall_sign_text", 180f, sign(Kind.WALL_SIGN, 180, txt(b, false, "Wall Sign", "Line 2", "Line 3", "Line 4"), null));
scene("wall_sign_text_E", 270f, sign(Kind.WALL_SIGN, 270, txt(b, false, "Wall East", "Line 2", "", ""), null));
scene("hanging_sign_text", 180f, sign(Kind.HANGING_SIGN, 180, txt(b, false, "Hanging", "sign text", "row 3", "row 4"), null));
scene("sign_text_red", 180f, sign(Kind.SIGN, 180, txt(DyeColor.RED, false, "RED INK", "colored", "sign text", ""), null));
scene("sign_text_glow", 180f, sign(Kind.SIGN, 180, txt(DyeColor.LIME, true, "GLOWING", "lime glow", "outline!", ""), null));
// Back side viewed head-on.
scene("sign_view_back", 0f, sign(Kind.SIGN, 180, txt(b, false, "FRONTXY", "f2", "", ""),
txt(DyeColor.BLUE, false, "BACKXY", "b2", "", "")));
scene("sign_1line", 180f, sign(Kind.SIGN, 180, txt(b, false, "", "SHOP", "", ""), null));
scene("sign_long", 180f, sign(Kind.SIGN, 180, txt(b, false, "a very long line here", "", "", ""), null));
// Banners: standing (rotation 0/4/6), patterned (front + side), wall (4 facings).
List<BannerPattern> pats = List.of(new BannerPattern("stripe_bottom", 0xFFCC2020),
new BannerPattern("cross", 0xFF2040CC), new BannerPattern("border", 0xFF20A020));
for (float yaw : new float[] {0, 90, 135}) {
String k = "banner_" + (int) yaw;
scenes.put(k, List.of(st(Kind.BANNER, 0, 0, yaw, null, false, null, 0xFFCC2020, null, List.of(), List.of())));
frontCam.put(k, yaw);
scene("banner_" + (int) yaw, yaw, st(Kind.BANNER, 0, 0, yaw, null, false, null, 0xFFCC2020, null, List.of(), List.of()));
}
scenes.put("banner_patterned", List.of(st(Kind.BANNER, 0, 0, 0, null, false, null, 0xFFFFFFFF, null, pats, List.of())));
frontCam.put("banner_patterned", 0f);
scenes.put("banner_side", scenes.get("banner_patterned"));
frontCam.put("banner_side", 90f);
BlockEntityState patterned = st(Kind.BANNER, 0, 0, 0, null, false, null, 0xFFFFFFFF, null, pats, List.of());
scene("banner_patterned", 0f, patterned);
scene("banner_side", 90f, patterned);
for (int i = 0; i < 4; i++) {
scenes.put("wall_banner_" + D[i], List.of(st(Kind.WALL_BANNER, 0, 0, i * 90, null, false, null, 0xFF2040CC, null, pats, List.of())));
frontCam.put("wall_banner_" + D[i], i * 90f);
scene("wall_banner_" + D[i], i * 90f,
st(Kind.WALL_BANNER, 0, 0, i * 90, null, false, null, 0xFF2040CC, null, pats, List.of()));
}
// --- shulker boxes: 6 facings ---
for (String d : new String[] {"up", "down", "north", "south", "east", "west"}) {
scenes.put("shulker_" + d, List.of(st(Kind.SHULKER_BOX, 0, 0, 0, d, false, d.equals("up") ? null : "red", 0, null, List.of(), List.of())));
frontCam.put("shulker_" + d, 0f);
scene("shulker_" + d, 0f,
st(Kind.SHULKER_BOX, 0, 0, 0, d, false, d.equals("up") ? null : "red", 0, null, List.of(), List.of()));
}
scenes.put("conduit", List.of(be(Kind.CONDUIT, 0)));
scenes.put("conduit_active", List.of(st(Kind.CONDUIT, 0, 0, 0, null, true, null, 0, null, List.of(), List.of())));
scene("conduit", null, be(Kind.CONDUIT, 0));
scene("conduit_active", null, st(Kind.CONDUIT, 0, 0, 0, null, true, null, 0, null, List.of(), List.of()));
List<String> sherds = List.of("heart_pottery_sherd", "skull_pottery_sherd", "angler_pottery_sherd", "brick");
for (int i = 0; i < 4; i++) {
scenes.put("pot_" + D[i], List.of(st(Kind.DECORATED_POT, 0, 0, i * 90, null, false, null, 0, null, List.of(), sherds)));
frontCam.put("pot_" + D[i], i * 90f);
scene("pot_" + D[i], i * 90f, st(Kind.DECORATED_POT, 0, 0, i * 90, null, false, null, 0, null, List.of(), sherds));
}
scenes.put("pot_plain", List.of(be(Kind.DECORATED_POT, 0)));
frontCam.put("pot_plain", 0f);
scenes.put("bell", List.of(be(Kind.BELL, 0)));
scene("pot_plain", 0f, be(Kind.DECORATED_POT, 0));
scene("bell", null, be(Kind.BELL, 0));
for (int i = 0; i < 4; i++) {
scenes.put("lectern_book_" + D[i], List.of(st(Kind.LECTERN, 0, 0, i * 90, null, true, null, 0, null, List.of(), List.of())));
frontCam.put("lectern_book_" + D[i], i * 90f);
scene("lectern_book_" + D[i], i * 90f, st(Kind.LECTERN, 0, 0, i * 90, null, true, null, 0, null, List.of(), List.of()));
}
// --- heads: every type facing the camera (rotation 0), plus rotations and wall mounts ---
for (String t : new String[] {"skeleton", "wither_skeleton", "zombie", "creeper", "player", "piglin", "dragon"}) {
scenes.put("head_" + t, List.of(head(Kind.HEAD, t, 0)));
frontCam.put("head_" + t, 0f);
}
scenes.put("head_dragon_side", List.of(head(Kind.HEAD, "dragon", 0)));
frontCam.put("head_dragon_side", 90f);
for (float yaw : new float[] {90, 157.5f, 270}) {
scenes.put("head_creeper_" + yaw, List.of(head(Kind.HEAD, "creeper", yaw)));
frontCam.put("head_creeper_" + yaw, yaw);
}
for (int i = 0; i < 4; i++) {
scenes.put("wall_head_" + D[i], List.of(head(Kind.WALL_HEAD, "skeleton", i * 90)));
frontCam.put("wall_head_" + D[i], i * 90f);
}
scenes.put("wall_dragon_N", List.of(head(Kind.WALL_HEAD, "dragon", 180)));
frontCam.put("wall_dragon_N", 180f);
scenes.put("wall_piglin_E", List.of(head(Kind.WALL_HEAD, "piglin", 270)));
frontCam.put("wall_piglin_E", 270f);
// --- block-entity contents (no block models in this harness: tables/campfires/shelves/cages absent) ---
scenes.put("enchant_book_closed", List.of(be(Kind.ENCHANTING_TABLE, 0)));
scenes.put("enchant_book_open", List.of(st(Kind.ENCHANTING_TABLE, 0, 0, 90, null, true, null, 0, null, List.of(), List.of())));
frontCam.put("enchant_book_open", 0f);
scenes.put("campfire_items", List.of(contents(Kind.CAMPFIRE, 0, items("beef", "cod", "porkchop", null), null, null, null, null)));
scenes.put("campfire_items_E", List.of(contents(Kind.CAMPFIRE, 270, items("beef", "cod", "porkchop", "salmon"), null, null, null, null)));
scenes.put("shelf_items_S", List.of(contents(Kind.SHELF, 0, items("diamond", "book", "golden_apple"), null, null, null, null)));
frontCam.put("shelf_items_S", 0f);
scenes.put("shelf_items_E", List.of(contents(Kind.SHELF, 270, items("stick", null, "emerald"), null, null, null, null)));
frontCam.put("shelf_items_E", 270f);
scenes.put("vault_item", List.of(contents(Kind.VAULT, 0, items("trial_key"), null, null, null, null)));
frontCam.put("vault_item", 0f);
// Heads: every type facing the camera, rotations and wall mounts.
for (String t : new String[] {"skeleton", "wither_skeleton", "zombie", "creeper", "player", "piglin", "dragon"}) {
scene("head_" + t, 0f, head(Kind.HEAD, t, 0));
}
scene("head_dragon_side", 90f, head(Kind.HEAD, "dragon", 0));
for (float yaw : new float[] {90, 157.5f, 270}) scene("head_creeper_" + yaw, yaw, head(Kind.HEAD, "creeper", yaw));
for (int i = 0; i < 4; i++) scene("wall_head_" + D[i], i * 90f, head(Kind.WALL_HEAD, "skeleton", i * 90));
scene("wall_dragon_N", 180f, head(Kind.WALL_HEAD, "dragon", 180));
scene("wall_piglin_E", 270f, head(Kind.WALL_HEAD, "piglin", 270));
// Block-entity contents.
scene("enchant_book_closed", null, be(Kind.ENCHANTING_TABLE, 0));
scene("enchant_book_open", 0f, st(Kind.ENCHANTING_TABLE, 0, 0, 90, null, true, null, 0, null, List.of(), List.of()));
scene("campfire_items", null, contents(Kind.CAMPFIRE, 0, items("beef", "cod", "porkchop", null), null, null, null, null));
scene("campfire_items_E", null, contents(Kind.CAMPFIRE, 270, items("beef", "cod", "porkchop", "salmon"), null, null, null, null));
scene("shelf_items_S", 0f, contents(Kind.SHELF, 0, items("diamond", "book", "golden_apple"), null, null, null, null));
scene("shelf_items_E", 270f, contents(Kind.SHELF, 270, items("stick", null, "emerald"), null, null, null, null));
scene("vault_item", 0f, contents(Kind.VAULT, 0, items("trial_key"), null, null, null, null));
String[] poses = {"standing", "running", "sitting", "star"};
String[] stages = {null, "exposed", "weathered", "oxidized"};
for (int i = 0; i < 4; i++) {
scenes.put("statue_" + poses[i], List.of(contents(Kind.COPPER_GOLEM_STATUE, i * 90, List.of(), null, null, poses[i], stages[i])));
frontCam.put("statue_" + poses[i], i * 90f);
scene("statue_" + poses[i], i * 90f,
contents(Kind.COPPER_GOLEM_STATUE, i * 90, List.of(), null, null, poses[i], stages[i]));
}
for (String mob : new String[] {"zombie", "pig", "spider", "blaze"}) {
scenes.put("spawner_" + mob, List.of(contents(Kind.SPAWNER, 0, List.of(), spawnerMob(mob), null, null, null)));
frontCam.put("spawner_" + mob, 0f);
scene("spawner_" + mob, 0f, contents(Kind.SPAWNER, 0, List.of(), spawnerMob(mob), null, null, null));
}
scenes.put("beacon_beam", List.of(contents(Kind.BEACON, 0, List.of(), null,
new Beam(List.of(new BeamSection(0xFFF9FFFE, 1), new BeamSection(0xFFB02E26, 2), new BeamSection(0xFF3C44AA, 1)), 1f), null, null)));
scene("beacon_beam", null, contents(Kind.BEACON, 0, List.of(), null, new Beam(List.of(
new BeamSection(0xFFF9FFFE, 1), new BeamSection(0xFFB02E26, 2), new BeamSection(0xFF3C44AA, 1)), 1f), null, null));
List<BlockEntityState> portal = new ArrayList<>();
for (int x = 0; x < 3; x++) for (int z = 0; z < 3; z++) portal.add(st(Kind.END_PORTAL, x, z, 0, null, false, null, 0, null, List.of(), List.of()));
scenes.put("end_portal", portal);
scenes.put("end_gateway", List.of(st(Kind.END_GATEWAY, 0, 0, 0, "down+up+north+south+west+east", false, null, 0, null, List.of(), List.of())));
scene("end_portal", null, portal.toArray(new BlockEntityState[0]));
scene("end_gateway", null, st(Kind.END_GATEWAY, 0, 0, 0, "down+up+north+south+west+east", false, null, 0, null, List.of(), List.of()));
// Decoration scenes (paintings + item frames), rendered via the same scene path.
LinkedHashMap<String, List<DecorationState>> decoScenes = new LinkedHashMap<>();
// kebab is 1x1; place a thin quad facing south spanning the block at z=0..0.0625.
// Paintings (thin quads facing south / north) and item frames, rendered via the same scene path.
decoScenes.put("painting_kebab_S", List.of(new DecorationState(DecorationState.Kind.PAINTING,
0, 0, 0, 1, 1, 0.0625, DecorationState.Facing.SOUTH, "kebab", null, null, 0, false, false)));
decoScenes.put("painting_kebab_N", List.of(new DecorationState(DecorationState.Kind.PAINTING,
0, 0, 0.9375, 1, 1, 1, DecorationState.Facing.NORTH, "kebab", null, null, 0, false, false)));
decoScenes.put("painting_pool_S", List.of(new DecorationState(DecorationState.Kind.PAINTING,
0, 0, 0, 2, 1, 0.0625, DecorationState.Facing.SOUTH, "pool", null, null, 0, false, false)));
decoScenes.put("item_frame", List.of(new DecorationState(DecorationState.Kind.ITEM_FRAME,
0.0625, 0.0625, 0, 0.9375, 0.9375, 0.0625, DecorationState.Facing.SOUTH, null, new eu.mhsl.minecraft.pixelpics.render.entity.ItemSpec("diamond", null, null, false), null, 0, false, false)));
decoScenes.put("item_frame_empty", List.of(new DecorationState(DecorationState.Kind.ITEM_FRAME,
0.0625, 0.0625, 0, 0.9375, 0.9375, 0.0625, DecorationState.Facing.SOUTH, null, null, null, 0, false, false)));
decoScenes.put("item_frame", List.of(southFrame(Harness.item("diamond"))));
decoScenes.put("item_frame_empty", List.of(southFrame(null)));
decoScenes.put("item_frame_E", List.of(new DecorationState(DecorationState.Kind.ITEM_FRAME,
0.9375, 0.0625, 0.0625, 1.0, 0.9375, 0.9375, DecorationState.Facing.EAST, null, new eu.mhsl.minecraft.pixelpics.render.entity.ItemSpec("diamond", null, null, false), null, 0, false, false)));
// Shields (plain / red base / blue + creeper + border patterns), chests (christmas with
// -Dpixelpics.testDate=2026-12-25) and a decorated map in frames (map/decorations sprites as MapRenderer).
0.9375, 0.0625, 0.0625, 1.0, 0.9375, 0.9375, DecorationState.Facing.EAST, null, Harness.item("diamond"), null, 0, false, false)));
// Shields (plain / red base / blue + creeper + border patterns) and chests (christmas with testDate).
BlockEntityState shieldPats = new BlockEntityState(Kind.BANNER, 0, 0, 0, 0, null, false, null, 0xFF3C44AA, null, null, null,
List.of(new BannerPattern("creeper", 0xFFFED83D), new BannerPattern("border", 0xFFF9FFFE)), List.of(), null, null);
BlockEntityState shieldRed = new BlockEntityState(Kind.BANNER, 0, 0, 0, 0, null, false, null, 0xFFB02E26, null, null, null,
List.of(), List.of(), null, null);
String[] frameItems = {"shield", "shield_red", "shield_patterns", "chest", "trapped_chest", "ender_chest"};
for (String id : frameItems) {
ItemSpec spec = switch (id) {
case "shield_red" -> new ItemSpec("shield", null, shieldRed, false);
case "shield_patterns" -> new ItemSpec("shield", null, shieldPats, false);
default -> new ItemSpec(id, null, null, false);
};
decoScenes.put("item_frame_" + id, List.of(new DecorationState(DecorationState.Kind.ITEM_FRAME,
0.0625, 0.0625, 0, 0.9375, 0.9375, 0.0625, DecorationState.Facing.SOUTH, null, spec, null, 0, false, false)));
decoScenes.put("item_frame_shield", List.of(southFrame(Harness.item("shield"))));
decoScenes.put("item_frame_shield_red", List.of(southFrame(new ItemSpec("shield", null, shieldRed, false))));
decoScenes.put("item_frame_shield_patterns", List.of(southFrame(new ItemSpec("shield", null, shieldPats, false))));
for (String id : new String[] {"chest", "trapped_chest", "ender_chest"}) {
decoScenes.put("item_frame_" + id, List.of(southFrame(Harness.item(id))));
}
// A checkered map with decorations (map/decorations sprites as MapRenderer), rotation 0 and 1.
int[][] mapPx = new int[128][128];
for (int y = 0; y < 128; y++) for (int x = 0; x < 128; x++) mapPx[y][x] = ((x / 16 + y / 16) & 1) == 0 ? 0xFF7FB238 : 0xFF3F76E4;
List<DecorationState.MapMarker> markers = List.of(
@@ -271,157 +227,74 @@ public class BlockEntityTestRender {
for (int rot : new int[] {0, 1}) {
decoScenes.put("item_frame_map_markers_r" + rot, List.of(new DecorationState(DecorationState.Kind.ITEM_FRAME,
0.0625, 0.0625, 0, 0.9375, 0.9375, 0.0625, DecorationState.Facing.SOUTH, null,
new ItemSpec("filled_map", null, null, false), mapPx, rot, false, false, markers)));
Harness.item("filled_map"), mapPx, rot, false, false, markers)));
}
}
public static void main(String[] args) throws Exception {
Harness h = new Harness("test");
DecorationBaker decoBaker = new DecorationBaker(h.textures, h.registry,
new ItemGeometry(h.reader, h.registry, h.textures, h.geo, h.beBaker));
WorldSnapshot empty = Harness.emptyWorld(-64, 320);
buildScenes();
List<BufferedImage> cells = new ArrayList<>();
List<String> names = new ArrayList<>(scenes.keySet());
for (String name : names) {
cells.add(labelCell(name, renderScene(renderer, empty, sky, scenes.get(name), frontCam.get(name))));
log.info("rendered " + name);
List<String> names = new ArrayList<>();
for (Map.Entry<String, Scene> e : scenes.entrySet()) {
Scene sc = e.getValue();
List<RenderedEntity> baked = new ArrayList<>();
for (BlockEntityState s : sc.states()) baked.add(h.beBaker.bake(s));
double[][] box = bounds(baked);
// Frame the beacon beam's foot, not 2048 blocks.
if (sc.states().get(0).kind() == Kind.BEACON) box[1][1] = box[0][1] + 4;
Vector center = Harness.center(box[0], box[1]);
double dist = Harness.frameDistance(box[0], box[1], 1.4, 0.6);
Vector cam = new Vector(center.getX() - dist, center.getY() + dist * 0.45, center.getZ() - dist * 0.35);
if (sc.frontYaw() != null) {
// Facing yaw (0 = south, 90 = west) → front direction (-sin, cos); offset 25° to the side.
double a = Math.toRadians(sc.frontYaw() + 25);
cam = new Vector(center.getX() - Math.sin(a) * dist, center.getY() + dist * 0.55, center.getZ() + Math.cos(a) * dist);
}
render(h, e.getKey(), Harness.job(empty, cam, center, TW, TH, List.of(), sc.states(), List.of(), List.of()), cells, names);
}
for (String name : decoScenes.keySet()) {
cells.add(labelCell(name, renderDeco(renderer, empty, sky, decoScenes.get(name))));
names.add(name);
log.info("rendered " + name);
for (Map.Entry<String, List<DecorationState>> e : decoScenes.entrySet()) {
List<RenderedEntity> baked = new ArrayList<>();
for (DecorationState s : e.getValue()) baked.add(decoBaker.bake(s));
double[][] box = bounds(baked);
Vector center = Harness.center(box[0], box[1]);
double dist = Harness.frameDistance(box[0], box[1], 1.4, 0.6);
// View from the front side (along +facing) so the picture is visible, not the back.
DecorationState.Facing f = e.getValue().get(0).facing();
double fx = f.axis() == 0 ? f.sign() : 0, fy = f.axis() == 1 ? f.sign() : 0, fz = f.axis() == 2 ? f.sign() : 0;
Vector cam = new Vector(center.getX() + fx * dist + 0.1, center.getY() + fy * dist + 0.1, center.getZ() + fz * dist + 0.1);
render(h, e.getKey(), Harness.job(empty, cam, center, TW, TH, List.of(), List.of(), e.getValue(), List.of()), cells, names);
}
File outDir = new File(System.getProperty("out", "/home/elias/Dokumente/PixelPics-BlockEntity-Renders"));
outDir.mkdirs();
for (int i = 0; i < names.size(); i++) ImageIO.write(cells.get(i), "png", new File(outDir, names.get(i) + ".png"));
int cols = 4, cw = cells.get(0).getWidth(), ch = cells.get(0).getHeight();
int rows = (cells.size() + cols - 1) / cols;
BufferedImage sheet = new BufferedImage(cols * cw, rows * ch, BufferedImage.TYPE_INT_RGB);
Graphics2D g = sheet.createGraphics();
g.setColor(new Color(40, 40, 48));
g.fillRect(0, 0, sheet.getWidth(), sheet.getHeight());
for (int i = 0; i < cells.size(); i++) g.drawImage(cells.get(i), (i % cols) * cw, (i / cols) * ch, null);
g.dispose();
File f = new File(outDir, "sheet.png");
ImageIO.write(sheet, "png", f);
System.out.println("WROTE " + f);
Harness.writeSheets(cells, 4, cells.size(), outDir, page -> "sheet.png");
}
/** Scenes viewed from the front (camera along the facing yaw, raised, slightly to the side). */
static final Map<String, Float> frontCam = new HashMap<>();
static BlockEntityState chest(Kind kind, int bx, int bz, float yaw, ChestKind ck, String variant) {
return new BlockEntityState(kind, bx, 0, bz, yaw, null, false, ck, 0, variant, null, null,
List.of(), List.of(), null, null);
static void render(Harness h, String name, RenderJob job,
List<BufferedImage> cells, List<String> names) {
cells.add(Harness.labelCell(name, h.renderer.execute(job)));
names.add(name);
h.log.info("rendered " + name);
}
/** A contents-bearing block-entity at the origin (items / spawner mob / beacon beam / statue pose). */
static BlockEntityState contents(Kind kind, float yaw, List<eu.mhsl.minecraft.pixelpics.render.entity.ItemSpec> items,
EntityState mob, Beam beam, String pose, String color) {
return new BlockEntityState(kind, 0, 0, 0, yaw, null, false, null, 0, color, null, null,
List.of(), List.of(), null, null, items, mob, beam, pose);
}
static List<eu.mhsl.minecraft.pixelpics.render.entity.ItemSpec> items(String... ids) {
List<eu.mhsl.minecraft.pixelpics.render.entity.ItemSpec> out = new ArrayList<>();
for (String id : ids) out.add(id == null ? null : new eu.mhsl.minecraft.pixelpics.render.entity.ItemSpec(id, null, null, false));
return out;
}
/** EntityTestRender's default state of a mob with its real bounding box (spawner scale depends on it). */
static EntityState spawnerMob(String type) throws Exception {
EntityState e = EntityTestRender.state(type, 0);
double[] dims = switch (type) {
case "zombie" -> new double[] {0.6, 1.95};
case "pig" -> new double[] {0.9, 0.9};
case "spider" -> new double[] {1.4, 0.9};
default -> new double[] {0.6, 1.8};
};
var comps = EntityState.class.getRecordComponents();
Object[] vals = new Object[comps.length];
Class<?>[] types = new Class<?>[comps.length];
for (int i = 0; i < comps.length; i++) {
types[i] = comps[i].getType();
vals[i] = switch (comps[i].getName()) {
case "width" -> dims[0];
case "height" -> dims[1];
default -> comps[i].getAccessor().invoke(e);
};
}
return EntityState.class.getDeclaredConstructor(types).newInstance(vals);
}
static BlockEntityState head(Kind kind, String type, float yaw) {
return st(kind, 0, 0, yaw, null, false, null, 0, type, List.of(), List.of());
}
static BufferedImage renderScene(DefaultScreenRenderer renderer, WorldSnapshot world, SkyContext sky,
List<BlockEntityState> states, Float frontYaw) {
/** {min, max} over the baked boxes (null entries skipped); the unit cube when nothing baked. */
static double[][] bounds(List<RenderedEntity> baked) {
double[] min = {Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE};
double[] max = {-Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE};
for (BlockEntityState s : states) {
RenderedEntity re = beBaker.bake(s);
for (RenderedEntity re : baked) {
if (re == null) continue;
for (int a = 0; a < 3; a++) { min[a] = Math.min(min[a], re.aabbMin[a]); max[a] = Math.max(max[a], re.aabbMax[a]); }
for (int a = 0; a < 3; a++) {
min[a] = Math.min(min[a], re.aabbMin[a]);
max[a] = Math.max(max[a], re.aabbMax[a]);
}
}
if (min[0] > max[0]) { min = new double[]{0, 0, 0}; max = new double[]{1, 1, 1}; }
if (states.get(0).kind() == Kind.BEACON) max[1] = min[1] + 4; // frame the beam's foot, not 2048 blocks
double cx = (min[0] + max[0]) / 2, cy = (min[1] + max[1]) / 2, cz = (min[2] + max[2]) / 2;
double ext = 0.5;
for (int a = 0; a < 3; a++) ext = Math.max(ext, max[a] - min[a]);
double dist = ext / (2 * Math.tan(V_FOV_HALF)) * 1.4 + 0.6;
Vector center = new Vector(cx, cy, cz);
Vector cam = new Vector(cx - dist, cy + dist * 0.45, cz - dist * 0.35);
if (frontYaw != null) {
// Facing yaw (0 = south, 90 = west) → front direction (-sin, cos); offset 25° to the side.
double a = Math.toRadians(frontYaw + 25);
cam = new Vector(cx - Math.sin(a) * dist, cy + dist * 0.55, cz + Math.cos(a) * dist);
}
Location loc = new Location(null, cam.getX(), cam.getY(), cam.getZ());
loc.setDirection(center.clone().subtract(cam));
List<Vector> rayMap = buildRayMap(loc, TW * SSAA, TH * SSAA);
RenderJob job = new RenderJob(world, rayMap, cam, TW, TH, sky, List.of(), states, List.of(), List.of());
return renderer.execute(job);
}
static BufferedImage renderDeco(DefaultScreenRenderer renderer, WorldSnapshot world, SkyContext sky,
List<DecorationState> states) {
double[] min = {Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE};
double[] max = {-Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE};
for (DecorationState s : states) {
RenderedEntity re = decoBaker.bake(s);
if (re == null) continue;
for (int a = 0; a < 3; a++) { min[a] = Math.min(min[a], re.aabbMin[a]); max[a] = Math.max(max[a], re.aabbMax[a]); }
}
if (min[0] > max[0]) { min = new double[]{0, 0, 0}; max = new double[]{1, 1, 1}; }
double cx = (min[0] + max[0]) / 2, cy = (min[1] + max[1]) / 2, cz = (min[2] + max[2]) / 2;
double ext = 0.5;
for (int a = 0; a < 3; a++) ext = Math.max(ext, max[a] - min[a]);
double dist = ext / (2 * Math.tan(V_FOV_HALF)) * 1.4 + 0.6;
// View from the front side (along +facing) so the picture is visible, not the back.
DecorationState.Facing f = states.get(0).facing();
double fx = f.axis() == 0 ? f.sign() : 0, fy = f.axis() == 1 ? f.sign() : 0, fz = f.axis() == 2 ? f.sign() : 0;
Vector center = new Vector(cx, cy, cz);
Vector cam = new Vector(cx + fx * dist + 0.1, cy + fy * dist + 0.1, cz + fz * dist + 0.1);
Location loc = new Location(null, cam.getX(), cam.getY(), cam.getZ());
loc.setDirection(center.clone().subtract(cam));
List<Vector> rayMap = buildRayMap(loc, TW * SSAA, TH * SSAA);
RenderJob job = new RenderJob(world, rayMap, cam, TW, TH, sky, List.of(), List.of(), states, List.of());
return renderer.execute(job);
}
static BufferedImage labelCell(String key, BufferedImage v) {
int labelH = 20;
BufferedImage cell = new BufferedImage(v.getWidth(), v.getHeight() + labelH, BufferedImage.TYPE_INT_RGB);
Graphics2D g = cell.createGraphics();
g.setColor(new Color(25, 25, 30));
g.fillRect(0, 0, cell.getWidth(), cell.getHeight());
g.drawImage(v, 0, labelH, null);
g.setColor(Color.WHITE);
g.setFont(new Font("SansSerif", Font.BOLD, 13));
g.drawString(key, 4, 15);
g.dispose();
return cell;
}
static List<Vector> buildRayMap(Location eye, int width, int height) {
return eu.mhsl.minecraft.pixelpics.render.render.DefaultScreenRenderer.buildRayMap(eye, width, height);
if (min[0] > max[0]) return new double[][] {{0, 0, 0}, {1, 1, 1}};
return new double[][] {min, max};
}
}
+36 -79
View File
@@ -1,11 +1,7 @@
import eu.mhsl.minecraft.pixelpics.assets.*;
import eu.mhsl.minecraft.pixelpics.render.entity.*;
import eu.mhsl.minecraft.pixelpics.render.render.*;
import eu.mhsl.minecraft.pixelpics.render.sky.SkyContext;
import eu.mhsl.minecraft.pixelpics.render.render.RenderJob;
import eu.mhsl.minecraft.pixelpics.render.snapshot.WorldSnapshot;
import eu.mhsl.minecraft.pixelpics.render.tint.BiomeTintProvider;
import org.bukkit.ChunkSnapshot;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.data.BlockData;
import org.bukkit.util.Vector;
@@ -16,41 +12,24 @@ import java.io.File;
import java.lang.reflect.Proxy;
import java.util.*;
import java.util.List;
import java.util.logging.Logger;
/**
* Reproducible render benchmark (no server): farm scenes with hundreds of mobs / dropped items on a block
* floor plus a "normal" scene (few mobs, translucent + additive layers). Renders every scene {@code -Druns}
* 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, village, normal. {@code -Djava.util.concurrent.ForkJoinPool.common.parallelism=N} pins the thread count.
* Reproducible render benchmark (no server): farm scenes with hundreds of mobs / dropped items on a grass floor
* plus a "normal" scene (few mobs, translucent + additive layers). Renders every scene {@code -Druns} times (after
* {@code -Dwarmup} runs), prints median/min 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 (default all):
* farm_mobs, farm_empty (same view, no entities), farm_mobs_close, farm_items, village, normal.
* {@code -Djava.util.concurrent.ForkJoinPool.common.parallelism=N} pins the thread count. Build: see {@link Harness}.
*/
public class EntityBenchmark {
static final String ROOT = "/home/elias/Dokumente/mcTestServer/plugins/PixelPics";
static final int W = 320, H = 240, SSAA = 3;
static final int W = 320, H = 240;
record Scene(String name, Vector cam, Vector look, List<EntityState> mobs, List<ObjectState> objects) {}
public static void main(String[] args) throws Exception {
Logger log = Logger.getLogger("bench");
ResourcePack pack = ResourcePackLoader.load(new File(ROOT, "resourcepack"), log).orElseThrow();
AssetReader reader = new AssetReader(pack);
TextureCache textures = new TextureCache(pack);
BlockModelRegistry registry = new BlockModelRegistry(reader, textures);
BiomeTintProvider tint = new BiomeTintProvider(textures);
var geo = new eu.mhsl.minecraft.pixelpics.render.entity.cem.CemModelLoader();
geo.load(new java.io.FileInputStream("src/main/resources/cem/cem_template_models.json"), log);
geo.load(new java.io.FileInputStream("src/main/resources/cem/vanilla_entity_models.json"), log);
var font = eu.mhsl.minecraft.pixelpics.assets.font.FontLoader.load(pack, textures, log);
SkinCache skins = new SkinCache();
var baker = new eu.mhsl.minecraft.pixelpics.render.entity.cem.CemBaker(geo, textures, skins);
var beBaker = new eu.mhsl.minecraft.pixelpics.render.entity.cem.BlockEntityBaker(geo, textures, skins, font);
DefaultScreenRenderer renderer = new DefaultScreenRenderer(registry, tint, textures, baker, beBaker, log);
Harness h = new Harness("bench");
WorldSnapshot world = farmWorld();
SkyContext sky = new SkyContext(6000, 0, 6000);
int runs = Integer.getInteger("runs", 5), warmup = Integer.getInteger("warmup", 2);
File outDir = new File(System.getProperty("out", "/home/elias/Dokumente/PixelPics-Entity-Renders/bench"));
outDir.mkdirs();
@@ -58,16 +37,13 @@ public class EntityBenchmark {
for (Scene sc : scenes()) {
if (!only.isEmpty() && !only.contains(sc.name)) continue;
Location loc = new Location(null, sc.cam.getX(), sc.cam.getY(), sc.cam.getZ());
loc.setDirection(sc.look.clone().subtract(sc.cam));
List<Vector> rayMap = EntityTestRender.buildRayMap(loc, W * SSAA, H * SSAA);
RenderJob job = new RenderJob(world, rayMap, sc.cam, W, H, sky, sc.mobs, List.of(), List.of(), sc.objects);
RenderJob job = Harness.job(world, sc.cam, sc.look, W, H, sc.mobs, List.of(), List.of(), sc.objects);
BufferedImage img = null;
for (int i = 0; i < warmup; i++) img = renderer.execute(job);
for (int i = 0; i < warmup; i++) img = h.renderer.execute(job);
long[] times = new long[runs];
for (int i = 0; i < runs; i++) {
long t0 = System.nanoTime();
img = renderer.execute(job);
img = h.renderer.execute(job);
times[i] = System.nanoTime() - t0;
}
Arrays.sort(times);
@@ -77,7 +53,7 @@ public class EntityBenchmark {
}
}
static List<Scene> scenes() {
static List<Scene> scenes() throws ReflectiveOperationException {
List<Scene> out = new ArrayList<>();
// Mob farm: 400 animals in a 20x20 pen (1.4 block spacing), seen from an elevated corner.
List<EntityState> farm = new ArrayList<>();
@@ -106,9 +82,9 @@ public class EntityBenchmark {
ItemSpec spec;
if (r.nextInt(4) == 0) {
int b = r.nextInt(blocks.length);
spec = new ItemSpec(blocks[b].getKey().getKey(), ObjectTestRender.block(blocks[b], blockState[b]), null, false);
spec = new ItemSpec(blocks[b].getKey().getKey(), Harness.block(blocks[b], blockState[b]), null, false);
} else {
spec = new ItemSpec(flat[r.nextInt(flat.length)], null, null, false);
spec = Harness.item(flat[r.nextInt(flat.length)]);
}
items.add(ObjectState.item(a, spec, "ground", true));
}
@@ -149,11 +125,10 @@ public class EntityBenchmark {
few.add(mob("villager", 4, 1, 6, 160, false));
few.add(mob("zombie", 6.5, 1, 6, 220, false));
few.add(mob("slime", 2, 1, 5.5, 120, false));
EntityState creeper = mob("creeper", 7, 1, 3.5, 250, false);
few.add(withPowered(creeper));
few.add(Harness.copy(mob("creeper", 7, 1, 3.5, 250, false), Map.of("powered", true)));
few.add(mob("chicken", 3.5, 1, 1.5, 90, false));
List<ObjectState> objs = new ArrayList<>();
objs.add(ObjectState.item(Affine.translation(4.5, 1.2, 2).mul(Affine.scale(0.5)), new ItemSpec("apple", null, null, false), "ground", true));
objs.add(ObjectState.item(Affine.translation(4.5, 1.2, 2).mul(Affine.scale(0.5)), Harness.item("apple"), "ground", true));
objs.add(ObjectState.text(Affine.translation(4.5, 3.2, 4).mul(Affine.rotY(Math.PI)).mul(Affine.scale(0.025)),
List.of("PixelPics bench"), 200, 0xFFFFFFFF, 0x40000000, true));
out.add(new Scene("normal", new Vector(0, 3.5, -2.5), new Vector(4.5, 1.5, 4), few, objs));
@@ -173,20 +148,20 @@ public class EntityBenchmark {
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, null, null, null, null);
static EntityState villager(double x, double z, float yaw, String type, String prof, int level, boolean baby)
throws ReflectiveOperationException {
return Harness.copy(mob("villager", x, 1, z, yaw, baby), Map.of("width", 0.6, "height", 1.95, "variant", type,
"profession", prof, "villagerLevel", level));
}
/** {@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(), s.pose(), s.partPoses(), s.shoulderLeft(), s.shoulderRight());
static EntityState with(EntityState s, int tint, EntityState.Equipment eq, int collar)
throws ReflectiveOperationException {
Map<String, Object> o = new HashMap<>();
o.put("tint", tint);
o.put("equipment", eq);
o.put("collar", collar);
return Harness.copy(s, o);
}
/** The variant the game always supplies (cow/pig/chicken have no texture without one). */
@@ -199,41 +174,23 @@ public class EntityBenchmark {
};
}
static EntityState withPowered(EntityState s) {
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(), s.tint(), s.sizeScale(), s.profession(), s.villagerLevel(),
s.markings(), s.saddle(), s.chest(), s.arms(), s.bodyEquip(), s.equipment(), s.invisible(), s.pitch(),
s.bodyDye(), s.tint2(), s.passenger(), s.rightHand(), s.leftHand(), s.headItem(), s.collar(), true,
s.mouthItem(), s.pose(), s.partPoses(), s.shoulderLeft(), s.shoulderRight());
}
/**
* Grass floor at y=0 over [-48, 48)². Nothing stacked on it: ambient occlusion asks Material.isOccluding()
* for non-air neighbours, which needs a server registry (not available standalone).
*/
static WorldSnapshot farmWorld() {
BlockData air = ObjectTestRender.block(Material.AIR, null);
BlockData grass = ObjectTestRender.block(Material.GRASS_BLOCK, "snowy=false");
BlockData air = Harness.block(Material.AIR, null);
BlockData grass = Harness.block(Material.GRASS_BLOCK, "snowy=false");
Map<Long, ChunkSnapshot> chunks = new HashMap<>();
for (int cx = -3; cx < 3; cx++) {
for (int cz = -3; cz < 3; cz++) {
final int bx0 = cx * 16, bz0 = cz * 16;
ChunkSnapshot cs = (ChunkSnapshot) Proxy.newProxyInstance(EntityBenchmark.class.getClassLoader(),
new Class<?>[]{ChunkSnapshot.class}, (p, m, a) -> {
switch (m.getName()) {
case "getBlockData": {
int y = (int) a[1];
return y == 0 ? grass : air;
}
case "getBiome": return null;
case "equals": return p == a[0];
case "hashCode": return System.identityHashCode(p);
case "toString": return "chunk";
}
Class<?> rt = m.getReturnType();
if (rt == boolean.class) return false;
if (rt.isPrimitive()) return 0;
return null;
new Class<?>[]{ChunkSnapshot.class}, (p, m, a) -> switch (m.getName()) {
case "getBlockData" -> (int) a[1] == 0 ? grass : air;
case "equals" -> p == a[0];
case "hashCode" -> System.identityHashCode(p);
case "toString" -> "chunk";
default -> Harness.defaultValue(m.getReturnType());
});
chunks.put(WorldSnapshot.chunkKey(cx, cz), cs);
}
+40 -148
View File
@@ -1,32 +1,23 @@
import eu.mhsl.minecraft.pixelpics.assets.*;
import eu.mhsl.minecraft.pixelpics.render.entity.*;
import eu.mhsl.minecraft.pixelpics.render.render.*;
import eu.mhsl.minecraft.pixelpics.render.sky.SkyContext;
import eu.mhsl.minecraft.pixelpics.render.snapshot.WorldSnapshot;
import eu.mhsl.minecraft.pixelpics.render.tint.BiomeTintProvider;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.data.BlockData;
import org.bukkit.util.Vector;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.lang.reflect.Proxy;
import java.util.*;
import java.util.List;
import java.util.logging.Logger;
/** Standalone (no server) renderer: every entity twice (yaw 45° / 225°) against empty sky -> contact sheets. */
/**
* Standalone (no server) renderer: every entity twice (yaw 45° / 225°) against empty sky -> contact sheets
* ({@code page_<n>.png}) + {@code einzeln/<key>.png} in {@code -Dout} (default ~/Dokumente/PixelPics-Entity-Renders).
* Args: case specs (default {@link #ENTITIES}), or {@code @variants} for {@link #VARIANT_CASES}. Build: see {@link Harness}.
*/
public class EntityTestRender {
static final String ROOT = "/home/elias/Dokumente/mcTestServer/plugins/PixelPics";
static final double V_FOV_HALF = Math.toRadians(eu.mhsl.minecraft.pixelpics.render.render.DefaultScreenRenderer.V_FOV_DEG / 2);
static final int SSAA = 3;
static final int TW = 200, TH = 230; // per-view tile size
// Entity type keys to render (current vanilla + bundled bedrock specials). variant=null (base look).
// Default case list: every entity type in its base look ("plain": the per-type tables below apply).
static final String[] ENTITIES = {
"allay","armadillo","armor_stand","axolotl","bat","bee","blaze","bogged","breeze","camel",
"cat","cave_spider","chicken","cod","copper_golem","cow","creaking","creeper","dolphin","donkey",
@@ -42,8 +33,10 @@ public class EntityTestRender {
"minecart","end_crystal","arrow","trident","leash_knot","evoker_fangs","wither_skull","shulker_bullet","cushion"
};
// "@variants" case set (before/after pixel diffs of variant / pose-flag / armor handling). Spec syntax:
// [baby:]type[@variant][+flag…], flags: chest, arms, tame (red collar), saddle, armor (full diamond), noarmor.
// "@variants" case set (before/after pixel diffs of refactors). Spec: [baby:]type[@variant][+flag…], flags:
// chest, arms, saddle, tame (red collar), armor (full diamond), elytra, red (sheep wool), sword / crossbow
// (right hand), crossbowl (left hand), mouth (sweet berries), passenger, shoulder (red + blue parrot), stand
// (custom armor-stand part poses), pitch:<deg>, p:<state pose> (sleeping, sitting, eyes_glowing, …).
static final List<String> VARIANT_CASES = new ArrayList<>();
static {
for (String v : new String[] {"tabby", "black", "red", "siamese", "british_shorthair", "calico", "persian",
@@ -68,13 +61,11 @@ public class EntityTestRender {
VARIANT_CASES.add("mooshroom@red");
VARIANT_CASES.add("mooshroom@brown");
VARIANT_CASES.add("baby:mooshroom@brown");
// Armor stand with/without arms, small, with armor.
for (String a : new String[] {"armor_stand", "armor_stand+arms", "baby:armor_stand", "baby:armor_stand+arms",
"armor_stand+arms+armor", "baby:armor_stand+arms+armor"}) VARIANT_CASES.add(a);
// Chests.
for (String c : new String[] {"llama@creamy", "llama@creamy+chest", "llama@brown+chest+saddle", "llama@gray+chest",
VARIANT_CASES.addAll(List.of("armor_stand", "armor_stand+arms", "baby:armor_stand", "baby:armor_stand+arms",
"armor_stand+arms+armor", "baby:armor_stand+arms+armor"));
VARIANT_CASES.addAll(List.of("llama@creamy", "llama@creamy+chest", "llama@brown+chest+saddle", "llama@gray+chest",
"trader_llama@white+chest", "baby:llama@creamy+chest", "donkey", "donkey+chest", "donkey+chest+saddle",
"mule", "mule+chest", "mule+chest+saddle", "baby:donkey+chest"}) VARIANT_CASES.add(c);
"mule", "mule+chest", "mule+chest+saddle", "baby:donkey+chest"));
// Armor wearers (and non-wearers given armor: nothing must appear).
for (String t : new String[] {"zombie", "husk", "drowned", "zombie_villager", "zombified_piglin", "skeleton",
"stray", "wither_skeleton", "bogged", "parched", "piglin", "piglin_brute", "giant", "player",
@@ -84,7 +75,7 @@ public class EntityTestRender {
VARIANT_CASES.add(t + "+armor");
// Layer / texture states: emissive eyes, sheep undercoat, angry wolves, tipped arrows, invulnerable
// wither, partial-alpha cutout textures, held items (piglin arm poses), elytra.
for (String c : new String[] {"spider", "cave_spider", "enderman", "phantom", "breeze", "warden",
VARIANT_CASES.addAll(List.of("spider", "cave_spider", "enderman", "phantom", "breeze", "warden",
"copper_golem@oxidized", "creaking", "creaking+p:eyes_glowing", "sheep@sheared", "sheep+red",
"sheep@sheared+red", "baby:sheep+red", "sheep+p:jeb", "wolf@pale+p:angry", "baby:wolf@pale+p:angry",
"wolf@pale+tame+p:angry", "arrow+p:tipped", "spectral_arrow", "wither+p:invulnerable",
@@ -92,12 +83,11 @@ public class EntityTestRender {
"piglin+sword+crossbowl", "piglin+crossbow+p:arms_crossbow_hold", "piglin+sword+p:arms_attacking_with_melee_weapon",
"zombified_piglin+sword", "piglin_brute+sword", "pillager+crossbow",
"vindicator+sword", "armor_stand+elytra", "zombie+elytra", "baby:armor_stand+armor",
"baby:armor_stand+arms+elytra"})
VARIANT_CASES.add(c);
"baby:armor_stand+arms+elytra"));
VARIANT_CASES.addAll(List.of(ENTITIES));
}
// Representative variants (the game always supplies these; null base textures wouldn't exist otherwise).
// Variants the game always supplies (several types have no texture without one); villager profession/level.
static final Map<String, String> VAR = Map.ofEntries(
Map.entry("cat", "tabby"), Map.entry("wolf", "pale"), Map.entry("axolotl", "lucy"),
Map.entry("parrot", "red"), Map.entry("rabbit", "brown"), Map.entry("horse", "white"),
@@ -108,7 +98,6 @@ public class EntityTestRender {
Map.entry("tropical_fish", "betty"), Map.entry("pufferfish", "big"), Map.entry("zombie_nautilus", "warm")
);
// Villager profession / level for the standalone render (biome type comes from VAR above).
static final Map<String, String> PROF = Map.ofEntries(
Map.entry("villager", "librarian"), Map.entry("zombie_villager", "farmer")
);
@@ -116,17 +105,17 @@ public class EntityTestRender {
Map.entry("villager", 5)
);
// Horse/llama/donkey equipment for the standalone render.
static final Map<String, String> MARK = Map.of("horse", "blackdots"); // coat markings
static final java.util.Set<String> SADDLE = java.util.Set.of("horse", "donkey", "mule", "nautilus", "pig", "strider", "camel");
static final java.util.Set<String> CHEST = java.util.Set.of("llama", "donkey");
static final Map<String, String> EQUIP = Map.ofEntries( // armor / carpet
// Plain cases: horse markings, saddles, chests, body armor / carpet / harness.
static final Map<String, String> MARK = Map.of("horse", "blackdots");
static final Set<String> SADDLE = Set.of("horse", "donkey", "mule", "nautilus", "pig", "strider", "camel");
static final Set<String> CHEST = Set.of("llama", "donkey");
static final Map<String, String> EQUIP = Map.ofEntries(
Map.entry("horse", "diamond"), Map.entry("llama", "red"), Map.entry("happy_ghast", "blue_harness"),
Map.entry("wolf", "armadillo_scute"), Map.entry("trader_llama", "trader_llama"),
Map.entry("nautilus", "diamond")
);
// Humanoid armor test cases (worn equipment, trims, glint, elytra).
// Plain cases: humanoid armor (diamond, dyed leather, trimmed iron, enchanted netherite, glint helmet + elytra).
static EntityState.EquipPiece P(String asset) { return new EntityState.EquipPiece(asset, 0, null, null, false); }
static EntityState.EquipPiece P(String asset, int dye, String trimMat, String trimPat, boolean glint) {
return new EntityState.EquipPiece(asset, dye, trimMat, trimPat, glint);
@@ -134,124 +123,50 @@ public class EntityTestRender {
static final Map<String, EntityState.Equipment> ARMOR = new HashMap<>();
static {
// Full diamond armor.
ARMOR.put("skeleton", new EntityState.Equipment(P("diamond"), P("diamond"), P("diamond"), P("diamond")));
// Dyed (orange) leather armor.
EntityState.EquipPiece le = P("leather", 0xFFFF8000, null, null, false);
ARMOR.put("zombie", new EntityState.Equipment(le, le, le, le));
// Iron armor with a coast/diamond trim.
EntityState.EquipPiece ir = P("iron", 0, "diamond", "coast", false);
ARMOR.put("armor_stand", new EntityState.Equipment(ir, ir, ir, ir));
// Enchanted netherite armor (glint).
EntityState.EquipPiece ne = P("netherite", 0, null, null, true);
ARMOR.put("wither_skeleton", new EntityState.Equipment(ne, ne, ne, ne));
// Enchanted diamond helmet + elytra in the chest slot.
ARMOR.put("player", new EntityState.Equipment(P("diamond", 0, null, null, true), P("elytra"), null, null));
// Gold helmet.
ARMOR.put("piglin", new EntityState.Equipment(P("gold"), null, null, null));
}
// Invisible test cases: armor_stand -> only its (trimmed iron) armor floats; creeper -> nothing renders.
static final java.util.Set<String> INVISIBLE = java.util.Set.of("armor_stand", "creeper");
static final Set<String> INVISIBLE = Set.of("armor_stand", "creeper");
public static void main(String[] args) throws Exception {
Logger log = Logger.getLogger("test");
ResourcePack pack = ResourcePackLoader.load(new File(ROOT, "resourcepack"), log).orElseThrow();
AssetReader reader = new AssetReader(pack);
TextureCache textures = new TextureCache(pack);
BlockModelRegistry registry = new BlockModelRegistry(reader, textures);
BiomeTintProvider tint = new BiomeTintProvider(textures);
eu.mhsl.minecraft.pixelpics.render.entity.cem.CemModelLoader geo = new eu.mhsl.minecraft.pixelpics.render.entity.cem.CemModelLoader();
int n = geo.load(new java.io.FileInputStream("src/main/resources/cem/cem_template_models.json"), log);
n = geo.load(new java.io.FileInputStream("src/main/resources/cem/vanilla_entity_models.json"), log);
log.info("Loaded " + n + " geometries");
SkinCache skins = new SkinCache();
eu.mhsl.minecraft.pixelpics.render.entity.cem.CemBaker baker = new eu.mhsl.minecraft.pixelpics.render.entity.cem.CemBaker(geo, textures, skins);
eu.mhsl.minecraft.pixelpics.assets.font.BitmapFont font =
eu.mhsl.minecraft.pixelpics.assets.font.FontLoader.load(pack, textures, log);
eu.mhsl.minecraft.pixelpics.render.entity.cem.BlockEntityBaker beBaker = new eu.mhsl.minecraft.pixelpics.render.entity.cem.BlockEntityBaker(geo, textures, skins, font);
DefaultScreenRenderer renderer = new DefaultScreenRenderer(registry, tint, textures, baker, beBaker, log);
BlockData air = (BlockData) Proxy.newProxyInstance(EntityTestRender.class.getClassLoader(),
new Class<?>[]{BlockData.class}, (p, m, a) -> {
switch (m.getName()) {
case "getMaterial": return Material.AIR;
case "equals": return p == a[0];
case "hashCode": return System.identityHashCode(p);
case "toString": return "air";
}
Class<?> rt = m.getReturnType();
if (rt == boolean.class) return false;
if (rt.isPrimitive()) return 0;
return null;
});
WorldSnapshot empty = new WorldSnapshot(Map.of(), 0, 1, air);
SkyContext sky = new SkyContext(6000, 0, 6000);
Harness h = new Harness("test");
WorldSnapshot empty = Harness.emptyWorld(0, 1);
String[] list = args.length == 0 ? ENTITIES
: args[0].equals("@variants") ? VARIANT_CASES.toArray(new String[0]) : args;
List<BufferedImage> cells = new ArrayList<>();
for (String key : list) {
BufferedImage v1 = renderEntity(renderer, baker, empty, sky, key, 45);
BufferedImage v2 = renderEntity(renderer, baker, empty, sky, key, 225);
cells.add(labelCell(key, v1, v2));
log.info("rendered " + key);
cells.add(Harness.labelCell(key, renderEntity(h, empty, key, 45), renderEntity(h, empty, key, 225)));
h.log.info("rendered " + key);
}
File outDir = new File(System.getProperty("out", "/home/elias/Dokumente/PixelPics-Entity-Renders"));
outDir.mkdirs();
// One image per entity (both 45°/225° views side by side), named by entity key.
File single = new File(outDir, "einzeln");
single.mkdirs();
for (int i = 0; i < list.length; i++) {
ImageIO.write(cells.get(i), "png", new File(single, list[i].replaceAll("[:@+]", "_") + ".png"));
}
// Compose contact-sheet pages: 2 columns.
int cols = 2, cellW = cells.get(0).getWidth(), cellH = cells.get(0).getHeight();
int perPage = cols * 5;
for (int page = 0, idx = 0; idx < cells.size(); page++) {
int count = Math.min(perPage, cells.size() - idx);
int rows = (count + cols - 1) / cols;
BufferedImage sheet = new BufferedImage(cols * cellW, rows * cellH, BufferedImage.TYPE_INT_RGB);
Graphics2D g = sheet.createGraphics();
g.setColor(new Color(40, 40, 48));
g.fillRect(0, 0, sheet.getWidth(), sheet.getHeight());
for (int i = 0; i < count; i++) {
int r = i / cols, c = i % cols;
g.drawImage(cells.get(idx + i), c * cellW, r * cellH, null);
}
g.dispose();
File f = new File(outDir, String.format("page_%d.png", page));
ImageIO.write(sheet, "png", f);
System.out.println("WROTE " + f);
idx += count;
}
Harness.writeSheets(cells, 2, 10, outDir, page -> "page_" + page + ".png");
}
static BufferedImage renderEntity(DefaultScreenRenderer renderer, eu.mhsl.minecraft.pixelpics.render.entity.cem.CemBaker baker, WorldSnapshot world,
SkyContext sky, String key, float yaw) {
static BufferedImage renderEntity(Harness h, WorldSnapshot world, String key, float yaw) {
EntityState s = state(key, yaw);
RenderedEntity re = baker.bake(s);
RenderedEntity re = h.baker.bake(s);
if (re == null || re.cubes.isEmpty()) { // invisible with no equipment -> nothing renders
return new BufferedImage(TW, TH, BufferedImage.TYPE_INT_RGB);
}
double cx = (re.aabbMin[0] + re.aabbMax[0]) / 2;
double cy = (re.aabbMin[1] + re.aabbMax[1]) / 2;
double cz = (re.aabbMin[2] + re.aabbMax[2]) / 2;
double ext = 0;
for (int a = 0; a < 3; a++) ext = Math.max(ext, re.aabbMax[a] - re.aabbMin[a]);
if (ext < 0.5) ext = 0.5;
double dist = ext / (2 * Math.tan(V_FOV_HALF)) * 1.25 + 0.4;
Vector center = new Vector(cx, cy, cz);
Vector cam = new Vector(cx - dist, cy + dist * 0.42, cz - dist * 0.15);
Location loc = new Location(null, cam.getX(), cam.getY(), cam.getZ());
loc.setDirection(center.clone().subtract(cam));
List<Vector> rayMap = buildRayMap(loc, TW * SSAA, TH * SSAA);
RenderJob job = new RenderJob(world, rayMap, cam, TW, TH, sky, List.of(s), List.of(), List.of(), List.of());
return renderer.execute(job);
Vector center = Harness.center(re.aabbMin, re.aabbMax);
double dist = Harness.frameDistance(re.aabbMin, re.aabbMax, 1.25, 0.4);
Vector cam = new Vector(center.getX() - dist, center.getY() + dist * 0.42, center.getZ() - dist * 0.15);
return h.renderer.execute(Harness.job(world, cam, center, TW, TH, List.of(s), List.of(), List.of(), List.of()));
}
/** Entity state for a case spec ({@code [baby:]type[@variant][+flag…]}, see VARIANT_CASES). */
@@ -266,7 +181,7 @@ public class EntityTestRender {
variant = key.substring(key.indexOf('@') + 1);
key = key.substring(0, key.indexOf('@'));
}
boolean plain = flagSplit.length == 1 && variant == null; // default ENTITIES entry: legacy test setup
boolean plain = flagSplit.length == 1 && variant == null;
if (variant == null) variant = VAR.get(key);
boolean isPlayer = key.equals("player");
int tint = key.equals("tropical_fish") ? 0xFFF9801D : (key.equals("sheep") ? 0xFFF9FFFE : 0);
@@ -283,8 +198,6 @@ public class EntityTestRender {
String equip = plain ? EQUIP.get(key) : null;
boolean arms = flags.contains("arms");
int collar = flags.contains("tame") ? 0xFFB02E26 : 0;
// Pose flags: "p:<state pose>" (sleeping, sitting, lying, …), "pitch:<deg>", "passenger", "shoulder"
// (red + blue parrot on the shoulders), "stand" (custom armor-stand part poses), "mouth" (sweet berries).
List<String> poses = new ArrayList<>();
float pitch = 0;
for (String f : flags) {
@@ -306,11 +219,10 @@ public class EntityTestRender {
partPoses.put("rightLegPose", new float[] {-40, 0, 0});
}
boolean shoulder = flags.contains("shoulder");
ItemSpec mouth = flags.contains("mouth") ? new ItemSpec("sweet_berries", null, null, false) : null;
// Held items: "sword" (right hand), "crossbow" (right hand), "crossbowl" (left hand).
ItemSpec right = flags.contains("sword") ? new ItemSpec("iron_sword", null, null, false)
: flags.contains("crossbow") ? new ItemSpec("crossbow", null, null, false) : null;
ItemSpec left = flags.contains("crossbowl") ? new ItemSpec("crossbow", null, null, false) : null;
ItemSpec mouth = flags.contains("mouth") ? Harness.item("sweet_berries") : null;
ItemSpec right = flags.contains("sword") ? Harness.item("iron_sword")
: flags.contains("crossbow") ? Harness.item("crossbow") : null;
ItemSpec left = flags.contains("crossbowl") ? Harness.item("crossbow") : null;
return new EntityState(key, 0, 0, 0, yaw, baby, 0.8, 1.0,
isPlayer, null, false, variant, tint, 1.0, PROF.get(key), LVL.getOrDefault(key, 0),
plain ? MARK.get(key) : null, saddle, chest, arms, equip, armor,
@@ -318,24 +230,4 @@ public class EntityTestRender {
collar, false, mouth, poses.isEmpty() ? null : String.join("+", poses), partPoses,
shoulder ? "red_blue" : null, shoulder ? "blue" : null);
}
static BufferedImage labelCell(String key, BufferedImage v1, BufferedImage v2) {
int w = v1.getWidth() + v2.getWidth();
int labelH = 20;
BufferedImage cell = new BufferedImage(w, v1.getHeight() + labelH, BufferedImage.TYPE_INT_RGB);
Graphics2D g = cell.createGraphics();
g.setColor(new Color(25, 25, 30));
g.fillRect(0, 0, cell.getWidth(), cell.getHeight());
g.drawImage(v1, 0, labelH, null);
g.drawImage(v2, v1.getWidth(), labelH, null);
g.setColor(Color.WHITE);
g.setFont(new Font("SansSerif", Font.BOLD, 13));
g.drawString(key, 4, 15);
g.dispose();
return cell;
}
static List<Vector> buildRayMap(Location eye, int width, int height) {
return eu.mhsl.minecraft.pixelpics.render.render.DefaultScreenRenderer.buildRayMap(eye, width, height);
}
}
+170
View File
@@ -0,0 +1,170 @@
import eu.mhsl.minecraft.pixelpics.assets.*;
import eu.mhsl.minecraft.pixelpics.assets.font.FontLoader;
import eu.mhsl.minecraft.pixelpics.render.entity.*;
import eu.mhsl.minecraft.pixelpics.render.entity.cem.BlockEntityBaker;
import eu.mhsl.minecraft.pixelpics.render.entity.cem.CemBaker;
import eu.mhsl.minecraft.pixelpics.render.entity.cem.CemModelLoader;
import eu.mhsl.minecraft.pixelpics.render.render.DefaultScreenRenderer;
import eu.mhsl.minecraft.pixelpics.render.render.RenderJob;
import eu.mhsl.minecraft.pixelpics.render.sky.SkyContext;
import eu.mhsl.minecraft.pixelpics.render.snapshot.WorldSnapshot;
import eu.mhsl.minecraft.pixelpics.render.tint.BiomeTintProvider;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.data.BlockData;
import org.bukkit.util.Vector;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Proxy;
import java.util.List;
import java.util.Map;
import java.util.function.IntFunction;
import java.util.logging.Logger;
/**
* Shared setup of the standalone render tools (no server: EntityTestRender, BlockEntityTestRender,
* ObjectTestRender, EntityBenchmark). Renders with the test server's resource pack and the repo's model JSONs.
*
* <p>Build/run from the repo root with a JDK 25: {@code CP=build/classes/java/main:build/resources/main:$(./gradlew -q
* printCompileClasspath)}, {@code javac -d <cls> -cp $CP $(ls tools/*.java | grep -v VanillaModelDump)},
* {@code java -cp <cls>:$CP [-Dout=<dir>] <Tool> [args]}.
*/
final class Harness {
static final String PACK_ROOT = "/home/elias/Dokumente/mcTestServer/plugins/PixelPics";
static final int SSAA = 3;
static final double V_FOV_HALF = Math.toRadians(DefaultScreenRenderer.V_FOV_DEG / 2);
static final SkyContext NOON = new SkyContext(6000, 0, 6000);
final Logger log;
final ResourcePack pack;
final AssetReader reader;
final TextureCache textures;
final BlockModelRegistry registry;
final CemModelLoader geo = new CemModelLoader();
final CemBaker baker;
final BlockEntityBaker beBaker;
final DefaultScreenRenderer renderer;
Harness(String logName) throws IOException {
log = Logger.getLogger(logName);
pack = ResourcePackLoader.load(new File(PACK_ROOT, "resourcepack"), log).orElseThrow();
reader = new AssetReader(pack);
textures = new TextureCache(pack);
registry = new BlockModelRegistry(reader, textures);
// Template set = entity fallback only; the vanilla dump is the primary model source.
for (String f : new String[] {"cem_template_models.json", "vanilla_entity_models.json"}) {
try (InputStream in = new FileInputStream("src/main/resources/cem/" + f)) {
geo.load(in, log);
}
}
SkinCache skins = new SkinCache();
baker = new CemBaker(geo, textures, skins);
beBaker = new BlockEntityBaker(geo, textures, skins, FontLoader.load(pack, textures, log));
renderer = new DefaultScreenRenderer(registry, new BiomeTintProvider(textures), textures, baker, beBaker, log);
}
/**
* Server-free BlockData stand-in: material + blockstate string are all the model registry needs. (Each proxy
* kind gets its own handler class: a shared generic one made the hot getMaterial/hashCode calls megamorphic and
* EntityBenchmark measurably slower.)
*/
static BlockData block(Material m, String state) {
String s = "minecraft:" + m.getKey().getKey() + (state == null ? "" : "[" + state + "]");
return (BlockData) Proxy.newProxyInstance(Harness.class.getClassLoader(),
new Class<?>[]{BlockData.class}, (p, meth, a) -> switch (meth.getName()) {
case "getMaterial" -> m;
case "getAsString", "toString" -> s;
case "equals" -> p == a[0];
case "hashCode" -> System.identityHashCode(p);
default -> defaultValue(meth.getReturnType());
});
}
/** false / 0 / null for an unstubbed proxy method. */
static Object defaultValue(Class<?> rt) {
if (rt == boolean.class) return false;
return rt.isPrimitive() ? 0 : null;
}
static WorldSnapshot emptyWorld(int minY, int maxY) {
return new WorldSnapshot(Map.of(), minY, maxY, block(Material.AIR, null));
}
/** Render job looking from {@code cam} at {@code look}, {@link #SSAA}x supersampled. */
static RenderJob job(WorldSnapshot world, Vector cam, Vector look, int w, int h, List<EntityState> mobs,
List<BlockEntityState> bes, List<DecorationState> decos, List<ObjectState> objects) {
Location loc = new Location(null, cam.getX(), cam.getY(), cam.getZ());
loc.setDirection(look.clone().subtract(cam));
List<Vector> rayMap = DefaultScreenRenderer.buildRayMap(loc, w * SSAA, h * SSAA);
return new RenderJob(world, rayMap, cam, w, h, NOON, mobs, bes, decos, objects);
}
/** Distance at which a box of extent {@code ext} (at least 0.5) fills the view, scaled {@code mul}, plus {@code add}. */
static double frameDistance(double[] min, double[] max, double mul, double add) {
double ext = 0.5;
for (int a = 0; a < 3; a++) ext = Math.max(ext, max[a] - min[a]);
return ext / (2 * Math.tan(V_FOV_HALF)) * mul + add;
}
static Vector center(double[] min, double[] max) {
return new Vector((min[0] + max[0]) / 2, (min[1] + max[1]) / 2, (min[2] + max[2]) / 2);
}
/** Copy of an entity state with some record components replaced (by name). */
static EntityState copy(EntityState s, Map<String, Object> overrides) throws ReflectiveOperationException {
var comps = EntityState.class.getRecordComponents();
Object[] vals = new Object[comps.length];
Class<?>[] types = new Class<?>[comps.length];
for (int i = 0; i < comps.length; i++) {
types[i] = comps[i].getType();
String n = comps[i].getName();
vals[i] = overrides.containsKey(n) ? overrides.get(n) : comps[i].getAccessor().invoke(s);
}
return EntityState.class.getDeclaredConstructor(types).newInstance(vals);
}
static ItemSpec item(String id) {
return new ItemSpec(id, null, null, false);
}
/** The views side by side under a 20 px label. */
static BufferedImage labelCell(String key, BufferedImage... views) {
int w = 0, labelH = 20;
for (BufferedImage v : views) w += v.getWidth();
BufferedImage cell = new BufferedImage(w, views[0].getHeight() + labelH, BufferedImage.TYPE_INT_RGB);
Graphics2D g = cell.createGraphics();
g.setColor(new Color(25, 25, 30));
g.fillRect(0, 0, cell.getWidth(), cell.getHeight());
for (int i = 0, x = 0; i < views.length; x += views[i].getWidth(), i++) g.drawImage(views[i], x, labelH, null);
g.setColor(Color.WHITE);
g.setFont(new Font("SansSerif", Font.BOLD, 13));
g.drawString(key, 4, 15);
g.dispose();
return cell;
}
/** Contact sheets of {@code cols} columns, {@code perPage} cells each, named {@code name(page)}. */
static void writeSheets(List<BufferedImage> cells, int cols, int perPage, File dir, IntFunction<String> name)
throws IOException {
int cw = cells.get(0).getWidth(), ch = cells.get(0).getHeight();
for (int page = 0, idx = 0; idx < cells.size(); page++, idx += perPage) {
int count = Math.min(perPage, cells.size() - idx);
BufferedImage sheet = new BufferedImage(cols * cw, (count + cols - 1) / cols * ch, BufferedImage.TYPE_INT_RGB);
Graphics2D g = sheet.createGraphics();
g.setColor(new Color(40, 40, 48));
g.fillRect(0, 0, sheet.getWidth(), sheet.getHeight());
for (int i = 0; i < count; i++) g.drawImage(cells.get(idx + i), (i % cols) * cw, (i / cols) * ch, null);
g.dispose();
File f = new File(dir, name.apply(page));
ImageIO.write(sheet, "png", f);
System.out.println("WROTE " + f);
}
}
}
+24 -79
View File
@@ -1,105 +1,50 @@
import eu.mhsl.minecraft.pixelpics.assets.*;
import eu.mhsl.minecraft.pixelpics.render.entity.*;
import eu.mhsl.minecraft.pixelpics.render.render.*;
import eu.mhsl.minecraft.pixelpics.render.sky.SkyContext;
import eu.mhsl.minecraft.pixelpics.render.snapshot.WorldSnapshot;
import eu.mhsl.minecraft.pixelpics.render.tint.BiomeTintProvider;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.data.BlockData;
import org.bukkit.util.Vector;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.lang.reflect.Proxy;
import java.util.*;
import java.util.List;
import java.util.logging.Logger;
/**
* Standalone (no server) render of {@link ObjectState}s — block/item/text displays, dropped items,
* falling blocks — in one scene, to check the ObjectBaker geometry, UV orientation and transforms.
* Output: ~/Dokumente/PixelPics-Entity-Renders/objects.png
* Standalone (no server) render of {@link ObjectState}s (block/item/text displays, dropped items) in one scene,
* to check the ObjectBaker geometry, UV orientation and transforms. Output: {@code -Dout} (default
* ~/Dokumente/PixelPics-Entity-Renders/objects.png). Build: see {@link Harness}.
*/
public class ObjectTestRender {
static final String ROOT = "/home/elias/Dokumente/mcTestServer/plugins/PixelPics";
/** Server-free BlockData stand-in: material + blockstate string are all the model registry needs. */
static BlockData block(Material m, String state) {
String s = "minecraft:" + m.getKey().getKey() + (state == null ? "" : "[" + state + "]");
return (BlockData) Proxy.newProxyInstance(ObjectTestRender.class.getClassLoader(),
new Class<?>[]{BlockData.class}, (p, meth, a) -> switch (meth.getName()) {
case "getMaterial" -> m;
case "getAsString" -> s;
case "equals" -> p == a[0];
case "hashCode" -> System.identityHashCode(p);
case "toString" -> s;
default -> {
Class<?> rt = meth.getReturnType();
yield rt == boolean.class ? false : (rt.isPrimitive() ? 0 : null);
}
});
static ObjectState item(Affine a, String id, Material block, String state) {
return ObjectState.item(a, new ItemSpec(id, block == null ? null : Harness.block(block, state), null, false), "none", false);
}
static ObjectState obj(ObjectState.Kind k, Affine a, BlockData b, String item, List<String> lines, int bg) {
return switch (k) {
case BLOCK -> ObjectState.block(a, b, null);
case ITEM -> ObjectState.item(a, new ItemSpec(item, b, null, false), "none", false);
case TEXT -> ObjectState.text(a, lines, 200, 0xFFFFFFFF, bg, true);
case LEASH -> ObjectState.leash(new double[6]);
};
static ObjectState text(Affine a, int background, String... lines) {
return ObjectState.text(a, List.of(lines), 200, 0xFFFFFFFF, background, true);
}
public static void main(String[] args) throws Exception {
Logger log = Logger.getLogger("test");
ResourcePack pack = ResourcePackLoader.load(new File(ROOT, "resourcepack"), log).orElseThrow();
AssetReader reader = new AssetReader(pack);
TextureCache textures = new TextureCache(pack);
BlockModelRegistry registry = new BlockModelRegistry(reader, textures);
BiomeTintProvider tint = new BiomeTintProvider(textures);
var geo = new eu.mhsl.minecraft.pixelpics.render.entity.cem.CemModelLoader();
var font = eu.mhsl.minecraft.pixelpics.assets.font.FontLoader.load(pack, textures, log);
SkinCache skins = new SkinCache();
var baker = new eu.mhsl.minecraft.pixelpics.render.entity.cem.CemBaker(geo, textures, skins);
var beBaker = new eu.mhsl.minecraft.pixelpics.render.entity.cem.BlockEntityBaker(geo, textures, skins, font);
geo.load(new java.io.FileInputStream("src/main/resources/cem/cem_template_models.json"), log);
geo.load(new java.io.FileInputStream("src/main/resources/cem/vanilla_entity_models.json"), log);
DefaultScreenRenderer renderer = new DefaultScreenRenderer(registry, tint, textures, baker, beBaker, log);
Harness h = new Harness("test");
List<ObjectState> objs = new ArrayList<>();
// Block displays: plain grass block (tinted), rotated + scaled stairs.
objs.add(obj(ObjectState.Kind.BLOCK, Affine.translation(0, 0, 0), block(Material.GRASS_BLOCK, "snowy=false"), null, List.of(), 0));
objs.add(obj(ObjectState.Kind.BLOCK,
Affine.translation(2, 0, 0).mul(Affine.rotY(Math.toRadians(30))).mul(Affine.scale(0.75)),
block(Material.OAK_STAIRS, "facing=north,half=bottom,shape=straight,waterlogged=false"), null, List.of(), 0));
// Item displays: a flat item and a block item (centred).
objs.add(obj(ObjectState.Kind.ITEM, Affine.translation(0.5, 2.0, 0.5), null, "diamond_sword", List.of(), 0));
objs.add(obj(ObjectState.Kind.ITEM, Affine.translation(2.5, 2.0, 0.5).mul(Affine.scale(0.6)),
block(Material.OAK_LOG, "axis=y"), "oak_log", List.of(), 0));
// Text display (facing the camera side), opaque background.
objs.add(obj(ObjectState.Kind.TEXT,
Affine.translation(1.5, 3.0, 0).mul(Affine.rotY(Math.PI)).mul(Affine.scale(0.025)), null, null,
List.of("Hallo PixelPics!", "Zeile zwei äöü"), 0xFF203040));
// Text seen from the BACK (front faces +Z, camera is on -Z) on the vanilla default 25% black
// background (blended), and a shield item.
objs.add(obj(ObjectState.Kind.TEXT, Affine.translation(-0.5, 3.0, 0).mul(Affine.scale(0.025)), null, null,
List.of("Rueckseite"), 0x40000000));
objs.add(obj(ObjectState.Kind.ITEM, Affine.translation(4, 1.0, 0.5), null, "shield", List.of(), 0));
objs.add(ObjectState.block(Affine.translation(0, 0, 0), Harness.block(Material.GRASS_BLOCK, "snowy=false"), null));
objs.add(ObjectState.block(Affine.translation(2, 0, 0).mul(Affine.rotY(Math.toRadians(30))).mul(Affine.scale(0.75)),
Harness.block(Material.OAK_STAIRS, "facing=north,half=bottom,shape=straight,waterlogged=false"), null));
// Item displays: a flat item and a block item.
objs.add(item(Affine.translation(0.5, 2.0, 0.5), "diamond_sword", null, null));
objs.add(item(Affine.translation(2.5, 2.0, 0.5).mul(Affine.scale(0.6)), "oak_log", Material.OAK_LOG, "axis=y"));
// Text facing the camera on an opaque background, and seen from the BACK (front faces +Z, camera is on -Z)
// on the vanilla default 25% black background (blended); a shield item.
objs.add(text(Affine.translation(1.5, 3.0, 0).mul(Affine.rotY(Math.PI)).mul(Affine.scale(0.025)), 0xFF203040,
"Hallo PixelPics!", "Zeile zwei äöü"));
objs.add(text(Affine.translation(-0.5, 3.0, 0).mul(Affine.scale(0.025)), 0x40000000, "Rueckseite"));
objs.add(item(Affine.translation(4, 1.0, 0.5), "shield", null, null));
// Dropped item (flat) on the ground.
objs.add(obj(ObjectState.Kind.ITEM, Affine.translation(-1, 0.3, 0.5).mul(Affine.scale(0.5)), null, "apple", List.of(), 0));
objs.add(item(Affine.translation(-1, 0.3, 0.5).mul(Affine.scale(0.5)), "apple", null, null));
BlockData air = block(Material.AIR, null);
WorldSnapshot empty = new WorldSnapshot(Map.of(), 0, 1, air);
SkyContext sky = new SkyContext(6000, 0, 6000);
int W = 480, H = 360;
Vector cam = new Vector(1.5, 3.2, -6.5);
Location loc = new Location(null, cam.getX(), cam.getY(), cam.getZ());
loc.setDirection(new Vector(1.2, 1.3, 0.5).subtract(cam));
List<Vector> rayMap = EntityTestRender.buildRayMap(loc, W * 3, H * 3);
RenderJob job = new RenderJob(empty, rayMap, cam, W, H, sky, List.of(), List.of(), List.of(), objs);
BufferedImage img = renderer.execute(job);
int w = 480, ht = 360;
BufferedImage img = h.renderer.execute(Harness.job(Harness.emptyWorld(0, 1), new Vector(1.5, 3.2, -6.5),
new Vector(1.2, 1.3, 0.5), w, ht, List.of(), List.of(), List.of(), objs));
File out = new File(System.getProperty("out", "/home/elias/Dokumente/PixelPics-Entity-Renders/objects.png"));
ImageIO.write(img, "png", out);
System.out.println("WROTE " + out);
File diff suppressed because it is too large. Load diff
+7 -6
View File
@@ -1,18 +1,19 @@
#!/usr/bin/env bash
# Builds the edge-case test plugin (PPTest.jar) against the compiled PixelPics classes.
# Usage: ./gradlew compileJava && tools/edgecase-plugin/build.sh <compile-classpath> [out.jar]
# (classpath = paper-api + its deps, e.g. the Gradle compileClasspath joined with ":")
# Then drop the jar into the TEST server's plugins/ (never production), start the server and run
# `pptest all` (or `pptest <scene>`) in its console; renders land in plugins/PPTest/<scene>.png.
# Scenes are built at world (5000 + i*64, 200, 5000) on stone platforms; see PPTest#onEnable.
# Usage: ./gradlew compileJava && tools/edgecase-plugin/build.sh [compile-classpath] [out.jar]
# classpath default: ./gradlew -q printCompileClasspath; JAVAC must be a JDK >= 25 (default: javac).
# Drop the jar into the TEST server's plugins/ (never production), start the server and run `pptest all`
# (or `pptest <scene>...`) in its console; renders land in plugins/PPTest/<scene>.png.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
API="${1:?compile classpath (paper-api + deps)}"
API="${1:-$(cd "$ROOT" && ./gradlew -q printCompileClasspath)}"
OUT="${2:-$ROOT/build/PPTest.jar}"
JAVAC="${JAVAC:-javac}"
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT
"$JAVAC" -nowarn -d "$TMP" -cp "$ROOT/build/classes/java/main:$API" "$ROOT/tools/edgecase-plugin/pptest/PPTest.java"
cp "$ROOT/tools/edgecase-plugin/plugin.yml" "$TMP/"
mkdir -p "$(dirname "$OUT")"
rm -f "$OUT" # zip would update an existing jar and keep stale classes
(cd "$TMP" && zip -q -r "$OUT" .)
echo "Wrote $OUT"
File diff suppressed because it is too large. Load diff
+15 -21
View File
@@ -1,12 +1,7 @@
#!/usr/bin/env bash
# Updates the bundled CEM entity-model set (the only asset PixelPics vendors itself).
#
# Source: the CEM Template Loader data (Ewan Howell / wynem) — vanilla Java entity models as JSON,
# already correctly posed. These ultimately mirror Mojang's hardcoded Java EntityModel classes.
#
# NOTE: this set is now only the FALLBACK (rest-pose geometry) for entity types missing from the vanilla
# client dump (+ the elytra fallback). Mob AND block-entity models come from that dump — regenerate it
# with tools/update-vanilla-models.sh <version> on every Minecraft update.
# Updates the vendored CEM template model set (Ewan Howell / wynem: vanilla Java entity models as JSON). It is
# only the rest-pose FALLBACK for entity types missing from the vanilla client dump (+ the elytra fallback);
# mob and block-entity models come from tools/update-vanilla-models.sh.
#
# Usage: tools/update-cem-models.sh # update if newer
# tools/update-cem-models.sh --force # always overwrite
@@ -20,21 +15,20 @@ trap 'rm -f "$TMP"' EXIT
echo "Fetching $URL ..."
curl -fsSL "$URL" -o "$TMP"
# Validate + read versions/counts.
read -r NEW_VER NEW_COUNT < <(python3 -c "
import json,sys
d=json.load(open('$TMP'))
assert 'models' in d and len(d['models'])>50, 'unexpected structure'
print(d.get('version','?'), len(d['models']))
")
CUR_VER="(none)"
if [ -f "$DEST" ]; then
CUR_VER="$(python3 -c "import json;print(json.load(open('$DEST')).get('version','?'))" 2>/dev/null || echo '?')"
fi
# Validates the download, prints "<new version> <model count> <current version>".
read -r NEW_VER NEW_COUNT CUR_VER < <(python3 - "$TMP" "$DEST" <<'PY'
import json, os, sys
d = json.load(open(sys.argv[1]))
assert 'models' in d and len(d['models']) > 50, 'unexpected structure'
cur = '(none)'
if os.path.exists(sys.argv[2]):
try: cur = json.load(open(sys.argv[2])).get('version', '?')
except Exception: cur = '?'
print(d.get('version', '?'), len(d['models']), cur)
PY
)
echo "Current: $CUR_VER Remote: $NEW_VER ($NEW_COUNT models)"
if [ "${1:-}" != "--force" ] && [ "$CUR_VER" = "$NEW_VER" ]; then
echo "Already up to date. (use --force to overwrite)"
exit 0
+31 -20
View File
@@ -1,14 +1,13 @@
#!/usr/bin/env bash
# Regenerates the vanilla entity-model dump straight from a Minecraft CLIENT jar (26.x+ ships
# unobfuscated, so no mappings are needed). This is the primary entity-model source of PixelPics:
# every mob is posed by its own renderer's setupAnim, and new entities/layers/baby/variant models of
# the new version are picked up automatically. Run it once per Minecraft update.
# Regenerates the vanilla entity-model dump from a Minecraft CLIENT jar (26.x+ ships unobfuscated, no mappings
# needed). This is PixelPics' primary entity-model source; run it once per Minecraft update.
#
# Needs the version installed in the launcher (~/.minecraft/versions/<ver>/<ver>.json + jar + libraries).
# Needs the version installed in the launcher (~/.minecraft/versions/<ver>/<ver>.json + jar + libraries) and a
# JDK (the dump runs as a source-file program): $JAVA, $JAVA_HOME or the JDK of `javac` on the PATH.
#
# Usage: tools/update-vanilla-models.sh 26.3 # -> src/main/resources/cem/ (bundled; rebuild jar)
# tools/update-vanilla-models.sh 26.4 <plugin-data-dir> # -> <dir>/vanilla_entity_models.json
# # (overrides the bundled copy, no rebuild)
# Usage: tools/update-vanilla-models.sh 26.3 # -> src/main/resources/cem/ (bundled; rebuild jar)
# tools/update-vanilla-models.sh 26.4 <plugin-data-dir> # -> <dir>/vanilla_entity_models.json
# # (overrides the bundled copy, no rebuild)
set -euo pipefail
VER="${1:?usage: $0 <minecraft-version> [output-dir]}"
@@ -17,9 +16,19 @@ OUT_DIR="${2:-$ROOT/src/main/resources/cem}"
MC="${MINECRAFT_DIR:-$HOME/.minecraft}"
JSON="$MC/versions/$VER/$VER.json"
JAR="$MC/versions/$VER/$VER.jar"
JAVA="${JAVA:-$(command -v java)}"
[[ -f "$JSON" && -f "$JAR" ]] || { echo "Version $VER not found under $MC/versions (start it once in the launcher)"; exit 1; }
NEED="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get("javaVersion", {}).get("majorVersion", 0))' "$JSON")"
is_jdk() { # a JDK (jdk.compiler) at least as new as the game's Java version
[[ -n "$1" && -x "$1" ]] && "$1" --list-modules 2>/dev/null | grep -q '^jdk.compiler@' &&
(( $("$1" --version 2>/dev/null | head -1 | grep -oE '[0-9]+' | head -1) >= NEED ))
}
JAVAC_JAVA="$(command -v javac >/dev/null && echo "$(dirname "$(readlink -f "$(command -v javac)")")/java" || true)"
for j in "${JAVA:-}" "${JAVA_HOME:+$JAVA_HOME/bin/java}" "$JAVAC_JAVA" "$(command -v java || true)"; do
if is_jdk "$j"; then JDK_JAVA="$j"; break; fi
done
[[ -n "${JDK_JAVA:-}" ]] || { echo "No JDK >= $NEED found (a JRE can't launch source files): set JAVA or JAVA_HOME"; exit 1; }
# Classpath: client jar + all libraries listed in the version json.
CP="$(python3 - "$JSON" "$MC" "$JAR" <<'EOF'
import json, os, sys
@@ -34,19 +43,21 @@ print(":".join(cp))
EOF
)"
TMP="$(mktemp --suffix=.json)"
trap 'rm -f "$TMP"; rm -rf "${WORK:-}"' EXIT
# Run from a scratch dir: the game's logger writes a logs/ folder into the working directory.
# Scratch dir as working directory: the game's logger writes a logs/ folder there.
WORK="$(mktemp -d)"
(cd "$WORK" && "$JAVA" -cp "$CP" "$ROOT/tools/VanillaModelDump.java" "$TMP" 2>&1) | grep -E "STDOUT|Exception" | sed 's/.*STDOUT\]: //'
python3 -c "
import json; d = json.load(open('$TMP'))
trap 'rm -rf "$WORK"' EXIT
OUT="$WORK/vanilla_entity_models.json"
if ! (cd "$WORK" && "$JDK_JAVA" -cp "$CP" "$ROOT/tools/VanillaModelDump.java" "$OUT" > "$WORK/dump.log" 2>&1); then
cat "$WORK/dump.log"; echo "Dump failed"; exit 1
fi
grep -E "STDOUT|Exception" "$WORK/dump.log" | sed 's/.*STDOUT\]: //' || true
python3 - "$OUT" <<'EOF'
import json, sys
d = json.load(open(sys.argv[1]))
assert len(d['models']) > 100, 'dump looks incomplete'
print('version', d.get('version'), '-', len(d['models']), 'models,', len(d['entities']), 'entity types')"
print('version', d.get('version'), '-', len(d['models']), 'models,', len(d['entities']), 'entity types')
EOF
mkdir -p "$OUT_DIR"
mv "$TMP" "$OUT_DIR/vanilla_entity_models.json"
chmod 644 "$OUT_DIR/vanilla_entity_models.json"
rm -rf "$WORK"
trap - EXIT
install -m 644 "$OUT" "$OUT_DIR/vanilla_entity_models.json"
echo "Wrote $OUT_DIR/vanilla_entity_models.json"
echo "Note: the runtime resource pack must come from the same version (incl. assets/minecraft/font for text)."