| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff 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() | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -808,7 +808,8 @@ class Poly3DCollection(PolyCollection): | |||
| 808 | 808 | triangulation and thus generates consistent surfaces. | |
| 809 | 809 | """ | |
| 810 | 810 | ||
| 811 | - def __init__(self, verts, *args, zsort='average', **kwargs): | ||
| 811 | + def __init__(self, verts, *args, zsort='average', shade=False, | ||
| 812 | + lightsource=None, **kwargs): | ||
| 812 | 813 | """ | |
| 813 | 814 | Parameters | |
| 814 | 815 | ---------- | |
@@ -819,6 +820,20 @@ def __init__(self, verts, *args, zsort='average', **kwargs): | |||
| 819 | 820 | zsort : {'average', 'min', 'max'}, default: 'average' | |
| 820 | 821 | The calculation method for the z-order. | |
| 821 | 822 | See `~.Poly3DCollection.set_zsort` for details. | |
| 823 | + shade : bool, default: False | ||
| 824 | + Whether to shade *facecolors* and *edgecolors*. | ||
| 825 | + | ||
| 826 | + .. versionadded:: 3.7 | ||
| 827 | + | ||
| 828 | + .. note:: | ||
| 829 | + *facecolors* and/or *edgecolors* must be provided for shading | ||
| 830 | + to work. | ||
| 831 | + | ||
| 832 | + lightsource : `~matplotlib.colors.LightSource` | ||
| 833 | + The lightsource to use when *shade* is True. | ||
| 834 | + | ||
| 835 | + .. versionadded:: 3.7 | ||
| 836 | + | ||
| 822 | 837 | *args, **kwargs | |
| 823 | 838 | All other parameters are forwarded to `.PolyCollection`. | |
| 824 | 839 | ||
@@ -827,6 +842,20 @@ def __init__(self, verts, *args, zsort='average', **kwargs): | |||
| 827 | 842 | Note that this class does a bit of magic with the _facecolors | |
| 828 | 843 | and _edgecolors properties. | |
| 829 | 844 | """ | |
| 845 | + if shade: | ||
| 846 | + normals = _generate_normals(verts) | ||
| 847 | + facecolor = kwargs.get('facecolors', None) | ||
| 848 | + if facecolor is not None: | ||
| 849 | + kwargs['facecolors'] = _shade_colors( | ||
| 850 | + facecolor, normals, lightsource | ||
| 851 | + ) | ||
| 852 | + | ||
| 853 | + edgecolor = kwargs.get('edgecolors', None) | ||
| 854 | + if edgecolor is not None: | ||
| 855 | + kwargs['edgecolors'] = _shade_colors( | ||
| 856 | + edgecolor, normals, lightsource | ||
| 857 | + ) | ||
| 858 | + | ||
| 830 | 859 | super().__init__(verts, *args, **kwargs) | |
| 831 | 860 | if isinstance(verts, np.ndarray): | |
| 832 | 861 | if verts.ndim != 3: | |
@@ -1086,3 +1115,84 @@ def _zalpha(colors, zs): | |||
| 1086 | 1115 | sats = 1 - norm(zs) * 0.7 | |
| 1087 | 1116 | rgba = np.broadcast_to(mcolors.to_rgba_array(colors), (len(zs), 4)) | |
| 1088 | 1117 | return np.column_stack([rgba[:, :3], rgba[:, 3] * sats]) | |
| 1118 | + | ||
| 1119 | + | ||
| 1120 | + def _generate_normals(polygons): | ||
| 1121 | + """ | ||
| 1122 | + Compute the normals of a list of polygons. | ||
| 1123 | + | ||
| 1124 | + Normals point towards the viewer for a face with its vertices in | ||
| 1125 | + counterclockwise order, following the right hand rule. | ||
| 1126 | + | ||
| 1127 | + Uses three points equally spaced around the polygon. | ||
| 1128 | + This normal of course might not make sense for polygons with more than | ||
| 1129 | + three points not lying in a plane, but it's a plausible and fast | ||
| 1130 | + approximation. | ||
| 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 | + *color* can also be an array of the same length as *normals*. | ||
| 1168 | + """ | ||
| 1169 | + if lightsource is None: | ||
| 1170 | + # chosen for backwards-compatibility | ||
| 1171 | + lightsource = mcolors.LightSource(azdeg=225, altdeg=19.4712) | ||
| 1172 | + | ||
| 1173 | + with np.errstate(invalid="ignore"): | ||
| 1174 | + shade = ((normals / np.linalg.norm(normals, axis=1, keepdims=True)) | ||
| 1175 | + @ lightsource.direction) | ||
| 1176 | + mask = ~np.isnan(shade) | ||
| 1177 | + | ||
| 1178 | + if mask.any(): | ||
| 1179 | + # convert dot product to allowed shading fractions | ||
| 1180 | + in_norm = mcolors.Normalize(-1, 1) | ||
| 1181 | + out_norm = mcolors.Normalize(0.3, 1).inverse | ||
| 1182 | + | ||
| 1183 | + def norm(x): | ||
| 1184 | + return out_norm(in_norm(x)) | ||
| 1185 | + | ||
| 1186 | + shade[~mask] = 0 | ||
| 1187 | + | ||
| 1188 | + color = mcolors.to_rgba_array(color) | ||
| 1189 | + # shape of color should be (M, 4) (where M is number of faces) | ||
| 1190 | + # shape of shade should be (M,) | ||
| 1191 | + # colors should have final shape of (M, 4) | ||
| 1192 | + alpha = color[:, 3] | ||
| 1193 | + colors = norm(shade)[:, np.newaxis] * color | ||
| 1194 | + colors[:, 3] = alpha | ||
| 1195 | + else: | ||
| 1196 | + colors = np.asanyarray(color).copy() | ||
| 1197 | + | ||
| 1198 | + return colors | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1668,15 +1668,13 @@ def plot_surface(self, X, Y, Z, *, norm=None, vmin=None, | |||
| 1668 | 1668 | ||
| 1669 | 1669 | # note that the striding causes some polygons to have more coordinates | |
| 1670 | 1670 | # than others | |
| 1671 | - polyc = art3d.Poly3DCollection(polys, **kwargs) | ||
| 1672 | 1671 | ||
| 1673 | 1672 | 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) | ||
| 1679 | 1676 | elif cmap: | |
| 1677 | + polyc = art3d.Poly3DCollection(polys, **kwargs) | ||
| 1680 | 1678 | # can't always vectorize, because polys might be jagged | |
| 1681 | 1679 | if isinstance(polys, np.ndarray): | |
| 1682 | 1680 | avg_z = polys[..., 2].mean(axis=-1) | |
@@ -1688,97 +1686,15 @@ def plot_surface(self, X, Y, Z, *, norm=None, vmin=None, | |||
| 1688 | 1686 | if norm is not None: | |
| 1689 | 1687 | polyc.set_norm(norm) | |
| 1690 | 1688 | 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) | ||
| 1697 | 1692 | ||
| 1698 | 1693 | self.add_collection(polyc) | |
| 1699 | 1694 | self.auto_scale_xyz(X, Y, Z, had_data) | |
| 1700 | 1695 | ||
| 1701 | 1696 | return polyc | |
| 1702 | 1697 | ||
| 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 | - | ||
| 1782 | 1698 | def plot_wireframe(self, X, Y, Z, **kwargs): | |
| 1783 | 1699 | """ | |
| 1784 | 1700 | Plot a 3D wireframe. | |
@@ -1975,9 +1891,8 @@ def plot_trisurf(self, *args, color=None, norm=None, vmin=None, vmax=None, | |||
| 1975 | 1891 | zt = z[triangles] | |
| 1976 | 1892 | verts = np.stack((xt, yt, zt), axis=-1) | |
| 1977 | 1893 | ||
| 1978 | - polyc = art3d.Poly3DCollection(verts, *args, **kwargs) | ||
| 1979 | - | ||
| 1980 | 1894 | if cmap: | |
| 1895 | + polyc = art3d.Poly3DCollection(verts, *args, **kwargs) | ||
| 1981 | 1896 | # average over the three points of each triangle | |
| 1982 | 1897 | avg_z = verts[:, :, 2].mean(axis=1) | |
| 1983 | 1898 | polyc.set_array(avg_z) | |
@@ -1986,12 +1901,9 @@ def plot_trisurf(self, *args, color=None, norm=None, vmin=None, vmax=None, | |||
| 1986 | 1901 | if norm is not None: | |
| 1987 | 1902 | polyc.set_norm(norm) | |
| 1988 | 1903 | 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) | ||
| 1995 | 1907 | ||
| 1996 | 1908 | self.add_collection(polyc) | |
| 1997 | 1909 | self.auto_scale_xyz(tri.x, tri.y, z, had_data) | |
@@ -2036,13 +1948,10 @@ def _3d_extend_contour(self, cset, stride=5): | |||
| 2036 | 1948 | ||
| 2037 | 1949 | # all polygons have 4 vertices, so vectorize | |
| 2038 | 1950 | 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) | ||
| 2043 | 1951 | polycol = art3d.Poly3DCollection(polyverts, | |
| 2044 | - facecolors=colors, | ||
| 2045 | - edgecolors=colors2) | ||
| 1952 | + facecolors=color, | ||
| 1953 | + edgecolors=color, | ||
| 1954 | + shade=True) | ||
| 2046 | 1955 | polycol.set_sort_zpos(z) | |
| 2047 | 1956 | self.add_collection3d(polycol) | |
| 2048 | 1957 | ||
@@ -2587,15 +2496,11 @@ def bar3d(self, x, y, z, dx, dy, dz, color=None, | |||
| 2587 | 2496 | if len(facecolors) < len(x): | |
| 2588 | 2497 | facecolors *= (6 * len(x)) | |
| 2589 | 2498 | ||
| 2590 | - if shade: | ||
| 2591 | - normals = self._generate_normals(polys) | ||
| 2592 | - sfacecolors = self._shade_colors(facecolors, normals, lightsource) | ||
| 2593 | - else: | ||
| 2594 | - sfacecolors = facecolors | ||
| 2595 | - | ||
| 2596 | 2499 | col = art3d.Poly3DCollection(polys, | |
| 2597 | 2500 | zsort=zsort, | |
| 2598 | - facecolor=sfacecolors, | ||
| 2501 | + facecolors=facecolors, | ||
| 2502 | + shade=shade, | ||
| 2503 | + lightsource=lightsource, | ||
| 2599 | 2504 | *args, **kwargs) | |
| 2600 | 2505 | self.add_collection(col) | |
| 2601 | 2506 | ||
@@ -2964,16 +2869,10 @@ def permutation_matrices(n): | |||
| 2964 | 2869 | # shade the faces | |
| 2965 | 2870 | facecolor = facecolors[coord] | |
| 2966 | 2871 | 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 | - ) | ||
| 2974 | 2872 | ||
| 2975 | 2873 | poly = art3d.Poly3DCollection( | |
| 2976 | - faces, facecolors=facecolor, edgecolors=edgecolor, **kwargs) | ||
| 2874 | + faces, facecolors=facecolor, edgecolors=edgecolor, | ||
| 2875 | + shade=shade, lightsource=lightsource, **kwargs) | ||
| 2977 | 2876 | self.add_collection3d(poly) | |
| 2978 | 2877 | polygons[coord] = poly | |
| 2979 | 2878 | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments