Refactor shading · matplotlib/matplotlib@0c248a2 · GitHub
Skip to content

Commit 0c248a2

Browse files
committed
Refactor shading
1 parent f6e7512 commit 0c248a2

3 files changed

Lines changed: 173 additions & 122 deletions

File tree

Lines changed: 33 additions & 0 deletions

lib/mpl_toolkits/mplot3d/art3d.py

Lines changed: 121 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
import numpy as np
1313

1414
from matplotlib import (
15-
artist, cbook, colors as mcolors, lines, text as mtext, path as mpath)
15+
_api, artist, cbook, colors as mcolors, lines, text as mtext,
16+
path as mpath)
1617
from matplotlib.collections import (
1718
LineCollection, PolyCollection, PatchCollection, PathCollection)
1819
from matplotlib.colors import Normalize
@@ -808,7 +809,8 @@ class Poly3DCollection(PolyCollection):
808809
triangulation and thus generates consistent surfaces.
809810
"""
810811

811-
def __init__(self, verts, *args, zsort='average', **kwargs):
812+
def __init__(self, verts, *args, zsort='average', shade=False,
813+
lightsource=None, **kwargs):
812814
"""
813815
Parameters
814816
----------
@@ -819,6 +821,20 @@ def __init__(self, verts, *args, zsort='average', **kwargs):
819821
zsort : {'average', 'min', 'max'}, default: 'average'
820822
The calculation method for the z-order.
821823
See `~.Poly3DCollection.set_zsort` for details.
824+
shade : bool, default: False
825+
Whether to shade *facecolors* and *edgecolors*.
826+
827+
.. versionadded:: 3.7
828+
829+
.. note::
830+
*facecolors* and/or *edgecolors* must be provided for shading
831+
to work.
832+
833+
lightsource : `~matplotlib.colors.LightSource`
834+
The lightsource to use when *shade* is True.
835+
836+
.. versionadded:: 3.7
837+
822838
*args, **kwargs
823839
All other parameters are forwarded to `.PolyCollection`.
824840
@@ -827,6 +843,28 @@ def __init__(self, verts, *args, zsort='average', **kwargs):
827843
Note that this class does a bit of magic with the _facecolors
828844
and _edgecolors properties.
829845
"""
846+
if 'facecolor' in kwargs:
847+
kwargs['facecolors'] = kwargs.pop('facecolor')
848+
if 'edgecolor' in kwargs:
849+
kwargs['edgecolors'] = kwargs.pop('edgecolor')
850+
if shade:
851+
normals = _generate_normals(verts)
852+
facecolors = kwargs.get('facecolors', None)
853+
if facecolors is not None:
854+
kwargs['facecolors'] = _shade_colors(
855+
facecolors, normals, lightsource
856+
)
857+
858+
edgecolors = kwargs.get('edgecolors', None)
859+
if edgecolors is not None:
860+
kwargs['edgecolors'] = _shade_colors(
861+
edgecolors, normals, lightsource
862+
)
863+
if facecolors is None and edgecolors in None:
864+
_api.warn_external(
865+
"You must provide at least one of facecolors and "
866+
"edgecolors for shade to work as expected.")
867+
830868
super().__init__(verts, *args, **kwargs)
831869
if isinstance(verts, np.ndarray):
832870
if verts.ndim != 3:
@@ -1086,3 +1124,84 @@ def _zalpha(colors, zs):
10861124
sats = 1 - norm(zs) * 0.7
10871125
rgba = np.broadcast_to(mcolors.to_rgba_array(colors), (len(zs), 4))
10881126
return np.column_stack([rgba[:, :3], rgba[:, 3] * sats])
1127+
1128+
1129+
def _generate_normals(polygons):
1130+
"""
1131+
Compute the normals of a list of polygons, one normal per polygon.
1132+
1133+
Normals point towards the viewer for a face with its vertices in
1134+
counterclockwise order, following the right hand rule.
1135+
1136+
Uses three points equally spaced around the polygon.
1137+
If the polygon points are not in a plane, this does not make sense, but
1138+
it is on the other hand impossible to compute a single shade in that case.
1139+
1140+
Parameters
1141+
----------
1142+
polygons : list of (M_i, 3) array-like, or (..., M, 3) array-like
1143+
A sequence of polygons to compute normals for, which can have
1144+
varying numbers of vertices. If the polygons all have the same
1145+
number of vertices and array is passed, then the operation will
1146+
be vectorized.
1147+
1148+
Returns
1149+
-------
1150+
normals : (..., 3) array
1151+
A normal vector estimated for the polygon.
1152+
"""
1153+
if isinstance(polygons, np.ndarray):
1154+
# optimization: polygons all have the same number of points, so can
1155+
# vectorize
1156+
n = polygons.shape[-2]
1157+
i1, i2, i3 = 0, n//3, 2*n//3
1158+
v1 = polygons[..., i1, :] - polygons[..., i2, :]
1159+
v2 = polygons[..., i2, :] - polygons[..., i3, :]
1160+
else:
1161+
# The subtraction doesn't vectorize because polygons is jagged.
1162+
v1 = np.empty((len(polygons), 3))
1163+
v2 = np.empty((len(polygons), 3))
1164+
for poly_i, ps in enumerate(polygons):
1165+
n = len(ps)
1166+
i1, i2, i3 = 0, n//3, 2*n//3
1167+
v1[poly_i, :] = ps[i1, :] - ps[i2, :]
1168+
v2[poly_i, :] = ps[i2, :] - ps[i3, :]
1169+
return np.cross(v1, v2)
1170+
1171+
1172+
def _shade_colors(color, normals, lightsource=None):
1173+
"""
1174+
Shade *color* using normal vectors given by *normals*,
1175+
assuming a *lightsource* (using default position if not given).
1176+
*color* can also be an array of the same length as *normals*.
1177+
"""
1178+
if lightsource is None:
1179+
# chosen for backwards-compatibility
1180+
lightsource = mcolors.LightSource(azdeg=225, altdeg=19.4712)
1181+
1182+
with np.errstate(invalid="ignore"):
1183+
shade = ((normals / np.linalg.norm(normals, axis=1, keepdims=True))
1184+
@ lightsource.direction)
1185+
mask = ~np.isnan(shade)
1186+
1187+
if mask.any():
1188+
# convert dot product to allowed shading fractions
1189+
in_norm = mcolors.Normalize(-1, 1)
1190+
out_norm = mcolors.Normalize(0.3, 1).inverse
1191+
1192+
def norm(x):
1193+
return out_norm(in_norm(x))
1194+
1195+
shade[~mask] = 0
1196+
1197+
color = mcolors.to_rgba_array(color)
1198+
# shape of color should be (M, 4) (where M is number of faces)
1199+
# shape of shade should be (M,)
1200+
# colors should have final shape of (M, 4)
1201+
alpha = color[:, 3]
1202+
colors = norm(shade)[:, np.newaxis] * color
1203+
colors[:, 3] = alpha
1204+
else:
1205+
colors = np.asanyarray(color).copy()
1206+
1207+
return colors

lib/mpl_toolkits/mplot3d/axes3d.py

Lines changed: 19 additions & 120 deletions

0 commit comments

Comments
 (0)