Open Surfaces : Shading and Highlighting

For closed surface objects, those enclosing a volume, faces can only be viewed from ‘outside’. The outside, or ‘front’ of the surface, is defined to have a positive outward normal. The face normals are set during object initiation by using a right-hand rule to set the order of vertex indices defined for each face. The directions of light source and outward normal set the shading and highlighting of the surface color.

When surface objects are not closed, faces can be viewed either from the front or back. In this case, view direction influences whether the front or back of the face is shown. Consequently, from a view perspective, some ‘back’ surface faces may become the ‘front’ surface. To compensate for this possible ambiguity in 3D visualizations, several techniques can be applied using S3Dlib:

  • orientable double cover
    For this case, surfaces are constructed so that only ‘front’ facing surfaces are viewable. The applicability of this method is dependent on the shape of the surface. This approach was use for an orientable surface in the Inner Surface example. The Shaded Mobius Strip Visualization is an example of visualizing a non-orientable surface.
  • mirrored color mapping
    Surfaces can be color mapped using mirrored colormaps. Using this method, face colors are only dependent on the magnitude of the face normals relative to lighting, not the direction.
  • extended control of surface shading
    The subset of arguments for the shading method was covered in the Shading, Highlighting and Color Mapped Normals guide for closed surfaces. Additional arguments are provided for use with open surfaces in the following.

The shade surface method has full signature as:

surface.shade(depth=0, direction=None, contrast=None, isAbs=False, ax=None, rview=False)

where the argument values, defaults, and constraints are as follows:

argument value default constraint
depth scalar 0 range: [0,1]
direction array (1,0,1)
contrast scalar 1 range: [0.1,3]
isAbs boolean False
ax axes3D None valid Maxplotlib 3D axes
rview boolean False for ax not None

The first three arguments are described in the Shading, Highlighting and Color Mapped Normals guide.

Setting the isAbs argument to True is equivalent to colormapping the surface using a mirrored colormap when shading is applied to the surface.

The ax argument is the 3D Maxplotlib axes which the surface object is to be added ( ax.add_collection(surface) ) . Therefore, shading must be applied after the axes view_init method is called.

Setting the rview argument to True uses the illumination direction vector relative to the axis viewing direction. This is only applied if the ax argument is set to other than None. The ‘relative to view’ is discussed in in the Viewer Lighting guide section.

In a similar manner, the surface hilite also has additional arguments:

surface.hilite(height=1, direction=None, focus=None, ax=None, rview=False)

where the arguments have been previously described.

Orientable Surface

Surface Shading

Surface with a consistent definition for an outward face are orientable . The following figure shows two open surface geometries which have been shaded using the control arguments ax and isAbs:


Shading without the use of ax or isAbs will provide a 3-dimensional effect to the visualization. When a surface is viewed showing predominantly only one side of the surface, this shading method provides sufficient clarity. For example, Setting a domain for function Plots or Sliced Polar Surface. Adding the axis view to shading, using the ax parameter, may provide a more realistic view. Depending on the geometry, this realistic view may be required to clarify the shape. For example, Hyperboloid of Revolution and Filled ParametricLine Surface 2. Also, the use of the ax parameter is necessary when inner and outer surfaces are colored differently, as seen in examples Inner and Outer Surface Coloring and Inner/Outer Surface Colormap.

Setting isAbs=True, effectively, provides a satin surface appearance with the lighting direction perpendicular to assigned direction.

For closed surfaces, ax will have no effect on the shading. However, the satin appearance will be created on closed surfaces if isAbs is set to True.

Face Normal Colormapping

To further enhance the distinction between ‘front’ and ‘back’ faces, a binary colormap can be used to map the geometry normals relative to the viewing direction. In this case, for a surface prior to shading, use the method:

surface.map_cmap_from_normals(direction=ax, cmap=bcmap)

where the direction argument is assigned to the Maxplotlib 3D axis (ax) and the cmap argument is assigned a binary colormap (bcamp). Using the same example above, but now using this method and colormap:


where the binary colormap is:


Non-Orientable Surface

For non-orientable surfaces, the face normals are not continuous. This is the case for a Mobius strip . As shown below, the discontinuity in the direction of face normals produces a discontinuity in the surface coloring when only normals are used for color mapping.


Surface Shading

Direct shading non-orientable surfaces will produce discontinuities in surface color as a result of the discontinuity in face normals. For continuous shading of non-orientable surfaces, the shade method arguments ax and isAbs should be used. Shading for the Mobius surface using these arguments are shown below.


Face Normal Colormapping

Color mapping can effectively produce a ‘type of’ shading. For non-orientable surfaces, continuous color mapping must be made using a colormap as either:

surface.map_cmap_from_normals(cmap=colormap, isAbs=True)

or creating a mirrored colormap, colormap_m, and then use:


These two approaches will result in the same surface coloring. The latter method requires the construction of a mirrored colormap, whereas the former method does not. The following illustrates these methods using three example starting colormaps.


Where the colormaps are:


Source Code

import copy
import numpy as np
import matplotlib.pyplot as plt
import s3dlib.surface as s3d
import s3dlib.cmap_utilities as cmu

# Visual comparison of shading arguments.

# 1. Define function to examine .....................................
def wavefunc(xyz) :
    x,y,z = xyz
    r = np.sqrt( x**2 + y**2)
    Z = np.sin( 6.0*r )/2
    return x,y,Z

mrez,melev,mazim,millum = 4,30,-20,[1,1,1]
def mobius(rtz) :
    r,t,z = rtz
    thickness = 0.33
    w = thickness*z 
    R = 1 + w * np.cos(0.5*t)
    Z = w * np.sin(0.5*t)
    return R,t,Z

title = ['shade()','shade(ax=ax)','shade(isAbs=True)']
color = 'wheat'
bcmap = cmu.binary_cmap( 'lightcoral', 'wheat',name='lcrl_wht' )

# Figure 1 Orientable Surfaces ===================================== 

obj = [
    s3d.PlanarSurface(6,color=color).map_geom_from_op(wavefunc) ,
# 3. Construct figure, add surfaces, and plot ......................
fig = plt.figure(figsize=(sc*6,sc*3.5))
fig.text(0.9,0.04,'Figure 1', fontsize='small')
for g in range(2) :           #.. geometries
    for t in range(3) :       #.. visualization types
        ax = fig.add_subplot(231+3*g+t,projection='3d')
        if g == 0 : ax.set_title(title[t],fontsize='large')
        sobj = copy.copy(obj[g])
        # uncomment for figure 1B
        # sobj.map_cmap_from_normals(direction=ax,cmap=bcmap)
        if t == 0 : sobj.shade(sh,direction=[1,1,1])
        if t == 1 : sobj.shade(sh,direction=[1,1,1], ax=ax)
        if t == 2 : sobj.shade(sh,direction=[1,1,1], isAbs=True)

# Figure 2 Mobius normals =========================================== 

redblue = cmu.hsv_cmap_gradient('b','+r')
surface = s3d.CylindricalSurface(mrez, basetype='squ_s')
surface.map_geom_from_op( mobius )
surface.map_cmap_from_normals(cmap=redblue, direction=[1,1,1])
surf = s3d.CylindricalSurface(2, basetype='squ_s')
facenormals = surf.map_geom_from_op( mobius ).facenormals(scale=0.3,color='k')
# 3. Construct figure, add surfaces, and plot ......................
fig = plt.figure(figsize=(4,3))
fig.text(0.85,0.04,'Figure 2', fontsize='small')
ax = plt.axes(projection='3d')
ax.set_title('Face Normals')
ax.set(xlim=(-0.8,0.8), ylim=(-0.8,0.8), zlim=(-0.8,0.8) )
plt.colorbar(surface.cBar_ScalarMappable, ax=ax, shrink=0.6 )

# Figure 3 Mobius Shading ===========================================

surf = s3d.CylindricalSurface(mrez, basetype='squ_s', color=color)
surf.map_geom_from_op( mobius )
# 3. Construct figure, add surfaces, and plot ......................
fig = plt.figure(figsize=(sc*6,sc*2 ))
fig.text(0.9,0.04,'Figure 3', fontsize='small')
for t in range(3) :       #.. visualization types
    ax = fig.add_subplot(131+t,projection='3d')
    sobj = copy.copy(surf)
    if t == 0 : sobj.shade(sh,direction=[1,1,1])
    if t == 1 : sobj.shade(sh,direction=[1,1,1], ax=ax)
    if t == 2 : sobj.shade(sh,direction=[1,1,1], isAbs=True)

# Figure 4 Mobius Colormapping ======================================

cmaps = ['hsv','viridis','blk_brn']
cmaps_mr = ['hsv_mr','viridis_mr','blk_brn_mr']
rlab = ['colormap', 'mirrored colormap\nor\nisAbs=True', 'mirrored & reversed\ncolormap']
# 3. Construct figure, add surfaces, and plot ......................
fig = plt.figure(figsize=(8.5,6))
fig.text(0.9,0.04,'Figure 4', fontsize='small')
for g in range(3) :           #.. shade control
    for t in range(4) :       #.. colormaps & label
        i = 1+4*g + t
        ax = fig.add_subplot(3,4,i,projection='3d')
        ax.set(xlim=(-0.8,0.8), ylim=(-0.8,0.8), zlim=(-0.8,0.8) )
        cm = cmaps if g<2 else cmaps_mr
        trez = mrez if t!=0 else mrez+1  # hsv, for smoother color
        if t<3 :
            if g==0 : ax.set_title(cmaps[t],fontsize='x-large')
            twist = s3d.CylindricalSurface(trez, basetype='squ_s')
            twist.map_geom_from_op( mobius )
            twist.map_cmap_from_normals(cmap=cm[t], direction=[1,1,1],isAbs=g==1)
        else :
            y = 0.83-0.34*g
            fig.text(0.86,y,rlab[g], ha='center', va='center',fontsize='medium')


# ===================================================================