resource-pack render engine

This commit is contained in:
2026-06-07 18:57:40 +02:00
parent 18c5fc4ffc
commit 211c7e8479
69 changed files with 4060 additions and 1398 deletions
+187
View File
@@ -0,0 +1,187 @@
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<String, String> 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<BufferedImage> 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<Vector> 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<Vector> 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<Vector> 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;
}
}