Surface Triangulation

The visual resolution of a 3D surface is dependent on the viewer’s interpretation of both the geometry and the color. The color interpretation itself is dependent on the distribution of surface color and the 3D lighting effect.

For many cases, visually smoothing a surface can be made by increasing the number of polyhedral faces used to represent the surface geometry. The Surface3DCollection method triangulate can be used to increase the number of faces in a mesh, enhancing the visual resolution for both geometry and color. A surface object, surface, is triangulated as:

surface.triangulate(rez)

where the single rez argument recursively subdivides each face into four faces rez times. In general, a rez of 3 or 4 is sufficient for geometric and color smoothing. For example, for a rez of 3, each original face will be composed of 64 sub-faces.

The default value for rez is 0. For this value, there is no effect on triangulated surface meshes. Other polygon mesh surfaces will be converted to triangular meshes when res is zero and triangulated further if rez is greater than zero.

However, for some cases, surface constructions may limit increasing the number of faces when

  • polygon meshes are predefined on input. Further subdivision of the polygons still will produce ‘flat’ meshes over the original polygons.

  • meshes are defined with vertex values which are used to assign face values. Matplotlib surface colors are set by polyhedral face values and so colors are not smoothed over faces.

  • surfaces are defined by a functional definition of geometry that creates anomalous variations using a higher resolution.

In the following sections, methods for increasing the perceived resolution of surfaces are described for these cases, which are used in conjunction with the triangulate method.

Geometry Smoothing

Geometric smoothing is made by linearly approximating the vertex normals across the triangulated flat surfaces. The vertex normals are then used to calculate the face normals. The triangulation method does not change the flat polyhedral face geometry. However, when applying the shade and hilite methods, these effects can be ‘smoothed’ over the flat subdivided faces by setting the flat argument to False, i.e.

surface.shade(flat=False)

and:

surface.hilite(flat=False)

The default value for the flat argument is True. In general, when using both the shade and hilite methods, the flat arguments are both set to the same value.

The effect of using triangulate and flat=False on a low resolution surface is demonstrated in the figure below.

../../_images/geom_sm.png

For these examples, geometric mapping was first made to generate the low rez surfaces (the left and middle surfaces). Since this surface is defined mathematically, functional mapping can be used with a starting high rez, without further triangulation. For comparison, this was used for the surface on the right.

By comparing the center and right surfaces, the initial high rez surface provides more visual detail of the geometry compared to the low rez triangulated surface. This occurs even though both surfaces have the same number of polyhedrons. However, simply increasing the rez may not be an option for predefined surface meshes. For such cases, geometric smoothing using triangulation is particularly useful. Consider the following example:

../../_images/bunny_sm1.png

In this case, the predefined mesh was obtained from an obj file, not generated from a functional operation ( see Stanford Bunny ). An additional example is shown in Unstructured coordinates, Smoothed Surface.

Note

Any geometric mapping or clipping, following the use of triangulate, will remove the effect of shading or highlighting using flat=False.

Color Smoothing

Surface coloration is made by assigning a color to each polyhedral face. When colormaps are used, colormap indices are mapped from either face scalar values or from vertex scalar values. Color smoothing over a flat triangulated face is accomplished by linear interpolation of these values over the sub-faces. In this sense, values are smoothed and not colors.

Face values

Face values are represented as colors using the mapping method of a surface object as:

surface.map_cmap_from_op(operatioon,cmap)

where the operation argument is a user-defined function of xyz face-center coordinates and returns a scalar value. The cmap argument is the colormap to represent the values. Surface faces, following geometric mapping, will retain the color. This is demonstrated in the Base Class Geometric Mapping and Face Color Array examples.

To increase the color distribution across ‘flat’ triangular faces, the triangulate method is use prior to the color mapping method, as:

surface.triangulate(rez)
surface.map_cmap_from_op(operatioon,cmap)

If surface methods are called in reverse order, the flat surfaces will visually remain flat, as shown in the following figure.

../../_images/fcolor_sm.png

In general, there would be no need to call the triangulate method following colormapping if the surfaces are to be visually flat, as example Platonic Solids. However, this may be needed when subsquent geometric mappings need to maintain the flat surface colors, for example Base Class Geometric Mapping and Random Grid Geometry .

Vertex values

Vertices can be assigned values in two different ways. First, passing a N X 4 vertCoor argument to the Surface3DCollection constructor (see the Vertex Coordinate Array with Values example).

Or secondly, using the surface object method:

surface.set_vertvals(values)

where values is array or list of N scalar values, with N being the number of surface vertices (see for example Vertex Value Assignment to a Mesh ).

Once vertex values are assinged, subsequent triangulation will smooth the vertex values over the flat faces. These values can then be mapped to a colormap, cmap, using:

surface.map_cmap_from_vertvals(cmap)

This is illustrated in the following figure by randomly assigning values to the vertices of a spherical grid geometry and colormapping as:

surf = s3d.SphericalSurface.grid(6,10,'d')
vertVals = np.random.uniform(-3,5,len(surf.vertices.T))
surf.set_vertvals(vertVals)
surf.triangulate(rez)
surf.map_cmap_from_vertvals(cmap,'vertex value')

with the result:

../../_images/vcolor_sm.png

Once surface coloring is applied, further geometric smoothing of the triangulated flat faces can be made by setting flat=False during shading, as shown below.

../../_images/vcolor_sm_0.png

Applying both vertex and geometric smoothing to a surface is also shown in the Cosmic Bunny example.

Polygon Intersection

During Matplotlib rendering of a Poly3DCollection, multiple polygons are stacked along the view direction. However, an incorrect rendering is produced for intersecting polygons with the visualization affected by the view direction. The visualization can be improved by triangulating the surface object into small polygons which approximate the resolution of the image.

For example, consider the simple case of a surface object composed of three intersecting triangles: The script below defines the vertices, v, and faces indicies, f, for two such surfaces.

v = [ [0,0,0], [1,0,0], [1,1,0], [0,1,0],
      [0,0,1], [1,0,1], [1,1,1], [0,1,1] ]
f = [ [1,6,4], [1,3,5], [0,2,7] ]
colors, tks = [ 'C0', 'C1', 'C2' ], [0,.5,1]

surf  = s3d.Surface3DCollection(v,f,color=colors)
surfT = s3d.Surface3DCollection(v,f,color=colors).triangulate(6)

As seen in the figure below, the visualization is incorrect without triangulation. With triangualtion, the intersection of the smaller triangles is not apparent since the triangle sizes are small compared to the image resolution.

../../_images/inter_sm.png

This simple surface example has ‘large’ polyhedrons compared to the figure size, hence the need for a rez value of 6. In general, the triangulate rez value will be smaller for surfaces composed of smaller polyhedrons. Triangulation is particularly needed for composite surfaces where polygon intersections are common in the composite.

Example python scripts

Figure 1: Geometry smoothing:

import numpy as np
import matplotlib.pyplot as plt
import s3dlib.surface as s3d

def deflate(rtp) :
    r,t,p = rtp
    scale = 0.3
    Rz = np.cos(p)
    Rxys = (1-scale)*np.sin(p) + scale*np.cos(4*t)
    R = np.sqrt( Rz**2 + Rxys**2)
    return R,t,p

# Construct figure, add surface, plot ..........................
rez,btype,trez,minmax = 1, 'dodeca', 4, (-.75,.75)
fig = plt.figure(figsize=(9,3))
fig.text(0.01,0.99,'Figure 1',va='top',ha='left',style='italic',c='b')
for i in range(3) :
    ax =fig.add_subplot(131+i, projection='3d', aspect='equal')
    ax.view_init(azim=-15)
    ax.set(xlim=minmax, ylim=minmax, zlim=minmax)
    ax.set_axis_off()

    surf = s3d.SphericalSurface(rez,btype)
    surf.map_geom_from_op(deflate)
    pos = .5
    if i==0 :
        pos -= .33
        title = "rez = "+str(rez)
        surf.shade().hilite()
    elif i==1 :
        title = "rez = "+str(rez) + "\ntriangulate(" + str(trez) + \
            "), flat=False"
        surf.triangulate(trez).shade(flat=False).hilite(flat=False)
    else :
        pos += .33
        title = "rez = "+str(rez+trez)
        surf = s3d.SphericalSurface(rez+trez,btype)
        surf.map_geom_from_op(deflate)
        surf.shade().hilite()
    info = 'faces: ' + str(len(surf.facecenters.T)) +'\nvertices: ' + str(len(surf.vertices.T))
    fig.text(pos,0.01,info, ha='center', va='bottom', fontsize='smaller')
    ax.set_title(title,fontsize='x-large')
    ax.add_collection3d(surf)

fig.tight_layout()
plt.show()

Figure 2: Face value:

import matplotlib.pyplot as plt
import s3dlib.surface as s3d

# Setup surface ................................................
trez, cmap, title = 4, 'jet', [None]*2
zDir = lambda c : s3d.SphericalSurface.coor_convert(c,True)[2]

title[0] = 'triangulate BEFORE\ncolor mapping'
triCmap = s3d.SphericalSurface.grid(6,10,'r')
triCmap.triangulate(trez)
triCmap.map_cmap_from_op(zDir,cmap)

title[1] = 'triangulate AFTER\ncolor mapping'
cmapTri = s3d.SphericalSurface.grid(6,10,'r')
cmapTri.map_cmap_from_op(zDir,cmap)
cmapTri.triangulate(trez)   # NOT needed, no effect on visualization

# Construct figure, add surface, plot ..........................
fig = plt.figure(figsize=(6,3))
fig.text(0.01,0.99,'Figure 2',va='top',ha='left',style='italic',c='b')
for i,surface in enumerate([triCmap, cmapTri]) :
    ax = fig.add_subplot(121+i, projection='3d', aspect='equal')
    ofst = 0.26 if i==0 else 0.67
    fig.text(ofst,0.02,title[i], ha='center', va='bottom', fontsize='large')
    ax.set_axis_off()
    minmax = (-.75,.75) if i==0 else (-.62,.62)
    ax.set(xlim=minmax, ylim=minmax, zlim=minmax)
    ax.add_collection3d(surface.shade().hilite(.5))

cbar = plt.colorbar(triCmap.cBar_ScalarMappable, ax=ax,  shrink=0.7 )
cbar.set_label('face values', rotation=270, labelpad=10)

fig.tight_layout(pad=1)
plt.show()

Figure 3: Vertex value:

import numpy as np
import matplotlib.pyplot as plt
import s3dlib.surface as s3d

# Setup surface ................................................
rez,cmap,seed = 4,'jet', 1
np.random.seed(seed)

surface = s3d.SphericalSurface.grid(6,10,'d')
edges    = surface.edges
vertices = surface.vertices
vertVals = np.random.uniform(-3,5,len(vertices.T))
surface.set_vertvals(vertVals)
surface.triangulate(rez)
surface.map_cmap_from_vertvals(cmap,'vertex value')

# Construct figure, add surface, plot ..........................
title = ['random vertex values', 'interpolated face values']

fig = plt.figure(figsize=(6,3))
fig.text(0.01,0.99,'Figure 3',va='top',ha='left',style='italic',c='b')
for i in range(2) :
    ax =fig.add_subplot(121+i, projection='3d', aspect='equal')
    ofst = 0.26 if i==0 else 0.7
    fig.text(ofst,0.02,title[i], ha='center', va='bottom', fontsize='large')
    ax.set_axis_off()
    minmax = (-.75,.75) if i==0 else (-.6,.6)
    ax.set(xlim=minmax, ylim=minmax, zlim=minmax)
    if i==0 : 
        ax.add_collection3d(edges.fade(.1))
        ax.scatter(*vertices, c=vertVals, cmap=cmap, s=100,edgecolor='k')
    else :
        ax.add_collection3d(surface.shade())

cbar = plt.colorbar(surface.cBar_ScalarMappable, ax=ax,  shrink=0.7 )
cbar.set_label(surface.cname, rotation=270, labelpad=5)

fig.tight_layout()
plt.show()

Figure 4: Intersecting triangles:

from matplotlib import pyplot as plt
import s3dlib.surface as s3d

v = [ [0,0,0], [1,0,0], [1,1,0], [0,1,0],
      [0,0,1], [1,0,1], [1,1,1], [0,1,1] ]
f = [ [1,6,4], [1,3,5], [0,2,7] ]
colors, tks = [ 'C0', 'C1', 'C2' ], [0,.5,1]

surf  = s3d.Surface3DCollection(v,f,color=colors)
surfT = s3d.Surface3DCollection(v,f,color=colors).triangulate(6)

vE,iE = [ [1,0,1], [1,1,1], [0,1,1], [1,1,0] ], [ [0,1,2],[1,3]]
title = ['INCORRECT\nvisualization', 'Intersecting Polygons\nusing triangulate(6)']
fig = plt.figure(figsize=(6,3))
fig.text(0.01,0.99,'Figure 4',va='top',ha='left',style='italic',c='b')
for i,surface in enumerate([surf,surfT]) :
    ax = fig.add_subplot(121+i, projection='3d', aspect='equal', focal_length=0.5)
    pos = 0.25 if i==0 else 0.75
    fig.text(pos,0.9,title[i],ha='center',va='center')
    ax.set(xlabel='X', ylabel='Y', zlabel='Z',
           xticks=tks, yticks=tks, zticks=tks  )
    ax.view_init(25,14)
    ax.add_collection3d(surface )
    ax.add_collection3d( s3d.ColorLine3DCollection(vE,iE,color='0.7',lw=1) )
fig.tight_layout(pad=3)
plt.show()