resource-pack render engine
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Executable
+47
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env bash
|
||||
# Updates the bundled CEM entity-model set (the only asset PixelPics vendors itself).
|
||||
#
|
||||
# Source: the CEM Template Loader data (Ewan Howell / wynem) — vanilla Java entity models as JSON,
|
||||
# already correctly posed. These ultimately mirror Mojang's hardcoded Java EntityModel classes.
|
||||
#
|
||||
# Most future-proof alternative for a NEW Minecraft version (sourced straight from the game):
|
||||
# 1. Install the JsonEM mod (Fabric), set dump_models=true in .minecraft/config/jsonem.properties
|
||||
# 2. Launch the game once -> models dumped to .minecraft/jsonem_dump
|
||||
# (a small format-adapter would be needed; the CEM source below covers vanilla until then.)
|
||||
#
|
||||
# Usage: tools/update-cem-models.sh # update if newer
|
||||
# tools/update-cem-models.sh --force # always overwrite
|
||||
set -euo pipefail
|
||||
|
||||
URL="https://wynem.com/assets/json/cem_template_models.json"
|
||||
DEST="$(cd "$(dirname "$0")/.." && pwd)/src/main/resources/cem/cem_template_models.json"
|
||||
TMP="$(mktemp)"
|
||||
trap 'rm -f "$TMP"' EXIT
|
||||
|
||||
echo "Fetching $URL ..."
|
||||
curl -fsSL "$URL" -o "$TMP"
|
||||
|
||||
# Validate + read versions/counts.
|
||||
read -r NEW_VER NEW_COUNT < <(python3 -c "
|
||||
import json,sys
|
||||
d=json.load(open('$TMP'))
|
||||
assert 'models' in d and len(d['models'])>50, 'unexpected structure'
|
||||
print(d.get('version','?'), len(d['models']))
|
||||
")
|
||||
|
||||
CUR_VER="(none)"
|
||||
if [ -f "$DEST" ]; then
|
||||
CUR_VER="$(python3 -c "import json;print(json.load(open('$DEST')).get('version','?'))" 2>/dev/null || echo '?')"
|
||||
fi
|
||||
|
||||
echo "Current: $CUR_VER Remote: $NEW_VER ($NEW_COUNT models)"
|
||||
|
||||
if [ "${1:-}" != "--force" ] && [ "$CUR_VER" = "$NEW_VER" ]; then
|
||||
echo "Already up to date. (use --force to overwrite)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$DEST")"
|
||||
cp "$TMP" "$DEST"
|
||||
echo "Updated -> $DEST (version $NEW_VER, $NEW_COUNT models)"
|
||||
echo "Rebuild/redeploy the plugin to apply."
|
||||
Reference in New Issue
Block a user