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;
|
||||
|
||||
Reference in new issue
Block a user