Triangle Mesh

[1]:
import numpy as np

from lapy import TriaMesh

First we create and initialize a TriaMesh of a cube. Note, this mesh is not oriented (some normals flip), so some functions below will complain until we fix the situation with .orient_() later.

[2]:
points = [[0.0,0.0,0.0], [0,1,0], [1,1,0], [1,0,0],
          [0,0,1], [0,1,1], [1,1,1], [1,0,1]]
trias = [[0,1,2], [2,3,0], [4,5,6], [6,7,4], [0,4,7], [7,3,0],
         [0,4,5], [5,1,0], [1,5,6], [6,2,1], [3,7,6], [6,2,3]]

T = TriaMesh(points,trias)

First we check a few properties of the mesh. Is it closed (no boundary)? Is it manifold (no edge has more than two triangles)? And is it consistently oriented (it won’t be)?

[3]:
T.is_closed()
[3]:
True
[4]:
T.is_manifold()
[4]:
True
[5]:
T.is_oriented()
[5]:
False

We can see that orientation is not OK also by looking at the max entry of the directed adjacency matrix. It is a sparse matrix (mainly zeros) adding a 1 for an edge from i to j. If any position has a 2 it means that two half edges are oriented in the same direction, which can only happen if the mesh is not oriented consistently or if it is non-manifold.

[6]:
print(np.max(T.adj_dir))
2.0

Many functions work on non-oriented meshes. For example, we can compute the Euler characteristics (2 for spherical topology), the areas of the triangles and the total mesh area.

[7]:
T.euler()
[7]:
2
[8]:
T.tria_areas()
[8]:
array([0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5])
[9]:
T.area()
[9]:
5.999999999999998

The volume inside the closed mesh, however, requires the mesh to be correctly oriented.

[10]:
T.volume()  # will fail for non-oriented meshes
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[11], line 1
----> 1 T.volume()  # will fail for non-oriented meshes

File ~/Work/software/lapy/LaPy/examples/lapy/tria_mesh.py:304, in TriaMesh.volume(self)
    302     return 0.0
    303 if not self.is_oriented():
--> 304     raise ValueError(
    305         "Error: Can only compute volume for oriented triangle meshes!"
    306     )
    307 v0 = self.v[self.t[:, 0], :]
    308 v1 = self.v[self.t[:, 1], :]

ValueError: Error: Can only compute volume for oriented triangle meshes!

But we can get the vertex degrees (number of edges at each vertex).

[11]:
T.vertex_degrees()
[11]:
array([6, 4, 4, 4, 4, 4, 6, 4])

The area that a vertex represents is computed by summing the areas of all triangles at that vertex and dividing it by 3.

[12]:
T.vertex_areas()
[12]:
array([1.        , 0.66666667, 0.66666667, 0.66666667, 0.66666667,
       0.66666667, 1.        , 0.66666667])

Of course their sum should be equal to the total surface area.

[13]:
sum(T.vertex_areas())
[13]:
6.0

Also the average edge length can be computed on non-oriented meshes easily.

[14]:
T.avg_edge_length()
[14]:
1.1380711874576983

And the triangle normals, but some will point outwards and some inwards.

[15]:
T.tria_normals()
[15]:
array([[ 0.,  0., -1.],
       [ 0., -0., -1.],
       [ 0.,  0., -1.],
       [ 0., -0., -1.],
       [ 0.,  1.,  0.],
       [ 0.,  1.,  0.],
       [-1.,  0.,  0.],
       [-1.,  0., -0.],
       [ 0.,  1.,  0.],
       [ 0.,  1.,  0.],
       [-1.,  0.,  0.],
       [-1.,  0., -0.]])

Triangle qualities measure how close a triangle is to the equilateral triangle. Triangles with a very short edge (sliver) are usually numerically unstable when solving equations with FEM and therefore get a low quality. Here all triangles are similar and have the same quality.

[16]:
T.tria_qualities()
[16]:
array([0.8660254, 0.8660254, 0.8660254, 0.8660254, 0.8660254, 0.8660254,
       0.8660254, 0.8660254, 0.8660254, 0.8660254, 0.8660254, 0.8660254])

Since all vertices are used in at least one triangle, we do not have free vertices. Usually free vertices exist if the mesh is obtained as the boundary of a tetrahedral mesh and the inner vertices are not removed. This keeps vertex indices fixed and allows mapping results from the surface back to the solid. Anyway, removing free vertices in our case will not change anything.

[17]:
T.has_free_vertices()
[17]:
False
[18]:
T.rm_free_vertices_()
[18]:
(array([0, 1, 2, 3, 4, 5, 6, 7]), [])

Let’s now orient the mesh consistently so that all triangle normals point outwards.

[19]:
T.orient_()
Searched mesh after 3 flooding iterations (0.0007233619689941406 sec).
[19]:
6
[20]:
T.is_oriented()
[20]:
True

Now we can also compute the enclosed volume.

[21]:
# Compute Volume (works only for oriented meshes)
T.volume()
[21]:
1.0

Once triangle normals are oriented we can also compute vertex normals (by aggregating the triangle normals at a vertex weighted by the angle).

[22]:
# Vertex normals averages tria normals (only oriented meshes)
T.vertex_normals()
[22]:
array([[-0.57735027, -0.57735027, -0.57735027],
       [-0.40824829,  0.81649658, -0.40824829],
       [ 0.40824829,  0.40824829, -0.81649658],
       [ 0.81649658, -0.40824829, -0.40824829],
       [-0.40824829, -0.40824829,  0.81649658],
       [-0.81649658,  0.40824829,  0.40824829],
       [ 0.57735027,  0.57735027,  0.57735027],
       [ 0.40824829, -0.81649658,  0.40824829]])

Vertex normals are important for rendering the mesh, or they can be used to shift vertices along the normals by an offset.

[23]:
T.normal_offset_(0.2*T.avg_edge_length())

So far our mesh was closed, here we create one with a boundary by dropping triangles.

[24]:
Tbdr = TriaMesh(T.v,T.t[2:,:])

Meshes can be refined by subdividing each triangle into 4 similar smaller ones.

[25]:
Tbdr.refine_()

Finally, we print the boundary loops, these are the vertices oredered along the boundary. Here we only have one loop, but meshes can have many.

[26]:
print(Tbdr.boundary_loops())
[[0, 8, 1, 13, 2, 16, 3, 9]]