123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635 |
- -- https://github.com/Nition/UnityOctree
- -- https://github.com/Nition/UnityOctree/blob/master/LICENCE
- -- https://github.com/Nition/UnityOctree/blob/master/Scripts/BoundsOctree.cs
- -- https://github.com/Nition/UnityOctree/blob/master/Scripts/BoundsOctreeNode.cs
- --- Octree
- -- @module octree
- local modules = (...):gsub('%.[^%.]+$', '') .. "."
- local intersect = require(modules .. "intersect")
- local mat4 = require(modules .. "mat4")
- local utils = require(modules .. "utils")
- local vec3 = require(modules .. "vec3")
- local Octree = {}
- local OctreeNode = {}
- local Node
- Octree.__index = Octree
- OctreeNode.__index = OctreeNode
- --== Octree ==--
- --- Constructor for the bounds octree.
- -- @param initialWorldSize Size of the sides of the initial node, in metres. The octree will never shrink smaller than this
- -- @param initialWorldPos Position of the centre of the initial node
- -- @param minNodeSize Nodes will stop splitting if the new nodes would be smaller than this (metres)
- -- @param looseness Clamped between 1 and 2. Values > 1 let nodes overlap
- local function new(initialWorldSize, initialWorldPos, minNodeSize, looseness)
- local tree = setmetatable({}, Octree)
- if minNodeSize > initialWorldSize then
- print("Minimum node size must be at least as big as the initial world size. Was: " .. minNodeSize .. " Adjusted to: " .. initialWorldSize)
- minNodeSize = initialWorldSize
- end
- -- The total amount of objects currently in the tree
- tree.count = 0
- -- Size that the octree was on creation
- tree.initialSize = initialWorldSize
- -- Minimum side length that a node can be - essentially an alternative to having a max depth
- tree.minSize = minNodeSize
- -- Should be a value between 1 and 2. A multiplier for the base size of a node.
- -- 1.0 is a "normal" octree, while values > 1 have overlap
- tree.looseness = utils.clamp(looseness, 1, 2)
- -- Root node of the octree
- tree.rootNode = Node(tree.initialSize, tree.minSize, tree.looseness, initialWorldPos)
- return tree
- end
- --- Used when growing the octree. Works out where the old root node would fit inside a new, larger root node.
- -- @param xDir X direction of growth. 1 or -1
- -- @param yDir Y direction of growth. 1 or -1
- -- @param zDir Z direction of growth. 1 or -1
- -- @return Octant where the root node should be
- local function get_root_pos_index(xDir, yDir, zDir)
- local result = xDir > 0 and 1 or 0
- if yDir < 0 then return result + 4 end
- if zDir > 0 then return result + 2 end
- end
- --- Add an object.
- -- @param obj Object to add
- -- @param objBounds 3D bounding box around the object
- function Octree:add(obj, objBounds)
- -- Add object or expand the octree until it can be added
- local count = 0 -- Safety check against infinite/excessive growth
- while not self.rootNode:add(obj, objBounds) do
- count = count + 1
- self:grow(objBounds.center - self.rootNode.center)
- if count > 20 then
- print("Aborted Add operation as it seemed to be going on forever (" .. count - 1 .. ") attempts at growing the octree.")
- return
- end
- self.count = self.count + 1
- end
- end
- --- Remove an object. Makes the assumption that the object only exists once in the tree.
- -- @param obj Object to remove
- -- @return bool True if the object was removed successfully
- function Octree:remove(obj)
- local removed = self.rootNode:remove(obj)
- -- See if we can shrink the octree down now that we've removed the item
- if removed then
- self.count = self.count - 1
- self:shrink()
- end
- return removed
- end
- --- Check if the specified bounds intersect with anything in the tree. See also: get_colliding.
- -- @param checkBounds bounds to check
- -- @return bool True if there was a collision
- function Octree:is_colliding(checkBounds)
- return self.rootNode:is_colliding(checkBounds)
- end
- --- Returns an array of objects that intersect with the specified bounds, if any. Otherwise returns an empty array. See also: is_colliding.
- -- @param checkBounds bounds to check
- -- @return table Objects that intersect with the specified bounds
- function Octree:get_colliding(checkBounds)
- return self.rootNode:get_colliding(checkBounds)
- end
- --- Cast a ray through the node and its children
- -- @param ray Ray with a position and a direction
- -- @param func Function to execute on any objects within child nodes
- -- @param out Table to store results of func in
- -- @return boolean True if an intersect detected
- function Octree:cast_ray(ray, func, out)
- assert(func)
- return self.rootNode:cast_ray(ray, func, out)
- end
- --- Draws node boundaries visually for debugging.
- function Octree:draw_bounds(cube)
- self.rootNode:draw_bounds(cube)
- end
- --- Draws the bounds of all objects in the tree visually for debugging.
- function Octree:draw_objects(cube, filter)
- self.rootNode:draw_objects(cube, filter)
- end
- --- Grow the octree to fit in all objects.
- -- @param direction Direction to grow
- function Octree:grow(direction)
- local xDirection = direction.x >= 0 and 1 or -1
- local yDirection = direction.y >= 0 and 1 or -1
- local zDirection = direction.z >= 0 and 1 or -1
- local oldRoot = self.rootNode
- local half = self.rootNode.baseLength / 2
- local newLength = self.rootNode.baseLength * 2
- local newCenter = self.rootNode.center + vec3(xDirection * half, yDirection * half, zDirection * half)
- -- Create a new, bigger octree root node
- self.rootNode = Node(newLength, self.minSize, self.looseness, newCenter)
- -- Create 7 new octree children to go with the old root as children of the new root
- local rootPos = get_root_pos_index(xDirection, yDirection, zDirection)
- local children = {}
- for i = 0, 7 do
- if i == rootPos then
- children[i+1] = oldRoot
- else
- xDirection = i % 2 == 0 and -1 or 1
- yDirection = i > 3 and -1 or 1
- zDirection = (i < 2 or (i > 3 and i < 6)) and -1 or 1
- children[i+1] = Node(self.rootNode.baseLength, self.minSize, self.looseness, newCenter + vec3(xDirection * half, yDirection * half, zDirection * half))
- end
- end
- -- Attach the new children to the new root node
- self.rootNode:set_children(children)
- end
- --- Shrink the octree if possible, else leave it the same.
- function Octree:shrink()
- self.rootNode = self.rootNode:shrink_if_possible(self.initialSize)
- end
- --== Octree Node ==--
- --- Constructor.
- -- @param baseLength Length of this node, not taking looseness into account
- -- @param minSize Minimum size of nodes in this octree
- -- @param looseness Multiplier for baseLengthVal to get the actual size
- -- @param center Centre position of this node
- local function new_node(baseLength, minSize, looseness, center)
- local node = setmetatable({}, OctreeNode)
- -- Objects in this node
- node.objects = {}
- -- Child nodes
- node.children = {}
- -- If there are already numObjectsAllowed in a node, we split it into children
- -- A generally good number seems to be something around 8-15
- node.numObjectsAllowed = 8
- node:set_values(baseLength, minSize, looseness, center)
- return node
- end
- local function new_bound(center, size)
- return {
- center = center,
- size = size,
- min = center - (size / 2),
- max = center + (size / 2)
- }
- end
- --- Add an object.
- -- @param obj Object to add
- -- @param objBounds 3D bounding box around the object
- -- @return boolean True if the object fits entirely within this node
- function OctreeNode:add(obj, objBounds)
- if not intersect.encapsulate_aabb(self.bounds, objBounds) then
- return false
- end
- -- We know it fits at this level if we've got this far
- -- Just add if few objects are here, or children would be below min size
- if #self.objects < self.numObjectsAllowed
- or self.baseLength / 2 < self.minSize then
- table.insert(self.objects, {
- data = obj,
- bounds = objBounds
- })
- else
- -- Fits at this level, but we can go deeper. Would it fit there?
- local best_fit_child
- -- Create the 8 children
- if #self.children == 0 then
- self:split()
- if #self.children == 0 then
- print("Child creation failed for an unknown reason. Early exit.")
- return false
- end
- -- Now that we have the new children, see if this node's existing objects would fit there
- for i = #self.objects, 1, -1 do
- local object = self.objects[i]
- -- Find which child the object is closest to based on where the
- -- object's center is located in relation to the octree's center.
- best_fit_child = self:best_fit_child(object.bounds)
- -- Does it fit?
- if intersect.encapsulate_aabb(self.children[best_fit_child].bounds, object.bounds) then
- self.children[best_fit_child]:add(object.data, object.bounds) -- Go a level deeper
- table.remove(self.objects, i) -- Remove from here
- end
- end
- end
- -- Now handle the new object we're adding now
- best_fit_child = self:best_fit_child(objBounds)
- if intersect.encapsulate_aabb(self.children[best_fit_child].bounds, objBounds) then
- self.children[best_fit_child]:add(obj, objBounds)
- else
- table.insert(self.objects, {
- data = obj,
- bounds = objBounds
- })
- end
- end
- return true
- end
- --- Remove an object. Makes the assumption that the object only exists once in the tree.
- -- @param obj Object to remove
- -- @return boolean True if the object was removed successfully
- function OctreeNode:remove(obj)
- local removed = false
- for i, object in ipairs(self.objects) do
- if object == obj then
- removed = table.remove(self.objects, i) and true or false
- break
- end
- end
- if not removed then
- for _, child in ipairs(self.children) do
- removed = child:remove(obj)
- if removed then break end
- end
- end
- if removed then
- -- Check if we should merge nodes now that we've removed an item
- if self:should_merge() then
- self:merge()
- end
- end
- return removed
- end
- --- Check if the specified bounds intersect with anything in the tree. See also: get_colliding.
- -- @param checkBounds Bounds to check
- -- @return boolean True if there was a collision
- function OctreeNode:is_colliding(checkBounds)
- -- Are the input bounds at least partially in this node?
- if not intersect.aabb_aabb(self.bounds, checkBounds) then
- return false
- end
- -- Check against any objects in this node
- for _, object in ipairs(self.objects) do
- if intersect.aabb_aabb(object.bounds, checkBounds) then
- return true
- end
- end
- -- Check children
- for _, child in ipairs(self.children) do
- if child:is_colliding(checkBounds) then
- return true
- end
- end
- return false
- end
- --- Returns an array of objects that intersect with the specified bounds, if any. Otherwise returns an empty array. See also: is_colliding.
- -- @param checkBounds Bounds to check. Passing by ref as it improve performance with structs
- -- @param results List results
- -- @return table Objects that intersect with the specified bounds
- function OctreeNode:get_colliding(checkBounds, results)
- results = results or {}
- -- Are the input bounds at least partially in this node?
- if not intersect.aabb_aabb(self.bounds, checkBounds) then
- return results
- end
- -- Check against any objects in this node
- for _, object in ipairs(self.objects) do
- if intersect.aabb_aabb(object.bounds, checkBounds) then
- table.insert(results, object.data)
- end
- end
- -- Check children
- for _, child in ipairs(self.children) do
- results = child:get_colliding(checkBounds, results)
- end
- return results
- end
- --- Cast a ray through the node and its children
- -- @param ray Ray with a position and a direction
- -- @param func Function to execute on any objects within child nodes
- -- @param out Table to store results of func in
- -- @param depth (used internally)
- -- @return boolean True if an intersect is detected
- function OctreeNode:cast_ray(ray, func, out, depth)
- depth = depth or 1
- if intersect.ray_aabb(ray, self.bounds) then
- if #self.objects > 0 then
- local hit = func(ray, self.objects, out)
- if hit then
- return hit
- end
- end
- for _, child in ipairs(self.children) do
- local hit = child:cast_ray(ray, func, out, depth + 1)
- if hit then
- return hit
- end
- end
- end
- return false
- end
- --- Set the 8 children of this octree.
- -- @param childOctrees The 8 new child nodes
- function OctreeNode:set_children(childOctrees)
- if #childOctrees ~= 8 then
- print("Child octree array must be length 8. Was length: " .. #childOctrees)
- return
- end
- self.children = childOctrees
- end
- --- We can shrink the octree if:
- --- - This node is >= double minLength in length
- --- - All objects in the root node are within one octant
- --- - This node doesn't have children, or does but 7/8 children are empty
- --- We can also shrink it if there are no objects left at all!
- -- @param minLength Minimum dimensions of a node in this octree
- -- @return table The new root, or the existing one if we didn't shrink
- function OctreeNode:shrink_if_possible(minLength)
- if self.baseLength < 2 * minLength then
- return self
- end
- if #self.objects == 0 and #self.children == 0 then
- return self
- end
- -- Check objects in root
- local bestFit = 0
- for i, object in ipairs(self.objects) do
- local newBestFit = self:best_fit_child(object.bounds)
- if i == 1 or newBestFit == bestFit then
- -- In same octant as the other(s). Does it fit completely inside that octant?
- if intersect.encapsulate_aabb(self.childBounds[newBestFit], object.bounds) then
- if bestFit < 1 then
- bestFit = newBestFit
- end
- else
- -- Nope, so we can't reduce. Otherwise we continue
- return self
- end
- else
- return self -- Can't reduce - objects fit in different octants
- end
- end
- -- Check objects in children if there are any
- if #self.children > 0 then
- local childHadContent = false
- for i, child in ipairs(self.children) do
- if child:has_any_objects() then
- if childHadContent then
- return self -- Can't shrink - another child had content already
- end
- if bestFit > 0 and bestFit ~= i then
- return self -- Can't reduce - objects in root are in a different octant to objects in child
- end
- childHadContent = true
- bestFit = i
- end
- end
- end
- -- Can reduce
- if #self.children == 0 then
- -- We don't have any children, so just shrink this node to the new size
- -- We already know that everything will still fit in it
- self:set_values(self.baseLength / 2, self.minSize, self.looseness, self.childBounds[bestFit].center)
- return self
- end
- -- We have children. Use the appropriate child as the new root node
- return self.children[bestFit]
- end
- --- Set values for this node.
- -- @param baseLength Length of this node, not taking looseness into account
- -- @param minSize Minimum size of nodes in this octree
- -- @param looseness Multiplier for baseLengthVal to get the actual size
- -- @param center Centre position of this node
- function OctreeNode:set_values(baseLength, minSize, looseness, center)
- -- Length of this node if it has a looseness of 1.0
- self.baseLength = baseLength
- -- Minimum size for a node in this octree
- self.minSize = minSize
- -- Looseness value for this node
- self.looseness = looseness
- -- Centre of this node
- self.center = center
- -- Actual length of sides, taking the looseness value into account
- self.adjLength = self.looseness * self.baseLength
- -- Create the bounding box.
- self.size = vec3(self.adjLength, self.adjLength, self.adjLength)
- -- Bounding box that represents this node
- self.bounds = new_bound(self.center, self.size)
- self.quarter = self.baseLength / 4
- self.childActualLength = (self.baseLength / 2) * self.looseness
- self.childActualSize = vec3(self.childActualLength, self.childActualLength, self.childActualLength)
- -- Bounds of potential children to this node. These are actual size (with looseness taken into account), not base size
- self.childBounds = {
- new_bound(self.center + vec3(-self.quarter, self.quarter, -self.quarter), self.childActualSize),
- new_bound(self.center + vec3( self.quarter, self.quarter, -self.quarter), self.childActualSize),
- new_bound(self.center + vec3(-self.quarter, self.quarter, self.quarter), self.childActualSize),
- new_bound(self.center + vec3( self.quarter, self.quarter, self.quarter), self.childActualSize),
- new_bound(self.center + vec3(-self.quarter, -self.quarter, -self.quarter), self.childActualSize),
- new_bound(self.center + vec3( self.quarter, -self.quarter, -self.quarter), self.childActualSize),
- new_bound(self.center + vec3(-self.quarter, -self.quarter, self.quarter), self.childActualSize),
- new_bound(self.center + vec3( self.quarter, -self.quarter, self.quarter), self.childActualSize)
- }
- end
- --- Splits the octree into eight children.
- function OctreeNode:split()
- if #self.children > 0 then return end
- local quarter = self.baseLength / 4
- local newLength = self.baseLength / 2
- table.insert(self.children, Node(newLength, self.minSize, self.looseness, self.center + vec3(-quarter, quarter, -quarter)))
- table.insert(self.children, Node(newLength, self.minSize, self.looseness, self.center + vec3( quarter, quarter, -quarter)))
- table.insert(self.children, Node(newLength, self.minSize, self.looseness, self.center + vec3(-quarter, quarter, quarter)))
- table.insert(self.children, Node(newLength, self.minSize, self.looseness, self.center + vec3( quarter, quarter, quarter)))
- table.insert(self.children, Node(newLength, self.minSize, self.looseness, self.center + vec3(-quarter, -quarter, -quarter)))
- table.insert(self.children, Node(newLength, self.minSize, self.looseness, self.center + vec3( quarter, -quarter, -quarter)))
- table.insert(self.children, Node(newLength, self.minSize, self.looseness, self.center + vec3(-quarter, -quarter, quarter)))
- table.insert(self.children, Node(newLength, self.minSize, self.looseness, self.center + vec3( quarter, -quarter, quarter)))
- end
- --- Merge all children into this node - the opposite of Split.
- --- Note: We only have to check one level down since a merge will never happen if the children already have children,
- --- since THAT won't happen unless there are already too many objects to merge.
- function OctreeNode:merge()
- for _, child in ipairs(self.children) do
- for _, object in ipairs(child.objects) do
- table.insert(self.objects, object)
- end
- end
- -- Remove the child nodes (and the objects in them - they've been added elsewhere now)
- self.children = {}
- end
- --- Find which child node this object would be most likely to fit in.
- -- @param objBounds The object's bounds
- -- @return number One of the eight child octants
- function OctreeNode:best_fit_child(objBounds)
- return (objBounds.center.x <= self.center.x and 0 or 1) + (objBounds.center.y >= self.center.y and 0 or 4) + (objBounds.center.z <= self.center.z and 0 or 2) + 1
- end
- --- Checks if there are few enough objects in this node and its children that the children should all be merged into this.
- -- @return boolean True there are less or the same abount of objects in this and its children than numObjectsAllowed
- function OctreeNode:should_merge()
- local totalObjects = #self.objects
- for _, child in ipairs(self.children) do
- if #child.children > 0 then
- -- If any of the *children* have children, there are definitely too many to merge,
- -- or the child would have been merged already
- return false
- end
- totalObjects = totalObjects + #child.objects
- end
- return totalObjects <= self.numObjectsAllowed
- end
- --- Checks if this node or anything below it has something in it.
- -- @return boolean True if this node or any of its children, grandchildren etc have something in the
- function OctreeNode:has_any_objects()
- if #self.objects > 0 then return true end
- for _, child in ipairs(self.children) do
- if child:has_any_objects() then return true end
- end
- return false
- end
- --- Draws node boundaries visually for debugging.
- -- @param cube Cube model to draw
- -- @param depth Used for recurcive calls to this 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
- })
|