321 lines
19 KiB
Java
321 lines
19 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 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<String> 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");
|
|
VARIANT_CASES.addAll(List.of(ENTITIES));
|
|
}
|
|
|
|
// 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"),
|
|
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<String, String> PROF = Map.ofEntries(
|
|
Map.entry("villager", "librarian"), Map.entry("zombie_villager", "farmer")
|
|
);
|
|
static final Map<String, Integer> LVL = Map.ofEntries(
|
|
Map.entry("villager", 5)
|
|
);
|
|
|
|
// Horse/llama/donkey equipment for the standalone render.
|
|
static final Map<String, String> MARK = Map.of("horse", "blackdots"); // coat markings
|
|
static final java.util.Set<String> SADDLE = java.util.Set.of("horse", "donkey", "mule", "nautilus", "pig", "strider", "camel");
|
|
static final java.util.Set<String> CHEST = java.util.Set.of("llama", "donkey");
|
|
static final Map<String, String> 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<String, EntityState.Equipment> 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<String> 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<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(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<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);
|
|
}
|
|
|
|
/** 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<String> 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);
|
|
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);
|
|
}
|
|
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:<state pose>" (sleeping, sitting, lying, …), "pitch:<deg>", "passenger", "shoulder"
|
|
// (red + blue parrot on the shoulders), "stand" (custom armor-stand part poses), "mouth" (sweet berries).
|
|
List<String> 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));
|
|
}
|
|
Collections.sort(poses);
|
|
Map<String, float[]> 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;
|
|
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"), null, null, 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<Vector> buildRayMap(Location eye, int width, int height) {
|
|
return eu.mhsl.minecraft.pixelpics.render.render.DefaultScreenRenderer.buildRayMap(eye, width, height);
|
|
}
|
|
}
|