Refactor shading · matplotlib/matplotlib@ca6e1c6 · GitHub
Skip to content

Commit ca6e1c6

Browse files
committed
Refactor shading
1 parent f6e7512 commit ca6e1c6

3 files changed

Lines changed: 163 additions & 121 deletions

File tree

Lines changed: 33 additions & 0 deletions

lib/mpl_toolkits/mplot3d/art3d.py

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -808,7 +808,8 @@ class Poly3DCollection(PolyCollection):
808808
triangulation and thus generates consistent surfaces.
809809
"""
810810

811-
def __init__(self, verts, *args, zsort='average', **kwargs):
811+
def __init__(self, verts, *args, zsort='average', shade=False,
812+
lightsource=None, **kwargs):
812813
"""
813814
Parameters
814815
----------
@@ -819,6 +820,20 @@ def __init__(self, verts, *args, zsort='average', **kwargs):
819820
zsort : {'average', 'min', 'max'}, default: 'average'
820821
The calculation method for the z-order.
821822
See `~.Poly3DCollection.set_zsort` for details.
823+
shade : bool, default: False
824+
Whether to shade *facecolors* and *edgecolors*.
825+
826+
.. versionadded:: 3.7
827+
828+
.. note::
829+
*facecolors* and/or *edgecolors* must be provided for shading
830+
to work.
831+
832+
lightsource : `~matplotlib.colors.LightSource`
833+
The lightsource to use when *shade* is True.
834+
835+
.. versionadded:: 3.7
836+
822837
*args, **kwargs
823838
All other parameters are forwarded to `.PolyCollection`.
824839
@@ -827,6 +842,20 @@ def __init__(self, verts, *args, zsort='average', **kwargs):
827842
Note that this class does a bit of magic with the _facecolors
828843
and _edgecolors properties.
829844
"""
845+
if shade:
846+
normals = _generate_normals(verts)
847+
facecolor = kwargs.get('facecolors', None)
848+
if facecolor is not None:
849+
kwargs['facecolors'] = _shade_colors(
850+
facecolor, normals, lightsource
851+
)
852+
853+
edgecolor = kwargs.get('edgecolors', None)
854+
if edgecolor is not None:
855+
kwargs['edgecolors'] = _shade_colors(
856+
edgecolor, normals, lightsource
857+
)
858+
830859
super().__init__(verts, *args, **kwargs)
831860
if isinstance(verts, np.ndarray):
832861
if verts.ndim != 3:
@@ -1086,3 +1115,84 @@ def _zalpha(colors, zs):
10861115
sats = 1 - norm(zs) * 0.7
10871116
rgba = np.broadcast_to(mcolors.to_rgba_array(colors), (len(zs), 4))
10881117
return np.column_stack([rgba[:, :3], rgba[:, 3] * sats])
1118+
1119+
1120+
def _generate_normals(polygons):
1121+
"""
1122+
Compute the normals of a list of polygons.
1123+
1124+
Normals point towards the viewer for a face with its vertices in
1125+
counterclockwise order, following the right hand rule.
1126+
1127+
Uses three points equally spaced around the polygon.
1128+
This normal of course might not make sense for polygons with more than
1129+
three points not lying in a plane, but it's a plausible and fast
1130+
approximation.
1131+
1132+
Parameters
1133+
----------
1134+
polygons : list of (M_i, 3) array-like, or (..., M, 3) array-like
1135+
A sequence of polygons to compute normals for, which can have
1136+
varying numbers of vertices. If the polygons all have the same
1137+
number of vertices and array is passed, then the operation will
1138+
be vectorized.
1139+
1140+
Returns
1141+
-------
1142+
normals : (..., 3) array
1143+
A normal vector estimated for the polygon.
1144+
"""
1145+
if isinstance(polygons, np.ndarray):
1146+
# optimization: polygons all have the same number of points, so can
1147+
# vectorize
1148+
n = polygons.shape[-2]
1149+
i1, i2, i3 = 0, n//3, 2*n//3
1150+
v1 = polygons[..., i1, :] - polygons[..., i2, :]
1151+
v2 = polygons[..., i2, :] - polygons[..., i3, :]
1152+
else:
1153+
# The subtraction doesn't vectorize because polygons is jagged.
1154+
v1 = np.empty((len(polygons), 3))
1155+
v2 = np.empty((len(polygons), 3))
1156+
for poly_i, ps in enumerate(polygons):
1157+
n = len(ps)
1158+
i1, i2, i3 = 0, n//3, 2*n//3
1159+
v1[poly_i, :] = ps[i1, :] - ps[i2, :]
1160+
v2[poly_i, :] = ps[i2, :] - ps[i3, :]
1161+
return np.cross(v1, v2)
1162+
1163+
1164+
def _shade_colors(color, normals, lightsource=None):
1165+
"""
1166+
Shade *color* using normal vectors given by *normals*.
1167+
*color* can also be an array of the same length as *normals*.
1168+
"""
1169+
if lightsource is None:
1170+
# chosen for backwards-compatibility
1171+
lightsource = mcolors.LightSource(azdeg=225, altdeg=19.4712)
1172+
1173+
with np.errstate(invalid="ignore"):
1174+
shade = ((normals / np.linalg.norm(normals, axis=1, keepdims=True))
1175+
@ lightsource.direction)
1176+
mask = ~np.isnan(shade)
1177+
1178+
if mask.any():
1179+
# convert dot product to allowed shading fractions
1180+
in_norm = mcolors.Normalize(-1, 1)
1181+
out_norm = mcolors.Normalize(0.3, 1).inverse
1182+
1183+
def norm(x):
1184+
return out_norm(in_norm(x))
1185+
1186+
shade[~mask] = 0
1187+
1188+
color = mcolors.to_rgba_array(color)
1189+
# shape of color should be (M, 4) (where M is number of faces)
1190+
# shape of shade should be (M,)
1191+
# colors should have final shape of (M, 4)
1192+
alpha = color[:, 3]
1193+
colors = norm(shade)[:, np.newaxis] * color
1194+
colors[:, 3] = alpha
1195+
else:
1196+
colors = np.asanyarray(color).copy()
1197+
1198+
return colors

lib/mpl_toolkits/mplot3d/axes3d.py

Lines changed: 19 additions & 120 deletions

0 commit comments

Comments
 (0)