1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244
|
"""
.. _texture_example:
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)
# %%
# But what if your dataset doesn't have texture coordinates? Then you can
# harness the :func:`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)
# %%
# 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)
# %%
# Note that this process can be completed with any image texture.
# use the puppy image
tex = examples.download_puppy_texture()
curvsurf.plot(texture=tex)
# %%
# Textures from Files
# +++++++++++++++++++
#
# What about loading your own texture from an image? This is often most easily
# done using the :func:`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)
# %%
# NumPy Arrays as Textures
# ++++++++++++++++++++++++
#
# Want to use a programmatically built image? :class:`pyvista.ImageData`
# objects can be converted to textures using :func:`pyvista.image_to_texture`
# and 3D NumPy (X by Y by RGB) arrays can be converted to textures using
# :func:`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)
# %%
# 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()
# %%
# 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]
# :func:`pyvista.numpy_to_texture`.
#
# Here we can download an image that has an alpha channel:
rgba = examples.download_rgba_texture()
rgba.n_components
# %%
# Render it
curvsurf.plot(texture=rgba, show_grid=True)
# %%
# 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
# :attr:`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")
# %%
# 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)
# %%
# 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 <https://github.com/pyvista/pyvista-support/issues/257>`_
#
# 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)
|