You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
232 lines
11 KiB
Java
232 lines
11 KiB
Java
package com.razz.dfashion.bbmodel;
|
|
|
|
import net.minecraft.client.model.geom.ModelPart;
|
|
import net.minecraft.core.Direction;
|
|
import org.joml.Vector3f;
|
|
|
|
import java.util.*;
|
|
|
|
public class BbmodelBaker {
|
|
|
|
/** Backward-compat: cosmetic pipeline call site. {@code naturalUv=false} keeps vanilla's
|
|
* cube vertex order which is calibrated for the player avatar's pre-flip transforms. */
|
|
public static Map<String, ModelPart> bake(Bbmodel model, int texWidth, int texHeight, boolean mirrorX) {
|
|
return bake(model, texWidth, texHeight, mirrorX, false);
|
|
}
|
|
|
|
/**
|
|
* @param naturalUv when true, polygons are constructed with a per-face vertex order that
|
|
* produces a 1:1 UV→cube mapping (Blockbench's authoring convention),
|
|
* matching decocraft's renderer. Use this for BlockEntity rendering, which
|
|
* doesn't go through the avatar pre-flip pipeline. {@code mirrorX} should
|
|
* be false in this mode.
|
|
*/
|
|
public static Map<String, ModelPart> bake(Bbmodel model, int texWidth, int texHeight,
|
|
boolean mirrorX, boolean naturalUv) {
|
|
Map<String, BbCube> cubeIndex = new HashMap<>();
|
|
for (BbCube c : model.elements()) cubeIndex.put(c.uuid(), c);
|
|
|
|
Map<String, BbGroup> groupIndex = new HashMap<>();
|
|
for (BbGroup g : model.groups()) groupIndex.put(g.uuid(), g);
|
|
|
|
Map<String, ModelPart> result = new HashMap<>();
|
|
for (BbOutlinerNode node : model.outliner()) {
|
|
if (node instanceof BbOutlinerNode.GroupRef gref) {
|
|
BbGroup g = groupIndex.get(gref.uuid());
|
|
// Top-level: parentOrigin = the group's own origin so the ModelPart's
|
|
// position resolves to (0, 0, 0). bone.translateAndRotate in the render
|
|
// layer already places us at the bone, so the ModelPart itself doesn't
|
|
// need to translate again.
|
|
result.put(g.name(),
|
|
buildPart(gref, g, g.origin(), cubeIndex, groupIndex, texWidth, texHeight, mirrorX, naturalUv));
|
|
}
|
|
// ElementRef at root = loose cube, skip (no bone to attach to)
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private static ModelPart buildPart(
|
|
BbOutlinerNode.GroupRef gref,
|
|
BbGroup group,
|
|
Vector3f parentOrigin,
|
|
Map<String, BbCube> cubeIndex,
|
|
Map<String, BbGroup> groupIndex,
|
|
int texWidth, int texHeight,
|
|
boolean mirrorX,
|
|
boolean naturalUv
|
|
) {
|
|
List<ModelPart.Cube> cubes = new ArrayList<>();
|
|
Map<String, ModelPart> children = new LinkedHashMap<>();
|
|
|
|
for (BbOutlinerNode child : gref.children()) {
|
|
switch (child) {
|
|
case BbOutlinerNode.ElementRef er -> {
|
|
BbCube bb = cubeIndex.get(er.uuid());
|
|
if (bb == null) continue; // locator ref, skip
|
|
|
|
if (isRotated(bb)) {
|
|
// ModelPart.Cube is axis-aligned, so per-cube rotation is done
|
|
// by wrapping the cube in its own one-cube ModelPart whose pose
|
|
// carries the rotation.
|
|
ModelPart.Cube inner = bbCubeToCube(bb, bb.origin(), texWidth, texHeight, mirrorX, naturalUv);
|
|
ModelPart wrapper = new ModelPart(List.of(inner), Map.of());
|
|
wrapper.setPos(
|
|
bb.origin().x - group.origin().x,
|
|
bb.origin().y - group.origin().y,
|
|
bb.origin().z - group.origin().z
|
|
);
|
|
wrapper.setRotation(
|
|
(float) Math.toRadians(bb.rotation().x),
|
|
(float) Math.toRadians(bb.rotation().y),
|
|
(float) Math.toRadians(bb.rotation().z)
|
|
);
|
|
children.put("cube_" + bb.uuid(), wrapper);
|
|
} else {
|
|
cubes.add(bbCubeToCube(bb, group.origin(), texWidth, texHeight, mirrorX, naturalUv));
|
|
}
|
|
}
|
|
case BbOutlinerNode.GroupRef nested -> {
|
|
BbGroup ng = groupIndex.get(nested.uuid());
|
|
children.put(ng.name(),
|
|
buildPart(nested, ng, group.origin(), cubeIndex, groupIndex, texWidth, texHeight, mirrorX, naturalUv));
|
|
}
|
|
}
|
|
}
|
|
|
|
ModelPart part = new ModelPart(cubes, children);
|
|
part.setPos(
|
|
group.origin().x - parentOrigin.x,
|
|
group.origin().y - parentOrigin.y,
|
|
group.origin().z - parentOrigin.z
|
|
);
|
|
part.setRotation(
|
|
(float) Math.toRadians(group.rotation().x),
|
|
(float) Math.toRadians(group.rotation().y),
|
|
(float) Math.toRadians(group.rotation().z)
|
|
);
|
|
return part;
|
|
}
|
|
|
|
private static boolean isRotated(BbCube bb) {
|
|
Vector3f r = bb.rotation();
|
|
return r != null && (r.x != 0f || r.y != 0f || r.z != 0f);
|
|
}
|
|
|
|
private static ModelPart.Cube bbCubeToCube(
|
|
BbCube bb, Vector3f referenceOrigin, int texWidth, int texHeight,
|
|
boolean mirrorX, boolean naturalUv
|
|
) {
|
|
float x = bb.from().x - referenceOrigin.x;
|
|
float y = bb.from().y - referenceOrigin.y;
|
|
float z = bb.from().z - referenceOrigin.z;
|
|
|
|
float w = bb.to().x - bb.from().x;
|
|
float h = bb.to().y - bb.from().y;
|
|
float d = bb.to().z - bb.from().z;
|
|
|
|
// Which faces are visible — texture == -1 means hidden in bbmodel.
|
|
Set<Direction> visibleFaces = EnumSet.noneOf(Direction.class);
|
|
if (bb.faces() != null) {
|
|
bb.faces().forEach((key, face) -> {
|
|
if (face.texture() != null && face.texture() >= 0) {
|
|
Direction dir = directionFromName(key);
|
|
if (dir != null) visibleFaces.add(dir);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Vanilla ctor gives us vertex geometry + placeholder box-UV polygons.
|
|
// mirrorX=true flips the cube on X so faces orient correctly after the
|
|
// player render-layer's scale(-1, -1, 1) un-flip. Block-entity renderers
|
|
// don't apply that pre-flip, so they pass mirrorX=false.
|
|
ModelPart.Cube cube = new ModelPart.Cube(
|
|
0, 0, x, y, z, w, h, d, 0, 0, 0, mirrorX, texWidth, texHeight, visibleFaces
|
|
);
|
|
|
|
// Vanilla writes polygons[] in this order, skipping any direction not in visibleFaces.
|
|
Direction[] order = {
|
|
Direction.DOWN, Direction.UP, Direction.WEST,
|
|
Direction.NORTH, Direction.EAST, Direction.SOUTH
|
|
};
|
|
|
|
// The 8 cube vertices, named by their world-space corner. Used by the natural-UV
|
|
// path to reorder polygon vertices so each face's UV maps Blockbench's [u0,v0,u1,v1]
|
|
// rectangle 1:1 onto the cube — top-left of UV → top-left of face, etc.
|
|
ModelPart.Vertex nwBot = naturalUv ? new ModelPart.Vertex(x, y, z, 0, 0) : null;
|
|
ModelPart.Vertex neBot = naturalUv ? new ModelPart.Vertex(x + w, y, z, 0, 0) : null;
|
|
ModelPart.Vertex neTop = naturalUv ? new ModelPart.Vertex(x + w, y + h, z, 0, 0) : null;
|
|
ModelPart.Vertex nwTop = naturalUv ? new ModelPart.Vertex(x, y + h, z, 0, 0) : null;
|
|
ModelPart.Vertex swBot = naturalUv ? new ModelPart.Vertex(x, y, z + d, 0, 0) : null;
|
|
ModelPart.Vertex seBot = naturalUv ? new ModelPart.Vertex(x + w, y, z + d, 0, 0) : null;
|
|
ModelPart.Vertex seTop = naturalUv ? new ModelPart.Vertex(x + w, y + h, z + d, 0, 0) : null;
|
|
ModelPart.Vertex swTop = naturalUv ? new ModelPart.Vertex(x, y + h, z + d, 0, 0) : null;
|
|
|
|
int idx = 0;
|
|
for (Direction dir : order) {
|
|
if (!visibleFaces.contains(dir)) continue;
|
|
BbFace bbFace = bb.faces().get(nameOf(dir));
|
|
if (bbFace != null && bbFace.uv() != null && bbFace.uv().length >= 4) {
|
|
ModelPart.Vertex[] verts = naturalUv
|
|
? naturalVertsFor(dir, nwBot, neBot, neTop, nwTop, swBot, seBot, seTop, swTop)
|
|
: cube.polygons[idx].vertices();
|
|
cube.polygons[idx] = new ModelPart.Polygon(
|
|
verts,
|
|
bbFace.uv()[0], bbFace.uv()[1],
|
|
bbFace.uv()[2], bbFace.uv()[3],
|
|
texWidth, texHeight,
|
|
mirrorX,
|
|
dir
|
|
);
|
|
}
|
|
idx++;
|
|
}
|
|
|
|
return cube;
|
|
}
|
|
|
|
/**
|
|
* Returns the four cube vertices for {@code dir} in (TR, TL, BL, BR) visual order, where
|
|
* "visual" is the orientation the bbmodel author sees in Blockbench's per-face UV editor.
|
|
* Vanilla's {@link ModelPart.Polygon} ctor remaps {@code vertices[0..3]} to UV
|
|
* {@code (u1,v0)→(u0,v0)→(u0,v1)→(u1,v1)} — pairing this with (TR, TL, BL, BR) gives a
|
|
* 1:1 mapping. UP/DOWN use a north-up convention (UV top = +Z = south? — see decocraft).
|
|
*
|
|
* <p>UP face: vertex[0]=NE-top → texture top-right. So texture's "north" (top) = cube's
|
|
* north edge, texture's "east" (right) = cube's east edge — Blockbench's standard preview
|
|
* orientation when looking down at the top face.
|
|
*
|
|
* <p>DOWN face: vertex[0]=SE-bot → texture top-right. Texture's "south" (top) = cube's
|
|
* south edge — viewing from below as if you tilted your head back, with south "above" you.
|
|
*/
|
|
private static ModelPart.Vertex[] naturalVertsFor(
|
|
Direction dir,
|
|
ModelPart.Vertex nwBot, ModelPart.Vertex neBot, ModelPart.Vertex neTop, ModelPart.Vertex nwTop,
|
|
ModelPart.Vertex swBot, ModelPart.Vertex seBot, ModelPart.Vertex seTop, ModelPart.Vertex swTop
|
|
) {
|
|
return switch (dir) {
|
|
case NORTH -> new ModelPart.Vertex[]{nwTop, neTop, neBot, nwBot};
|
|
case SOUTH -> new ModelPart.Vertex[]{seTop, swTop, swBot, seBot};
|
|
case EAST -> new ModelPart.Vertex[]{neTop, seTop, seBot, neBot};
|
|
case WEST -> new ModelPart.Vertex[]{swTop, nwTop, nwBot, swBot};
|
|
case UP -> new ModelPart.Vertex[]{neTop, nwTop, swTop, seTop};
|
|
case DOWN -> new ModelPart.Vertex[]{seBot, swBot, nwBot, neBot};
|
|
};
|
|
}
|
|
|
|
private static String nameOf(Direction d) {
|
|
return d.getName();
|
|
}
|
|
|
|
private static Direction directionFromName(String name) {
|
|
return switch (name) {
|
|
case "down" -> Direction.DOWN;
|
|
case "up" -> Direction.UP;
|
|
case "north" -> Direction.NORTH;
|
|
case "south" -> Direction.SOUTH;
|
|
case "west" -> Direction.WEST;
|
|
case "east" -> Direction.EAST;
|
|
default -> null;
|
|
};
|
|
}
|
|
}
|