diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/TQuad.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/TQuad.java index bdf230a6ac..8d0ae60c73 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/TQuad.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/TQuad.java @@ -25,37 +25,42 @@ public class TQuad { private ModelQuadFacing facing; private final float[] extents; + private float[] vertexPositions; private final int packedNormal; - private float dotProduct; + private final float accurateDotProduct; + private float quantizedDotProduct; private Vector3fc center; // null on aligned quads private Vector3fc quantizedNormal; + private Vector3fc accurateNormal; - private TQuad(ModelQuadFacing facing, float[] extents, Vector3fc center, int packedNormal) { + private TQuad(ModelQuadFacing facing, float[] extents, float[] vertexPositions, Vector3fc center, int packedNormal) { this.facing = facing; this.extents = extents; + this.vertexPositions = vertexPositions; this.center = center; this.packedNormal = packedNormal; if (this.facing.isAligned()) { - this.dotProduct = getAlignedDotProduct(this.facing, this.extents); + this.accurateDotProduct = getAlignedDotProduct(this.facing, this.extents); } else { float normX = NormI8.unpackX(this.packedNormal); float normY = NormI8.unpackY(this.packedNormal); float normZ = NormI8.unpackZ(this.packedNormal); - this.dotProduct = this.getCenter().dot(normX, normY, normZ); + this.accurateDotProduct = this.getCenter().dot(normX, normY, normZ); } + this.quantizedDotProduct = this.accurateDotProduct; } private static float getAlignedDotProduct(ModelQuadFacing facing, float[] extents) { return extents[facing.ordinal()] * facing.getSign(); } - static TQuad fromAligned(ModelQuadFacing facing, float[] extents, Vector3fc center) { - return new TQuad(facing, extents, center, ModelQuadFacing.PACKED_ALIGNED_NORMALS[facing.ordinal()]); + static TQuad fromAligned(ModelQuadFacing facing, float[] extents, float[] vertexPositions, Vector3fc center) { + return new TQuad(facing, extents, vertexPositions, center, ModelQuadFacing.PACKED_ALIGNED_NORMALS[facing.ordinal()]); } - static TQuad fromUnaligned(ModelQuadFacing facing, float[] extents, Vector3fc center, int packedNormal) { - return new TQuad(facing, extents, center, packedNormal); + static TQuad fromUnaligned(ModelQuadFacing facing, float[] extents, float[] vertexPositions, Vector3fc center, int packedNormal) { + return new TQuad(facing, extents, vertexPositions, center, packedNormal); } public ModelQuadFacing getFacing() { @@ -73,9 +78,9 @@ public ModelQuadFacing useQuantizedFacing() { this.getQuantizedNormal(); this.facing = ModelQuadFacing.fromNormal(this.quantizedNormal.x(), this.quantizedNormal.y(), this.quantizedNormal.z()); if (this.facing.isAligned()) { - this.dotProduct = getAlignedDotProduct(this.facing, this.extents); + this.quantizedDotProduct = getAlignedDotProduct(this.facing, this.extents); } else { - this.dotProduct = this.getCenter().dot(this.quantizedNormal); + this.quantizedDotProduct = this.getCenter().dot(this.quantizedNormal); } } @@ -86,6 +91,31 @@ public float[] getExtents() { return this.extents; } + public float[] getVertexPositions() { + // calculate vertex positions from extents if there's no cached value + // (we don't want to be preemptively collecting vertex positions for all aligned quads) + if (this.vertexPositions == null) { + this.vertexPositions = new float[12]; + + var facingAxis = this.facing.getAxis(); + var xRange = facingAxis == 0 ? 0 : 3; + var yRange = facingAxis == 1 ? 0 : 3; + var zRange = facingAxis == 2 ? 0 : 3; + + var itemIndex = 0; + for (int x = 0; x <= xRange; x += 3) { + for (int y = 0; y <= yRange; y += 3) { + for (int z = 0; z <= zRange; z += 3) { + this.vertexPositions[itemIndex++] = this.extents[x]; + this.vertexPositions[itemIndex++] = this.extents[y + 1]; + this.vertexPositions[itemIndex++] = this.extents[z + 2]; + } + } + } + } + return this.vertexPositions; + } + public Vector3fc getCenter() { // calculate aligned quad center on demand if (this.center == null) { @@ -97,8 +127,12 @@ public Vector3fc getCenter() { return this.center; } - public float getDotProduct() { - return this.dotProduct; + public float getAccurateDotProduct() { + return this.accurateDotProduct; + } + + public float getQuantizedDotProduct() { + return this.quantizedDotProduct; } public int getPackedNormal() { @@ -116,6 +150,20 @@ public Vector3fc getQuantizedNormal() { return this.quantizedNormal; } + public Vector3fc getAccurateNormal() { + if (this.facing.isAligned()) { + return this.facing.getAlignedNormal(); + } else { + if (this.accurateNormal == null) { + this.accurateNormal = new Vector3f( + NormI8.unpackX(this.packedNormal), + NormI8.unpackY(this.packedNormal), + NormI8.unpackZ(this.packedNormal)); + } + return this.accurateNormal; + } + } + private void computeQuantizedNormal() { float normX = NormI8.unpackX(this.packedNormal); float normY = NormI8.unpackY(this.packedNormal); @@ -150,7 +198,7 @@ int getQuadHash() { } else { result = 31 * result + this.packedNormal; } - result = 31 * result + Float.hashCode(this.dotProduct); + result = 31 * result + Float.hashCode(this.quantizedDotProduct); return result; } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/TranslucentGeometryCollector.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/TranslucentGeometryCollector.java index 6edd1314e7..2125b23b45 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/TranslucentGeometryCollector.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/TranslucentGeometryCollector.java @@ -116,7 +116,7 @@ public void appendQuad(int packedNormal, ChunkVertexEncoder.Vertex[] vertices, M float lastX = vertices[3].x; float lastY = vertices[3].y; float lastZ = vertices[3].z; - int uniqueQuads = 0; + int uniqueVertexes = 0; float posXExtent = Float.NEGATIVE_INFINITY; float posYExtent = Float.NEGATIVE_INFINITY; @@ -141,7 +141,7 @@ public void appendQuad(int packedNormal, ChunkVertexEncoder.Vertex[] vertices, M xSum += x; ySum += y; zSum += z; - uniqueQuads++; + uniqueVertexes++; } if (i != 3) { lastX = x; @@ -185,13 +185,38 @@ public void appendQuad(int packedNormal, ChunkVertexEncoder.Vertex[] vertices, M } Vector3fc center = null; - if (!facing.isAligned() || uniqueQuads != 4) { - var centerX = xSum / uniqueQuads; - var centerY = ySum / uniqueQuads; - var centerZ = zSum / uniqueQuads; + if (!facing.isAligned() || uniqueVertexes != 4) { + var centerX = xSum / uniqueVertexes; + var centerY = ySum / uniqueVertexes; + var centerZ = zSum / uniqueVertexes; center = new Vector3f(centerX, centerY, centerZ); } + // check if we need to store vertex positions for this quad, only necessary if it's unaligned or rotated (yet aligned) + var needsVertexPositions = uniqueVertexes != 4 || !facing.isAligned(); + if (!needsVertexPositions) { + for (int i = 0; i < 4; i++) { + var vertex = vertices[i]; + if (vertex.x != posYExtent && vertex.x != negYExtent || + vertex.y != posZExtent && vertex.y != negZExtent || + vertex.z != posXExtent && vertex.z != negXExtent) { + needsVertexPositions = true; + break; + } + } + } + + float[] vertexPositions = null; + if (needsVertexPositions) { + vertexPositions = new float[12]; + for (int i = 0, itemIndex = 0; i < 4; i++) { + var vertex = vertices[i]; + vertexPositions[itemIndex++] = vertex.x; + vertexPositions[itemIndex++] = vertex.y; + vertexPositions[itemIndex++] = vertex.z; + } + } + if (facing.isAligned()) { // only update global extents if there are no unaligned quads since this is only // used for the convex box test which doesn't work with unaligned quads anyway @@ -204,11 +229,11 @@ public void appendQuad(int packedNormal, ChunkVertexEncoder.Vertex[] vertices, M this.extents[5] = Math.min(this.extents[5], negZExtent); } - var quad = TQuad.fromAligned(facing, extents, center); + var quad = TQuad.fromAligned(facing, extents, vertexPositions, center); quadList.add(quad); var extreme = this.alignedExtremes[direction]; - var distance = quad.getDotProduct(); + var distance = quad.getAccurateDotProduct(); // check if this is a new dot product for this distance var existingExtreme = this.alignedExtremes[direction]; @@ -225,11 +250,11 @@ public void appendQuad(int packedNormal, ChunkVertexEncoder.Vertex[] vertices, M } else { this.hasUnaligned = true; - var quad = TQuad.fromUnaligned(facing, extents, center, packedNormal); + var quad = TQuad.fromUnaligned(facing, extents, vertexPositions, center, packedNormal); quadList.add(quad); // update the two unaligned normals that are tracked - var distance = quad.getDotProduct(); + var distance = quad.getAccurateDotProduct(); if (packedNormal == this.unalignedANormal) { if (Float.isNaN(this.unalignedADistance1)) { this.unalignedADistance1 = distance; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/bsp_tree/BSPNode.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/bsp_tree/BSPNode.java index 20a48780d1..6362cbc07f 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/bsp_tree/BSPNode.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/bsp_tree/BSPNode.java @@ -65,7 +65,7 @@ private static boolean doubleLeafPossible(TQuad quadA, TQuad quadB) { // opposite normal (distance irrelevant) if (NormI8.isOpposite(packedNormalA, packedNormalB) // same normal and same distance - || packedNormalA == packedNormalB && quadA.getDotProduct() == quadB.getDotProduct()) { + || packedNormalA == packedNormalB && quadA.getAccurateDotProduct() == quadB.getAccurateDotProduct()) { return true; } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/bsp_tree/InnerPartitionBSPNode.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/bsp_tree/InnerPartitionBSPNode.java index f56c9a501d..3ee6572093 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/bsp_tree/InnerPartitionBSPNode.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/bsp_tree/InnerPartitionBSPNode.java @@ -540,7 +540,7 @@ static private BSPNode buildSNRLeafNodeFromQuads(BSPWorkspace workspace, IntArra for (int i = 0; i < indexes.size(); i++) { var quadIndex = indexes.getInt(i); - keys[i] = MathUtil.floatToComparableInt(workspace.quads[quadIndex].getDotProduct()); + keys[i] = MathUtil.floatToComparableInt(workspace.quads[quadIndex].getAccurateDotProduct()); } quadIndexes = RadixSort.sort(keys); @@ -553,7 +553,7 @@ static private BSPNode buildSNRLeafNodeFromQuads(BSPWorkspace workspace, IntArra for (int i = 0; i < indexes.size(); i++) { var quadIndex = indexes.getInt(i); - int dotProductComponent = MathUtil.floatToComparableInt(workspace.quads[quadIndex].getDotProduct()); + int dotProductComponent = MathUtil.floatToComparableInt(workspace.quads[quadIndex].getAccurateDotProduct()); sortData[i] = (long) dotProductComponent << 32 | quadIndex; } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/DynamicTopoData.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/DynamicTopoData.java index 7b5fc1e491..200ec74a93 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/DynamicTopoData.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/DynamicTopoData.java @@ -1,7 +1,6 @@ package net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data; import it.unimi.dsi.fastutil.objects.Object2ReferenceOpenHashMap; -import net.caffeinemc.mods.sodium.client.render.chunk.data.BuiltSectionMeshParts; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.TQuad; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.trigger.GeometryPlanes; import net.caffeinemc.mods.sodium.client.util.sorting.RadixSort; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/StaticNormalRelativeData.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/StaticNormalRelativeData.java index e0f253697f..888ccd2034 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/StaticNormalRelativeData.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/StaticNormalRelativeData.java @@ -1,6 +1,5 @@ package net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data; -import net.caffeinemc.mods.sodium.client.render.chunk.data.BuiltSectionMeshParts; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.SortType; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.TQuad; import net.caffeinemc.mods.sodium.client.util.MathUtil; @@ -50,7 +49,7 @@ private static StaticNormalRelativeData fromDoubleUnaligned(int[] vertexCounts, final var keys = new int[quads.length]; for (int q = 0; q < quads.length; q++) { - keys[q] = MathUtil.floatToComparableInt(quads[q].getDotProduct()); + keys[q] = MathUtil.floatToComparableInt(quads[q].getAccurateDotProduct()); } var indices = RadixSort.sort(keys); @@ -62,7 +61,7 @@ private static StaticNormalRelativeData fromDoubleUnaligned(int[] vertexCounts, final var sortData = new long[quads.length]; for (int q = 0; q < quads.length; q++) { - int dotProductComponent = MathUtil.floatToComparableInt(quads[q].getDotProduct()); + int dotProductComponent = MathUtil.floatToComparableInt(quads[q].getAccurateDotProduct()); sortData[q] = (long) dotProductComponent << 32 | q; } @@ -116,7 +115,7 @@ private static StaticNormalRelativeData fromMixed(int[] vertexCounts, final var keys = new int[count]; for (int q = 0; q < count; q++) { - keys[q] = MathUtil.floatToComparableInt(quads[quadIndex++].getDotProduct()); + keys[q] = MathUtil.floatToComparableInt(quads[quadIndex++].getAccurateDotProduct()); } var indices = RadixSort.sort(keys); @@ -127,7 +126,7 @@ private static StaticNormalRelativeData fromMixed(int[] vertexCounts, } else { for (int i = 0; i < count; i++) { var quad = quads[quadIndex++]; - int dotProductComponent = MathUtil.floatToComparableInt(quad.getDotProduct()); + int dotProductComponent = MathUtil.floatToComparableInt(quad.getAccurateDotProduct()); sortData[i] = (long) dotProductComponent << 32 | i; } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/TopoGraphSorting.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/TopoGraphSorting.java index d45af2c756..eb8e9b3dea 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/TopoGraphSorting.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/TopoGraphSorting.java @@ -19,6 +19,8 @@ * significantly more robust topo sorting. */ public class TopoGraphSorting { + private static final float HALF_SPACE_EPSILON = 0.001f; + private TopoGraphSorting() { } @@ -35,8 +37,36 @@ private static boolean pointOutsideHalfSpace(float planeDistance, Vector3fc plan return planeNormal.dot(point) > planeDistance; } - private static boolean pointInsideHalfSpace(float planeDistance, Vector3fc planeNormal, Vector3fc point) { - return planeNormal.dot(point) < planeDistance; + /** + * Test if the given point is within the half space defined by the plane anchor and the plane normal. The normal points away from the space considered to be inside. + *

+ * A small epsilon is added in the test to account for floating point errors, making it harder for a point on the edge to be considered inside. + * + * @param planeDistance dot product of the plane + * @param planeNormal the normal of the plane + * @param x x coordinate of the point + * @param y y coordinate of the point + * @param z z coordinate of the point + * @return true if the point is inside the half space + */ + private static boolean pointInsideHalfSpaceEpsilon(float planeDistance, Vector3fc planeNormal, float x, float y, float z) { + return planeNormal.dot(x, y, z) + HALF_SPACE_EPSILON < planeDistance; + } + + /** + * Test if the given point is outside the half space defined by the plane anchor and the plane normal. The normal points away from the space considered to be inside. + *

+ * A small epsilon is subtracted in the test to account for floating point errors, making it harder for a point on the edge to be considered outside. + * + * @param planeDistance dot product of the plane + * @param planeNormal the normal of the plane + * @param x x coordinate of the point + * @param y y coordinate of the point + * @param z z coordinate of the point + * @return true if the point is inside the half space + */ + private static boolean pointOutsideHalfSpaceEpsilon(float planeDistance, Vector3fc planeNormal, float x, float y, float z) { + return planeNormal.dot(x, y, z) - HALF_SPACE_EPSILON > planeDistance; } public static boolean orthogonalQuadVisibleThrough(TQuad quadA, TQuad quadB) { @@ -127,12 +157,9 @@ private static boolean visibilityWithSeparator(TQuad quadA, TQuad quadB, * Checks if one quad is visible through the other quad. This accepts arbitrary * quads, even unaligned ones. * - * @param quad the quad through which the other quad is being - * tested + * @param quad the quad through which the other quad is being tested * @param other the quad being tested - * @param distancesByNormal a map of normals to sorted arrays of face plane - * distances for disproving that the quads are visible - * through each other, null to disable + * @param distancesByNormal a map of normals to sorted arrays of face plane distances for disproving that the quads are visible through each other, null to disable * @return true if the other quad is visible through the first quad */ private static boolean quadVisibleThrough(TQuad quad, TQuad other, @@ -141,11 +168,12 @@ private static boolean quadVisibleThrough(TQuad quad, TQuad other, return false; } - // aligned quads - var quadFacing = quad.useQuantizedFacing(); - var otherFacing = other.useQuantizedFacing(); - boolean result; + var quadFacing = quad.getFacing(); + var otherFacing = other.getFacing(); + boolean result = false; if (quadFacing != ModelQuadFacing.UNASSIGNED && otherFacing != ModelQuadFacing.UNASSIGNED) { + // aligned quads + // opposites never see each other if (quadFacing.getOpposite() == otherFacing) { return false; @@ -162,15 +190,45 @@ private static boolean quadVisibleThrough(TQuad quad, TQuad other, } } else { // at least one unaligned quad - // this is an approximation since our quads don't store all their vertices. - // check that other center is within the half space of quad and that quad isn't - // in the half space of other - result = pointInsideHalfSpace(quad.getDotProduct(), quad.getQuantizedNormal(), other.getCenter()) - && !pointInsideHalfSpace(other.getDotProduct(), other.getQuantizedNormal(), quad.getCenter()); + + var quadDot = quad.getAccurateDotProduct(); + var quadNormal = quad.getAccurateNormal(); + var otherVertexPositions = other.getVertexPositions(); + + // at least one of the other quad's vertexes must be inside the half space of the first quad + var otherInsideQuad = false; + for (int i = 0, itemIndex = 0; i < 4; i++) { + if (pointInsideHalfSpaceEpsilon(quadDot, quadNormal, + otherVertexPositions[itemIndex++], + otherVertexPositions[itemIndex++], + otherVertexPositions[itemIndex++])) { + otherInsideQuad = true; + break; + } + } + if (otherInsideQuad) { + var otherDot = other.getAccurateDotProduct(); + var otherNormal = other.getAccurateNormal(); + var quadVertexPositions = quad.getVertexPositions(); + + // not all the quad's vertexes must be inside the half space of the other quad + // i.e. there must be at least one vertex outside the other quad + var quadNotFullyInsideOther = false; + for (int i = 0, itemIndex = 0; i < 4; i++) { + if (pointOutsideHalfSpaceEpsilon(otherDot, otherNormal, + quadVertexPositions[itemIndex++], + quadVertexPositions[itemIndex++], + quadVertexPositions[itemIndex++])) { + quadNotFullyInsideOther = true; + break; + } + } + + result = quadNotFullyInsideOther; + } } - // if enabled and necessary, try to disprove this see-through relationship with - // a separator plane + // if enabled and necessary, try to disprove this see-through relationship with a separator plane if (result && distancesByNormal != null) { return visibilityWithSeparator(quad, other, distancesByNormal, cameraPos); } @@ -209,10 +267,7 @@ public static boolean topoGraphSort( for (int i = 0; i < allQuads.length; i++) { TQuad quad = allQuads[i]; - // NOTE: This approximation may introduce wrong sorting if the real and the - // quantized normal aren't the same. A quad may be ignored with the quantized - // normal, but it's actually visible in camera. - if (pointOutsideHalfSpace(quad.getDotProduct(), quad.getQuantizedNormal(), cameraPos)) { + if (pointOutsideHalfSpace(quad.getAccurateDotProduct(), quad.getAccurateNormal(), cameraPos)) { activeToRealIndex[quadCount] = i; quads[quadCount] = quad; quadCount++; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/trigger/GeometryPlanes.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/trigger/GeometryPlanes.java index 8f75c2db7d..16acdf0230 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/trigger/GeometryPlanes.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/trigger/GeometryPlanes.java @@ -92,9 +92,9 @@ public void addUnalignedPlane(SectionPos sectionPos, Vector3fc normal, float dis public void addQuadPlane(SectionPos sectionPos, TQuad quad) { var facing = quad.useQuantizedFacing(); if (facing.isAligned()) { - this.addAlignedPlane(sectionPos, facing.ordinal(), quad.getDotProduct()); + this.addAlignedPlane(sectionPos, facing.ordinal(), quad.getQuantizedDotProduct()); } else { - this.addUnalignedPlane(sectionPos, quad.getQuantizedNormal(), quad.getDotProduct()); + this.addUnalignedPlane(sectionPos, quad.getQuantizedNormal(), quad.getQuantizedDotProduct()); } }