apply vanilla uvlock under blockstate x/y rotations and face zero-thickness elements towards the ray
x rotations without uvlock already sampled the model-space element like vanilla (lying logs/pillars, vertical observers/dispensers verified against FaceBakery for every blockstate variant). The remaining divergence was uvlock: faces kept model-extent UVs and were sampled in world orientation, so top stairs, ceiling buttons, fences/walls and ceiling vines showed shifted or rotated textures. Rotated uvlocked elements now also go through the model-space path, with each face's final UVs mapped by vanilla's BlockMath face transformation (inverted, as in FaceBakery). Zero-thickness elements (vine, lichen) always reported their low face; pick the face turned towards the ray instead. Edge-case test plugin: xrot/xrot2 scenes (lying pillars, vertical observers/dispensers/droppers, top stairs, ceiling buttons).
This commit is contained in:
1 parent
5852d4b367
commit
a0b7ec3a90
6 files changed
+123
-13
No files matched your search
@@ -96,9 +96,13 @@ public final class ModelBaker {
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
// Rotated variants keep the model-space element; intersectors hit it with the inverse-rotated ray
|
||||
// (vanilla bakes the same way). Without uvlock the textures thus turn with the model; with uvlock
|
||||
// each face's final UVs are additionally transformed so the texture stays world-aligned.
|
||||
Element unrotated = null;
|
||||
if (xSteps != 0 || ySteps != 0) {
|
||||
unrotated = variant.uvlock() ? uvLocked(modelElement, xSteps, ySteps) : modelElement;
|
||||
}
|
||||
baked.add(new Element(from, to, faces, rotOrigin, rotAxis, rotAngle, rescale, unrotated, xSteps, ySteps));
|
||||
|
||||
// Accumulate average color from the element's face textures.
|
||||
@@ -149,6 +153,50 @@ public final class ModelBaker {
|
||||
};
|
||||
}
|
||||
|
||||
/** The model-space element with every face's UVs locked to the world under the given rotation. */
|
||||
private static Element uvLocked(Element e, int xSteps, int ySteps) {
|
||||
Face[] locked = new Face[6];
|
||||
for (Direction d : Direction.values()) {
|
||||
Face f = e.faces[d.ordinal()];
|
||||
if (f != null) locked[d.ordinal()] = f.withUvLock(uvLockTransform(d, xSteps, ySteps));
|
||||
}
|
||||
return new Element(e.from, e.to, locked, e.rotOrigin, e.rotAxis, e.rotAngleRad, e.rescale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Vanilla uvlock ({@code BlockMath.getFaceTransformation}, inverted as in {@code FaceBakery}): the final
|
||||
* texture coordinates of model face {@code d}, centred on the texture, are mapped by
|
||||
* {@code M = G2L(d) · R⁻¹ · L2G(D)} where R is the blockstate rotation, D the world face {@code d} turns into,
|
||||
* and L2G the per-face UV frames (south = identity). For side faces this keeps the texture world-aligned;
|
||||
* up/down under x rotations follow vanilla's frames (not strictly world-aligned). M is one of the 8 symmetries
|
||||
* of the unit UV square, returned as the affine of {@link Face#uvLock}.
|
||||
*/
|
||||
static double[] uvLockTransform(Direction d, int xSteps, int ySteps) {
|
||||
Direction world = d.rotateX(xSteps).rotateY(ySteps);
|
||||
double[] cu = lockAxis(d, world, 1, 0, xSteps, ySteps);
|
||||
double[] cv = lockAxis(d, world, 0, 1, xSteps, ySteps);
|
||||
// u' = ½ + cu.x (u-½) + cv.x (v-½), v' = ½ + cu.y (u-½) + cv.y (v-½)
|
||||
return new double[] {0.5 - 0.5 * (cu[0] + cv[0]), cu[0], cv[0], 0.5 - 0.5 * (cu[1] + cv[1]), cu[1], cv[1]};
|
||||
}
|
||||
|
||||
private static double[] lockAxis(Direction d, Direction world, double u, double v, int xSteps, int ySteps) {
|
||||
double[] w = uvFrame(world, u, v, 0, false);
|
||||
double[] m = Element.unrotate(w[0], w[1], w[2], xSteps, ySteps, false);
|
||||
return uvFrame(d, m[0], m[1], m[2], true);
|
||||
}
|
||||
|
||||
/** Vanilla {@code BlockMath.VANILLA_UV_TRANSFORM_LOCAL_TO_GLOBAL} (or its inverse) applied to a vector. */
|
||||
private static double[] uvFrame(Direction d, double x, double y, double z, boolean inverse) {
|
||||
return switch (d) {
|
||||
case SOUTH -> new double[] {x, y, z};
|
||||
case NORTH -> new double[] {-x, y, -z}; // rotY(180°)
|
||||
case EAST -> inverse ? new double[] {-z, y, x} : new double[] {z, y, -x}; // rotY(90°)
|
||||
case WEST -> inverse ? new double[] {z, y, -x} : new double[] {-z, y, x}; // rotY(-90°)
|
||||
case UP -> inverse ? new double[] {x, -z, y} : new double[] {x, z, -y}; // rotX(-90°)
|
||||
case DOWN -> inverse ? new double[] {x, z, -y} : new double[] {x, -z, y}; // rotX(90°)
|
||||
};
|
||||
}
|
||||
|
||||
private int[][] resolveTexture(String ref, Map<String, String> vars) {
|
||||
if (ref == null) return null;
|
||||
String current = ref;
|
||||
|
||||
@@ -19,9 +19,10 @@ 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.
|
||||
// Blockstate rotation: the model-space element and the 90° steps (x first, then y) that turn it into this
|
||||
// world-space one. Intersectors hit the unrotated element with the inverse-rotated ray, so textures stay
|
||||
// bound to the model (vanilla); under uvlock its faces carry a UV transform keeping them world-aligned.
|
||||
// Null/0 when unrotated.
|
||||
public final Element unrotated;
|
||||
public final int xSteps;
|
||||
public final int ySteps;
|
||||
|
||||
@@ -11,7 +11,17 @@ public final class Face {
|
||||
public final int rotation; // 0/90/180/270, applied to the sampled UV
|
||||
public final int tintIndex; // -1 = no tint
|
||||
|
||||
// uvlock: affine applied to the final texture (u, v) as {a, b, c, d, e, f} with u' = a + b*u + c*v and
|
||||
// v' = d + e*u + f*v (one of the 8 symmetries of the unit square), null when not locked.
|
||||
public final double[] uvLock;
|
||||
|
||||
public Face(int[][] texture, double u1, double v1, double u2, double v2, int rotation, int tintIndex) {
|
||||
this(texture, u1, v1, u2, v2, rotation, tintIndex, null);
|
||||
}
|
||||
|
||||
public Face(
|
||||
int[][] texture, double u1, double v1, double u2, double v2, int rotation, int tintIndex, double[] uvLock) {
|
||||
this.uvLock = uvLock;
|
||||
this.texture = texture;
|
||||
this.u1 = u1;
|
||||
this.v1 = v1;
|
||||
@@ -21,6 +31,16 @@ public final class Face {
|
||||
this.tintIndex = tintIndex;
|
||||
}
|
||||
|
||||
/** This face with another texture and tint index (UVs, rotation and uvlock kept). */
|
||||
public Face withTexture(int[][] texture, int tintIndex) {
|
||||
return new Face(texture, u1, v1, u2, v2, rotation, tintIndex, uvLock);
|
||||
}
|
||||
|
||||
/** This face with its final texture coordinates transformed by a uvlock affine (see {@link #uvLock}). */
|
||||
public Face withUvLock(double[] lock) {
|
||||
return new Face(texture, u1, v1, u2, v2, rotation, tintIndex, lock);
|
||||
}
|
||||
|
||||
/**
|
||||
* Samples the ARGB pixel for a position within the face, where {@code (s, t)} are in 0..1 across
|
||||
* the face's two in-plane axes (s = horizontal, t = vertical, top-left origin).
|
||||
@@ -48,6 +68,11 @@ public final class Face {
|
||||
|
||||
double u = u1 + (u2 - u1) * rs;
|
||||
double v = v1 + (v2 - v1) * rt;
|
||||
if (uvLock != null) {
|
||||
double lu = uvLock[0] + uvLock[1] * u + uvLock[2] * v;
|
||||
v = uvLock[3] + uvLock[4] * u + uvLock[5] * v;
|
||||
u = lu;
|
||||
}
|
||||
|
||||
int h = texture.length;
|
||||
if (h == 0) return 0;
|
||||
|
||||
@@ -110,8 +110,8 @@ public final class ItemGeometry {
|
||||
ResolvedModel model = registry.get(data);
|
||||
if (model == null || !model.hasGeometry) return;
|
||||
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.
|
||||
// Rotated: bake the model-space element turned by the blockstate rotation, so textures stay bound
|
||||
// to the model like vanilla (uvlocked faces carry their UV transform); otherwise the 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];
|
||||
@@ -468,6 +468,6 @@ public final class ItemGeometry {
|
||||
return copy;
|
||||
});
|
||||
}
|
||||
return new Face(t, f.u1, f.v1, f.u2, f.v2, f.rotation, -1);
|
||||
return f.withTexture(t, -1);
|
||||
}
|
||||
}
|
||||
@@ -45,8 +45,8 @@ public final class ElementIntersector {
|
||||
Element element = model.elements.get(i);
|
||||
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.
|
||||
// Blockstate rotation: 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 (uvlock: see Face#uvLock).
|
||||
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);
|
||||
@@ -130,12 +130,13 @@ public final class ElementIntersector {
|
||||
double inv = 1.0 / d[a];
|
||||
double t1 = (from[a] - o[a]) * inv;
|
||||
double t2 = (to[a] - o[a]) * inv;
|
||||
boolean neg = true;
|
||||
// Entered through the low face when travelling +axis. Decided by the direction, not t1 > t2, so
|
||||
// zero-thickness elements (vines, lichen: from == to) show the face turned towards the ray.
|
||||
boolean neg = inv > 0;
|
||||
if (t1 > t2) {
|
||||
double tmp = t1;
|
||||
t1 = t2;
|
||||
t2 = tmp;
|
||||
neg = false;
|
||||
}
|
||||
if (t1 > tmin) {
|
||||
tmin = t1;
|
||||
|
||||
@@ -56,6 +56,9 @@ public class PPTest extends JavaPlugin {
|
||||
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));
|
||||
// Blockstate x rotations (+ x/y combined, uvlock): lying pillars, vertical observers/dispensers, top stairs.
|
||||
scenes.put("xrot", new Scene("xrot", this::xrot, 3.5, 5.5, -4.5, 6, 0.5, 3.5));
|
||||
scenes.put("xrot2", new Scene("xrot2", this::xrot, 11, 5, 10.5, 6, 0.5, 3.5));
|
||||
// Chests: same layout seen from all four sides (single/double per facing, trapped, ender, copper).
|
||||
scenes.put("chests_s", new Scene("chests_s", this::chests, 4.5, 5, 13, 4.5, 0, 3));
|
||||
scenes.put("chests_n", new Scene("chests_n", this::chests, 4.5, 5, -7, 4.5, 0, 3));
|
||||
@@ -491,6 +494,38 @@ public class PPTest extends JavaPlugin {
|
||||
o.clone().add(4, 0, -2).getBlock().setBlockData(Bukkit.createBlockData("minecraft:furnace[facing=north]"), false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Row z=0: oak log, basalt, quartz pillar, hay bale lying along x (x=90,y=90) and z (x=90). Row z=3: observer,
|
||||
* dispenser, dropper facing up and down. Row z=6: oak and stone-brick stairs half=top in all four facings
|
||||
* (x=180 + uvlock), oak buttons on the ceiling. A stone column at z=-2 marks north.
|
||||
*/
|
||||
private void xrot(Location o) {
|
||||
java.util.function.BiConsumer<int[], String> put = (p, data) -> o.clone().add(p[0], p[1], p[2]).getBlock()
|
||||
.setBlockData(Bukkit.createBlockData("minecraft:" + data), false);
|
||||
String[] pillars = {"oak_log", "basalt", "quartz_pillar", "hay_block"};
|
||||
for (int i = 0; i < pillars.length; i++) {
|
||||
put.accept(new int[] {i * 3, 0, 0}, pillars[i] + "[axis=x]");
|
||||
put.accept(new int[] {i * 3 + 1, 0, 0}, pillars[i] + "[axis=z]");
|
||||
}
|
||||
String[] vertical = {"observer", "dispenser", "dropper"};
|
||||
for (int i = 0; i < vertical.length; i++) {
|
||||
put.accept(new int[] {i * 3, 0, 3}, vertical[i] + "[facing=up]");
|
||||
put.accept(new int[] {i * 3 + 1, 1, 3}, vertical[i] + "[facing=down]");
|
||||
put.accept(new int[] {i * 3 + 1, 0, 3}, "glass");
|
||||
}
|
||||
String[] f = {"north", "east", "south", "west"};
|
||||
for (int i = 0; i < f.length; i++) {
|
||||
put.accept(new int[] {i * 2, 0, 6}, "oak_stairs[facing=" + f[i] + ",half=top]");
|
||||
put.accept(new int[] {i * 2 + 1, 0, 6}, "stone_brick_stairs[facing=" + f[i] + ",half=top]");
|
||||
}
|
||||
put.accept(new int[] {9, 1, 6}, "stone");
|
||||
put.accept(new int[] {9, 0, 6}, "oak_button[face=ceiling,facing=north]");
|
||||
put.accept(new int[] {10, 1, 6}, "stone");
|
||||
put.accept(new int[] {10, 0, 6}, "oak_button[face=ceiling,facing=east]");
|
||||
put.accept(new int[] {0, 0, -2}, "stone");
|
||||
put.accept(new int[] {0, 1, -2}, "stone");
|
||||
}
|
||||
|
||||
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