mikedh / trimesh

Python library for loading and using triangular meshes.
https://trimesh.org
MIT License
3k stars 580 forks source link

Combining/union/concatenating adjacent meshes #2285

Open Kylsha opened 1 month ago

Kylsha commented 1 month ago

Hello! Recently started using trimesh in a purpose of converting geojson polygons into extruded solids for 3d printing. Tested it on some data and found an issue. Due to some demands of 3d printing process all adjacent solids should be combined together. Let's say we got three simple boxes:

import trimesh
import pyglet
from shapely import Polygon

pts1 = ((0, 0), (10, 0), (10, 10), (0, 10))
pts2 = ((10, 10), (20, 10), (20, 20), (10, 20))
pts3 = ((10, 0), (20, 0), (20, 10), (10, 10))

p1 = Polygon(pts1, [])
p2 = Polygon(pts2, [])
p3 = Polygon(pts3, [])

mesh1 = trimesh.creation.extrude_polygon(p1, 10)
mesh2 = trimesh.creation.extrude_polygon(p2, 10)
mesh3 = trimesh.creation.extrude_polygon(p3, 10)

I tested two methods to do combine all of them.

  1. union - can stuck at moment when meshes are touching each other at single point. Looks like it is an error and cannot be resolved

united_meshes = mesh1.union(mesh2)

image

After this error nothing can be further added to this mesh and trimesh throws an error "Not all meshes are volumes!"

  1. concatenate - works better but keeps internal faces which can be seen later in AutoCAD:

concatenated_meshes = trimesh.util.concatenate([mesh1, mesh2, mesh3])

image

Tried for solutions like:

concatenated_meshes .merge_vertices(merge_tex=True, merge_norm=True)
concatenated_meshes .remove_unreferenced_vertices()
concatenated_meshes .update_faces(concatenated_meshes .unique_faces())
but nothing worked yet. Some interior faces circled red on image above persist in a mesh. 

Is there any way to delete that faces or even more better way to join meshes? It's also would be nice if there is a way to avoid creating triangle faces. Those primitives are simple extrusions of path and if it were extruded manually in AutoCAD, there wouldn't be any of diagonal edge, Thanks in advance!

mikedh commented 1 month ago

Hey, yeah I'd avoid mesh boolean operations if you can even though they've gotten a lot better since manifold3d came out. A 2D union works pretty well and you can polygon.buffer(1-e5) first if that's flaky:

In [1]: import numpy as np

In [2]: from shapely.ops import unary_union

In [3]: from shapely import Polygon

In [4]: pts1 = ((0, 0), (10, 0), (10, 10), (0, 10))
   ...: pts2 = ((10, 10), (20, 10), (20, 20), (10, 20))
   ...: pts3 = ((10, 0), (20, 0), (20, 10), (10, 10))

In [5]: p1 = Polygon(pts1, [])
   ...: p2 = Polygon(pts2, [])
   ...: p3 = Polygon(pts3, [])

In [7]: import trimesh

In [8]: trimesh.creation.extrude_polygon(unary_union([p1, p2, p3]), 10)
Out[8]: <trimesh.Trimesh(vertices.shape=(16, 3), faces.shape=(28, 3))

In [9]: trimesh.creation.extrude_polygon(unary_union([p1, p2, p3]), 10).is_volume
Out[9]: True

Or if everything is a quad like the example, you can use trimesh methods which merge vertices and find the outline by using trimesh.grouping.group_rows(mesh.edges_sorted, require_count=1) since edges that aren't on the boundary of a well-constructed mesh occur twice:

In [1]: import numpy as np

In [2]: import trimesh

In [3]: pts1 = ((0, 0), (10, 0), (10, 10), (0, 10))
   ...: pts2 = ((10, 10), (20, 10), (20, 20), (10, 20))
   ...: pts3 = ((10, 0), (20, 0), (20, 10), (10, 10))

In [5]: vertices = np.vstack([pts1, pts2, pts3], dtype=np.float64)

In [6]: quads = np.arange(len(vertices), dtype=np.int64).reshape((-1, 4))

In [7]: mesh = trimesh.Trimesh(vertices=vertices, faces=quads)

# quads are triangulated on load and duplicate vertices are merged by default
In [8]: mesh
Out[8]: <trimesh.Trimesh(vertices.shape=(8, 2), faces.shape=(6, 3))>

In [9]: mesh.outline()
Out[9]: <trimesh.Path3D(vertices.shape=(8, 2), len(entities)=1)>

In [11]: mesh.outline().to_planar()[0].extrude(10)
Out[11]: <trimesh.primitives.Extrusion>

In [12]: e = mesh.outline().to_planar()[0].extrude(10)

In [14]: e.is_volume
Out[14]: True
Kylsha commented 1 month ago

@mikedh thanks for response! Your second code snippet does right thing! My complete code for example:

import numpy as np
import trimesh
import ezdxf

pts1 = ((0, 0),   (10, 0),  (10, 10), (0, 10))
pts2 = ((10, 10), (20, 10), (20, 20), (10, 20))
pts3 = ((10, 0),  (20, 0),  (20, 10), (10, 10))

vertices = np.vstack([pts1, pts2, pts3], dtype=np.float64)
quads = np.arange(len(vertices), dtype=np.int64).reshape((-1, 4))
mesh = trimesh.Trimesh(vertices=vertices, faces=quads)

extruded_mesh = mesh.outline().to_planar()[0].extrude(10)

# creating a dxf document with mesh
doc = ezdxf.new("R2000")
msp = doc.modelspace()

new_mesh = msp.add_mesh()
new_mesh.dxf.subdivision_levels = 0
with new_mesh.edit_data() as mesh_data:
    mesh_data.vertices = extruded_mesh.vertices.tolist()
    mesh_data.faces = extruded_mesh.faces.tolist()

doc.saveas("mesh.dxf")

image

It does not remove some faces on exterior but it's not a really big deal. Inner rows and faces are gone and it's okay.

And sorry, I forgot to point that extrusion height of polygons may be different because it refers to real building heights given in geojson source file.

It looks like everything is still about filtering faces which are created during polygon extrusion. And method trimesh.grouping.group_rows returns empty list for concatenated polygon made of three extruded meshed, because each row/face there is unique. image

pts1 = ((0, 0),   (10, 0),  (10, 10), (0, 10))
pts2 = ((10, 10), (20, 10), (20, 20), (10, 20))
pts3 = ((10, 0),  (20, 0),  (20, 10), (10, 10))

p1 = Polygon(pts1, [])
p2 = Polygon(pts2, [])
p3 = Polygon(pts3, [])

# extruded meshes with different heights
mesh1 = trimesh.creation.extrude_polygon(p1, 10) 
mesh2 = trimesh.creation.extrude_polygon(p2, 20)
mesh3 = trimesh.creation.extrude_polygon(p3, 30)

concatenated_mesh = trimesh.util.concatenate([mesh1, mesh2, mesh3])
filtered_faces = trimesh.grouping.group_rows(concatenated_mesh.edges_sorted, require_count=1) #will be empty

@mikedh is there some solution for this case?