275 lines
17 KiB
Java
275 lines
17 KiB
Java
import eu.mhsl.minecraft.pixelpics.assets.*;
|
||
import eu.mhsl.minecraft.pixelpics.render.entity.*;
|
||
import eu.mhsl.minecraft.pixelpics.render.entity.BlockEntityState.*;
|
||
import eu.mhsl.minecraft.pixelpics.render.entity.cem.*;
|
||
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) block-entity renderer: each scene framed and labelled into a contact sheet. */
|
||
public class BlockEntityTestRender {
|
||
|
||
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 = 220, TH = 240;
|
||
|
||
static BlockEntityBaker beBaker;
|
||
static DecorationBaker decoBaker;
|
||
|
||
static BlockEntityState be(Kind kind, int bx, int by, int bz, float yaw) {
|
||
return new BlockEntityState(kind, bx, by, bz, yaw, ChestKind.SINGLE, 0, null, "oak",
|
||
null, null, null, List.of(), List.of(), null, null, null);
|
||
}
|
||
|
||
static BlockEntityState sign(Kind kind, float yaw, SignText front, SignText back) {
|
||
return new BlockEntityState(kind, 0, 0, 0, yaw, null, 0, null, "oak",
|
||
null, null, null, List.of(), List.of(), null, front, back);
|
||
}
|
||
|
||
static SignText txt(org.bukkit.DyeColor dye, boolean glow, String... lines) {
|
||
return new SignText(List.of(lines),
|
||
eu.mhsl.minecraft.pixelpics.render.util.ColorUtil.signFillArgb(dye, glow),
|
||
eu.mhsl.minecraft.pixelpics.render.util.ColorUtil.signOutlineArgb(dye), glow);
|
||
}
|
||
|
||
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);
|
||
CemModelLoader geo = new CemModelLoader();
|
||
geo.load(new java.io.FileInputStream("/tmp/cem_models.json"), log);
|
||
SkinCache skins = new SkinCache();
|
||
eu.mhsl.minecraft.pixelpics.assets.font.BitmapFont font =
|
||
eu.mhsl.minecraft.pixelpics.assets.font.FontLoader.load(pack, textures, log);
|
||
log.info("font glyphs loaded: " + (font.isEmpty() ? "NONE" : "ok"));
|
||
CemBaker baker = new CemBaker(geo, textures, skins);
|
||
beBaker = new BlockEntityBaker(geo, textures, skins, font);
|
||
decoBaker = new DecorationBaker(textures);
|
||
DefaultScreenRenderer renderer = new DefaultScreenRenderer(registry, tint, textures, baker, beBaker, log);
|
||
|
||
BlockData air = (BlockData) Proxy.newProxyInstance(BlockEntityTestRender.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(), -64, 320, air);
|
||
SkyContext sky = new SkyContext(6000, 0, 6000);
|
||
|
||
// Scenes: label -> list of block-entity states.
|
||
LinkedHashMap<String, List<BlockEntityState>> scenes = new LinkedHashMap<>();
|
||
scenes.put("chest_S", List.of(be(Kind.CHEST, 0, 0, 0, 0)));
|
||
scenes.put("chest_N", List.of(be(Kind.CHEST, 0, 0, 0, 180)));
|
||
scenes.put("chest_E", List.of(be(Kind.CHEST, 0, 0, 0, 270)));
|
||
scenes.put("chest_W", List.of(be(Kind.CHEST, 0, 0, 0, 90)));
|
||
scenes.put("double_chest", List.of(
|
||
new BlockEntityState(Kind.CHEST, 0, 0, 0, 0, ChestKind.LEFT, 0, null, null, null, null, null, List.of(), List.of(), null, null, null),
|
||
new BlockEntityState(Kind.CHEST, 1, 0, 0, 0, ChestKind.RIGHT, 0, null, null, null, null, null, List.of(), List.of(), null, null, null)));
|
||
scenes.put("sign", List.of(be(Kind.SIGN, 0, 0, 0, 0)));
|
||
scenes.put("wall_sign", List.of(be(Kind.WALL_SIGN, 0, 0, 0, 0)));
|
||
scenes.put("hanging_sign", List.of(be(Kind.HANGING_SIGN, 0, 0, 0, 0)));
|
||
// --- sign text (calibration) --- facingDeg=180 → front (model −Z) faces the camera (north).
|
||
org.bukkit.DyeColor B = org.bukkit.DyeColor.BLACK;
|
||
scenes.put("sign_text", List.of(sign(Kind.SIGN, 180,
|
||
txt(B, false, "Hello World", "zweite Zeile", "Gruesse: AÖÜ", "ßäöü 12345"), null)));
|
||
scenes.put("wall_sign_text", List.of(sign(Kind.WALL_SIGN, 180,
|
||
txt(B, false, "Wall Sign", "Line 2", "Line 3", "Line 4"), null)));
|
||
scenes.put("hanging_sign_text", List.of(sign(Kind.HANGING_SIGN, 180,
|
||
txt(B, false, "Hanging", "sign text", "row 3", "row 4"), null)));
|
||
scenes.put("sign_text_red", List.of(sign(Kind.SIGN, 180,
|
||
txt(org.bukkit.DyeColor.RED, false, "RED INK", "colored", "sign text", ""), null)));
|
||
scenes.put("sign_text_glow", List.of(sign(Kind.SIGN, 180,
|
||
txt(org.bukkit.DyeColor.LIME, true, "GLOWING", "lime glow", "outline!", ""), null)));
|
||
scenes.put("sign_text_front", List.of(sign(Kind.SIGN, 180,
|
||
txt(B, false, "FRONT", "side", "", ""), txt(org.bukkit.DyeColor.BLUE, false, "BACK", "side", "", ""))));
|
||
// View the BACK side head-on (facingDeg=0 turns the back face toward the camera).
|
||
scenes.put("sign_view_back", List.of(sign(Kind.SIGN, 0,
|
||
txt(B, false, "FRONTXY", "f2", "", ""), txt(org.bukkit.DyeColor.BLUE, false, "BACKXY", "b2", "", ""))));
|
||
scenes.put("sign_1line", List.of(sign(Kind.SIGN, 180, txt(B, false, "", "SHOP", "", ""), null)));
|
||
scenes.put("sign_2line", List.of(sign(Kind.SIGN, 180, txt(B, false, "", "Welcome", "home!", ""), null)));
|
||
scenes.put("sign_long", List.of(sign(Kind.SIGN, 180, txt(B, false, "a very long line here", "", "", ""), null)));
|
||
scenes.put("banner", List.of(new BlockEntityState(Kind.BANNER, 0, 0, 0, 0, null, 0xFFCC2020, null, null, null, null, null, List.of(), List.of(), null, null, null)));
|
||
scenes.put("banner_patterned", List.of(new BlockEntityState(Kind.BANNER, 0, 0, 0, 0, null, 0xFFFFFFFF, null, null, null, null, null,
|
||
List.of(new BannerPattern("stripe_bottom", 0xFFCC2020), new BannerPattern("cross", 0xFF2040CC), new BannerPattern("border", 0xFF20A020)), List.of(), null, null, null)));
|
||
scenes.put("wall_banner", List.of(new BlockEntityState(Kind.WALL_BANNER, 0, 0, 0, 0, null, 0xFF2040CC, null, null, null, null, null, List.of(), List.of(), null, null, null)));
|
||
scenes.put("bed", List.of(
|
||
new BlockEntityState(Kind.BED, 0, 0, 0, 0, null, 0, "red", null, BedPart.FOOT, null, null, List.of(), List.of(), null, null, null),
|
||
new BlockEntityState(Kind.BED, 0, 0, 1, 0, null, 0, "red", null, BedPart.HEAD, null, null, List.of(), List.of(), null, null, null)));
|
||
scenes.put("shulker_box", List.of(new BlockEntityState(Kind.SHULKER_BOX, 0, 0, 0, 0, null, 0, null, null, null, null, null, List.of(), List.of(), null, null, null)));
|
||
scenes.put("conduit", List.of(be(Kind.CONDUIT, 0, 0, 0, 0)));
|
||
scenes.put("decorated_pot", List.of(be(Kind.DECORATED_POT, 0, 0, 0, 0)));
|
||
scenes.put("decorated_pot_sherds", List.of(new BlockEntityState(Kind.DECORATED_POT, 0, 0, 0, 0, null, 0, null, null, null, null, null,
|
||
List.of(), List.of("angler_pottery_sherd", "heart_pottery_sherd", "skull_pottery_sherd", "brick"), null, null, null)));
|
||
scenes.put("bell", List.of(be(Kind.BELL, 0, 0, 0, 0)));
|
||
scenes.put("head_skeleton", List.of(head("skeleton")));
|
||
scenes.put("head_zombie", List.of(head("zombie")));
|
||
scenes.put("head_creeper", List.of(head("creeper")));
|
||
scenes.put("head_dragon", List.of(head("dragon")));
|
||
scenes.put("head_piglin", List.of(head("piglin")));
|
||
scenes.put("wither_skull", List.of(headKind(Kind.HEAD, "wither_skeleton")));
|
||
scenes.put("wall_head", List.of(headKind(Kind.WALL_HEAD, "skeleton")));
|
||
|
||
// Decoration scenes (paintings + item frames), rendered via the same scene path.
|
||
LinkedHashMap<String, List<DecorationState>> decoScenes = new LinkedHashMap<>();
|
||
// kebab is 1x1; place a thin quad facing south spanning the block at z=0..0.0625.
|
||
decoScenes.put("painting_kebab_S", List.of(new DecorationState(DecorationState.Kind.PAINTING,
|
||
0, 0, 0, 1, 1, 0.0625, DecorationState.Facing.SOUTH, "kebab", null, 0, false)));
|
||
decoScenes.put("painting_kebab_N", List.of(new DecorationState(DecorationState.Kind.PAINTING,
|
||
0, 0, 0.9375, 1, 1, 1, DecorationState.Facing.NORTH, "kebab", null, 0, false)));
|
||
decoScenes.put("painting_pool_S", List.of(new DecorationState(DecorationState.Kind.PAINTING,
|
||
0, 0, 0, 2, 1, 0.0625, DecorationState.Facing.SOUTH, "pool", null, 0, false)));
|
||
decoScenes.put("item_frame", List.of(new DecorationState(DecorationState.Kind.ITEM_FRAME,
|
||
0.0625, 0.0625, 0, 0.9375, 0.9375, 0.0625, DecorationState.Facing.SOUTH, null, "diamond", 0, false)));
|
||
decoScenes.put("item_frame_empty", List.of(new DecorationState(DecorationState.Kind.ITEM_FRAME,
|
||
0.0625, 0.0625, 0, 0.9375, 0.9375, 0.0625, DecorationState.Facing.SOUTH, null, null, 0, false)));
|
||
decoScenes.put("item_frame_E", List.of(new DecorationState(DecorationState.Kind.ITEM_FRAME,
|
||
0.9375, 0.0625, 0.0625, 1.0, 0.9375, 0.9375, DecorationState.Facing.EAST, null, "diamond", 0, false)));
|
||
|
||
List<BufferedImage> cells = new ArrayList<>();
|
||
List<String> names = new ArrayList<>(scenes.keySet());
|
||
for (String name : names) {
|
||
cells.add(labelCell(name, renderScene(renderer, empty, sky, scenes.get(name))));
|
||
log.info("rendered " + name);
|
||
}
|
||
for (String name : decoScenes.keySet()) {
|
||
cells.add(labelCell(name, renderDeco(renderer, empty, sky, decoScenes.get(name))));
|
||
names.add(name);
|
||
log.info("rendered " + name);
|
||
}
|
||
|
||
File outDir = new File("/home/elias/Dokumente/PixelPics-BlockEntity-Renders");
|
||
outDir.mkdirs();
|
||
for (int i = 0; i < names.size(); i++) ImageIO.write(cells.get(i), "png", new File(outDir, names.get(i) + ".png"));
|
||
|
||
int cols = 4, cw = cells.get(0).getWidth(), ch = cells.get(0).getHeight();
|
||
int rows = (cells.size() + cols - 1) / cols;
|
||
BufferedImage sheet = new BufferedImage(cols * cw, rows * 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 < cells.size(); i++) g.drawImage(cells.get(i), (i % cols) * cw, (i / cols) * ch, null);
|
||
g.dispose();
|
||
File f = new File(outDir, "sheet.png");
|
||
ImageIO.write(sheet, "png", f);
|
||
System.out.println("WROTE " + f);
|
||
}
|
||
|
||
static BlockEntityState head(String type) { return headKind(Kind.HEAD, type); }
|
||
static BlockEntityState headKind(Kind kind, String type) {
|
||
return new BlockEntityState(kind, 0, 0, 0, 0, null, 0, null, null, null, type, null, List.of(), List.of(), null, null, null);
|
||
}
|
||
|
||
static BufferedImage renderScene(DefaultScreenRenderer renderer, WorldSnapshot world, SkyContext sky,
|
||
List<BlockEntityState> states) {
|
||
double[] min = {Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE};
|
||
double[] max = {-Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE};
|
||
for (BlockEntityState s : states) {
|
||
RenderedEntity re = beBaker.bake(s);
|
||
if (re == null) continue;
|
||
for (int a = 0; a < 3; a++) { min[a] = Math.min(min[a], re.aabbMin[a]); max[a] = Math.max(max[a], re.aabbMax[a]); }
|
||
}
|
||
if (min[0] > max[0]) { min = new double[]{0, 0, 0}; max = new double[]{1, 1, 1}; }
|
||
double cx = (min[0] + max[0]) / 2, cy = (min[1] + max[1]) / 2, cz = (min[2] + max[2]) / 2;
|
||
double ext = 0.5;
|
||
for (int a = 0; a < 3; a++) ext = Math.max(ext, max[a] - min[a]);
|
||
double dist = ext / (2 * Math.tan(H_FOV_HALF)) * 1.4 + 0.6;
|
||
|
||
Vector center = new Vector(cx, cy, cz);
|
||
Vector cam = new Vector(cx - dist, cy + dist * 0.45, cz - dist * 0.35);
|
||
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(), states);
|
||
return renderer.execute(job);
|
||
}
|
||
|
||
static BufferedImage renderDeco(DefaultScreenRenderer renderer, WorldSnapshot world, SkyContext sky,
|
||
List<DecorationState> states) {
|
||
double[] min = {Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE};
|
||
double[] max = {-Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE};
|
||
for (DecorationState s : states) {
|
||
RenderedEntity re = decoBaker.bake(s);
|
||
if (re == null) continue;
|
||
for (int a = 0; a < 3; a++) { min[a] = Math.min(min[a], re.aabbMin[a]); max[a] = Math.max(max[a], re.aabbMax[a]); }
|
||
}
|
||
if (min[0] > max[0]) { min = new double[]{0, 0, 0}; max = new double[]{1, 1, 1}; }
|
||
double cx = (min[0] + max[0]) / 2, cy = (min[1] + max[1]) / 2, cz = (min[2] + max[2]) / 2;
|
||
double ext = 0.5;
|
||
for (int a = 0; a < 3; a++) ext = Math.max(ext, max[a] - min[a]);
|
||
double dist = ext / (2 * Math.tan(H_FOV_HALF)) * 1.4 + 0.6;
|
||
// View from the front side (along +facing) so the picture is visible, not the back.
|
||
DecorationState.Facing f = states.get(0).facing();
|
||
double fx = f.axis() == 0 ? f.sign() : 0, fy = f.axis() == 1 ? f.sign() : 0, fz = f.axis() == 2 ? f.sign() : 0;
|
||
Vector center = new Vector(cx, cy, cz);
|
||
Vector cam = new Vector(cx + fx * dist + 0.1, cy + fy * dist + 0.1, cz + fz * dist + 0.1);
|
||
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(), List.of(), states);
|
||
return renderer.execute(job);
|
||
}
|
||
|
||
static BufferedImage labelCell(String key, BufferedImage v) {
|
||
int labelH = 20;
|
||
BufferedImage cell = new BufferedImage(v.getWidth(), v.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(v, 0, 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) {
|
||
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;
|
||
}
|
||
}
|