3
$\begingroup$

Is there a way to use the terminal to check if a mesh is water tight? I want to do something like:

if my_mesh_obj.water_tight == True:
    print('It is water tight')
else:
    print('It is not water tight')
$\endgroup$
  • 1
    $\begingroup$ There is a mesh select non manifold operator. Selects al the plane, none of the cube. Assuming manifold and watertight are same, could select none, run the op, if any selection then it's not "watertight". $\endgroup$ – batFINGER Dec 5 '19 at 3:29
5
$\begingroup$

In order to check if a mesh is watertight, we can test that the mesh is 2-manifold and no boundary edges exist [1].

The following definitions are loosely based on the quoted sources, some terminology and explanations were adjusted. This may not be up to standard for a scientific publication.

Definition: 2-manifold [1] [2]:

  1. Each edge is incident to one or two faces.
  2. For each vertex, if one where to remove that vertex, the remaining vertices from the fan would remain connected. A fan is the set of incident faces to the vertex.
  3. No self-intersections occur.

Definition: Watertight [1]:

  • A mesh that is 2-manifold and has no boundary edges. Therefore each edge has two incident faces.

In layman terms, there shouldn't be any:

  • internal faces
  • two or more triangle fans connected only through a single vertex
  • disconnected islands
  • holes in the mesh / areas with no thickness

bpy.ops.mesh.select_non_manifold already performs these tests as batFINGER mentioned, except for the self-intersection check. This requires the use of a tree structure to efficiently determine intersections with itself, for instance a k-d tree, AABB tree, BVH tree or octree.

The script below selects only vertices that are non-manifold through bpy.ops.mesh.select_non_manifold(extend=False). The extend=False argument is important since the current vertex selection should be ignored. The is_watertight function then iterates over the vertices and checks if any vertex is selected. Once it finds a selected vertex, it's clear that the mesh is non-manifold and therefore not watertight.

The self-intersection test is performed using a BVH tree. In case you don't need the test because you can guarantee that the mesh doesn't have self-intersections, you can disable it by setting the check_self_intersection argument to False.

Please be aware that the BVH overlap computation in mathutils doesn't allow to exclude the comparison between the exact same face when comparing the tree with itself. Therefore a small but non-zero epsilon is used during the intersection calculation to avoid false positives, where each face would be considered to be intersecting itself. However this non-zero epsilon also allows slight intersections below the threshold to pass as watertight. Therefore a proper implementation would exclude the comparison of faces with themselves. Given that the example is supposed to be self-contained, only use modules readily available in Blender and be reasonably efficient, I've used the mathutils BVH overlap function despite its shortcomings.

import bpy
import bmesh
import mathutils


def is_watertight(object: bpy.types.Object, check_self_intersection=True) -> bool:
    """
    Checks whether the given object is watertight or not
    :param object: Object the inspect
    :return: True if watertight, False otherwise
    """
    old_active_object = bpy.context.view_layer.objects.active
    old_mode = old_active_object.mode

    bpy.context.view_layer.objects.active = object

    bpy.ops.object.mode_set(mode='EDIT')
    bpy.ops.mesh.select_non_manifold(extend=False)
    bm = bmesh.from_edit_mesh(object.data)

    is_watertight = True

    for v in bm.verts:
        if v.select:
            is_watertight = False
            break

    # Only check for self intersection if manifold
    if is_watertight and check_self_intersection:
        bvh_tree = mathutils.bvhtree.BVHTree.FromBMesh(bm, epsilon=0.000001)
        intersections = bvh_tree.overlap(bvh_tree)

        if intersections:
            is_watertight = False

    bpy.context.view_layer.objects.active = old_active_object
    bpy.ops.object.mode_set(mode=old_mode)

    return is_watertight


if __name__ == "__main__":
    object = bpy.context.view_layer.objects.active
    if is_watertight(object):
        print("It's watertight.")
    else:
        print("It's not watertight.")

References:

[1] David Stutz, A Formal Definition of Watertight Meshes, 2018. [Online]. Available: https://davidstutz.de/a-formal-definition-of-watertight-meshes/. [Accessed: 2019-12-05]

[2] C.-K. Shene, Mesh Basics, 2010. [Online]. Available: https://pages.mtu.edu/~shene/COURSES/cs3621/SLIDES/Mesh.pdf. [Accessed: 2019-12-05]

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.