diff --git a/Source/Engine/Tools/ModelTool/MeshAccelerationStructure.cpp b/Source/Engine/Tools/ModelTool/MeshAccelerationStructure.cpp new file mode 100644 index 000000000..aa04e4a6d --- /dev/null +++ b/Source/Engine/Tools/ModelTool/MeshAccelerationStructure.cpp @@ -0,0 +1,522 @@ +// Copyright (c) 2012-2022 Wojciech Figat. All rights reserved. + +#if COMPILE_WITH_MODEL_TOOL + +#include "MeshAccelerationStructure.h" +#include "Engine/Core/Math/Math.h" +#include "Engine/Content/Assets/Model.h" +#include "Engine/Profiler/ProfilerCPU.h" + +void MeshAccelerationStructure::BuildBVH(int32 node, int32 maxLeafSize, Array& scratch) +{ + auto& root = _bvh[node]; + ASSERT_LOW_LAYER(root.Leaf.IsLeaf); + if (root.Leaf.TriangleCount <= maxLeafSize) + return; + + // Spawn two leaves + const int32 childIndex = _bvh.Count(); + _bvh.AddDefault(2); + auto& left = _bvh.Get()[childIndex]; + auto& right = _bvh.Get()[childIndex + 1]; + left.Leaf.IsLeaf = 1; + right.Leaf.IsLeaf = 1; + left.Leaf.MeshIndex = root.Leaf.MeshIndex; + right.Leaf.MeshIndex = root.Leaf.MeshIndex; + + // Mid-point splitting based on the largest axis + Vector3 boundsSize; + root.Bounds.GetSize(boundsSize); + int32 axisCount = 0; + int32 axis = 0; +RETRY: + if (axisCount == 0) + { + // Pick the highest axis + axis = 0; + if (boundsSize.Y > boundsSize.X && boundsSize.Y >= boundsSize.Z) + axis = 1; + else if (boundsSize.Z > boundsSize.X) + axis = 2; + } + else if (axisCount == 3) + { + // Failed to split + _bvh.Resize(childIndex); + return; + } + else + { + // Go to the next axis + axis = (axis + 1) % 3; + } + const float midPoint = root.Bounds.Minimum.Raw[axis] + boundsSize.Raw[axis] * 0.5f; + const Mesh& meshData = _meshes[root.Leaf.MeshIndex]; + const Vector3* vb = meshData.VertexBuffer.Get(); + int32 indexStart = root.Leaf.TriangleIndex * 3; + int32 indexEnd = indexStart + root.Leaf.TriangleCount * 3; + left.Leaf.TriangleCount = 0; + right.Leaf.TriangleCount = 0; + if (meshData.Use16BitIndexBuffer) + { + struct Tri + { + uint16 I0, I1, I2; + }; + scratch.Resize(root.Leaf.TriangleCount * sizeof(Tri)); + auto dst = (Tri*)scratch.Get(); + auto ib16 = meshData.IndexBuffer.Get(); + for (int32 i = indexStart; i < indexEnd;) + { + const Tri tri = { ib16[i++], ib16[i++], ib16[i++] }; + const float v0 = vb[tri.I0].Raw[axis]; + const float v1 = vb[tri.I1].Raw[axis]; + const float v2 = vb[tri.I2].Raw[axis]; + const float centroid = (v0 + v1 + v2) * 0.333f; + if (centroid <= midPoint) + dst[left.Leaf.TriangleCount++] = tri; // Left + else + dst[root.Leaf.TriangleCount - ++right.Leaf.TriangleCount] = tri; // Right + } + Platform::MemoryCopy(ib16 + indexStart, dst, root.Leaf.TriangleCount * 3 * sizeof(uint16)); + if (left.Leaf.TriangleCount == 0 || right.Leaf.TriangleCount == 0) + { + axisCount++; + goto RETRY; + } + + left.Bounds = BoundingBox(vb[dst[0].I0]); + indexStart = 0; + indexEnd = left.Leaf.TriangleCount * 3; + for (int32 i = indexStart; i < indexEnd; i++) + left.Bounds.Merge(vb[((uint16*)scratch.Get())[i]]); + + right.Bounds = BoundingBox(vb[dst[root.Leaf.TriangleCount - 1].I0]); + indexStart = left.Leaf.TriangleCount; + indexEnd = root.Leaf.TriangleCount * 3; + for (int32 i = indexStart; i < indexEnd; i++) + right.Bounds.Merge(vb[((uint16*)scratch.Get())[i]]); + } + else + { + struct Tri + { + uint32 I0, I1, I2; + }; + scratch.Resize(root.Leaf.TriangleCount * sizeof(Tri)); + auto dst = (Tri*)scratch.Get(); + auto ib32 = meshData.IndexBuffer.Get(); + for (int32 i = indexStart; i < indexEnd;) + { + const Tri tri = { ib32[i++], ib32[i++], ib32[i++] }; + const float v0 = vb[tri.I0].Raw[axis]; + const float v1 = vb[tri.I1].Raw[axis]; + const float v2 = vb[tri.I2].Raw[axis]; + const float centroid = (v0 + v1 + v2) * 0.333f; + if (centroid <= midPoint) + dst[left.Leaf.TriangleCount++] = tri; // Left + else + dst[root.Leaf.TriangleCount - ++right.Leaf.TriangleCount] = tri; // Right + } + Platform::MemoryCopy(ib32 + indexStart, dst, root.Leaf.TriangleCount * 3 * sizeof(uint32)); + if (left.Leaf.TriangleCount == 0 || right.Leaf.TriangleCount == 0) + { + axisCount++; + goto RETRY; + } + + left.Bounds = BoundingBox(vb[dst[0].I0]); + indexStart = 0; + indexEnd = left.Leaf.TriangleCount * 3; + for (int32 i = indexStart; i < indexEnd; i++) + left.Bounds.Merge(vb[((uint32*)scratch.Get())[i]]); + + right.Bounds = BoundingBox(vb[dst[root.Leaf.TriangleCount - 1].I0]); + indexStart = left.Leaf.TriangleCount; + indexEnd = root.Leaf.TriangleCount * 3; + for (int32 i = indexStart; i < indexEnd; i++) + right.Bounds.Merge(vb[((uint32*)scratch.Get())[i]]); + } + ASSERT_LOW_LAYER(left.Leaf.TriangleCount + right.Leaf.TriangleCount == root.Leaf.TriangleCount); + left.Leaf.TriangleIndex = root.Leaf.TriangleIndex; + right.Leaf.TriangleIndex = left.Leaf.TriangleIndex + left.Leaf.TriangleCount; + + // Convert into a node + root.Node.IsLeaf = 0; + root.Node.ChildIndex = childIndex; + root.Node.ChildrenCount = 2; + + // Split children + BuildBVH(childIndex, maxLeafSize, scratch); + BuildBVH(childIndex + 1, maxLeafSize, scratch); +} + +bool MeshAccelerationStructure::PointQueryBVH(int32 node, const Vector3& point, float& hitDistance, Vector3& hitPoint, Triangle& hitTriangle) const +{ + const auto& root = _bvh[node]; + bool hit = false; + if (root.Leaf.IsLeaf) + { + // Find closest triangle + Vector3 p; + const Mesh& meshData = _meshes[root.Leaf.MeshIndex]; + const Vector3* vb = meshData.VertexBuffer.Get(); + const int32 indexStart = root.Leaf.TriangleIndex * 3; + const int32 indexEnd = indexStart + root.Leaf.TriangleCount * 3; + if (meshData.Use16BitIndexBuffer) + { + const uint16* ib16 = meshData.IndexBuffer.Get(); + for (int32 i = indexStart; i < indexEnd;) + { + Vector3 v0 = vb[ib16[i++]]; + Vector3 v1 = vb[ib16[i++]]; + Vector3 v2 = vb[ib16[i++]]; + CollisionsHelper::ClosestPointPointTriangle(point, v0, v1, v2, p); + const float distance = Vector3::Distance(point, p); + if (distance < hitDistance) + { + hitDistance = distance; + hitPoint = p; + hitTriangle = Triangle(v0, v1, v2); + hit = true; + } + } + } + else + { + const uint32* ib32 = meshData.IndexBuffer.Get(); + for (int32 i = indexStart; i < indexEnd;) + { + Vector3 v0 = vb[ib32[i++]]; + Vector3 v1 = vb[ib32[i++]]; + Vector3 v2 = vb[ib32[i++]]; + CollisionsHelper::ClosestPointPointTriangle(point, v0, v1, v2, p); + const float distance = Vector3::Distance(point, p); + if (distance < hitDistance) + { + hitDistance = distance; + hitPoint = p; + hitTriangle = Triangle(v0, v1, v2); + hit = true; + } + } + } + } + else + { + // Check all nested nodes + for (uint32 i = 0; i < root.Node.ChildrenCount; i++) + { + const int32 index = root.Node.ChildIndex + i; + if (_bvh[index].Bounds.Distance(point) >= hitDistance) + continue; + if (PointQueryBVH(index, point, hitDistance, hitPoint, hitTriangle)) + hit = true; + } + } + return hit; +} + +bool MeshAccelerationStructure::RayCastBVH(int32 node, const Ray& ray, float& hitDistance, Vector3& hitNormal, Triangle& hitTriangle) const +{ + const auto& root = _bvh[node]; + if (!root.Bounds.Intersects(ray)) + return false; + Vector3 normal; + float distance; + bool hit = false; + if (root.Leaf.IsLeaf) + { + // Ray cast along triangles in the leaf + const Mesh& meshData = _meshes[root.Leaf.MeshIndex]; + const Vector3* vb = meshData.VertexBuffer.Get(); + const int32 indexStart = root.Leaf.TriangleIndex * 3; + const int32 indexEnd = indexStart + root.Leaf.TriangleCount * 3; + if (meshData.Use16BitIndexBuffer) + { + const uint16* ib16 = meshData.IndexBuffer.Get(); + for (int32 i = indexStart; i < indexEnd;) + { + Vector3 v0 = vb[ib16[i++]]; + Vector3 v1 = vb[ib16[i++]]; + Vector3 v2 = vb[ib16[i++]]; + if (CollisionsHelper::RayIntersectsTriangle(ray, v0, v1, v2, distance, normal) && distance < hitDistance) + { + hitDistance = distance; + hitNormal = normal; + hitTriangle = Triangle(v0, v1, v2); + hit = true; + } + } + } + else + { + const uint32* ib32 = meshData.IndexBuffer.Get(); + for (int32 i = indexStart; i < indexEnd;) + { + Vector3 v0 = vb[ib32[i++]]; + Vector3 v1 = vb[ib32[i++]]; + Vector3 v2 = vb[ib32[i++]]; + if (CollisionsHelper::RayIntersectsTriangle(ray, v0, v1, v2, distance, normal) && distance < hitDistance) + { + hitDistance = distance; + hitNormal = normal; + hitTriangle = Triangle(v0, v1, v2); + hit = true; + } + } + } + } + else + { + // Ray cast all child nodes + Triangle triangle; + for (uint32 i = 0; i < root.Node.ChildrenCount; i++) + { + const int32 index = root.Node.ChildIndex + i; + distance = hitDistance; + if (RayCastBVH(index, ray, distance, normal, triangle) && distance < hitDistance) + { + hitDistance = distance; + hitNormal = normal; + hitTriangle = triangle; + hit = true; + } + } + } + return hit; +} + +void MeshAccelerationStructure::Add(Model* model, int32 lodIndex) +{ + PROFILE_CPU(); + lodIndex = Math::Clamp(lodIndex, model->HighestResidentLODIndex(), model->LODs.Count() - 1); + ModelLOD& lod = model->LODs[lodIndex]; + const int32 meshesStart = _meshes.Count(); + _meshes.AddDefault(lod.Meshes.Count()); + for (int32 i = 0; i < lod.Meshes.Count(); i++) + { + auto& mesh = lod.Meshes[i]; + auto& meshData = _meshes[meshesStart + i]; + if (model->IsVirtual()) + { + meshData.Indices = mesh.GetTriangleCount() * 3; + meshData.Vertices = mesh.GetVertexCount(); + mesh.DownloadDataGPU(MeshBufferType::Index, meshData.IndexBuffer); + mesh.DownloadDataGPU(MeshBufferType::Vertex0, meshData.VertexBuffer); + } + else + { + mesh.DownloadDataCPU(MeshBufferType::Index, meshData.IndexBuffer, meshData.Indices); + mesh.DownloadDataCPU(MeshBufferType::Vertex0, meshData.VertexBuffer, meshData.Vertices); + } + if (!meshData.IndexBuffer.IsAllocated() && meshData.IndexBuffer.Length() != 0) + { + // BVH nodes modifies index buffer (sorts data in-place) so clone it + meshData.IndexBuffer.Copy(meshData.IndexBuffer.Get(), meshData.IndexBuffer.Length()); + } + meshData.Use16BitIndexBuffer = mesh.Use16BitIndexBuffer(); + meshData.Bounds = mesh.GetBox(); + } +} + +void MeshAccelerationStructure::Add(Vector3* vb, int32 vertices, void* ib, int32 indices, bool use16BitIndex, bool copy) +{ + auto& meshData = _meshes.AddOne(); + if (copy) + { + meshData.VertexBuffer.Copy((const byte*)vb, vertices * sizeof(Vector3)); + } + else + { + meshData.VertexBuffer.Link((const byte*)vb, vertices * sizeof(Vector3)); + } + meshData.IndexBuffer.Copy((const byte*)ib, indices * (use16BitIndex ? sizeof(uint16) : sizeof(uint32))); + meshData.Vertices = vertices; + meshData.Indices = indices; + meshData.Use16BitIndexBuffer = use16BitIndex; +} + +void MeshAccelerationStructure::BuildBVH(int32 maxLeafSize) +{ + if (_meshes.Count() == 0) + return; + PROFILE_CPU(); + + // Estimate memory usage + int32 trianglesCount = 0; + for (const Mesh& meshData : _meshes) + trianglesCount += meshData.Indices / 3; + _bvh.Clear(); + _bvh.EnsureCapacity(trianglesCount / maxLeafSize); + + // Init with the root node and all meshes as leaves + auto& root = _bvh.AddOne(); + root.Node.IsLeaf = 0; + root.Node.ChildIndex = 1; + root.Node.ChildrenCount = _meshes.Count(); + root.Bounds = _meshes[0].Bounds; + for (int32 i = 0; i < _meshes.Count(); i++) + { + const Mesh& meshData = _meshes[i]; + auto& child = _bvh.AddOne(); + child.Leaf.IsLeaf = 1; + child.Leaf.MeshIndex = i; + child.Leaf.TriangleIndex = 0; + child.Leaf.TriangleCount = meshData.Indices / 3; + child.Bounds = meshData.Bounds; + BoundingBox::Merge(root.Bounds, meshData.Bounds, root.Bounds); + } + + // Sub-divide mesh nodes into smaller leaves + Array scratch; + for (int32 i = 0; i < _meshes.Count(); i++) + BuildBVH(i + 1, maxLeafSize, scratch); +} + +bool MeshAccelerationStructure::PointQuery(const Vector3& point, float& hitDistance, Vector3& hitPoint, Triangle& hitTriangle, float maxDistance) const +{ + hitDistance = maxDistance >= MAX_float ? maxDistance : maxDistance * maxDistance; + bool hit = false; + + // BVH + if (_bvh.Count() != 0) + { + Array> stack; + stack.Push(0); + while (stack.HasItems()) + { + const int32 node = stack.Pop(); + auto& root = _bvh[node]; + + // Skip too far nodes + if (root.Bounds.Distance(point) >= hitDistance) + continue; + + if (root.Leaf.IsLeaf) + { + // Check this leaf + hit |= PointQueryBVH(node, point, hitDistance, hitPoint, hitTriangle); + } + else + { + // Check this node children + for (uint32 i = 0; i < root.Node.ChildrenCount; i++) + stack.Push(root.Node.ChildIndex + i); + } + } + //hit = PointQueryBVH(0, point, hitDistance, hitPoint, hitTriangle); + return hit; + } + + // Brute-force + { + Vector3 p; + for (const Mesh& meshData : _meshes) + { + const Vector3* vb = meshData.VertexBuffer.Get(); + if (meshData.Use16BitIndexBuffer) + { + const uint16* ib16 = meshData.IndexBuffer.Get(); + for (int32 i = 0; i < meshData.Indices;) + { + Vector3 v0 = vb[ib16[i++]]; + Vector3 v1 = vb[ib16[i++]]; + Vector3 v2 = vb[ib16[i++]]; + CollisionsHelper::ClosestPointPointTriangle(point, v0, v1, v2, p); + const float distance = Vector3::DistanceSquared(point, p); + if (distance < hitDistance) + { + hitDistance = distance; + hitPoint = p; + hitTriangle = Triangle(v0, v1, v2); + hit = true; + } + } + } + else + { + const uint32* ib32 = meshData.IndexBuffer.Get(); + for (int32 i = 0; i < meshData.Indices;) + { + Vector3 v0 = vb[ib32[i++]]; + Vector3 v1 = vb[ib32[i++]]; + Vector3 v2 = vb[ib32[i++]]; + CollisionsHelper::ClosestPointPointTriangle(point, v0, v1, v2, p); + const float distance = Vector3::DistanceSquared(point, p); + if (distance < hitDistance) + { + hitDistance = distance; + hitPoint = p; + hitTriangle = Triangle(v0, v1, v2); + hit = true; + } + } + } + } + if (hit) + hitDistance = Math::Sqrt(hitDistance); + return hit; + } +} + +bool MeshAccelerationStructure::RayCast(const Ray& ray, float& hitDistance, Vector3& hitNormal, Triangle& hitTriangle, float maxDistance) const +{ + hitDistance = maxDistance; + + // BVH + if (_bvh.Count() != 0) + { + return RayCastBVH(0, ray, hitDistance, hitNormal, hitTriangle); + } + + // Brute-force + { + Vector3 normal; + float distance; + bool hit = false; + for (const Mesh& meshData : _meshes) + { + if (!meshData.Bounds.Intersects(ray)) + continue; + const Vector3* vb = meshData.VertexBuffer.Get(); + if (meshData.Use16BitIndexBuffer) + { + const uint16* ib16 = meshData.IndexBuffer.Get(); + for (int32 i = 0; i < meshData.Indices;) + { + Vector3 v0 = vb[ib16[i++]]; + Vector3 v1 = vb[ib16[i++]]; + Vector3 v2 = vb[ib16[i++]]; + if (CollisionsHelper::RayIntersectsTriangle(ray, v0, v1, v2, distance, normal) && distance < hitDistance) + { + hitDistance = distance; + hitNormal = normal; + hitTriangle = Triangle(v0, v1, v2); + hit = true; + } + } + } + else + { + const uint32* ib32 = meshData.IndexBuffer.Get(); + for (int32 i = 0; i < meshData.Indices;) + { + Vector3 v0 = vb[ib32[i++]]; + Vector3 v1 = vb[ib32[i++]]; + Vector3 v2 = vb[ib32[i++]]; + if (CollisionsHelper::RayIntersectsTriangle(ray, v0, v1, v2, distance, normal) && distance < hitDistance) + { + hitDistance = distance; + hitNormal = normal; + hitTriangle = Triangle(v0, v1, v2); + hit = true; + } + } + } + } + return hit; + } +} + +#endif diff --git a/Source/Engine/Tools/ModelTool/MeshAccelerationStructure.h b/Source/Engine/Tools/ModelTool/MeshAccelerationStructure.h new file mode 100644 index 000000000..8e3684d63 --- /dev/null +++ b/Source/Engine/Tools/ModelTool/MeshAccelerationStructure.h @@ -0,0 +1,75 @@ +// Copyright (c) 2012-2022 Wojciech Figat. All rights reserved. + +#pragma once + +#if COMPILE_WITH_MODEL_TOOL + +#include "Engine/Core/Math/Triangle.h" +#include "Engine/Core/Math/BoundingBox.h" +#include "Engine/Core/Types/DataContainer.h" +#include "Engine/Core/Collections/Array.h" + +class Model; + +/// +/// Acceleration Structure utility for robust ray tracing mesh geometry with optimized data structure. +/// +class FLAXENGINE_API MeshAccelerationStructure +{ +private: + struct Mesh + { + BytesContainer IndexBuffer, VertexBuffer; + int32 Indices, Vertices; + bool Use16BitIndexBuffer; + BoundingBox Bounds; + }; + + struct BVH + { + BoundingBox Bounds; + + union + { + struct + { + uint32 IsLeaf : 1; + uint16 TriangleCount : 15; + uint16 MeshIndex : 16; + uint32 TriangleIndex; + } Leaf; + + struct + { + uint32 IsLeaf : 1; + uint32 ChildrenCount : 31; + int32 ChildIndex; + } Node; + }; + }; + + Array> _meshes; + Array _bvh; + + void BuildBVH(int32 node, int32 maxLeafSize, Array& scratch); + bool PointQueryBVH(int32 node, const Vector3& point, float& hitDistance, Vector3& hitPoint, Triangle& hitTriangle) const; + bool RayCastBVH(int32 node, const Ray& ray, float& hitDistance, Vector3& hitNormal, Triangle& hitTriangle) const; + +public: + // Adds the model geometry for the build to the structure. + void Add(Model* model, int32 lodIndex); + + // Adds the triangles geometry for the build to the structure. + void Add(Vector3* vb, int32 vertices, void* ib, int32 indices, bool use16BitIndex, bool copy = false); + + // Builds Bounding Volume Hierarchy (BVH) structure for accelerated geometry queries. + void BuildBVH(int32 maxLeafSize = 16); + + // Queries the closest triangle. + bool PointQuery(const Vector3& point, float& hitDistance, Vector3& hitPoint, Triangle& hitTriangle, float maxDistance = MAX_float) const; + + // Ray traces the triangles. + bool RayCast(const Ray& ray, float& hitDistance, Vector3& hitNormal, Triangle& hitTriangle, float maxDistance = MAX_float) const; +}; + +#endif