diff --git a/CHANGELOG.md b/CHANGELOG.md index 10394cefa..e391a13da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,12 +3,15 @@ ## 1.2.0 ### Added - -- Message class along with helper creation methods. +- `Mesh.Intersects(Ray)` (same as `Ray.Intersects(Mesh)`) +- `Ray.NearbyPoints()` +- `PointOctree` +- `Message` class along with helper creation methods. ### Changed - MeshElement constructor signature modified to be compatible with code generation. +- Improved performance of mesh/ray intersection - `BBox3.Extend` method is public now - `AdaptiveGrid.Boundary` can be left null. - `Obstacle` properties `Points`, `Offset`, `Perimeter` and `Transform` can be modified from outside. diff --git a/Elements.Benchmarks/Mesh.cs b/Elements.Benchmarks/Mesh.cs new file mode 100644 index 000000000..3ba82dbb5 --- /dev/null +++ b/Elements.Benchmarks/Mesh.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using BenchmarkDotNet.Attributes; +using Elements.Geometry; + +namespace Elements.Benchmarks +{ + public class MeshRayIntersection + { + private Mesh _mesh; + private List _rays; + + public MeshRayIntersection() + { + var random = new Random(10); + _mesh = new Mesh(); + _rays = new List(); + var xCount = 100; + var yCount = 300; + MeshConstruction.BuildRandomMesh(_mesh, random, xCount, yCount); + + // create 1000 random rays + for (int i = 0; i < 1000; i++) + { + var ray = new Ray(new Vector3(random.NextDouble() * xCount, random.NextDouble() * yCount, 2.1), new Vector3(random.NextDouble() * 2 - 1, random.NextDouble() * 2 - 1, -1)); + _rays.Add(ray); + } + } + + + [Benchmark(Description = "Intersect 1000 rays with mesh.")] + public void IntersectRays() + { + foreach (var ray in _rays) + { + ray.Intersects(_mesh, out var _); + } + } + } + public class MeshConstruction + { + public static void BuildRandomMesh(Mesh m, Random random, int xCount, int yCount) + { + for (int i = 0; i < xCount; i++) + { + for (int j = 0; j < yCount; j++) + { + var point = new Vector3(i, j, random.NextDouble() * 2); + var c = m.AddVertex(point); + if (i != 0 && j != 0) + { + // add faces + var d = m.Vertices[i * yCount + j - 1]; + var a = m.Vertices[(i - 1) * yCount + j - 1]; + var b = m.Vertices[(i - 1) * yCount + j]; + m.AddTriangle(a, b, c); + m.AddTriangle(c, d, a); + } + } + } + } + + [Params(1000, 5000, 10000, 30000)] + public int VertexCount { get; set; } + + [Benchmark(Description = "Construct Mesh")] + public void ConstructMesh() + { + var mesh = new Mesh(); + BuildRandomMesh(mesh, new Random(10), 100, VertexCount / 100); + } + + } +} \ No newline at end of file diff --git a/Elements/src/Geometry/BBox3.cs b/Elements/src/Geometry/BBox3.cs index 0ce618551..e88b21f5c 100644 --- a/Elements/src/Geometry/BBox3.cs +++ b/Elements/src/Geometry/BBox3.cs @@ -94,7 +94,7 @@ public BBox3(IEnumerable points) /// Extend a bounding box with a new point /// /// The point which should be inside the extended bounding box - public void Extend(Vector3 point) + public BBox3 Extend(Vector3 point) { var newMin = new Vector3(Min.X, Min.Y, Min.Z); if (point.X < this.Min.X) newMin.X = point.X; @@ -107,6 +107,7 @@ public void Extend(Vector3 point) if (point.Y > this.Max.Y) newMax.Y = point.Y; if (point.Z > this.Max.Z) newMax.Z = point.Z; this.Max = newMax; + return this; } /// diff --git a/Elements/src/Geometry/Line.cs b/Elements/src/Geometry/Line.cs index 6da482692..5e85e8625 100644 --- a/Elements/src/Geometry/Line.cs +++ b/Elements/src/Geometry/Line.cs @@ -1,5 +1,3 @@ -using System.Net.Sockets; -using System.Numerics; using Elements.Validators; using System; using System.Collections.Generic; @@ -510,7 +508,7 @@ public static bool PointOnLine(Vector3 point, Vector3 start, Vector3 end, bool i var delta = end - start; var lambda = (point - start).Dot(delta) / (end - start).Dot(delta); - if( lambda > 0 && lambda < 1) + if (lambda > 0 && lambda < 1) { var pointOnLine = start + lambda * delta; return pointOnLine.IsAlmostEqualTo(point); diff --git a/Elements/src/Geometry/Mesh.cs b/Elements/src/Geometry/Mesh.cs index c2ac78b1b..ce5c1ffd1 100644 --- a/Elements/src/Geometry/Mesh.cs +++ b/Elements/src/Geometry/Mesh.cs @@ -1,7 +1,7 @@ +using Elements.Search; using Elements.Serialization.JSON; using LibTessDotNet.Double; using Newtonsoft.Json; -using Octree; using System; using System.Collections.Generic; using System.IO; @@ -16,16 +16,24 @@ namespace Elements.Geometry [JsonConverter(typeof(MeshConverter))] public partial class Mesh { - private PointOctree _octree = new PointOctree(100000, new Octree.Point(0f, 0f, 0f), (float)Vector3.EPSILON); - /// The mesh' vertices. + private double _maxTriangleSize = 0; + private PointOctree _octree = null; + + /// The mesh's vertices. [JsonProperty("Vertices", Required = Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public IList Vertices { get; set; } - /// The mesh' triangles. + /// The mesh's triangles. [JsonProperty("Triangles", Required = Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public IList Triangles { get; set; } + private BBox3 _bbox = new BBox3(); + /// + /// The mesh's bounding box. + /// + public BBox3 BoundingBox => _bbox; + /// /// Construct a mesh. /// @@ -34,8 +42,16 @@ public partial class Mesh [JsonConstructor] public Mesh(IList @vertices, IList @triangles) { - this.Vertices = @vertices; - this.Triangles = @triangles; + Vertices = new List(); + Triangles = new List(); + foreach (var v in @vertices) + { + AddVertex(v); + } + foreach (var t in @triangles) + { + AddTriangle(t); + } } /// @@ -182,10 +198,18 @@ public GraphicsBuffers GetBuffers() public Triangle AddTriangle(Vertex a, Vertex b, Vertex c) { var t = new Triangle(a, b, c); - if (t.HasDuplicatedVertices(out Vector3 duplicate)) + if (!Validators.Validator.DisableValidationOnConstruction && t.HasDuplicatedVertices(out Vector3 duplicate)) { throw new ArgumentException($"Not a valid Triangle. Duplicate vertex at {duplicate}."); } + for (int i = 0; i < 3; i++) + { + var sideLength = t.Vertices[i].Position.DistanceTo(t.Vertices[(i + 1) % 3].Position); + if (sideLength > this._maxTriangleSize) + { + this._maxTriangleSize = sideLength; + } + } this.Triangles.Add(t); return t; } @@ -200,6 +224,14 @@ public Triangle AddTriangle(Triangle t) { throw new ArgumentException($"Not a valid Triangle. Duplicate vertex at {duplicate}."); } + for (int i = 0; i < 3; i++) + { + var sideLength = t.Vertices[i].Position.DistanceTo(t.Vertices[(i + 1) % 3].Position); + if (sideLength > this._maxTriangleSize) + { + this._maxTriangleSize = sideLength; + } + } this.Triangles.Add(t); return t; } @@ -223,11 +255,12 @@ public Vertex AddVertex(Vector3 position, bool merge = false, double edgeAngle = 30.0) { - var p = new Octree.Point((float)position.X, (float)position.Y, (float)position.Z); + var v = new Vertex(position, normal, color); + if (merge) { - var search = this._octree.GetNearby(p, (float)Vector3.EPSILON); + var search = GetOctree().GetNearby(position, Vector3.EPSILON); if (search.Length > 0) { var angle = search[0].Normal.AngleTo(normal); @@ -237,12 +270,13 @@ public Vertex AddVertex(Vector3 position, } } } + // If the octree is null, do nothing — we'll build it when we need it. If we've already constructed it, let's keep it up to date. + this._octree?.Add(v, position); - var v = new Vertex(position, normal, color); v.UV = uv; this.Vertices.Add(v); v.Index = (this.Vertices.Count) - 1; - this._octree.Add(v, p); + this._bbox = this._bbox.Extend(v.Position); return v; } @@ -253,10 +287,27 @@ public Vertex AddVertex(Vector3 position, public Vertex AddVertex(Vertex v) { this.Vertices.Add(v); + // If the octree is null, do nothing — we'll build it when we need it. If we've already constructed it, let's keep it up to date. + this._octree?.Add(v, v.Position); + this._bbox = this._bbox.Extend(v.Position); v.Index = (this.Vertices.Count) - 1; return v; } + private PointOctree GetOctree() + { + if (_octree == null) + { + _octree = new PointOctree(Math.Max(_bbox.Max.DistanceTo(_bbox.Min), 100), _bbox.PointAt(0.5, 0.5, 0.5), Vector3.EPSILON); + // make sure existing vertices are added to the octree — we're initializing it for the first time + foreach (var v in Vertices) + { + _octree.Add(v, v.Position); + } + } + return _octree; + } + /// /// Calculate the volume of the mesh. /// This value will be inexact for open meshes. @@ -335,6 +386,28 @@ public List GetNakedBoundaries() return polygons; } + /// + /// Does the provided ray intersect this mesh mesh? + /// + /// The Ray to intersect. + /// The location of intersection. + /// True if an intersection result occurs. + /// False if no intersection occurs. + public bool Intersects(Ray ray, out Vector3 intersection) + { + var nearbyVertices = GetOctree().GetNearby(ray, _maxTriangleSize).ToList(); + var nearbyTriangles = nearbyVertices.SelectMany(v => v.Triangles).Distinct(); + intersection = default; + foreach (var t in nearbyTriangles) + { + if (ray.Intersects(t, out intersection)) + { + return true; + } + } + return false; + } + private double SignedVolumeOfTriangle(Triangle t) { var p1 = t.Vertices[0].Position; diff --git a/Elements/src/Geometry/Ray.cs b/Elements/src/Geometry/Ray.cs index 0835beb41..28358e47c 100644 --- a/Elements/src/Geometry/Ray.cs +++ b/Elements/src/Geometry/Ray.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using Elements.Geometry.Solids; +using Elements.Search; namespace Elements.Geometry { @@ -282,15 +283,7 @@ public bool Intersects(Topography topo, out Vector3 result) public bool Intersects(Mesh mesh, out Vector3 result) { - result = default; - foreach (var t in mesh.Triangles) - { - if (this.Intersects(t, out result)) - { - return true; - } - } - return false; + return mesh.Intersects(this, out result); } /// @@ -370,6 +363,24 @@ public bool Intersects(Vector3 start, Vector3 end, out Vector3 result) return false; } + /// + /// Find points in the collection that are within the provided distance of this ray. + /// + /// The collection of points to search + /// The maximum distance from the ray. + /// Points that are within the given distance of the ray. + public Vector3[] NearbyPoints(IEnumerable points, double distance) + { + // TODO: calibrate these values + var octree = new PointOctree(10000, (0, 0, 0), (float)Vector3.EPSILON * 100); + foreach (var point in points) + { + octree.Add(point, point); + } + var nearbyPoints = octree.GetNearby(this, (float)distance); + return nearbyPoints; + } + /// /// Is this ray equal to the provided ray? /// diff --git a/Elements/src/Search/Octree.cs b/Elements/src/Search/Octree.cs new file mode 100644 index 000000000..909a6bd77 --- /dev/null +++ b/Elements/src/Search/Octree.cs @@ -0,0 +1,146 @@ +using o = Octree; +using Elements.Geometry; +using System.Collections.Generic; + +namespace Elements.Search +{ + /// + /// A Dynamic Octree for storing any objects that can be described as a single point. This is a thin wrapper around the PointOctree class from NetOctree (https://github.com/mcserep/NetOctree). + /// + /// + /// Octree: An octree is a tree data structure which divides 3D space into smaller partitions (nodes) + /// and places objects into the appropriate nodes. This allows fast access to objects + /// in an area of interest without having to check every object. + /// + /// Dynamic: The octree grows or shrinks as required when objects as added or removed. + /// It also splits and merges nodes as appropriate. There is no maximum depth. + /// + /// The content of the octree can be anything, since the bounds data is supplied separately. + public class PointOctree + { + private readonly o.PointOctree _octree; + + /// + /// Constructor for the point octree. + /// + /// Size of the sides of the initial node. The octree will never shrink smaller than this. + /// Position of the center of the initial node. + /// Nodes will stop splitting if the new nodes would be smaller than this. + public PointOctree(double initialWorldSize, Vector3 initialWorldPos, double minNodeSize) + { + _octree = new o.PointOctree((float)initialWorldSize, initialWorldPos.ToOctreePoint(), (float)minNodeSize); + } + + /// + /// Returns all objects in the tree. + /// If none, returns an empty array (not null). + /// + /// All objects. + public ICollection GetAll() + { + return _octree.GetAll(); + } + + /// + /// Add an object. + /// + /// Object to add. + /// Position of the object. + public void Add(T obj, Vector3 objPos) + { + _octree.Add(obj, objPos.ToOctreePoint()); + } + + /// + /// Returns objects that are within of the specified ray. + /// If none, returns an empty array (not null). + /// + /// The ray. + /// Maximum distance from the ray to consider. + /// Objects within range. + public T[] GetNearby(Ray ray, double maxDistance) + { + return _octree.GetNearby(ray.ToOctreeRay(), (float)maxDistance); + } + + /// + /// Returns objects that are within of the specified position. + /// If none, returns an empty array (not null). + /// + /// The position. Passing as ref to improve performance since it won't have to be copied. + /// Maximum distance from the position to consider. + /// Objects within range. + public T[] GetNearby(Vector3 position, double maxDistance) + { + return _octree.GetNearby(position.ToOctreePoint(), (float)maxDistance); + } + + /// + /// The total amount of objects currently in the tree + /// + public int Count + { + get + { + return _octree.Count; + } + } + + /// + /// Gets the bounding box that represents the whole octree + /// + /// The bounding box of the root node. + public BBox3 MaxBounds + { + get + { + return _octree.MaxBounds.ToBbox3(); + } + } + + /// + /// Remove an object. Makes the assumption that the object only exists once in the tree. + /// + /// Object to remove. + /// True if the object was removed successfully. + public bool Remove(T obj) + { + return _octree.Remove(obj); + } + + /// + /// Removes the specified object at the given position. Makes the assumption that the object only exists once in the tree. + /// + /// Object to remove. + /// Position of the object. + /// True if the object was removed successfully. + public bool Remove(T obj, Vector3 objPos) + { + return _octree.Remove(obj, objPos.ToOctreePoint()); + } + } + + internal static class OctreeExtensions + { + internal static o.Ray ToOctreeRay(this Ray ray) + { + return new o.Ray(ray.Origin.ToOctreePoint(), ray.Direction.ToOctreePoint()); + } + + internal static o.Point ToOctreePoint(this Vector3 point) + { + return new o.Point((float)point.X, (float)point.Y, (float)point.Z); + } + + internal static BBox3 ToBbox3(this o.BoundingBox bbox) + { + return new BBox3(bbox.Min.ToVector3(), bbox.Max.ToVector3()); + } + + internal static Vector3 ToVector3(this o.Point p) + { + return new Vector3(p.X, p.Y, p.Z); + } + } + +} \ No newline at end of file diff --git a/Elements/test/MeshTests.cs b/Elements/test/MeshTests.cs index 85c18175d..c13cd3de1 100644 --- a/Elements/test/MeshTests.cs +++ b/Elements/test/MeshTests.cs @@ -1,4 +1,8 @@ +using System; using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; using Elements.Geometry; using Elements.Geometry.Solids; using Elements.Serialization.JSON; @@ -7,7 +11,7 @@ namespace Elements.Tests { - public class MeshTests + public class MeshTests : ModelTest { [Fact] public void Volume() @@ -45,6 +49,58 @@ public void ReadMeshSerializedAsNull() Newtonsoft.Json.JsonConvert.DeserializeObject(json, new[] { new MeshConverter() }); } + [Fact] + public void IntersectsRays() + { + Name = nameof(IntersectsRays); + var random = new Random(10); + var _mesh = new Mesh(); + var _rays = new List(); + var xCount = 100; + var yCount = 100; + for (int i = 0; i < xCount; i++) + { + for (int j = 0; j < yCount; j++) + { + var point = new Vector3(i, j, random.NextDouble() * 2); + var c = _mesh.AddVertex(point); + if (i != 0 && j != 0) + { + // add faces + var d = _mesh.Vertices[i * yCount + j - 1]; + var a = _mesh.Vertices[(i - 1) * yCount + j - 1]; + var b = _mesh.Vertices[(i - 1) * yCount + j]; + _mesh.AddTriangle(a, b, c); + _mesh.AddTriangle(c, d, a); + } + } + } + + // create 1000 random rays + for (int i = 0; i < 1000; i++) + { + var ray = new Ray(new Vector3(random.NextDouble() * (xCount - 1), random.NextDouble() * (yCount - 1), 5), new Vector3(0, 0, -1)); + _rays.Add(ray); + Model.AddElement(new ModelCurve(new Line(ray.Origin, ray.Origin + ray.Direction * 0.1), BuiltInMaterials.XAxis)); + } + _mesh.ComputeNormals(); + Model.AddElement(new MeshElement(_mesh) { Material = new Material("b") { Color = (0.6, 0.6, 0.6, 1), DoubleSided = true } }); + + var pts = new List(); + + foreach (var ray in _rays) + { + if (ray.Intersects(_mesh, out var p)) + { + pts.Add(p); + var l = new Line(p, ray.Origin); + Model.AddElement(l); + } + } + + Assert.Equal(_rays.Count, pts.Count); + } + public class InputsWithMesh { [JsonConstructor] diff --git a/Elements/test/RayTests.cs b/Elements/test/RayTests.cs index 6f0163a32..25abf2b8f 100644 --- a/Elements/test/RayTests.cs +++ b/Elements/test/RayTests.cs @@ -302,6 +302,34 @@ private static void RayIntersectsGeometryWithTransformation() Assert.True(ray.Intersects(mass, out var _)); } + [Fact] + private void RayNearbyPoints() + { + Name = nameof(RayNearbyPoints); + var points = new List(); + var random = new Random(1); + for (int i = 0; i < 1000; i++) + { + var point = new Vector3(random.NextDouble() * 10, random.NextDouble() * 10, random.NextDouble() * 10); + points.Add(point); + } + var modelpts = new ModelPoints(points, BuiltInMaterials.ZAxis); + Model.AddElement(modelpts); + var ray = new Ray((0, 0, 0), new Vector3(1, 1, 1)); + var nearbyPoints = ray.NearbyPoints(points, 1); + var rayAsLine = new Line((0, 0, 0), (10, 10, 10)); + Model.AddElement(rayAsLine); + foreach (var p in nearbyPoints) + { + var distance = p.DistanceTo(rayAsLine, out var pt); + var line = new Line(p, pt); + var mc = new ModelCurve(line, BuiltInMaterials.XAxis); + Model.AddElement(mc); + Assert.True(distance < 1); + } + + } + private static Vector3 Center(Triangle t) { return new Vector3[] { t.Vertices[0].Position, t.Vertices[1].Position, t.Vertices[2].Position }.Average();