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)); } }