Skip to content

ParametricObjectsDemo

vtk-examples/Python/GeometricObjects/ParametricObjectsDemo

Description

Demonstrates the Parametric classes added by Andrew Maclean and additional classes added by Tim Meehan. The parametric spline is also included.

Options are provided to:

  • Specify a single surface (-s SURFACE_NAME)
  • Color the back-face (-b)
  • Add normals (-n)
  • Display the geometric bounds of the object (-l)

You can save a screenshot by pressing "k".

With respect to your VTK build you may need to specify one or more of:

-DVTK_MODULE_ENABLE_VTK_cli11=WANT
-DVTK_MODULE_ENABLE_VTK_fmt=WANT

If -DVTK_BUILD_TESTING=ON is specified when building VTK then VTK:cli11 and VTK::fmt will be automatically enabled.

Note

To really appreciate the complexity of some of these surfaces, select a single surface, and use the options -b -n. Also try specifying wireframe (toggle "w" on the keyboard) and zooming in and out.

Tip

If you color the back face, the three-dimensional orientable surfaces will only show backface coloring inside the surface e.g ConicSpiral or Torus. For three dimensional non-orientable surfaces; backface coloring is visible because of the twisting used to generate these surfaces e.g Boy or Figure8Klein.

Cite

See: Parametric Equations for Surfaces, for more information. This paper provides a description of fifteen surfaces, including their parametric equations and derivatives. Also provided is an example of how to create your own surface, namely the Figure-8 Torus.

Other languages

See (Cxx), (CSharp)

Question

If you have a question about this example, please use the VTK Discourse Forum

Code

ParametricObjectsDemo.py

#!/usr/bin/env python3

"""
    Demonstrate all the parametric objects.
"""

from pathlib import Path

# noinspection PyUnresolvedReferences
import vtkmodules.vtkInteractionStyle
# noinspection PyUnresolvedReferences
import vtkmodules.vtkRenderingFreeType
# noinspection PyUnresolvedReferences
import vtkmodules.vtkRenderingOpenGL2
from vtkmodules.vtkCommonColor import vtkNamedColors
from vtkmodules.vtkCommonComputationalGeometry import (
    vtkParametricBohemianDome,
    vtkParametricBour,
    vtkParametricBoy,
    vtkParametricCatalanMinimal,
    vtkParametricConicSpiral,
    vtkParametricCrossCap,
    vtkParametricDini,
    vtkParametricEllipsoid,
    vtkParametricEnneper,
    vtkParametricFigure8Klein,
    vtkParametricHenneberg,
    vtkParametricKlein,
    vtkParametricKuen,
    vtkParametricMobius,
    vtkParametricPluckerConoid,
    vtkParametricPseudosphere,
    vtkParametricRandomHills,
    vtkParametricRoman,
    vtkParametricSpline,
    vtkParametricSuperEllipsoid,
    vtkParametricSuperToroid,
    vtkParametricTorus
)
from vtkmodules.vtkCommonCore import (
    vtkMinimalStandardRandomSequence,
    vtkPoints
)
from vtkmodules.vtkFiltersCore import (
    vtkGlyph3D,
    vtkMaskPoints
)
from vtkmodules.vtkFiltersSources import (
    vtkArrowSource,
    vtkParametricFunctionSource
)
from vtkmodules.vtkIOImage import (
    vtkPNGWriter
)
from vtkmodules.vtkRenderingCore import (
    vtkActor,
    vtkActor2D,
    vtkPolyDataMapper,
    vtkProperty,
    vtkRenderWindow,
    vtkRenderWindowInteractor,
    vtkRenderer,
    vtkTextMapper,
    vtkTextProperty,
    vtkWindowToImageFilter
)


def get_program_parameters():
    import argparse
    description = 'Display the parametric surfaces.'
    epilogue = '''
   '''
    parser = argparse.ArgumentParser(description=description, epilog=epilogue,
                                     formatter_class=argparse.RawDescriptionHelpFormatter)
    parser.add_argument('-s', '--surface_name', default=None, help='The name of the surface.')
    parser.add_argument('-b', '--back_face', action='store_true', help='Color the back face.')
    parser.add_argument('-n', '--normals', action='store_true', help='Display normals.')
    parser.add_argument('-l', '--limits', action='store_true', help='Display the geometric bounds of the object..')
    args = parser.parse_args()
    return args.surface_name, args.back_face, args.normals, args.limits


def main():
    surface_name, back_face, normals, limits = get_program_parameters()

    # Get the parametric functions and build the pipeline
    pfn = get_parametric_functions()

    # Check for a single surface.
    single_surface = [None, False]
    if surface_name:
        sn = surface_name.lower()
        for t in pfn.keys():
            if sn == t.lower():
                single_surface[0] = t
                single_surface[1] = True
    if surface_name and not single_surface[1]:
        print('Nonexistent surface:', surface_name)
        return

    if single_surface[1]:
        renderer_size = 1000
        grid_column_dimensions = 1
        grid_row_dimensions = 1
    else:
        renderer_size = 200
        grid_column_dimensions = 5
        grid_row_dimensions = 5

    colors = vtkNamedColors()

    # Create one text property for all.
    text_property = vtkTextProperty()
    text_property.SetJustificationToCentered()
    text_property.SetFontSize(int(renderer_size / 12))
    text_property.SetColor(colors.GetColor3d("LavenderBlush"))

    # Create a parametric function source, renderer, mapper, and actor
    # for each object.
    pfn_srcs = []
    renderers = []
    mappers = []
    actors = []
    text_mappers = []
    text_actors = []

    # Glyph the normals.
    mask_pts = []
    arrow = []
    glyph = []
    glyph_mapper = []
    glyph_actor = []

    back_property = vtkProperty()
    if back_face:
        back_property.SetColor(colors.GetColor3d("Peru"))

    # Now decide on the surfaces to build.
    surfaces = dict()
    if single_surface[1]:
        surfaces[single_surface[0]] = pfn[single_surface[0]]
    else:
        surfaces = pfn

    # The bounding boxes for each object.
    bounding_boxes = dict()
    indexed_names = dict()
    # The index of each parametric object.
    obj_idx = -1
    sorted_names = list()
    for obj in sorted(surfaces.keys()):
        obj_idx += 1
        indexed_names[obj_idx] = obj
        pfn_srcs.append(vtkParametricFunctionSource())
        pfn_srcs[obj_idx].SetParametricFunction(surfaces[obj])
        pfn_srcs[obj_idx].SetUResolution(51)
        pfn_srcs[obj_idx].SetVResolution(51)
        pfn_srcs[obj_idx].SetWResolution(51)
        pfn_srcs[obj_idx].Update()

        mappers.append(vtkPolyDataMapper())
        mappers[obj_idx].SetInputConnection(pfn_srcs[obj_idx].GetOutputPort())

        actors.append(vtkActor())
        actors[obj_idx].SetMapper(mappers[obj_idx])
        actors[obj_idx].GetProperty().SetColor(colors.GetColor3d("NavajoWhite"))
        if back_face:
            actors[obj_idx].SetBackfaceProperty(back_property)

        text_mappers.append(vtkTextMapper())
        text_mappers[obj_idx].SetInput(obj)
        text_mappers[obj_idx].SetTextProperty(text_property)

        text_actors.append(vtkActor2D())
        text_actors[obj_idx].SetMapper(text_mappers[obj_idx])
        text_actors[obj_idx].SetPosition(renderer_size / 2.0, 8)

        renderers.append(vtkRenderer())
        renderers[obj_idx].SetBackground(colors.GetColor3d("MidnightBlue"))

        bounds = pfn_srcs[obj_idx].GetOutput().GetBounds()
        bounding_boxes[obj] = bounds

        if normals:
            # Glyphing
            mask_pts.append(vtkMaskPoints())
            mask_pts[obj_idx].RandomModeOn()
            mask_pts[obj_idx].SetMaximumNumberOfPoints(150)
            mask_pts[obj_idx].SetInputConnection(pfn_srcs[obj_idx].GetOutputPort())

            arrow.append(vtkArrowSource())
            arrow[obj_idx].SetTipResolution(16)
            arrow[obj_idx].SetTipLength(0.3)
            arrow[obj_idx].SetTipRadius(0.1)

            glyph_scale = get_maximum_length(bounding_boxes[obj])

            glyph.append(vtkGlyph3D())
            glyph[obj_idx].SetSourceConnection(arrow[obj_idx].GetOutputPort())
            glyph[obj_idx].SetInputConnection(mask_pts[obj_idx].GetOutputPort())
            glyph[obj_idx].SetVectorModeToUseNormal()
            glyph[obj_idx].SetScaleFactor(glyph_scale / 10.0)
            glyph[obj_idx].OrientOn()
            glyph[obj_idx].Update()

            glyph_mapper.append(vtkPolyDataMapper())
            glyph_mapper[obj_idx].SetInputConnection(
                glyph[obj_idx].GetOutputPort())

            glyph_actor.append(vtkActor())
            glyph_actor[obj_idx].SetMapper(glyph_mapper[obj_idx])
            glyph_actor[obj_idx].GetProperty().SetColor(colors.GetColor3d("GreenYellow"))

    # Need a renderer even if there is no actor.
    for i in range(obj_idx + 1, grid_column_dimensions * grid_row_dimensions):
        renderers.append(vtkRenderer())
        renderers[i].SetBackground(colors.GetColor3d("MidnightBlue"))
        sorted_names.append(None)

    ren_win = vtkRenderWindow()
    ren_win.SetSize(renderer_size * grid_column_dimensions, renderer_size * grid_row_dimensions)

    for row in range(0, grid_row_dimensions):
        for col in range(0, grid_column_dimensions):
            index = row * grid_column_dimensions + col
            # (xmin, ymin, xmax, ymax)
            viewport = [
                float(col) * renderer_size / (grid_column_dimensions * renderer_size),
                float(grid_row_dimensions - (row + 1)) * renderer_size / (grid_row_dimensions * renderer_size),
                float(col + 1) * renderer_size / (grid_column_dimensions * renderer_size),
                float(grid_row_dimensions - row) * renderer_size / (grid_row_dimensions * renderer_size)]
            ren_win.AddRenderer(renderers[index])
            renderers[index].SetViewport(viewport)
            if index > obj_idx:
                continue
            renderers[index].AddActor(actors[index])
            # Normals can only be computed for polygons and triangle strips.
            # The Spline is a line.
            if normals and indexed_names[index] != 'Spline':
                renderers[index].AddActor(glyph_actor[index])
            renderers[index].AddActor(text_actors[index])
            renderers[index].ResetCamera()
            renderers[index].GetActiveCamera().Azimuth(30)
            renderers[index].GetActiveCamera().Elevation(-30)
            renderers[index].GetActiveCamera().Zoom(0.9)
            renderers[index].ResetCameraClippingRange()

    iren = vtkRenderWindowInteractor()
    iren.SetRenderWindow(ren_win)

    if limits:
        for k, v in bounding_boxes.items():
            display_bounding_box_and_center(k, v)

    if surface_name:
        fn = single_surface[0]
    else:
        fn = 'ParametricObjectsDemo'
    ren_win.SetWindowName(fn)

    print_callback = PrintCallback(iren, fn, 1, False)
    iren.AddObserver('KeyPressEvent', print_callback)

    iren.Initialize()
    iren.Start()


def get_parametric_functions():
    """
    Create a map of the parametric functions and set some parameters.
    The first key groups the parametric functions and the
      second key is the name of the function.

    :return: The map of functions.
    """
    pfn = dict()
    pfn['Boy'] = vtkParametricBoy()
    pfn['ConicSpiral'] = vtkParametricConicSpiral()
    pfn['CrossCap'] = vtkParametricCrossCap()
    pfn['Dini'] = vtkParametricDini()
    pfn['Ellipsoid'] = vtkParametricEllipsoid()
    pfn['Enneper'] = vtkParametricEnneper()
    pfn['Figure8Klein'] = vtkParametricFigure8Klein()
    pfn['Klein'] = vtkParametricKlein()
    pfn['Mobius'] = vtkParametricMobius()
    pfn['RandomHills'] = vtkParametricRandomHills()
    pfn['Roman'] = vtkParametricRoman()
    pfn['SuperEllipsoid'] = vtkParametricSuperEllipsoid()
    pfn['SuperToroid'] = vtkParametricSuperToroid()
    pfn['Torus'] = vtkParametricTorus()
    pfn['Spline'] = vtkParametricSpline()
    # Extra parametric surfaces.
    pfn['BohemianDome'] = vtkParametricBohemianDome()
    pfn['Bour'] = vtkParametricBour()
    pfn['CatalanMinimal'] = vtkParametricCatalanMinimal()
    pfn['Henneberg'] = vtkParametricHenneberg()
    pfn['Kuen'] = vtkParametricKuen()
    pfn['PluckerConoid'] = vtkParametricPluckerConoid()
    pfn['Pseudosphere'] = vtkParametricPseudosphere()
    # Now set some parameters.
    pfn["Ellipsoid"].SetXRadius(0.5)
    pfn["Ellipsoid"].SetYRadius(2.0)
    pfn["Mobius"].SetRadius(2.0)
    pfn["Mobius"].SetMinimumV(-0.5)
    pfn["Mobius"].SetMaximumV(0.5)
    pfn["RandomHills"].AllowRandomGenerationOn()
    pfn["RandomHills"].SetRandomSeed(1)
    pfn["RandomHills"].SetNumberOfHills(30)
    pfn["SuperEllipsoid"].SetN1(0.5)
    pfn["SuperEllipsoid"].SetN2(0.4)
    pfn["SuperToroid"].SetN1(0.5)
    pfn["SuperToroid"].SetN2(3.0)
    # The spline needs points
    spline_points = vtkPoints()
    rng = vtkMinimalStandardRandomSequence()
    rng.SetSeed(8775070)
    for p in range(0, 10):
        xyz = [None] * 3
        for idx in range(0, len(xyz)):
            xyz[idx] = rng.GetRangeValue(-1.0, 1.0)
            rng.Next()
        spline_points.InsertNextPoint(xyz)

    pfn["Spline"].SetPoints(spline_points)
    # Extra parametric surfaces.
    pfn["BohemianDome"].SetA(5.0)
    pfn["BohemianDome"].SetB(1.0)
    pfn["BohemianDome"].SetC(2.0)
    pfn["Kuen"].SetDeltaV0(0.001)

    return pfn


def get_centre(bounds):
    """
    Get the centre of the object from the bounding box.

    :param bounds: The bounding box of the object.
    :return:
    """
    if len(bounds) != 6:
        return None
    return [bounds[i] - (bounds[i] - bounds[i - 1]) / 2.0 for i in range(1, len(bounds), 2)]


def get_maximum_length(bounds):
    """
    Calculate the maximum length of the side bounding box.

    :param bounds: The bounding box of the object.
    :return:
    """
    if len(bounds) != 6:
        return None
    return max([bounds[i] - bounds[i - 1] for i in range(1, len(bounds), 2)])


def display_bounding_box_and_center(name, bounds):
    """
    Display the dimensions of the bounding box, maximum diagonal length
     and coordinates of the centre.

    :param name: The name of the object.
    :param bounds: The bounding box of the object.
    :return:
    """
    if len(bounds) != 6:
        return
    max_len = get_maximum_length(bounds)
    centre = get_centre(bounds)
    s = '{:21s}\n'.format(name)
    s += '{:21s}{:1s}'.format('  Bounds (min, max)', ':')
    s += '{:s}({:6.2f}, {:6.2f})'.format(' x:', bounds[0], bounds[1])
    s += '{:s}({:6.2f}, {:6.2f})'.format(' y:', bounds[2], bounds[3])
    s += '{:s}({:6.2f}, {:6.2f})\n'.format(' z:', bounds[4], bounds[5])
    if max_len:
        s += '  Maximum side length: {:6.2f}\n'.format(max_len)
    if centre:
        s += '  Centre (x, y, z)   : ({:6.2f}, {:6.2f}, {:6.2f})\n'.format(centre[0], centre[1], centre[2])
    print(s)


class PrintCallback:
    def __init__(self, caller, file_name, image_quality=1, rgba=True):
        self.caller = caller
        self.image_quality = image_quality
        # rgba is the the buffer type,
        #  (if true, there is no background in the screenshot).
        self.rgba = rgba
        parent = Path(file_name).resolve().parent
        pth = Path(parent) / file_name
        self.path = Path(str(pth)).with_suffix('.png')

    def __call__(self, caller, ev):
        # Save the screenshot.
        if caller.GetKeyCode() == "k":
            w2if = vtkWindowToImageFilter()
            w2if.SetInput(caller.GetRenderWindow())
            w2if.SetScale(self.image_quality, self.image_quality)
            if self.rgba:
                w2if.SetInputBufferTypeToRGBA()
            else:
                w2if.SetInputBufferTypeToRGB()
            # Read from the front buffer.
            w2if.ReadFrontBufferOn()
            w2if.Update()
            writer = vtkPNGWriter()
            writer.SetFileName(self.path)
            writer.SetInputData(w2if.GetOutput())
            writer.Write()
            print('Screenshot saved to:', self.path.name)


if __name__ == '__main__':
    main()