188 lines
10 KiB
Java
188 lines
10 KiB
Java
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;
|
|
}
|
|
}
|