20

I need to check if two meshes intersect or not. At the moment I have some python code to find intersections of world axes aligned bounding boxes, next step would be to implement check for mesh intersection for objects with intersecting bounding boxes.

In Blender Python API I found only very low level functions (like check if ray intersects a triangle), unless I'm missing something using them for my purpose would be slow and inefficient. Is there some kind of python library I could use?

By the way, I think all I need is already implemented in rigid body physics: it has a noticeable reaction when objects are intersecting, and seems to be well optimized. But I think I cannot access its internal data like if there is mesh intersection, so I have to continue with my own implementation. Please correct me if I'm wrong.

What I'm actually trying to achieve is to be able to do something with intersecting objects, for example search for intersecting objects in selection, and then delete or select them, but it all boils down to checking if two meshes intersect or not.

Lissanro Rayen
  • 639
  • 1
  • 6
  • 10

2 Answers2

24

There's unfortunately no way to use the Bullet physics engine to test for collisions. So you really have to ray_cast() to find intersections.

The 3D Printing Toolbox addon comes with a self-intersection checker. I quickly adapted the code to test for intersections between two objects (updated for Blender 2.80+):

import bpy
import bmesh

def bmesh_copy_from_object(obj, transform=True, triangulate=True, apply_modifiers=False): """ Returns a transformed, triangulated copy of the mesh """

assert(obj.type == 'MESH')

if apply_modifiers and obj.modifiers:
    me = obj.to_mesh(bpy.context.scene, True, 'PREVIEW', calc_tessface=False)
    bm = bmesh.new()
    bm.from_mesh(me)
    bpy.data.meshes.remove(me)
else:
    me = obj.data
    if obj.mode == 'EDIT':
        bm_orig = bmesh.from_edit_mesh(me)
        bm = bm_orig.copy()
    else:
        bm = bmesh.new()
        bm.from_mesh(me)

# Remove custom data layers to save memory
for elem in (bm.faces, bm.edges, bm.verts, bm.loops):
    for layers_name in dir(elem.layers):
        if not layers_name.startswith("_"):
            layers = getattr(elem.layers, layers_name)
            for layer_name, layer in layers.items():
                layers.remove(layer)

if transform:
    bm.transform(obj.matrix_world)

if triangulate:
    bmesh.ops.triangulate(bm, faces=bm.faces)

return bm

def bmesh_check_intersect_objects(obj, obj2): """ Check if any faces intersect with the other object

returns a boolean
"""
assert(obj != obj2)

# Triangulate
bm = bmesh_copy_from_object(obj, transform=True, triangulate=True)
bm2 = bmesh_copy_from_object(obj2, transform=True, triangulate=True)

# If bm has more edges, use bm2 instead for looping over its edges
# (so we cast less rays from the simpler object to the more complex object)
if len(bm.edges) > len(bm2.edges):
    bm2, bm = bm, bm2

# Create a real mesh (lame!)
scene = bpy.context.scene
me_tmp = bpy.data.meshes.new(name="~temp~")
bm2.to_mesh(me_tmp)
bm2.free()
obj_tmp = bpy.data.objects.new(name=me_tmp.name, object_data=me_tmp)
# scene.objects.link(obj_tmp)
bpy.context.collection.objects.link(obj_tmp)
ray_cast = obj_tmp.ray_cast

intersect = False

EPS_NORMAL = 0.000001
EPS_CENTER = 0.01  # should always be bigger

#for ed in me_tmp.edges:
for ed in bm.edges:
    v1, v2 = ed.verts

    # setup the edge with an offset
    co_1 = v1.co.copy()
    co_2 = v2.co.copy()
    co_mid = (co_1 + co_2) * 0.5
    no_mid = (v1.normal + v2.normal).normalized() * EPS_NORMAL
    co_1 = co_1.lerp(co_mid, EPS_CENTER) + no_mid
    co_2 = co_2.lerp(co_mid, EPS_CENTER) + no_mid

    success, co, no, index = ray_cast(co_1, (co_2 - co_1).normalized(), distance = ed.calc_length())
    if index != -1:
        intersect = True
        break

# scene.objects.unlink(obj_tmp)
bpy.context.collection.objects.unlink(obj_tmp)
bpy.data.objects.remove(obj_tmp)
bpy.data.meshes.remove(me_tmp)


return intersect

obj = bpy.context.object

obj2 = (ob for ob in bpy.context.selected_objects if ob != obj).next()

intersect = bmesh_check_intersect_objects(obj, obj2)

print("There are%s intersections." % ("" if intersect else " NO"))

Select two mesh objects and run the script. It will print the result to the system console.

Note that there's a special case: If, for instance, a cube is fully inside another cube, it will report "NO intersections" - since there are none. But if you think of the objects as solid bodies, they do collide.

Anson Savage
  • 3,392
  • 3
  • 31
  • 67
CodeManX
  • 29,298
  • 3
  • 89
  • 128
  • Thanks, your answer is very helpful (I will remember to upvote when I have enough rep). There is unexpected special case though: if one object intersects with triangle(s) in another object but not with any of its edges, script may not find intersection (depends on which object will be chosen as "simple"). But it was very easy to fix, if casting rays from the simpler to the more complex object finds no intersection, try the other way around. – Lissanro Rayen May 03 '14 at 02:59
  • Good find! I did not expect such issues since both meshes are triangulated. Maybe there's another way to handle this corner case without casting a lot of rays again? I admit, I don't fully understand what is done in the calculation for the ray start and end points, maybe something can be improved here? – CodeManX May 03 '14 at 11:37
  • This corner case is common in scenes with object(s) intersecting with large plane or low-poly leaves intersecting with each other, etc. It is possible to cast less rays by using "continue" if current edge is not touching volume of intersection of bounding boxes. However, most of the time is spent in bmesh_copy_from_object(), even if we call it no more than once per each object, it still takes many times more than anything else. Removing bmesh.ops.triangulate() line gives very noticeable improvement. What was the purpose of triangulation? – Lissanro Rayen May 04 '14 at 10:35
  • Quads and Ngons can be non-planar, thus a triangulation is needed for the intersection test. Otherwise, there would be false-positives and false-negatives. For planar polygons, it should be possible to not triangulate them (not sure if this operator is any helpful: https://developer.blender.org/rB64124ba904dc) – CodeManX May 12 '15 at 10:37
  • 1
    Hi, anything new/up-to-date with this ? This script gives me an error at the line co, no, index = ray_cast(co_1, co_2) – gordie Apr 11 '18 at 07:46
  • 3
    The signature changed: bpy.types.Object.ray_cast (origin, direction, distance), was (start, end) – CodeManX Apr 11 '18 at 09:49
  • To make the update clearer to newer folks, ray_cast now returns a tuple of four values instead of three. If you add an extra variable to catch the first value, this script works just fine. I.e., change co, no, index = ray_cast(co_1, co_2) to something like extra_var, co, no, index = ray_cast(co_1, co_2). – Justin Helps Jul 13 '18 at 04:45
  • success, co, no, index = ray_cast(co_1, co_2 - co_1, ed.calc_length()) and on the next line if success: should do the trick. The second argument needs to be a vector in standard position, not an arbitrary point in object or world space anymore. The direction is normalized in native code for the ray cast, although the pre-test in isect_ray_aabb_v3_simple() doesn't use a unit vector... Not sure if it simply doesn't matter or if this could lead to false results. Possibly safer: success, co, no, index = ray_cast(co_1, (co_2 - co_1).normalized(), ed.calc_length()). – CodeManX Jul 13 '18 at 12:54
  • 1
    @CodeManX Is that possible for you to update the script for 2.8 or 2.9. I made adjustments in multiple places but still getting several errors. – Sourav Oct 07 '20 at 21:37
  • @Sourav I just submitted an updated :) – Anson Savage Mar 04 '22 at 22:50
1

Here's an extremely fast method using BVHTree.

from mathutils.bvhtree import BVHTree
import bpy
import bmesh

def create_bvh_tree_from_object(obj): bm = bmesh.new() bm.from_mesh(obj.data) bm.transform(obj.matrix_world) bvh = BVHTree.FromBMesh(bm) bm.free() return bvh

def check_bvh_intersection(obj_1, obj_2): bvh1 = create_bvh_tree_from_object(obj_1) bvh2 = create_bvh_tree_from_object(obj_2) return bvh1.overlap(bvh2)

Test on file objects

for obj_1 in bpy.data.objects: if obj_1.type != "MESH": continue for obj_2 in bpy.data.objects: if obj_2.type != "MESH" or obj_2 == obj_1: continue intersection = check_bvh_intersection(obj_1, obj_2) print(f"{obj_1.name}{'' if intersection else ' DOES NOT'} intersect {obj_2.name}")

Gorgious
  • 30,723
  • 2
  • 44
  • 101