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 eu.mhsl.minecraft.pixelpics.render.util.MathUtil; 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 H_FOV_HALF = Math.toRadians(35); static final Vector BASE = new Vector(1, 0, 0); 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_coral","parched","camel_husk","oak_boat" }; // 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") ); 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("/tmp/cem_models.json"), log); log.info("Loaded " + n + " geometries"); eu.mhsl.minecraft.pixelpics.render.entity.cem.CemBaker baker = new eu.mhsl.minecraft.pixelpics.render.entity.cem.CemBaker(geo, textures, new SkinCache()); DefaultScreenRenderer renderer = new DefaultScreenRenderer(registry, tint, textures, baker, 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 ? args : ENTITIES; 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("/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] + ".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) { boolean isPlayer = key.equals("player"); EntityState s = new EntityState(key, 0, 0, 0, yaw, yaw, 0, 0, 0, 0, false, 0.8, 1.0, isPlayer, null, false, VAR.get(key), 0, 1.0); RenderedEntity re = baker.bake(s); 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(H_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)); return renderer.execute(job); } 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; } // Replicated from DefaultScreenRenderer.buildRayMap. static List buildRayMap(Location eye, int width, int height) { Vector dir = eye.getDirection(); double angleYaw = Math.atan2(dir.getZ(), dir.getX()); double anglePitch = Math.atan2(dir.getY(), Math.sqrt(dir.getX() * dir.getX() + dir.getZ() * dir.getZ())); double yawHalf = H_FOV_HALF; double pitchHalf = Math.atan(Math.tan(yawHalf) * ((double) height / width)); Vector ll = MathUtil.doubleYawPitchRotation(BASE, -yawHalf, -pitchHalf, angleYaw, anglePitch); Vector ul = MathUtil.doubleYawPitchRotation(BASE, -yawHalf, pitchHalf, angleYaw, anglePitch); Vector lr = MathUtil.doubleYawPitchRotation(BASE, yawHalf, -pitchHalf, angleYaw, anglePitch); Vector ur = MathUtil.doubleYawPitchRotation(BASE, yawHalf, pitchHalf, angleYaw, anglePitch); List rayMap = new ArrayList<>(width * height); Vector leftFrac = ul.clone().subtract(ll).multiply(1.0 / (height - 1)); Vector rightFrac = ur.clone().subtract(lr).multiply(1.0 / (height - 1)); for (int pitch = 0; pitch < height; pitch++) { Vector leftPitch = ul.clone().subtract(leftFrac.clone().multiply(pitch)); Vector rightPitch = ur.clone().subtract(rightFrac.clone().multiply(pitch)); Vector yawFrac = rightPitch.clone().subtract(leftPitch).multiply(1.0 / (width - 1)); for (int yaw = 0; yaw < width; yaw++) { rayMap.add(leftPitch.clone().add(yawFrac.clone().multiply(yaw)).normalize()); } } return rayMap; } }