[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)
)
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.
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)
)
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)
)
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
Here is a different approach that seems to return the same result for your Torus example. I havent 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.
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.
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.
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 youll 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.
True, I’ve edited my previous post. About intersecting twice for most edges. I’m not sure about a solution for this. Any ideas?
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
)
)
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 didnt realize it until Denis brought it to the table in another Thread. This is what fverts.x<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.