Files
PixelPics/tools/BlockEntityTestRender.java
T

301 lines
19 KiB
Java

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 <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 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<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,
int baseColor, String head, List<BannerPattern> pats, List<String> 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<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<ItemSpec> items(String... ids) {
List<ItemSpec> 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<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}) {
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<String> 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<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()));
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<DecorationState.MapMarker> 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<BufferedImage> cells = new ArrayList<>();
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 (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"));
Harness.writeSheets(cells, 4, cells.size(), outDir, page -> "sheet.png");
}
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);
}
/** {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 (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};
}
}