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

Skip to content

Commit 0c248a2

Browse files
committed
Refactor shading
1 parent f6e7512 commit 0c248a2

File tree

3 files changed

+173
-122
lines changed

3 files changed

+173
-122
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: 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
Original file line numberDiff line numberDiff line change
@@ -1668,15 +1668,13 @@ def plot_surface(self, X, Y, Z, *, norm=None, vmin=None,
16681668

16691669
# note that the striding causes some polygons to have more coordinates
16701670
# than others
1671-
polyc = art3d.Poly3DCollection(polys, **kwargs)
16721671

16731672
if fcolors is not None:
1674-
if shade:
1675-
colset = self._shade_colors(
1676-
colset, self._generate_normals(polys), lightsource)
1677-
polyc.set_facecolors(colset)
1678-
polyc.set_edgecolors(colset)
1673+
polyc = art3d.Poly3DCollection(
1674+
polys, edgecolors=colset, facecolors=colset, shade=shade,
1675+
lightsource=lightsource, **kwargs)
16791676
elif cmap:
1677+
polyc = art3d.Poly3DCollection(polys, **kwargs)
16801678
# can't always vectorize, because polys might be jagged
16811679
if isinstance(polys, np.ndarray):
16821680
avg_z = polys[..., 2].mean(axis=-1)
@@ -1688,97 +1686,15 @@ def plot_surface(self, X, Y, Z, *, norm=None, vmin=None,
16881686
if norm is not None:
16891687
polyc.set_norm(norm)
16901688
else:
1691-
if shade:
1692-
colset = self._shade_colors(
1693-
color, self._generate_normals(polys), lightsource)
1694-
else:
1695-
colset = color
1696-
polyc.set_facecolors(colset)
1689+
polyc = art3d.Poly3DCollection(
1690+
polys, facecolors=color, shade=shade,
1691+
lightsource=lightsource, **kwargs)
16971692

16981693
self.add_collection(polyc)
16991694
self.auto_scale_xyz(X, Y, Z, had_data)
17001695

17011696
return polyc
17021697

1703-
def _generate_normals(self, polygons):
1704-
"""
1705-
Compute the normals of a list of polygons.
1706-
1707-
Normals point towards the viewer for a face with its vertices in
1708-
counterclockwise order, following the right hand rule.
1709-
1710-
Uses three points equally spaced around the polygon.
1711-
This normal of course might not make sense for polygons with more than
1712-
three points not lying in a plane, but it's a plausible and fast
1713-
approximation.
1714-
1715-
Parameters
1716-
----------
1717-
polygons : list of (M_i, 3) array-like, or (..., M, 3) array-like
1718-
A sequence of polygons to compute normals for, which can have
1719-
varying numbers of vertices. If the polygons all have the same
1720-
number of vertices and array is passed, then the operation will
1721-
be vectorized.
1722-
1723-
Returns
1724-
-------
1725-
normals : (..., 3) array
1726-
A normal vector estimated for the polygon.
1727-
"""
1728-
if isinstance(polygons, np.ndarray):
1729-
# optimization: polygons all have the same number of points, so can
1730-
# vectorize
1731-
n = polygons.shape[-2]
1732-
i1, i2, i3 = 0, n//3, 2*n//3
1733-
v1 = polygons[..., i1, :] - polygons[..., i2, :]
1734-
v2 = polygons[..., i2, :] - polygons[..., i3, :]
1735-
else:
1736-
# The subtraction doesn't vectorize because polygons is jagged.
1737-
v1 = np.empty((len(polygons), 3))
1738-
v2 = np.empty((len(polygons), 3))
1739-
for poly_i, ps in enumerate(polygons):
1740-
n = len(ps)
1741-
i1, i2, i3 = 0, n//3, 2*n//3
1742-
v1[poly_i, :] = ps[i1, :] - ps[i2, :]
1743-
v2[poly_i, :] = ps[i2, :] - ps[i3, :]
1744-
return np.cross(v1, v2)
1745-
1746-
def _shade_colors(self, color, normals, lightsource=None):
1747-
"""
1748-
Shade *color* using normal vectors given by *normals*.
1749-
*color* can also be an array of the same length as *normals*.
1750-
"""
1751-
if lightsource is None:
1752-
# chosen for backwards-compatibility
1753-
lightsource = mcolors.LightSource(azdeg=225, altdeg=19.4712)
1754-
1755-
with np.errstate(invalid="ignore"):
1756-
shade = ((normals / np.linalg.norm(normals, axis=1, keepdims=True))
1757-
@ lightsource.direction)
1758-
mask = ~np.isnan(shade)
1759-
1760-
if mask.any():
1761-
# convert dot product to allowed shading fractions
1762-
in_norm = mcolors.Normalize(-1, 1)
1763-
out_norm = mcolors.Normalize(0.3, 1).inverse
1764-
1765-
def norm(x):
1766-
return out_norm(in_norm(x))
1767-
1768-
shade[~mask] = 0
1769-
1770-
color = mcolors.to_rgba_array(color)
1771-
# shape of color should be (M, 4) (where M is number of faces)
1772-
# shape of shade should be (M,)
1773-
# colors should have final shape of (M, 4)
1774-
alpha = color[:, 3]
1775-
colors = norm(shade)[:, np.newaxis] * color
1776-
colors[:, 3] = alpha
1777-
else:
1778-
colors = np.asanyarray(color).copy()
1779-
1780-
return colors
1781-
17821698
def plot_wireframe(self, X, Y, Z, **kwargs):
17831699
"""
17841700
Plot a 3D wireframe.
@@ -1975,9 +1891,8 @@ def plot_trisurf(self, *args, color=None, norm=None, vmin=None, vmax=None,
19751891
zt = z[triangles]
19761892
verts = np.stack((xt, yt, zt), axis=-1)
19771893

1978-
polyc = art3d.Poly3DCollection(verts, *args, **kwargs)
1979-
19801894
if cmap:
1895+
polyc = art3d.Poly3DCollection(verts, *args, **kwargs)
19811896
# average over the three points of each triangle
19821897
avg_z = verts[:, :, 2].mean(axis=1)
19831898
polyc.set_array(avg_z)
@@ -1986,12 +1901,9 @@ def plot_trisurf(self, *args, color=None, norm=None, vmin=None, vmax=None,
19861901
if norm is not None:
19871902
polyc.set_norm(norm)
19881903
else:
1989-
if shade:
1990-
normals = self._generate_normals(verts)
1991-
colset = self._shade_colors(color, normals, lightsource)
1992-
else:
1993-
colset = color
1994-
polyc.set_facecolors(colset)
1904+
polyc = art3d.Poly3DCollection(
1905+
verts, *args, shade=shade, lightsource=lightsource,
1906+
facecolors=color, **kwargs)
19951907

19961908
self.add_collection(polyc)
19971909
self.auto_scale_xyz(tri.x, tri.y, z, had_data)
@@ -2036,13 +1948,10 @@ def _3d_extend_contour(self, cset, stride=5):
20361948

20371949
# all polygons have 4 vertices, so vectorize
20381950
polyverts = np.array(polyverts)
2039-
normals = self._generate_normals(polyverts)
2040-
2041-
colors = self._shade_colors(color, normals)
2042-
colors2 = self._shade_colors(color, normals)
20431951
polycol = art3d.Poly3DCollection(polyverts,
2044-
facecolors=colors,
2045-
edgecolors=colors2)
1952+
facecolors=color,
1953+
edgecolors=color,
1954+
shade=True)
20461955
polycol.set_sort_zpos(z)
20471956
self.add_collection3d(polycol)
20481957

@@ -2587,15 +2496,11 @@ def bar3d(self, x, y, z, dx, dy, dz, color=None,
25872496
if len(facecolors) < len(x):
25882497
facecolors *= (6 * len(x))
25892498

2590-
if shade:
2591-
normals = self._generate_normals(polys)
2592-
sfacecolors = self._shade_colors(facecolors, normals, lightsource)
2593-
else:
2594-
sfacecolors = facecolors
2595-
25962499
col = art3d.Poly3DCollection(polys,
25972500
zsort=zsort,
2598-
facecolor=sfacecolors,
2501+
facecolors=facecolors,
2502+
shade=shade,
2503+
lightsource=lightsource,
25992504
*args, **kwargs)
26002505
self.add_collection(col)
26012506

@@ -2964,16 +2869,10 @@ def permutation_matrices(n):
29642869
# shade the faces
29652870
facecolor = facecolors[coord]
29662871
edgecolor = edgecolors[coord]
2967-
if shade:
2968-
normals = self._generate_normals(faces)
2969-
facecolor = self._shade_colors(facecolor, normals, lightsource)
2970-
if edgecolor is not None:
2971-
edgecolor = self._shade_colors(
2972-
edgecolor, normals, lightsource
2973-
)
29742872

29752873
poly = art3d.Poly3DCollection(
2976-
faces, facecolors=facecolor, edgecolors=edgecolor, **kwargs)
2874+
faces, facecolors=facecolor, edgecolors=edgecolor,
2875+
shade=shade, lightsource=lightsource, **kwargs)
29772876
self.add_collection3d(poly)
29782877
polygons[coord] = poly
29792878

0 commit comments

Comments
 (0)