cambridge/libs/cpml/bvh.lua

558 lines
16 KiB
Lua
Raw Permalink Normal View History

2022-07-04 23:05:40 -05:00
-- https://github.com/benraziel/bvh-tree
--- BVH Tree
-- @module bvh
local modules = (...):gsub('%.[^%.]+$', '') .. "."
local intersect = require(modules .. "intersect")
local vec3 = require(modules .. "vec3")
local EPSILON = 1e-6
local BVH = {}
local BVHNode = {}
local Node
BVH.__index = BVH
BVHNode.__index = BVHNode
local function new(triangles, maxTrianglesPerNode)
local tree = setmetatable({}, BVH)
local trianglesArray = {}
for _, triangle in ipairs(triangles) do
local p1 = triangle[1]
local p2 = triangle[2]
local p3 = triangle[3]
table.insert(trianglesArray, p1.x or p1[1])
table.insert(trianglesArray, p1.y or p1[2])
table.insert(trianglesArray, p1.z or p1[3])
table.insert(trianglesArray, p2.x or p2[1])
table.insert(trianglesArray, p2.y or p2[2])
table.insert(trianglesArray, p2.z or p2[3])
table.insert(trianglesArray, p3.x or p3[1])
table.insert(trianglesArray, p3.y or p3[2])
table.insert(trianglesArray, p3.z or p3[3])
end
tree._trianglesArray = trianglesArray
tree._maxTrianglesPerNode = maxTrianglesPerNode or 10
tree._bboxArray = tree.calcBoundingBoxes(trianglesArray)
-- clone a helper array
tree._bboxHelper = {}
for _, bbox in ipairs(tree._bboxArray) do
table.insert(tree._bboxHelper, bbox)
end
-- create the root node, add all the triangles to it
local triangleCount = #triangles
local extents = tree:calcExtents(1, triangleCount, EPSILON)
tree._rootNode = Node(extents[1], extents[2], 1, triangleCount, 1)
tree._nodes_to_split = { tree._rootNode }
while #tree._nodes_to_split > 0 do
local node = table.remove(tree._nodes_to_split)
tree:splitNode(node)
end
return tree
end
function BVH:intersectAABB(aabb)
local nodesToIntersect = { self._rootNode }
local trianglesInIntersectingNodes = {} -- a list of nodes that intersect the ray (according to their bounding box)
local intersectingTriangles = {}
-- go over the BVH tree, and extract the list of triangles that lie in nodes that intersect the box.
-- note: these triangles may not intersect the box themselves
while #nodesToIntersect > 0 do
local node = table.remove(nodesToIntersect)
local node_aabb = {
min = node._extentsMin,
max = node._extentsMax
}
if intersect.aabb_aabb(aabb, node_aabb) then
if node._node0 then
table.insert(nodesToIntersect, node._node0)
end
if node._node1 then
table.insert(nodesToIntersect, node._node1)
end
for i=node._startIndex, node._endIndex do
table.insert(trianglesInIntersectingNodes, self._bboxArray[1+(i-1)*7])
end
end
end
-- insert all node triangles, don't bother being more specific yet.
local triangle = { vec3(), vec3(), vec3() }
for i=1, #trianglesInIntersectingNodes do
local triIndex = trianglesInIntersectingNodes[i]
-- print(triIndex, #self._trianglesArray)
triangle[1].x = self._trianglesArray[1+(triIndex-1)*9]
triangle[1].y = self._trianglesArray[1+(triIndex-1)*9+1]
triangle[1].z = self._trianglesArray[1+(triIndex-1)*9+2]
triangle[2].x = self._trianglesArray[1+(triIndex-1)*9+3]
triangle[2].y = self._trianglesArray[1+(triIndex-1)*9+4]
triangle[2].z = self._trianglesArray[1+(triIndex-1)*9+5]
triangle[3].x = self._trianglesArray[1+(triIndex-1)*9+6]
triangle[3].y = self._trianglesArray[1+(triIndex-1)*9+7]
triangle[3].z = self._trianglesArray[1+(triIndex-1)*9+8]
table.insert(intersectingTriangles, {
triangle = { triangle[1]:clone(), triangle[2]:clone(), triangle[3]:clone() },
triangleIndex = triIndex
})
end
return intersectingTriangles
end
function BVH:intersectRay(rayOrigin, rayDirection, backfaceCulling)
local nodesToIntersect = { self._rootNode }
local trianglesInIntersectingNodes = {} -- a list of nodes that intersect the ray (according to their bounding box)
local intersectingTriangles = {}
local invRayDirection = vec3(
1 / rayDirection.x,
1 / rayDirection.y,
1 / rayDirection.z
)
-- go over the BVH tree, and extract the list of triangles that lie in nodes that intersect the ray.
-- note: these triangles may not intersect the ray themselves
while #nodesToIntersect > 0 do
local node = table.remove(nodesToIntersect)
if BVH.intersectNodeBox(rayOrigin, invRayDirection, node) then
if node._node0 then
table.insert(nodesToIntersect, node._node0)
end
if node._node1 then
table.insert(nodesToIntersect, node._node1)
end
for i=node._startIndex, node._endIndex do
table.insert(trianglesInIntersectingNodes, self._bboxArray[1+(i-1)*7])
end
end
end
-- go over the list of candidate triangles, and check each of them using ray triangle intersection
local triangle = { vec3(), vec3(), vec3() }
local ray = {
position = vec3(rayOrigin.x, rayOrigin.y, rayOrigin.z),
direction = vec3(rayDirection.x, rayDirection.y, rayDirection.z)
}
for i=1, #trianglesInIntersectingNodes do
local triIndex = trianglesInIntersectingNodes[i]
-- print(triIndex, #self._trianglesArray)
triangle[1].x = self._trianglesArray[1+(triIndex-1)*9]
triangle[1].y = self._trianglesArray[1+(triIndex-1)*9+1]
triangle[1].z = self._trianglesArray[1+(triIndex-1)*9+2]
triangle[2].x = self._trianglesArray[1+(triIndex-1)*9+3]
triangle[2].y = self._trianglesArray[1+(triIndex-1)*9+4]
triangle[2].z = self._trianglesArray[1+(triIndex-1)*9+5]
triangle[3].x = self._trianglesArray[1+(triIndex-1)*9+6]
triangle[3].y = self._trianglesArray[1+(triIndex-1)*9+7]
triangle[3].z = self._trianglesArray[1+(triIndex-1)*9+8]
local intersectionPoint, intersectionDistance = intersect.ray_triangle(ray, triangle, backfaceCulling)
if intersectionPoint then
table.insert(intersectingTriangles, {
triangle = { triangle[1]:clone(), triangle[2]:clone(), triangle[3]:clone() },
triangleIndex = triIndex,
intersectionPoint = intersectionPoint,
intersectionDistance = intersectionDistance
})
end
end
return intersectingTriangles
end
function BVH.calcBoundingBoxes(trianglesArray)
local p1x, p1y, p1z
local p2x, p2y, p2z
local p3x, p3y, p3z
local minX, minY, minZ
local maxX, maxY, maxZ
local bboxArray = {}
for i=1, #trianglesArray / 9 do
p1x = trianglesArray[1+(i-1)*9]
p1y = trianglesArray[1+(i-1)*9+1]
p1z = trianglesArray[1+(i-1)*9+2]
p2x = trianglesArray[1+(i-1)*9+3]
p2y = trianglesArray[1+(i-1)*9+4]
p2z = trianglesArray[1+(i-1)*9+5]
p3x = trianglesArray[1+(i-1)*9+6]
p3y = trianglesArray[1+(i-1)*9+7]
p3z = trianglesArray[1+(i-1)*9+8]
minX = math.min(p1x, p2x, p3x)
minY = math.min(p1y, p2y, p3y)
minZ = math.min(p1z, p2z, p3z)
maxX = math.max(p1x, p2x, p3x)
maxY = math.max(p1y, p2y, p3y)
maxZ = math.max(p1z, p2z, p3z)
BVH.setBox(bboxArray, i, i, minX, minY, minZ, maxX, maxY, maxZ)
end
return bboxArray
end
function BVH:calcExtents(startIndex, endIndex, expandBy)
expandBy = expandBy or 0
if startIndex > endIndex then
return { vec3(), vec3() }
end
local minX = math.huge
local minY = math.huge
local minZ = math.huge
local maxX = -math.huge
local maxY = -math.huge
local maxZ = -math.huge
for i=startIndex, endIndex do
minX = math.min(self._bboxArray[1+(i-1)*7+1], minX)
minY = math.min(self._bboxArray[1+(i-1)*7+2], minY)
minZ = math.min(self._bboxArray[1+(i-1)*7+3], minZ)
maxX = math.max(self._bboxArray[1+(i-1)*7+4], maxX)
maxY = math.max(self._bboxArray[1+(i-1)*7+5], maxY)
maxZ = math.max(self._bboxArray[1+(i-1)*7+6], maxZ)
end
return {
vec3(minX - expandBy, minY - expandBy, minZ - expandBy),
vec3(maxX + expandBy, maxY + expandBy, maxZ + expandBy)
}
end
function BVH:splitNode(node)
local num_elements = node:elementCount()
if (num_elements <= self._maxTrianglesPerNode) or (num_elements <= 0) then
return
end
local startIndex = node._startIndex
local endIndex = node._endIndex
local leftNode = { {},{},{} }
local rightNode = { {},{},{} }
local extentCenters = { node:centerX(), node:centerY(), node:centerZ() }
local extentsLength = {
node._extentsMax.x - node._extentsMin.x,
node._extentsMax.y - node._extentsMin.y,
node._extentsMax.z - node._extentsMin.z
}
local objectCenter = {}
for i=startIndex, endIndex do
objectCenter[1] = (self._bboxArray[1+(i-1)*7+1] + self._bboxArray[1+(i-1)*7+4]) * 0.5 -- center = (min + max) / 2
objectCenter[2] = (self._bboxArray[1+(i-1)*7+2] + self._bboxArray[1+(i-1)*7+5]) * 0.5 -- center = (min + max) / 2
objectCenter[3] = (self._bboxArray[1+(i-1)*7+3] + self._bboxArray[1+(i-1)*7+6]) * 0.5 -- center = (min + max) / 2
for j=1, 3 do
if objectCenter[j] < extentCenters[j] then
table.insert(leftNode[j], i)
else
table.insert(rightNode[j], i)
end
end
end
-- check if we couldn't split the node by any of the axes (x, y or z). halt
-- here, dont try to split any more (cause it will always fail, and we'll
-- enter an infinite loop
local splitFailed = {
#leftNode[1] == 0 or #rightNode[1] == 0,
#leftNode[2] == 0 or #rightNode[2] == 0,
#leftNode[3] == 0 or #rightNode[3] == 0
}
if splitFailed[1] and splitFailed[2] and splitFailed[3] then
return
end
-- choose the longest split axis. if we can't split by it, choose next best one.
local splitOrder = { 1, 2, 3 }
table.sort(splitOrder, function(a, b)
return extentsLength[a] > extentsLength[b]
end)
local leftElements
local rightElements
for i=1, 3 do
local candidateIndex = splitOrder[i]
if not splitFailed[candidateIndex] then
leftElements = leftNode[candidateIndex]
rightElements = rightNode[candidateIndex]
break
end
end
-- sort the elements in range (startIndex, endIndex) according to which node they should be at
local node0Start = startIndex
local node1Start = node0Start + #leftElements
local node0End = node1Start - 1
local node1End = endIndex
local currElement
local helperPos = node._startIndex
local concatenatedElements = {}
for _, element in ipairs(leftElements) do
table.insert(concatenatedElements, element)
end
for _, element in ipairs(rightElements) do
table.insert(concatenatedElements, element)
end
-- print(#leftElements, #rightElements, #concatenatedElements)
for i=1, #concatenatedElements do
currElement = concatenatedElements[i]
BVH.copyBox(self._bboxArray, currElement, self._bboxHelper, helperPos)
helperPos = helperPos + 1
end
-- copy results back to main array
for i=1+(node._startIndex-1)*7, node._endIndex*7 do
self._bboxArray[i] = self._bboxHelper[i]
end
-- create 2 new nodes for the node we just split, and add links to them from the parent node
local node0Extents = self:calcExtents(node0Start, node0End, EPSILON)
local node1Extents = self:calcExtents(node1Start, node1End, EPSILON)
local node0 = Node(node0Extents[1], node0Extents[2], node0Start, node0End, node._level + 1)
local node1 = Node(node1Extents[1], node1Extents[2], node1Start, node1End, node._level + 1)
node._node0 = node0
node._node1 = node1
node:clearShapes()
-- add new nodes to the split queue
table.insert(self._nodes_to_split, node0)
table.insert(self._nodes_to_split, node1)
end
function BVH._calcTValues(minVal, maxVal, rayOriginCoord, invdir)
local res = { min=0, max=0 }
if invdir >= 0 then
res.min = ( minVal - rayOriginCoord ) * invdir
res.max = ( maxVal - rayOriginCoord ) * invdir
else
res.min = ( maxVal - rayOriginCoord ) * invdir
res.max = ( minVal - rayOriginCoord ) * invdir
end
return res
end
function BVH.intersectNodeBox(rayOrigin, invRayDirection, node)
local t = BVH._calcTValues(node._extentsMin.x, node._extentsMax.x, rayOrigin.x, invRayDirection.x)
local ty = BVH._calcTValues(node._extentsMin.y, node._extentsMax.y, rayOrigin.y, invRayDirection.y)
if t.min > ty.max or ty.min > t.max then
return false
end
-- These lines also handle the case where tmin or tmax is NaN
-- (result of 0 * Infinity). x !== x returns true if x is NaN
if ty.min > t.min or t.min ~= t.min then
t.min = ty.min
end
if ty.max < t.max or t.max ~= t.max then
t.max = ty.max
end
local tz = BVH._calcTValues(node._extentsMin.z, node._extentsMax.z, rayOrigin.z, invRayDirection.z)
if t.min > tz.max or tz.min > t.max then
return false
end
if tz.min > t.min or t.min ~= t.min then
t.min = tz.min
end
if tz.max < t.max or t.max ~= t.max then
t.max = tz.max
end
--return point closest to the ray (positive side)
if t.max < 0 then
return false
end
return true
end
function BVH.setBox(bboxArray, pos, triangleId, minX, minY, minZ, maxX, maxY, maxZ)
bboxArray[1+(pos-1)*7] = triangleId
bboxArray[1+(pos-1)*7+1] = minX
bboxArray[1+(pos-1)*7+2] = minY
bboxArray[1+(pos-1)*7+3] = minZ
bboxArray[1+(pos-1)*7+4] = maxX
bboxArray[1+(pos-1)*7+5] = maxY
bboxArray[1+(pos-1)*7+6] = maxZ
end
function BVH.copyBox(sourceArray, sourcePos, destArray, destPos)
destArray[1+(destPos-1)*7] = sourceArray[1+(sourcePos-1)*7]
destArray[1+(destPos-1)*7+1] = sourceArray[1+(sourcePos-1)*7+1]
destArray[1+(destPos-1)*7+2] = sourceArray[1+(sourcePos-1)*7+2]
destArray[1+(destPos-1)*7+3] = sourceArray[1+(sourcePos-1)*7+3]
destArray[1+(destPos-1)*7+4] = sourceArray[1+(sourcePos-1)*7+4]
destArray[1+(destPos-1)*7+5] = sourceArray[1+(sourcePos-1)*7+5]
destArray[1+(destPos-1)*7+6] = sourceArray[1+(sourcePos-1)*7+6]
end
function BVH.getBox(bboxArray, pos, outputBox)
outputBox.triangleId = bboxArray[1+(pos-1)*7]
outputBox.minX = bboxArray[1+(pos-1)*7+1]
outputBox.minY = bboxArray[1+(pos-1)*7+2]
outputBox.minZ = bboxArray[1+(pos-1)*7+3]
outputBox.maxX = bboxArray[1+(pos-1)*7+4]
outputBox.maxY = bboxArray[1+(pos-1)*7+5]
outputBox.maxZ = bboxArray[1+(pos-1)*7+6]
end
local function new_node(extentsMin, extentsMax, startIndex, endIndex, level)
return setmetatable({
_extentsMin = extentsMin,
_extentsMax = extentsMax,
_startIndex = startIndex,
_endIndex = endIndex,
_level = level
--_node0 = nil
--_node1 = nil
}, BVHNode)
end
function BVHNode:elementCount()
return (self._endIndex + 1) - self._startIndex
end
function BVHNode:centerX()
return (self._extentsMin.x + self._extentsMax.x) * 0.5
end
function BVHNode:centerY()
return (self._extentsMin.y + self._extentsMax.y) * 0.5
end
function BVHNode:centerZ()
return (self._extentsMin.z + self._extentsMax.z) * 0.5
end
function BVHNode:clearShapes()
self._startIndex = 0
self._endIndex = -1
end
function BVHNode.ngSphereRadius(extentsMin, extentsMax)
local centerX = (extentsMin.x + extentsMax.x) * 0.5
local centerY = (extentsMin.y + extentsMax.y) * 0.5
local centerZ = (extentsMin.z + extentsMax.z) * 0.5
local extentsMinDistSqr =
(centerX - extentsMin.x) * (centerX - extentsMin.x) +
(centerY - extentsMin.y) * (centerY - extentsMin.y) +
(centerZ - extentsMin.z) * (centerZ - extentsMin.z)
local extentsMaxDistSqr =
(centerX - extentsMax.x) * (centerX - extentsMax.x) +
(centerY - extentsMax.y) * (centerY - extentsMax.y) +
(centerZ - extentsMax.z) * (centerZ - extentsMax.z)
return math.sqrt(math.max(extentsMinDistSqr, extentsMaxDistSqr))
end
--[[
--- Draws node boundaries visually for debugging.
-- @param cube Cube model to draw
-- @param depth Used for recurcive calls to self method
function OctreeNode:draw_bounds(cube, depth)
depth = depth or 0
local tint = depth / 7 -- Will eventually get values > 1. Color rounds to 1 automatically
love.graphics.setColor(tint * 255, 0, (1 - tint) * 255)
local m = mat4()
:translate(self.center)
:scale(vec3(self.adjLength, self.adjLength, self.adjLength))
love.graphics.updateMatrix("transform", m)
love.graphics.setWireframe(true)
love.graphics.draw(cube)
love.graphics.setWireframe(false)
for _, child in ipairs(self.children) do
child:draw_bounds(cube, depth + 1)
end
love.graphics.setColor(255, 255, 255)
end
--- Draws the bounds of all objects in the tree visually for debugging.
-- @param cube Cube model to draw
-- @param filter a function returning true or false to determine visibility.
function OctreeNode:draw_objects(cube, filter)
local tint = self.baseLength / 20
love.graphics.setColor(0, (1 - tint) * 255, tint * 255, 63)
for _, object in ipairs(self.objects) do
if filter and filter(object.data) or not filter then
local m = mat4()
:translate(object.bounds.center)
:scale(object.bounds.size)
love.graphics.updateMatrix("transform", m)
love.graphics.draw(cube)
end
end
for _, child in ipairs(self.children) do
child:draw_objects(cube, filter)
end
love.graphics.setColor(255, 255, 255)
end
--]]
Node = setmetatable({
new = new_node
}, {
__call = function(_, ...) return new_node(...) end
})
return setmetatable({
new = new
}, {
__call = function(_, ...) return new(...) end
})