From a99acad80c030619be0c01151532a61a2d037bee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Wed, 4 Nov 2026 13:20:40 +0100 Subject: [PATCH] fix blockstate y-rotation direction and rotate top/bottom UVs without uvlock Blockstate "y" rotations turned the geometry counter-clockwise while the faces were re-assigned clockwise (vanilla). Every east/west variant of an asymmetric block ended up mirrored on the wrong side of its cell: shelf mushrooms and ladders floated away from their support, stairs faced backwards. Rotate clockwise like vanilla, adjust element-rotation axes, and turn up/down face textures with the model unless uvlock is set. Edge-case test plugin: shelf mushroom (side + top view) and rotated-block scenes. --- .../pixelpics/assets/ModelBaker.java | 24 ++++++++++--- tools/edgecase-plugin/pptest/PPTest.java | 36 +++++++++++++++++++ 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/ModelBaker.java b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/ModelBaker.java index 2460789..d66f86c 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/ModelBaker.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/ModelBaker.java @@ -86,6 +86,12 @@ 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); @@ -162,10 +168,16 @@ 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) { - // (x,z) -> (z, 1-x): 90 deg clockwise viewed from above. + // 90 deg clockwise viewed from above (vanilla), see rotatePointY. double[] c1 = rotatePointY(from); double[] c2 = rotatePointY(to); return minMax(c1, c2); @@ -178,8 +190,10 @@ public final class ModelBaker { } private double[] rotatePointY(double[] p) { + // Vanilla blockstate "y": clockwise seen from above (north -> east, south -> west), i.e. a rotation + // by -90° about +Y: (dx, dz) -> (-dz, dx). Must match Direction.rotateY used for the faces. double x = p[0] - 0.5, z = p[2] - 0.5; - return new double[] {0.5 + z, p[1], 0.5 - x}; + return new double[] {0.5 - z, p[1], 0.5 + x}; } private double[] rotatePointX(double[] p) { @@ -214,10 +228,10 @@ public final class ModelBaker { private record AxisRotation(int axis, boolean flip) {} private AxisRotation rotateAxisY(int axis) { - // Y rotation maps x<->z; the y axis is unchanged. + // Clockwise Y rotation maps +x -> +z and +z -> -x; the y axis is unchanged. return switch (axis) { - case 0 -> new AxisRotation(2, true); // x -> z - case 2 -> new AxisRotation(0, false); // z -> x + case 0 -> new AxisRotation(2, false); // x -> z + case 2 -> new AxisRotation(0, true); // z -> -x default -> new AxisRotation(axis, false); }; } diff --git a/tools/edgecase-plugin/pptest/PPTest.java b/tools/edgecase-plugin/pptest/PPTest.java index 8419fc6..95e82be 100644 --- a/tools/edgecase-plugin/pptest/PPTest.java +++ b/tools/edgecase-plugin/pptest/PPTest.java @@ -46,6 +46,9 @@ public class PPTest extends JavaPlugin { scenes.put("pixelblock", new Scene("pixelblock", this::pixelblock, 1.5, 1.9, -1.3, 0.5, 0.5, 0.5)); scenes.put("misc", new Scene("misc", this::misc, 5, 3, -7, 5, 0.8, 1.5)); scenes.put("dbg", new Scene("dbg", this::dbg, 1, 1.6, -3, 1, 0.8, 0.5)); + scenes.put("shelf", new Scene("shelf", this::shelf, 3.5, 3.2, -2.5, 0.5, 1.5, 0.5)); + scenes.put("shelftop", new Scene("shelftop", this::shelf, 0.52, 6, 0.5, 0.5, 0, 0.5)); + scenes.put("rotated", new Scene("rotated", this::rotated, 3, 4.5, -4, 3, 0.5, 2)); scenes.put("vehicles", new Scene("vehicles", this::vehicles, 5, 3, -7, 5, 0.6, 1.5)); } @@ -380,6 +383,39 @@ public class PPTest extends JavaPlugin { getLogger().info("dbg text=" + t1.isValid() + " opacity=" + t1.getTextOpacity() + " shield=" + i4.getItemStack()); } + /** World blocks: a log column with shelf mushrooms (26.x) on all four sides, both growth stages. */ + private void shelf(Location o) { + for (int y = 0; y < 3; y++) o.clone().add(0, y, 0).getBlock().setType(Material.OAK_LOG, false); + o.clone().add(0, 0, -3).getBlock().setType(Material.DIAMOND_BLOCK, false); // north marker + o.clone().add(3, 0, 0).getBlock().setType(Material.GOLD_BLOCK, false); // east marker + String[] f = {"north", "south", "east", "west"}; + int[][] off = {{0, -1}, {0, 1}, {1, 0}, {-1, 0}}; + for (int i = 0; i < 4; i++) { + for (int age = 0; age < 2; age++) { + o.clone().add(off[i][0], 1 + age, off[i][1]).getBlock().setBlockData( + Bukkit.createBlockData("minecraft:shelf_mushroom[age=" + age + ",facing=" + f[i] + "]"), false); + } + } + } + + /** Blockstate y-rotations: ladders, stairs, wall torches, furnaces, logs on all sides of stone pillars. */ + private void rotated(Location o) { + String[] f = {"north", "south", "east", "west"}; + int[][] off = {{0, -1}, {0, 1}, {1, 0}, {-1, 0}}; + String[] kinds = {"ladder[facing=%s]", "wall_torch[facing=%s]", "oak_stairs[facing=%s]", "furnace[facing=%s]"}; + for (int k = 0; k < kinds.length; k++) { + Location c = o.clone().add(k * 3 - 1, 0, 2); + c.getBlock().setType(Material.STONE, false); + c.clone().add(0, 1, 0).getBlock().setType(Material.STONE, false); + for (int i = 0; i < 4; i++) { + c.clone().add(off[i][0], k == 2 || k == 3 ? 0 : 1, off[i][1]).getBlock().setBlockData( + Bukkit.createBlockData("minecraft:" + String.format(kinds[k], f[i])), false); + } + } + o.clone().add(9, 0, 2).getBlock().setBlockData(Bukkit.createBlockData("minecraft:oak_log[axis=x]"), false); + o.clone().add(9, 0, 4).getBlock().setBlockData(Bukkit.createBlockData("minecraft:oak_log[axis=z]"), 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);