171 lines
8.1 KiB
Java
171 lines
8.1 KiB
Java
import eu.mhsl.minecraft.pixelpics.assets.*;
|
|
import eu.mhsl.minecraft.pixelpics.assets.font.FontLoader;
|
|
import eu.mhsl.minecraft.pixelpics.render.entity.*;
|
|
import eu.mhsl.minecraft.pixelpics.render.entity.cem.BlockEntityBaker;
|
|
import eu.mhsl.minecraft.pixelpics.render.entity.cem.CemBaker;
|
|
import eu.mhsl.minecraft.pixelpics.render.entity.cem.CemModelLoader;
|
|
import eu.mhsl.minecraft.pixelpics.render.render.DefaultScreenRenderer;
|
|
import eu.mhsl.minecraft.pixelpics.render.render.RenderJob;
|
|
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.io.FileInputStream;
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.lang.reflect.Proxy;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.function.IntFunction;
|
|
import java.util.logging.Logger;
|
|
|
|
/**
|
|
* Shared setup of the standalone render tools (no server: EntityTestRender, BlockEntityTestRender,
|
|
* ObjectTestRender, EntityBenchmark). Renders with the test server's resource pack and the repo's model JSONs.
|
|
*
|
|
* <p>Build/run from the repo root with a JDK 25: {@code CP=build/classes/java/main:build/resources/main:$(./gradlew -q
|
|
* printCompileClasspath)}, {@code javac -d <cls> -cp $CP $(ls tools/*.java | grep -v VanillaModelDump)},
|
|
* {@code java -cp <cls>:$CP [-Dout=<dir>] <Tool> [args]}.
|
|
*/
|
|
final class Harness {
|
|
|
|
static final String PACK_ROOT = "/home/elias/Dokumente/mcTestServer/plugins/PixelPics";
|
|
static final int SSAA = 3;
|
|
static final double V_FOV_HALF = Math.toRadians(DefaultScreenRenderer.V_FOV_DEG / 2);
|
|
static final SkyContext NOON = new SkyContext(6000, 0, 6000);
|
|
|
|
final Logger log;
|
|
final ResourcePack pack;
|
|
final AssetReader reader;
|
|
final TextureCache textures;
|
|
final BlockModelRegistry registry;
|
|
final CemModelLoader geo = new CemModelLoader();
|
|
final CemBaker baker;
|
|
final BlockEntityBaker beBaker;
|
|
final DefaultScreenRenderer renderer;
|
|
|
|
Harness(String logName) throws IOException {
|
|
log = Logger.getLogger(logName);
|
|
pack = ResourcePackLoader.load(new File(PACK_ROOT, "resourcepack"), log).orElseThrow();
|
|
reader = new AssetReader(pack);
|
|
textures = new TextureCache(pack);
|
|
registry = new BlockModelRegistry(reader, textures);
|
|
// Template set = entity fallback only; the vanilla dump is the primary model source.
|
|
for (String f : new String[] {"cem_template_models.json", "vanilla_entity_models.json"}) {
|
|
try (InputStream in = new FileInputStream("src/main/resources/cem/" + f)) {
|
|
geo.load(in, log);
|
|
}
|
|
}
|
|
SkinCache skins = new SkinCache();
|
|
baker = new CemBaker(geo, textures, skins);
|
|
beBaker = new BlockEntityBaker(geo, textures, skins, FontLoader.load(pack, textures, log));
|
|
renderer = new DefaultScreenRenderer(registry, new BiomeTintProvider(textures), textures, baker, beBaker, log);
|
|
}
|
|
|
|
/**
|
|
* Server-free BlockData stand-in: material + blockstate string are all the model registry needs. (Each proxy
|
|
* kind gets its own handler class: a shared generic one made the hot getMaterial/hashCode calls megamorphic and
|
|
* EntityBenchmark measurably slower.)
|
|
*/
|
|
static BlockData block(Material m, String state) {
|
|
String s = "minecraft:" + m.getKey().getKey() + (state == null ? "" : "[" + state + "]");
|
|
return (BlockData) Proxy.newProxyInstance(Harness.class.getClassLoader(),
|
|
new Class<?>[]{BlockData.class}, (p, meth, a) -> switch (meth.getName()) {
|
|
case "getMaterial" -> m;
|
|
case "getAsString", "toString" -> s;
|
|
case "equals" -> p == a[0];
|
|
case "hashCode" -> System.identityHashCode(p);
|
|
default -> defaultValue(meth.getReturnType());
|
|
});
|
|
}
|
|
|
|
/** false / 0 / null for an unstubbed proxy method. */
|
|
static Object defaultValue(Class<?> rt) {
|
|
if (rt == boolean.class) return false;
|
|
return rt.isPrimitive() ? 0 : null;
|
|
}
|
|
|
|
static WorldSnapshot emptyWorld(int minY, int maxY) {
|
|
return new WorldSnapshot(Map.of(), minY, maxY, block(Material.AIR, null));
|
|
}
|
|
|
|
/** Render job looking from {@code cam} at {@code look}, {@link #SSAA}x supersampled. */
|
|
static RenderJob job(WorldSnapshot world, Vector cam, Vector look, int w, int h, List<EntityState> mobs,
|
|
List<BlockEntityState> bes, List<DecorationState> decos, List<ObjectState> objects) {
|
|
Location loc = new Location(null, cam.getX(), cam.getY(), cam.getZ());
|
|
loc.setDirection(look.clone().subtract(cam));
|
|
List<Vector> rayMap = DefaultScreenRenderer.buildRayMap(loc, w * SSAA, h * SSAA);
|
|
return new RenderJob(world, rayMap, cam, w, h, NOON, mobs, bes, decos, objects);
|
|
}
|
|
|
|
/** Distance at which a box of extent {@code ext} (at least 0.5) fills the view, scaled {@code mul}, plus {@code add}. */
|
|
static double frameDistance(double[] min, double[] max, double mul, double add) {
|
|
double ext = 0.5;
|
|
for (int a = 0; a < 3; a++) ext = Math.max(ext, max[a] - min[a]);
|
|
return ext / (2 * Math.tan(V_FOV_HALF)) * mul + add;
|
|
}
|
|
|
|
static Vector center(double[] min, double[] max) {
|
|
return new Vector((min[0] + max[0]) / 2, (min[1] + max[1]) / 2, (min[2] + max[2]) / 2);
|
|
}
|
|
|
|
/** Copy of an entity state with some record components replaced (by name). */
|
|
static EntityState copy(EntityState s, Map<String, Object> overrides) throws ReflectiveOperationException {
|
|
var comps = EntityState.class.getRecordComponents();
|
|
Object[] vals = new Object[comps.length];
|
|
Class<?>[] types = new Class<?>[comps.length];
|
|
for (int i = 0; i < comps.length; i++) {
|
|
types[i] = comps[i].getType();
|
|
String n = comps[i].getName();
|
|
vals[i] = overrides.containsKey(n) ? overrides.get(n) : comps[i].getAccessor().invoke(s);
|
|
}
|
|
return EntityState.class.getDeclaredConstructor(types).newInstance(vals);
|
|
}
|
|
|
|
static ItemSpec item(String id) {
|
|
return new ItemSpec(id, null, null, false);
|
|
}
|
|
|
|
/** The views side by side under a 20 px label. */
|
|
static BufferedImage labelCell(String key, BufferedImage... views) {
|
|
int w = 0, labelH = 20;
|
|
for (BufferedImage v : views) w += v.getWidth();
|
|
BufferedImage cell = new BufferedImage(w, views[0].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());
|
|
for (int i = 0, x = 0; i < views.length; x += views[i].getWidth(), i++) g.drawImage(views[i], x, labelH, null);
|
|
g.setColor(Color.WHITE);
|
|
g.setFont(new Font("SansSerif", Font.BOLD, 13));
|
|
g.drawString(key, 4, 15);
|
|
g.dispose();
|
|
return cell;
|
|
}
|
|
|
|
/** Contact sheets of {@code cols} columns, {@code perPage} cells each, named {@code name(page)}. */
|
|
static void writeSheets(List<BufferedImage> cells, int cols, int perPage, File dir, IntFunction<String> name)
|
|
throws IOException {
|
|
int cw = cells.get(0).getWidth(), ch = cells.get(0).getHeight();
|
|
for (int page = 0, idx = 0; idx < cells.size(); page++, idx += perPage) {
|
|
int count = Math.min(perPage, cells.size() - idx);
|
|
BufferedImage sheet = new BufferedImage(cols * cw, (count + cols - 1) / cols * ch, 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++) g.drawImage(cells.get(idx + i), (i % cols) * cw, (i / cols) * ch, null);
|
|
g.dispose();
|
|
File f = new File(dir, name.apply(page));
|
|
ImageIO.write(sheet, "png", f);
|
|
System.out.println("WROTE " + f);
|
|
}
|
|
}
|
|
}
|