Bézier triangular patches defined as Plotly trisurfs

One year ago we presented here a method for visualizing Bézier triangular surfaces via Python Plotly. Since at that time Plotly could plot only rectangular patches we devised a tricky method to associate a rectangular meshgrid to a triangulation. Now Plotly can plot trisurfs, and here we present how we triangulate the parameter domain of a triangular patch in order to plot it as a trisurf.

The new method is based on the theoretical presentation of Bézier triangular surfaces made in the previous notebook.


In [1]:
import numpy as np
from __future__ import division

Below we define functions that perform the triangulation of the equilateral triangle $\Delta$, in $\mathbb{R}^3$, of vertices $(0,0,1)$, $(1,0,0)$, $(0,1,0)$. Unlike the triangulation method provided by matplotlib.tri, we define a triangulation that returns the barycentric coordinates of vertices, not the cartesian ones.

The triangulate function called for an integer p returns the vertices of a triangulation having $p+1$ points on each side of the triangle $\Delta$, simplices returns the indices corresponding to vertices that form the unfilled triangles in the image below, while simplicesCompl returns the indices corresponding to vertices of filled triangles:


In [2]:
from IPython.display import Image
Image(filename='Imag/triangulgroups.png')


Out[2]:

In [3]:
def triangulate(p):
    I=range(p, -1, -1)
    return [(i/p,j/p, 1-(i+j)/p) for i in I for j in range(p-i, -1, -1)]

def simplices(p):
    
    triplets=[]
    i=0
    j=1
    for nr in range(1,p+1):
        for k in range(nr):
            triplets.append([i,j,j+1])
            i+=1
            j+=1
        j+=1
    return np.array(triplets).reshape((len(triplets), 3))

In [4]:
def simplicesCompl(p):
    
    triplets=[]
    i=1
    j=2
    t=4
    m=2
    for nr in range(1,p):
        for k in range(nr):
            triplets.append([i,j,t])
            if k<nr-1:
                i=i+1
                j=j+1
                t+=1
        i=j+1
        j=i+1
        t=t+nr+m
        m-=1
    return np.array(triplets).reshape((len(triplets), 3))

In [5]:
def simplicesAll(p):# a function that returns all simplices returned by both functions above
    
    triplets=[]
    i=0
    j=1
    for nr in range(1,p+1):
        for k in range(nr):
            triplets.append([i,j,j+1])
            i+=1
            j+=1
        j+=1
    i=1
    j=2
    t=4
    m=2
    for nr in range(1,p):
        for k in range(nr):
            triplets.append([i,j,t])
            if k<nr-1:
                i=i+1
                j=j+1
                t+=1
        i=j+1
        j=i+1
        t=t+nr+m
        m-=1    
    return np.array(triplets).reshape((len(triplets), 3))

A point of the triangular surface is evaluated according to the triangular de Casteljau algorithm.


In [6]:
def deCasteljau_step(n, b, lam):
    #n is the polynomial degree of the surface
    # b is the list of control points
    #lam is a triplet representing baricentric coordinates of a point in the associated 
    #triangulation of Delta
    i=0
    j=1
    for nr in range(1, n+1):
        for k in range(nr):
            b[i]=lam[0]*b[i]+lam[1]*b[j]+lam[2]*b[j+1]
            i+=1
            j+=1
        j+=1
    return b[:-(n+1)]

In [7]:
def deCasteljau(n,b,lam):
    #recursive function that returns a point on the triangular surface corresponding to
    #the barycentric coordinates [lam[0], lam[1], lam[2]
    
    if len(b)>1:       
        return deCasteljau(n-1, deCasteljau_step(n, b, lam), lam)   
    else: 
        return b[0]

To each point (triplet of barycentric coordinates) in a triangulation one associates a point on the Bézier patch.

The following function discretizes a Bézier surface of degree n, control points b, and triangulation vertices ( barycenters), defined above:


In [8]:
def surface_points(n, b, barycenters):
    
    points=[]
    for weight in barycenters:
        b_aux=np.array(b)
        points.append(deCasteljau(n, b_aux, weight))
    return zip(*points)

set_data_for_Surface prepare data for a plot:


In [9]:
def set_data_for_Surface(n, b, p):
    
    if len(b)!=(n+1)*(n+2)/2:
        raise ValueError('incorect number of control points')
    barycenters=triangulate(p)
    
    x,y,z=surface_points(n, b, barycenters   )
    return x,y, z

In [10]:
def map_z2color(zval, cmap, vmin, vmax):
    #map the normalized value zval to a corresponding color in the colormap cmap
    
    if vmin>=vmax:
        raise ValueError('incorrect relation between vmin and vmax')
    t=(zval-vmin)/float((vmax-vmin))#normalize val
    C=map(np.uint8, np.array(cmap(t)[:3])*255)
    return 'rgb'+str((C[0], C[1], C[2]))
    

def tri_indices(simplices):
    #simplices is a numpy array defining the simplices of the triangulation
    #returns the lists of indices i, j, k
     
    return ([triplet[c] for triplet in simplices] for c in range(3))

In [11]:
import plotly.plotly as py
from plotly.graph_objs import *
import matplotlib.cm as cm
import plotly
plotly.offline.init_notebook_mode()



In [12]:
def plotly_trisurf(x, y, z, simplices, colormap=cm.RdBu):
    #x, y, z are lists of coordinates of the triangle vertices 
    #simplices are the simplices that define the triangulation;
    #simplices  is a numpy array of shape (no_triangles, 3)
    #insert here the  type check for input data
    
    points3D=np.vstack((x,y,z)).T
    tri_vertices= points3D[simplices]# vertices of the surface triangles     
    zmean=[np.mean(tri[:,2]) for tri in tri_vertices ]# mean values of z-coordinates of 
                                                      #triangle vertices
    min_zmean=np.min(zmean)
    max_zmean=np.max(zmean)  
    facecolor=[map_z2color(zz,  colormap, min_zmean, max_zmean) for zz in zmean] 
    I,J,K=tri_indices(simplices)
    
    triangles=Mesh3d(x=x,
                     y=y,
                     z=z,
                     facecolor=facecolor, 
                     i=I,
                     j=J,
                     k=K,
                     name=''
                    )
    
    
    return triangles

Function that plots triangles with a color not a colormap:


In [13]:
def plotly_trisurf_color(x, y, z, simplices, color):
    #x, y, z are lists of coordinates of the triangle vertices 
    #simplices are the simplices that define the triangulation;
    #simplices  is a numpy array of shape (no_triangles, 3)
     
    facecolor=[color]*simplices.shape[0]
    I,J,K=tri_indices(simplices)
    triangles=Mesh3d(x=x,
                     y=y,
                     z=z,
                     facecolor=facecolor, 
                     i=I,
                     j=J,
                     k=K,
                     name=''
                    )
    
    
    return triangles

We plot three surfaces corresponding to the same input data (Bézier control points), with the global triangulation, complementary triangulations colored distinctly, complementary triangulations colored with the same colorscale:


In [14]:
n=3
b=[[0,5.0, 3.0],
   [-1.2, 4, 4.4],
   [ 1.7, 3.0, 4.6],
   [-2.3, 2.8, 6.0],
   [-0.5, 2.5, 5.2],
   [2.8,2, 6.25],
   [-3.8,0, 4.2],
   [-1.8, -0.63, 3.17],
   [1.9,1.0, 3.0],
   [3.5, 0.2, 5.8 ]]
p=40

In [15]:
x,y,z=set_data_for_Surface(n, b, p)
tr=plotly_trisurf(x, y, z, simplicesAll(p), colormap=cm.viridis)
data=Data([tr])

In [16]:
axis = dict(
showbackground=True, 
backgroundcolor="rgb(230, 230,230)",
gridcolor="rgb(255, 255, 255)",      
zerolinecolor="rgb(255, 255, 255)",  
    )

layout = Layout(
         title='Bezier triangular patch defined as a trisurf',
         width=800,
         height=800,
         showlegend=False,
         scene=Scene(xaxis=XAxis(axis),
                     yaxis=YAxis(axis), 
                     zaxis=ZAxis(axis), 
                     aspectratio=dict(x=1,
                                      y=1,
                                      z=1
                                     ),
                    )
        )

fig = Figure(data=data, layout=layout)

In [17]:
plotly.offline.iplot(fig)



In [19]:
trace1=plotly_trisurf_color(x, y, z, simplices(p), color='blue')
trace2=plotly_trisurf_color(x, y, z, simplicesCompl(p), color='white')
data1=Data([trace1, trace2])

In [20]:
fig1 = Figure(data=data1, layout=layout)
fig1['layout'].update(title='Bezier triangular patch defined as a trisurf with interleaved colors')

In [21]:
plotly.offline.iplot(fig1)


Finally we plot the surface defined by two Plotly Surface instances: one for the simplices corresponding to unfilled triangles in the image above, and another for the simplices representing the filled triangles:


In [23]:
trace3=plotly_trisurf(x, y, z, simplices(p), colormap=cm.viridis)
trace4=plotly_trisurf(x, y, z, simplicesCompl(p), colormap=cm.viridis)
data2=Data([trace3, trace4])
fig2 = Figure(data=data2, layout=layout)
fig2['layout'].update(title='Bezier triangular patch defined as a trisurf')
plotly.offline.iplot(fig2)



In [24]:
from IPython.core.display import HTML
def  css_styling():
    styles = open("./custom.css", "r").read()
    return HTML(styles)
css_styling()


Out[24]: