diff --git a/Engine3D/Engine3D-Tests/Raytrace/SpatialSubdivisionTests.cs b/Engine3D/Engine3D-Tests/Raytrace/SpatialSubdivisionTests.cs index b109bb0..61f1a26 100644 --- a/Engine3D/Engine3D-Tests/Raytrace/SpatialSubdivisionTests.cs +++ b/Engine3D/Engine3D-Tests/Raytrace/SpatialSubdivisionTests.cs @@ -3,15 +3,24 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; +using Engine3D; using Microsoft.VisualStudio.TestTools.UnitTesting; using Engine3D.Raytrace; +using Assert = Microsoft.VisualStudio.TestTools.UnitTesting.Assert; +using Triangle = Engine3D.Raytrace.Triangle; using Vector = Engine3D.Vector; +// TODO: Use Benchmark.net to micro-benchmark some raytracing and make performance measurements more accurate +// TODO: Make a test that passes random parameters to tree building (tree depth vs max tris per node) for large model, to find optimal parameters for given model + namespace Engine3D_Tests { [TestClass] public class SpatialSubdivisionTests { + private const string largeModelFileName = "../../large-model.3ds"; + private static Random random = new Random(); private static RenderContext context = new RenderContext(random); @@ -281,6 +290,142 @@ public void RayIntersectTreeMostlyFromOutside_Performance() Console.WriteLine("Performance: {0} million ray/tri per second", millionRayTriPerSec); } + [TestMethod] + public void RayIntersectLargeModelFromInside_Performance() + { +#if DEBUG + // my laptop in High Performance mode + const double minMillionRaysPerSec = 35.0; + const double maxMillionRaysPerSec = 45.0; +#elif APPVEYOR_PERFORMANCE_MARGINS + // AppVeyor build server + const double minMillionRaysPerSec = 210.0; + const double maxMillionRaysPerSec = 240.0; +#else + // my laptop in High Performance mode + const double minMillionRaysPerSec = 73.0; + const double maxMillionRaysPerSec = 90.0; +#endif + + // Load 3D model from disk + var model = new Model(); + using (Stream stream = new FileStream(largeModelFileName, FileMode.Open, FileAccess.Read)) + { + model.Load3dsModelFromStream(stream); + } + + List triangleList = MakeRayTracableGeometry_simple(model); + int numTriangles = triangleList.Count; + + SpatialSubdivision geometryInTree = MakeRayTracableGeometry_subdivided(triangleList, model); + + //TestTree_InsideOut(geometryInTree, triangleList, numRays: 10000); // 1m:25s + + // Inside-out rays + // 1.3s for 1 million rays when using triangleSpaceSize (i.e. mostly missing model bounding box) + // 10.4s for 10,000 rays when using unit cube space! + const int numRays = 1000; + var numRaysHit = 0; + DateTime startTime = DateTime.Now; + for (var i = 1; i <= numRays; i++) + { + var start = MakeRandomVector(-0.5, 0.5, -0.5, 0.5, -0.5, 0.5); + var dir = MakeRandomVector(-1, 1, -1, 1, -1, 1); + var info = geometryInTree.IntersectRay(start, dir, context); + if (info != null) + numRaysHit++; + } + var elapsedTime = DateTime.Now - startTime; + Assert.IsTrue(numRays * 0.4 < numRaysHit && numRaysHit < numRays * 0.5, "Num rays hit {0} should be 40-50% of total rays {1}", numRaysHit, numRays); + var millionRayTriPerSec = numRays / 1000000.0 / elapsedTime.TotalSeconds * numTriangles; + Assert.IsTrue(minMillionRaysPerSec < millionRayTriPerSec && millionRayTriPerSec < maxMillionRaysPerSec, + "Rays per second {0:f2} not between {1} and {2} (millions)", millionRayTriPerSec, minMillionRaysPerSec, maxMillionRaysPerSec); + Console.WriteLine("Performance: {0} million ray/tri per second", millionRayTriPerSec); + } + + [TestMethod] + public void RayIntersectLargeModelMostlyFromOutside_Performance() + { +#if DEBUG + // my laptop in High Performance mode + const double minMillionRaysPerSec = 30.0; + const double maxMillionRaysPerSec = 33.0; +#elif APPVEYOR_PERFORMANCE_MARGINS + // AppVeyor build server + const double minMillionRaysPerSec = 130.0; + const double maxMillionRaysPerSec = 170.0; +#else + // my laptop in High Performance mode + const double minMillionRaysPerSec = 68.0; + const double maxMillionRaysPerSec = 79.0; +#endif + + // Load 3D model from disk + var model = new Model(); + using (Stream stream = new FileStream(largeModelFileName, FileMode.Open, FileAccess.Read)) + { + model.Load3dsModelFromStream(stream); + } + + List triangleList = MakeRayTracableGeometry_simple(model); + int numTriangles = triangleList.Count; + + SpatialSubdivision geometryInTree = MakeRayTracableGeometry_subdivided(triangleList, model); + + //TestTree_OutsideIn(geometryInTree, triangleList, numRays: 10000); // 1m:44s + + // Outside-in rays + // 1.9s for 1 million rays when using triangleSpaceSize (i.e. mostly missing model bounding box) + // 12.8s for 10,000 rays when using unit cube space! + // So rendering a 1024x768 image should take around 13 mins. I measured the (4 thread) renderer taking 2.5 mins. + const int numRays = 1000; + var numRaysHit = 0; + DateTime startTime = DateTime.Now; + for (var i = 1; i <= numRays; i++) + { + var start = MakeRandomVector(-5, 5, -5, 5, -5, 5); + var end = MakeRandomVector(-0.5, 0.5, -0.5, 0.5, -0.5, 0.5); + var dir = end - start; + var info = geometryInTree.IntersectRay(start, dir, context); + if (info != null) + numRaysHit++; + } + var elapsedTime = DateTime.Now - startTime; + Assert.IsTrue(numRays * 0.6 < numRaysHit && numRaysHit < numRays * 0.7, "Num rays hit {0} should be 60-70% of total rays {1}", numRaysHit, numRays); + var millionRayTriPerSec = numRays / 1000000.0 / elapsedTime.TotalSeconds * numTriangles; + Assert.IsTrue(minMillionRaysPerSec < millionRayTriPerSec && millionRayTriPerSec < maxMillionRaysPerSec, + "Rays per second {0:f2} not between {1} and {2} (millions)", millionRayTriPerSec, minMillionRaysPerSec, maxMillionRaysPerSec); + Console.WriteLine("Performance: {0} million ray/tri per second", millionRayTriPerSec); + } + + private static List MakeRayTracableGeometry_simple(Model model) + { + // Convert the 3D triangle model into a set of triangle objects that can be ray traced + var geom = new List(); + int triIndex = 0; + foreach (Engine3D.Triangle tri in model.Triangles) + { + Vector v1 = model.Vertices[tri.vertexIndex1].pos; + Vector v2 = model.Vertices[tri.vertexIndex2].pos; + Vector v3 = model.Vertices[tri.vertexIndex3].pos; + uint color = Surface.PackColorAndAlpha(tri.diffuseMaterial, 1.0); + // let the triangle know its index within this collection of geometry + geom.Add(new Triangle(v1, v2, v3, color) {TriangleIndex = triIndex}); + triIndex++; + } + + return geom; + } + + private static SpatialSubdivision MakeRayTracableGeometry_subdivided(List triangleList, Model model) + { + // Create a box bounding the 3D triangle model. + var boundingBox = new AxisAlignedBox(model.Min, model.Max); + + // Create the spatial subdivison object. + return new SpatialSubdivision(triangleList, boundingBox); + } + [TestMethod] public void TreeCorrectness1() { diff --git a/Engine3D/Engine3D-Tests/Raytrace/TriangleTests.cs b/Engine3D/Engine3D-Tests/Raytrace/TriangleTests.cs index e0ad9ce..7fe5d80 100644 --- a/Engine3D/Engine3D-Tests/Raytrace/TriangleTests.cs +++ b/Engine3D/Engine3D-Tests/Raytrace/TriangleTests.cs @@ -348,6 +348,109 @@ public void RayIntersectAABBPerformance() Console.WriteLine("Performance: {0} million rays per second", millionRaysPerSec); } + [TestMethod] + public void ClipLineSegmentToAABBPerformance() + { +#if DEBUG + // my laptop in High Performance mode + const double minMillionLinesPerSec = 0; + const double maxMillionLinesPerSec = 0; +#elif APPVEYOR_PERFORMANCE_MARGINS + // AppVeyor build server + const double minMillionLinesPerSec = 0; + const double maxMillionLinesPerSec = 0; +#else + // my laptop in High Performance mode + const double minMillionLinesPerSec = 0.45; // lines extend 100 units outside of box + const double maxMillionLinesPerSec = 0.60; + //const double minMillionLinesPerSec = 0.85; // lines extend barely outside of box + //const double maxMillionLinesPerSec = 1.2; +#endif + + const int numLines = 1000000; + var box = new AxisAlignedBox(new Vector(-0.5, -0.5, -0.5), new Vector(0.5, 0.5, 0.5)); + var lineTouchesBox = 0; + + DateTime startTime = DateTime.Now; + for (var i = 0; i < numLines; i++) + { + //var start = new Vector(0.5, 0.5, 0.5); + //var start = MakeRandomVector(-0.3, 0.3, -0.3, 0.3, 0.5, 1); + //var end = MakeRandomVector(10, 10, 10); + + var end = MakeRandomVector(-0.3, 0.3, -0.3, 0.3, 0.5, 1); + var dir = MakeRandomVector(-0.5, 0.5, -0.5, 0.5, -1, -1); + var start = end + dir * 100; + + //var dir = MakeRandomVector(-0.5, 0.5, -0.5, 0.5, -1, -1); + //var dir = MakeRandomVector(1, 1, -1); + //var dir = end - start; + + if(box.ClipLineSegment(ref start, ref end)) + lineTouchesBox++; + } + var elapsedTime = DateTime.Now - startTime; + Assert.IsTrue(numLines * 0.998 < lineTouchesBox && lineTouchesBox <= numLines * 1.0, + "Num lines touching box {0} should be roughly same as total lines {1}", lineTouchesBox, numLines); + //Assert.IsTrue(numLines * 0.380 < lineTouchesBox && lineTouchesBox <= numLines * 0.383, + // "Num lines touching box {0} should be roughly 40% of total lines {1}", lineTouchesBox, numLines); + var millionLinesPerSec = numLines / 1000000.0 / elapsedTime.TotalSeconds; + Assert.IsTrue(minMillionLinesPerSec < millionLinesPerSec && millionLinesPerSec < maxMillionLinesPerSec, + "Lines per second {0:f3} not between {1} and {2} (millions)", millionLinesPerSec, minMillionLinesPerSec, maxMillionLinesPerSec); + Console.WriteLine("Performance: {0} million lines per second", millionLinesPerSec); + } + + [TestMethod] + public void LineIntersectAABBPerformance() + { +#if DEBUG + // my laptop in High Performance mode + const double minMillionLinesPerSec = 0; + const double maxMillionLinesPerSec = 0; +#elif APPVEYOR_PERFORMANCE_MARGINS + // AppVeyor build server + const double minMillionLinesPerSec = 0; + const double maxMillionLinesPerSec = 0; +#else + // my laptop in High Performance mode + const double minMillionLinesPerSec = 0.94; + const double maxMillionLinesPerSec = 1.10; +#endif + + const int numLines = 1000000; + var box = new AxisAlignedBox(new Vector(-0.5, -0.5, -0.5), new Vector(0.5, 0.5, 0.5)); + var lineTouchesBox = 0; + + DateTime startTime = DateTime.Now; + for (var i = 0; i < numLines; i++) + { + //var start = new Vector(0.5, 0.5, 0.5); + //var start = MakeRandomVector(-0.3, 0.3, -0.3, 0.3, 0.5, 1); + //var end = MakeRandomVector(10, 10, 10); + + var end = MakeRandomVector(-0.3, 0.3, -0.3, 0.3, 0.5, 1); + var dir = MakeRandomVector(-0.5, 0.5, -0.5, 0.5, -1, -1); + var start = end + dir * 100; + + //var dir = MakeRandomVector(-0.5, 0.5, -0.5, 0.5, -1, -1); + //var dir = MakeRandomVector(1, 1, -1); + //var dir = end - start; + + var info = box.IntersectLineSegment(start, end); + if (info != null) + lineTouchesBox++; + } + var elapsedTime = DateTime.Now - startTime; + Assert.IsTrue(numLines * 0.998 < lineTouchesBox && lineTouchesBox <= numLines * 1.0, + "Num lines touching box {0} should be roughly same as total lines {1}", lineTouchesBox, numLines); + //Assert.IsTrue(numLines * 0.380 < lineTouchesBox && lineTouchesBox <= numLines * 0.383, + // "Num lines touching box {0} should be roughly 40% of total lines {1}", lineTouchesBox, numLines); + var millionLinesPerSec = numLines / 1000000.0 / elapsedTime.TotalSeconds; + Assert.IsTrue(minMillionLinesPerSec < millionLinesPerSec && millionLinesPerSec < maxMillionLinesPerSec, + "Lines per second {0:f3} not between {1} and {2} (millions)", millionLinesPerSec, minMillionLinesPerSec, maxMillionLinesPerSec); + Console.WriteLine("Performance: {0} million lines per second", millionLinesPerSec); + } + [TestMethod] public void BuildVoxelGridPerformance() { diff --git a/Engine3D/Engine3D-Tests/large-model.3ds b/Engine3D/Engine3D-Tests/large-model.3ds new file mode 100644 index 0000000..3b844e5 Binary files /dev/null and b/Engine3D/Engine3D-Tests/large-model.3ds differ diff --git a/Engine3D/Raytrace/AxisAlignedBox.cs b/Engine3D/Raytrace/AxisAlignedBox.cs index 17187cb..9c73355 100644 --- a/Engine3D/Raytrace/AxisAlignedBox.cs +++ b/Engine3D/Raytrace/AxisAlignedBox.cs @@ -114,6 +114,7 @@ public IntersectionInfo IntersectLineSegment(Vector start, Vector end) IntersectionInfo closest = new IntersectionInfo(); closest.rayFrac = double.MaxValue; + // TODO: somehow intersect line against all planes at once, or guess at nearest plane? foreach (Plane plane in planes) { // Does ray intersect this plane? @@ -210,6 +211,11 @@ public bool ClipLineSegment(ref Vector start, ref Vector end) // so original line segment must intersect box at two points. intersection = IntersectLineSegment(end, originalStart); Contract.Assume(intersection != null, "Intersection must exist"); + + // TODO: HACK! This should never happen - logic bug! + //if (intersection == null) + // return true; + end = intersection.pos; } return true; @@ -305,7 +311,7 @@ private void ProjectOntoAxis(Vector axis, out double intervalMin, out double int /// /// /// True iff triangle is 'outside' one of the planes - static private bool IsTrianglesOutsidePlanes(Triangle tri, IEnumerable planes) + private static bool IsTrianglesOutsidePlanes(Triangle tri, IEnumerable planes) { // TODO: a triangle can be 'inside' all planes (each plane has at least one triangle vertex 'inside' the plane), // yet the triangle can still not intersect the volume defined by the planes.