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()