• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

excessive / cpml / 6132588926

09 Sep 2023 06:38PM UTC coverage: 14.013% (-44.7%) from 58.701%
6132588926

push

github

FatalError42O
fixed Busted support (hopefully)

975 of 6958 relevant lines covered (14.01%)

10.82 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.42
/modules/octree.lua
1
-- https://github.com/Nition/UnityOctree
2
-- https://github.com/Nition/UnityOctree/blob/master/LICENCE
3
-- https://github.com/Nition/UnityOctree/blob/master/Scripts/BoundsOctree.cs
4
-- https://github.com/Nition/UnityOctree/blob/master/Scripts/BoundsOctreeNode.cs
5

6
--- Octree
7
-- @module octree
8

9
local intersect  = require(modules .. "intersect")
3✔
10
local mat4       = require(modules .. "mat4")
×
11
local utils      = require(modules .. "utils")
×
12
local vec3       = require(modules .. "vec3")
×
13
local Octree     = {}
×
14
local OctreeNode = {}
×
15
local Node
16

17
Octree.__index     = Octree
×
18
OctreeNode.__index = OctreeNode
×
19

20
--== Octree ==--
21

22
--- Constructor for the bounds octree.
23
-- @param initialWorldSize Size of the sides of the initial node, in metres. The octree will never shrink smaller than this
24
-- @param initialWorldPos Position of the centre of the initial node
25
-- @param minNodeSize Nodes will stop splitting if the new nodes would be smaller than this (metres)
26
-- @param looseness Clamped between 1 and 2. Values > 1 let nodes overlap
27
local function new(initialWorldSize, initialWorldPos, minNodeSize, looseness)
28
        local tree = setmetatable({}, Octree)
×
29

30
        if minNodeSize > initialWorldSize then
×
31
                print("Minimum node size must be at least as big as the initial world size. Was: " .. minNodeSize .. " Adjusted to: " .. initialWorldSize)
×
32
                minNodeSize = initialWorldSize
×
33
        end
34

35
        -- The total amount of objects currently in the tree
36
        tree.count = 0
×
37

38
        -- Size that the octree was on creation
39
        tree.initialSize = initialWorldSize
×
40

41
        -- Minimum side length that a node can be - essentially an alternative to having a max depth
42
        tree.minSize = minNodeSize
×
43

44
        -- Should be a value between 1 and 2. A multiplier for the base size of a node.
45
        -- 1.0 is a "normal" octree, while values > 1 have overlap
46
        tree.looseness = utils.clamp(looseness, 1, 2)
×
47

48
        -- Root node of the octree
49
        tree.rootNode = Node(tree.initialSize, tree.minSize, tree.looseness, initialWorldPos)
×
50

51
        return tree
×
52
end
53

54
--- Used when growing the octree. Works out where the old root node would fit inside a new, larger root node.
55
-- @param xDir X direction of growth. 1 or -1
56
-- @param yDir Y direction of growth. 1 or -1
57
-- @param zDir Z direction of growth. 1 or -1
58
-- @return Octant where the root node should be
59
local function get_root_pos_index(xDir, yDir, zDir)
60
        local result = xDir > 0 and 1 or 0
×
61
        if yDir < 0 then return result + 4 end
×
62
        if zDir > 0 then return result + 2 end
×
63
end
64

65
--- Add an object.
66
-- @param obj Object to add
67
-- @param objBounds 3D bounding box around the object
68
function Octree:add(obj, objBounds)
×
69
        -- Add object or expand the octree until it can be added
70
        local count = 0 -- Safety check against infinite/excessive growth
×
71

72
        while not self.rootNode:add(obj, objBounds) do
×
73
                count = count + 1
×
74
                self:grow(objBounds.center - self.rootNode.center)
×
75

76
                if count > 20 then
×
77
                        print("Aborted Add operation as it seemed to be going on forever (" .. count - 1 .. ") attempts at growing the octree.")
×
78
                        return
×
79
                end
80

81
                self.count = self.count + 1
×
82
        end
83
end
84

85
--- Remove an object. Makes the assumption that the object only exists once in the tree.
86
-- @param obj Object to remove
87
-- @return bool True if the object was removed successfully
88
function Octree:remove(obj)
×
89
        local removed = self.rootNode:remove(obj)
×
90

91
        -- See if we can shrink the octree down now that we've removed the item
92
        if removed then
×
93
                self.count = self.count - 1
×
94
                self:shrink()
×
95
        end
96

97
        return removed
×
98
end
99

100
--- Check if the specified bounds intersect with anything in the tree. See also: get_colliding.
101
-- @param checkBounds bounds to check
102
-- @return bool True if there was a collision
103
function Octree:is_colliding(checkBounds)
×
104
        return self.rootNode:is_colliding(checkBounds)
×
105
end
106

107
--- Returns an array of objects that intersect with the specified bounds, if any. Otherwise returns an empty array. See also: is_colliding.
108
-- @param checkBounds bounds to check
109
-- @return table Objects that intersect with the specified bounds
110
function Octree:get_colliding(checkBounds)
×
111
        return self.rootNode:get_colliding(checkBounds)
×
112
end
113

114
--- Cast a ray through the node and its children
115
-- @param ray Ray with a position and a direction
116
-- @param func Function to execute on any objects within child nodes
117
-- @param out Table to store results of func in
118
-- @return boolean True if an intersect detected
119
function Octree:cast_ray(ray, func, out)
×
120
        assert(func)
×
121
        return self.rootNode:cast_ray(ray, func, out)
×
122
end
123

124
--- Draws node boundaries visually for debugging.
125
function Octree:draw_bounds(cube)
×
126
        self.rootNode:draw_bounds(cube)
×
127
end
128

129
--- Draws the bounds of all objects in the tree visually for debugging.
130
function Octree:draw_objects(cube, filter)
×
131
        self.rootNode:draw_objects(cube, filter)
×
132
end
133

134
--- Grow the octree to fit in all objects.
135
-- @param direction Direction to grow
136
function Octree:grow(direction)
×
137
        local xDirection = direction.x >= 0 and 1 or -1
×
138
        local yDirection = direction.y >= 0 and 1 or -1
×
139
        local zDirection = direction.z >= 0 and 1 or -1
×
140

141
        local oldRoot   = self.rootNode
×
142
        local half      = self.rootNode.baseLength / 2
×
143
        local newLength = self.rootNode.baseLength * 2
×
144
        local newCenter = self.rootNode.center + vec3(xDirection * half, yDirection * half, zDirection * half)
×
145

146
        -- Create a new, bigger octree root node
147
        self.rootNode = Node(newLength, self.minSize, self.looseness, newCenter)
×
148

149
        -- Create 7 new octree children to go with the old root as children of the new root
150
        local rootPos  = get_root_pos_index(xDirection, yDirection, zDirection)
×
151
        local children = {}
×
152

153
        for i = 0, 7 do
×
154
                if i == rootPos then
×
155
                        children[i+1] = oldRoot
×
156
                else
157
                        xDirection  = i % 2 == 0 and -1 or 1
×
158
                        yDirection  = i > 3 and -1 or 1
×
159
                        zDirection  = (i < 2 or (i > 3 and i < 6)) and -1 or 1
×
160
                        children[i+1] = Node(self.rootNode.baseLength, self.minSize, self.looseness, newCenter + vec3(xDirection * half, yDirection * half, zDirection * half))
×
161
                end
162
        end
163

164
        -- Attach the new children to the new root node
165
        self.rootNode:set_children(children)
×
166
end
167

168
--- Shrink the octree if possible, else leave it the same.
169
function Octree:shrink()
×
170
        self.rootNode = self.rootNode:shrink_if_possible(self.initialSize)
×
171
end
172

173
--== Octree Node ==--
174

175
--- Constructor.
176
-- @param baseLength Length of this node, not taking looseness into account
177
-- @param minSize Minimum size of nodes in this octree
178
-- @param looseness Multiplier for baseLengthVal to get the actual size
179
-- @param center Centre position of this node
180
local function new_node(baseLength, minSize, looseness, center)
181
        local node = setmetatable({}, OctreeNode)
×
182

183
        -- Objects in this node
184
        node.objects = {}
×
185

186
        -- Child nodes
187
        node.children = {}
×
188

189
        -- If there are already numObjectsAllowed in a node, we split it into children
190
        -- A generally good number seems to be something around 8-15
191
        node.numObjectsAllowed = 8
×
192

193
        node:set_values(baseLength, minSize, looseness, center)
×
194

195
        return node
×
196
end
197

198
local function new_bound(center, size)
199
        return {
×
200
                center = center,
201
                size   = size,
202
                min    = center - (size / 2),
203
                max    = center + (size / 2)
×
204
        }
205
end
206

207
--- Add an object.
208
-- @param obj Object to add
209
-- @param objBounds 3D bounding box around the object
210
-- @return boolean True if the object fits entirely within this node
211
function OctreeNode:add(obj, objBounds)
×
212
        if not intersect.encapsulate_aabb(self.bounds, objBounds) then
×
213
                return false
×
214
        end
215

216
        -- We know it fits at this level if we've got this far
217
        -- Just add if few objects are here, or children would be below min size
218
        if #self.objects < self.numObjectsAllowed
×
219
        or self.baseLength / 2 < self.minSize then
×
220
                table.insert(self.objects, {
×
221
                        data   = obj,
222
                        bounds = objBounds
×
223
                })
224
        else
225
                -- Fits at this level, but we can go deeper. Would it fit there?
226

227
                local best_fit_child
228

229
                -- Create the 8 children
230
                if #self.children == 0 then
×
231
                        self:split()
×
232

233
                        if #self.children == 0 then
×
234
                                print("Child creation failed for an unknown reason. Early exit.")
×
235
                                return false
×
236
                        end
237

238
                        -- Now that we have the new children, see if this node's existing objects would fit there
239
                        for i = #self.objects, 1, -1 do
×
240
                                local object = self.objects[i]
×
241
                                -- Find which child the object is closest to based on where the
242
                                -- object's center is located in relation to the octree's center.
243
                                best_fit_child = self:best_fit_child(object.bounds)
×
244

245
                                -- Does it fit?
246
                                if intersect.encapsulate_aabb(self.children[best_fit_child].bounds, object.bounds) then
×
247
                                        self.children[best_fit_child]:add(object.data, object.bounds) -- Go a level deeper
×
248
                                        table.remove(self.objects, i) -- Remove from here
×
249
                                end
250
                        end
251
                end
252

253
                -- Now handle the new object we're adding now
254
                best_fit_child = self:best_fit_child(objBounds)
×
255

256
                if intersect.encapsulate_aabb(self.children[best_fit_child].bounds, objBounds) then
×
257
                        self.children[best_fit_child]:add(obj, objBounds)
×
258
                else
259
                        table.insert(self.objects, {
×
260
                                data   = obj,
261
                                bounds = objBounds
×
262
                        })
263
                end
264
        end
265

266
        return true
×
267
end
268

269
--- Remove an object. Makes the assumption that the object only exists once in the tree.
270
-- @param obj Object to remove
271
-- @return boolean True if the object was removed successfully
272
function OctreeNode:remove(obj)
×
273
        local removed = false
×
274

275
        for i, object in ipairs(self.objects) do
×
276
                if object == obj then
×
277
                        removed = table.remove(self.objects, i) and true or false
×
278
                        break
279
                end
280
        end
281

282
        if not removed then
×
283
                for _, child in ipairs(self.children) do
×
284
                        removed = child:remove(obj)
×
285
                        if removed then break end
×
286
                end
287
        end
288

289
        if removed then
×
290
                -- Check if we should merge nodes now that we've removed an item
291
                if self:should_merge() then
×
292
                        self:merge()
×
293
                end
294
        end
295

296
        return removed
×
297
end
298

299
--- Check if the specified bounds intersect with anything in the tree. See also: get_colliding.
300
-- @param checkBounds Bounds to check
301
-- @return boolean True if there was a collision
302
function OctreeNode:is_colliding(checkBounds)
×
303
        -- Are the input bounds at least partially in this node?
304
        if not intersect.aabb_aabb(self.bounds, checkBounds) then
×
305
                return false
×
306
        end
307

308
        -- Check against any objects in this node
309
        for _, object in ipairs(self.objects) do
×
310
                if intersect.aabb_aabb(object.bounds, checkBounds) then
×
311
                        return true
×
312
                end
313
        end
314

315
        -- Check children
316
        for _, child in ipairs(self.children) do
×
317
                if child:is_colliding(checkBounds) then
×
318
                        return true
×
319
                end
320
        end
321

322
        return false
×
323
end
324

325
--- Returns an array of objects that intersect with the specified bounds, if any. Otherwise returns an empty array. See also: is_colliding.
326
-- @param checkBounds Bounds to check. Passing by ref as it improve performance with structs
327
-- @param results List results
328
-- @return table Objects that intersect with the specified bounds
329
function OctreeNode:get_colliding(checkBounds, results)
×
330
        results = results or {}
×
331

332
        -- Are the input bounds at least partially in this node?
333
        if not intersect.aabb_aabb(self.bounds, checkBounds) then
×
334
                return results
×
335
        end
336

337
        -- Check against any objects in this node
338
        for _, object in ipairs(self.objects) do
×
339
                if intersect.aabb_aabb(object.bounds, checkBounds) then
×
340
                        table.insert(results, object.data)
×
341
                end
342
        end
343

344
        -- Check children
345
        for _, child in ipairs(self.children) do
×
346
                results = child:get_colliding(checkBounds, results)
×
347
        end
348

349
        return results
×
350
end
351

352
--- Cast a ray through the node and its children
353
-- @param ray Ray with a position and a direction
354
-- @param func Function to execute on any objects within child nodes
355
-- @param out Table to store results of func in
356
-- @param depth (used internally)
357
-- @return boolean True if an intersect is detected
358
function OctreeNode:cast_ray(ray, func, out, depth)
×
359
        depth = depth or 1
×
360

361
        if intersect.ray_aabb(ray, self.bounds) then
×
362
                if #self.objects > 0 then
×
363
                        local hit = func(ray, self.objects, out)
×
364

365
                        if hit then
×
366
                                return hit
×
367
                        end
368
                end
369

370
                for _, child in ipairs(self.children) do
×
371
                        local hit = child:cast_ray(ray, func, out, depth + 1)
×
372

373
                        if hit then
×
374
                                return hit
×
375
                        end
376
                end
377
        end
378

379
        return false
×
380
end
381

382
--- Set the 8 children of this octree.
383
-- @param childOctrees The 8 new child nodes
384
function OctreeNode:set_children(childOctrees)
×
385
        if #childOctrees ~= 8 then
×
386
                print("Child octree array must be length 8. Was length: " .. #childOctrees)
×
387
                return
×
388
        end
389

390
        self.children = childOctrees
×
391
end
392

393
--- We can shrink the octree if:
394
--- - This node is >= double minLength in length
395
--- - All objects in the root node are within one octant
396
--- - This node doesn't have children, or does but 7/8 children are empty
397
--- We can also shrink it if there are no objects left at all!
398
-- @param minLength Minimum dimensions of a node in this octree
399
-- @return table The new root, or the existing one if we didn't shrink
400
function OctreeNode:shrink_if_possible(minLength)
×
401
        if self.baseLength < 2 * minLength then
×
402
                return self
×
403
        end
404

405
        if #self.objects == 0 and #self.children == 0 then
×
406
                return self
×
407
        end
408

409
        -- Check objects in root
410
        local bestFit = 0
×
411

412
        for i, object in ipairs(self.objects) do
×
413
                local newBestFit = self:best_fit_child(object.bounds)
×
414

415
                if i == 1 or newBestFit == bestFit then
×
416
                        -- In same octant as the other(s). Does it fit completely inside that octant?
417
                        if intersect.encapsulate_aabb(self.childBounds[newBestFit], object.bounds) then
×
418
                                if bestFit < 1 then
×
419
                                        bestFit = newBestFit
×
420
                                end
421
                        else
422
                                -- Nope, so we can't reduce. Otherwise we continue
423
                                return self
×
424
                        end
425
                else
426
                        return self -- Can't reduce - objects fit in different octants
×
427
                end
428
        end
429

430
        -- Check objects in children if there are any
431
        if #self.children > 0 then
×
432
                local childHadContent = false
×
433

434
                for i, child in ipairs(self.children) do
×
435
                        if child:has_any_objects() then
×
436
                                if childHadContent then
×
437
                                        return self -- Can't shrink - another child had content already
×
438
                                end
439

440
                                if bestFit > 0 and bestFit ~= i then
×
441
                                        return self -- Can't reduce - objects in root are in a different octant to objects in child
×
442
                                end
443

444
                                childHadContent = true
×
445
                                bestFit = i
×
446
                        end
447
                end
448
        end
449

450
        -- Can reduce
451
        if #self.children == 0 then
×
452
                -- We don't have any children, so just shrink this node to the new size
453
                -- We already know that everything will still fit in it
454
                self:set_values(self.baseLength / 2, self.minSize, self.looseness, self.childBounds[bestFit].center)
×
455
                return self
×
456
        end
457

458
        -- We have children. Use the appropriate child as the new root node
459
        return self.children[bestFit]
×
460
end
461

462
--- Set values for this node.
463
-- @param baseLength Length of this node, not taking looseness into account
464
-- @param minSize Minimum size of nodes in this octree
465
-- @param looseness Multiplier for baseLengthVal to get the actual size
466
-- @param center Centre position of this node
467
function OctreeNode:set_values(baseLength, minSize, looseness, center)
×
468
        -- Length of this node if it has a looseness of 1.0
469
        self.baseLength = baseLength
×
470

471
        -- Minimum size for a node in this octree
472
        self.minSize = minSize
×
473

474
        -- Looseness value for this node
475
        self.looseness = looseness
×
476

477
        -- Centre of this node
478
        self.center = center
×
479

480
        -- Actual length of sides, taking the looseness value into account
481
        self.adjLength = self.looseness * self.baseLength
×
482

483
        -- Create the bounding box.
484
        self.size = vec3(self.adjLength, self.adjLength, self.adjLength)
×
485

486
        -- Bounding box that represents this node
487
        self.bounds = new_bound(self.center, self.size)
×
488

489
        self.quarter           = self.baseLength / 4
×
490
        self.childActualLength = (self.baseLength / 2) * self.looseness
×
491
        self.childActualSize   = vec3(self.childActualLength, self.childActualLength, self.childActualLength)
×
492

493
        -- Bounds of potential children to this node. These are actual size (with looseness taken into account), not base size
494
        self.childBounds =  {
×
495
                new_bound(self.center + vec3(-self.quarter,  self.quarter, -self.quarter), self.childActualSize),
×
496
                new_bound(self.center + vec3( self.quarter,  self.quarter, -self.quarter), self.childActualSize),
×
497
                new_bound(self.center + vec3(-self.quarter,  self.quarter,  self.quarter), self.childActualSize),
×
498
                new_bound(self.center + vec3( self.quarter,  self.quarter,  self.quarter), self.childActualSize),
×
499
                new_bound(self.center + vec3(-self.quarter, -self.quarter, -self.quarter), self.childActualSize),
×
500
                new_bound(self.center + vec3( self.quarter, -self.quarter, -self.quarter), self.childActualSize),
×
501
                new_bound(self.center + vec3(-self.quarter, -self.quarter,  self.quarter), self.childActualSize),
×
502
                new_bound(self.center + vec3( self.quarter, -self.quarter,  self.quarter), self.childActualSize)
×
503
        }
504
end
505

506
--- Splits the octree into eight children.
507
function OctreeNode:split()
×
508
        if #self.children > 0 then return end
×
509

510
        local quarter   = self.baseLength / 4
×
511
        local newLength = self.baseLength / 2
×
512

513
        table.insert(self.children, Node(newLength, self.minSize, self.looseness, self.center + vec3(-quarter,  quarter, -quarter)))
×
514
        table.insert(self.children, Node(newLength, self.minSize, self.looseness, self.center + vec3( quarter,  quarter, -quarter)))
×
515
        table.insert(self.children, Node(newLength, self.minSize, self.looseness, self.center + vec3(-quarter,  quarter,  quarter)))
×
516
        table.insert(self.children, Node(newLength, self.minSize, self.looseness, self.center + vec3( quarter,  quarter,  quarter)))
×
517
        table.insert(self.children, Node(newLength, self.minSize, self.looseness, self.center + vec3(-quarter, -quarter, -quarter)))
×
518
        table.insert(self.children, Node(newLength, self.minSize, self.looseness, self.center + vec3( quarter, -quarter, -quarter)))
×
519
        table.insert(self.children, Node(newLength, self.minSize, self.looseness, self.center + vec3(-quarter, -quarter,  quarter)))
×
520
        table.insert(self.children, Node(newLength, self.minSize, self.looseness, self.center + vec3( quarter, -quarter,  quarter)))
×
521
end
522

523
--- Merge all children into this node - the opposite of Split.
524
--- Note: We only have to check one level down since a merge will never happen if the children already have children,
525
--- since THAT won't happen unless there are already too many objects to merge.
526
function OctreeNode:merge()
×
527
        for _, child in ipairs(self.children) do
×
528
                for _, object in ipairs(child.objects) do
×
529
                        table.insert(self.objects, object)
×
530
                end
531
        end
532

533
        -- Remove the child nodes (and the objects in them - they've been added elsewhere now)
534
        self.children = {}
×
535
end
536

537
--- Find which child node this object would be most likely to fit in.
538
-- @param objBounds The object's bounds
539
-- @return number One of the eight child octants
540
function OctreeNode:best_fit_child(objBounds)
×
541
        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
×
542
end
543

544
--- Checks if there are few enough objects in this node and its children that the children should all be merged into this.
545
-- @return boolean True there are less or the same abount of objects in this and its children than numObjectsAllowed
546
function OctreeNode:should_merge()
×
547
        local totalObjects = #self.objects
×
548

549
        for _, child in ipairs(self.children) do
×
550
                if #child.children > 0 then
×
551
                        -- If any of the *children* have children, there are definitely too many to merge,
552
                        -- or the child would have been merged already
553
                        return false
×
554
                end
555

556
                totalObjects = totalObjects + #child.objects
×
557
        end
558

559
        return totalObjects <= self.numObjectsAllowed
×
560
end
561

562
--- Checks if this node or anything below it has something in it.
563
-- @return boolean True if this node or any of its children, grandchildren etc have something in the
564
function OctreeNode:has_any_objects()
×
565
        if #self.objects > 0 then return true end
×
566

567
        for _, child in ipairs(self.children) do
×
568
                if child:has_any_objects() then return true end
×
569
        end
570

571
        return false
×
572
end
573

574
Node = setmetatable({
×
575
        new = new_node
×
576
}, {
×
577
        __call = function(_, ...) return new_node(...) end
×
578
})
579

580
return setmetatable({
×
581
        new = new
×
582
}, {
×
583
        __call = function(_, ...) return new(...) end
×
584
})
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc