2025·
C#BVHSpatial AccelerationCollision DetectionGPU Instancing

Code Example — BVH Generation

Bounding Volume Hierarchy generation for 3D meshes — collision and ray-cast acceleration structures.

Code Example — BVH Generation

The BVHGeneration script is designed to create Bounding Volume Hierarchy (BVH) data for a given 3D mesh. In this case, the generated BVH structures are instanced on the GPU and used for efficient spatial queries for collision between particles and complex 3D meshes. BVHs allow efficient elimination of large, non-relevant areas of a mesh, significantly reducing the number of intersection tests required, helping to efficiently resolve collisions.

BVH visualisation
using System;
using System.Collections.Generic;
using UnityEngine;

public class BVHGeneration : MonoBehaviour
{
// Public variables exposed in the inspector
[Header("References")]
public GameObject bvhTargetObject; // the object the BVH tree will be generated for

[Header("Settings")]
public int maxTrisPerLeafNode;  // Maximum number of triangles per leaf node
public bool generateBVH;        // Used to initate the generation of the BVH data by the user

[Header("Debug Settings")]
public bool debugNodes;         // Toggle to debug nodes
public int debugNodeDepth = 0;  // Depth of the nodes to debug

// Private variables
private Mesh targetMesh; // Mesh of the test object
private Vector3[] verts; // Array of vertices in the mesh

// Enum to represent the axis
private enum AxisEnum
{
    x,
    y,
    z
}

// Lists to hold BVH data
private List<int> leafNodeTris = new List<int>();                // List of triangle indexes in leaf nodes only, used for assembling tri indices array for use on the GPU
private List<BVHNodeGPU> GPUBVHNodes = new List<BVHNodeGPU>();   // List of BVH nodes, structured for use on the GPU

// Instance of BVHSaveAndLoad class
private BVHSaveAndLoad BVHSaveAndLoad;

// Lists to manage BVH nodes
[Header("BVH Nodes Management")]
private List<BVHNode> nodes = new List<BVHNode>();                          // All generated BVH nodes
private List<BVHNode> nodesToSplitNow = new List<BVHNode>();                // Nodes that need immediate splitting
private List<BVHNode> nodesToSplitLater = new List<BVHNode>();              // Nodes deferred for splitting to maintain loop integrity
private List<BVHNode> nodesToRemoveFromSplitList = new List<BVHNode>();     // Nodes to remove from 'nodesToSplitNow' after splitting, maintain loop integrity

// BVH node
[Serializable] public class BVHNode
{
    public Vector3 minBounds;                       // min bounds of the node AABB
    public Vector3 maxBounds;                       // min bounds of the node AABB
    public uint leftChildIndex = uint.MaxValue;     // the index of the left child node
    public uint rightChildIndex = uint.MaxValue;    // the index of the right child node
    public uint startTriangleIndex;                 // the index of the nodes first triangle index in triangle indices
    public uint triangleCount;                      // the number of triangles within the node
    public List<NodeTriangle> tris;                 // list of triangles within the node
}

// GPU version of a BVH node, restructured to remove list of tris so it works on GPU (the tri indices are combined into one single buffer anyway, so we only need the start index and tri count to be able to access all of the nodes tris)
[Serializable] public struct BVHNodeGPU
{
    public Vector3 minBounds;
    public Vector3 maxBounds;
    public uint leftChildIndex;
    public uint rightChildIndex;
    public uint startTriangleIndex;
    public uint triangleCount;
}

// triangle data required for triangles in a node
[Serializable] public struct NodeTriangle
{
    // index to vertices in "verts"
    public int index1;
    public int index2;
    public int index3;

    public Vector3 centroid;

    // min and max bounds of the triangle
    public float minX;
    public float maxX;
    public float minY;
    public float maxY;
    public float minZ;
    public float maxZ;
}

private void Start()
{
    BVHSaveAndLoad = GetComponent<BVHSaveAndLoad>();
}

void Update()
{
    // Check if BVH generation test is enabled
    if (generateBVH)
    {
        // Update the mesh reference from the test object
        targetMesh = bvhTargetObject.GetComponent<MeshFilter>().sharedMesh;

        // Generate the BVH for the mesh
        GenerateBVH(targetMesh);

        // Populate leaf node triangles and GPU BVH nodes list
        ConvertBVHDataForGPU();

        // Save the generated BVH data to a binary file
        SaveBVHData(targetMesh);

        // Disable the BVH generation test toggle
        generateBVH = false;
    }

    // Check if node debugging is enabled
    if (debugNodes)
    {
        // Debug nodes at the specified node depth
        DebugNodes(debugNodeDepth);

        // Disable the node debugging toggle
        debugNodes = false;
    }
}

/// <summary>
/// Saves the BVH (Bounding Volume Hierarchy) data for the given mesh.
/// </summary>
/// <param name="mesh">The mesh for which the BVH data is being saved.</param>
void SaveBVHData(Mesh mesh)
{
    // Create a new BVHData instance
    BVHSaveAndLoad.BVHData bVHData = new BVHSaveAndLoad.BVHData();

    // Set the mesh index in the BVHData instance. The mesh name is used to lookup the mesh index, if not alread created, a new mesh index will be generated
    bVHData.meshIndex = BVHSaveAndLoad.GetMeshIndex(mesh.name);

    // Convert and assign GPU BVH nodes list to an array
    bVHData.Nodes = GPUBVHNodes.ToArray();

    // Convert and assign leaf node triangle indices list to an array
    bVHData.TriangleIndexes = leafNodeTris.ToArray();

    // Assign the vertices of the test mesh to the BVHData instance
    bVHData.Vertices = targetMesh.vertices;

    // Save the BVH data to binary file, using the BVHSaveAndLoad instance
    BVHSaveAndLoad.SaveBVHData(bVHData);
}

/// <summary>
/// Converts BVH data into a format suitable for GPU processing by populating the lists of leaf node triangles and GPU BVH nodes.
/// </summary>
void ConvertBVHDataForGPU()
{
    // Clear existing data
    GPUBVHNodes.Clear();
    leafNodeTris.Clear();
    int leafNodeTriCount = 0;

    // Iterate through each node in the list
    for (int i = 0; i < nodes.Count; i++)
    {
        BVHNode node = nodes[i];

        // Check if the current node is a leaf node (this is true where the index is set to "uint.MaxValue")
        if (node.leftChildIndex == uint.MaxValue)
        {
            // Update the start triangle index and triangle count for the leaf node
            node.startTriangleIndex = (uint)leafNodeTriCount;
            node.triangleCount = (uint)node.tris.Count;

            // Add the indices of each triangle in the leaf node to the leafNodeTris list
            foreach (var tri in node.tris)
            {
                leafNodeTris.Add(tri.index1);
                leafNodeTris.Add(tri.index2);
                leafNodeTris.Add(tri.index3);
                leafNodeTriCount += 3;
            }
        }

        // Create a GPU BVH node from the current node
        BVHNodeGPU bvhNodeGPU = new BVHNodeGPU
        {
            minBounds = node.minBounds,
            maxBounds = node.maxBounds,
            leftChildIndex = node.leftChildIndex,
            rightChildIndex = node.rightChildIndex,
            startTriangleIndex = node.startTriangleIndex,
            triangleCount = node.triangleCount
        };

        // Add the GPU BVH node to the list
        GPUBVHNodes.Add(bvhNodeGPU);
    }
}

/// <summary>
/// Generates the root BVH node from the given mesh.
/// </summary>
/// <param name="mesh">The mesh for which the BVH root node is generated.</param>
/// <returns>The generated root BVH node.</returns>
BVHNode GenerateRootNode(Mesh mesh)
{
    // Create a new BVH node and add it to the nodes and nodesToSplitNow lists
    BVHNode node = new BVHNode();
    nodes.Add(node);
    nodesToSplitNow.Add(node);

    // Extract vertex and triangle indices from the mesh
    int[] triIndices = mesh.triangles;
    verts = mesh.vertices;

    // List to hold triangle centroids
    List<NodeTriangle> nodeTriangles = new List<NodeTriangle>();

    // Calculate the axis-aligned bounding box (AABB) for the root node
    CalculateAABBRootNode(triIndices, verts, out node.minBounds, out node.maxBounds);

    // Assemble the centroid list from the triangle indices
    for (int i = 0; i < triIndices.Length; i += 3)
    {
        int vertIndex1 = triIndices[i];
        int vertIndex2 = triIndices[i + 1];
        int vertIndex3 = triIndices[i + 2];

        NodeTriangle nodeTriangle = new NodeTriangle
        {
            index1 = vertIndex1,
            index2 = vertIndex2,
            index3 = vertIndex3,
            centroid = (verts[vertIndex1] + verts[vertIndex2] + verts[vertIndex3]) / 3,
            minX = Mathf.Min(verts[vertIndex1].x, verts[vertIndex2].x, verts[vertIndex3].x),
            maxX = Mathf.Max(verts[vertIndex1].x, verts[vertIndex2].x, verts[vertIndex3].x),
            minY = Mathf.Min(verts[vertIndex1].y, verts[vertIndex2].y, verts[vertIndex3].y),
            maxY = Mathf.Max(verts[vertIndex1].y, verts[vertIndex2].y, verts[vertIndex3].y),
            minZ = Mathf.Min(verts[vertIndex1].z, verts[vertIndex2].z, verts[vertIndex3].z),
            maxZ = Mathf.Max(verts[vertIndex1].z, verts[vertIndex2].z, verts[vertIndex3].z)
        };

        // Add the calculated triangle centroid to the list
        nodeTriangles.Add(nodeTriangle);
    }

    // Assign the list of triangle centroids to the node
    node.tris = nodeTriangles;

    return node;
}

/// <summary>
/// Generates the BVH (Bounding Volume Hierarchy) from the given mesh.
/// </summary>
/// <param name="mesh">The mesh to generate the BVH for.</param>
void GenerateBVH(Mesh mesh)
{
    // Clear existing nodes and nodes to split
    nodes.Clear();
    nodesToSplitNow.Clear();

    // Generate the root node from the mesh
    GenerateRootNode(mesh);

    int currentDepth = 0;

    // Continue splitting nodes until there are none left to split
    while (nodesToSplitNow.Count > 0)
    {
        currentDepth++;

        // Split each node in the current list of nodes to be split
        foreach (BVHNode nodeToSplit in nodesToSplitNow)
        {
            // Get the optimal slice position and axis for splitting the node
            (float slicePos, AxisEnum axis) = GetSlicePositionAndAxis(nodeToSplit);

            // Split the node at the determined slice position
            SplitNodeAtCut(nodeToSplit, axis, slicePos);
        }

        // Remove nodes that have been split from the current list
        foreach (BVHNode splitNode in nodesToRemoveFromSplitList)
        {
            nodesToSplitNow.Remove(splitNode);
        }
        nodesToRemoveFromSplitList.Clear();

        // Add nodes that need to be split in the next iteration
        nodesToSplitNow.AddRange(nodesToSplitLater);
        nodesToSplitLater.Clear();

        // Break if the depth exceeds the maximum allowed value to prevent infinite loops
        if (currentDepth > 30)
        {
            Debug.LogError("Error: BVH generation exceeded maximum tree depth. Breaking loop to prevent infinite iteration.");
            break;
        }
    }
}

/// <summary>
/// Evaluates the split plane for a given axis and updates the lowest cost and slice position if a better split is found.
/// </summary>
/// <param name="node">The BVH node to split.</param>
/// /// <param name="axis">The axis to split on.</param>
/// <param name="slicePos">The position of the slice, on the given axis.</param>
/// <param name="lowestCost">The current lowest cost for the given axis, updated if a better split is found.</param>
/// <param name="lowestCostSlice">The slice position corresponding to the lowest cost, updated if a better split is found.</param>
void EvaluateSlice(BVHNode node, AxisEnum axis, float slicePos, ref float lowestCost, ref float lowestCostSlice)
{
    if ((axis == AxisEnum.x && (slicePos <= node.minBounds.x || slicePos >= node.maxBounds.x)) ||
        (axis == AxisEnum.y && (slicePos <= node.minBounds.y || slicePos >= node.maxBounds.y)) ||
        (axis == AxisEnum.z && (slicePos <= node.minBounds.z || slicePos >= node.maxBounds.z)))
    {
        return;
    }

    // Calculate sliced AABB bounds for children A and B
    Vector3 childAMin = node.minBounds;
    Vector3 childAMax = node.maxBounds;
    Vector3 childBMin = node.minBounds;
    Vector3 childBMax = node.maxBounds;

    if (axis == AxisEnum.x)
    {
        childAMax.x = slicePos;
        childBMin.x = slicePos;
    }
    else if (axis == AxisEnum.y)
    {
        childAMax.y = slicePos;
        childBMin.y = slicePos;
    }
    else if (axis == AxisEnum.z)
    {
        childAMax.z = slicePos;
        childBMin.z = slicePos;
    }

    // Assemble triangle lists for each child
    List<NodeTriangle> childATris = new List<NodeTriangle>();
    List<NodeTriangle> childBTris = new List<NodeTriangle>();

    // Assemble list of triangles that intersect the AABBs
    foreach (var tri in node.tris)
    {
        Vector3 vert1 = verts[tri.index1];
        Vector3 vert2 = verts[tri.index2];
        Vector3 vert3 = verts[tri.index3];

        // if triangle intersects the child A bounding box, add the tri to child A tris
        if (TriangleAABBIntersection.TriangleIntersectsAABB(vert1, vert2, vert3, childAMin, childAMax))
        {
            childATris.Add(tri);
        }
        // if triangle intersects the child B bounding box, add the tri to child B tris
        else if (TriangleAABBIntersection.TriangleIntersectsAABB(vert1, vert2, vert3, childBMin, childBMax))
        {
            childBTris.Add(tri);
        }
    }

    if (childATris.Count != 0 && childBTris.Count != 0)
    {
        // Clip the node A bounds based on an AABB that encapsulates all of its triangles
        Vector3 min = Vector3.positiveInfinity;
        Vector3 max = Vector3.negativeInfinity;
        CalculateAABBForTriangles(childATris, ref min, ref max, false);
        childAMin = min;
        childAMax = max;

        // Clip the node B bounds based on an AABB that encapsulates all of its triangles
        min = Vector3.positiveInfinity;
        max = Vector3.negativeInfinity;
        CalculateAABBForTriangles(childBTris, ref min, ref max, false);
        childBMin = min;
        childBMax = max;

        float costAtSplit = CalculateSAHCost(node.minBounds, node.maxBounds, childAMin, childAMax, childBMin, childBMax, childATris.Count, childBTris.Count);

        if (costAtSplit < lowestCost)
        {
            lowestCost = costAtSplit;
            lowestCostSlice = slicePos;
        }
    }
}

/// <summary>
/// Evaluates the cost of splitting a BVH node along a given axis at various positions,
/// and updates the lowest cost and corresponding slice position if a better split is found.
/// </summary>
/// <param name="node">The BVH node to evaluate splits for.</param>
/// <param name="axis">The axis to split along (x, y, or z).</param>
/// <param name="lowestCost">Reference to the current lowest cost, updated if a better split is found.</param>
/// <param name="lowestCostSlice">Reference to the slice position corresponding to the lowest cost, updated if a better split is found.</param>
void EvaluateSlices(BVHNode node, AxisEnum axis, ref float lowestCost, ref float lowestCostSlice)
{
    // Iterate through each triangle in the node
    foreach (var tri in node.tris)
    {
        // Evaluate the cost of splitting at the centroid position of the triangle
        float slicePos = axis switch
        {
            AxisEnum.x => tri.centroid.x,
            AxisEnum.y => tri.centroid.y,
            AxisEnum.z => tri.centroid.z,
            _ => throw new ArgumentOutOfRangeException()
        };
        EvaluateSlice(node, axis, slicePos, ref lowestCost, ref lowestCostSlice);

        // Evaluate the cost of splitting at each vertex position of the triangle
        int[] indices = { tri.index1, tri.index2, tri.index3 };
        foreach (int index in indices)
        {
            slicePos = axis switch
            {
                AxisEnum.x => verts[index].x,
                AxisEnum.y => verts[index].y,
                AxisEnum.z => verts[index].z,
                _ => throw new ArgumentOutOfRangeException()
            };
            EvaluateSlice(node, axis, slicePos, ref lowestCost, ref lowestCostSlice);
        }
    }
}

/// <summary>
/// Determines the optimal slice position and axis for splitting a BVH node by evaluating the cost of splits along the x, y, and z axes.
/// </summary>
/// <param name="node">The BVH node to evaluate for splitting.</param>
/// <returns>A tuple containing the optimal slice position and the corresponding axis.</returns>
(float, AxisEnum) GetSlicePositionAndAxis(BVHNode node)
{
    // Initialize variables for the lowest cost and slice positions
    float lowestCostXSlice = float.PositiveInfinity;
    float lowestCostYSlice = float.PositiveInfinity;
    float lowestCostZSlice = float.PositiveInfinity;
    float lowestCostX = float.PositiveInfinity;
    float lowestCostY = float.PositiveInfinity;
    float lowestCostZ = float.PositiveInfinity;

    // Evaluate slices for each axis
    EvaluateSlices(node, AxisEnum.x, ref lowestCostX, ref lowestCostXSlice);
    EvaluateSlices(node, AxisEnum.y, ref lowestCostY, ref lowestCostYSlice);
    EvaluateSlices(node, AxisEnum.z, ref lowestCostZ, ref lowestCostZSlice);

    // Determine the lowest cost and corresponding axis
    float lowestCost = Mathf.Min(lowestCostX, lowestCostY, lowestCostZ);

    // If no viable slice was found, log a message and draw the bounds for debugging
    if (lowestCost == float.PositiveInfinity)
    {
        Debug.Log("No viable slices, tri count: " + node.tris.Count);
        DrawBounds(node.minBounds, node.maxBounds, bvhTargetObject.transform, 2, Color.blue);
    }

    // Return the slice position and axis corresponding to the lowest cost
    if (lowestCost == lowestCostX)
    {
        return (lowestCostXSlice, AxisEnum.x);
    }
    else if (lowestCost == lowestCostY)
    {
        return (lowestCostYSlice, AxisEnum.y);
    }
    else
    {
        return (lowestCostZSlice, AxisEnum.z);
    }
}

/// <summary>
/// Splits a BVH node at the specified slice position along the given axis, creating two child nodes.
/// </summary>
/// <param name="node">The BVH node to split.</param>
/// <param name="axis">The axis along which to split the node (x, y, or z).</param>
/// <param name="slicePos">The position along the axis at which to split the node.</param>
void SplitNodeAtCut(BVHNode node, AxisEnum axis, float slicePos)
{
    // Create two new BVH nodes for the split
    BVHNode nodeA = new BVHNode();
    BVHNode nodeB = new BVHNode();

    // Lists to hold triangles for each child node
    List<NodeTriangle> childATris = new List<NodeTriangle>();
    List<NodeTriangle> childBTris = new List<NodeTriangle>();

    // Calculate the bounds for the child nodes based on the split axis
    switch (axis)
    {
        case AxisEnum.x:
            nodeA.minBounds = node.minBounds;
            nodeA.maxBounds = new Vector3(slicePos, node.maxBounds.y, node.maxBounds.z);
            nodeB.minBounds = new Vector3(slicePos, node.minBounds.y, node.minBounds.z);
            nodeB.maxBounds = node.maxBounds;
            break;
        case AxisEnum.y:
            nodeA.minBounds = node.minBounds;
            nodeA.maxBounds = new Vector3(node.maxBounds.x, slicePos, node.maxBounds.z);
            nodeB.minBounds = new Vector3(node.minBounds.x, slicePos, node.minBounds.z);
            nodeB.maxBounds = node.maxBounds;
            break;
        case AxisEnum.z:
            nodeA.minBounds = node.minBounds;
            nodeA.maxBounds = new Vector3(node.maxBounds.x, node.maxBounds.y, slicePos);
            nodeB.minBounds = new Vector3(node.minBounds.x, node.minBounds.y, slicePos);
            nodeB.maxBounds = node.maxBounds;
            break;
    }

    // Distribute triangles between the child nodes based on their intersection with the new bounds
    foreach (var tri in node.tris)
    {
        Vector3 vert1 = verts[tri.index1];
        Vector3 vert2 = verts[tri.index2];
        Vector3 vert3 = verts[tri.index3];

        if (TriangleAABBIntersection.TriangleIntersectsAABB(vert1, vert2, vert3, nodeA.minBounds, nodeA.maxBounds))
        {
            childATris.Add(tri);
        }
        else if (TriangleAABBIntersection.TriangleIntersectsAABB(vert1, vert2, vert3, nodeB.minBounds, nodeB.maxBounds))
        {
            childBTris.Add(tri);
        }
    }

    // Calculate and update the AABB bounds for the child nodes based on their triangles
    Vector3 min = Vector3.positiveInfinity;
    Vector3 max = Vector3.negativeInfinity;
    CalculateAABBForTriangles(childATris, ref min, ref max, true);
    nodeA.minBounds = min;
    nodeA.maxBounds = max;

    min = Vector3.positiveInfinity;
    max = Vector3.negativeInfinity;
    CalculateAABBForTriangles(childBTris, ref min, ref max, true);
    nodeB.minBounds = min;
    nodeB.maxBounds = max;

    // Assign the triangle lists to the child nodes
    nodeA.tris = childATris;
    nodeB.tris = childBTris;

    // Add the child nodes to the node list and update the parent node's child indices
    nodes.Add(nodeA);
    node.leftChildIndex = (uint)nodes.Count - 1;
    nodes.Add(nodeB);
    node.rightChildIndex = (uint)nodes.Count - 1;

    // Mark the parent node for removal from the list of nodes to be split
    nodesToRemoveFromSplitList.Add(node);

    // If the child nodes have more triangles than allowed per leaf, add them to the list to be split later
    if (nodeA.tris.Count > maxTrisPerLeafNode)
    {
        nodesToSplitLater.Add(nodeA);
    }
    if (nodeB.tris.Count > maxTrisPerLeafNode)
    {
        nodesToSplitLater.Add(nodeB);
    }
}

/// <summary>
/// Calculates the axis-aligned bounding box (AABB) for a list of triangles and updates the minimum and maximum bounds.
/// </summary>
/// <param name="nodeTriangles">The list of triangles to calculate the AABB for.</param>
/// <param name="min">Reference to the minimum bounds, updated by this function.</param>
/// <param name="max">Reference to the maximum bounds, updated by this function.</param>
/// <param name="actualSplit">Indicates whether the calculation is for an actual split, used for debugging purposes.</param>
void CalculateAABBForTriangles(List<NodeTriangle> nodeTriangles, ref Vector3 min, ref Vector3 max, bool actualSplit)
{
    // Iterate through each triangle in the list
    for (int i = 0; i < nodeTriangles.Count; i++)
    {
        // Update the minimum and maximum x bounds based on the triangle's x bounds
        min.x = Mathf.Min(min.x, nodeTriangles[i].minX);
        max.x = Mathf.Max(max.x, nodeTriangles[i].maxX);

        // Update the minimum and maximum y bounds based on the triangle's y bounds
        min.y = Mathf.Min(min.y, nodeTriangles[i].minY);
        max.y = Mathf.Max(max.y, nodeTriangles[i].maxY);

        // Update the minimum and maximum z bounds based on the triangle's z bounds
        min.z = Mathf.Min(min.z, nodeTriangles[i].minZ);
        max.z = Mathf.Max(max.z, nodeTriangles[i].maxZ);
    }

    // If there are no triangles and this is an actual split, log a debug message
    if (nodeTriangles.Count == 0 && actualSplit)
    {
        Debug.LogWarning("No triangles found during AABB calculation.");
    }
}

/// <summary>
/// Calculates the axis-aligned bounding box (AABB) for the root node of the BVH using the given triangle indices and vertices.
/// </summary>
/// <param name="triIndices">The array of triangle indices to calculate the AABB for.</param>
/// <param name="verts">The array of vertices.</param>
/// <param name="minBounds">Output parameter for the minimum bounds of the AABB.</param>
/// <param="maxBounds">Output parameter for the maximum bounds of the AABB.</param>
void CalculateAABBRootNode(int[] triIndices, Vector3[] verts, out Vector3 minBounds, out Vector3 maxBounds)
{
    // Initialize the minimum and maximum bounds
    minBounds = Vector3.positiveInfinity;
    maxBounds = Vector3.negativeInfinity;

    // Iterate through each triangle index in the array
    foreach (int tri in triIndices)
    {
        // Get the vertex corresponding to the current triangle index
        Vector3 vertex = verts[tri];

        // Update the minimum bounds
        minBounds = Vector3.Min(minBounds, vertex);

        // Update the maximum bounds
        maxBounds = Vector3.Max(maxBounds, vertex);
    }
}

/// <summary>
/// Calculates the Surface Area Heuristic (SAH) cost for splitting a BVH node into two child nodes.
/// </summary>
/// <param name="parentMin">The minimum bounds of the parent node.</param>
/// <param name="parentMax">The maximum bounds of the parent node.</param>
/// <param name="childAMin">The minimum bounds of the first child node.</param>
/// <param name="childAMax">The maximum bounds of the first child node.</param>
/// <param name="childBMin">The minimum bounds of the second child node.</param>
/// <param name="childBMax">The maximum bounds of the second child node.</param>
/// <param name="triCountA">The number of triangles in the first child node.</param>
/// <param name="triCountB">The number of triangles in the second child node.</param>
/// <returns>The SAH cost for the split.</returns>
public float CalculateSAHCost(Vector3 parentMin, Vector3 parentMax, Vector3 childAMin, Vector3 childAMax, Vector3 childBMin, Vector3 childBMax, int triCountA, int triCountB)
{
    // Calculate the surface area of the parent node's AABB
    float surfaceAreaParent = CalculateAABBSurfaceArea(parentMin, parentMax);

    // Calculate the proportion of the parent node's surface area occupied by the first child node
    float surfaceAreaChildAProportionOfParent = CalculateAABBSurfaceArea(childAMin, childAMax) / surfaceAreaParent;

    // Calculate the proportion of the parent node's surface area occupied by the second child node
    float surfaceAreaChildBProportionOfParent = CalculateAABBSurfaceArea(childBMin, childBMax) / surfaceAreaParent;

    // Calculate the SAH cost using the proportions of the surface areas and the number of triangles in each child node
    float cost = surfaceAreaChildAProportionOfParent * triCountA + surfaceAreaChildBProportionOfParent * triCountB;

    // Return the calculated SAH cost
    return cost;
}

/// <summary>
/// Calculates the surface area of an axis-aligned bounding box (AABB) given its minimum and maximum bounds.
/// </summary>
/// <param name="minBounds">The minimum bounds of the AABB.</param>
/// <param name="maxBounds">The maximum bounds of the AABB.</param>
/// <returns>The surface area of the AABB.</returns>
float CalculateAABBSurfaceArea(Vector3 minBounds, Vector3 maxBounds)
{
    // Calculate the extents (width, height, depth) of the AABB
    Vector3 extents = maxBounds - minBounds;

    // Calculate the surface area using the formula: 2 * (width * height + height * depth + depth * width)
    float surfaceArea = 2 * (extents.x * extents.y + extents.y * extents.z + extents.z * extents.x);

    // Return the calculated surface area
    return surfaceArea;
}

/// <summary>
/// Debugs the BVH nodes at a specified depth by drawing their bounds and triangles.
/// </summary>
/// <param name="nodeDepth">The depth of the nodes to debug.</param>
void DebugNodes(int nodeDepth)
{
    // Get the root node
    BVHNode rootNode = nodes[0];

    // Initialize lists to hold parent and child nodes
    List<BVHNode> parentNodes = new List<BVHNode>();
    List<BVHNode> childNodes = new List<BVHNode> { rootNode };

    // Iterate through the specified depth
    for (int depth = 0; depth < nodeDepth; depth++)
    {
        // Transfer child nodes to parent nodes
        parentNodes.Clear();
        parentNodes.AddRange(childNodes);
        childNodes.Clear();

        // Add children of the current parent nodes to the childNodes list
        foreach (BVHNode parentNode in parentNodes)
        {
            if (parentNode.leftChildIndex != uint.MaxValue)
            {
                childNodes.Add(nodes[(int)parentNode.leftChildIndex]);
                childNodes.Add(nodes[(int)parentNode.rightChildIndex]);
            }
        }
    }

    // Draw the bounds and triangles of the child nodes
    for (int i = 0; i < childNodes.Count; i++)
    {
        Color color = (i % 2 == 0) ? Color.green : Color.red;
        color.a = 0.5f;

        // Draw the triangles of the node
        DebugTris(childNodes[i].tris, verts, bvhTargetObject.transform, 5, color);

        // Draw the bounds of the node
        color.a = 1f;
        DrawBounds(childNodes[i].minBounds, childNodes[i].maxBounds, bvhTargetObject.transform, 5, color);
    }

    // Log a message if no nodes are found at the specified depth
    if (childNodes.Count == 0)
    {
        Debug.Log("No nodes at this depth");
    }
}

/// <summary>
/// Draws the axis-aligned bounding box (AABB) with the specified minimum and maximum bounds.
/// </summary>
/// <param name="min">The minimum bounds of the AABB.</param>
/// <param name="max">The maximum bounds of the AABB.</param>
/// <param name="objectTransform">The transform of the object to which the AABB is relative.</param>
/// <param name="duration">The duration for which the lines should be visible.</param>
/// <param name="color">The color of the lines.</param>
void DrawBounds(Vector3 min, Vector3 max, Transform objectTransform, float duration, Color color)
{
    // Calculate the transformed positions of the eight corners of the AABB
    Vector3 corner1 = objectTransform.TransformPoint(new Vector3(min.x, min.y, min.z));
    Vector3 corner2 = objectTransform.TransformPoint(new Vector3(min.x, min.y, max.z));
    Vector3 corner3 = objectTransform.TransformPoint(new Vector3(min.x, max.y, max.z));
    Vector3 corner4 = objectTransform.TransformPoint(new Vector3(min.x, max.y, min.z));
    Vector3 corner5 = objectTransform.TransformPoint(new Vector3(max.x, min.y, min.z));
    Vector3 corner6 = objectTransform.TransformPoint(new Vector3(max.x, min.y, max.z));
    Vector3 corner7 = objectTransform.TransformPoint(new Vector3(max.x, max.y, max.z));
    Vector3 corner8 = objectTransform.TransformPoint(new Vector3(max.x, max.y, min.z));

    // Draw bottom square of the AABB
    Debug.DrawLine(corner1, corner2, color, duration);
    Debug.DrawLine(corner2, corner6, color, duration);
    Debug.DrawLine(corner6, corner5, color, duration);
    Debug.DrawLine(corner5, corner1, color, duration);

    // Draw top square of the AABB
    Debug.DrawLine(corner4, corner3, color, duration);
    Debug.DrawLine(corner3, corner7, color, duration);
    Debug.DrawLine(corner7, corner8, color, duration);
    Debug.DrawLine(corner8, corner4, color, duration);

    // Draw vertical lines connecting the top and bottom squares
    Debug.DrawLine(corner1, corner4, color, duration);
    Debug.DrawLine(corner2, corner3, color, duration);
    Debug.DrawLine(corner6, corner7, color, duration);
    Debug.DrawLine(corner5, corner8, color, duration);
}

/// <summary>
/// Draws the triangles and their centroids for debugging purposes.
/// </summary>
/// <param name="tris">The list of triangles to debug.</param>
/// <param name="verts">The array of vertices.</param>
/// <param name="objectTransform">The transform of the object to which the triangles are relative.</param>
/// <param name="duration">The duration for which the lines should be visible.</param>
/// <param name="color">The color of the lines and centroids.</param>
void DebugTris(List<NodeTriangle> tris, Vector3[] verts, Transform objectTransform, float duration, Color color)
{
    // Iterate through each triangle in the list
    for (int i = 0; i < tris.Count; i++)
    {
        // Transform the vertices of the triangle to world space
        Vector3 vert1 = objectTransform.TransformPoint(verts[tris[i].index1]);
        Vector3 vert2 = objectTransform.TransformPoint(verts[tris[i].index2]);
        Vector3 vert3 = objectTransform.TransformPoint(verts[tris[i].index3]);

        // Transform the centroid of the triangle to world space
        Vector3 centroid = objectTransform.TransformPoint(tris[i].centroid);

        // Draw the edges of the triangle
        Debug.DrawLine(vert1, vert2, color, duration);
        Debug.DrawLine(vert2, vert3, color, duration);
        Debug.DrawLine(vert3, vert1, color, duration);

        // Draw the centroid of the triangle
        DebugExtensions.DrawPosition(centroid, 0.01f, color, duration, true);
    }
}