Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit d9d75f2

Browse files
committed
Refactor shading
1 parent 14c50c8 commit d9d75f2

File tree

3 files changed

+166
-124
lines changed

3 files changed

+166
-124
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
``Poly3DCollection`` supports shading
2+
-------------------------------------
3+
4+
It is now possible to shade a `.Poly3DCollection`. This is useful if the
5+
polygons are obtained from e.g. a 3D model.
6+
7+
.. plot::
8+
:include-source: true
9+
10+
import numpy as np
11+
import matplotlib.pyplot as plt
12+
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
13+
14+
# Define 3D shape
15+
block = np.array([
16+
[[1, 1, 0],
17+
[1, 0, 0],
18+
[0, 1, 0]],
19+
[[1, 1, 0],
20+
[1, 1, 1],
21+
[1, 0, 0]],
22+
[[1, 1, 0],
23+
[1, 1, 1],
24+
[0, 1, 0]],
25+
[[1, 0, 0],
26+
[1, 1, 1],
27+
[0, 1, 0]]
28+
])
29+
30+
ax = plt.subplot(projection='3d')
31+
pc = Poly3DCollection(block, facecolors='b', shade=True)
32+
ax.add_collection(pc)
33+
plt.show()

lib/mpl_toolkits/mplot3d/art3d.py

Lines changed: 113 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+
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,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

Comments
 (0)