2025, Dec 11 15:00
Matplotlib 3D Mesh Coloring: Map Per-Vertex Scalars to Face Colors with Poly3DCollection
Learn how to color a 3D triangle mesh in Matplotlib by per-vertex scalars: build face colors, normalize, apply a colormap, and render with Poly3DCollection.
Coloring a 3D triangle mesh by per-vertex scalars sounds straightforward, yet common plotting shortcuts don’t do exactly that. plot_trisurf focuses on z-based shading, and tripcolor operates in 2D. If the goal is a true 3D surface whose face color reflects your own scalar field defined on vertices, you need to explicitly build the mesh faces and assign colors derived from those scalars.
Problem setup
The data consists of 3D vertex coordinates (x, y, z), a scalar per vertex, and triangle connectivity describing which vertices form each face. Rendering geometry alone is easy, but the missing piece is mapping the scalar field to face colors in 3D.
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d.art3d import Poly3DCollection as Mesh3D
# Vertex positions
xs = np.array([0, 1, 0, 1])
ys = np.array([0, 0, 1, 1])
zs = np.array([0, 0, 0, 1])
vals = np.array([0.1, 0.5, 0.9, 0.6]) # per-vertex scalar
# Triangle connectivity by vertex index
faces_idx = np.array([
[0, 1, 2],
[1, 2, 3]
])
# Build triangle vertex triplets
poly_pts = [list(zip(xs[t], ys[t], zs[t])) for t in faces_idx]
# Basic plot: geometry only, not colored by the scalars yet
canvas = plt.figure()
axes3d = canvas.add_subplot(111, projection='3d')
surf = Mesh3D(poly_pts, facecolors='lightgray', edgecolors='k', linewidths=0.5)
axes3d.add_collection3d(surf)
axes3d.auto_scale_xyz(xs, ys, zs)
plt.show()This draws the mesh, but the faces are not colored by the vertex scalar field. That is the gap we need to close.
What’s actually missing
The geometry pipeline alone doesn’t know how to translate per-vertex values into face colors. tripcolor can color triangles but only in a 2D projection, while plot_trisurf is tied to z-based shading. To color a 3D surface by your own scalar, you must convert vertex-level scalars to a per-face color and pass these colors into a 3D polygon collection.
A simple and effective choice is to compute a representative value per face, for example the average of the three vertex scalars, normalize these values, map them through a colormap, and use the resulting RGBA colors for the faces.
Solution: build face colors from vertex scalars
The following code constructs the triangles, computes an average scalar per face, normalizes it, converts to RGBA via a colormap, and renders the colored surface. It also adds a colorbar for reference.
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d.art3d import Poly3DCollection as Mesh3D
from matplotlib import cm as cms
# Vertex positions
xs = np.array([0, 1, 0, 1])
ys = np.array([0, 0, 1, 1])
zs = np.array([0, 0, 0, 1])
vals = np.array([0.1, 0.5, 0.9, 0.6]) # per-vertex scalar
# Triangle connectivity by vertex index
faces_idx = np.array([
[0, 1, 2],
[1, 2, 3]
])
# Build triangle vertex triplets
poly_pts = [list(zip(xs[t], ys[t], zs[t])) for t in faces_idx]
# Aggregate per-face scalar (mean of three vertex values)
face_vals = np.mean(vals[faces_idx], axis=1)
# Normalize and map to RGBA via a colormap
scaler = plt.Normalize(vals.min(), vals.max())
face_rgba = cms.viridis(scaler(face_vals))
# Plot
canvas = plt.figure()
axes3d = canvas.add_subplot(111, projection='3d')
surf = Mesh3D(poly_pts, facecolors=face_rgba, edgecolors='k', linewidths=0.5)
axes3d.add_collection3d(surf)
axes3d.auto_scale_xyz(xs, ys, zs)
plt.colorbar(cms.ScalarMappable(norm=scaler, cmap='viridis'), ax=axes3d, label='Scalar value')
plt.show()Here, the face color is the colormap evaluation of the per-face scalar, and the face scalar is defined as the average of the three vertex scalars. The mapping is explicit and under your control.
Why this approach matters
Explicit color mapping ensures the visual encodings match the data semantics. Instead of letting a plotting routine infer colors from z or from a 2D projection, you define exactly how to derive face color from per-vertex values and how to normalize and map those values. That removes ambiguity and makes your 3D surface visualization faithful to the scalar field you actually care about.
Takeaways
Build face polygons from your triangle indices, reduce per-vertex scalars to a per-face value, normalize within the scalar range, and apply a colormap to obtain RGBA face colors. With Poly3DCollection, this gives a straightforward, reproducible way to render a 3D mesh whose faces reflect the data field defined on its vertices.