diff --git a/projects/common/src/client/java/dan200/computercraft/client/model/turtle/ModelTransformer.java b/projects/common/src/client/java/dan200/computercraft/client/model/turtle/ModelTransformer.java
new file mode 100644
index 000000000..4c4cd829e
--- /dev/null
+++ b/projects/common/src/client/java/dan200/computercraft/client/model/turtle/ModelTransformer.java
@@ -0,0 +1,98 @@
+// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
+//
+// SPDX-License-Identifier: MPL-2.0
+
+package dan200.computercraft.client.model.turtle;
+
+import com.mojang.blaze3d.vertex.DefaultVertexFormat;
+import com.mojang.blaze3d.vertex.VertexFormat;
+import com.mojang.blaze3d.vertex.VertexFormatElement;
+import com.mojang.math.Transformation;
+import net.minecraft.client.renderer.block.model.BakedQuad;
+import net.minecraft.client.resources.model.BakedModel;
+import net.minecraft.core.Direction;
+import org.joml.Matrix4f;
+import org.joml.Vector4f;
+
+import javax.annotation.Nullable;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Applies a {@link Transformation} (or rather a {@link Matrix4f}) to a list of {@link BakedQuad}s.
+ *
+ * This does a little bit of magic compared with other system (i.e. Forge's {@code QuadTransformers}), as it needs to
+ * handle flipping models upside down.
+ *
+ * This is typically used with a {@link BakedModel} subclass - see the loader-specific projects.
+ */
+public final class ModelTransformer {
+ public static final int[] ORDER = new int[]{ 3, 2, 1, 0 };
+ public static final int STRIDE = DefaultVertexFormat.BLOCK.getIntegerSize();
+ private static final int POS_OFFSET = findOffset(DefaultVertexFormat.BLOCK, DefaultVertexFormat.ELEMENT_POSITION);
+
+ private final Matrix4f transformation;
+ private final boolean invert;
+ private @Nullable TransformedQuads cache;
+
+ public ModelTransformer(Transformation transformation) {
+ this.transformation = transformation.getMatrix();
+ invert = transformation.getMatrix().determinant() < 0;
+ }
+
+ public List transform(List quads) {
+ if (quads.isEmpty()) return List.of();
+
+ // We do some basic caching here to avoid recomputing every frame. Most turtle models don't have culled faces,
+ // so it's not worth being smarter here.
+ var cache = this.cache;
+ if (cache != null && quads.equals(cache.original())) return cache.transformed();
+
+ List transformed = new ArrayList<>(quads.size());
+ for (var quad : quads) transformed.add(transformQuad(quad));
+ this.cache = new TransformedQuads(quads, transformed);
+ return transformed;
+ }
+
+ private BakedQuad transformQuad(BakedQuad quad) {
+ var inputData = quad.getVertices();
+ var outputData = new int[inputData.length];
+ for (var i = 0; i < 4; i++) {
+ var inStart = STRIDE * i;
+ // Reverse the order of the quads if we're inverting
+ var outStart = STRIDE * (invert ? ORDER[i] : i);
+ System.arraycopy(inputData, inStart, outputData, outStart, STRIDE);
+
+ // Apply the matrix to our position
+ var inPosStart = inStart + POS_OFFSET;
+ var outPosStart = outStart + POS_OFFSET;
+
+ var x = Float.intBitsToFloat(inputData[inPosStart]);
+ var y = Float.intBitsToFloat(inputData[inPosStart + 1]);
+ var z = Float.intBitsToFloat(inputData[inPosStart + 2]);
+
+ // Transform the position
+ var pos = new Vector4f(x, y, z, 1);
+ transformation.transformProject(pos);
+
+ outputData[outPosStart] = Float.floatToRawIntBits(pos.x());
+ outputData[outPosStart + 1] = Float.floatToRawIntBits(pos.y());
+ outputData[outPosStart + 2] = Float.floatToRawIntBits(pos.z());
+ }
+
+ var direction = Direction.rotate(transformation, quad.getDirection());
+ return new BakedQuad(outputData, quad.getTintIndex(), direction, quad.getSprite(), quad.isShade());
+ }
+
+ private record TransformedQuads(List original, List transformed) {
+ }
+
+ private static int findOffset(VertexFormat format, VertexFormatElement element) {
+ var offset = 0;
+ for (var other : format.getElements()) {
+ if (other == element) return offset / Integer.BYTES;
+ offset += element.getByteSize();
+ }
+ throw new IllegalArgumentException("Cannot find " + element + " in " + format);
+ }
+}
diff --git a/projects/common/src/client/java/dan200/computercraft/client/render/TurtleBlockEntityRenderer.java b/projects/common/src/client/java/dan200/computercraft/client/render/TurtleBlockEntityRenderer.java
index eae29199e..70576eaef 100644
--- a/projects/common/src/client/java/dan200/computercraft/client/render/TurtleBlockEntityRenderer.java
+++ b/projects/common/src/client/java/dan200/computercraft/client/render/TurtleBlockEntityRenderer.java
@@ -10,6 +10,7 @@ import com.mojang.math.Axis;
import com.mojang.math.Transformation;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.turtle.TurtleSide;
+import dan200.computercraft.client.model.turtle.ModelTransformer;
import dan200.computercraft.client.platform.ClientPlatformHelper;
import dan200.computercraft.client.turtle.TurtleUpgradeModellers;
import dan200.computercraft.shared.computer.core.ComputerFamily;
@@ -30,6 +31,7 @@ import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.RandomSource;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.HitResult;
+import org.joml.Vector4f;
import javax.annotation.Nullable;
import java.util.List;
@@ -146,16 +148,30 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer quads, @Nullable int[] tints) {
var matrix = transform.last();
+ var inverted = matrix.pose().determinant() < 0;
for (var bakedquad : quads) {
var tint = -1;
@@ -167,7 +183,50 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer> 16 & 255) / 255.0F;
var g = (float) (tint >> 8 & 255) / 255.0F;
var b = (float) (tint & 255) / 255.0F;
- buffer.putBulkData(matrix, bakedquad, r, g, b, lightmapCoord, overlayLight);
+ if (inverted) {
+ putBulkQuadInvert(buffer, matrix, bakedquad, r, g, b, lightmapCoord, overlayLight);
+ } else {
+ buffer.putBulkData(matrix, bakedquad, r, g, b, lightmapCoord, overlayLight);
+ }
+ }
+ }
+
+ /**
+ * A version of {@link VertexConsumer#putBulkData(PoseStack.Pose, BakedQuad, float, float, float, int, int)} for
+ * when the matrix is inverted.
+ *
+ * @param buffer The buffer to draw to.
+ * @param pose The current matrix stack.
+ * @param quad The quad to draw.
+ * @param red The red tint of this quad.
+ * @param green The green tint of this quad.
+ * @param blue The blue tint of this quad.
+ * @param lightmapCoord The lightmap coordinate
+ * @param overlayLight The overlay light.
+ */
+ private static void putBulkQuadInvert(VertexConsumer buffer, PoseStack.Pose pose, BakedQuad quad, float red, float green, float blue, int lightmapCoord, int overlayLight) {
+ var matrix = pose.pose();
+ // It's a little dubious to transform using this matrix rather than the normal matrix. This mirrors the logic in
+ // Direction.rotate (so not out of nowhere!), but is a little suspicious.
+ var dirNormal = quad.getDirection().getNormal();
+ var normal = matrix.transform(new Vector4f(dirNormal.getX(), dirNormal.getY(), dirNormal.getZ(), 0.0f)).normalize();
+
+ var vertices = quad.getVertices();
+ for (var vertex : ModelTransformer.ORDER) {
+ var i = vertex * ModelTransformer.STRIDE;
+
+ var x = Float.intBitsToFloat(vertices[i]);
+ var y = Float.intBitsToFloat(vertices[i + 1]);
+ var z = Float.intBitsToFloat(vertices[i + 2]);
+ var transformed = matrix.transform(new Vector4f(x, y, z, 1));
+
+ var u = Float.intBitsToFloat(vertices[i + 4]);
+ var v = Float.intBitsToFloat(vertices[i + 5]);
+ buffer.vertex(
+ transformed.x(), transformed.y(), transformed.z(),
+ red, green, blue, 1.0F, u, v, overlayLight, lightmapCoord,
+ normal.x(), normal.y(), normal.z()
+ );
}
}
diff --git a/projects/fabric/src/client/java/dan200/computercraft/client/model/TransformedBakedModel.java b/projects/fabric/src/client/java/dan200/computercraft/client/model/TransformedBakedModel.java
index 9862101ff..01706cf90 100644
--- a/projects/fabric/src/client/java/dan200/computercraft/client/model/TransformedBakedModel.java
+++ b/projects/fabric/src/client/java/dan200/computercraft/client/model/TransformedBakedModel.java
@@ -4,84 +4,32 @@
package dan200.computercraft.client.model;
-import com.mojang.blaze3d.vertex.DefaultVertexFormat;
-import com.mojang.blaze3d.vertex.VertexFormat;
-import com.mojang.blaze3d.vertex.VertexFormatElement;
import com.mojang.math.Transformation;
+import dan200.computercraft.client.model.turtle.ModelTransformer;
import net.minecraft.client.renderer.block.model.BakedQuad;
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.core.Direction;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.block.state.BlockState;
-import org.joml.Matrix4f;
-import org.joml.Vector4f;
import javax.annotation.Nullable;
-import java.util.ArrayList;
import java.util.List;
/**
* A {@link BakedModel} which applies a transformation matrix to its underlying quads.
+ *
+ * @see ModelTransformer
*/
public class TransformedBakedModel extends CustomBakedModel {
- private static final int STRIDE = DefaultVertexFormat.BLOCK.getIntegerSize();
- private static final int POS_OFFSET = findOffset(DefaultVertexFormat.BLOCK, DefaultVertexFormat.ELEMENT_POSITION);
-
- private final Matrix4f transformation;
- private @Nullable TransformedQuads cache;
+ private final ModelTransformer transformation;
public TransformedBakedModel(BakedModel model, Transformation transformation) {
super(model);
- this.transformation = transformation.getMatrix();
+ this.transformation = new ModelTransformer(transformation);
}
@Override
public List getQuads(@Nullable BlockState blockState, @Nullable Direction face, RandomSource rand) {
- var cache = this.cache;
- var quads = wrapped.getQuads(blockState, face, rand);
- if (quads.isEmpty()) return List.of();
-
- // We do some basic caching here to avoid recomputing every frame. Most turtle models don't have culled faces,
- // so it's not worth being smarter here.
- if (cache != null && quads.equals(cache.original())) return cache.transformed();
-
- List transformed = new ArrayList<>(quads.size());
- for (var quad : quads) transformed.add(transformQuad(quad));
- this.cache = new TransformedQuads(quads, transformed);
- return transformed;
- }
-
- private BakedQuad transformQuad(BakedQuad quad) {
- var vertexData = quad.getVertices().clone();
- for (var i = 0; i < 4; i++) {
- // Apply the matrix to our position
- var start = STRIDE * i + POS_OFFSET;
-
- var x = Float.intBitsToFloat(vertexData[start]);
- var y = Float.intBitsToFloat(vertexData[start + 1]);
- var z = Float.intBitsToFloat(vertexData[start + 2]);
-
- // Transform the position
- var pos = new Vector4f(x, y, z, 1);
- transformation.transformProject(pos);
-
- vertexData[start] = Float.floatToRawIntBits(pos.x());
- vertexData[start + 1] = Float.floatToRawIntBits(pos.y());
- vertexData[start + 2] = Float.floatToRawIntBits(pos.z());
- }
-
- return new BakedQuad(vertexData, quad.getTintIndex(), quad.getDirection(), quad.getSprite(), quad.isShade());
- }
-
- private record TransformedQuads(List original, List transformed) {
- }
-
- private static int findOffset(VertexFormat format, VertexFormatElement element) {
- var offset = 0;
- for (var other : format.getElements()) {
- if (other == element) return offset / Integer.BYTES;
- offset += element.getByteSize();
- }
- throw new IllegalArgumentException("Cannot find " + element + " in " + format);
+ return transformation.transform(wrapped.getQuads(blockState, face, rand));
}
}
diff --git a/projects/forge/src/client/java/dan200/computercraft/client/model/TransformedBakedModel.java b/projects/forge/src/client/java/dan200/computercraft/client/model/TransformedBakedModel.java
index d1eecbbda..2d457fe52 100644
--- a/projects/forge/src/client/java/dan200/computercraft/client/model/TransformedBakedModel.java
+++ b/projects/forge/src/client/java/dan200/computercraft/client/model/TransformedBakedModel.java
@@ -5,6 +5,7 @@
package dan200.computercraft.client.model;
import com.mojang.math.Transformation;
+import dan200.computercraft.client.model.turtle.ModelTransformer;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.renderer.block.model.BakedQuad;
import net.minecraft.client.resources.model.BakedModel;
@@ -12,7 +13,6 @@ import net.minecraft.core.Direction;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraftforge.client.model.BakedModelWrapper;
-import net.minecraftforge.client.model.QuadTransformers;
import net.minecraftforge.client.model.data.ModelData;
import javax.annotation.Nullable;
@@ -20,16 +20,15 @@ import java.util.List;
/**
* A {@link BakedModel} which applies a transformation matrix to its underlying quads.
+ *
+ * @see ModelTransformer
*/
public class TransformedBakedModel extends BakedModelWrapper {
- private final Transformation transformation;
- private final boolean invert;
- private @Nullable TransformedQuads cache;
+ private final ModelTransformer transformation;
public TransformedBakedModel(BakedModel model, Transformation transformation) {
super(model);
- this.transformation = transformation;
- invert = transformation.getNormalMatrix().determinant() < 0;
+ this.transformation = new ModelTransformer(transformation);
}
@Override
@@ -39,19 +38,6 @@ public class TransformedBakedModel extends BakedModelWrapper {
@Override
public List getQuads(@Nullable BlockState state, @Nullable Direction side, RandomSource rand, ModelData extraData, @Nullable RenderType renderType) {
- var cache = this.cache;
- var quads = originalModel.getQuads(state, side, rand, extraData, renderType);
- if (quads.isEmpty()) return List.of();
-
- // We do some basic caching here to avoid recomputing every frame. Most turtle models don't have culled faces,
- // so it's not worth being smarter here.
- if (cache != null && quads.equals(cache.original())) return cache.transformed();
-
- var transformed = QuadTransformers.applying(transformation).process(quads);
- this.cache = new TransformedQuads(quads, transformed);
- return transformed;
- }
-
- private record TransformedQuads(List original, List transformed) {
+ return transformation.transform(originalModel.getQuads(state, side, rand, extraData, renderType));
}
}