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.RenderJob; import eu.mhsl.minecraft.pixelpics.render.snapshot.WorldSnapshot; import eu.mhsl.minecraft.pixelpics.render.util.ColorUtil; import org.bukkit.DyeColor; import org.bukkit.util.Vector; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.File; import java.util.*; import java.util.List; /** * Standalone (no server, no block models: sign boards, tables, campfires, cages are absent) block-entity and * decoration renderer: each scene framed and labelled -> {@code .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 int TW = 220, TH = 240; static final String[] D = {"S", "W", "N", "E"}; // facing yaw i·90 /** Block entities, viewed from the front ({@code frontYaw}: facing yaw, null = fixed diagonal view). */ record Scene(List states, Float frontYaw) {} static final Map scenes = new LinkedHashMap<>(); static final Map> 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, int baseColor, String head, List pats, List sherds) { return new BlockEntityState(kind, bx, 0, bz, yaw, direction, active, ChestKind.SINGLE, baseColor, color, head, null, pats, sherds, null, null); } static BlockEntityState be(Kind kind, float yaw) { 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(DyeColor dye, boolean glow, String... lines) { return new SignText(List.of(lines), ColorUtil.signFillArgb(dye, glow), ColorUtil.signOutlineArgb(dye), glow); } /** A contents-bearing block entity at the origin (items / spawner mob / beacon beam / statue pose). */ static BlockEntityState contents(Kind kind, float yaw, List 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 items(String... ids) { List out = new ArrayList<>(); for (String id : ids) out.add(id == null ? null : Harness.item(id)); 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 { 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; 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", 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++) { scene("sign_text_" + D[i], i * 90f, sign(Kind.SIGN, i * 90, txt(b, false, "Hello World", "zweite Zeile", "Gruesse: AÖÜ", "ßäöü 12345"), null)); } 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 pats = List.of(new BannerPattern("stripe_bottom", 0xFFCC2020), new BannerPattern("cross", 0xFF2040CC), new BannerPattern("border", 0xFF20A020)); for (float yaw : new float[] {0, 90, 135}) { scene("banner_" + (int) yaw, yaw, st(Kind.BANNER, 0, 0, yaw, null, false, null, 0xFFCC2020, null, List.of(), List.of())); } 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++) { scene("wall_banner_" + D[i], i * 90f, st(Kind.WALL_BANNER, 0, 0, i * 90, null, false, null, 0xFF2040CC, null, pats, List.of())); } for (String d : new String[] {"up", "down", "north", "south", "east", "west"}) { scene("shulker_" + d, 0f, st(Kind.SHULKER_BOX, 0, 0, 0, d, false, d.equals("up") ? null : "red", 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 sherds = List.of("heart_pottery_sherd", "skull_pottery_sherd", "angler_pottery_sherd", "brick"); for (int i = 0; i < 4; i++) { scene("pot_" + D[i], i * 90f, st(Kind.DECORATED_POT, 0, 0, i * 90, null, false, null, 0, null, List.of(), sherds)); } scene("pot_plain", 0f, be(Kind.DECORATED_POT, 0)); scene("bell", null, be(Kind.BELL, 0)); for (int i = 0; i < 4; i++) { 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, 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++) { 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"}) { scene("spawner_" + mob, 0f, contents(Kind.SPAWNER, 0, List.of(), spawnerMob(mob), null, 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 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())); 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())); // 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(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, 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); 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 markers = List.of( new DecorationState.MapMarker("frame", 0, 0, 0), new DecorationState.MapMarker("red_banner", -60, -50, 8), new DecorationState.MapMarker("target_x", 70, 60, 0), new DecorationState.MapMarker("frame", 40, -90, 4), new DecorationState.MapMarker("blue_banner", -100, 90, 0)); 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, 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 cells = new ArrayList<>(); List names = new ArrayList<>(); for (Map.Entry e : scenes.entrySet()) { Scene sc = e.getValue(); List 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 (Map.Entry> e : decoScenes.entrySet()) { List 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")); Harness.writeSheets(cells, 4, cells.size(), outDir, page -> "sheet.png"); } static void render(Harness h, String name, RenderJob job, List cells, List names) { cells.add(Harness.labelCell(name, h.renderer.execute(job))); names.add(name); h.log.info("rendered " + name); } /** {min, max} over the baked boxes (null entries skipped); the unit cube when nothing baked. */ static double[][] bounds(List baked) { double[] min = {Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE}; double[] max = {-Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE}; 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]); } } if (min[0] > max[0]) return new double[][] {{0, 0, 0}, {1, 1, 1}}; return new double[][] {min, max}; } }