301 lines
19 KiB
Java
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};
|
|
}
|
|
}
|