import eu.mhsl.minecraft.pixelpics.assets.*; import eu.mhsl.minecraft.pixelpics.render.entity.*; import eu.mhsl.minecraft.pixelpics.render.entity.BlockEntityState.*; import eu.mhsl.minecraft.pixelpics.render.entity.cem.*; import eu.mhsl.minecraft.pixelpics.render.render.*; 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) block-entity renderer: each scene framed and labelled into a contact sheet. */ public class BlockEntityTestRender { 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 = 220, TH = 240; static BlockEntityBaker beBaker; static DecorationBaker decoBaker; static BlockEntityState be(Kind kind, int bx, int by, int bz, float yaw) { return new BlockEntityState(kind, bx, by, bz, yaw, ChestKind.SINGLE, 0, null, "oak", null, null, null, List.of(), List.of(), null); } 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); CemModelLoader geo = new CemModelLoader(); geo.load(new java.io.FileInputStream("/tmp/cem_models.json"), log); SkinCache skins = new SkinCache(); CemBaker baker = new CemBaker(geo, textures, skins); beBaker = new BlockEntityBaker(geo, textures, skins); decoBaker = new DecorationBaker(textures); DefaultScreenRenderer renderer = new DefaultScreenRenderer(registry, tint, textures, baker, beBaker, log); BlockData air = (BlockData) Proxy.newProxyInstance(BlockEntityTestRender.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(), -64, 320, air); SkyContext sky = new SkyContext(6000, 0, 6000); // Scenes: label -> list of block-entity states. LinkedHashMap> scenes = new LinkedHashMap<>(); scenes.put("chest_S", List.of(be(Kind.CHEST, 0, 0, 0, 0))); scenes.put("chest_N", List.of(be(Kind.CHEST, 0, 0, 0, 180))); scenes.put("chest_E", List.of(be(Kind.CHEST, 0, 0, 0, 270))); scenes.put("chest_W", List.of(be(Kind.CHEST, 0, 0, 0, 90))); scenes.put("double_chest", List.of( new BlockEntityState(Kind.CHEST, 0, 0, 0, 0, ChestKind.LEFT, 0, null, null, null, null, null, List.of(), List.of(), null), new BlockEntityState(Kind.CHEST, 1, 0, 0, 0, ChestKind.RIGHT, 0, null, null, null, null, null, List.of(), List.of(), null))); scenes.put("sign", List.of(be(Kind.SIGN, 0, 0, 0, 0))); scenes.put("wall_sign", List.of(be(Kind.WALL_SIGN, 0, 0, 0, 0))); scenes.put("hanging_sign", List.of(be(Kind.HANGING_SIGN, 0, 0, 0, 0))); scenes.put("banner", List.of(new BlockEntityState(Kind.BANNER, 0, 0, 0, 0, null, 0xFFCC2020, null, null, null, null, null, List.of(), List.of(), null))); scenes.put("banner_patterned", List.of(new BlockEntityState(Kind.BANNER, 0, 0, 0, 0, null, 0xFFFFFFFF, null, null, null, null, null, List.of(new BannerPattern("stripe_bottom", 0xFFCC2020), new BannerPattern("cross", 0xFF2040CC), new BannerPattern("border", 0xFF20A020)), List.of(), null))); scenes.put("wall_banner", List.of(new BlockEntityState(Kind.WALL_BANNER, 0, 0, 0, 0, null, 0xFF2040CC, null, null, null, null, null, List.of(), List.of(), null))); scenes.put("bed", List.of( new BlockEntityState(Kind.BED, 0, 0, 0, 0, null, 0, "red", null, BedPart.FOOT, null, null, List.of(), List.of(), null), new BlockEntityState(Kind.BED, 0, 0, 1, 0, null, 0, "red", null, BedPart.HEAD, null, null, List.of(), List.of(), null))); scenes.put("shulker_box", List.of(new BlockEntityState(Kind.SHULKER_BOX, 0, 0, 0, 0, null, 0, null, null, null, null, null, List.of(), List.of(), null))); scenes.put("conduit", List.of(be(Kind.CONDUIT, 0, 0, 0, 0))); scenes.put("decorated_pot", List.of(be(Kind.DECORATED_POT, 0, 0, 0, 0))); scenes.put("decorated_pot_sherds", List.of(new BlockEntityState(Kind.DECORATED_POT, 0, 0, 0, 0, null, 0, null, null, null, null, null, List.of(), List.of("angler_pottery_sherd", "heart_pottery_sherd", "skull_pottery_sherd", "brick"), null))); scenes.put("bell", List.of(be(Kind.BELL, 0, 0, 0, 0))); scenes.put("head_skeleton", List.of(head("skeleton"))); scenes.put("head_zombie", List.of(head("zombie"))); scenes.put("head_creeper", List.of(head("creeper"))); scenes.put("head_dragon", List.of(head("dragon"))); scenes.put("head_piglin", List.of(head("piglin"))); scenes.put("wither_skull", List.of(headKind(Kind.HEAD, "wither_skeleton"))); scenes.put("wall_head", List.of(headKind(Kind.WALL_HEAD, "skeleton"))); // Decoration scenes (paintings + item frames), rendered via the same scene path. LinkedHashMap> 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, 0, 0, 0, 1, 1, 0.0625, DecorationState.Facing.SOUTH, "kebab", null, 0, false))); decoScenes.put("painting_kebab_N", List.of(new DecorationState(DecorationState.Kind.PAINTING, 0, 0, 0.9375, 1, 1, 1, DecorationState.Facing.NORTH, "kebab", null, 0, false))); decoScenes.put("painting_pool_S", List.of(new DecorationState(DecorationState.Kind.PAINTING, 0, 0, 0, 2, 1, 0.0625, DecorationState.Facing.SOUTH, "pool", null, 0, false))); decoScenes.put("item_frame", List.of(new DecorationState(DecorationState.Kind.ITEM_FRAME, 0.0625, 0.0625, 0, 0.9375, 0.9375, 0.0625, DecorationState.Facing.SOUTH, null, "diamond", 0, false))); 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, 0, false))); 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, "diamond", 0, false))); List cells = new ArrayList<>(); List names = new ArrayList<>(scenes.keySet()); for (String name : names) { cells.add(labelCell(name, renderScene(renderer, empty, sky, scenes.get(name)))); log.info("rendered " + name); } for (String name : decoScenes.keySet()) { cells.add(labelCell(name, renderDeco(renderer, empty, sky, decoScenes.get(name)))); names.add(name); log.info("rendered " + name); } File outDir = new File("/home/elias/Dokumente/PixelPics-BlockEntity-Renders"); outDir.mkdirs(); for (int i = 0; i < names.size(); i++) ImageIO.write(cells.get(i), "png", new File(outDir, names.get(i) + ".png")); 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); } static BlockEntityState head(String type) { return headKind(Kind.HEAD, type); } static BlockEntityState headKind(Kind kind, String type) { return new BlockEntityState(kind, 0, 0, 0, 0, null, 0, null, null, null, type, null, List.of(), List.of(), null); } static BufferedImage renderScene(DefaultScreenRenderer renderer, WorldSnapshot world, SkyContext sky, List states) { double[] min = {Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE}; double[] max = {-Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE}; for (BlockEntityState s : states) { RenderedEntity re = beBaker.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(H_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); 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(), states); return renderer.execute(job); } static BufferedImage renderDeco(DefaultScreenRenderer renderer, WorldSnapshot world, SkyContext sky, List 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(H_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 rayMap = buildRayMap(loc, TW * SSAA, TH * SSAA); RenderJob job = new RenderJob(world, rayMap, cam, TW, TH, sky, List.of(), List.of(), states); 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 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; } }