Note
Go to the end to download the full example as a Python script or as a Jupyter notebook.
Surface with multiple textures#
This example demonstrates one possible method for displaying a 3D surface with multiple textures.
Thanks to Emmanuel Reynaud and Luis Gutierrez for providing the gorgeous coral model for this demo. You can find the data on FigShare: https://zenodo.org/records/13380203
More information on the methods used to generate this model can be found in L. Gutierrez-Heredia, C. Keogh, E. G. Reynaud, Assessing the Capabilities of Additive Manufacturing Technologies for Coral Studies, Education, and Monitoring. Front. Mar. Sci. 5 (2018), doi:10.3389/fmars.2018.00278.
A bit about 3D models#
A standard way to define a 3D model (mesh, or Surface in napari) is by listing vertices (3D point coordinates) and faces (triplets of vertex indices - each face is a triangle in 3D space). Meshes are often stored in “Wavefront” (.obj) files, which may have companion material (.mtl) files that describe some shading properties (base color, shinyness, etc.) for different parts of the model.
In some cases, the color of a vertex is given by a single point value that is then colormapped on the fly (vertex_values). In other cases, each vertex or face may be assigned a specific color (vertex_colors). These methods are demonstrated in Surface with texture and vertex_colors.
In the case of “photorealistic” models, the color of each vertex is instead determined by mapping a vertex to a point in an image called a texture using 2D texture coordinates in the range [0, 1]. The color of each individual pixel is smoothly interpolated (sampled) on the fly from the texture (the GPU makes this interpolation very fast).
Napari does not (yet) support models with multiple textures or materials. If the textures don’t overlap, you can display them on separate meshes as shown in this demo. If the textures do overlap, you may instead be able to combine the textures as images. This relies on textures having the same texture coordinates, and may require resizing the textures to match each other.
import os
import matplotlib.pyplot as plt
import pooch
from vispy.io import imread, read_mesh
import napari
Download the model#
download = pooch.DOIDownloader(progressbar=True)
doi = '10.5281/zenodo.13380203'
tmp_dir = pooch.os_cache('napari-surface-texture-example')
os.makedirs(tmp_dir, exist_ok=True)
data_files = {
'mesh': 'PocilloporaDamicornisSkin.obj',
# "materials": "PocilloporaVerrugosaSkinCrop.mtl", # not yet supported
'Texture_0': 'PocilloporaDamicornisSkin_Texture_0.jpg',
'GeneratedMat2': 'PocilloporaDamicornisSkin_GeneratedMat2.png',
}
print(f'downloading data into {tmp_dir}')
for file_name in data_files.values():
if not (tmp_dir / file_name).exists():
print(f'downloading {file_name}')
download(
f'doi:{doi}/{file_name}',
output_file=tmp_dir / file_name,
pooch=None,
)
else:
print(f'using cached {tmp_dir / file_name}')
downloading data into /home/runner/.cache/napari-surface-texture-example
downloading PocilloporaDamicornisSkin.obj
0%| | 0.00/93.8M [00:00<?, ?B/s]
0%| | 41.0k/93.8M [00:00<04:39, 335kB/s]
0%| | 135k/93.8M [00:00<03:07, 501kB/s]
1%|▏ | 478k/93.8M [00:00<01:03, 1.47MB/s]
1%|▍ | 1.23M/93.8M [00:00<00:29, 3.09MB/s]
3%|▉ | 2.52M/93.8M [00:00<00:16, 5.68MB/s]
6%|██▏ | 5.45M/93.8M [00:00<00:07, 12.4MB/s]
9%|███▍ | 8.80M/93.8M [00:00<00:04, 17.8MB/s]
13%|████▊ | 12.2M/93.8M [00:00<00:03, 22.3MB/s]
16%|██████ | 15.3M/93.8M [00:01<00:03, 22.9MB/s]
20%|███████▎ | 18.6M/93.8M [00:01<00:02, 25.7MB/s]
23%|████████▌ | 21.7M/93.8M [00:01<00:02, 26.4MB/s]
27%|█████████▊ | 24.9M/93.8M [00:01<00:02, 28.0MB/s]
30%|███████████ | 28.1M/93.8M [00:01<00:02, 29.2MB/s]
33%|████████████▎ | 31.1M/93.8M [00:01<00:02, 28.7MB/s]
37%|█████████████▌ | 34.3M/93.8M [00:01<00:02, 29.7MB/s]
40%|██████████████▊ | 37.5M/93.8M [00:01<00:01, 30.4MB/s]
43%|████████████████ | 40.6M/93.8M [00:01<00:01, 28.4MB/s]
47%|█████████████████▎ | 43.9M/93.8M [00:02<00:01, 29.5MB/s]
50%|██████████████████▌ | 47.0M/93.8M [00:02<00:01, 29.1MB/s]
54%|███████████████████▊ | 50.4M/93.8M [00:02<00:01, 30.3MB/s]
57%|█████████████████████▏ | 53.6M/93.8M [00:02<00:01, 29.8MB/s]
61%|██████████████████████▍ | 56.9M/93.8M [00:02<00:01, 30.7MB/s]
64%|███████████████████████▊ | 60.2M/93.8M [00:02<00:01, 31.5MB/s]
68%|█████████████████████████ | 63.4M/93.8M [00:02<00:01, 29.1MB/s]
71%|██████████████████████████▎ | 66.7M/93.8M [00:02<00:00, 30.2MB/s]
75%|███████████████████████████▌ | 69.9M/93.8M [00:02<00:00, 30.0MB/s]
78%|████████████████████████████▉ | 73.3M/93.8M [00:03<00:00, 31.1MB/s]
82%|██████████████████████████████▏ | 76.6M/93.8M [00:03<00:00, 31.8MB/s]
85%|███████████████████████████████▍ | 79.8M/93.8M [00:03<00:00, 31.0MB/s]
89%|████████████████████████████████▊ | 83.1M/93.8M [00:03<00:00, 31.6MB/s]
92%|██████████████████████████████████ | 86.4M/93.8M [00:03<00:00, 32.0MB/s]
96%|███████████████████████████████████▎ | 89.7M/93.8M [00:03<00:00, 29.4MB/s]
99%|████████████████████████████████████▋| 92.9M/93.8M [00:03<00:00, 30.4MB/s]
0%| | 0.00/93.8M [00:00<?, ?B/s]
100%|██████████████████████████████████████| 93.8M/93.8M [00:00<00:00, 514GB/s]
downloading PocilloporaDamicornisSkin_Texture_0.jpg
0%| | 0.00/17.3M [00:00<?, ?B/s]
0%| | 19.5k/17.3M [00:00<01:52, 153kB/s]
1%|▎ | 113k/17.3M [00:00<00:39, 437kB/s]
2%|▋ | 298k/17.3M [00:00<00:19, 871kB/s]
6%|██▏ | 1.01M/17.3M [00:00<00:05, 2.88MB/s]
12%|████▍ | 2.08M/17.3M [00:00<00:02, 5.38MB/s]
22%|████████ | 3.78M/17.3M [00:00<00:01, 8.97MB/s]
39%|██████████████▍ | 6.72M/17.3M [00:00<00:00, 15.3MB/s]
55%|████████████████████▍ | 9.58M/17.3M [00:00<00:00, 19.3MB/s]
72%|██████████████████████████▍ | 12.4M/17.3M [00:01<00:00, 20.3MB/s]
89%|█████████████████████████████████ | 15.4M/17.3M [00:01<00:00, 23.2MB/s]
0%| | 0.00/17.3M [00:00<?, ?B/s]
100%|█████████████████████████████████████| 17.3M/17.3M [00:00<00:00, 76.9GB/s]
downloading PocilloporaDamicornisSkin_GeneratedMat2.png
0%| | 0.00/120k [00:00<?, ?B/s]
35%|█████████████▋ | 42.0k/120k [00:00<00:00, 370kB/s]
0%| | 0.00/120k [00:00<?, ?B/s]
100%|████████████████████████████████████████| 120k/120k [00:00<00:00, 335MB/s]
Load the model#
Next, read the model data from the .obj file. Currently napari/vispy do not support reading material properties (.mtl files) nor separate texture and vertex indices (i.e. repeated vertices). Normal vectors read from the file are also ignored and re-calculated from the faces.
Load the textures#
This model comes with two textures: Texture_0 is generated from photogrammetry of the actual object, and GeneratedMat2 is a generated material to fill in parts of the model lacking photographic texture.
photo_texture = imread(tmp_dir / data_files['Texture_0'])
generated_texture = imread(tmp_dir / data_files['GeneratedMat2'])
This is what the texture images look like in 2D:
fig, axs = plt.subplots(1, 2)
axs[0].set_title(f'Texture_0 {photo_texture.shape}')
axs[0].imshow(photo_texture)
axs[0].set_xticks((0, photo_texture.shape[1]), labels=(0.0, 1.0))
axs[0].set_yticks((0, photo_texture.shape[0]), labels=(0.0, 1.0))
axs[1].set_title(f'GeneratedMat2 {generated_texture.shape}')
axs[1].imshow(generated_texture)
axs[1].set_xticks((0, generated_texture.shape[1]), labels=(0.0, 1.0))
axs[1].set_yticks((0, generated_texture.shape[0]), labels=(0.0, 1.0))
fig.show()
Create the napari layers#
Next create two separate layers with the same mesh - once with each texture. In this example the texture coordinates happen to be the same for each texture, but this is not a strict requirement.
photo_texture_layer = napari.layers.Surface(
(vertices, faces),
texture=photo_texture,
texcoords=texcoords,
name='Texture_0',
)
generated_texture_layer = napari.layers.Surface(
(vertices, faces),
texture=generated_texture,
texcoords=texcoords,
name='GeneratedMat2',
)
Add the layers to a viewer#
Finally, create the viewer and add the Surface layers. sphinx_gallery_thumbnail_number = 2
viewer = napari.Viewer(ndisplay=3)
viewer.add_layer(photo_texture_layer)
viewer.add_layer(generated_texture_layer)
viewer.camera.angles = (90.0, 0.0, -75.0)
viewer.camera.zoom = 75
if __name__ == '__main__':
napari.run()
Total running time of the script: (0 minutes 28.926 seconds)