Applying Textures#

Plot a mesh with an image projected onto it as a texture.

from __future__ import annotations

from matplotlib.pyplot import get_cmap
import numpy as np

import pyvista as pv
from pyvista import examples

Texture mapping is easily implemented using PyVista. Many of the geometric objects come preloaded with texture coordinates, so quickly creating a surface and displaying an image is simply:

# load a sample texture
tex = examples.download_masonry_texture()

# create a surface to host this texture
surf = pv.Cylinder()

surf.plot(texture=tex)
texture

But what if your dataset doesn’t have texture coordinates? Then you can harness the pyvista.DataSetFilters.texture_map_to_plane() filter to properly map an image to a dataset’s surface. For example, let’s map that same image of bricks to a curvey surface:

# create a structured surface
x = np.arange(-10, 10, 0.25)
y = np.arange(-10, 10, 0.25)
x, y = np.meshgrid(x, y)
r = np.sqrt(x**2 + y**2)
z = np.sin(r)
curvsurf = pv.StructuredGrid(x, y, z)

# Map the curved surface to a plane - use best fitting plane
curvsurf.texture_map_to_plane(inplace=True)

curvsurf.plot(texture=tex)
texture

Display scalar data along with a texture by ensuring the interpolate_before_map setting is False and specifying both the texture and scalars arguments.

elevated = curvsurf.elevation()

elevated.plot(scalars='Elevation', cmap='terrain', texture=tex, interpolate_before_map=False)
texture

Note that this process can be completed with any image texture.

# use the puppy image
tex = examples.download_puppy_texture()
curvsurf.plot(texture=tex)
texture

Textures from Files#

What about loading your own texture from an image? This is often most easily done using the pyvista.read_texture() function - simply pass an image file’s path, and this function with handle making a vtkTexture for you to use.

image_file = examples.mapfile
tex = pv.read_texture(image_file)
curvsurf.plot(texture=tex)
texture

NumPy Arrays as Textures#

Want to use a programmatically built image? pyvista.ImageData objects can be converted to textures using pyvista.image_to_texture() and 3D NumPy (X by Y by RGB) arrays can be converted to textures using pyvista.numpy_to_texture().

# create an image using numpy,
xx, yy = np.meshgrid(np.linspace(-200, 200, 20), np.linspace(-200, 200, 20))
A, b = 500, 100
zz = A * np.exp(-0.5 * ((xx / b) ** 2.0 + (yy / b) ** 2.0))

# Creating a custom RGB image
cmap = get_cmap("nipy_spectral")
norm = lambda x: (x - np.nanmin(x)) / (np.nanmax(x) - np.nanmin(x))
hue = norm(zz.ravel())
colors = (cmap(hue)[:, 0:3] * 255.0).astype(np.uint8)
image = colors.reshape((xx.shape[0], xx.shape[1], 3), order="F")

# Convert 3D numpy array to texture
tex = pv.numpy_to_texture(image)

# Render it
curvsurf.plot(texture=tex)
texture

Create a GIF Movie with updating textures#

Generate a moving gif from an active plotter with updating textures.

mesh = curvsurf.extract_surface()

# Create a plotter object
plotter = pv.Plotter(notebook=False, off_screen=True)

actor = plotter.add_mesh(mesh, smooth_shading=True, color="white")

# Open a gif
plotter.open_gif("texture.gif")

# Update Z and write a frame for each updated position
nframe = 15
for phase in np.linspace(0, 2 * np.pi, nframe + 1)[:nframe]:
    # create an image using numpy,
    z = np.sin(r + phase)
    mesh.points[:, -1] = z.ravel()

    # Creating a custom RGB image
    zz = A * np.exp(-0.5 * ((xx / b) ** 2.0 + (yy / b) ** 2.0))
    hue = norm(zz.ravel()) * 0.5 * (1.0 + np.sin(phase))
    colors = (cmap(hue)[:, 0:3] * 255.0).astype(np.uint8)
    image = colors.reshape((xx.shape[0], xx.shape[1], 3), order="F")

    # Convert 3D numpy array to texture
    actor.texture = pv.numpy_to_texture(image)

    # must update normals when smooth shading is enabled
    mesh.compute_normals(cell_normals=False, inplace=True)
    plotter.write_frame()
    plotter.clear()

# Closes and finalizes movie
plotter.close()
texture

Textures with Transparency#

Textures can also specify per-pixel opacity values. The image must contain a 4th channel specifying the opacity value from 0 [transparent] to 255 [fully visible]. To enable this feature just pass the opacity array as the 4th channel of the image as a 3 dimensional matrix with shape [nrows, ncols, 4] pyvista.numpy_to_texture().

Here we can download an image that has an alpha channel:

rgba = examples.download_rgba_texture()
rgba.n_components
4
# Render it
curvsurf.plot(texture=rgba, show_grid=True)
texture

Repeating Textures#

What if you have a single texture that you’d like to repeat across a mesh? Simply define the texture coordinates for all nodes explicitly.

Here we create the texture coordinates to fill up the grid with several mappings of a single texture. In order to do this we must define texture coordinates outside of the typical (0, 1) range:

axial_num_puppies = 4
xc = np.linspace(0, axial_num_puppies, curvsurf.dimensions[0])
yc = np.linspace(0, axial_num_puppies, curvsurf.dimensions[1])

xxc, yyc = np.meshgrid(xc, yc)
puppy_coords = np.c_[yyc.ravel(), xxc.ravel()]

By defining texture coordinates that range (0, 4) on each axis, we will produce 4 repetitions of the same texture on this mesh.

Then we must associate those texture coordinates with the mesh through the pyvista.DataSet.active_texture_coordinates property.

curvsurf.active_texture_coordinates = puppy_coords

Now display all the puppies.

# use the puppy image
tex = examples.download_puppy_texture()
curvsurf.plot(texture=tex, cpos="xy")
texture

Spherical Texture Coordinates#

We have a built in convienance method for mapping textures to spherical coordinate systems much like the planar mapping demoed above.

mesh = pv.Sphere()
tex = examples.download_masonry_texture()

mesh.texture_map_to_sphere(inplace=True)
mesh.plot(texture=tex)
texture

The helper method above does not always produce the desired texture coordinates, so sometimes it must be done manually. Here is a great, user contributed example from this support issue

Manually create the texture coordinates for a globe map. First, we create the mesh that will be used as the globe. Note the start_theta for a slight overlappig

sphere = pv.Sphere(
    radius=1,
    theta_resolution=120,
    phi_resolution=120,
    start_theta=270.001,
    end_theta=270,
)

# Initialize the texture coordinates array
sphere.active_texture_coordinates = np.zeros((sphere.points.shape[0], 2))

# Populate by manually calculating
for i in range(sphere.points.shape[0]):
    sphere.active_texture_coordinates[i] = [
        0.5 + np.arctan2(-sphere.points[i, 0], sphere.points[i, 1]) / (2 * np.pi),
        0.5 + np.arcsin(sphere.points[i, 2]) / np.pi,
    ]

# And let's display it with a world map
tex = examples.load_globe_texture()
sphere.plot(texture=tex)
texture

Total running time of the script: (0 minutes 7.577 seconds)

Gallery generated by Sphinx-Gallery