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

Skip to content

Commit 5bee4ad

Browse files
committed
Refactor shading
1 parent 54360f3 commit 5bee4ad

File tree

3 files changed

+169
-124
lines changed

3 files changed

+169
-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: 116 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,23 @@ 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 shade:
847+
normals = _generate_normals(verts)
848+
facecolors = kwargs.get('facecolors', None)
849+
if facecolors is not None:
850+
kwargs['facecolors'] = _shade_colors(
851+
facecolors, normals, lightsource
852+
)
853+
854+
edgecolors = kwargs.get('edgecolors', None)
855+
if edgecolors is not None:
856+
kwargs['edgecolors'] = _shade_colors(
857+
edgecolors, normals, lightsource
858+
)
859+
if facecolors is None and edgecolors in None:
860+
_api.warn_external(
861+
"You must provide facecolors, edgecolors, or both for "
862+
"shade to work as expected.")
830863
super().__init__(verts, *args, **kwargs)
831864
if isinstance(verts, np.ndarray):
832865
if verts.ndim != 3:
@@ -1086,3 +1119,84 @@ def _zalpha(colors, zs):
10861119
sats = 1 - norm(zs) * 0.7
10871120
rgba = np.broadcast_to(mcolors.to_rgba_array(colors), (len(zs), 4))
10881121
return np.column_stack([rgba[:, :3], rgba[:, 3] * sats])
1122+
1123+
1124+
def _generate_normals(polygons):
1125+
"""
1126+
Compute the normals of a list of polygons, one normal per polygon.
1127+
1128+
Normals point towards the viewer for a face with its vertices in
1129+
counterclockwise order, following the right hand rule.
1130+
1131+
Uses three points equally spaced around the polygon. This method assumes
1132+
that the points are in a plane. Otherwise, more than one shade is required,
1133+
which is not supported.
1134+
1135+
Parameters
1136+
----------
1137+
polygons : list of (M_i, 3) array-like, or (..., M, 3) array-like
1138+
A sequence of polygons to compute normals for, which can have
1139+
varying numbers of vertices. If the polygons all have the same
1140+
number of vertices and array is passed, then the operation will
1141+
be vectorized.
1142+
1143+
Returns
1144+
-------
1145+
normals : (..., 3) array
1146+
A normal vector estimated for the polygon.
1147+
"""
1148+
if isinstance(polygons, np.ndarray):
1149+
# optimization: polygons all have the same number of points, so can
1150+
# vectorize
1151+
n = polygons.shape[-2]
1152+
i1, i2, i3 = 0, n//3, 2*n//3
1153+
v1 = polygons[..., i1, :] - polygons[..., i2, :]
1154+
v2 = polygons[..., i2, :] - polygons[..., i3, :]
1155+
else:
1156+
# The subtraction doesn't vectorize because polygons is jagged.
1157+
v1 = np.empty((len(polygons), 3))
1158+
v2 = np.empty((len(polygons), 3))
1159+
for poly_i, ps in enumerate(polygons):
1160+
n = len(ps)
1161+
i1, i2, i3 = 0, n//3, 2*n//3
1162+
v1[poly_i, :] = ps[i1, :] - ps[i2, :]
1163+
v2[poly_i, :] = ps[i2, :] - ps[i3, :]
1164+
return np.cross(v1, v2)
1165+
1166+
1167+
def _shade_colors(color, normals, lightsource=None):
1168+
"""
1169+
Shade *color* using normal vectors given by *normals*,
1170+
assuming a *lightsource* (using default position if not given).
1171+
*color* can also be an array of the same length as *normals*.
1172+
"""
1173+
if lightsource is None:
1174+
# chosen for backwards-compatibility
1175+
lightsource = mcolors.LightSource(azdeg=225, altdeg=19.4712)
1176+
1177+
with np.errstate(invalid="ignore"):
1178+
shade = ((normals / np.linalg.norm(normals, axis=1, keepdims=True))
1179+
@ lightsource.direction)
1180+
mask = ~np.isnan(shade)
1181+
1182+
if mask.any():
1183+
# convert dot product to allowed shading fractions
1184+
in_norm = mcolors.Normalize(-1, 1)
1185+
out_norm = mcolors.Normalize(0.3, 1).inverse
1186+
1187+
def norm(x):
1188+
return out_norm(in_norm(x))
1189+
1190+
shade[~mask] = 0
1191+
1192+
color = mcolors.to_rgba_array(color)
1193+
# shape of color should be (M, 4) (where M is number of faces)
1194+
# shape of shade should be (M,)
1195+
# colors should have final shape of (M, 4)
1196+
alpha = color[:, 3]
1197+
colors = norm(shade)[:, np.newaxis] * color
1198+
colors[:, 3] = alpha
1199+
else:
1200+
colors = np.asanyarray(color).copy()
1201+
1202+
return colors

0 commit comments

Comments
 (0)