234 lines
15 KiB
Java
234 lines
15 KiB
Java
import eu.mhsl.minecraft.pixelpics.render.entity.*;
|
|
import eu.mhsl.minecraft.pixelpics.render.snapshot.WorldSnapshot;
|
|
import org.bukkit.util.Vector;
|
|
|
|
import javax.imageio.ImageIO;
|
|
import java.awt.image.BufferedImage;
|
|
import java.io.File;
|
|
import java.util.*;
|
|
import java.util.List;
|
|
|
|
/**
|
|
* Standalone (no server) renderer: every entity twice (yaw 45° / 225°) against empty sky -> contact sheets
|
|
* ({@code page_<n>.png}) + {@code einzeln/<key>.png} in {@code -Dout} (default ~/Dokumente/PixelPics-Entity-Renders).
|
|
* Args: case specs (default {@link #ENTITIES}), or {@code @variants} for {@link #VARIANT_CASES}. Build: see {@link Harness}.
|
|
*/
|
|
public class EntityTestRender {
|
|
|
|
static final int TW = 200, TH = 230; // per-view tile size
|
|
|
|
// Default case list: every entity type in its base look ("plain": the per-type tables below apply).
|
|
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 refactors). Spec: [baby:]type[@variant][+flag…], flags:
|
|
// chest, arms, saddle, tame (red collar), armor (full diamond), elytra, red (sheep wool), sword / crossbow
|
|
// (right hand), crossbowl (left hand), mouth (sweet berries), passenger, shoulder (red + blue parrot), stand
|
|
// (custom armor-stand part poses), pitch:<deg>, p:<state pose> (sleeping, sitting, eyes_glowing, …).
|
|
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");
|
|
VARIANT_CASES.addAll(List.of("armor_stand", "armor_stand+arms", "baby:armor_stand", "baby:armor_stand+arms",
|
|
"armor_stand+arms+armor", "baby:armor_stand+arms+armor"));
|
|
VARIANT_CASES.addAll(List.of("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"));
|
|
// 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");
|
|
// Layer / texture states: emissive eyes, sheep undercoat, angry wolves, tipped arrows, invulnerable
|
|
// wither, partial-alpha cutout textures, held items (piglin arm poses), elytra.
|
|
VARIANT_CASES.addAll(List.of("spider", "cave_spider", "enderman", "phantom", "breeze", "warden",
|
|
"copper_golem@oxidized", "creaking", "creaking+p:eyes_glowing", "sheep@sheared", "sheep+red",
|
|
"sheep@sheared+red", "baby:sheep+red", "sheep+p:jeb", "wolf@pale+p:angry", "baby:wolf@pale+p:angry",
|
|
"wolf@pale+tame+p:angry", "arrow+p:tipped", "spectral_arrow", "wither+p:invulnerable",
|
|
"llama@gray", "llama@white", "tropical_fish@brinely", "piglin+sword", "piglin+crossbow",
|
|
"piglin+sword+crossbowl", "piglin+crossbow+p:arms_crossbow_hold", "piglin+sword+p:arms_attacking_with_melee_weapon",
|
|
"zombified_piglin+sword", "piglin_brute+sword", "pillager+crossbow",
|
|
"vindicator+sword", "armor_stand+elytra", "zombie+elytra", "baby:armor_stand+armor",
|
|
"baby:armor_stand+arms+elytra"));
|
|
VARIANT_CASES.addAll(List.of(ENTITIES));
|
|
}
|
|
|
|
// Variants the game always supplies (several types have no texture without one); villager profession/level.
|
|
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")
|
|
);
|
|
|
|
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)
|
|
);
|
|
|
|
// Plain cases: horse markings, saddles, chests, body armor / carpet / harness.
|
|
static final Map<String, String> MARK = Map.of("horse", "blackdots");
|
|
static final Set<String> SADDLE = Set.of("horse", "donkey", "mule", "nautilus", "pig", "strider", "camel");
|
|
static final Set<String> CHEST = Set.of("llama", "donkey");
|
|
static final Map<String, String> EQUIP = Map.ofEntries(
|
|
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")
|
|
);
|
|
|
|
// Plain cases: humanoid armor (diamond, dyed leather, trimmed iron, enchanted netherite, glint helmet + 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 {
|
|
ARMOR.put("skeleton", new EntityState.Equipment(P("diamond"), P("diamond"), P("diamond"), P("diamond")));
|
|
EntityState.EquipPiece le = P("leather", 0xFFFF8000, null, null, false);
|
|
ARMOR.put("zombie", new EntityState.Equipment(le, le, le, le));
|
|
EntityState.EquipPiece ir = P("iron", 0, "diamond", "coast", false);
|
|
ARMOR.put("armor_stand", new EntityState.Equipment(ir, ir, ir, ir));
|
|
EntityState.EquipPiece ne = P("netherite", 0, null, null, true);
|
|
ARMOR.put("wither_skeleton", new EntityState.Equipment(ne, ne, ne, ne));
|
|
ARMOR.put("player", new EntityState.Equipment(P("diamond", 0, null, null, true), P("elytra"), null, null));
|
|
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 Set<String> INVISIBLE = Set.of("armor_stand", "creeper");
|
|
|
|
public static void main(String[] args) throws Exception {
|
|
Harness h = new Harness("test");
|
|
WorldSnapshot empty = Harness.emptyWorld(0, 1);
|
|
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) {
|
|
cells.add(Harness.labelCell(key, renderEntity(h, empty, key, 45), renderEntity(h, empty, key, 225)));
|
|
h.log.info("rendered " + key);
|
|
}
|
|
|
|
File outDir = new File(System.getProperty("out", "/home/elias/Dokumente/PixelPics-Entity-Renders"));
|
|
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"));
|
|
}
|
|
Harness.writeSheets(cells, 2, 10, outDir, page -> "page_" + page + ".png");
|
|
}
|
|
|
|
static BufferedImage renderEntity(Harness h, WorldSnapshot world, String key, float yaw) {
|
|
EntityState s = state(key, yaw);
|
|
RenderedEntity re = h.baker.bake(s);
|
|
if (re == null || re.cubes.isEmpty()) { // invisible with no equipment -> nothing renders
|
|
return new BufferedImage(TW, TH, BufferedImage.TYPE_INT_RGB);
|
|
}
|
|
Vector center = Harness.center(re.aabbMin, re.aabbMax);
|
|
double dist = Harness.frameDistance(re.aabbMin, re.aabbMax, 1.25, 0.4);
|
|
Vector cam = new Vector(center.getX() - dist, center.getY() + dist * 0.42, center.getZ() - dist * 0.15);
|
|
return h.renderer.execute(Harness.job(world, cam, center, TW, TH, List.of(s), List.of(), List.of(), List.of()));
|
|
}
|
|
|
|
/** 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;
|
|
if (variant == null) variant = VAR.get(key);
|
|
boolean isPlayer = key.equals("player");
|
|
int tint = key.equals("tropical_fish") ? 0xFFF9801D : (key.equals("sheep") ? 0xFFF9FFFE : 0);
|
|
if (key.equals("sheep") && flags.contains("red")) tint = 0xFFB02E26;
|
|
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);
|
|
}
|
|
if (flags.contains("elytra")) armor = new EntityState.Equipment(null, P("elytra"), null, null);
|
|
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;
|
|
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));
|
|
}
|
|
// Flags the snapshot derives from the entity: sheep undercoat (non-white wool), pillager crossbow hold.
|
|
if (key.equals("sheep") && flags.contains("red")) poses.add("undercoat");
|
|
if (key.equals("pillager") && flags.contains("crossbow")) poses.add("arms_crossbow_hold");
|
|
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") ? Harness.item("sweet_berries") : null;
|
|
ItemSpec right = flags.contains("sword") ? Harness.item("iron_sword")
|
|
: flags.contains("crossbow") ? Harness.item("crossbow") : null;
|
|
ItemSpec left = flags.contains("crossbowl") ? Harness.item("crossbow") : 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"), right, left, null,
|
|
collar, false, mouth, poses.isEmpty() ? null : String.join("+", poses), partPoses,
|
|
shoulder ? "red_blue" : null, shoulder ? "blue" : null);
|
|
}
|
|
}
|