Notifications
Clear all

[Closed] Find self intersections

Hi,

I’ve made a little script which enables you to find self intersections in meshes. It uses a hacky method by intersecting the mesh with a slightly offset copy of itself. For my purposes this works fine though.
Running this script will make a torusknot and display the self intersections.
The part of the method where the most memory is going is where I’m calculating the average normal for each vertex. I’m using this to offset the copy of the mesh. If somebody has some suggestions to make this more efficient I’d be happy to hear.
Inspired by methods by Matan Halberstadt and Klunk.

function fn_getFaceSelfIntersection theMesh gridsize = if isKindOf theMesh editable_mesh then
 (	
 	/*<FUNCTION>
 	Description:
 		Detects self-intersecting faces on a mesh
 		Calculating self intersection is tricky. It doesn't work if you intersect rays from the edges with its own faces.
 		Every ray is a hit in that case
 		A hack is to offset a copy of the mesh ever so slightly and intersect the original with the copy. Inspired by the
 		3d printing toolbox from blender
 		Other references:
 			 http://forums.cgsociety.org/showthread.php?f=98&t=1090782&highlight=intersect 
 			 http://forums.cgsociety.org/showthread.php?f=98&t=838041&highlight=intersection 	
 	Arguments:
 		<mesh object> theMesh: the object we're checking self-intersections on
 		<integer> gridSize: the size of the grid for the intersection algorithm
 	Return:
 		intersecting faces are painted in the viewport and selected in the mesh
 	</FUNCTION>*/
 	
 	--set up the intersect grid
 	start = timestamp()
 	mem = heapfree
 	local cgrid = RayMeshGridIntersect()
 	cgrid.initialize gridsize
 	cgrid.addNode theMesh
 	cgrid.buildGrid()
 	local intersectsegfn = cgrid.intersectSegment
 	format "setup grid:% ms mem:%
" (timestamp() - start) (mem-heapfree)
 	
 	--create a copy of the mesh and offset it ever so slightly
 	start = timestamp()
 	mem = heapfree	
 	local theOffsetMultiplier = 0.001 --the offset for the copied mesh
 	local theCopiedMesh = copy theMesh
 	local getFacesUsingVert = meshop.getFacesUsingVert --storing this method in a local saves some memory
 	for i = 1 to theCopiedMesh.numVerts do
 	(
 		--getting the averaged face-normals for every vertex is the most memory intensive operation of this method
 		--pre-storing all facenormals in an array uses more memory and doesn't influence the speed, so I'm not doing that
 		--get the faces for this vertex
 		local arrFace = getFacesUsingVert theCopiedMesh i
 		
 		--adding all adjacent face normals gives us the vertex normal. We're disregarding any smoothing
 		--we're looking for shell-like behaviour here
 		local theVertNormal = [0,0,0]
 		for f in arrFace do theVertNormal += getFaceNormal theCopiedMesh f 
 		theVertNormal = normalize theVertNormal
 		
 		--offset the vertex along its normal using a tiny multiplier
 		setVert theCopiedMesh i ((getVert theCopiedMesh i) + theOffsetMultiplier*theVertNormal)
 	)
 	format "offset mesh:% ms mem:%
" (timestamp() - start) (mem-heapfree)
 	
 	--perform the intersection between the original mesh and the copy
 	start = timestamp()
 	mem = heapfree	
 	local theMeshfaces = #{}
 	local numfaces = theCopiedMesh.numfaces
 	for i = 1 to numfaces do
 	(
 		local fverts = getface theCopiedMesh i
 		local a = getVert theCopiedMesh fverts.x
 		local b = getVert theCopiedMesh fverts.y
 		local c = getVert theCopiedMesh fverts.z
 		if intersectsegfn a b false > 0 then theMeshfaces[i] = true
 		if intersectsegfn b c false > 0 then theMeshfaces[i] = true
 		if intersectsegfn c a false > 0 then theMeshfaces[i] = true
 	)
 	delete theCopiedMesh --delete the copy. We don't need it
 	
 	--color the object to indicate the self-intersecting faces
 	meshop.setFaceColor theMesh 0 theMeshfaces (color 255 0 0)	
 	theMesh.vertexColorType = #color
 	theMesh.shadeVertexColors = 1
 	theMesh.showVertexColors = true	
 	theMesh.selectedfaces = theMeshfaces
 	
 	cgrid.free()
 	format "intersect mesh:% ms mem:%
" (timestamp() - start) (mem-heapfree)
 )else format "% is not an editable mesh, please convert it first.
" theMesh.name
 
 
 (
 	local theObject = Torus_Knot Base_Curve:0 Segments:120 sides:36 radius:50 radius2:20 p:1 q:2 Eccentricity:1 Twist:0 Lumps:2 Lump_Height:1.8 Lump_Offset:123
 	converttomesh theObject
 	local start = timestamp()
 	local mem = heapfree
 	fn_getFaceSelfIntersection theObject 25
 	format "total:% ms mem:%
" (timestamp() - start) (mem-heapfree)
 )
27 Replies

Hi Klaas,

    At first I thought the memory increase was in:
local theVertNormal = [0,0,0]
        for f in arrFace do theVertNormal += getFaceNormal theCopiedMesh f 
        theVertNormal = normalize theVertNormal

So I changed it to:

for f in arrFace do (
        	faceVerts = getFace theCopiedMesh f
        	v1 = getVert theCopiedMesh faceVerts.x
        	v2 = getVert theCopiedMesh faceVerts.y
        	v3 = getVert theCopiedMesh faceVerts.z
        	theVertNormal += (cross (v1 - v2) (v2 - v3))
        )

And the memory was the same, and it is a bit slower. So from here, I see the problem is in the point3 addition, no matter what you add, it increases the memory. Why?

    In the Torus example you are using, the following seems to return the same result, but I am not sure it will work with any geometry:
for i = 1 to theCopiedMesh.numVerts do
   (
   	setVert theCopiedMesh i ((getVert theCopiedMesh i) + theOffsetMultiplier*(getNormal theCopiedMesh i))
   )

Uses 7 times less memory and runs a bit faster.

1 Reply
(@denist)
Joined: 1 year ago

Posts: 0

unfortunately there is no built-in mxs function that returns average (or render) normal of a vertex. getNormal returns the normal of the first face that uses this vertex. so it can’t be used in general case to “push” the mesh.

with the “+=” operation the max creates every time new instance of point3 value and doesn’t free memory for old one. it’s a known source of the memory leak. there is a trick how avoid this leaking:


 delete objects
 (
 	b = box()
 	converttomesh b
 	gc()
 
 	m1 = heapfree
 	normal = [0,0,0]
 	for k=1 to 10000 do 
 	(
 		n = getfacenormal b 1 
 		-- normal += n -- LEAKS!
 
 	    normal.x += n.x 
 		normal.y += n.y 
 		normal.z += n.z 
 	)
 	format "memory:%
" (m1 - heapfree)
 )
 
3 Replies
(@polytools3d)
Joined: 1 year ago

Posts: 0

Than you Denis, good to know. By the way, this also leaks memory

(
 	m1 = heapfree
 	n = 0
 	for k=1 to 10000 do n += 1
 	format "memory:%
" (m1 - heapfree)
 )
(@denist)
Joined: 1 year ago

Posts: 0

true. because max creates an instance of integer ;)… but in case a.x += x, the max uses a chuck of pre-allocated memory. sometimes i use this trick to minimize “integer-leak” as well

(@denist)
Joined: 1 year ago

Posts: 0

sometimes a little leak causes big problem… for example in scripted controllers.

Here is a different approach that seems to return the same result for your Torus example. I haven’t tested it with other geometries so it might not work in all cases.

start = timestamp()
       mem = heapfree	
       local theOffsetMultiplier = 0.001 --the offset for the copied mesh
       local theCopiedMesh = copy theMesh
       setFaceSelection theCopiedMesh #{1..theCopiedMesh.numfaces}
       extrudeFace theCopiedMesh #{1..theCopiedMesh.numfaces} theOffsetMultiplier 0 dir:#independent
       meshop.deleteFaces theCopiedMesh (#{1..theCopiedMesh.numfaces} - (getFaceSelection theCopiedMesh))
       format "offset mesh:% ms mem:%
" (timestamp() - start) (mem-heapfree)
For the "Offset Mesh" part, it is 137 times faster and uses 3300 times less memory for the Torus example.

Original:
setup grid:7 ms mem:624L
offset mesh:551 ms mem:1935624L
intersect mesh:993 ms mem:496L
total:1551 ms mem:1936864L

New:
setup grid:7 ms mem:624L
offset mesh:4 ms mem:584L
intersect mesh:984 ms mem:496L
total:998 ms mem:1824L

  Again, I do not know if it will work in all cases.

actually it’s not true. but for sure it returns wrong value. to get correct direction for pushing vertex we have to calculate average normal using all vertex faces.

1 Reply
(@polytools3d)
Joined: 1 year ago

Posts: 0
 From the Help:

“getNormal <mesh> <vert_index_integer>
Returns the normal at the indexed vertex’s position as a point3. The normal is based on the faces that use the vertex and the smoothing groups assigned to those faces.”

the question is –
What is self-intersection? if the self-intersection is the fact that any edge of the mesh intersects with any face, probably it will be easier and faster just cast a ray from every vertex in direction of surrounded edges and check intersection.

1 Reply
(@grabjacket)
Joined: 1 year ago

Posts: 0
 I've tried this, and the result is that it intersects with every face. That's why I'm using the slightly offset mesh to test against.

with the “+=” operation the max creates every time new instance of point3 value and doesn’t free memory for old one. it’s a known source of the memory leak. there is a trick how avoid this leaking:
This tip made this part of the code about 7 times more memory efficient!

start = timestamp()
mem = heapfree
local theOffsetMultiplier = 0.001 –the offset for the copied mesh
local theCopiedMesh = copy theMesh
extrudeFace theCopiedMesh #{1…theCopiedMesh.numfaces} theOffsetMultiplier 0 dir:#independent
format “offset mesh:% ms mem:%
” (timestamp() – start) (mem-heapfree)
This is a good suggestion as well. I was so proud of myself finally getting into the normals and figuring out the solution. But this method seems to do the same, and a lot quicker.
Thanks for the help guys! Here’s the updated code

function fn_getFaceSelfIntersection theMesh gridsize = if isKindOf theMesh editable_mesh then
    (	
    	/*<FUNCTION>
    	Description:
    		Detects self-intersecting faces on a mesh
    		Calculating self intersection is tricky. It doesn't work if you intersect rays from the edges with its own faces.
    		Every ray is a hit in that case
    		A hack is to offset a copy of the mesh ever so slightly and intersect the original with the copy. Inspired by the
    		3d printing toolbox from blender
    		Other references:
    			 http://forums.cgsociety.org/showthread.php?f=98&t=1090782&highlight=intersect 
    			 http://forums.cgsociety.org/showthread.php?f=98&t=838041&highlight=intersection 	
    	Arguments:
    		<mesh object> theMesh: the object we're checking self-intersections on
    		<integer> gridSize: the size of the grid for the intersection algorithm
    	Return:
    		intersecting faces are painted in the viewport and selected in the mesh
    	</FUNCTION>*/
    	
    	--set up the intersect grid
    	start = timestamp()
    	mem = heapfree
    	local cgrid = RayMeshGridIntersect()
    	cgrid.initialize gridsize
    	cgrid.addNode theMesh
    	cgrid.buildGrid()
    	local intersectsegfn = cgrid.intersectSegment
    	format "setup grid:% ms mem:%
" (timestamp() - start) (mem-heapfree)
     
    	--create a copy of the mesh and offset it ever so slightly
    	--Suggestion by polytools3d  http://forums.cgsociety.org/showpost.php?p=7622514&postcount=4 
    	--works very fast
    	start = timestamp()
    	mem = heapfree	
    	local theOffsetMultiplier = 0.001 --the offset for the copied mesh
    	local theCopiedMesh = copy theMesh
    	setFaceSelection theCopiedMesh #{1..theCopiedMesh.numfaces}
	extrudeFace theCopiedMesh #{1..theCopiedMesh.numfaces} theOffsetMultiplier 100
	--if there were open edges, this line deletes the extra created faces
	meshop.deleteFaces theCopiedMesh (#{1..theCopiedMesh.numfaces} - (getFaceSelection theCopiedMesh)) 

    	format "offset mesh:% ms mem:%
" (timestamp() - start) (mem-heapfree)
    	
    	--perform the intersection between the original mesh and the copy
    	start = timestamp()
    	mem = heapfree	
    	local theMeshfaces = #{}
    	local numfaces = theCopiedMesh.numfaces
    	for i = 1 to numfaces do
    	(
    		local fverts = getface theCopiedMesh i
    		local a = getVert theCopiedMesh fverts.x
    		local b = getVert theCopiedMesh fverts.y
    		local c = getVert theCopiedMesh fverts.z
    		if intersectsegfn a b true > 0 then theMeshfaces[i] = true
    		if intersectsegfn b c true > 0 then theMeshfaces[i] = true
    		if intersectsegfn c a true > 0 then theMeshfaces[i] = true
    	)
     
    	delete theCopiedMesh --delete the copy. We don't need it
    	
    	--color the object to indicate the self-intersecting faces
    	meshop.setFaceColor theMesh 0 theMeshfaces (color 255 0 0)	
    	theMesh.vertexColorType = #color
    	theMesh.shadeVertexColors = 1
    	theMesh.showVertexColors = true	
    	theMesh.selectedfaces = theMeshfaces
    	
    	cgrid.free()
    	format "intersect mesh:% ms mem:%
" (timestamp() - start) (mem-heapfree)
    )else format "% is not an editable mesh, please convert it first.
" theMesh.name
    
    
    (
    	delete objects
    	local theObject = Torus_Knot Base_Curve:0 Segments:120 sides:36 radius:50 radius2:20 p:1 q:2 Eccentricity:1 Twist:0 Lumps:2 Lump_Height:1.8 Lump_Offset:123
    -- 	local theObject = box()
    	converttomesh theObject
    	local start = timestamp()
    	local mem = heapfree
    	fn_getFaceSelfIntersection theObject 25
    	format "total:% ms mem:%
" (timestamp() - start) (mem-heapfree)
    )

try to autosmooth(>90 deg) a cube. and check the normal. that will not be an average value.

1 Reply
(@polytools3d)
Joined: 1 year ago

Posts: 0

Well, I guess that’s how Max is… You love it, then you hate it, then you love it again, and so on…

I am glad the extrudeFace worked for your needs. However, I would not rely on it as a generic approach for every case.

If the mesh has open edges, then extrudeFaces will add faces for every open edge, so you’ll need either to delete the remaining faces or to change to avoid an error:

local numfaces = theCopiedMesh.numfaces
to:
local numfaces = theMesh.numfaces

Also, intersectSegment is checking twice the “same” edge for every not open edge, so there might be some way to improve it.

2 Replies
(@grabjacket)
Joined: 1 year ago

Posts: 0

True, I’ve edited my previous post. About intersecting twice for most edges. I’m not sure about a solution for this. Any ideas?

(@polytools3d)
Joined: 1 year ago

Posts: 0

Thanks to this tip from DenisT “vtIndex[1] < vtIndex[2]” (from another post) we could:

--perform the intersection between the original mesh and the copy
 start = timestamp()
 mem = heapfree	
 local theMeshfaces = #{}
 local numfaces = theCopiedMesh.numfaces
 
 local cGetEdgesUsingVert = meshop.getEdgesUsingVert
 local cGetFacesUsingEdge = meshop.getFacesUsingEdge
 local meshOpenEdges = meshop.getOpenEdges theCopiedMesh
 
 local meshFaces = #{1..numfaces}
 for i in meshFaces do
 (
 	local fverts = getface theCopiedMesh i
 	local a = getVert theCopiedMesh fverts.x
 	local b = getVert theCopiedMesh fverts.y
 	local c = getVert theCopiedMesh fverts.z
 	
 	if fverts.x < fverts.y or meshOpenEdges[i*3-2] do (
 		if intersectsegfn a b true > 0 do (
 			local edgesA = cGetEdgesUsingVert theCopiedMesh fverts.x
 			local edgesB = cGetEdgesUsingVert theCopiedMesh fverts.y
 			local faces = cGetFacesUsingEdge theCopiedMesh (edgesA*edgesB)
 			join theMeshfaces faces
 			for f in faces do deleteItem meshFaces f
 		)
 	)
 	if fverts.y < fverts.z or meshOpenEdges[i*3-1] do (
 		if intersectsegfn b c true > 0 do (
 			local edgesA = cGetEdgesUsingVert theCopiedMesh fverts.y
 			local edgesB = cGetEdgesUsingVert theCopiedMesh fverts.z
 			local faces = cGetFacesUsingEdge theCopiedMesh (edgesA*edgesB)
 			join theMeshfaces faces
 			for f in faces do deleteItem meshFaces f
 		)
 	)
 	if fverts.z < fverts.x or meshOpenEdges[i*3] do (
 		if intersectsegfn c a true > 0 do (
 			local edgesA = cGetEdgesUsingVert theCopiedMesh fverts.z
 			local edgesB = cGetEdgesUsingVert theCopiedMesh fverts.x
 			local faces = cGetFacesUsingEdge theCopiedMesh (edgesA*edgesB)
 			join theMeshfaces faces
 			for f in faces do deleteItem meshFaces f
 		)
 	)
 )		
  delete theCopiedMesh --delete the copy. We don't need it

This runs about 1.5 times faster but uses more memory.
Although it works with the Torus, I have tested it with a default Teapot, and I get different results from your original code and this, so I am not sure which one returns the correct faces. This needs a lot of manual verification to be sure it will work in all cases.
I still have that feeling… It could be at least 2x faster. Perhaps with a different approach?

Oh Boy, I think this is a bit out of my league. I need some more practice to get to this level. I don’t really see what’s going on with this code. Could you maybe add some comments to explain it a bit?

 	if fverts.x < fverts.y or meshOpenEdges[i*3-2] do (
 		 if intersectsegfn a b true > 0 do (
 			 local edgesA = cGetEdgesUsingVert theCopiedMesh fverts.x
 			 local edgesB = cGetEdgesUsingVert theCopiedMesh fverts.y
 			 local faces = cGetFacesUsingEdge theCopiedMesh (edgesA*edgesB)
 			 join theMeshfaces faces
 			 for f in faces do deleteItem meshFaces f
 		 )
 	 )
1 Reply
(@polytools3d)
Joined: 1 year ago

Posts: 0
 For every shared edge, the order of its vertices is reversed, so if you have a face with vertices [1,2,3], then an adjacent face could have vertices [2,1,8]. So we have edge (1,2), (2,3) and (3,1) for face A and (2,1), (1,8) and (8,2) for face B.

If we already checked the edge with vertices (1,2), there is no need to check vertices (2,1), as they are the same edge (actually they are different but share the same vertices). This was supposed to be obvious, but I didn’t realize it until Denis brought it to the table in another Thread. This is what “fverts.x&lt;fverts.y” does. This is not the case for the open edges, so we need to get them first and treat them separately.
 
 Given a triangle face N, its edges are N*3-2, N*3-1, N*3. So for face 1 we have edge 1, edge 2, edge 3, for face 20 we have edge 58, edge 59, and edge 60. This is what [i*3-2] does. Usually, but not always, the third shared edge is a hidden edge.
 
 Once the check passed, we get the edges for each of the 2 vertices, and the ones they shared are the edges that use the same vertices (edgesA*edgesB).
 
 Now we have 2 edges that belong to 2 different faces (A, B) but share their vertices so if A is intersecting another face C, so must be B.
if fverts.x < fverts.y or meshOpenEdges[i*3-2] do (
     	if intersectsegfn a b true > 0 do (
     		local edgesA = cGetEdgesUsingVert theCopiedMesh fverts.x
     		local edgesB = cGetEdgesUsingVert theCopiedMesh fverts.y
     		local faces = cGetFacesUsingEdge theCopiedMesh (edgesA*edgesB)
     		join theMeshfaces faces
     		for f in faces do deleteItem meshFaces f
     	)
     )

Here is a graphic that may be useful for others. If you find any errors please let me know.

Page 1 / 2