Notifications
Clear all

[Closed] Octree problem

Hi,
I’m trying to implement an octree in mxs but I’m having trouble with (what I think) is assigning the right node to right branch. It’s either in the findBranch or insertNode method that fails.


-- http://code.activestate.com/recipes/498121-python-octree-implementation/ 
(
	clearListener()
	delete $*
	
	local MAXOBJECTSPERCUBE = 10
	local NUMTESTOBJECTS = 1000
	local WORLDSIZE = 15000.0
	
	local debuggArr = #()
	
	fn dirLookUp n =
		case n of ( 3: 1; 2: 2; (-2): 3; (-1): 4; 1: 5; 0: 6; (-4): 7; (-3):8 )

	struct octNode
	(
		position, 
		size, 
		isLeafNode,
		data, 
		branches,
		bbox,
		on create do
		(
			this.isLeafNode = true
			this.branches = for i = 1 to 8 collect undefined
			bbox = dummy pos:position boxSize:[size, size, size]
		)
	)

	struct ocTree
	(
		root,
		worldSize,
		
		fn addNode pos sze objs = 
			(octNode position:pos size:sze data:objs),
		
 		fn findBranch root position =
		(
			local result = 0
			for i = 0 to 2 do
			(
				if root.position[i + 1] <= position[i + 1] then
					result +=  (-4 / (i + 1) / 2)
				else
					result +=  (4 / (i + 1) / 2)
			)
			appendIfUnique debuggArr result
			(dirLookUp result)
		),
		
		fn insertNode root size parent objData =
		(
			if root == undefined then
			(
				local pos = parent.position
				local offset = size / 2
				local branch = findBranch parent objData.position
				local newCenter = [0, 0, 0]
				
				if branch == 1 then
					newCenter = [pos.x - offset, pos.y - offset, pos.z - offset]
				else if branch == 2 then
					newCenter = [pos.x - offset, pos.y - offset, pos.z + offset]
				else if branch == 3 then
					newCenter = [pos.x + offset, pos.y - offset, pos.z + offset]
				else if branch == 4 then
					newCenter = [pos.x + offset, pos.y - offset, pos.z - offset]
				else if branch == 5 then
					newCenter = [pos.x - offset, pos.y + offset, pos.z - offset]
				else if branch == 6 then
					newCenter = [pos.x - offset, pos.y + offset, pos.z + offset]
				else if branch == 7 then
					newCenter = [pos.x + offset, pos.y + offset, pos.z + offset]
				else if branch == 8 then
					newCenter = [pos.x + offset, pos.y + offset, pos.z - offset]  
					
									
				return (addNode newCenter size #(objData))
			)
			
			else if root.position != objData.position and not root.isLeafNode then
			(
				local branch = findBranch root objData.position
				local newSize = root.size / 2
				root.branches[branch] = insertNode root.branches[branch] newSize root objData
			)
			
			else if root.isLeafNode then
			(
				if root.data.count < MAXOBJECTSPERCUBE then
					append root.data objData
				else if root.data.count == MAXOBJECTSPERCUBE then
				(
					append root.data objData
					local objList = root.data
					root.data = #()
					root.isLeafNode = false
					local newSize = root.size / 2
					
					for ob in objList do
					(
						local branch = findBranch root ob.position
						root.branches[branch] = insertNode root.branches[branch] newSize root ob
					)
				)
			)
			root
		),
		
		fn findPosition root position =
		(
			if root == undefined then 
				undefined
			else if root.isLeafNode then 
				root.data
			else 
				findPosition root.branches[findBranch root position] position
		), 
		
		on create do
		(
			root = addNode [0,0,0] worldSize #()
		)
	)
	
	fn objCenter obj =
		obj.min + 0.5 * (obj.max - obj.min)
		
	local myTree = ocTree worldSize:WORLDSIZE	
	local testObjs = #()
	testObjs[NUMTESTOBJECTS] = undefined
	
	with undo off
	(
		with redraw off 
		(
			local start = timestamp()
			for i = 1 to NUMTESTOBJECTS do
			(
				testObj = box pos:(random [-4500.0, -4500.0, -4500.0] [4500.0,4500.0,4500.0]) height:500 width:500 length:500 --point  pos:(random [-4500.0, -4500.0, -4500.0] [4500.0,4500.0,4500.0])
				testObj.pivot = (objCenter testObj)
				testObjs[i] = testObj
				myTree.insertNode myTree.root WORLDSIZE myTree.root testObj
			)
			format "Creating ocTree done. Build time: % seconds
" ((timestamp() - start) / 1000.0)
	/* 		for i = 1 to 10 do
			(
				format "Fetching neighbours for %
" testObjs[i].name
				results = myTree.findPosition myTree.root testObjs[i].position
				for j in results do
					format "% @ %; distance %
" j.name j.position (distance testObjs[i].position j.position)
			) */
			results = myTree.findPosition myTree.root testObjs[1].position
			select (results + testObjs[1])
		)
	)
	print debuggArr
)


Reference: http://code.activestate.com/recipes/498121-python-octree-implementation/
I’m guessing it has to do with some of the methods not mapping 1:1 to mxs, but as far as I can see I’ve taken that into account.

22 Replies

Found the problem, it was the findBranch method. Turns out max rounds off numbers different than python.
In python -4/3 = -2.
but in max -4/3 = -1
so the findBranch method would not produce all of the 8 possible answers.

quickfix:


fn findBranch root position =
(
	local result = 0
	for i = 0 to 2 do
	(
		if root.position[i + 1] <= position[i + 1] then
			result += case i of (0:-2; 1:-1; 2:-1) --result += (-4 / (i + 1) / 2)
		else
			result += case i of (0: 2; 1: 1; 2: 0) --result += (4 / (i + 1) / 2)
	)
	appendIfUnique debuggArr result
	(dirLookUp result)
)

Stumbled into a new problem.
Basically, I am making a simple custom scatter that check for object intersections, and I am using the octree as an acceleration structure for the spatial lookups.
I’m having a hard time finding a light weight method for doing a nearest neighbour search, preferably a radius based search or a k nearest neighbour.

When I look up a position for an object – let’s call it K – now, I get the the octree node for K, and for that node’s parent I search recursively through that parent’s branches and collecting the objects. For each object found I delete K if it intersects with one of the objects collected.
That works fine if K’s bounding box is noton the edge of an octree node that is notbranched together with a neighbouring octree node. So now intersections between objects will occur when objects are placed on these described borders.

Picture to illustrate; the circled object is the object I do a neighbour search on. The selected objects (the ones with a bounding box displayed) are the ones I found.

Larger image
You see that it lays on the edge of a green dummy (octree node) and also intersects with another teapot

If you run this code it will scatter some objects on a sphere and build an octree at the same time. It will choose a random scattered object and select it’s neighbours found by the octree. If you find two objects that are intersecting you’ll see that they also are intersecting one of the green dummies representing a octree node.


(
	-- http://code.activestate.com/recipes/498121-python-octree-implementation/ 

	clearListener()
	delete $*
	
	global debugArr
	
	fn dirLookUp n =
		case n of ( 3: 1; 2: 2; (-2): 3; (-1): 4; 1: 5; 0: 6; (-4): 7; (-3):8 )

	struct octNode
	(
		position, 
		size, 
		isLeafNode,
		data, 
		branches,
		bbox,
		parent,
		on create do
		(
			this.isLeafNode = true
			this.branches = for i = 1 to 8 collect undefined
			bbox = dummy pos:position boxSize:[size, size, size]
		)
	)

	struct ocTree
	(
		root,
		worldSize,
		position,
		maxObjectsPerCube,
		
		fn addNode pos sze objs parent = 
			(octNode position:pos size:sze data:objs parent:parent),
		
		fn findBranch root position =
		(
			local result = 0
			for i = 0 to 2 do
			(
				if root.position[i + 1] <= position[i + 1] then
					result += case i of (0:-2; 1:-1; 2:-1) --result += (-4 / (i + 1) / 2)
				else
					result += case i of (0: 2; 1: 1; 2: 0) --result += (4 / (i + 1) / 2)
			)
			(dirLookUp result)
		),
		
		fn insertNode root size parent objData =
		(
			if root == undefined then
			(
				local pos = parent.position
				local offset = size / 2
				local branch = findBranch parent objData.position
				local newCenter = [0, 0, 0]
				
				if branch == 1 then
					newCenter = [pos.x - offset, pos.y - offset, pos.z - offset]
				else if branch == 2 then
					newCenter = [pos.x - offset, pos.y - offset, pos.z + offset]
				else if branch == 3 then
					newCenter = [pos.x + offset, pos.y - offset, pos.z + offset]
				else if branch == 4 then
					newCenter = [pos.x + offset, pos.y - offset, pos.z - offset]
				else if branch == 5 then
					newCenter = [pos.x - offset, pos.y + offset, pos.z - offset]
				else if branch == 6 then
					newCenter = [pos.x - offset, pos.y + offset, pos.z + offset]
				else if branch == 7 then
					newCenter = [pos.x + offset, pos.y + offset, pos.z + offset]
				else if branch == 8 then
					newCenter = [pos.x + offset, pos.y + offset, pos.z - offset]  
					
									
				return (addNode newCenter size #(objData) parent)
			)
			
			else if root.position != objData.position and not root.isLeafNode then
			(
				local branch = findBranch root objData.position
				local newSize = root.size / 2
				root.branches[branch] = insertNode root.branches[branch] newSize root objData
			)
			
			else if root.isLeafNode then
			(
				if root.data.count < maxObjectsPerCube then
					append root.data objData
				else if root.data.count == maxObjectsPerCube then
				(
					append root.data objData
					local objList = root.data
					root.data = #()
					root.isLeafNode = false
					local newSize = root.size / 2
					
					for ob in objList do
					(
						local branch = findBranch root ob.position
						root.branches[branch] = insertNode root.branches[branch] newSize root ob
					)
				)
			)
			root
		),
		
		fn findPosition root position =
		(
			if root == undefined then 
				undefined
			else if root.isLeafNode then 
				root
			else 
				findPosition root.branches[findBranch root position] position
		), 
		
		fn getNodesFromBranches octNode arr =
		(
			if octNode.isLeafNode then 
				join arr octNode.data
			else
				for i in octNode.branches where i != undefined do
					getNodesFromBranches i arr
		),
		
		on create do
		(
			root = addNode position worldSize #() undefined
		)
	)
	
	fn objCenter obj =
		obj.min + 0.5 * (obj.max - obj.min)
			
	fn ocTreeScatter obj numObjs refObjs asInstance tryCountLim:1e4= 
	(
		local worldSize = 0.80 * distance obj.max obj.min
		local maxObjectsPerCube = 16
		local tries = 0
		local objsPlaced = 0
		local objsToDelete = #()
		
		global ot = ocTree worldSize:worldSize position:(objCenter obj) maxObjectsPerCube:maxObjectsPerCube
		
		local copyType = if asInstance then instance else copy
		local fVerts; local p1; local newObj; local f; local parentData; local curBox; local neighbours; local col
		local gVert = polyop.getVert
		local gFaceNormal = polyop.getFaceNormal
		local getVertsUsingFace = polyop.getVertsUsingFace
		
		if classOf obj != editable_poly then convertTo obj polyMeshObject
		
		while tries < tryCountLim and objsPlaced < numObjs do
		(
			objsPlaced += 1
			newObj = copyType refObjs[random 1 refObjs.count] 
			f = random 1 obj.numFaces
			fVerts = (getVertsUsingFace obj f) as array
			if fVerts.count > 3 then fVerts = for i = 1 to 3 collect fVerts[random 1 fVerts.count]
			p1 = gVert obj fVerts[1] +  ((random 0.0 1.0) * (gVert obj fVerts[2] - gVert obj fVerts[1]))
			newObj.pos = p1 + ((random 0.0 1.0) * (gVert obj fVerts[3] - p1))
			newObj.dir = gFaceNormal obj f
			
			neighbours = #()
			col = false
			
			curBox = ot.findPosition ot.root (objCenter newObj)
			if curBox != undefined then 
				parentData = curBox.parent
	
			if parentData != undefined then
				ot.getNodesFromBranches parentData neighbours 
			
 			if neighbours != undefined then
				for n in neighbours where not col and intersects newObj n do
				(
					tries += 1
					objsPlaced -= 1
					append objsToDelete newObj
					col = true
				) 

			if not col then 
			(
				centerPivot newObj
				ot.insertNode ot.root worldSize ot.root newObj
			)
		)
	--	format "Deleting % object(s) that intersected
" objsToDelete.count
		delete objsToDelete
	)
	
	local testScatObj = geoSphere radius:40
	local refObjs = #(teapot radius:1, box height:3 width:1 length:1)
	local numObjects = 1000
	
	local	start = timestamp()
	with redraw off ( with undo off ( ocTreeScatter testScatObj numObjects refObjs true))
	format "Scattering % objects took % seconds.
" numObjects ((timestamp() - start) / 1000.0)

	-- debug
 	local testObj = objects[random 2 objects.count]
	local testNode = ot.findPosition ot.root testObj.position
	global testArr = #()
	format "

Looking up and selecting %'s neighbours
" testObj.name
	ot.getNodesFromBranches testNode.parent testArr
	select testArr
	--/debug
	
	gc()
)

Would appreciate some help : )

And for the solution for my first problem, I don’t think that Max “rounds” off numbers differently, but it’s operator precedence and association rules differ from python’s.

I didn’t ran your code yet but it sounds like your problem is that you search for intersections only in the tree node that the object is in and not on it’s tree node neiboghrs. That explains the intersections on borders of tree nodes.

Yup, that is exactly the issue. The problem is how I would go on keeping an updated list of the octnode neighbours while building the tree.

Think I almost got this working now.
Instead of looking up only the object’s position I also look up a scaled version of the object’s bounding vertices. That way I pick up on the objects laying near a non branched neighbour. This is working nicely except for < 1 ‰ of the objects from what I can see after manually checking. I guess that comes from over-scaling the object’s bounding box.

I did some tests scattering instances of a teapot and a box on a geosphere. When comparing with a brute force method, scattering 1000 objects with bounding box collision check, they perform about the same speed wise ~ 3-4 sec on my old machine. But when I tried scattering 10000 objects with 10000 attempts, the octree uses ~40 seconds while the brute force used ~400 seconds. The octree method did on average 17 neighbour lookups. The brute force did off course do N look ups where N is the currently number of placed objects. Both successfully scattered around 8000 objects before they dried out their 10k attempts. That will off course vary on the size of the objects scattered and the size of the object they are scattered on.

Here is the code if anyone wants to try it


(
	-- http://code.activestate.com/recipes/498121-python-octree-implementation/ 

	clearListener()
	delete $*
	
	global debugArr
	
	fn dirLookUp n =
		case n of ( 3: 1; 2: 2; (-2): 3; (-1): 4; 1: 5; 0: 6; (-4): 7; (-3):8 )

	struct octNode
	(
		position, 
		size, 
		isLeafNode,
		data, 
		branches,
		bbox,
		on create do
		(
			this.isLeafNode = true
			this.branches = for i = 1 to 8 collect undefined
			bbox = dummy pos:position boxSize:[size, size, size]
		)
	)

	struct ocTree
	(
		root,
		worldSize,
		position,
		maxObjectsPerCube,
		
		fn addNode pos sze objs = 
			(octNode position:pos size:sze data:objs),
		
		fn findBranch root position =
		(
			local result = 0
			for i = 0 to 2 do
			(
				if root.position[i + 1] <= position[i + 1] then
					result += case i of (0:-2; 1:-1; 2:-1) --result += (-4 / (i + 1) / 2)
				else
					result += case i of (0: 2; 1: 1; 2: 0) --result += (4 / (i + 1) / 2)
			)
			(dirLookUp result)
		),
		
		fn insertNode root size parent objData =
		(
			if root == undefined then
			(
				local pos = parent.position
				local offset = size / 2
				local branch = findBranch parent objData.position
				local newCenter = [0, 0, 0]
				
				if branch == 1 then
					newCenter = [pos.x - offset, pos.y - offset, pos.z - offset]
				else if branch == 2 then
					newCenter = [pos.x - offset, pos.y - offset, pos.z + offset]
				else if branch == 3 then
					newCenter = [pos.x + offset, pos.y - offset, pos.z + offset]
				else if branch == 4 then
					newCenter = [pos.x + offset, pos.y - offset, pos.z - offset]
				else if branch == 5 then
					newCenter = [pos.x - offset, pos.y + offset, pos.z - offset]
				else if branch == 6 then
					newCenter = [pos.x - offset, pos.y + offset, pos.z + offset]
				else if branch == 7 then
					newCenter = [pos.x + offset, pos.y + offset, pos.z + offset]
				else if branch == 8 then
					newCenter = [pos.x + offset, pos.y + offset, pos.z - offset]  
					
									
				return (addNode newCenter size #(objData))
			)
			
			else if root.position != objData.position and not root.isLeafNode then
			(
				local branch = findBranch root objData.position
				local newSize = root.size / 2
				root.branches[branch] = insertNode root.branches[branch] newSize root objData
			)
			
			else if root.isLeafNode then
			(
				if root.data.count < maxObjectsPerCube then
					append root.data objData
				else if root.data.count == maxObjectsPerCube then
				(
					append root.data objData
					local objList = root.data
					root.data = #()
					root.isLeafNode = false
					local newSize = root.size / 2
					
					for ob in objList do
					(
						local branch = findBranch root ob.position
						root.branches[branch] = insertNode root.branches[branch] newSize root ob
					)
				)
			)
			root
		),
		
		fn findPosition root position =
		(
			if root == undefined then 
				undefined
			else if root.isLeafNode then 
				root
			else 
				findPosition root.branches[findBranch root position] position
		), 
		
		fn getNodesFromBranches octNode arr =
		(
			if octNode.isLeafNode then 
				join arr octNode.data
			else
				for i in octNode.branches where i != undefined do
					getNodesFromBranches i arr
		),
		
		fn isInside obj points =
		(
			local oMin = obj.min
			local oMax = obj.max
			local outsidePoints = for i in points where not(\
				i.x < oMax.x and i.x > oMin.x \
				and i.y < oMax.y and i.y > oMin.y \
				and i.z < oMax.z and i.z > oMin.z) collect i
			outsidePoints
		),
		
		fn getBBVerts obj center: = 
		(
			local verts = #(obj.max, obj.min, [obj.max.x, obj.max.y, obj.min.z], \
				[obj.max.x, obj.min.y, obj.max.z], [obj.max.x, obj.min.y, obj.min.z], \
				[obj.min.x, obj.max.y, obj.max.z], [obj.min.x, obj.min.y, obj.max.z], \
				[obj.min.x, obj.max.y, obj.min.z])
			
			if center != unsupplied then
			(
				local scaleVal = distance obj.max obj.min
				verts = for v in verts collect v += scaleVal * normalize (v - center)
			)
			verts
		),
		
		fn objCenter obj =
			obj.min + 0.5 * (obj.max - obj.min),
		
		fn findNeighbours obj debug:false =
		(
			local curBox = findPosition root (objCenter obj)
			local nbNodes = #()
			local neighbours = #()
			
			if curBox != undefined then
			(
				for l in (isInside curBox.bbox (getBBVerts obj center:(objCenter obj) doScale:true)) do
					appendIfUnique nbNodes (findPosition root l)
				
				append nbNodes curBox
				for n in nbNodes where n != undefined do
					getNodesFromBranches n neighbours
				
				if debug then
				(
					print obj.name
					print nbNodes.count
					print (isInside curBox.bbox (getBBVerts obj center:(objCenter obj) doScale:true))
					select (for i in nbNodes collect i.bbox)
				)
			)
			neighbours
		),
		
		on create do
		(
			root = addNode position worldSize #()
		)
	)
	
	fn objCenter obj =
		obj.min + 0.5 * (obj.max - obj.min)
	
	fn ocTreeScatter obj numObjs refObjs asInstance tryCountLim:1e4= 
	(
		local	start = timestamp()
		local worldSize = 0.60 * distance obj.max obj.min
		local maxObjectsPerCube = 16
		local tries = 0
		local objsPlaced = 0
		local objsToDelete = #()
		
		local ot = ocTree worldSize:worldSize position:(objCenter obj) maxObjectsPerCube:maxObjectsPerCube
		
		local copyType = if asInstance then instance else copy
		local fVerts; local p1; local newObj; local f; local neighbours;
		local col = false
		local gVert = polyop.getVert
		local gFaceNormal = polyop.getFaceNormal
		local getVertsUsingFace = polyop.getVertsUsingFace
		local its = 0
		local avgLookups = 0
		if classOf obj != editable_poly then convertTo obj polyMeshObject
		
		while tries < tryCountLim and objsPlaced < numObjs do
		(
			its += 1
			objsPlaced += 1
			newObj = if not col then copyType refObjs[random 1 refObjs.count] else newObj
			f = random 1 obj.numFaces
			fVerts = (getVertsUsingFace obj f) as array
			if fVerts.count > 3 then fVerts = for i = 1 to 3 collect fVerts[random 1 fVerts.count]
			p1 = gVert obj fVerts[1] +  ((random 0.0 1.0) * (gVert obj fVerts[2] - gVert obj fVerts[1]))
			newObj.pos = p1 + ((random 0.0 1.0) * (gVert obj fVerts[3] - p1))
			newObj.dir = gFaceNormal obj f
			
			neighbours = #()
			
			col = false
		
			neighbours = ot.findNeighbours newObj
			avgLookups += neighbours.count

			for n in neighbours where not col and intersects newObj n do
			(
				tries += 1
				objsPlaced -= 1
				col = true
			) 
			
			if not col then 
			(
				centerPivot newObj
				ot.insertNode ot.root worldSize ot.root newObj
			)
		)
		format "Total iterations: %
" its
		format "Number of tries: %
" tries
		format "Average lookups: %
" (avgLookups / its as float)
		format "Scattering % objects took % seconds.
" objsPlaced ((timestamp() - start) / 1000.0)
	)
	
	local testScatObj = geoSphere radius:40 segs:10
	local refObjs = #(teapot radius:0.3, box height:0.3 width:0.3 length:0.3)
	local numObjects = 10000
	
	with redraw off 
	(
		with undo off 
		( 
			ocTreeScatter testScatObj numObjects refObjs true tryCountLim:1e4
		--	delete $Dummy*
		)
	)
	
/*
	-- debug
 	local testObj = objects[random 2 objects.count]
--	local testNode = ot.findPosition ot.root testObj.position
	global testArr = #()
	format "

Looking up and selecting %'s neighbours
" testObj.name
	testArr = ot.findNeighbours testObj --ot.getNodesFromBranches testNode.parent testArr
	select testArr
	--/debug
*/	
	gc()
)

Nice script. interesting to see an convertion of this phy script.
This can be used for many many purposes.Thank you for sharing this

i see some strange numbers…

let’s say we have 10000 objects. we need to find all objects intersected with another. going simplest way we can check intersection of every object with any other. it’s easy to calculate that it needs 50,000,000 iterations all together.
there is a built-in mxs function intersects. using this function i found all intersections by bounding box by ~11 secs and 0 bytes of memory use.
using simple sorting i can improve the speed at least 2-3 times. so the realistic number has to be ~3-4 secs for 10,000 objects.

1 Reply
(@denist)
Joined: 11 months ago

Posts: 0

my bad… i was wrong in my calculations. one “0” was missed… sorry.
simply using the intersects method takes ~100 secs to get all intersections for 10,000 nodes. so we have to do something more clever.

Interesting, I’m using the intersects method as well, but I think that the reason for the time difference is that I try to rescatter an object if there is an intersection up to a given boundary. Did you scatter the objects on another object? The actual scatter code I use is fairly lightweight, so I don’t think it’s that. Can you run the code I supplied in my previous post and post the time?

On my old pc this snippet took ~120 sec.
Is this how you brute forced it?


objs = #()
objs[1e4] = undefined
start = timestamp()
for i = 1 to 1e4 do
(
	obj =  box pos:(random [-500, -500, -500] [500, 500, 500])
	objs[i] = obj
	for j = 1 to i - 1 do 
		intersects obj objs[j]
)
format "Time: %
" ((timestamp() - start) / 1e3)

well… after some code optimization I got a result:


 (
 	delete objects
 	seed 1
 	nodes = for k=1 to 10000 collect 
 	(
 		n = box pos:(1000*(random [-1,-1,-1] [1,1,1])) length:(random 10 50) width:(random 10 50) height:(random 10 50)
 	)
 	completeRedraw()
 	gc()
 )
 

for this set of objects (10,000) my algorithm (not Octree algorithm) found 1,645 intersected nodes for ~1000 ms. (time:1021 memory:6096272L)
of course I don’t count the time of nodes creation…

I will show my algorithm soon, but before that I want to give you some time to beat me…

Page 1 / 3