mirror of
https://github.com/SashLilac/cambridge.git
synced 2024-11-22 08:09:03 -06:00
635 lines
21 KiB
Lua
635 lines
21 KiB
Lua
-- 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
|
|
})
|