1212import numpy as np
1313
1414from matplotlib import (
15- artist , cbook , colors as mcolors , lines , text as mtext , path as mpath )
15+ artist , cbook , colors as mcolors , lines , text as mtext ,
16+ path as mpath )
1617from matplotlib .collections import (
1718 LineCollection , PolyCollection , PatchCollection , PathCollection )
1819from 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,17 @@ 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*. When activating
826+ *shade*, *facecolors* and/or *edgecolors* must be provided.
827+
828+ .. versionadded:: 3.7
829+
830+ lightsource : `~matplotlib.colors.LightSource`
831+ The lightsource to use when *shade* is True.
832+
833+ .. versionadded:: 3.7
834+
822835 *args, **kwargs
823836 All other parameters are forwarded to `.PolyCollection`.
824837
@@ -827,6 +840,23 @@ def __init__(self, verts, *args, zsort='average', **kwargs):
827840 Note that this class does a bit of magic with the _facecolors
828841 and _edgecolors properties.
829842 """
843+ if shade :
844+ normals = _generate_normals (verts )
845+ facecolors = kwargs .get ('facecolors' , None )
846+ if facecolors is not None :
847+ kwargs ['facecolors' ] = _shade_colors (
848+ facecolors , normals , lightsource
849+ )
850+
851+ edgecolors = kwargs .get ('edgecolors' , None )
852+ if edgecolors is not None :
853+ kwargs ['edgecolors' ] = _shade_colors (
854+ edgecolors , normals , lightsource
855+ )
856+ if facecolors is None and edgecolors in None :
857+ raise ValueError (
858+ "You must provide facecolors, edgecolors, or both for "
859+ "shade to work." )
830860 super ().__init__ (verts , * args , ** kwargs )
831861 if isinstance (verts , np .ndarray ):
832862 if verts .ndim != 3 :
@@ -1086,3 +1116,84 @@ def _zalpha(colors, zs):
10861116 sats = 1 - norm (zs ) * 0.7
10871117 rgba = np .broadcast_to (mcolors .to_rgba_array (colors ), (len (zs ), 4 ))
10881118 return np .column_stack ([rgba [:, :3 ], rgba [:, 3 ] * sats ])
1119+
1120+
1121+ def _generate_normals (polygons ):
1122+ """
1123+ Compute the normals of a list of polygons, one normal per polygon.
1124+
1125+ Normals point towards the viewer for a face with its vertices in
1126+ counterclockwise order, following the right hand rule.
1127+
1128+ Uses three points equally spaced around the polygon. This method assumes
1129+ that the points are in a plane. Otherwise, more than one shade is required,
1130+ which is not supported.
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+ assuming a *lightsource* (using default position if not given).
1168+ *color* can also be an array of the same length as *normals*.
1169+ """
1170+ if lightsource is None :
1171+ # chosen for backwards-compatibility
1172+ lightsource = mcolors .LightSource (azdeg = 225 , altdeg = 19.4712 )
1173+
1174+ with np .errstate (invalid = "ignore" ):
1175+ shade = ((normals / np .linalg .norm (normals , axis = 1 , keepdims = True ))
1176+ @ lightsource .direction )
1177+ mask = ~ np .isnan (shade )
1178+
1179+ if mask .any ():
1180+ # convert dot product to allowed shading fractions
1181+ in_norm = mcolors .Normalize (- 1 , 1 )
1182+ out_norm = mcolors .Normalize (0.3 , 1 ).inverse
1183+
1184+ def norm (x ):
1185+ return out_norm (in_norm (x ))
1186+
1187+ shade [~ mask ] = 0
1188+
1189+ color = mcolors .to_rgba_array (color )
1190+ # shape of color should be (M, 4) (where M is number of faces)
1191+ # shape of shade should be (M,)
1192+ # colors should have final shape of (M, 4)
1193+ alpha = color [:, 3 ]
1194+ colors = norm (shade )[:, np .newaxis ] * color
1195+ colors [:, 3 ] = alpha
1196+ else :
1197+ colors = np .asanyarray (color ).copy ()
1198+
1199+ return colors
0 commit comments