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

+6
View File
@@ -60,3 +60,9 @@ spotless {
if (file("local.gradle").exists()) { if (file("local.gradle").exists()) {
apply from: "local.gradle" apply from: "local.gradle"
} }
// Classpath for the standalone tools (tools/*.java harnesses, tools/edgecase-plugin/build.sh).
tasks.register('printCompileClasspath') {
def cp = sourceSets.main.compileClasspath
doLast { println cp.asPath }
}
+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.*;
import eu.mhsl.minecraft.pixelpics.render.entity.BlockEntityState.*; import eu.mhsl.minecraft.pixelpics.render.entity.BlockEntityState.*;
import eu.mhsl.minecraft.pixelpics.render.entity.cem.*; import eu.mhsl.minecraft.pixelpics.render.entity.cem.*;
import eu.mhsl.minecraft.pixelpics.render.render.*; 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.snapshot.WorldSnapshot;
import eu.mhsl.minecraft.pixelpics.render.tint.BiomeTintProvider; import eu.mhsl.minecraft.pixelpics.render.util.ColorUtil;
import org.bukkit.Location; import org.bukkit.DyeColor;
import org.bukkit.Material;
import org.bukkit.block.data.BlockData;
import org.bukkit.util.Vector; import org.bukkit.util.Vector;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.File; import java.io.File;
import java.lang.reflect.Proxy;
import java.util.*; import java.util.*;
import java.util.List; 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 { 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 int TW = 220, TH = 240;
static final String[] D = {"S", "W", "N", "E"}; // facing yaw i·90
static BlockEntityBaker beBaker; /** Block entities, viewed from the front ({@code frontYaw}: facing yaw, null = fixed diagonal view). */
static DecorationBaker decoBaker; 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. */ /** 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, 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()); 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) { 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, return new BlockEntityState(kind, 0, 0, 0, yaw, null, false, null, 0, null, null, null,
List.of(), List.of(), front, back); List.of(), List.of(), front, back);
} }
static SignText txt(org.bukkit.DyeColor dye, boolean glow, String... lines) { static SignText txt(DyeColor dye, boolean glow, String... lines) {
return new SignText(List.of(lines), return new SignText(List.of(lines), ColorUtil.signFillArgb(dye, glow), ColorUtil.signOutlineArgb(dye), glow);
eu.mhsl.minecraft.pixelpics.render.util.ColorUtil.signFillArgb(dye, glow),
eu.mhsl.minecraft.pixelpics.render.util.ColorUtil.signOutlineArgb(dye), glow);
} }
public static void main(String[] args) throws Exception { /** A contents-bearing block entity at the origin (items / spawner mob / beacon beam / statue pose). */
Logger log = Logger.getLogger("test"); static BlockEntityState contents(Kind kind, float yaw, List<ItemSpec> items, EntityState mob, Beam beam,
ResourcePack pack = ResourcePackLoader.load(new File(ROOT, "resourcepack"), log).orElseThrow(); String pose, String color) {
AssetReader reader = new AssetReader(pack); return new BlockEntityState(kind, 0, 0, 0, yaw, null, false, null, 0, color, null, null,
TextureCache textures = new TextureCache(pack); List.of(), List.of(), null, null, items, mob, beam, pose);
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);
BlockData air = (BlockData) Proxy.newProxyInstance(BlockEntityTestRender.class.getClassLoader(), static List<ItemSpec> items(String... ids) {
new Class<?>[]{BlockData.class}, (p, m, a) -> { List<ItemSpec> out = new ArrayList<>();
switch (m.getName()) { for (String id : ids) out.add(id == null ? null : Harness.item(id));
case "getMaterial": return Material.AIR; return out;
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);
// Scenes: label -> list of block-entity states. /** EntityTestRender's default state of a mob with its real bounding box (spawner scale depends on it). */
LinkedHashMap<String, List<BlockEntityState>> scenes = new LinkedHashMap<>(); static EntityState spawnerMob(String type) throws Exception {
// Chests: single + double (all four facings), trapped/ender/copper. A double chest's LEFT half has double[] dims = switch (type) {
// its partner (RIGHT) on facing.getClockWise(): S→west, W→north, N→east, E→south. case "zombie" -> new double[] {0.6, 1.95};
String[] dirs = {"S", "W", "N", "E"}; 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}}; int[][] partner = {{-1, 0}, {0, -1}, {1, 0}, {0, 1}};
for (int i = 0; i < 4; i++) { for (int i = 0; i < 4; i++) {
float yaw = i * 90; float yaw = i * 90;
scenes.put("chest_" + dirs[i], List.of(chest(Kind.CHEST, 0, 0, yaw, ChestKind.SINGLE, null))); scene("chest_" + D[i], yaw, chest(Kind.CHEST, 0, 0, yaw, ChestKind.SINGLE, null));
frontCam.put("chest_" + dirs[i], yaw); scene("double_chest_" + D[i], yaw, chest(Kind.CHEST, 0, 0, yaw, ChestKind.LEFT, null),
scenes.put("double_chest_" + dirs[i], List.of( chest(Kind.CHEST, partner[i][0], partner[i][1], yaw, ChestKind.RIGHT, null));
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);
} }
scenes.put("double_chest_back", scenes.get("double_chest_S")); scenes.put("double_chest_back", new Scene(scenes.get("double_chest_S").states(), 180f));
frontCam.put("double_chest_back", 180f); scene("trapped_double", 0f, chest(Kind.TRAPPED_CHEST, 0, 0, 0, ChestKind.LEFT, null),
scenes.put("trapped_double", List.of( chest(Kind.TRAPPED_CHEST, -1, 0, 0, ChestKind.RIGHT, null));
chest(Kind.TRAPPED_CHEST, 0, 0, 0, ChestKind.LEFT, null), scene("ender_chest", 0f, chest(Kind.ENDER_CHEST, 0, 0, 0, ChestKind.SINGLE, null));
chest(Kind.TRAPPED_CHEST, -1, 0, 0, ChestKind.RIGHT, null))); scene("copper_chest", 0f, chest(Kind.COPPER_CHEST, 0, 0, 0, ChestKind.SINGLE, null));
frontCam.put("trapped_double", 0f); scene("copper_oxidized_double", 0f, chest(Kind.COPPER_CHEST, 0, 0, 0, ChestKind.LEFT, "oxidized"),
scenes.put("ender_chest", List.of(chest(Kind.ENDER_CHEST, 0, 0, 0, ChestKind.SINGLE, null))); chest(Kind.COPPER_CHEST, -1, 0, 0, ChestKind.RIGHT, "oxidized"));
frontCam.put("ender_chest", 0f);
scenes.put("copper_chest", List.of(chest(Kind.COPPER_CHEST, 0, 0, 0, ChestKind.SINGLE, null))); // Sign text (boards are block models, not drawn here); facing 180 → front faces north.
frontCam.put("copper_chest", 0f); DyeColor b = DyeColor.BLACK;
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"};
for (int i = 0; i < 4; i++) { for (int i = 0; i < 4; i++) {
float yaw = i * 90; scene("sign_text_" + D[i], i * 90f,
scenes.put("sign_text_" + D[i], List.of(sign(Kind.SIGN, yaw, txt(B, false, "Hello World", "zweite Zeile", "Gruesse: AÖÜ", "ßäöü 12345"), null))); sign(Kind.SIGN, i * 90, txt(b, false, "Hello World", "zweite Zeile", "Gruesse: AÖÜ", "ßäöü 12345"), null));
frontCam.put("sign_text_" + D[i], yaw);
} }
scenes.put("sign_text_diag", List.of(sign(Kind.SIGN, 22.5f * 3, txt(B, false, "rot 3", "67.5 deg", "", ""), null))); scene("sign_text_diag", 22.5f * 3, sign(Kind.SIGN, 22.5f * 3, txt(b, false, "rot 3", "67.5 deg", "", ""), null));
frontCam.put("sign_text_diag", 22.5f * 3); scene("wall_sign_text", 180f, sign(Kind.WALL_SIGN, 180, txt(b, false, "Wall Sign", "Line 2", "Line 3", "Line 4"), null));
scenes.put("wall_sign_text", List.of(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));
frontCam.put("wall_sign_text", 180f); scene("hanging_sign_text", 180f, sign(Kind.HANGING_SIGN, 180, txt(b, false, "Hanging", "sign text", "row 3", "row 4"), null));
scenes.put("wall_sign_text_E", List.of(sign(Kind.WALL_SIGN, 270, txt(B, false, "Wall East", "Line 2", "", ""), null))); scene("sign_text_red", 180f, sign(Kind.SIGN, 180, txt(DyeColor.RED, false, "RED INK", "colored", "sign text", ""), null));
frontCam.put("wall_sign_text_E", 270f); scene("sign_text_glow", 180f, sign(Kind.SIGN, 180, txt(DyeColor.LIME, true, "GLOWING", "lime glow", "outline!", ""), null));
scenes.put("hanging_sign_text", List.of(sign(Kind.HANGING_SIGN, 180, txt(B, false, "Hanging", "sign text", "row 3", "row 4"), null))); // Back side viewed head-on.
frontCam.put("hanging_sign_text", 180f); scene("sign_view_back", 0f, sign(Kind.SIGN, 180, txt(b, false, "FRONTXY", "f2", "", ""),
scenes.put("sign_text_red", List.of(sign(Kind.SIGN, 180, txt(org.bukkit.DyeColor.RED, false, "RED INK", "colored", "sign text", ""), null))); txt(DyeColor.BLUE, false, "BACKXY", "b2", "", "")));
frontCam.put("sign_text_red", 180f); scene("sign_1line", 180f, sign(Kind.SIGN, 180, txt(b, false, "", "SHOP", "", ""), null));
scenes.put("sign_text_glow", List.of(sign(Kind.SIGN, 180, txt(org.bukkit.DyeColor.LIME, true, "GLOWING", "lime glow", "outline!", ""), null))); scene("sign_long", 180f, sign(Kind.SIGN, 180, txt(b, false, "a very long line here", "", "", ""), null));
frontCam.put("sign_text_glow", 180f);
// Back side viewed head-on: the camera looks at the side opposite the front. // Banners: standing (rotation 0/4/6), patterned (front + side), wall (4 facings).
scenes.put("sign_view_back", List.of(sign(Kind.SIGN, 180, txt(B, false, "FRONTXY", "f2", "", ""), txt(org.bukkit.DyeColor.BLUE, false, "BACKXY", "b2", "", "")))); List<BannerPattern> pats = List.of(new BannerPattern("stripe_bottom", 0xFFCC2020),
frontCam.put("sign_view_back", 0f); new BannerPattern("cross", 0xFF2040CC), new BannerPattern("border", 0xFF20A020));
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));
for (float yaw : new float[] {0, 90, 135}) { for (float yaw : new float[] {0, 90, 135}) {
String k = "banner_" + (int) yaw; scene("banner_" + (int) yaw, yaw, st(Kind.BANNER, 0, 0, yaw, null, false, null, 0xFFCC2020, null, List.of(), List.of()));
scenes.put(k, List.of(st(Kind.BANNER, 0, 0, yaw, null, false, null, 0xFFCC2020, null, List.of(), List.of())));
frontCam.put(k, yaw);
} }
scenes.put("banner_patterned", List.of(st(Kind.BANNER, 0, 0, 0, null, false, null, 0xFFFFFFFF, null, pats, List.of()))); BlockEntityState patterned = st(Kind.BANNER, 0, 0, 0, null, false, null, 0xFFFFFFFF, null, pats, List.of());
frontCam.put("banner_patterned", 0f); scene("banner_patterned", 0f, patterned);
scenes.put("banner_side", scenes.get("banner_patterned")); scene("banner_side", 90f, patterned);
frontCam.put("banner_side", 90f);
for (int i = 0; i < 4; i++) { 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()))); scene("wall_banner_" + D[i], i * 90f,
frontCam.put("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"}) { 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()))); scene("shulker_" + d, 0f,
frontCam.put("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))); scene("conduit", null, 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_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"); List<String> sherds = List.of("heart_pottery_sherd", "skull_pottery_sherd", "angler_pottery_sherd", "brick");
for (int i = 0; i < 4; i++) { 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))); scene("pot_" + D[i], i * 90f, st(Kind.DECORATED_POT, 0, 0, i * 90, null, false, null, 0, null, List.of(), sherds));
frontCam.put("pot_" + D[i], i * 90f);
} }
scenes.put("pot_plain", List.of(be(Kind.DECORATED_POT, 0))); scene("pot_plain", 0f, be(Kind.DECORATED_POT, 0));
frontCam.put("pot_plain", 0f); scene("bell", null, be(Kind.BELL, 0));
scenes.put("bell", List.of(be(Kind.BELL, 0)));
for (int i = 0; i < 4; i++) { 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()))); scene("lectern_book_" + D[i], i * 90f, st(Kind.LECTERN, 0, 0, i * 90, null, true, null, 0, null, List.of(), List.of()));
frontCam.put("lectern_book_" + D[i], i * 90f);
} }
// --- 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) --- // Heads: every type facing the camera, rotations and wall mounts.
scenes.put("enchant_book_closed", List.of(be(Kind.ENCHANTING_TABLE, 0))); for (String t : new String[] {"skeleton", "wither_skeleton", "zombie", "creeper", "player", "piglin", "dragon"}) {
scenes.put("enchant_book_open", List.of(st(Kind.ENCHANTING_TABLE, 0, 0, 90, null, true, null, 0, null, List.of(), List.of()))); scene("head_" + t, 0f, head(Kind.HEAD, t, 0));
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))); scene("head_dragon_side", 90f, head(Kind.HEAD, "dragon", 0));
scenes.put("campfire_items_E", List.of(contents(Kind.CAMPFIRE, 270, items("beef", "cod", "porkchop", "salmon"), null, null, null, null))); for (float yaw : new float[] {90, 157.5f, 270}) scene("head_creeper_" + yaw, yaw, head(Kind.HEAD, "creeper", yaw));
scenes.put("shelf_items_S", List.of(contents(Kind.SHELF, 0, items("diamond", "book", "golden_apple"), null, null, null, null))); for (int i = 0; i < 4; i++) scene("wall_head_" + D[i], i * 90f, head(Kind.WALL_HEAD, "skeleton", i * 90));
frontCam.put("shelf_items_S", 0f); scene("wall_dragon_N", 180f, head(Kind.WALL_HEAD, "dragon", 180));
scenes.put("shelf_items_E", List.of(contents(Kind.SHELF, 270, items("stick", null, "emerald"), null, null, null, null))); scene("wall_piglin_E", 270f, head(Kind.WALL_HEAD, "piglin", 270));
frontCam.put("shelf_items_E", 270f);
scenes.put("vault_item", List.of(contents(Kind.VAULT, 0, items("trial_key"), null, null, null, null))); // Block-entity contents.
frontCam.put("vault_item", 0f); 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[] poses = {"standing", "running", "sitting", "star"};
String[] stages = {null, "exposed", "weathered", "oxidized"}; String[] stages = {null, "exposed", "weathered", "oxidized"};
for (int i = 0; i < 4; i++) { 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]))); scene("statue_" + poses[i], i * 90f,
frontCam.put("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"}) { 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))); scene("spawner_" + mob, 0f, contents(Kind.SPAWNER, 0, List.of(), spawnerMob(mob), null, null, null));
frontCam.put("spawner_" + mob, 0f);
} }
scenes.put("beacon_beam", List.of(contents(Kind.BEACON, 0, List.of(), null, scene("beacon_beam", null, contents(Kind.BEACON, 0, List.of(), null, new Beam(List.of(
new Beam(List.of(new BeamSection(0xFFF9FFFE, 1), new BeamSection(0xFFB02E26, 2), new BeamSection(0xFF3C44AA, 1)), 1f), null, null))); new BeamSection(0xFFF9FFFE, 1), new BeamSection(0xFFB02E26, 2), new BeamSection(0xFF3C44AA, 1)), 1f), null, null));
List<BlockEntityState> portal = new ArrayList<>(); 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())); 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); scene("end_portal", null, portal.toArray(new BlockEntityState[0]));
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_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. // Paintings (thin quads facing south / north) and 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.
decoScenes.put("painting_kebab_S", List.of(new DecorationState(DecorationState.Kind.PAINTING, 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))); 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, 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))); 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, 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))); 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, decoScenes.put("item_frame", List.of(southFrame(Harness.item("diamond"))));
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(southFrame(null)));
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_E", List.of(new DecorationState(DecorationState.Kind.ITEM_FRAME, 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))); 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).
// 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).
BlockEntityState shieldPats = new BlockEntityState(Kind.BANNER, 0, 0, 0, 0, null, false, null, 0xFF3C44AA, null, null, null, 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); 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, BlockEntityState shieldRed = new BlockEntityState(Kind.BANNER, 0, 0, 0, 0, null, false, null, 0xFFB02E26, null, null, null,
List.of(), List.of(), null, null); List.of(), List.of(), null, null);
String[] frameItems = {"shield", "shield_red", "shield_patterns", "chest", "trapped_chest", "ender_chest"}; decoScenes.put("item_frame_shield", List.of(southFrame(Harness.item("shield"))));
for (String id : frameItems) { decoScenes.put("item_frame_shield_red", List.of(southFrame(new ItemSpec("shield", null, shieldRed, false))));
ItemSpec spec = switch (id) { decoScenes.put("item_frame_shield_patterns", List.of(southFrame(new ItemSpec("shield", null, shieldPats, false))));
case "shield_red" -> new ItemSpec("shield", null, shieldRed, false); for (String id : new String[] {"chest", "trapped_chest", "ender_chest"}) {
case "shield_patterns" -> new ItemSpec("shield", null, shieldPats, false); decoScenes.put("item_frame_" + id, List.of(southFrame(Harness.item(id))));
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)));
} }
// A checkered map with decorations (map/decorations sprites as MapRenderer), rotation 0 and 1.
int[][] mapPx = new int[128][128]; 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; 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( List<DecorationState.MapMarker> markers = List.of(
@@ -271,157 +227,74 @@ public class BlockEntityTestRender {
for (int rot : new int[] {0, 1}) { for (int rot : new int[] {0, 1}) {
decoScenes.put("item_frame_map_markers_r" + rot, List.of(new DecorationState(DecorationState.Kind.ITEM_FRAME, 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, 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<BufferedImage> cells = new ArrayList<>();
List<String> names = new ArrayList<>(scenes.keySet()); List<String> names = new ArrayList<>();
for (String name : names) { for (Map.Entry<String, Scene> e : scenes.entrySet()) {
cells.add(labelCell(name, renderScene(renderer, empty, sky, scenes.get(name), frontCam.get(name)))); Scene sc = e.getValue();
log.info("rendered " + name); 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()) { for (Map.Entry<String, List<DecorationState>> e : decoScenes.entrySet()) {
cells.add(labelCell(name, renderDeco(renderer, empty, sky, decoScenes.get(name)))); List<RenderedEntity> baked = new ArrayList<>();
names.add(name); for (DecorationState s : e.getValue()) baked.add(decoBaker.bake(s));
log.info("rendered " + name); 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")); File outDir = new File(System.getProperty("out", "/home/elias/Dokumente/PixelPics-BlockEntity-Renders"));
outDir.mkdirs(); outDir.mkdirs();
for (int i = 0; i < names.size(); i++) ImageIO.write(cells.get(i), "png", new File(outDir, names.get(i) + ".png")); 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");
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);
} }
/** Scenes viewed from the front (camera along the facing yaw, raised, slightly to the side). */ static void render(Harness h, String name, RenderJob job,
static final Map<String, Float> frontCam = new HashMap<>(); List<BufferedImage> cells, List<String> names) {
cells.add(Harness.labelCell(name, h.renderer.execute(job)));
static BlockEntityState chest(Kind kind, int bx, int bz, float yaw, ChestKind ck, String variant) { names.add(name);
return new BlockEntityState(kind, bx, 0, bz, yaw, null, false, ck, 0, variant, null, null, h.log.info("rendered " + name);
List.of(), List.of(), null, null);
} }
/** A contents-bearing block-entity at the origin (items / spawner mob / beacon beam / statue pose). */ /** {min, max} over the baked boxes (null entries skipped); the unit cube when nothing baked. */
static BlockEntityState contents(Kind kind, float yaw, List<eu.mhsl.minecraft.pixelpics.render.entity.ItemSpec> items, static double[][] bounds(List<RenderedEntity> baked) {
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) {
double[] min = {Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE}; double[] min = {Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE};
double[] max = {-Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE}; double[] max = {-Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE};
for (BlockEntityState s : states) { for (RenderedEntity re : baked) {
RenderedEntity re = beBaker.bake(s);
if (re == null) continue; 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 (min[0] > max[0]) return new double[][] {{0, 0, 0}, {1, 1, 1}};
if (states.get(0).kind() == Kind.BEACON) max[1] = min[1] + 4; // frame the beam's foot, not 2048 blocks return new double[][] {min, max};
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);
} }
} }
+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.entity.*;
import eu.mhsl.minecraft.pixelpics.render.render.*; 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.snapshot.WorldSnapshot;
import eu.mhsl.minecraft.pixelpics.render.tint.BiomeTintProvider;
import org.bukkit.ChunkSnapshot; import org.bukkit.ChunkSnapshot;
import org.bukkit.Location;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.block.data.BlockData; import org.bukkit.block.data.BlockData;
import org.bukkit.util.Vector; import org.bukkit.util.Vector;
@@ -16,41 +12,24 @@ import java.io.File;
import java.lang.reflect.Proxy; import java.lang.reflect.Proxy;
import java.util.*; import java.util.*;
import java.util.List; 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 * Reproducible render benchmark (no server): farm scenes with hundreds of mobs / dropped items on a grass floor
* floor plus a "normal" scene (few mobs, translucent + additive layers). Renders every scene {@code -Druns} * plus a "normal" scene (few mobs, translucent + additive layers). Renders every scene {@code -Druns} times (after
* times (after {@code -Dwarmup} runs), prints the median wall time and writes one PNG per scene to * {@code -Dwarmup} runs), prints median/min wall time and writes one PNG per scene to {@code -Dout} (default
* {@code -Dout} (default ~/Dokumente/PixelPics-Entity-Renders/bench) for before/after pixel diffs. * ~/Dokumente/PixelPics-Entity-Renders/bench) for before/after pixel diffs. Args: scene names (default all):
* Args: scene names to run (default all): farm_mobs, farm_empty (same view, no entities), farm_mobs_close, * farm_mobs, farm_empty (same view, no entities), farm_mobs_close, farm_items, village, normal.
* farm_items, village, normal. {@code -Djava.util.concurrent.ForkJoinPool.common.parallelism=N} pins the thread count. * {@code -Djava.util.concurrent.ForkJoinPool.common.parallelism=N} pins the thread count. Build: see {@link Harness}.
*/ */
public class EntityBenchmark { public class EntityBenchmark {
static final String ROOT = "/home/elias/Dokumente/mcTestServer/plugins/PixelPics"; static final int W = 320, H = 240;
static final int W = 320, H = 240, SSAA = 3;
record Scene(String name, Vector cam, Vector look, List<EntityState> mobs, List<ObjectState> objects) {} record Scene(String name, Vector cam, Vector look, List<EntityState> mobs, List<ObjectState> objects) {}
public static void main(String[] args) throws Exception { public static void main(String[] args) throws Exception {
Logger log = Logger.getLogger("bench"); Harness h = new Harness("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);
WorldSnapshot world = farmWorld(); WorldSnapshot world = farmWorld();
SkyContext sky = new SkyContext(6000, 0, 6000);
int runs = Integer.getInteger("runs", 5), warmup = Integer.getInteger("warmup", 2); 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")); File outDir = new File(System.getProperty("out", "/home/elias/Dokumente/PixelPics-Entity-Renders/bench"));
outDir.mkdirs(); outDir.mkdirs();
@@ -58,16 +37,13 @@ public class EntityBenchmark {
for (Scene sc : scenes()) { for (Scene sc : scenes()) {
if (!only.isEmpty() && !only.contains(sc.name)) continue; if (!only.isEmpty() && !only.contains(sc.name)) continue;
Location loc = new Location(null, sc.cam.getX(), sc.cam.getY(), sc.cam.getZ()); RenderJob job = Harness.job(world, sc.cam, sc.look, W, H, sc.mobs, List.of(), List.of(), sc.objects);
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);
BufferedImage img = null; 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]; long[] times = new long[runs];
for (int i = 0; i < runs; i++) { for (int i = 0; i < runs; i++) {
long t0 = System.nanoTime(); long t0 = System.nanoTime();
img = renderer.execute(job); img = h.renderer.execute(job);
times[i] = System.nanoTime() - t0; times[i] = System.nanoTime() - t0;
} }
Arrays.sort(times); 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<>(); List<Scene> out = new ArrayList<>();
// Mob farm: 400 animals in a 20x20 pen (1.4 block spacing), seen from an elevated corner. // Mob farm: 400 animals in a 20x20 pen (1.4 block spacing), seen from an elevated corner.
List<EntityState> farm = new ArrayList<>(); List<EntityState> farm = new ArrayList<>();
@@ -106,9 +82,9 @@ public class EntityBenchmark {
ItemSpec spec; ItemSpec spec;
if (r.nextInt(4) == 0) { if (r.nextInt(4) == 0) {
int b = r.nextInt(blocks.length); 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 { } 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)); 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("villager", 4, 1, 6, 160, false));
few.add(mob("zombie", 6.5, 1, 6, 220, false)); few.add(mob("zombie", 6.5, 1, 6, 220, false));
few.add(mob("slime", 2, 1, 5.5, 120, false)); few.add(mob("slime", 2, 1, 5.5, 120, false));
EntityState creeper = mob("creeper", 7, 1, 3.5, 250, false); few.add(Harness.copy(mob("creeper", 7, 1, 3.5, 250, false), Map.of("powered", true)));
few.add(withPowered(creeper));
few.add(mob("chicken", 3.5, 1, 1.5, 90, false)); few.add(mob("chicken", 3.5, 1, 1.5, 90, false));
List<ObjectState> objs = new ArrayList<>(); 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)), 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)); 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)); 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, static final int[] DYES = {0xFFF9FFFE, 0xFFF9801D, 0xFFC74EBD, 0xFF3AB3DA, 0xFFFED83D, 0xFF80C71F, 0xFFF38BAA,
0xFF474F52, 0xFF9D9D97, 0xFF169C9C, 0xFF8932B8, 0xFF3C44AA, 0xFF835432, 0xFF5E7C16, 0xFFB02E26, 0xFF1D1D21}; 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) { 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, throws ReflectiveOperationException {
false, null, false, type, 0, 1.0, prof, level, return Harness.copy(mob("villager", x, 1, z, yaw, baby), Map.of("width", 0.6, "height", 1.95, "variant", type,
null, false, false, false, null, null, "profession", prof, "villagerLevel", level));
false, 0, 0, 0, false, null, null, null, 0, false, null, null, null, null, null);
} }
/** {@code s} with wool tint, humanoid armor and collar dye replaced. */ /** {@code s} with wool tint, humanoid armor and collar dye replaced. */
static EntityState with(EntityState s, int tint, EntityState.Equipment eq, int collar) { 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(), throws ReflectiveOperationException {
s.player(), s.skinUrl(), s.slim(), s.variant(), tint, s.sizeScale(), s.profession(), s.villagerLevel(), Map<String, Object> o = new HashMap<>();
s.markings(), s.saddle(), s.chest(), s.arms(), s.bodyEquip(), eq, s.invisible(), s.pitch(), o.put("tint", tint);
s.bodyDye(), s.tint2(), s.passenger(), s.rightHand(), s.leftHand(), s.headItem(), collar, s.powered(), o.put("equipment", eq);
s.mouthItem(), s.pose(), s.partPoses(), s.shoulderLeft(), s.shoulderRight()); o.put("collar", collar);
return Harness.copy(s, o);
} }
/** The variant the game always supplies (cow/pig/chicken have no texture without one). */ /** 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() * 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). * for non-air neighbours, which needs a server registry (not available standalone).
*/ */
static WorldSnapshot farmWorld() { static WorldSnapshot farmWorld() {
BlockData air = ObjectTestRender.block(Material.AIR, null); BlockData air = Harness.block(Material.AIR, null);
BlockData grass = ObjectTestRender.block(Material.GRASS_BLOCK, "snowy=false"); BlockData grass = Harness.block(Material.GRASS_BLOCK, "snowy=false");
Map<Long, ChunkSnapshot> chunks = new HashMap<>(); Map<Long, ChunkSnapshot> chunks = new HashMap<>();
for (int cx = -3; cx < 3; cx++) { for (int cx = -3; cx < 3; cx++) {
for (int cz = -3; cz < 3; cz++) { for (int cz = -3; cz < 3; cz++) {
final int bx0 = cx * 16, bz0 = cz * 16;
ChunkSnapshot cs = (ChunkSnapshot) Proxy.newProxyInstance(EntityBenchmark.class.getClassLoader(), ChunkSnapshot cs = (ChunkSnapshot) Proxy.newProxyInstance(EntityBenchmark.class.getClassLoader(),
new Class<?>[]{ChunkSnapshot.class}, (p, m, a) -> { new Class<?>[]{ChunkSnapshot.class}, (p, m, a) -> switch (m.getName()) {
switch (m.getName()) { case "getBlockData" -> (int) a[1] == 0 ? grass : air;
case "getBlockData": { case "equals" -> p == a[0];
int y = (int) a[1]; case "hashCode" -> System.identityHashCode(p);
return y == 0 ? grass : air; case "toString" -> "chunk";
} default -> Harness.defaultValue(m.getReturnType());
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;
}); });
chunks.put(WorldSnapshot.chunkKey(cx, cz), cs); 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.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.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 org.bukkit.util.Vector;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.File; import java.io.File;
import java.lang.reflect.Proxy;
import java.util.*; import java.util.*;
import java.util.List; 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 { 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 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 = { static final String[] ENTITIES = {
"allay","armadillo","armor_stand","axolotl","bat","bee","blaze","bogged","breeze","camel", "allay","armadillo","armor_stand","axolotl","bat","bee","blaze","bogged","breeze","camel",
"cat","cave_spider","chicken","cod","copper_golem","cow","creaking","creeper","dolphin","donkey", "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" "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: // "@variants" case set (before/after pixel diffs of refactors). Spec: [baby:]type[@variant][+flag…], flags:
// [baby:]type[@variant][+flag…], flags: chest, arms, tame (red collar), saddle, armor (full diamond), noarmor. // 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 final List<String> VARIANT_CASES = new ArrayList<>();
static { static {
for (String v : new String[] {"tabby", "black", "red", "siamese", "british_shorthair", "calico", "persian", 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@red");
VARIANT_CASES.add("mooshroom@brown"); VARIANT_CASES.add("mooshroom@brown");
VARIANT_CASES.add("baby:mooshroom@brown"); VARIANT_CASES.add("baby:mooshroom@brown");
// Armor stand with/without arms, small, with armor. VARIANT_CASES.addAll(List.of("armor_stand", "armor_stand+arms", "baby:armor_stand", "baby:armor_stand+arms",
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"));
"armor_stand+arms+armor", "baby:armor_stand+arms+armor"}) VARIANT_CASES.add(a); VARIANT_CASES.addAll(List.of("llama@creamy", "llama@creamy+chest", "llama@brown+chest+saddle", "llama@gray+chest",
// Chests.
for (String c : new String[] {"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", "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). // Armor wearers (and non-wearers given armor: nothing must appear).
for (String t : new String[] {"zombie", "husk", "drowned", "zombie_villager", "zombified_piglin", "skeleton", for (String t : new String[] {"zombie", "husk", "drowned", "zombie_villager", "zombified_piglin", "skeleton",
"stray", "wither_skeleton", "bogged", "parched", "piglin", "piglin_brute", "giant", "player", "stray", "wither_skeleton", "bogged", "parched", "piglin", "piglin_brute", "giant", "player",
@@ -84,7 +75,7 @@ public class EntityTestRender {
VARIANT_CASES.add(t + "+armor"); VARIANT_CASES.add(t + "+armor");
// Layer / texture states: emissive eyes, sheep undercoat, angry wolves, tipped arrows, invulnerable // Layer / texture states: emissive eyes, sheep undercoat, angry wolves, tipped arrows, invulnerable
// wither, partial-alpha cutout textures, held items (piglin arm poses), elytra. // 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", "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", "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", "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", "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", "zombified_piglin+sword", "piglin_brute+sword", "pillager+crossbow",
"vindicator+sword", "armor_stand+elytra", "zombie+elytra", "baby:armor_stand+armor", "vindicator+sword", "armor_stand+elytra", "zombie+elytra", "baby:armor_stand+armor",
"baby:armor_stand+arms+elytra"}) "baby:armor_stand+arms+elytra"));
VARIANT_CASES.add(c);
VARIANT_CASES.addAll(List.of(ENTITIES)); 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( static final Map<String, String> VAR = Map.ofEntries(
Map.entry("cat", "tabby"), Map.entry("wolf", "pale"), Map.entry("axolotl", "lucy"), Map.entry("cat", "tabby"), Map.entry("wolf", "pale"), Map.entry("axolotl", "lucy"),
Map.entry("parrot", "red"), Map.entry("rabbit", "brown"), Map.entry("horse", "white"), 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") 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( static final Map<String, String> PROF = Map.ofEntries(
Map.entry("villager", "librarian"), Map.entry("zombie_villager", "farmer") Map.entry("villager", "librarian"), Map.entry("zombie_villager", "farmer")
); );
@@ -116,17 +105,17 @@ public class EntityTestRender {
Map.entry("villager", 5) Map.entry("villager", 5)
); );
// Horse/llama/donkey equipment for the standalone render. // Plain cases: horse markings, saddles, chests, body armor / carpet / harness.
static final Map<String, String> MARK = Map.of("horse", "blackdots"); // coat markings static final Map<String, String> MARK = Map.of("horse", "blackdots");
static final java.util.Set<String> SADDLE = java.util.Set.of("horse", "donkey", "mule", "nautilus", "pig", "strider", "camel"); static final Set<String> SADDLE = Set.of("horse", "donkey", "mule", "nautilus", "pig", "strider", "camel");
static final java.util.Set<String> CHEST = java.util.Set.of("llama", "donkey"); static final Set<String> CHEST = Set.of("llama", "donkey");
static final Map<String, String> EQUIP = Map.ofEntries( // armor / carpet static final Map<String, String> EQUIP = Map.ofEntries(
Map.entry("horse", "diamond"), Map.entry("llama", "red"), Map.entry("happy_ghast", "blue_harness"), 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("wolf", "armadillo_scute"), Map.entry("trader_llama", "trader_llama"),
Map.entry("nautilus", "diamond") 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) { return new EntityState.EquipPiece(asset, 0, null, null, false); }
static EntityState.EquipPiece P(String asset, int dye, String trimMat, String trimPat, boolean glint) { static EntityState.EquipPiece P(String asset, int dye, String trimMat, String trimPat, boolean glint) {
return new EntityState.EquipPiece(asset, dye, trimMat, trimPat, 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 final Map<String, EntityState.Equipment> ARMOR = new HashMap<>();
static { static {
// Full diamond armor.
ARMOR.put("skeleton", new EntityState.Equipment(P("diamond"), P("diamond"), P("diamond"), P("diamond"))); 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); EntityState.EquipPiece le = P("leather", 0xFFFF8000, null, null, false);
ARMOR.put("zombie", new EntityState.Equipment(le, le, le, le)); 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); EntityState.EquipPiece ir = P("iron", 0, "diamond", "coast", false);
ARMOR.put("armor_stand", new EntityState.Equipment(ir, ir, ir, ir)); ARMOR.put("armor_stand", new EntityState.Equipment(ir, ir, ir, ir));
// Enchanted netherite armor (glint).
EntityState.EquipPiece ne = P("netherite", 0, null, null, true); EntityState.EquipPiece ne = P("netherite", 0, null, null, true);
ARMOR.put("wither_skeleton", new EntityState.Equipment(ne, ne, ne, ne)); 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)); 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)); 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. // 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 { public static void main(String[] args) throws Exception {
Logger log = Logger.getLogger("test"); Harness h = new Harness("test");
ResourcePack pack = ResourcePackLoader.load(new File(ROOT, "resourcepack"), log).orElseThrow(); WorldSnapshot empty = Harness.emptyWorld(0, 1);
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);
String[] list = args.length == 0 ? ENTITIES String[] list = args.length == 0 ? ENTITIES
: args[0].equals("@variants") ? VARIANT_CASES.toArray(new String[0]) : args; : args[0].equals("@variants") ? VARIANT_CASES.toArray(new String[0]) : args;
List<BufferedImage> cells = new ArrayList<>(); List<BufferedImage> cells = new ArrayList<>();
for (String key : list) { for (String key : list) {
BufferedImage v1 = renderEntity(renderer, baker, empty, sky, key, 45); cells.add(Harness.labelCell(key, renderEntity(h, empty, key, 45), renderEntity(h, empty, key, 225)));
BufferedImage v2 = renderEntity(renderer, baker, empty, sky, key, 225); h.log.info("rendered " + key);
cells.add(labelCell(key, v1, v2));
log.info("rendered " + key);
} }
File outDir = new File(System.getProperty("out", "/home/elias/Dokumente/PixelPics-Entity-Renders")); 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"); File single = new File(outDir, "einzeln");
single.mkdirs(); single.mkdirs();
for (int i = 0; i < list.length; i++) { for (int i = 0; i < list.length; i++) {
ImageIO.write(cells.get(i), "png", new File(single, list[i].replaceAll("[:@+]", "_") + ".png")); ImageIO.write(cells.get(i), "png", new File(single, list[i].replaceAll("[:@+]", "_") + ".png"));
} }
Harness.writeSheets(cells, 2, 10, outDir, page -> "page_" + page + ".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;
}
} }
static BufferedImage renderEntity(DefaultScreenRenderer renderer, eu.mhsl.minecraft.pixelpics.render.entity.cem.CemBaker baker, WorldSnapshot world, static BufferedImage renderEntity(Harness h, WorldSnapshot world, String key, float yaw) {
SkyContext sky, String key, float yaw) {
EntityState s = state(key, 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 if (re == null || re.cubes.isEmpty()) { // invisible with no equipment -> nothing renders
return new BufferedImage(TW, TH, BufferedImage.TYPE_INT_RGB); return new BufferedImage(TW, TH, BufferedImage.TYPE_INT_RGB);
} }
double cx = (re.aabbMin[0] + re.aabbMax[0]) / 2; Vector center = Harness.center(re.aabbMin, re.aabbMax);
double cy = (re.aabbMin[1] + re.aabbMax[1]) / 2; double dist = Harness.frameDistance(re.aabbMin, re.aabbMax, 1.25, 0.4);
double cz = (re.aabbMin[2] + re.aabbMax[2]) / 2; Vector cam = new Vector(center.getX() - dist, center.getY() + dist * 0.42, center.getZ() - dist * 0.15);
double ext = 0; return h.renderer.execute(Harness.job(world, cam, center, TW, TH, List.of(s), List.of(), List.of(), List.of()));
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);
} }
/** Entity state for a case spec ({@code [baby:]type[@variant][+flag…]}, see VARIANT_CASES). */ /** 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); variant = key.substring(key.indexOf('@') + 1);
key = key.substring(0, key.indexOf('@')); 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); if (variant == null) variant = VAR.get(key);
boolean isPlayer = key.equals("player"); boolean isPlayer = key.equals("player");
int tint = key.equals("tropical_fish") ? 0xFFF9801D : (key.equals("sheep") ? 0xFFF9FFFE : 0); 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; String equip = plain ? EQUIP.get(key) : null;
boolean arms = flags.contains("arms"); boolean arms = flags.contains("arms");
int collar = flags.contains("tame") ? 0xFFB02E26 : 0; 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<>(); List<String> poses = new ArrayList<>();
float pitch = 0; float pitch = 0;
for (String f : flags) { for (String f : flags) {
@@ -306,11 +219,10 @@ public class EntityTestRender {
partPoses.put("rightLegPose", new float[] {-40, 0, 0}); partPoses.put("rightLegPose", new float[] {-40, 0, 0});
} }
boolean shoulder = flags.contains("shoulder"); boolean shoulder = flags.contains("shoulder");
ItemSpec mouth = flags.contains("mouth") ? new ItemSpec("sweet_berries", null, null, false) : null; ItemSpec mouth = flags.contains("mouth") ? Harness.item("sweet_berries") : null;
// Held items: "sword" (right hand), "crossbow" (right hand), "crossbowl" (left hand). ItemSpec right = flags.contains("sword") ? Harness.item("iron_sword")
ItemSpec right = flags.contains("sword") ? new ItemSpec("iron_sword", null, null, false) : flags.contains("crossbow") ? Harness.item("crossbow") : null;
: flags.contains("crossbow") ? new ItemSpec("crossbow", null, null, false) : null; ItemSpec left = flags.contains("crossbowl") ? Harness.item("crossbow") : null;
ItemSpec left = flags.contains("crossbowl") ? new ItemSpec("crossbow", null, null, false) : null;
return new EntityState(key, 0, 0, 0, yaw, baby, 0.8, 1.0, 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), isPlayer, null, false, variant, tint, 1.0, PROF.get(key), LVL.getOrDefault(key, 0),
plain ? MARK.get(key) : null, saddle, chest, arms, equip, armor, 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, collar, false, mouth, poses.isEmpty() ? null : String.join("+", poses), partPoses,
shoulder ? "red_blue" : null, shoulder ? "blue" : null); 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.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.Material;
import org.bukkit.block.data.BlockData;
import org.bukkit.util.Vector; import org.bukkit.util.Vector;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.File; import java.io.File;
import java.lang.reflect.Proxy;
import java.util.*; import java.util.*;
import java.util.List; import java.util.List;
import java.util.logging.Logger;
/** /**
* Standalone (no server) render of {@link ObjectState}s — block/item/text displays, dropped items, * Standalone (no server) render of {@link ObjectState}s (block/item/text displays, dropped items) in one scene,
* falling blocks — in one scene, to check the ObjectBaker geometry, UV orientation and transforms. * to check the ObjectBaker geometry, UV orientation and transforms. Output: {@code -Dout} (default
* Output: ~/Dokumente/PixelPics-Entity-Renders/objects.png * ~/Dokumente/PixelPics-Entity-Renders/objects.png). Build: see {@link Harness}.
*/ */
public class ObjectTestRender { public class ObjectTestRender {
static final String ROOT = "/home/elias/Dokumente/mcTestServer/plugins/PixelPics"; 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);
/** 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 obj(ObjectState.Kind k, Affine a, BlockData b, String item, List<String> lines, int bg) { static ObjectState text(Affine a, int background, String... lines) {
return switch (k) { return ObjectState.text(a, List.of(lines), 200, 0xFFFFFFFF, background, true);
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]);
};
} }
public static void main(String[] args) throws Exception { public static void main(String[] args) throws Exception {
Logger log = Logger.getLogger("test"); Harness h = new Harness("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);
List<ObjectState> objs = new ArrayList<>(); List<ObjectState> objs = new ArrayList<>();
// Block displays: plain grass block (tinted), rotated + scaled stairs. // 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(ObjectState.block(Affine.translation(0, 0, 0), Harness.block(Material.GRASS_BLOCK, "snowy=false"), null));
objs.add(obj(ObjectState.Kind.BLOCK, objs.add(ObjectState.block(Affine.translation(2, 0, 0).mul(Affine.rotY(Math.toRadians(30))).mul(Affine.scale(0.75)),
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));
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.
// Item displays: a flat item and a block item (centred). objs.add(item(Affine.translation(0.5, 2.0, 0.5), "diamond_sword", null, null));
objs.add(obj(ObjectState.Kind.ITEM, Affine.translation(0.5, 2.0, 0.5), null, "diamond_sword", List.of(), 0)); objs.add(item(Affine.translation(2.5, 2.0, 0.5).mul(Affine.scale(0.6)), "oak_log", Material.OAK_LOG, "axis=y"));
objs.add(obj(ObjectState.Kind.ITEM, Affine.translation(2.5, 2.0, 0.5).mul(Affine.scale(0.6)), // Text facing the camera on an opaque background, and seen from the BACK (front faces +Z, camera is on -Z)
block(Material.OAK_LOG, "axis=y"), "oak_log", List.of(), 0)); // on the vanilla default 25% black background (blended); a shield item.
// Text display (facing the camera side), opaque background. objs.add(text(Affine.translation(1.5, 3.0, 0).mul(Affine.rotY(Math.PI)).mul(Affine.scale(0.025)), 0xFF203040,
objs.add(obj(ObjectState.Kind.TEXT, "Hallo PixelPics!", "Zeile zwei äöü"));
Affine.translation(1.5, 3.0, 0).mul(Affine.rotY(Math.PI)).mul(Affine.scale(0.025)), null, null, objs.add(text(Affine.translation(-0.5, 3.0, 0).mul(Affine.scale(0.025)), 0x40000000, "Rueckseite"));
List.of("Hallo PixelPics!", "Zeile zwei äöü"), 0xFF203040)); objs.add(item(Affine.translation(4, 1.0, 0.5), "shield", null, null));
// 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));
// Dropped item (flat) on the ground. // 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); int w = 480, ht = 360;
WorldSnapshot empty = new WorldSnapshot(Map.of(), 0, 1, air); BufferedImage img = h.renderer.execute(Harness.job(Harness.emptyWorld(0, 1), new Vector(1.5, 3.2, -6.5),
SkyContext sky = new SkyContext(6000, 0, 6000); new Vector(1.2, 1.3, 0.5), w, ht, List.of(), List.of(), List.of(), objs));
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);
File out = new File(System.getProperty("out", "/home/elias/Dokumente/PixelPics-Entity-Renders/objects.png")); File out = new File(System.getProperty("out", "/home/elias/Dokumente/PixelPics-Entity-Renders/objects.png"));
ImageIO.write(img, "png", out); ImageIO.write(img, "png", out);
System.out.println("WROTE " + 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 #!/usr/bin/env bash
# Builds the edge-case test plugin (PPTest.jar) against the compiled PixelPics classes. # 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] # Usage: ./gradlew compileJava && tools/edgecase-plugin/build.sh [compile-classpath] [out.jar]
# (classpath = paper-api + its deps, e.g. the Gradle compileClasspath joined with ":") # classpath default: ./gradlew -q printCompileClasspath; JAVAC must be a JDK >= 25 (default: javac).
# Then drop the jar into the TEST server's plugins/ (never production), start the server and run # Drop the jar into the TEST server's plugins/ (never production), start the server and run `pptest all`
# `pptest all` (or `pptest <scene>`) in its console; renders land in plugins/PPTest/<scene>.png. # (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.
set -euo pipefail set -euo pipefail
ROOT="$(cd "$(dirname "$0")/../.." && pwd)" 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}" OUT="${2:-$ROOT/build/PPTest.jar}"
JAVAC="${JAVAC:-javac}" JAVAC="${JAVAC:-javac}"
TMP="$(mktemp -d)" TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT trap 'rm -rf "$TMP"' EXIT
"$JAVAC" -nowarn -d "$TMP" -cp "$ROOT/build/classes/java/main:$API" "$ROOT/tools/edgecase-plugin/pptest/PPTest.java" "$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/" 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" .) (cd "$TMP" && zip -q -r "$OUT" .)
echo "Wrote $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 #!/usr/bin/env bash
# Updates the bundled CEM entity-model set (the only asset PixelPics vendors itself). # 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);
# Source: the CEM Template Loader data (Ewan Howell / wynem) — vanilla Java entity models as JSON, # mob and block-entity models come from tools/update-vanilla-models.sh.
# 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.
# #
# Usage: tools/update-cem-models.sh # update if newer # Usage: tools/update-cem-models.sh # update if newer
# tools/update-cem-models.sh --force # always overwrite # tools/update-cem-models.sh --force # always overwrite
@@ -20,21 +15,20 @@ trap 'rm -f "$TMP"' EXIT
echo "Fetching $URL ..." echo "Fetching $URL ..."
curl -fsSL "$URL" -o "$TMP" curl -fsSL "$URL" -o "$TMP"
# Validate + read versions/counts. # Validates the download, prints "<new version> <model count> <current version>".
read -r NEW_VER NEW_COUNT < <(python3 -c " read -r NEW_VER NEW_COUNT CUR_VER < <(python3 - "$TMP" "$DEST" <<'PY'
import json,sys import json, os, sys
d=json.load(open('$TMP')) d = json.load(open(sys.argv[1]))
assert 'models' in d and len(d['models'])>50, 'unexpected structure' assert 'models' in d and len(d['models']) > 50, 'unexpected structure'
print(d.get('version','?'), len(d['models'])) cur = '(none)'
") if os.path.exists(sys.argv[2]):
try: cur = json.load(open(sys.argv[2])).get('version', '?')
CUR_VER="(none)" except Exception: cur = '?'
if [ -f "$DEST" ]; then print(d.get('version', '?'), len(d['models']), cur)
CUR_VER="$(python3 -c "import json;print(json.load(open('$DEST')).get('version','?'))" 2>/dev/null || echo '?')" PY
fi )
echo "Current: $CUR_VER Remote: $NEW_VER ($NEW_COUNT models)" echo "Current: $CUR_VER Remote: $NEW_VER ($NEW_COUNT models)"
if [ "${1:-}" != "--force" ] && [ "$CUR_VER" = "$NEW_VER" ]; then if [ "${1:-}" != "--force" ] && [ "$CUR_VER" = "$NEW_VER" ]; then
echo "Already up to date. (use --force to overwrite)" echo "Already up to date. (use --force to overwrite)"
exit 0 exit 0
+31 -20
View File
@@ -1,14 +1,13 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Regenerates the vanilla entity-model dump straight from a Minecraft CLIENT jar (26.x+ ships # Regenerates the vanilla entity-model dump from a Minecraft CLIENT jar (26.x+ ships unobfuscated, no mappings
# unobfuscated, so no mappings are needed). This is the primary entity-model source of PixelPics: # needed). This is PixelPics' primary entity-model source; run it once per Minecraft update.
# 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.
# #
# 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) # 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 # tools/update-vanilla-models.sh 26.4 <plugin-data-dir> # -> <dir>/vanilla_entity_models.json
# # (overrides the bundled copy, no rebuild) # # (overrides the bundled copy, no rebuild)
set -euo pipefail set -euo pipefail
VER="${1:?usage: $0 <minecraft-version> [output-dir]}" 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}" MC="${MINECRAFT_DIR:-$HOME/.minecraft}"
JSON="$MC/versions/$VER/$VER.json" JSON="$MC/versions/$VER/$VER.json"
JAR="$MC/versions/$VER/$VER.jar" 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; } [[ -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. # Classpath: client jar + all libraries listed in the version json.
CP="$(python3 - "$JSON" "$MC" "$JAR" <<'EOF' CP="$(python3 - "$JSON" "$MC" "$JAR" <<'EOF'
import json, os, sys import json, os, sys
@@ -34,19 +43,21 @@ print(":".join(cp))
EOF EOF
)" )"
TMP="$(mktemp --suffix=.json)" # Scratch dir as working directory: the game's logger writes a logs/ folder there.
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.
WORK="$(mktemp -d)" WORK="$(mktemp -d)"
(cd "$WORK" && "$JAVA" -cp "$CP" "$ROOT/tools/VanillaModelDump.java" "$TMP" 2>&1) | grep -E "STDOUT|Exception" | sed 's/.*STDOUT\]: //' trap 'rm -rf "$WORK"' EXIT
python3 -c " OUT="$WORK/vanilla_entity_models.json"
import json; d = json.load(open('$TMP')) 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' 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" mkdir -p "$OUT_DIR"
mv "$TMP" "$OUT_DIR/vanilla_entity_models.json" install -m 644 "$OUT" "$OUT_DIR/vanilla_entity_models.json"
chmod 644 "$OUT_DIR/vanilla_entity_models.json"
rm -rf "$WORK"
trap - EXIT
echo "Wrote $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)." echo "Note: the runtime resource pack must come from the same version (incl. assets/minecraft/font for text)."