bind block textures to the model under blockstate rotations (vanilla UVs)
- face UV orientation now follows vanilla BlockElement.uvsByFace: north/ east/down faces were sampled mirrored. Default UVs adjusted to match. Block and entity faces share one mapping (ElementIntersector.faceUv), so the separate blockUv mode of the entity intersector is gone. - x/y blockstate rotations without uvlock: elements keep their model-space copy and are intersected with the inverse-rotated ray, so textures turn with the model exactly like vanilla (pistons facing down showed their side textures in different directions per face). Replaces the earlier up/down-only UV rotation. Display entities use the same model-space path. Edge-case test plugin: piston/observer/dispenser scenes.
This commit is contained in:
1 parent
73e521131b
commit
37fa1d8f56
8 files changed
+139
-95
No files matched your search
@@ -68,6 +68,8 @@ public final class ModelBaker {
|
||||
rescale = dto.rotation.rescale;
|
||||
}
|
||||
|
||||
Element modelElement = new Element(from, to, faces, rotOrigin, rotAxis, rotAngle, rescale);
|
||||
|
||||
// Apply blockstate variant rotation (90-degree steps) to box + faces + element rotation.
|
||||
for (int i = 0; i < xSteps; i++) {
|
||||
double[][] r = rotateBoxX(from, to);
|
||||
@@ -86,12 +88,6 @@ public final class ModelBaker {
|
||||
from = r[0];
|
||||
to = r[1];
|
||||
faces = rotateFacesY(faces);
|
||||
// Without uvlock the top/bottom textures turn with the model (vanilla); side faces already
|
||||
// keep their orientation through the face re-assignment. With uvlock they stay world-aligned.
|
||||
if (!variant.uvlock()) {
|
||||
faces[Direction.UP.ordinal()] = turned(faces[Direction.UP.ordinal()], 90);
|
||||
faces[Direction.DOWN.ordinal()] = turned(faces[Direction.DOWN.ordinal()], 90);
|
||||
}
|
||||
if (rotAxis >= 0) {
|
||||
rotOrigin = rotatePointY(rotOrigin);
|
||||
AxisRotation na = rotateAxisY(rotAxis);
|
||||
@@ -100,7 +96,10 @@ public final class ModelBaker {
|
||||
}
|
||||
}
|
||||
|
||||
baked.add(new Element(from, to, faces, rotOrigin, rotAxis, rotAngle, rescale));
|
||||
// Without uvlock, textures turn with the model (vanilla): keep the model-space element so the
|
||||
// intersectors can sample it exactly. With uvlock they stay world-aligned (the rotated faces).
|
||||
Element unrotated = (!variant.uvlock() && (xSteps != 0 || ySteps != 0)) ? modelElement : null;
|
||||
baked.add(new Element(from, to, faces, rotOrigin, rotAxis, rotAngle, rescale, unrotated, xSteps, ySteps));
|
||||
|
||||
// Accumulate average color from the element's face textures.
|
||||
for (Face f : faces) {
|
||||
@@ -140,9 +139,13 @@ public final class ModelBaker {
|
||||
*/
|
||||
private double[] defaultUv(Direction dir, double[] f, double[] t) {
|
||||
return switch (dir) {
|
||||
case UP, DOWN -> new double[] {f[0], f[2], t[0], t[2]};
|
||||
case NORTH, SOUTH -> new double[] {f[0], 1 - t[1], t[0], 1 - f[1]};
|
||||
case WEST, EAST -> new double[] {f[2], 1 - t[1], t[2], 1 - f[1]};
|
||||
// Vanilla BlockElement.uvsByFace (u/v in 0..1).
|
||||
case DOWN -> new double[] {f[0], 1 - t[2], t[0], 1 - f[2]};
|
||||
case UP -> new double[] {f[0], f[2], t[0], t[2]};
|
||||
case NORTH -> new double[] {1 - t[0], 1 - t[1], 1 - f[0], 1 - f[1]};
|
||||
case SOUTH -> new double[] {f[0], 1 - t[1], t[0], 1 - f[1]};
|
||||
case WEST -> new double[] {f[2], 1 - t[1], t[2], 1 - f[1]};
|
||||
case EAST -> new double[] {1 - t[2], 1 - t[1], 1 - f[2], 1 - f[1]};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -168,12 +171,6 @@ public final class ModelBaker {
|
||||
};
|
||||
}
|
||||
|
||||
/** The face with its texture lookup rotated by a further {@code deg} (90° steps). */
|
||||
private static Face turned(Face f, int deg) {
|
||||
if (f == null) return null;
|
||||
return new Face(f.texture, f.u1, f.v1, f.u2, f.v2, (f.rotation + deg) % 360, f.tintIndex);
|
||||
}
|
||||
|
||||
// --- 90-degree box rotations around the block center (0.5, 0.5, 0.5) ---
|
||||
|
||||
private double[][] rotateBoxY(double[] from, double[] to) {
|
||||
|
||||
@@ -19,6 +19,13 @@ public final class Element {
|
||||
public final double rotAngleRad; // radians
|
||||
public final boolean rescale;
|
||||
|
||||
// Blockstate rotation WITHOUT uvlock: the model-space element and the 90° steps (x first, then y) that
|
||||
// turn it into this world-space one. Textures stay bound to the model, so intersectors hit the
|
||||
// unrotated element with the inverse-rotated ray. Null/0 when unrotated or uvlocked.
|
||||
public final Element unrotated;
|
||||
public final int xSteps;
|
||||
public final int ySteps;
|
||||
|
||||
public Element(
|
||||
double[] from,
|
||||
double[] to,
|
||||
@@ -27,6 +34,23 @@ public final class Element {
|
||||
int rotAxis,
|
||||
double rotAngleRad,
|
||||
boolean rescale) {
|
||||
this(from, to, faces, rotOrigin, rotAxis, rotAngleRad, rescale, null, 0, 0);
|
||||
}
|
||||
|
||||
public Element(
|
||||
double[] from,
|
||||
double[] to,
|
||||
Face[] faces,
|
||||
double[] rotOrigin,
|
||||
int rotAxis,
|
||||
double rotAngleRad,
|
||||
boolean rescale,
|
||||
Element unrotated,
|
||||
int xSteps,
|
||||
int ySteps) {
|
||||
this.unrotated = unrotated;
|
||||
this.xSteps = xSteps;
|
||||
this.ySteps = ySteps;
|
||||
this.from = from;
|
||||
this.to = to;
|
||||
this.faces = faces;
|
||||
@@ -36,6 +60,29 @@ public final class Element {
|
||||
this.rescale = rescale;
|
||||
}
|
||||
|
||||
/** Model space -> world: vanilla blockstate rotation (x steps, then y steps; clockwise about the centre). */
|
||||
public static double[] rotate(double x, double y, double z, int xSteps, int ySteps, boolean point) {
|
||||
double c = point ? 0.5 : 0;
|
||||
double dx = x - c, dy = y - c, dz = z - c;
|
||||
for (int i = 0; i < xSteps; i++) {
|
||||
double ny = dz, nz = -dy; // (dy, dz) -> (dz, -dy)
|
||||
dy = ny;
|
||||
dz = nz;
|
||||
}
|
||||
for (int i = 0; i < ySteps; i++) {
|
||||
double nx = -dz, nz = dx; // (dx, dz) -> (-dz, dx)
|
||||
dx = nx;
|
||||
dz = nz;
|
||||
}
|
||||
return new double[] {dx + c, dy + c, dz + c};
|
||||
}
|
||||
|
||||
/** World -> model space: the inverse of {@link #rotate}. */
|
||||
public static double[] unrotate(double x, double y, double z, int xSteps, int ySteps, boolean point) {
|
||||
double[] p = rotate(x, y, z, 0, (4 - ySteps) % 4, point);
|
||||
return rotate(p[0], p[1], p[2], (4 - xSteps) % 4, 0, point);
|
||||
}
|
||||
|
||||
public boolean isAxisAligned() {
|
||||
return rotAxis < 0 || rotAngleRad == 0.0;
|
||||
}
|
||||
|
||||
@@ -16,15 +16,7 @@ public final class EntityCube {
|
||||
public final double[] aabbMin = {Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE};
|
||||
public final double[] aabbMax = {-Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE};
|
||||
|
||||
/** Faces use BLOCK-model UV orientation (resolved block-model faces) instead of entity box-UV. */
|
||||
public final boolean blockUv;
|
||||
|
||||
public EntityCube(double[] from, double[] to, Face[] faces, Affine toWorld) {
|
||||
this(from, to, faces, toWorld, false);
|
||||
}
|
||||
|
||||
public EntityCube(double[] from, double[] to, Face[] faces, Affine toWorld, boolean blockUv) {
|
||||
this.blockUv = blockUv;
|
||||
this.from = from;
|
||||
this.to = to;
|
||||
this.faces = faces;
|
||||
|
||||
@@ -2,6 +2,7 @@ package eu.mhsl.minecraft.pixelpics.render.entity;
|
||||
|
||||
import eu.mhsl.minecraft.pixelpics.assets.model.Direction;
|
||||
import eu.mhsl.minecraft.pixelpics.assets.model.Face;
|
||||
import eu.mhsl.minecraft.pixelpics.render.raytrace.ElementIntersector;
|
||||
import eu.mhsl.minecraft.pixelpics.render.raytrace.FaceHit;
|
||||
import eu.mhsl.minecraft.pixelpics.render.util.ColorUtil;
|
||||
import org.bukkit.util.Vector;
|
||||
@@ -85,52 +86,9 @@ public final class EntityIntersector {
|
||||
double fx = frac(px, cube.from[0], cube.to[0]);
|
||||
double fy = frac(py, cube.from[1], cube.to[1]);
|
||||
double fz = frac(pz, cube.from[2], cube.to[2]);
|
||||
// (s,t) = Blockbench/Java box-UV (lerp_x, lerp_y) for this face (see BoxUv). Front/right faces
|
||||
// run their horizontal axis opposite to back/left (they're viewed from the other side).
|
||||
double s, tt;
|
||||
if (cube.blockUv) {
|
||||
// Block-model faces (see ElementIntersector): side faces run along +x / +z, up/down along x,z.
|
||||
switch (dir) {
|
||||
case UP, DOWN -> {
|
||||
s = fx;
|
||||
tt = fz;
|
||||
}
|
||||
case NORTH, SOUTH -> {
|
||||
s = fx;
|
||||
tt = 1 - fy;
|
||||
}
|
||||
default -> {
|
||||
s = fz;
|
||||
tt = 1 - fy;
|
||||
}
|
||||
}
|
||||
} else
|
||||
switch (dir) {
|
||||
case UP -> {
|
||||
s = fx;
|
||||
tt = fz;
|
||||
}
|
||||
case DOWN -> {
|
||||
s = fx;
|
||||
tt = 1 - fz;
|
||||
}
|
||||
case NORTH -> {
|
||||
s = 1 - fx;
|
||||
tt = 1 - fy;
|
||||
}
|
||||
case SOUTH -> {
|
||||
s = fx;
|
||||
tt = 1 - fy;
|
||||
}
|
||||
case EAST -> {
|
||||
s = 1 - fz;
|
||||
tt = 1 - fy;
|
||||
}
|
||||
default -> {
|
||||
s = fz;
|
||||
tt = 1 - fy;
|
||||
} // WEST
|
||||
}
|
||||
// Vanilla face-UV orientation, shared by block-model faces and entity box-UV (see BoxUv).
|
||||
double[] st = ElementIntersector.faceUv(dir, fx, fy, fz);
|
||||
double s = st[0], tt = st[1];
|
||||
int color = face.sample(s, tt);
|
||||
if (!ColorUtil.opaque(color)) return null;
|
||||
|
||||
|
||||
@@ -179,8 +179,7 @@ public final class CemBaker implements EntityBaker<EntityState> {
|
||||
|
||||
List<EntityCube> cubes = new ArrayList<>(baked.size());
|
||||
for (CemGeometry.Baked b : baked) cubes.add(new EntityCube(b.from(), b.to(), b.faces(), place.mul(b.world())));
|
||||
for (EntityCube c : itemCubes)
|
||||
cubes.add(new EntityCube(c.from, c.to, c.faces, place.mul(c.toWorld), c.blockUv));
|
||||
for (EntityCube c : itemCubes) cubes.add(new EntityCube(c.from, c.to, c.faces, place.mul(c.toWorld)));
|
||||
return RenderedEntity.of(cubes);
|
||||
}
|
||||
|
||||
|
||||
@@ -109,10 +109,14 @@ public final class ItemGeometry {
|
||||
if (data == null) return;
|
||||
ResolvedModel model = registry.get(data);
|
||||
if (model == null || !model.hasGeometry) return;
|
||||
for (Element e : model.elements) {
|
||||
for (Element world : model.elements) {
|
||||
// Rotated without uvlock: bake the model-space element turned by the blockstate rotation, so
|
||||
// textures stay bound to the model (vanilla); otherwise the world-space element as is.
|
||||
Element e = world.unrotated != null ? world.unrotated : world;
|
||||
Affine variant = world.unrotated != null ? variantRotation(world.xSteps, world.ySteps) : Affine.identity();
|
||||
Face[] faces = new Face[6];
|
||||
for (int i = 0; i < 6; i++) faces[i] = tint(e.faces[i], data);
|
||||
out.add(new EntityCube(e.from, e.to, faces, toWorld.mul(elementTransform(e)), true));
|
||||
out.add(new EntityCube(e.from, e.to, faces, toWorld.mul(variant).mul(elementTransform(e))));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,7 +125,7 @@ public final class ItemGeometry {
|
||||
if (state == null || blockEntities == null) return;
|
||||
RenderedEntity r = blockEntities.bake(state); // placed at cell (bx,by,bz) = (0,0,0)
|
||||
if (r == null) return;
|
||||
for (EntityCube c : r.cubes) out.add(new EntityCube(c.from, c.to, c.faces, toWorld.mul(c.toWorld), c.blockUv));
|
||||
for (EntityCube c : r.cubes) out.add(new EntityCube(c.from, c.to, c.faces, toWorld.mul(c.toWorld)));
|
||||
}
|
||||
|
||||
// --- item definition / model resolution --------------------------------------------------------
|
||||
@@ -426,6 +430,14 @@ public final class ItemGeometry {
|
||||
return boxes;
|
||||
}
|
||||
|
||||
/** Vanilla blockstate rotation (x steps then y steps, clockwise, about the block centre); see Element#rotate. */
|
||||
private static Affine variantRotation(int xSteps, int ySteps) {
|
||||
return Affine.translation(0.5, 0.5, 0.5)
|
||||
.mul(Affine.rotY(-Math.PI / 2 * ySteps))
|
||||
.mul(Affine.rotX(-Math.PI / 2 * xSteps))
|
||||
.mul(Affine.translation(-0.5, -0.5, -0.5));
|
||||
}
|
||||
|
||||
/** Element rotation (origin/axis/angle, optional rescale) as an affine in block units. */
|
||||
private static Affine elementTransform(Element e) {
|
||||
if (e.isAxisAligned()) return Affine.identity();
|
||||
|
||||
@@ -40,9 +40,26 @@ public final class ElementIntersector {
|
||||
List<Candidate> candidates = new ArrayList<>(model.elements.size());
|
||||
for (int i = 0; i < model.elements.size(); i++) {
|
||||
Element element = model.elements.get(i);
|
||||
Candidate c = element.isAxisAligned()
|
||||
? intersectAabb(element, ox, oy, oz, dx, dy, dz)
|
||||
: intersectObb(element, ox, oy, oz, dx, dy, dz);
|
||||
Candidate c;
|
||||
if (element.unrotated != null) {
|
||||
// Blockstate rotation without uvlock: hit the model-space element with the inverse-rotated ray
|
||||
// (same t: rigid transform), so faces and UVs stay bound to the model like in vanilla.
|
||||
Element m = element.unrotated;
|
||||
double[] mo = Element.unrotate(ox, oy, oz, element.xSteps, element.ySteps, true);
|
||||
double[] md = Element.unrotate(dx, dy, dz, element.xSteps, element.ySteps, false);
|
||||
c = m.isAxisAligned()
|
||||
? intersectAabb(m, mo[0], mo[1], mo[2], md[0], md[1], md[2])
|
||||
: intersectObb(m, mo[0], mo[1], mo[2], md[0], md[1], md[2]);
|
||||
if (c != null) {
|
||||
double[] n = Element.rotate(
|
||||
c.normal[0], c.normal[1], c.normal[2], element.xSteps, element.ySteps, false);
|
||||
c = new Candidate(c.element, c.t, c.dir, c.s, c.t2, n, 0);
|
||||
}
|
||||
} else {
|
||||
c = element.isAxisAligned()
|
||||
? intersectAabb(element, ox, oy, oz, dx, dy, dz)
|
||||
: intersectObb(element, ox, oy, oz, dx, dy, dz);
|
||||
}
|
||||
if (c != null) candidates.add(new Candidate(c.element, c.t, c.dir, c.s, c.t2, c.normal, i));
|
||||
}
|
||||
if (candidates.isEmpty()) return null;
|
||||
@@ -149,26 +166,28 @@ public final class ElementIntersector {
|
||||
double fracY = frac(py, from[1], to[1]);
|
||||
double fracZ = frac(pz, from[2], to[2]);
|
||||
|
||||
double s, t;
|
||||
switch (dir) {
|
||||
// Texture V is top-down (0 = texture top). For side faces the texture top is the block
|
||||
// top (high Y), so t = 1 - fracY.
|
||||
case UP, DOWN -> {
|
||||
s = fracX;
|
||||
t = fracZ;
|
||||
}
|
||||
case NORTH, SOUTH -> {
|
||||
s = fracX;
|
||||
t = 1 - fracY;
|
||||
}
|
||||
default -> {
|
||||
s = fracZ;
|
||||
t = 1 - fracY;
|
||||
} // WEST, EAST
|
||||
}
|
||||
double[] st = faceUv(dir, fracX, fracY, fracZ);
|
||||
double s = st[0], t = st[1];
|
||||
return new Candidate(e, tEntry, dir, s, t, normal, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Face-local (s, t) in 0..1 for a hit at block fractions (fx, fy, fz), matching vanilla's
|
||||
* {@code BlockElement.uvsByFace}: a face's UV rect {@code [u1,v1,u2,v2]} is lerped with these, so north/east
|
||||
* faces run their horizontal axis towards -x/-z (left-to-right seen from outside) and down runs v towards
|
||||
* north. Shared with the entity intersector (entity box-UV uses the same orientation).
|
||||
*/
|
||||
public static double[] faceUv(Direction dir, double fx, double fy, double fz) {
|
||||
return switch (dir) {
|
||||
case UP -> new double[] {fx, fz};
|
||||
case DOWN -> new double[] {fx, 1 - fz};
|
||||
case NORTH -> new double[] {1 - fx, 1 - fy};
|
||||
case SOUTH -> new double[] {fx, 1 - fy};
|
||||
case WEST -> new double[] {fz, 1 - fy};
|
||||
case EAST -> new double[] {1 - fz, 1 - fy};
|
||||
};
|
||||
}
|
||||
|
||||
private static Direction faceFor(int axis, boolean negFace) {
|
||||
return switch (axis) {
|
||||
case 0 -> negFace ? Direction.WEST : Direction.EAST;
|
||||
|
||||
@@ -51,6 +51,10 @@ public class PPTest extends JavaPlugin {
|
||||
scenes.put("rotated", new Scene("rotated", this::rotated, 3, 4.5, -4, 3, 0.5, 2));
|
||||
scenes.put("rotated2", new Scene("rotated2", this::rotated, -0.5, 3.5, 9.5, -0.5, 0.5, 5));
|
||||
scenes.put("redstone", new Scene("redstone", this::redstone, 3, 3, -3, 3, 0, 2));
|
||||
scenes.put("piston", new Scene("piston", this::piston, 4.5, 3.2, -3.5, 5, 1, 1));
|
||||
scenes.put("pistonback", new Scene("pistonback", this::piston, 5.5, 3.2, 5.5, 5, 1, 1));
|
||||
scenes.put("pistondown", new Scene("pistondown", this::piston, -1.6, 2.6, -1.6, 0.5, 1.4, 0.5));
|
||||
scenes.put("pistonup", new Scene("pistonup", this::piston, 0.4, 2.6, -1.6, 2.5, 1.4, 0.5));
|
||||
scenes.put("vehicles", new Scene("vehicles", this::vehicles, 5, 3, -7, 5, 0.6, 1.5));
|
||||
}
|
||||
|
||||
@@ -442,6 +446,22 @@ public class PPTest extends JavaPlugin {
|
||||
put.accept(new int[] {4, 0, 3}, "blue_stained_glass_pane[west=true,north=true]");
|
||||
}
|
||||
|
||||
/** x/y blockstate rotations without uvlock: pistons in all six directions, observers, dispensers, logs. */
|
||||
private void piston(Location o) {
|
||||
String[] f = {"down", "up", "north", "south", "east", "west"};
|
||||
for (int i = 0; i < f.length; i++) {
|
||||
o.clone().add(i * 2, 1, 0).getBlock().setBlockData(
|
||||
Bukkit.createBlockData("minecraft:piston[facing=" + f[i] + ",extended=false]"), false);
|
||||
o.clone().add(i * 2, 1, 2).getBlock().setBlockData(
|
||||
Bukkit.createBlockData("minecraft:observer[facing=" + f[i] + "]"), false);
|
||||
o.clone().add(i * 2, 0, 2).getBlock().setBlockData(
|
||||
Bukkit.createBlockData("minecraft:dispenser[facing=" + f[i] + "]"), false);
|
||||
}
|
||||
o.clone().add(0, 0, -2).getBlock().setBlockData(Bukkit.createBlockData("minecraft:oak_log[axis=x]"), false);
|
||||
o.clone().add(2, 0, -2).getBlock().setBlockData(Bukkit.createBlockData("minecraft:oak_log[axis=z]"), false);
|
||||
o.clone().add(4, 0, -2).getBlock().setBlockData(Bukkit.createBlockData("minecraft:furnace[facing=north]"), false);
|
||||
}
|
||||
|
||||
private void vehicles(Location o) {
|
||||
spawn(o, 0, 0, 0, 0, EntityType.CHEST_MINECART);
|
||||
spawn(o, 1.8, 0, 0, 90, EntityType.FURNACE_MINECART);
|
||||
|
||||
Reference in new issue
Block a user