Note: This is an archived version of the Blender Developer Wiki (archived 2024). The current developer documentation is available on developer.blender.org/docs.

User:Howardt/Boolean

Boolean

Note: These notes are out of date. They were for an earlier version of New Boolean. The current design follows very closely the paper Mesh Arrangements for Solid Geometry, by Zhou, Grinspun, Zorin, and Jacobson. Maybe I'll update the notes on this page to match that, at some point.

These are developer notes for a new implementation of Boolean (and Intersect) to replace the current BMesh implementation.

The goals of this reimplementation are:

  • Handle coplanar intersections properly.
  • Handle not-completely-sealed solids for boolean operations.
  • Be robust: don't fail due to numerical issues.
  • Be efficient: approximately as fast or faster than the BMesh boolean.

High-Level Overview of Algorithm

The algorithm will either (1) work on one mesh as a whole; or (2) work on two parts of one mesh (perhaps the two parts come from two objects that are joined into one mesh) that together make up the whole mesh. Call the first the self case, and the second the normal or binary case. For the binary case, we will speak of the two submeshes as the sides of the Boolean, side A and side B.

There are two big phases:

  1. Intersection
  2. Boolean or Intersection Cutting

For the Intersect (Knife) Tool, the second phase splits some edges according to a knife mode specified by the user. For the Boolean Tool or Modifier, the second phase eliminates and/or flips the normals of some geometry to make the result look like a Boolean of the implied volumes of the sides.

Intersection

Intersection has to find all places where the vertices, edges, and faces of one side intersect those of the other side. Or, for the self-intersect case, all places where the vertices, edges, and faces intersect other vertices, edges, or faces in the mesh.

The result of the intersection is a new mesh, where some elements get subdivided because of an intersection, and some get merged. For example:

Intersecting Triangles

When a triangle pokes through another one, the intersection is a line segment in the face of the second triangle. In the intersection on the right, both original triangles have been subdivided to include that intersection edge. Notice that because BMesh does not allow holes or faces with repeated vertices, two support edges also need to be added in the blue triangle. We call adding these support edges making a valid BMesh.

Another example:

Intersecting Triangles with overlapping edges

In the above example, the triangles on the two sides are initially not connected, but two vertices are in the same position in space. and the green triangle's edge overlaps part of the blue one's edge. The result after intersection looks the same but the blue triangle now has a split edge (so it is a quad now), and one half of the split edge is shared with the green triangle.

Many other cases can occur, and I won't show pictures of them all, but they include: triangle just touching another triangle at a vertex, on an edge or in a face; and many more possibilities if the two triangles are coplanar. And of course, the faces can be arbitrary ngons, not just triangles.

An important parameter for intersection is epsilon: things that are a distance of epsilon or less apart should be regarded as coincident, and therefore merged. So, for example, two vertices that are within epsilon of each other should be merged into one vertex. An intersection point that is within epsilon of an edge should be snapped onto that edge. Edges that have endpoints within epsilon of each other go away when their vertices merge. And so on. We could do all of this merging on the mesh as a whole before starting, but that means that geometry that the user has deliberately separated (e.g., by an edge split) will be merged, against the user's wishes. For self-intersection, there is no way around this, but for binary intersection, we will try to not merge things that are on the same side unless they take part in an intersection with the other side.

Intersecting Two Faces in Different Planes

A typical case of intersecting two faces in different planes looks like this:

Intersecting Triangles with overlapping edges

Here, face A and face B are on planes which intersect on line L.

The result of intersecting the faces is a set of intervals on line L. An interval could be just a single point, if a vertex from one face happens to land on L. In the above diagram, the intersection result are those parts of the red line that are in both the green and the grey faces.

An algorithm to calculate the set of intervals is:

  • For each adjacent pair of vertices vi and vi+1 of face B, calculate the intersection of that segment with L and record the fraction along L between two reference points where the intersection occurs. If either end of the segment is on L (within epsilon), record that fact.
  • Sort those intersection points in order of the fraction-along.
  • Analyze the intervals between successive intersection points to decide if they are inside or outside face B. Use this analysis to form the set of intervals that are within face B and on line L. This set may include single points.
  • Repeat the above to find intersection intervals that are within face A and on line L.
  • Intersect the two sets of intersection intervals to get the final set: the intersection intervals that are both in faces A and B and on line L.

Parts

The concept of a Part is useful in developing the algorithm for intersection. A Part is a set of faces on the same plane. For example, in this diagram:

Two Parts

the parts are A and B. Part A has two disconnected pieces, one of which consists of many faces. Part B is connected but likewise consists of many faces.

The algorithm we will develop will use Parts as a way to organize sub-computations.

Intersecting Multiple Parts

When there are multiple parts to intersect simultaneously, we need to do multiple face-face intersections and combine the results.

Consider this case, which shows four parts (A, B, C, and D), each a single face:

Four Intersecting Parts

We can intersect each of the part pairs separately. The following shows the separate intersections of {A,B}, {A,C}, {B,C}, {B,D}, and {C,D}. (A and D don't intersect). The intersections are shown with black lines and the new vertices created are labeled with lowercase latters a to j.

Pairwise Intersections

How do we combine this into a new model with all of the intersections? There are several important things to notice:

  • Some intersections cause new vertices in the middle of other faces. For example, vertex h in face B in the {B,D} intersection. Blender doesn't allow edges to end in the middle of faces, so at least one support edge will need to be added from h to one of the corners of face B.
  • Some intersections make new vertices that are (should be) identical to vertices created in another pairwise intersection. For example, vertex b in the {A,B} intersection is the same as vertex e in the {B,C} intersection. We need to recognize this and not create two separate vertices.
  • We need to combine the results of several pairwise intersections together when the involve the same face, because there might be interference between those results. For example, edge ab in the {A,B} intersection and edge ef in the {B,C} intersection both need to be considered together when subdividing face B to show the result of the entire intersection. Another case is: edge ef from the {B,C} intersection and edge ij from the {C,D} intersection need to be considered (and intersected with each other!) while subdividing face C.
  • We have to notice cases where edges are shared or should be shared. For example, edge dc is the intersection of A and C. It is also an existing edge. We need to end up with one edge here, whether or not the initial model had one or two edges there. This may also mean merging the coincident vertices on those faces, if they were not merged already.

We can see that a basic algorithm needed is to take a collection of faces, edges, and vertices, all in one plane, and run a (2D) intersection algorithm on them. It needs to:

  • Intersect edges that cross each other, creating new vertices and subdividing the edges.
  • Insert vertices into faces or on edges (if they are within epsilon of the edge).
  • Merge vertices that are within epsilon of each other.
  • Merge edges that are coincident; if edges are collinear and overlap, then they need to be subdivided.
  • Insert support edges needed to make a valid BMesh.
  • All the while keeping track of the original faces and edges that the new geometry might be subdivisions of (this is so that we can properly propagate properties such as materials, UV coordinates, crease values, etc.)

There is now a robust 2D intersection library routine in the Blender utility library that can do all of this, called BLI_delaunay_2d_cdt_calc. It was specifically coded and added to be used in this Boolean intersection code. It works by making what is called a Constrained Delaunay Triangulation of all of the input vertices and edges (including edges of faces). A Constrained Delaunay Triangulation is a triangulation with nicely-shaped triangles which respects (keeps) the input edges as part of the triangulation. The algorithm used is an O(n log n) Divide-and-Conquer algorithm, and it uses exact predicates for orientation calculations in order to achieve robustness. An output option for the routine will throw away most of the triangulation edges, leaving only enough to make for a valid BMesh.

Meshes and Mesh Changes

Most of the Blender code that does editing operations on Meshes uses the BMesh representation of meshes. It is a set of vertices, edges, faces, and loops (a loop is a vertex+edge that is one side of a face). There are pointers in and among the elements that allow fast access to other elements that they are attached to or adjacent to. However, when Meshes are used in Object mode, or persisted to disk, they use a representation called Mesh which is also a set of vertices, edges, faces, and loops -- but without the fast-access paths to adjacent geometry. One inefficiency right now in Blender is that the Modifier stack operates on Meshes, so if you want to use BMesh in a modifier, then you have to convert into BMesh on entry and back into Mesh on exit.

It occurred to me during the development of the new Boolean algorithm that I didn't really need the adjacency information, or at least, not all of it. So, thinking about the Modifier case, I wondered if one could write the Boolean algorithm in a way that would work either on BMesh or on Mesh. To try this out, I made an abstract mesh interface that I call IMesh, and have programmed the whole algorithm in terms of that. This may turn out to be a failed experiment, as I have currently only implemented the BMesh half of the implementation of IMesh, but we'll see.

The IMesh interface is mostly readonly. Rather than directly edit the mesh as the algorithm proceeds, the algorithm accumulates a delta to the IMesh in a data structure called MeshChange. Then there is one routine which will apply a MeshChange to the mesh that underlies the IMesh. This approach makes it much easier to have a Mesh backing the IMesh, because it is not efficient to edit a Mesh.

There are several other reasons for this accumulate-all-the-changes approach rather than change the mesh on the fly, as the algorithm proceeds:

  1. There is the potential to parallelize the Intersection algorithm: changes can be calculated on separate parts of a mesh in parallel, and then the changes can be merged later. Parallel changes on the fly would both be hard to reason about (in the cases where the changes touch) and would probably need a giant lock that would make it questionable whether the parallelization would save time.
  2. Operations like removing edges or merging vertices are not always possible (or possible without undesired side effects) in BMesh, so the code has to package up a set of changes that are possible sometimes, and this makes the code convoluted and hard to understand.
  3. It just seems harder to understand an algorithm where the underlying faces, edges, and vertices are constantly changing. A previous attempt at a new Boolean code tried working directly on BMesh but ran into a steep this-is-getting-too-complex wall due to such considerations.

Throughout the new Boolean algorithm, vertices, edges, and faces are referred to using integer ids, starting from zero and going sequentially, separately, for each of vertices, edges, and faces. Using integers makes it easy to debug, makes it easier to get consistent output on different runs and different machines (pointers can cause problems, if used for hashing), and also makes it easy to tell which geometric elements are original and which are new.

The MeshChange data structure is mainly four component data structures:

  • MeshAdd add, which has additions to the integer id space for new vertices, new edges, and new faces. In addition to the data needed to specify where those elements are, it also keeps track of how they were derived from original geometric elements (to be used for transferring properties, later).
  • MeshDelete delete, which keeps track of which original geometry elements (vertices, edges, faces) will be deleted. The algorithm doesn't edit existing elements (that is, it doesn't split edges or faces), but rather recreates any changed element from scratch and deletes the originals.
  • IntIntMap vert_merge_map, which says which vertices need to merge into other vertices.
  • IntSet intersection_edges, which records which edges are actually part the result of intersecting the two parts or the self intersection (this is used to select those edges after the whole intersection is done, if this is the intersection tool).

Intersection Algorithm

Now that the pieces have been explained, here is the overall structure of the intersection algorithm:

  1. Find the set(s) of Parts, by finding and grouping things in the same plane. For the self case, there is just one PartSet called all_parts, containing all the parts of the whole module. For the normal (binary) case, there are are two PartSets: a_parts and b_parts, containing the parts of the A side and the B side of the Boolean operation respectively. The routine that finds the PartSets is called find_coplanar_parts.
  2. Intersect a pair of PartSets. For self, the pair is {all_parts, all_parts}; for normal, the pair is {a_parts, b_parts}. The routine that does this is called intersect_partset_pair. It produces as output a specification of how the mesh should change to make the intersection, in a MeshChange structure, which was described previously.

The intersect_partset_pair(a_partset, b_partset, meshchange) routine works as follows:

For each possibly-intersecting pair a, b from the two PartSets:
Calculate the actual or potential intersection using part_part_intersect. If the intersection is non-empty then it produces a structure called a PartPartIntersect and also modifies meshchange to add new vertices and edges that are needed by the intersection. If the two Parts are not coplanar, the PartPartIntersect will contain the intervals the line of the plane-plane intersection, as described in a section above. If they are coplanar then the intersection is not actually calculated at this point: we just put all the geometry from both parts into the PartPartIntersect. So this is really only a potentially intersecting set of geometry. The important point is that all of the geometry is in the same plane or on the same line. Record the PartPartIntersect (PPI) in a list of PPIs for each Part. It will go in the list of both part a and part b.
For all Parts a:
Use a routine called self_intersect_part_and_ppis to intersect a with the PartPartIntersects we recorded for it in the previous loop. That routine uses the Constrained Delaunay Triangulation routine as explained in the previous section to do a 2D intersection of the Part with all of the other geometry that has been accumulated as intersecting or potentially intersecting. The result of this routine is a PartPartIntersect which has all of the geometry that should replace the current geometry of the Part. It also will add any needed new vertices and edges to meshchange, and also records there some geometry that should be deleted from the Mesh. (At the moment the returned PPI isn't used except as an indication that something has changed. The meshchange structure has all the needed change information.

The above is written for the normal case, where there are two parts. There are some slight changes for the self case, mainly just avoiding doing the same work twice.

The part of the code that looks for potentially intersecting Part pairs is faster than just an O(n2) loop comparing all pairs. It uses a Bounding Volume Hierarchy (BVH) tree to quickly find only pairs of Parts whose bounding boxes intersect. For many Boolean operations intersecting two very large dense shells, the number of part pairs that intersect may be as small as the square root of the total number of faces, so this is quite a savings.

Example

Using the Four Intersecting Parts model given above, here is what happens in the algorithm.

First, four Parts are found. Each Part has one face. So part_part_intersect discovers the intersection lines and new vertices shown in the Pairwise Intersection diagram above. As the algorithm finds new vertices and edges, it checks to see if they are already part of the original imesh or the meshchange-so-far, and reuses the old indices if so. Here are how the data structures evolve during the pairwise intersection (the parts happened to be ordered: D, A, B, C). For easy reference, here is a copy of the pairwise intersections:

Pairwise Intersections
  • D ∩ A: no intersection
  • D ∩ B: meshadd = verts{h,g}, edges{hg}. ppi = {hg}.
  • D ∩ C: meshadd = verts{h,g,i,j}, edges{hg,ij}. ppi = {ij}. (meshadd has incremental add to previous)
  • A ∩ B: meshadd = verts{h,g,i,j,a,b}, edges{hg,ij,ab}. ppi = {ab}.
  • A ∩ C: meshadd = verts{h,g,i,j,a,b}, edges{hg,ij,ab}. ppi = {cd}. (c and d already existed, as did cd).
  • B ∩ C: meshadd = verts{h,g,i,j,a,b,f}, edges{hg,ij,ab,bf}. ppi = {bf}. (e was same as already-added b).

After this step, we have ppi lists for each part gathered by looking at all of the ppi's above where that part is an operand. So:

  • ppis(D) = {hg,ij}
  • ppis(A) = {ab,cd}
  • ppis(B) = {hg,ab,bf}
  • ppis(C) = {ij,cd,bf}

At this point, we haven't put anything into meshdelete. That happens in the next steps.

The next steps are do 2D intersections of each Part with all of their ppi's. The following diagram shows the inputs to that process on the left and the outputs on the right. For the inputs, the input faces are shown in pink and the input edges are shown in red.

Self intersecting Parts with their PartPartIntersects

Note that each case has resulted in one more vertex being added (k, l, m, and n, respectively). And the last three cases required one ore more additional support edges in order to make a valid BMesh.

The final meshchange has all of the vertices and edges shown in black in the diagram at the right, except for those that are the same as they were in the original mesh. Now the meshdelete has edges in it. For example, the old top edge of face D is deleted because it has been replaced by two new shorter edges attached to vertex h. Those new edges have recorded the fact that they have that old edges as example -- when we create a new BMesh edge, we can specify an example and that will copy over all of the attributes from the example, such as crease, sharpness, bevel weight, etc.

Boolean