import eu.mhsl.minecraft.pixelpics.assets.*; 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.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.lang.reflect.Proxy; import java.util.*; import java.util.List; import java.util.logging.Logger; /** Standalone (no server) renderer: every entity twice (yaw 45° / 225°) against empty sky -> contact sheets. */ 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 // Entity type keys to render (current vanilla + bundled bedrock specials). variant=null (base look). static final String[] ENTITIES = { "allay","armadillo","armor_stand","axolotl","bat","bee","blaze","bogged","breeze","camel", "cat","cave_spider","chicken","cod","copper_golem","cow","creaking","creeper","dolphin","donkey", "drowned","elder_guardian","enderman","endermite","evoker","fox","frog","ghast","giant","glow_squid", "goat","guardian","happy_ghast","hoglin","horse","husk","illusioner","iron_golem","llama","magma_cube", "mooshroom","mule","ocelot","panda","parrot","phantom","pig","piglin","piglin_brute","pillager", "polar_bear","pufferfish","rabbit","ravager","salmon","sheep","shulker","silverfish","skeleton","skeleton_horse", "slime","sniffer","snow_golem","spider","squid","stray","strider","tadpole","trader_llama","tropical_fish", "turtle","vex","villager","vindicator","wandering_trader","warden","witch","wither","wither_skeleton","wolf", "zoglin","zombie","zombie_horse","zombie_villager","zombified_piglin","ender_dragon","player","mannequin", "nautilus","zombie_nautilus","parched","camel_husk","oak_boat","sulfur_cube","oak_chest_boat","bamboo_raft", "baby:zombie","baby:cow","baby:villager","baby:wolf","baby:sheep","baby:chicken","baby:horse","baby:pig", "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: // [baby:]type[@variant][+flag…], flags: chest, arms, tame (red collar), saddle, armor (full diamond), noarmor. static final List VARIANT_CASES = new ArrayList<>(); static { for (String v : new String[] {"tabby", "black", "red", "siamese", "british_shorthair", "calico", "persian", "ragdoll", "white", "jellie", "all_black"}) { VARIANT_CASES.add("cat@" + v); VARIANT_CASES.add("baby:cat@" + v + "+tame"); } for (String v : new String[] {"pale", "spotted", "snowy", "black", "ashen", "rusty", "woods", "chestnut", "striped"}) { VARIANT_CASES.add("wolf@" + v); VARIANT_CASES.add("wolf@" + v + "+tame"); VARIANT_CASES.add("baby:wolf@" + v); VARIANT_CASES.add("baby:wolf@" + v + "+tame"); } for (String t : new String[] {"cow", "pig", "chicken"}) for (String v : new String[] {"temperate", "warm", "cold"}) { VARIANT_CASES.add(t + "@" + v); VARIANT_CASES.add("baby:" + t + "@" + v); } for (String v : new String[] {"temperate", "warm", "cold"}) VARIANT_CASES.add("frog@" + v); for (String v : new String[] {"temperate", "warm"}) VARIANT_CASES.add("zombie_nautilus@" + v); VARIANT_CASES.add("mooshroom@red"); VARIANT_CASES.add("mooshroom@brown"); VARIANT_CASES.add("baby:mooshroom@brown"); // Armor stand with/without arms, small, with armor. 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"}) VARIANT_CASES.add(a); // 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", "mule", "mule+chest", "mule+chest+saddle", "baby:donkey+chest"}) VARIANT_CASES.add(c); // Armor wearers (and non-wearers given armor: nothing must appear). for (String t : new String[] {"zombie", "husk", "drowned", "zombie_villager", "zombified_piglin", "skeleton", "stray", "wither_skeleton", "bogged", "parched", "piglin", "piglin_brute", "giant", "player", "mannequin", "armor_stand", "baby:zombie", "baby:piglin", "baby:zombified_piglin", "baby:husk", "baby:drowned", "baby:zombie_villager", "villager", "pillager", "vindicator", "witch", "cow", "pig", "creeper", "enderman", "evoker", "illusioner", "wandering_trader", "copper_golem", "fox", "panda"}) VARIANT_CASES.add(t + "+armor"); // Layer / texture states: emissive eyes, sheep undercoat, angry wolves, tipped arrows, invulnerable // wither, partial-alpha cutout textures, held items (piglin arm poses), elytra. for (String c : new String[] {"spider", "cave_spider", "enderman", "phantom", "breeze", "warden", "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", "wolf@pale+tame+p:angry", "arrow+p:tipped", "spectral_arrow", "wither+p:invulnerable", "llama@gray", "llama@white", "tropical_fish@brinely", "piglin+sword", "piglin+crossbow", "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", "vindicator+sword", "armor_stand+elytra", "zombie+elytra", "baby:armor_stand+armor", "baby:armor_stand+arms+elytra"}) VARIANT_CASES.add(c); VARIANT_CASES.addAll(List.of(ENTITIES)); } // Representative variants (the game always supplies these; null base textures wouldn't exist otherwise). static final Map VAR = Map.ofEntries( 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("llama", "creamy"), Map.entry("trader_llama", "creamy"), Map.entry("fox", "red"), Map.entry("mooshroom", "red"), Map.entry("frog", "temperate"), Map.entry("panda", "normal"), Map.entry("cow", "temperate"), Map.entry("pig", "temperate"), Map.entry("chicken", "temperate"), Map.entry("villager", "taiga"), Map.entry("zombie_villager", "swamp"), 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 PROF = Map.ofEntries( Map.entry("villager", "librarian"), Map.entry("zombie_villager", "farmer") ); static final Map LVL = Map.ofEntries( Map.entry("villager", 5) ); // Horse/llama/donkey equipment for the standalone render. static final Map MARK = Map.of("horse", "blackdots"); // coat markings static final java.util.Set SADDLE = java.util.Set.of("horse", "donkey", "mule", "nautilus", "pig", "strider", "camel"); static final java.util.Set CHEST = java.util.Set.of("llama", "donkey"); static final Map EQUIP = Map.ofEntries( // armor / carpet 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("nautilus", "diamond") ); // Humanoid armor test cases (worn equipment, trims, glint, elytra). 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) { return new EntityState.EquipPiece(asset, dye, trimMat, trimPat, glint); } static final Map ARMOR = new HashMap<>(); static { // Full diamond armor. 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); 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); ARMOR.put("armor_stand", new EntityState.Equipment(ir, ir, ir, ir)); // Enchanted netherite armor (glint). EntityState.EquipPiece ne = P("netherite", 0, null, null, true); 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)); // Gold helmet. 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. static final java.util.Set INVISIBLE = java.util.Set.of("armor_stand", "creeper"); public static void main(String[] args) throws Exception { Logger log = Logger.getLogger("test"); ResourcePack pack = ResourcePackLoader.load(new File(ROOT, "resourcepack"), log).orElseThrow(); AssetReader reader = new AssetReader(pack); TextureCache textures = new TextureCache(pack); BlockModelRegistry registry = new BlockModelRegistry(reader, textures); BiomeTintProvider tint = new BiomeTintProvider(textures); 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 : args[0].equals("@variants") ? VARIANT_CASES.toArray(new String[0]) : args; List cells = new ArrayList<>(); for (String key : list) { BufferedImage v1 = renderEntity(renderer, baker, empty, sky, key, 45); BufferedImage v2 = renderEntity(renderer, baker, empty, sky, key, 225); cells.add(labelCell(key, v1, v2)); log.info("rendered " + key); } 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"); single.mkdirs(); for (int i = 0; i < list.length; i++) { ImageIO.write(cells.get(i), "png", new File(single, list[i].replaceAll("[:@+]", "_") + ".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, SkyContext sky, String key, float yaw) { EntityState s = state(key, yaw); RenderedEntity re = baker.bake(s); if (re == null || re.cubes.isEmpty()) { // invisible with no equipment -> nothing renders return new BufferedImage(TW, TH, BufferedImage.TYPE_INT_RGB); } double cx = (re.aabbMin[0] + re.aabbMax[0]) / 2; double cy = (re.aabbMin[1] + re.aabbMax[1]) / 2; double cz = (re.aabbMin[2] + re.aabbMax[2]) / 2; double ext = 0; 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 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). */ static EntityState state(String spec, float yaw) { boolean baby = spec.startsWith("baby:"); if (baby) spec = spec.substring(5); String[] flagSplit = spec.split("\\+"); Set flags = new HashSet<>(Arrays.asList(flagSplit).subList(1, flagSplit.length)); String key = flagSplit[0]; String variant = null; if (key.contains("@")) { variant = key.substring(key.indexOf('@') + 1); key = key.substring(0, key.indexOf('@')); } boolean plain = flagSplit.length == 1 && variant == null; // default ENTITIES entry: legacy test setup if (variant == null) variant = VAR.get(key); boolean isPlayer = key.equals("player"); int tint = key.equals("tropical_fish") ? 0xFFF9801D : (key.equals("sheep") ? 0xFFF9FFFE : 0); if (key.equals("sheep") && flags.contains("red")) tint = 0xFFB02E26; int tint2 = key.equals("tropical_fish") ? 0xFF3AB3DA : 0; EntityState.Equipment armor = plain ? (baby ? null : ARMOR.get(key)) : null; if (flags.contains("armor")) { EntityState.EquipPiece d = P("diamond"); armor = new EntityState.Equipment(d, d, d, d); } if (flags.contains("elytra")) armor = new EntityState.Equipment(null, P("elytra"), null, null); boolean chest = plain ? CHEST.contains(key) : flags.contains("chest"); boolean saddle = plain ? SADDLE.contains(key) : flags.contains("saddle"); String equip = plain ? EQUIP.get(key) : null; boolean arms = flags.contains("arms"); int collar = flags.contains("tame") ? 0xFFB02E26 : 0; // Pose flags: "p:" (sleeping, sitting, lying, …), "pitch:", "passenger", "shoulder" // (red + blue parrot on the shoulders), "stand" (custom armor-stand part poses), "mouth" (sweet berries). List poses = new ArrayList<>(); float pitch = 0; for (String f : flags) { if (f.startsWith("p:")) poses.add(f.substring(2)); if (f.startsWith("pitch:")) pitch = Float.parseFloat(f.substring(6)); } // Flags the snapshot derives from the entity: sheep undercoat (non-white wool), pillager crossbow hold. if (key.equals("sheep") && flags.contains("red")) poses.add("undercoat"); if (key.equals("pillager") && flags.contains("crossbow")) poses.add("arms_crossbow_hold"); Collections.sort(poses); Map partPoses = null; if (flags.contains("stand")) { partPoses = new HashMap<>(); partPoses.put("headPose", new float[] {-20, 30, 0}); partPoses.put("bodyPose", new float[] {10, 20, 0}); partPoses.put("rightArmPose", new float[] {-90, 0, 0}); partPoses.put("leftArmPose", new float[] {-30, 0, -40}); partPoses.put("leftLegPose", new float[] {30, 0, -10}); partPoses.put("rightLegPose", new float[] {-40, 0, 0}); } boolean shoulder = flags.contains("shoulder"); ItemSpec mouth = flags.contains("mouth") ? new ItemSpec("sweet_berries", null, null, false) : null; // Held items: "sword" (right hand), "crossbow" (right hand), "crossbowl" (left hand). ItemSpec right = flags.contains("sword") ? new ItemSpec("iron_sword", null, null, false) : flags.contains("crossbow") ? new ItemSpec("crossbow", null, null, false) : 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, 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 && !baby && INVISIBLE.contains(key), pitch, 0, tint2, flags.contains("passenger"), right, left, null, collar, false, mouth, poses.isEmpty() ? null : String.join("+", poses), partPoses, 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 buildRayMap(Location eye, int width, int height) { return eu.mhsl.minecraft.pixelpics.render.render.DefaultScreenRenderer.buildRayMap(eye, width, height); } }