diff --git a/CadRevealComposer.Tests/Operations/DrawCallEstimatorTests.cs b/CadRevealComposer.Tests/Operations/DrawCallEstimatorTests.cs index d73852c8..e124fdc2 100644 --- a/CadRevealComposer.Tests/Operations/DrawCallEstimatorTests.cs +++ b/CadRevealComposer.Tests/Operations/DrawCallEstimatorTests.cs @@ -28,6 +28,7 @@ public void ConeAndCylinder() var geometry = new APrimitive[] { new Cone( + Matrix4x4.Identity, 0f, 0f, Vector3.One, @@ -40,6 +41,7 @@ public void ConeAndCylinder() new BoundingBox(-Vector3.One, Vector3.One) ), new GeneralCylinder( + Matrix4x4.Identity, 0f, 0f, Vector3.One, @@ -64,6 +66,7 @@ public void SolidClosedGeneralConeTorusAndClosedCylinder() var geometry = new APrimitive[] { new Cone( + Matrix4x4.Identity, 0f, 0f, Vector3.One, @@ -85,6 +88,7 @@ public void SolidClosedGeneralConeTorusAndClosedCylinder() new BoundingBox(-Vector3.One, Vector3.One) ), new GeneralCylinder( + Matrix4x4.Identity, 0f, 0f, Vector3.One, diff --git a/CadRevealComposer.Tests/Utils/MeshTools/MeshToolsTests.cs b/CadRevealComposer.Tests/Utils/MeshTools/MeshToolsTests.cs index 734f9f31..285df5cd 100644 --- a/CadRevealComposer.Tests/Utils/MeshTools/MeshToolsTests.cs +++ b/CadRevealComposer.Tests/Utils/MeshTools/MeshToolsTests.cs @@ -1,6 +1,6 @@ namespace CadRevealComposer.Tests.Utils.MeshTools; -using CadRevealFbxProvider.BatchUtils; +using CadRevealComposer.Utils.MeshTools; using System.Numerics; using Tessellation; diff --git a/CadRevealComposer.Tests/Utils/MeshTools/VertexCacheOptimizerTests.cs b/CadRevealComposer.Tests/Utils/MeshTools/VertexCacheOptimizerTests.cs new file mode 100644 index 00000000..f0c6ca25 --- /dev/null +++ b/CadRevealComposer.Tests/Utils/MeshTools/VertexCacheOptimizerTests.cs @@ -0,0 +1,48 @@ +namespace CadRevealComposer.Tests; + +using CadRevealComposer.Utils.MeshTools; + +[TestFixture] +public class VertexCacheOptimizerTests +{ + [Test] + public void OptimizeVertexCacheFifo_WithValidInput_GivesValuesFromReferenceImplementation() + { + // @formatter:off + // csharpier-ignore-start + uint[] input = { + 0,1,2,3,1,0,6,5,0,0,2,6,6,2,7,0,2,6,7,1,3,3,4,7,8,13,11,9,10,8,10,12,14,11,13,12,14,13,15,15,10,14,9,8,11,11,12,9,16,17,18,18,19,16,20,21,22,22,23,20,20,23,16,16,19,20,18,17,22,22,21,18,18,21,20,20,19,18,16,23,22,22,17,16 + }; + + uint[] expectedFifo = + { + 0,1,2,3,1,0,6,5,0,0,2,6,0,2,6,7,1,3,3,4,7,6,2,7,8,13,11,9,10,8,9,8,11,11,13,12,14,13,15,11,12,9,10,12,14,15,10,14,16,17,18,18,19,16,20,23,16,16,19,20,16,23,22,22,17,16,18,17,22,22,21,18,18,21,20,20,19,18,20,21,22,22,23,20 + }; + // csharpier-ignore-end + // @formatter:on + + uint[] output = new uint[input.Length]; + VertexCacheOptimizer.OptimizeVertexCacheFifo(output, input, 24, 16); + Assert.That(output, Is.Not.EqualTo(input)); + Assert.That(expectedFifo, Is.EqualTo(output)); + } + + [Test] + public void OptimizeVertexCache_WithValidInput_GivesValuesFromReferenceImplementation() + { + // @formatter:off + // csharpier-ignore-start + uint[] input = { + 0,1,2,3,1,0,6,5,0,0,2,6,6,2,7,0,2,6,7,1,3,3,4,7,8,13,11,9,10,8,10,12,14,11,13,12,14,13,15,15,10,14,9,8,11,11,12,9,16,17,18,18,19,16,20,21,22,22,23,20,20,23,16,16,19,20,18,17,22,22,21,18,18,21,20,20,19,18,16,23,22,22,17,16 + }; + uint[] expectedNotFifo = { + 0,1,2,3,1,0,7,1,3,3,4,7,6,2,7,0,2,6,0,2,6,6,5,0,8,13,11,11,13,12,9,8,11,11,12,9,9,10,8,10,12,14,14,13,15,15,10,14,16,17,18,18,17,22,22,17,16,18,19,16,16,23,22,22,21,18,20,23,16,22,23,20,16,19,20,20,21,22,20,19,18,18,21,20 + }; + // csharpier-ignore-end + // @formatter:on + uint[] output = new uint[input.Length]; + VertexCacheOptimizer.OptimizeVertexCache(output, input, 24); + Assert.That(output, Is.Not.EqualTo(input)); + Assert.That(output, Is.EqualTo(expectedNotFifo)); + } +} diff --git a/CadRevealComposer/CadRevealComposer.csproj b/CadRevealComposer/CadRevealComposer.csproj index e9e84bb8..32fc4d53 100644 --- a/CadRevealComposer/CadRevealComposer.csproj +++ b/CadRevealComposer/CadRevealComposer.csproj @@ -12,6 +12,7 @@ + diff --git a/CadRevealComposer/CadRevealComposerRunner.cs b/CadRevealComposer/CadRevealComposerRunner.cs index 16701355..e9485b4b 100644 --- a/CadRevealComposer/CadRevealComposerRunner.cs +++ b/CadRevealComposer/CadRevealComposerRunner.cs @@ -1,12 +1,13 @@ namespace CadRevealComposer; -using CadRevealFbxProvider.BatchUtils; +using CadRevealComposer.Utils.MeshTools; using Configuration; using IdProviders; using ModelFormatProvider; using Operations; using Operations.SectorSplitting; using Primitives; +using Shadow; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -128,7 +129,7 @@ TreeIndexGenerator treeIndexGenerator Console.WriteLine($"Split into {sectors.Length} sectors in {stopwatch.Elapsed}"); stopwatch.Restart(); - var sectorInfos = sectors.Select(s => SerializeSector(s, outputDirectory.FullName)).ToArray(); + var sectorInfos = sectors.SelectMany(s => SerializeSector(s, outputDirectory.FullName)).ToArray(); Console.WriteLine($"Serialized {sectors.Length} sectors in {stopwatch.Elapsed}"); stopwatch.Restart(); @@ -167,16 +168,41 @@ private static void PrintSectorStats(ImmutableArray sec "μ DLsize", "v DLsize" ); + + var shadowSectors = sectorsWithDownloadSize + .Where(x => x.Filename != null && x.Filename!.StartsWith("shadow")) + .ToArray(); + var realSectors = sectorsWithDownloadSize.Except(shadowSectors).ToArray(); + + Console.WriteLine($"Found {realSectors.Count()} real sectors and {shadowSectors.Count()} shadow sectors"); + // Add stuff you would like for a quick overview here: using (new TeamCityLogBlock("Sector Stats")) { - Console.WriteLine($"Sector Count: {sectorsWithDownloadSize.Length}"); + Console.WriteLine($"Total sector Count: {sectorsWithDownloadSize.Length}"); + Console.WriteLine($"Total real sector Count: {realSectors.Length}"); + Console.WriteLine($"Total shadow sector Count: {shadowSectors.Length}"); + Console.WriteLine( $"Sum all sectors .glb size megabytes: {BytesToMegabytes(sectorsWithDownloadSize.Sum(x => x.DownloadSize)):F2}MB" ); + Console.WriteLine( + $"Sum all real sectors .glb size megabytes: {BytesToMegabytes(realSectors.Sum(x => x.DownloadSize)):F2}MB" + ); + Console.WriteLine( + $"Sum all sectors .glb size megabytes: {BytesToMegabytes(shadowSectors.Sum(x => x.DownloadSize)):F2}MB" + ); + Console.WriteLine( $"Total Estimated Triangle Count: {sectorsWithDownloadSize.Sum(x => x.EstimatedTriangleCount)}" ); + Console.WriteLine( + $"Real Sectors Estimated Triangle Count: {realSectors.Sum(x => x.EstimatedTriangleCount)}" + ); + Console.WriteLine( + $"Shadow Sectors Estimated Triangle Count: {shadowSectors.Sum(x => x.EstimatedTriangleCount)}" + ); + Console.WriteLine($"Depth Stats:"); Console.WriteLine( $"|{headers.Item1, 5}|{headers.Item2, 7}|{headers.Item3, 10}|{headers.Item4, 11}|{headers.Item5, 10}|{headers.Item6, 10}|{headers.Item7, 10}|{headers.Item8, 17}|{headers.Item9, 10}|{headers.Item10, 8}|" @@ -218,7 +244,7 @@ private static void PrintSectorStats(ImmutableArray sec } } - private static SceneCreator.SectorInfo SerializeSector(InternalSector p, string outputDirectory) + private static IEnumerable SerializeSector(InternalSector p, string outputDirectory) { var (estimatedTriangleCount, estimatedDrawCalls) = DrawCallEstimator.Estimate(p.Geometries); @@ -239,8 +265,65 @@ private static SceneCreator.SectorInfo SerializeSector(InternalSector p, string ); if (sectorFilename != null) + { SceneCreator.ExportSectorGeometries(sectorInfo.Geometries, sectorFilename, outputDirectory); - return sectorInfo; + + var shadowSectorFilename = "shadow_" + sectorFilename; + + var shadowSectorInfo = CreateShadowSector(p, shadowSectorFilename); + SceneCreator.ExportSectorGeometries(shadowSectorInfo.Geometries, shadowSectorFilename, outputDirectory); + + yield return shadowSectorInfo; + } + yield return sectorInfo; + } + + private static SceneCreator.SectorInfo CreateShadowSector(InternalSector realSector, string shadowSectorFilename) + { + var shadowGeometries = CreateShadowGeometries(realSector.Geometries); + + var (estimatedTriangleCount, estimatedDrawCalls) = DrawCallEstimator.Estimate(shadowGeometries); + + var shadowSectorInfo = new SceneCreator.SectorInfo( + SectorId: realSector.SectorId + 10000, // TODO + ParentSectorId: realSector.ParentSectorId, + Depth: realSector.Depth, + Path: realSector.Path, + Filename: shadowSectorFilename, + EstimatedTriangleCount: estimatedTriangleCount, + EstimatedDrawCalls: estimatedDrawCalls, + MinNodeDiagonal: realSector.MinNodeDiagonal, + MaxNodeDiagonal: realSector.MaxNodeDiagonal, + Geometries: shadowGeometries, + SubtreeBoundingBox: realSector.SubtreeBoundingBox, + GeometryBoundingBox: realSector.GeometryBoundingBox + ); + + return shadowSectorInfo; + } + + private static APrimitive[] CreateShadowGeometries(APrimitive[] realGeometries) + { + var shadowGeometry = new List(); + + // TODO: Investigate, can everything made into a box be instances now? + + foreach (var geometry in realGeometries) + { + switch (geometry) + { + // Skip circles, rings and quads because they are only used as caps, and shadow boxes do not need them + case Circle: + case GeneralRing: + case Quad: + continue; + default: + shadowGeometry.Add(ShadowCreator.CreateShadow(geometry)); + break; + } + } + + return shadowGeometry.ToArray(); } private static IEnumerable CalculateDownloadSizes( diff --git a/CadRevealComposer/Primitives/APrimitive.cs b/CadRevealComposer/Primitives/APrimitive.cs index abaf0897..73d3e01a 100644 --- a/CadRevealComposer/Primitives/APrimitive.cs +++ b/CadRevealComposer/Primitives/APrimitive.cs @@ -17,6 +17,7 @@ BoundingBox AxisAlignedBoundingBox ) : APrimitive(TreeIndex, Color, AxisAlignedBoundingBox); public sealed record Cone( + Matrix4x4 InstanceMatrix, float Angle, float ArcAngle, Vector3 CenterA, @@ -30,6 +31,7 @@ BoundingBox AxisAlignedBoundingBox ) : APrimitive(TreeIndex, Color, AxisAlignedBoundingBox); public sealed record EccentricCone( + Matrix4x4 InstanceMatrix, Vector3 CenterA, Vector3 CenterB, Vector3 Normal, @@ -41,6 +43,7 @@ BoundingBox AxisAlignedBoundingBox ) : APrimitive(TreeIndex, Color, AxisAlignedBoundingBox); public sealed record EllipsoidSegment( + Matrix4x4 InstanceMatrix, float HorizontalRadius, float VerticalRadius, float Height, @@ -52,6 +55,7 @@ BoundingBox AxisAlignedBoundingBox ) : APrimitive(TreeIndex, Color, AxisAlignedBoundingBox); public sealed record GeneralCylinder( + Matrix4x4 InstanceMatrix, float Angle, float ArcAngle, Vector3 CenterA, diff --git a/CadRevealComposer/Shadow/ConeShadowCreator.cs b/CadRevealComposer/Shadow/ConeShadowCreator.cs new file mode 100644 index 00000000..8b0f438c --- /dev/null +++ b/CadRevealComposer/Shadow/ConeShadowCreator.cs @@ -0,0 +1,28 @@ +namespace CadRevealComposer.Shadow; + +using Primitives; +using System; +using System.Numerics; +using Utils; + +public static class ConeShadowCreator +{ + public static APrimitive CreateShadow(this Cone cone) + { + if (!cone.InstanceMatrix.DecomposeAndNormalize(out _, out var rotation, out var position)) + { + throw new Exception("Failed to decompose matrix to transform. Input Matrix: " + cone.InstanceMatrix); + } + + var coneHeight = Vector3.Distance(cone.CenterA, cone.CenterB); + var radius = float.Max(cone.RadiusA, cone.RadiusB); + var shadowConeScale = new Vector3(radius * 2, radius * 2, coneHeight); + + var shadowBoxMatrix = + Matrix4x4.CreateScale(shadowConeScale) + * Matrix4x4.CreateFromQuaternion(rotation) + * Matrix4x4.CreateTranslation(position); + + return new Box(shadowBoxMatrix, cone.TreeIndex, cone.Color, cone.AxisAlignedBoundingBox); + } +} diff --git a/CadRevealComposer/Shadow/CylinderShadowCreator.cs b/CadRevealComposer/Shadow/CylinderShadowCreator.cs new file mode 100644 index 00000000..35e84ba1 --- /dev/null +++ b/CadRevealComposer/Shadow/CylinderShadowCreator.cs @@ -0,0 +1,27 @@ +namespace CadRevealComposer.Shadow; + +using Primitives; +using System; +using System.Numerics; +using Utils; + +public static class CylinderShadowCreator +{ + public static APrimitive CreateShadow(this GeneralCylinder cylinder) + { + if (!cylinder.InstanceMatrix.DecomposeAndNormalize(out _, out var rotation, out var position)) + { + throw new Exception("Failed to decompose matrix to transform. Input Matrix: " + cylinder.InstanceMatrix); + } + + var cylinderHeight = Vector3.Distance(cylinder.CenterA, cylinder.CenterB); + var newScale = new Vector3(cylinder.Radius * 2, cylinder.Radius * 2, cylinderHeight); + + var shadowBoxMatrix = + Matrix4x4.CreateScale(newScale) + * Matrix4x4.CreateFromQuaternion(rotation) + * Matrix4x4.CreateTranslation(position); + + return new Box(shadowBoxMatrix, cylinder.TreeIndex, cylinder.Color, cylinder.AxisAlignedBoundingBox); + } +} diff --git a/CadRevealComposer/Shadow/EccentricConeCreator.cs b/CadRevealComposer/Shadow/EccentricConeCreator.cs new file mode 100644 index 00000000..d04acb5f --- /dev/null +++ b/CadRevealComposer/Shadow/EccentricConeCreator.cs @@ -0,0 +1,28 @@ +namespace CadRevealComposer.Shadow; + +using Primitives; +using System; +using System.Numerics; +using Utils; + +public static class EccentricConeCreator +{ + public static APrimitive CreateShadow(this EccentricCone cone) + { + if (!cone.InstanceMatrix.DecomposeAndNormalize(out _, out var rotation, out var position)) + { + throw new Exception("Failed to decompose matrix to transform. Input Matrix: " + cone.InstanceMatrix); + } + + var coneHeight = Vector3.Distance(cone.CenterA, cone.CenterB); + var radius = float.Max(cone.RadiusA, cone.RadiusB); + var shadowConeScale = new Vector3(radius * 2, radius * 2, coneHeight); + + var shadowBoxMatrix = + Matrix4x4.CreateScale(shadowConeScale) + * Matrix4x4.CreateFromQuaternion(rotation) + * Matrix4x4.CreateTranslation(position); + + return new Box(shadowBoxMatrix, cone.TreeIndex, cone.Color, cone.AxisAlignedBoundingBox); + } +} diff --git a/CadRevealComposer/Shadow/EllipsoidSegmentShadowCreator.cs b/CadRevealComposer/Shadow/EllipsoidSegmentShadowCreator.cs new file mode 100644 index 00000000..d5899c5d --- /dev/null +++ b/CadRevealComposer/Shadow/EllipsoidSegmentShadowCreator.cs @@ -0,0 +1,37 @@ +namespace CadRevealComposer.Shadow; + +using Primitives; +using System; +using System.Numerics; +using Utils; + +public static class EllipsoidSegmentShadowCreator +{ + public static APrimitive CreateShadow(this EllipsoidSegment ellipsoidSegment) + { + if (!ellipsoidSegment.InstanceMatrix.DecomposeAndNormalize(out _, out var rotation, out var position)) + { + throw new Exception( + "Failed to decompose matrix to transform. Input Matrix: " + ellipsoidSegment.InstanceMatrix + ); + } + + var shadowConeScale = new Vector3( + ellipsoidSegment.HorizontalRadius * 2, + ellipsoidSegment.HorizontalRadius * 2, + ellipsoidSegment.Height + ); + + var shadowBoxMatrix = + Matrix4x4.CreateScale(shadowConeScale) + * Matrix4x4.CreateFromQuaternion(rotation) + * Matrix4x4.CreateTranslation(position); + + return new Box( + shadowBoxMatrix, + ellipsoidSegment.TreeIndex, + ellipsoidSegment.Color, + ellipsoidSegment.AxisAlignedBoundingBox + ); + } +} diff --git a/CadRevealComposer/Shadow/InstancedMeshShadowCreator.cs b/CadRevealComposer/Shadow/InstancedMeshShadowCreator.cs new file mode 100644 index 00000000..ad3046f4 --- /dev/null +++ b/CadRevealComposer/Shadow/InstancedMeshShadowCreator.cs @@ -0,0 +1,67 @@ +namespace CadRevealComposer.Shadow; + +using CadRevealComposer.Primitives; +using CadRevealComposer.Tessellation; +using System.Linq; +using System.Numerics; + +public static class InstancedMeshShadowCreator +{ + public static InstancedMesh CreateShadow(this InstancedMesh instanceMesh) + { + var boundingBox = instanceMesh.AxisAlignedBoundingBox; + // csharpier-ignore + uint[] indices = + { + 0, 1, 2, + 1, 2, 3, + + 0, 1, 4, + 1, 4, 5, + + 0, 2, 4, + 2, 4, 6, + + 2, 3, 6, + 3, 6, 7, + + 1, 3, 5, + 3, 5, 7, + + 4, 5, 6, + 5, 6, 7 + }; + + var templateMesh = instanceMesh.TemplateMesh; + + var minX = templateMesh.Vertices.Min(x => x.X); + var minY = templateMesh.Vertices.Min(y => y.Y); + var minZ = templateMesh.Vertices.Min(z => z.Z); + var maxX = templateMesh.Vertices.Max(x => x.X); + var maxY = templateMesh.Vertices.Max(y => y.Y); + var maxZ = templateMesh.Vertices.Max(z => z.Z); + + var min = new Vector3(minX, minY, minZ); + var max = new Vector3(maxX, maxY, maxZ); + + var v0 = new Vector3(min.X, min.Y, min.Z); + var v1 = new Vector3(max.X, min.Y, min.Z); + var v2 = new Vector3(min.X, max.Y, min.Z); + var v3 = new Vector3(max.X, max.Y, min.Z); + var v4 = new Vector3(min.X, min.Y, max.Z); + var v5 = new Vector3(max.X, min.Y, max.Z); + var v6 = new Vector3(min.X, max.Y, max.Z); + var v7 = new Vector3(max.X, max.Y, max.Z); + + Vector3[] vertices = { v0, v1, v2, v3, v4, v5, v6, v7 }; + + var error = instanceMesh.TemplateMesh.Error; + + var mesh = new Mesh(vertices, indices, error); + + return instanceMesh with + { + TemplateMesh = mesh + }; + } +} diff --git a/CadRevealComposer/Shadow/ShadowCreator.cs b/CadRevealComposer/Shadow/ShadowCreator.cs new file mode 100644 index 00000000..db90150b --- /dev/null +++ b/CadRevealComposer/Shadow/ShadowCreator.cs @@ -0,0 +1,54 @@ +namespace CadRevealComposer.Shadow; + +using Primitives; +using System; + +public static class ShadowCreator +{ + public static APrimitive CreateShadow(APrimitive primitive) + { + switch (primitive) + { + case InstancedMesh instancedMesh: // TODO: It is uneccessary to calculate the box for every instance, as the template is the same for each group + return instancedMesh.CreateShadow(); + case TriangleMesh triangleMesh: + return triangleMesh.CreateShadow(); + case Box box: // Boxes can stay as they are + return box; + case GeneralCylinder cylinder: + return cylinder.CreateShadow(); + case Cone cone: + return cone.CreateShadow(); + case EccentricCone eccentricCone: + return eccentricCone.CreateShadow(); + case EllipsoidSegment ellipsoidSegment: + return ellipsoidSegment.CreateShadow(); + case Nut nut: // Nut is currently not used + return nut; + case TorusSegment torusSegment: + return torusSegment.CreateShadow(); + + // Dummies used while developing + default: + + throw new Exception("Some primitives were not handled when creating shadows"); + + //var dummyScale = new Vector3(0.1f); + //var dummyRotation = Quaternion.Identity; + //var dummyPosition = Vector3.Zero; + + //var dummyMatrix = + // Matrix4x4.CreateScale(dummyScale) + // * Matrix4x4.CreateFromQuaternion(dummyRotation) + // * Matrix4x4.CreateTranslation(dummyPosition); + + //var dummyBox = new Box( + // dummyMatrix, + // primitive.TreeIndex, + // primitive.Color, + // primitive.AxisAlignedBoundingBox + //); + //return dummyBox; + } + } +} diff --git a/CadRevealComposer/Shadow/TorusSegmentShadowCreator.cs b/CadRevealComposer/Shadow/TorusSegmentShadowCreator.cs new file mode 100644 index 00000000..d9b3e948 --- /dev/null +++ b/CadRevealComposer/Shadow/TorusSegmentShadowCreator.cs @@ -0,0 +1,43 @@ +namespace CadRevealComposer.Shadow; + +using Primitives; +using System; +using System.Numerics; +using Utils; + +public static class TorusSegmentShadowCreator +{ + private const float SizeThreshold = 10f; + + public static APrimitive CreateShadow(this TorusSegment torusSegment) + { + if (!torusSegment.InstanceMatrix.DecomposeAndNormalize(out _, out var rotation, out var position)) + { + throw new Exception( + "Failed to decompose matrix to transform. Input Matrix: " + torusSegment.InstanceMatrix + ); + } + + // Let the largest tori stay as they are, to avoid too large and weird boxes (like on TrollA) + if (torusSegment.Radius > SizeThreshold) + return torusSegment; + + // TODO Control this + var height = torusSegment.TubeRadius * 2 * 0.001f; + var side = torusSegment.Radius * 2 * 0.001f; + + var scale = new Vector3(side, side, height); + + var shadowBoxMatrix = + Matrix4x4.CreateScale(scale) + * Matrix4x4.CreateFromQuaternion(rotation) + * Matrix4x4.CreateTranslation(position); + + return new Box( + shadowBoxMatrix, + torusSegment.TreeIndex, + torusSegment.Color, + torusSegment.AxisAlignedBoundingBox + ); + } +} diff --git a/CadRevealComposer/Shadow/TriangleMeshShadowCreator.cs b/CadRevealComposer/Shadow/TriangleMeshShadowCreator.cs new file mode 100644 index 00000000..463b14c1 --- /dev/null +++ b/CadRevealComposer/Shadow/TriangleMeshShadowCreator.cs @@ -0,0 +1,55 @@ +namespace CadRevealComposer.Shadow; + +using CadRevealComposer.Primitives; +using CadRevealComposer.Utils; +using System.Numerics; + +public static class TriangleMeshShadowCreator +{ + private const float SizeTreshold = 3.0f; // Arbitrary number + + public static APrimitive CreateShadow(this TriangleMesh triangleMesh) + { + var bb = triangleMesh.AxisAlignedBoundingBox; + var bbSize = bb.Max - bb.Min; + + // If two sides is greater then threshold, there could be a large diagonal that would look weird as a box + int largeSizeCounts = 0; + + if (bbSize.X > SizeTreshold) + largeSizeCounts++; + if (bbSize.Y > SizeTreshold) + largeSizeCounts++; + if (bbSize.Z > SizeTreshold) + largeSizeCounts++; + + if (largeSizeCounts >= 2) + { + return triangleMesh; + } + + var scale = bbSize; + var rotation = Quaternion.Identity; + var position = bb.Center; + + var matrix = + Matrix4x4.CreateScale(scale) + * Matrix4x4.CreateFromQuaternion(rotation) + * Matrix4x4.CreateTranslation(position); + + return new Box(matrix, triangleMesh.TreeIndex, triangleMesh.Color, triangleMesh.AxisAlignedBoundingBox); + } + + private static TriangleMesh SimplifyTriangleMesh(TriangleMesh triangleMesh) + { + var mesh = triangleMesh.Mesh; + var simplifiedMesh = Simplify.SimplifyMeshLossy(mesh, 5.0f); + + return new TriangleMesh( + simplifiedMesh, + triangleMesh.TreeIndex, + triangleMesh.Color, + triangleMesh.AxisAlignedBoundingBox + ); + } +} diff --git a/CadRevealComposer/Utils/MeshTools/MeshTools.cs b/CadRevealComposer/Utils/MeshTools/MeshTools.cs index 645622eb..8ea62494 100644 --- a/CadRevealComposer/Utils/MeshTools/MeshTools.cs +++ b/CadRevealComposer/Utils/MeshTools/MeshTools.cs @@ -1,33 +1,68 @@ -namespace CadRevealFbxProvider.BatchUtils; +namespace CadRevealComposer.Utils.MeshTools; -using CadRevealComposer.Tessellation; -using CadRevealComposer.Utils.Comparers; +using System; using System.Collections.Generic; +using System.Diagnostics.Contracts; using System.Linq; using System.Numerics; +using Tessellation; public static class MeshTools { + /// + /// Reduce a meshes precision (in place) (to easier do vertex squashing later) + /// + public static void ReducePrecisionInPlace(Mesh mesh, int precisionDigits = 5) + { + void Round(ref Vector3 v) + { + v.X = MathF.Round(v.X, precisionDigits); + v.Y = MathF.Round(v.Y, precisionDigits); + v.Z = MathF.Round(v.Z, precisionDigits); + } + + for (int index = 0; index < mesh.Vertices.Length; index++) + { + Round(ref mesh.Vertices[index]); + } + } + /// /// Remove re-used vertices, and remap the Triangle indices to the new unique table. /// Saves memory but assumes the mesh ONLY has Position and Index data. Discards any normals! /// /// Returns a new Mesh /// + [Pure] public static Mesh DeduplicateVertices(Mesh input) { - var comparer = new XyzVector3EqualityComparer(); - var alreadyFoundVerticesToIndexMap = new Dictionary(comparer); - - var newVertices = new List(); + var newVertices = input.Vertices.ToArray(); var indicesCopy = input.Indices.ToArray(); + DeduplicateVerticesInPlace(ref newVertices, ref indicesCopy); + return new Mesh(newVertices, indicesCopy, 0); + } + + /// + /// Remove re-used vertices, and remap the Triangle indices to the new unique table. + /// Saves memory but assumes the mesh ONLY has Position and Index data. Discards any normals! + /// + public static void DeduplicateVerticesInPlace( + ref T[] vertices, + ref uint[] indices, + IEqualityComparer? equalityComparer = null + ) + where T : notnull + { + var alreadyFoundVerticesToIndexMap = new Dictionary(equalityComparer); + + var newVertices = new List(); // The index in the oldVertexIndexToNewIndexRemap array is the old index, and the value is the new index. (Think of it as a dict) - var oldVertexIndexToNewIndexRemap = new uint[input.Vertices.Count()]; + var oldVertexIndexToNewIndexRemap = new uint[vertices.Length]; - for (uint i = 0; i < input.Vertices.Count(); i++) + for (uint i = 0; i < vertices.Length; i++) { - var vertex = input.Vertices[(int)i]; + var vertex = vertices[(int)i]; if (!alreadyFoundVerticesToIndexMap.TryGetValue(vertex, out uint newIndex)) { newIndex = (uint)newVertices.Count; @@ -40,13 +75,48 @@ public static Mesh DeduplicateVertices(Mesh input) // Explicitly clear to unload memory as soon as possible. alreadyFoundVerticesToIndexMap.Clear(); - for (int i = 0; i < indicesCopy.Length; i++) + for (int i = 0; i < indices.Length; i++) { - var originalIndex = indicesCopy[i]; + var originalIndex = indices[i]; var vertexIndex = oldVertexIndexToNewIndexRemap[originalIndex]; - indicesCopy[i] = vertexIndex; + indices[i] = vertexIndex; + } + + Array.Resize(ref vertices, newVertices.Count); + for (var i = 0; i < vertices.Length; i++) + { + vertices[i] = newVertices[i]; } + } + + /// + /// Optimizes for best rendering performance without losing precision. + /// Removes duplicate vertices, reorders indices and vertices, and optimizes for GPU cache. + /// + /// Probably a minor improvement to rendering performance for most meshes, but usually really quick! + /// + public static void OptimizeInPlace( + ref T[] vertices, + ref uint[] indices, + IEqualityComparer? vertexComparer = null + ) + where T : notnull + { + DeduplicateVerticesInPlace(ref vertices, ref indices, vertexComparer); + VertexCacheOptimizer.OptimizeVertexCache(indices, indices, (uint)vertices.Length); + VertexFetchOptimizer.OptimizeVertexFetch(vertices.AsSpan(), indices, vertices); + } - return new Mesh(newVertices.ToArray(), indicesCopy, 0); + /// + /// + /// + /// Returns a new mesh. + /// + public static Mesh OptimizeMesh(Mesh m) + { + var indices = m.Indices.ToArray(); + var verts = m.Vertices.ToArray(); + OptimizeInPlace(ref verts, ref indices); + return new Mesh(verts, indices, m.Error); } } diff --git a/CadRevealComposer/Utils/MeshTools/VertexCacheOptimizer.cs b/CadRevealComposer/Utils/MeshTools/VertexCacheOptimizer.cs new file mode 100644 index 00000000..3d437f74 --- /dev/null +++ b/CadRevealComposer/Utils/MeshTools/VertexCacheOptimizer.cs @@ -0,0 +1,630 @@ +namespace CadRevealComposer.Utils.MeshTools; + +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +public static class VertexCacheOptimizer +{ + // MIT License + // + // Copyright (c) 2016-2023 Arseny Kapoulkine, 2023 Nils Henrik Hals + // + // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + // + // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + // + // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + // This is a port of the vcacheoptimizer.cpp file from https://github.com/zeux/meshoptimizer/: + // https://github.com/zeux/meshoptimizer/blob/b4afc3af005dfeffdbde60bf677106fac41c1f9f/src/vcacheoptimizer.cpp + // Most of the comments, all magic numbers etc is from that implementation. + // I have added dotnet specific changes and optimizations + + // This work is based on: + // Tom Forsyth. Linear-Speed Vertex Cache Optimisation. 2006 + // Pedro Sander, Diego Nehab and Joshua Barczak. Fast Triangle Reordering for Vertex Locality and Reduced Overdraw. 2007 + const uint CacheSizeMax = 16; + const uint ValenceMax = 8; + + class VertexScoreTable + { + public float[] Cache = new float[1 + CacheSizeMax]; + public float[] Live = new float[1 + ValenceMax]; + }; + + ref struct TriangleAdjacency + { + // Count of references to a given Vertex + public Span Counts; + + // Offsets of the Vertex in the Data array + public Span Offsets; + + // The Vertexes + public Span Data; + }; + + // Tuned to minimize the Average Cache Miss Ratio (ACMR) of a GPU that has a cache profile similar to NVidia and AMD + // csharpier-ignore + static readonly VertexScoreTable VertexScoreTableInstance = new VertexScoreTable() + { + Cache = new float[] + { + 0, + 0.779f, + 0.791f, + 0.789f, + 0.981f, + 0.843f, + 0.726f, + 0.847f, + 0.882f, + 0.867f, + 0.799f, + 0.642f, + 0.613f, + 0.600f, + 0.568f, + 0.372f, + 0.234f + }, + Live = new float[] { 0, 0.995f, 0.713f, 0.450f, 0.404f, 0.059f, 0.005f, 0.147f, 0.006f } + }; + + // Tuned to minimize the encoded index buffer size + static readonly VertexScoreTable KVertexScoreTableStrip = new VertexScoreTable + { + Cache = new[] + { + 0, + 1.000f, + 1.000f, + 1.000f, + 0.453f, + 0.561f, + 0.490f, + 0.459f, + 0.179f, + 0.526f, + 0.000f, + 0.227f, + 0.184f, + 0.490f, + 0.112f, + 0.050f, + 0.131f + }, + Live = new[] { 0, 0.956f, 0.786f, 0.577f, 0.558f, 0.618f, 0.549f, 0.499f, 0.489f }, + }; + + private static void BuildTriangleAdjacency( + ref TriangleAdjacency adjacency, + ReadOnlySpan indices, + uint vertexCount + ) + { + int indexCount = indices.Length; + int faceCount = indexCount / 3; + + // allocate arrays + // Vertex use count + adjacency.Counts = new uint[vertexCount]; + // Vertex offsets? + adjacency.Offsets = new uint[vertexCount]; + // Data for each index? + adjacency.Data = new uint[indexCount]; + + // fill triangle counts + for (int i = 0; i < indexCount; ++i) + { + Debug.Assert(indices[i] < vertexCount); + + // Vertex use count + adjacency.Counts[(int)indices[i]]++; + } + + // fill offset table + uint offset = 0; + + for (int i = 0; i < vertexCount; ++i) + { + adjacency.Offsets[i] = offset; + offset += adjacency.Counts[i]; + } + + Debug.Assert(offset == indexCount); + + // fill triangle data + for (int i = 0; i < faceCount; ++i) + { + uint a = indices[i * 3 + 0], + b = indices[i * 3 + 1], + c = indices[i * 3 + 2]; + + adjacency.Data[(int)adjacency.Offsets[(int)a]++] = (uint)i; + adjacency.Data[(int)adjacency.Offsets[(int)b]++] = (uint)i; + adjacency.Data[(int)adjacency.Offsets[(int)c]++] = (uint)i; + } + + // fix offsets that have been disturbed by the previous pass + for (int i = 0; i < vertexCount; ++i) + { + Debug.Assert(adjacency.Offsets[i] >= adjacency.Counts[i]); + + adjacency.Offsets[i] -= adjacency.Counts[i]; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint GetNextVertexDeadEnd( + Span deadEnd, + ref uint deadEndTop, + ref uint inputCursor, + Span liveTriangles, + uint vertexCount + ) + { + // check dead-end stack + while (deadEndTop != 0) + { + uint vertex = deadEnd[(int)--deadEndTop]; + + if (liveTriangles[(int)vertex] > 0) + return vertex; + } + + // input order + while (inputCursor < vertexCount) + { + if (liveTriangles[(int)inputCursor] > 0) + return inputCursor; + + ++inputCursor; + } + + return ~0u; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint GetNextVertexNeighbor( + ReadOnlySpan nextCandidates, + ReadOnlySpan liveTriangles, + ReadOnlySpan cacheTimestamps, + uint timestamp, + uint cacheSize + ) + { + uint bestCandidate = ~0u; + int bestPriority = -1; + + for (int i = 0; i < nextCandidates.Length; i++) + { + int vertex = (int)nextCandidates[i]; + + // otherwise we don't need to process it + if (liveTriangles[vertex] > 0) + { + int priority = 0; + + // will it be in cache after fanning? + if (2 * liveTriangles[vertex] + timestamp - cacheTimestamps[vertex] <= cacheSize) + { + priority = (int)(timestamp - cacheTimestamps[vertex]); // position in cache + } + + if (priority > bestPriority) + { + bestCandidate = (uint)vertex; + bestPriority = priority; + } + } + } + + return bestCandidate; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float VertexScore(ref VertexScoreTable table, int cachePosition, uint liveTriangles) + { + Debug.Assert(cachePosition >= -1 && cachePosition < CacheSizeMax); + + uint liveTrianglesClamped = liveTriangles < ValenceMax ? liveTriangles : ValenceMax; + + return table.Cache[1 + cachePosition] + table.Live[liveTrianglesClamped]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint GetNextTriangleDeadEnd(ref uint inputCursor, ref bool[] emittedFlags, uint faceCount) + { + // input order + while (inputCursor < faceCount) + { + if (!emittedFlags[inputCursor]) + return inputCursor; + + ++inputCursor; + } + + return ~0u; + } + + static void OptimizeVertexCacheTable( + Span destination, + ReadOnlySpan inputIndices, + uint vertexCount, + VertexScoreTable table + ) + { + // If input and output are the same reference we keep a copy of the input indexes + var indices = inputIndices == destination ? inputIndices.ToArray() : inputIndices; + uint indexCount = (uint)indices.Length; + Debug.Assert(indices.Length % 3 == 0); + + // guard for empty meshes + if (indexCount == 0 || vertexCount == 0) + return; + + const uint cacheSize = 16; + // ReSharper disable once ConditionIsAlwaysTrueOrFalse + Debug.Assert(cacheSize <= CacheSizeMax); + + uint faceCount = indexCount / 3; + + // build adjacency information + TriangleAdjacency adjacency = new TriangleAdjacency(); + BuildTriangleAdjacency(ref adjacency, indices, vertexCount); + + // live triangle counts + uint[] liveTriangles = adjacency.Counts.ToArray(); + + // emitted flags + bool[] emittedFlags = new bool[faceCount]; + + // compute initial vertex scores + float[] vertexScores = new float[vertexCount]; + + for (uint i = 0; i < vertexCount; ++i) + vertexScores[i] = VertexScore(ref table, -1, liveTriangles[i]); + + // compute triangle scores + float[] triangleScores = new float[faceCount]; + + for (uint i = 0; i < faceCount; ++i) + { + uint a = indices[(int)(i * 3 + 0)]; + uint b = indices[(int)(i * 3 + 1)]; + uint c = indices[(int)(i * 3 + 2)]; + + triangleScores[i] = vertexScores[a] + vertexScores[b] + vertexScores[c]; + } + + // uint cache_holder[2 * (kCacheSizeMax + 3)]; + uint[] cache = new uint[(CacheSizeMax + 3)]; + uint[] cacheNew = new uint[CacheSizeMax + 3]; + uint cacheCount = 0; + + uint currentTriangle = 0; + uint inputCursor = 1; + + uint outputTriangle = 0; + + while (currentTriangle != ~0u) + { + Debug.Assert(outputTriangle < faceCount); + + uint a = indices[(int)(currentTriangle * 3 + 0)]; + uint b = indices[(int)(currentTriangle * 3 + 1)]; + uint c = indices[(int)(currentTriangle * 3 + 2)]; + + // output indices + destination[(int)(outputTriangle * 3 + 0)] = a; + destination[(int)(outputTriangle * 3 + 1)] = b; + destination[(int)(outputTriangle * 3 + 2)] = c; + outputTriangle++; + + // update emitted flags + emittedFlags[currentTriangle] = true; + triangleScores[currentTriangle] = 0; + + // new triangle + uint cacheWrite = 0; + cacheNew[cacheWrite++] = a; + cacheNew[cacheWrite++] = b; + cacheNew[cacheWrite++] = c; + + // old triangles + for (uint i = 0; i < cacheCount; ++i) + { + uint index = cache[i]; + + if (index != a && index != b && index != c) + { + cacheNew[cacheWrite++] = index; + } + } + + // Swap caches + (cache, cacheNew) = (cacheNew, cache); + cacheCount = cacheWrite > cacheSize ? cacheSize : cacheWrite; + + // update live triangle counts + liveTriangles[a]--; + liveTriangles[b]--; + liveTriangles[c]--; + + // remove emitted triangle from adjacency data + // this makes sure that we spend less time traversing these lists on subsequent iterations + for (uint k = 0; k < 3; ++k) + { + uint index = indices[(int)(currentTriangle * 3 + k)]; + + int neighborsSize = (int)adjacency.Counts[(int)index]; + var neighbors = adjacency.Data.Slice((int)adjacency.Offsets[(int)index], neighborsSize); + + for (int i = 0; i < neighborsSize; ++i) + { + uint tri = neighbors[i]; + + if (tri == currentTriangle) + { + neighbors[i] = neighbors[neighborsSize - 1]; + adjacency.Counts[(int)index]--; + break; + } + } + } + + uint bestTriangle = ~0u; + float bestScore = 0; + + // update cache positions, vertex scores and triangle scores, and find next best triangle + for (uint i = 0; i < cacheWrite; ++i) + { + uint index = cache[i]; + + int cachePosition = i >= cacheSize ? -1 : (int)i; + + // update vertex score + float score = VertexScore(ref table, cachePosition, liveTriangles[index]); + float scoreDiff = score - vertexScores[index]; + + vertexScores[index] = score; + + // update scores of vertex triangles + + uint neighborsBegin = adjacency.Offsets[(int)index]; + uint neighborsEnd = neighborsBegin + adjacency.Counts[(int)index]; + + for (uint it = neighborsBegin; it != neighborsEnd; ++it) + { + uint tri = adjacency.Data[(int)it]; + Debug.Assert(!emittedFlags[tri]); + + float triScore = triangleScores[tri] + scoreDiff; + Debug.Assert(triScore > 0); + + if (bestScore < triScore) + { + bestTriangle = tri; + bestScore = triScore; + } + + triangleScores[tri] = triScore; + } + } + + // step through input triangles in order if we hit a dead-end + currentTriangle = bestTriangle; + + if (currentTriangle == ~0u) + { + currentTriangle = GetNextTriangleDeadEnd(ref inputCursor, ref emittedFlags, faceCount); + } + } + + Debug.Assert(inputCursor == faceCount); + Debug.Assert(outputTriangle == faceCount); + } + + /// + /// Vertex transform cache optimizer + /// Reorders indices to reduce the number of GPU vertex shader invocations + /// If index buffer contains multiple ranges for multiple draw calls, this functions needs to be called on each range individually. + /// + /// Destination array, can be same as input if you want in-place edits + /// Input indexes, can be same as destination + /// Vertex count of the mesh (Vertices.Length) + public static void OptimizeVertexCache(Span destination, ReadOnlySpan inputIndices, uint vertexCount) + { + OptimizeVertexCacheTable(destination, inputIndices, vertexCount, VertexScoreTableInstance); + } + + /// + /// Vertex transform cache optimizer for strip-like caches + /// Produces inferior results to OptimizeVertexCache from the GPU vertex cache perspective + /// However, the resulting index order is more optimal if the goal is to reduce the triangle strip length or improve compression efficiency + /// + /// Destination array, can be same as input if you want in-place edits + /// Input index array + /// Vertex count of the mesh (Vertices.Length) + public static void OptimizeVertexCacheStrip( + Span destination, + ReadOnlySpan inputIndices, + uint vertexCount + ) + { + OptimizeVertexCacheTable(destination, inputIndices, vertexCount, KVertexScoreTableStrip); + } + + /// + /// Vertex transform cache optimizer for FIFO caches + /// Reorders indices to reduce the number of GPU vertex shader invocations + /// Generally takes ~3x less time to optimize meshes but produces inferior results compared to OptimizeVertexCache + /// If index buffer contains multiple ranges for multiple draw calls, this functions needs to be called on each range individually. + /// destination must contain enough space for the resulting index buffer (index_count elements) + /// + /// Destination array, can be same as input if you want in-place edits. Must be initialized and same length as input + /// Input index array + /// Vertex count of the mesh (Vertices.Length) + /// CacheSize should be less than the actual GPU cache size to avoid cache thrashing + public static void OptimizeVertexCacheFifo( + Span destination, + ReadOnlySpan inputIndices, + uint vertexCount, + uint cacheSize = 16 + ) + { + // If input and output are the same, keep a copy of the input indexes + var indices = inputIndices == destination ? inputIndices.ToArray() : inputIndices; + uint indexCount = (uint)indices.Length; + Debug.Assert(indexCount % 3 == 0); + Debug.Assert(cacheSize >= 3); + + // guard for empty meshes + if (indexCount == 0 || vertexCount == 0) + return; + + uint faceCount = indexCount / 3; + + // build adjacency information + TriangleAdjacency adjacency = new TriangleAdjacency(); + BuildTriangleAdjacency(ref adjacency, indices, vertexCount); + + // live triangle counts + Span liveTriangles = adjacency.Counts.ToArray(); + // cache time stamps + Span cacheTimestamps = new uint[vertexCount]; + + // dead-end stack + Span deadEnd = new uint[indexCount]; + uint deadEndTop = 0; + + // emitted flags + Span emittedFlags = new bool[faceCount]; + + uint currentVertex = 0; + + uint timestamp = cacheSize + 1; + uint inputCursor = 1; // vertex to restart from in case of dead-end + + uint outputTriangle = 0; + + while (currentVertex != ~0u) + { + uint nextCandidatesBegin = deadEndTop; + + // emit all vertex neighbors + var neighbors = adjacency.Data.Slice( + (int)adjacency.Offsets[(int)currentVertex], + (int)adjacency.Counts[(int)currentVertex] + ); + for (uint i = 0; i < neighbors.Length; ++i) + { + uint triangle = neighbors[(int)i]; + + if (!emittedFlags[(int)triangle]) + { + uint a = indices[(int)(triangle * 3 + 0)], + b = indices[(int)(triangle * 3 + 1)], + c = indices[(int)(triangle * 3 + 2)]; + + // output indices + destination[(int)(outputTriangle * 3 + 0)] = a; + destination[(int)(outputTriangle * 3 + 1)] = b; + destination[(int)(outputTriangle * 3 + 2)] = c; + outputTriangle++; + + // update dead-end stack + deadEnd[(int)(deadEndTop + 0)] = a; + deadEnd[(int)(deadEndTop + 1)] = b; + deadEnd[(int)(deadEndTop + 2)] = c; + deadEndTop += 3; + + // update live triangle counts + liveTriangles[(int)a]--; + liveTriangles[(int)b]--; + liveTriangles[(int)c]--; + + // update cache info + // if vertex is not in cache, put it in cache + if (timestamp - cacheTimestamps[(int)a] > cacheSize) + cacheTimestamps[(int)a] = timestamp++; + + if (timestamp - cacheTimestamps[(int)b] > cacheSize) + cacheTimestamps[(int)b] = timestamp++; + + if (timestamp - cacheTimestamps[(int)c] > cacheSize) + cacheTimestamps[(int)c] = timestamp++; + + // update emitted flags + emittedFlags[(int)triangle] = true; + } + } + + // next candidates are the ones we pushed to dead-end stack just now + var nextCandidates = deadEnd.Slice((int)nextCandidatesBegin, (int)(deadEndTop - nextCandidatesBegin)); + + currentVertex = GetNextVertexNeighbor(nextCandidates, liveTriangles, cacheTimestamps, timestamp, cacheSize); + + if (currentVertex == ~0u) + { + currentVertex = GetNextVertexDeadEnd( + deadEnd, + ref deadEndTop, + ref inputCursor, + liveTriangles, + vertexCount + ); + } + } + + Debug.Assert(outputTriangle == faceCount); + } + + /// + /// Computes the Average Cache Miss Ratio from the Input indices and selected cache size. + /// Can be used to evaluate the performance of the OptimizeVertexCache methods. + /// + /// The indices + /// The cache size + /// + public static float ComputeAverageCacheMissRatio(ReadOnlySpan indices, uint cacheSize = CacheSizeMax) + { + // From https://github.com/Sigkill79/sts/blob/master/sts_vertex_cache_optimizer.h + uint numCacheMisses = 0; + Span cache = new int[cacheSize]; + + Debug.Assert(indices.Length % 3 == 0, "Index input has to be triangles"); + + for (int i = 0; i < cacheSize; ++i) + cache[i] = -1; + + for (int v = 0; v < indices.Length; ++v) + { + uint index = indices[v]; + // check if vertex in cache + bool foundInCache = false; + for (int c = 0; c < cacheSize && cache[c] >= 0 && !foundInCache; ++c) + { + if (cache[c] == index) + foundInCache = true; + } + + if (!foundInCache) + { + ++numCacheMisses; + for (int c = (int)cacheSize - 1; c >= 1; --c) + { + cache[c] = cache[c - 1]; + } + + cache[0] = (int)index; + } + } + + float triangleCount = (indices.Length / 3f); + return numCacheMisses / triangleCount; + } +} diff --git a/CadRevealComposer/Utils/MeshTools/VertexFetchOptimizer.cs b/CadRevealComposer/Utils/MeshTools/VertexFetchOptimizer.cs new file mode 100644 index 00000000..6d2f45cb --- /dev/null +++ b/CadRevealComposer/Utils/MeshTools/VertexFetchOptimizer.cs @@ -0,0 +1,98 @@ +namespace CadRevealComposer.Utils.MeshTools; + +using System; +using System.Diagnostics; + +public static class VertexFetchOptimizer +{ + // MIT License + // + // Copyright (c) 2016-2023 Arseny Kapoulkine, 2023 Nils Henrik Hals + // + // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + // + // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + // + // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + // This is a port of the vcacheoptimizer.cpp file from https://github.com/zeux/meshoptimizer/: + // https://github.com/zeux/meshoptimizer/blob/b4afc3af005dfeffdbde60bf677106fac41c1f9f/src/vcacheoptimizer.cpp + // Most of the comments, all magic numbers etc is from that implementation. + // I have added dotnet specific changes and optimizations + + public static uint OptimizeVertexFetchRemap( + Span destination, + ReadOnlySpan indices, + uint indexCount, + uint vertexCount + ) + { + Debug.Assert(indexCount % 3 == 0); + + destination.Fill(uint.MaxValue); + + uint nextVertex = 0; + + for (int i = 0; i < indexCount; ++i) + { + uint index = indices[i]; + Debug.Assert(index < vertexCount); + + if (destination[(int)index] == uint.MaxValue) + { + destination[(int)index] = nextVertex++; + } + } + + Debug.Assert(nextVertex <= vertexCount); + + return nextVertex; + } + + /// + /// * Vertex fetch cache optimizer + /// * Reorders vertices and changes indices to reduce the amount of GPU memory fetches during vertex processing + /// * Returns the number of unique vertices, which is the same as input vertex count unless some vertices are unused + /// + /// The output array, can be same as the vertices input for in-place optimization + /// Will reorder the indices + /// A span of vertices + /// The T is not actually used + public static uint OptimizeVertexFetch(Span destination, Span indices, ReadOnlySpan vertices) + { + var indexCount = indices.Length; + var vertexCount = vertices.Length; + Debug.Assert(indexCount % 3 == 0); + // support in-place optimization (Copy the input vertices if its the same as output to avoid modifying while running) + vertices = destination == vertices ? vertices.ToArray() : vertices; + + // build vertex remap table + Span vertexRemap = new uint[vertexCount]; + vertexRemap.Fill(uint.MaxValue); + + uint nextVertex = 0; + + for (int i = 0; i < indexCount; ++i) + { + uint index = indices[i]; + Debug.Assert(index < vertexCount); + + // Reference to the uint at that point in the Span. Hacky dirty C++ stuffs + ref var remap = ref vertexRemap[(int)index]; + + if (remap == uint.MaxValue) // vertex was not added to destination VB + { + // add vertex + destination[(int)nextVertex] = vertices[(int)index]; + remap = nextVertex++; + } + + // modify indices in place + indices[i] = remap; + } + + Debug.Assert(nextVertex <= vertexCount); + return nextVertex; + } +} diff --git a/CadRevealComposer/Utils/Simplify.cs b/CadRevealComposer/Utils/Simplify.cs new file mode 100644 index 00000000..ed8dd3c3 --- /dev/null +++ b/CadRevealComposer/Utils/Simplify.cs @@ -0,0 +1,151 @@ +namespace CadRevealComposer.Utils; + +using g3; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Threading; +using Tessellation; + +public static class Simplify +{ + private static DMesh3 ConvertMeshToDMesh3(Mesh mesh) + { + return DMesh3Builder.Build< + Vector3d, + int, + object /* Normals */ + >(mesh.Vertices.Select(Vec3ToVec3d), mesh.Indices.Select(x => (int)x)); + + // Alternative method? + // var dMesh = new DMesh3(); + // dMesh.BeginUnsafeTrianglesInsert(); + // dMesh.BeginUnsafeVerticesInsert(); + // dMesh.TrianglesBuffer.Add(mesh.Triangles.Select(x => (int)x).ToArray()); + // dMesh.VerticesBuffer.Add(mesh.Vertices.SelectMany(x => x.AsEnumerable()).Select(x => (double)x).ToArray()); + // dMesh.NormalsBuffer.Add(mesh.Normals.SelectMany(x => x.AsEnumerable()).ToArray()); + // dMesh.EndUnsafeTrianglesInsert(); + // dMesh.EndUnsafeVerticesInsert(); + // return dMesh; + } + + static Vector3d Vec3ToVec3d(Vector3 vec3f) + { + return new Vector3d(vec3f.X, vec3f.Y, vec3f.Z); + } + + static Vector3 Vec3fToVec3(Vector3f vec3f) + { + return new Vector3(vec3f.x, vec3f.y, vec3f.z); + } + + private static Mesh ConvertDMesh3ToMesh(DMesh3 dMesh3) + { + var verts = new Vector3[dMesh3.VertexCount]; + var normals = new Vector3[dMesh3.VertexCount]; + + for (int vertexIndex = 0; vertexIndex < dMesh3.VertexCount; vertexIndex++) + { + verts[vertexIndex] = Vec3fToVec3(dMesh3.GetVertexf(vertexIndex)); + normals[vertexIndex] = Vec3fToVec3(dMesh3.GetVertexNormal(vertexIndex)); + } + + var mesh = new Mesh(verts, dMesh3.Triangles().SelectMany(x => x.array).Select(x => (uint)x).ToArray(), 0.0f); + return mesh; + } + + /// + /// Remove re-used vertices, and remap the Triangle indices to the new unique table. + /// Saves memory but assumes the mesh ONLY has Position and Index data and that you will not add normals later + /// + public static Mesh RemapDuplicatedVertices(Mesh input) + { + var alreadyFoundVertices = new Dictionary(); + + var newVertices = new List(); + var indicesCopy = input.Indices.ToArray(); + + // The index in the oldVertexIndexToNewIndexRemap array is the old index, and the value is the new index. (Think of it as a dict) + var oldVertexIndexToNewIndexRemap = new uint[input.Vertices.Length]; + + for (uint i = 0; i < input.Vertices.Length; i++) + { + var vertex = input.Vertices[i]; + if (!alreadyFoundVertices.TryGetValue(vertex, out uint newIndex)) + { + newIndex = (uint)newVertices.Count; + newVertices.Add(vertex); + alreadyFoundVertices.Add(vertex, newIndex); + } + + oldVertexIndexToNewIndexRemap[i] = newIndex; + } + + // Explicitly clear to unload memory as soon as possible. + alreadyFoundVertices.Clear(); + + for (int i = 0; i < indicesCopy.Length; i++) + { + var originalIndex = indicesCopy[i]; + var vertexIndex = oldVertexIndexToNewIndexRemap[originalIndex]; + indicesCopy[i] = vertexIndex; + } + + return new Mesh(newVertices.ToArray(), indicesCopy, 0); + } + + public static int SimplificationBefore = 0; + public static int SimplificationAfter = 0; + public static int SimplificationBeforeTriangleCount = 0; + public static int SimplificationAfterTriangleCount = 0; + + /// + /// Lossy Simplification of the Mesh. + /// This will reduce the quality of the mesh based on a threshold + /// + /// + /// Usually meters + /// + public static Mesh SimplifyMeshLossy(Mesh mesh, float thresholdInMeshUnits = 0.01f) + { + Interlocked.Add(ref SimplificationBefore, mesh.Vertices.Length); + Interlocked.Add(ref SimplificationBeforeTriangleCount, mesh.TriangleCount); + + MeshTools.MeshTools.ReducePrecisionInPlace(mesh); + + var meshCopy = MeshTools.MeshTools.OptimizeMesh(mesh); + + var dMesh = ConvertMeshToDMesh3(meshCopy); + + var reducer = new Reducer(dMesh) + { +#if DEBUG + // Remark, veery slow Consider enabling if needed + // ENABLE_DEBUG_CHECKS = true, +#endif + MinimizeQuadricPositionError = true, + PreserveBoundaryShape = true, + AllowCollapseFixedVertsWithSameSetID = true, + }; + + try + { + reducer.ReduceToEdgeLength(thresholdInMeshUnits); + // Remove optimized stuff from the mesh. This is important or the exporter will fail. + if (!dMesh.IsCompact) + dMesh.CompactInPlace(); + + var reducedMesh = ConvertDMesh3ToMesh(dMesh); + var lastPassMesh = MeshTools.MeshTools.OptimizeMesh(reducedMesh); + Interlocked.Add(ref SimplificationAfter, lastPassMesh.Vertices.Length); + Interlocked.Add(ref SimplificationAfterTriangleCount, lastPassMesh.TriangleCount); + return lastPassMesh; + } + catch (Exception e) + { + Console.WriteLine("Failed to optimize mesh: " + e); + return mesh; + } + } +} diff --git a/CadRevealComposer/Utils/VectorExtensions.cs b/CadRevealComposer/Utils/VectorExtensions.cs index a58933bc..fc292873 100644 --- a/CadRevealComposer/Utils/VectorExtensions.cs +++ b/CadRevealComposer/Utils/VectorExtensions.cs @@ -57,6 +57,46 @@ public static bool EqualsWithinTolerance(this Vector3 vector, Vector3 other, flo && Math.Abs(vector.Z - other.Z) < tolerance; } + /// + /// An equals function that aligns components within a "Grid" cell. + /// That means that you cannot have an equality list of many nearly equal Vec3s in a row, + /// since when one leaves the "grid cell" its in its own cell. + /// + /// this + /// Other + /// The number of fractional digits to keep + /// True if equal given the precision + public static bool EqualsWithinGridTolerance(this Vector3 v, Vector3 other, int precisionDigits) + { + return v.RoundInPlace(precisionDigits).Equals(other.RoundInPlace(precisionDigits)); + } + + /// + /// Rounds XYZ elements of the vector to specified number of fractional digits, and rounds midpoint values to the nearest even number. + /// + /// The vertex to be rounded. + /// The number of fractional digits to keep + /// The same Vertex + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref Vector3 RoundInPlace(this ref Vector3 v, int precisionDigits) + { + v.X = MathF.Round(v.X, precisionDigits); + v.Y = MathF.Round(v.Y, precisionDigits); + v.Z = MathF.Round(v.Z, precisionDigits); + return ref v; + } + + /// + /// Returns a new Vector3, and does not modify the input + /// + /// The vertex to be rounded. + /// The number of fractional digits to keep + /// A new vertex + public static Vector3 Round(this Vector3 v, int precisionDigits) + { + return v.RoundInPlace(precisionDigits); + } + /// /// Checks that each vector component from both vectors are within the given factor. /// diff --git a/CadRevealRvmProvider/Converters/RvmCylinderConverter.cs b/CadRevealRvmProvider/Converters/RvmCylinderConverter.cs index f445373e..df8b2fc1 100644 --- a/CadRevealRvmProvider/Converters/RvmCylinderConverter.cs +++ b/CadRevealRvmProvider/Converters/RvmCylinderConverter.cs @@ -75,6 +75,7 @@ Color color var (showCapA, showCapB) = PrimitiveCapHelper.CalculateCapVisibility(rvmCylinder, centerA, centerB); yield return new Cone( + InstanceMatrix: rvmCylinder.Matrix, Angle: 0f, ArcAngle: 2f * MathF.PI, centerA, diff --git a/CadRevealRvmProvider/Converters/RvmEllipticalDishConverter.cs b/CadRevealRvmProvider/Converters/RvmEllipticalDishConverter.cs index d9e69cfa..4d98b993 100644 --- a/CadRevealRvmProvider/Converters/RvmEllipticalDishConverter.cs +++ b/CadRevealRvmProvider/Converters/RvmEllipticalDishConverter.cs @@ -35,6 +35,7 @@ Color color * Matrix4x4.CreateTranslation(position); yield return new EllipsoidSegment( + rvmEllipticalDish.Matrix, horizontalRadius, verticalRadius, verticalRadius, diff --git a/CadRevealRvmProvider/Converters/RvmRectangularTorusConverter.cs b/CadRevealRvmProvider/Converters/RvmRectangularTorusConverter.cs index ad477f05..e02399dd 100644 --- a/CadRevealRvmProvider/Converters/RvmRectangularTorusConverter.cs +++ b/CadRevealRvmProvider/Converters/RvmRectangularTorusConverter.cs @@ -48,6 +48,7 @@ Color color var bbBox = rvmRectangularTorus.CalculateAxisAlignedBoundingBox()!.ToCadRevealBoundingBox(); yield return new Cone( + rvmRectangularTorus.Matrix, 0, arcAngle, centerA, @@ -64,6 +65,7 @@ Color color if (radiusInner > 0) { yield return new Cone( + rvmRectangularTorus.Matrix, 0, arcAngle, centerA, diff --git a/CadRevealRvmProvider/Converters/RvmSnoutConverter.cs b/CadRevealRvmProvider/Converters/RvmSnoutConverter.cs index 2d35b84d..a1af72ce 100644 --- a/CadRevealRvmProvider/Converters/RvmSnoutConverter.cs +++ b/CadRevealRvmProvider/Converters/RvmSnoutConverter.cs @@ -110,6 +110,7 @@ BoundingBox bbox var localToWorldXAxis = Vector3.Transform(Vector3.UnitX, rotation); yield return new Cone( + InstanceMatrix: rvmSnout.Matrix, Angle: 0f, ArcAngle: 2f * MathF.PI, centerA, @@ -184,6 +185,7 @@ BoundingBox bbox var eccentricCenterB = position - eccentricNormal * halfLength; yield return new EccentricCone( + rvmSnout.Matrix, eccentricCenterA, eccentricCenterB, normal, // TODO CHECK WHY NOT eccentricNormal @@ -251,8 +253,8 @@ BoundingBox bbox var (planeRotationA, planeNormalA, planeSlopeA) = rvmSnout.GetTopSlope(); var (planeRotationB, planeNormalB, planeSlopeB) = rvmSnout.GetBottomSlope(); - (var ellipsePolarA, _, _) = rvmSnout.GetTopCapEllipse(); - (var ellipsePolarB, _, _) = rvmSnout.GetBottomCapEllipse(); + var (ellipsePolarA, _, _) = rvmSnout.GetTopCapEllipse(); + var (ellipsePolarB, _, _) = rvmSnout.GetBottomCapEllipse(); var semiMinorAxisA = ellipsePolarA.semiMinorAxis * scale.X; var semiMajorAxisA = ellipsePolarA.semiMajorAxis * scale.X; @@ -270,6 +272,7 @@ BoundingBox bbox var planeB = new Vector4(-planeNormalB, 1 + extendedHeightB); yield return new GeneralCylinder( + InstanceMatrix: rvmSnout.Matrix, Angle: 0f, ArcAngle: 2f * MathF.PI, extendedCenterA, diff --git a/CadRevealRvmProvider/Converters/RvmSphereConverter.cs b/CadRevealRvmProvider/Converters/RvmSphereConverter.cs index 42eb8cf0..8cf454f5 100644 --- a/CadRevealRvmProvider/Converters/RvmSphereConverter.cs +++ b/CadRevealRvmProvider/Converters/RvmSphereConverter.cs @@ -25,6 +25,7 @@ Color color var radius = rvmSphere.Radius * scale.X; var diameter = radius * 2f; yield return new EllipsoidSegment( + rvmSphere.Matrix, radius, radius, diameter, diff --git a/CadRevealRvmProvider/Converters/RvmSphericalDishConverter.cs b/CadRevealRvmProvider/Converters/RvmSphericalDishConverter.cs index 77acb31a..4b81e589 100644 --- a/CadRevealRvmProvider/Converters/RvmSphericalDishConverter.cs +++ b/CadRevealRvmProvider/Converters/RvmSphericalDishConverter.cs @@ -38,7 +38,17 @@ Color color * Matrix4x4.CreateFromQuaternion(rotation) * Matrix4x4.CreateTranslation(position); - yield return new EllipsoidSegment(sphereRadius, sphereRadius, height, center, normal, treeIndex, color, bbBox); + yield return new EllipsoidSegment( + rvmSphericalDish.Matrix, + sphereRadius, + sphereRadius, + height, + center, + normal, + treeIndex, + color, + bbBox + ); var showCap = PrimitiveCapHelper.CalculateCapVisibility(rvmSphericalDish, position);