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};
}
}