diff --git a/rotorpy/utils/animate.py b/rotorpy/utils/animate.py index 81b9cea..769690c 100644 --- a/rotorpy/utils/animate.py +++ b/rotorpy/utils/animate.py @@ -11,7 +11,6 @@ from matplotlib.animation import FuncAnimation import matplotlib.pyplot as plt from scipy.spatial.transform import Rotation -from rotorpy.utils.axes3ds import Axes3Ds from rotorpy.utils.shapes import Quadrotor import os @@ -94,12 +93,9 @@ def animate(time, position, rotation, wind, animate_wind, world, filename=None, else: fig = plt.figure('Animation') fig.clear() - ax = Axes3Ds(fig) + ax = fig.add_subplot(projection='3d') if not show_axes: ax.set_axis_off() - ax.set_xlim(-1,1) - ax.set_ylim(-1,1) - ax.set_zlim(-1,1) quad = Quadrotor(ax, wind=animate_wind) @@ -114,7 +110,7 @@ def animate(time, position, rotation, wind, animate_wind, world, filename=None, def update(frame): title_artist.set_text('t = {:.2f}'.format(time[frame])) quad.transform(position=position[frame,:], rotation=rotation[frame,:,:], wind=wind[frame,:]) - [a.do_3d_projection(fig.canvas.get_renderer()) for a in quad.artists] + # [a.do_3d_projection(fig.canvas.get_renderer()) for a in quad.artists] # No longer necessary in newer matplotlib? return world_artists + list(quad.artists) + [title_artist] ani = ClosingFuncAnimation(fig=fig, diff --git a/rotorpy/utils/axes3ds.py b/rotorpy/utils/axes3ds.py deleted file mode 100644 index 0ece269..0000000 --- a/rotorpy/utils/axes3ds.py +++ /dev/null @@ -1,159 +0,0 @@ -""" -This module provides Axes3Ds ("Axes3D Spatial"), a drop-in replacement for -Axes3D which incorporates the improvements proposed by eric-wieser in matplotlib -issue #8896. - -The purpose is to reduce the distortion when projecting 3D scenes into the 2D -image. For example, the projection of a sphere will be (closer to) a circle. -""" - -""" -License agreement for matplotlib versions 1.3.0 and later -========================================================= - -1. This LICENSE AGREEMENT is between the Matplotlib Development Team -("MDT"), and the Individual or Organization ("Licensee") accessing and -otherwise using matplotlib software in source or binary form and its -associated documentation. - -2. Subject to the terms and conditions of this License Agreement, MDT -hereby grants Licensee a nonexclusive, royalty-free, world-wide license -to reproduce, analyze, test, perform and/or display publicly, prepare -derivative works, distribute, and otherwise use matplotlib -alone or in any derivative version, provided, however, that MDT's -License Agreement and MDT's notice of copyright, i.e., "Copyright (c) -2012- Matplotlib Development Team; All Rights Reserved" are retained in -matplotlib alone or in any derivative version prepared by -Licensee. - -3. In the event Licensee prepares a derivative work that is based on or -incorporates matplotlib or any part thereof, and wants to -make the derivative work available to others as provided herein, then -Licensee hereby agrees to include in any such work a brief summary of -the changes made to matplotlib . - -4. MDT is making matplotlib available to Licensee on an "AS -IS" basis. MDT MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR -IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, MDT MAKES NO AND -DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS -FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF MATPLOTLIB -WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. - -5. MDT SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF MATPLOTLIB - FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR -LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING -MATPLOTLIB , OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF -THE POSSIBILITY THEREOF. - -6. This License Agreement will automatically terminate upon a material -breach of its terms and conditions. - -7. Nothing in this License Agreement shall be deemed to create any -relationship of agency, partnership, or joint venture between MDT and -Licensee. This License Agreement does not grant permission to use MDT -trademarks or trade name in a trademark sense to endorse or promote -products or services of Licensee, or any third party. - -8. By copying, installing or otherwise using matplotlib , -Licensee agrees to be bound by the terms and conditions of this License -Agreement. - -""" - -import numpy as np - -from mpl_toolkits.mplot3d import Axes3D -from mpl_toolkits.mplot3d import proj3d - -# Patch note: An update of the original implementation in proj3d.py. -def world_transformation(xmin, xmax, - ymin, ymax, - zmin, zmax, pb_aspect=None): - """ - produce a matrix that scales homogenous coords in the specified ranges - to [0, 1], or [0, pb_aspect[i]] if the plotbox aspect ratio is specified - """ - dx = xmax - xmin - dy = ymax - ymin - dz = zmax - zmin - if pb_aspect is not None: - ax, ay, az = pb_aspect - dx /= ax - dy /= ay - dz /= az - - return np.array([[1/dx, 0, 0, -xmin/dx], - [0, 1/dy, 0, -ymin/dy], - [0, 0, 1/dz, -zmin/dz], - [0, 0, 0, 1]]) - -class Axes3Ds(Axes3D): - """ - Class Axes3Ds ("Axes3D Spatial") is a drop-in replacement for Axes3D which - incorporates the improvements proposed by eric-wieser in matplotlib issue - #8896. - """ - - # Patch note: A new function. - def apply_aspect(self, position=None): - if position is None: - position = self.get_position(original=True) - - # in the superclass, we would go through and actually deal with axis - # scales and box/datalim. Those are all irrelevant - all we need to do - # is make sure our coordinate system is square. - figW, figH = self.get_figure().get_size_inches() - fig_aspect = figH / figW - box_aspect = 1 - pb = position.frozen() - pb1 = pb.shrunk_to_aspect(box_aspect, pb, fig_aspect) - self.set_position(pb1.anchored(self.get_anchor(), pb), 'active') - - # Patch note: Overwritten to use the updated version of world_transformation - # and the new pb_aspect value. - def get_proj(self): - """ - Create the projection matrix from the current viewing position. - elev stores the elevation angle in the z plane - azim stores the azimuth angle in the x,y plane - dist is the distance of the eye viewing point from the object - point. - """ - # chosen for similarity with the initial view before gh-8896 - pb_aspect = np.array([4, 4, 3]) / 3.5 - - relev, razim = np.pi * self.elev/180, np.pi * self.azim/180 - - xmin, xmax = self.get_xlim3d() - ymin, ymax = self.get_ylim3d() - zmin, zmax = self.get_zlim3d() - - # transform to uniform world coordinates 0-1.0,0-1.0,0-1.0 - worldM = world_transformation(xmin, xmax, - ymin, ymax, - zmin, zmax, pb_aspect=pb_aspect) - - # look into the middle of the new coordinates - R = pb_aspect / 2 - - xp = R[0] + np.cos(razim) * np.cos(relev) * self.dist - yp = R[1] + np.sin(razim) * np.cos(relev) * self.dist - zp = R[2] + np.sin(relev) * self.dist - E = np.array((xp, yp, zp)) - - self.eye = E - self.vvec = R - E - self.vvec = self.vvec / np.linalg.norm(self.vvec) - - if abs(relev) > np.pi/2: - # upside down - V = np.array((0, 0, -1)) - else: - V = np.array((0, 0, 1)) - zfront, zback = -self.dist, self.dist - - viewM = proj3d.view_transformation(E, R, V) - projM = self._projection(zfront, zback) - M0 = np.dot(viewM, worldM) - M = np.dot(projM, M0) - return M diff --git a/rotorpy/utils/occupancy_map.py b/rotorpy/utils/occupancy_map.py index cd8fab1..e684b2b 100644 --- a/rotorpy/utils/occupancy_map.py +++ b/rotorpy/utils/occupancy_map.py @@ -233,7 +233,6 @@ class OccupancyMap: if __name__ == "__main__": - from axes3ds import Axes3Ds import matplotlib.pyplot as plt import os @@ -245,7 +244,7 @@ if __name__ == "__main__": # Create a figure fig = plt.figure() - ax = Axes3Ds(fig) + ax = fig.add_subplot(projection='3d') # Draw the world world.draw(ax) diff --git a/rotorpy/utils/plotter.py b/rotorpy/utils/plotter.py index 77a7272..0185497 100644 --- a/rotorpy/utils/plotter.py +++ b/rotorpy/utils/plotter.py @@ -2,7 +2,7 @@ import numpy as np from scipy.spatial.transform import Rotation import matplotlib.pyplot as plt -from rotorpy.utils.axes3ds import Axes3Ds +# from rotorpy.utils.axes3ds import Axes3Ds from rotorpy.utils.animate import animate import os @@ -38,7 +38,8 @@ class Plotter(): # 3D Paths fig = plt.figure('3D Path') - ax = Axes3Ds(fig) + # ax = Axes3Ds(fig) + ax = fig.add_subplot(projection='3d') self.world.draw(ax) ax.plot3D(self.x[:,0], self.x[:,1], self.x[:,2], 'b.') ax.plot3D(self.x_des[:,0], self.x_des[:,1], self.x_des[:,2], 'k') diff --git a/rotorpy/utils/shapes.py b/rotorpy/utils/shapes.py index bea1572..397b7ab 100644 --- a/rotorpy/utils/shapes.py +++ b/rotorpy/utils/shapes.py @@ -14,6 +14,58 @@ from mpl_toolkits.mplot3d import art3d import matplotlib.colors as mcolors from scipy.spatial.transform import Rotation +""" +Necessary functions for visualization +From original mplot3d version by John Porter (Created: 23 Sep 2005) + +Parts fixed by Reinier Heeres +Minor additions by Ben Axelrod +Significant updates and revisions by Ben Root + +Current as of matplotlib v3.2.2 but changed at some point. +Modified by Spencer Folk +""" + +def _generate_normals(polygons): + ''' + Generate normals for polygons by using the first three points. + This normal of course might not make sense for polygons with + more than three points not lying in a plane. + ''' + + normals = [] + for verts in polygons: + v1 = np.array(verts[0]) - np.array(verts[1]) + v2 = np.array(verts[2]) - np.array(verts[0]) + normals.append(np.cross(v1, v2)) + return normals + +def _shade_colors(color, normals): + ''' + Shade *color* using normal vectors given by *normals*. + *color* can also be an array of the same length as *normals*. + ''' + + shade = np.array([np.dot(n / np.linalg.norm(n), [-1, -1, 0.5]) + if np.linalg.norm(n) else np.nan + for n in normals]) + mask = ~np.isnan(shade) + + if len(shade[mask]) > 0: + norm = mcolors.Normalize(min(shade[mask]), max(shade[mask])) + shade[~mask] = min(shade[mask]) + color = mcolors.to_rgba_array(color) + # shape of color should be (M, 4) (where M is number of faces) + # shape of shade should be (M,) + # colors should have final shape of (M, 4) + alpha = color[:, 3] + colors = (0.5 + norm(shade)[:, np.newaxis] * 0.5) * color + colors[:, 3] = alpha + else: + colors = np.asanyarray(color).copy() + + return colors + class Face(): def __init__(self, ax, corners, *, @@ -45,7 +97,7 @@ class Face(): # Precompute verticies and normal vectors in reference configuration. self.verts = np.reshape(corners, (1, -1, 3)) - self.normals = np.asarray(self.ax._generate_normals(self.verts)) + self.normals = np.asarray(_generate_normals(self.verts)) # Instantiate and add collection. self.polyc = art3d.Poly3DCollection(self.verts, linewidth=linewidth, antialiased=antialiased, alpha=alpha, edgecolors=edgecolors, facecolors=self.facecolors) @@ -65,7 +117,7 @@ class Face(): if self.shade: normals = np.matmul(rotation, self.normals.T).T - colset = self.ax._shade_colors(self.facecolors, normals) + colset = _shade_colors(self.facecolors, normals) else: colset = self.facecolors self.polyc.set_facecolors(colset) @@ -103,7 +155,7 @@ class Cuboid(): # Precompute verticies and normal vectors in reference configuration. self.verts = self.build_verts(x_span, y_span, z_span) - self.normals = np.asarray(self.ax._generate_normals(self.verts)) + self.normals = np.asarray(_generate_normals(self.verts)) # Instantiate and add collection. self.polyc = art3d.Poly3DCollection(self.verts, linewidth=linewidth, antialiased=antialiased, alpha=alpha, edgecolors=edgecolors, facecolors=self.facecolors) @@ -123,7 +175,7 @@ class Cuboid(): if self.shade: normals = np.matmul(rotation, self.normals.T).T - colset = self.ax._shade_colors(self.facecolors, normals) + colset = _shade_colors(self.facecolors, normals) else: colset = self.facecolors self.polyc.set_facecolors(colset) @@ -181,7 +233,7 @@ class Cylinder(): # Precompute verticies and normal vectors in reference configuration. self.verts = self.build_verts(radius, height, n_pts) - self.normals = np.asarray(self.ax._generate_normals(self.verts)) + self.normals = np.asarray(_generate_normals(self.verts)) # Instantiate and add collection. self.polyc = art3d.Poly3DCollection(self.verts, color='b', linewidth=0, antialiased=False) @@ -200,7 +252,7 @@ class Cylinder(): if self.shade: normals = np.matmul(rotation, self.normals.T).T - colset = self.ax._shade_colors(self.color, normals) + colset = _shade_colors(self.color, normals) else: colset = self.color self.polyc.set_facecolors(colset) @@ -311,12 +363,11 @@ class Quadrotor(): self.wind_vector = [self.ax.quiver(position[0], position[1], position[2], wind[0], wind[1], wind[2], color='r', linewidth=1.5)] if __name__ == '__main__': - from axes3ds import Axes3Ds import matplotlib.pyplot as plt # Test Face fig = plt.figure(num=4, clear=True) - ax = Axes3Ds(fig) + ax = fig.add_subplot(projection='3d') corners = np.array([(1,1,1), (-1,1,1), (-1,-1,1), (1,-1,1)]) z_plus_face = Face(ax, corners=corners, facecolors='b') x_plus_face = Face(ax, corners=corners, facecolors='r') @@ -336,7 +387,7 @@ if __name__ == '__main__': # Test Cuboid fig = plt.figure(num=0, clear=True) - ax = Axes3Ds(fig) + ax = fig.add_subplot(projection='3d') cuboid = Cuboid(ax, x_span=1, y_span=1, z_span=1) cuboid.transform(position=np.array([[0, 0, 0]]), rotation=np.identity(3)) rotation = Rotation.from_rotvec(np.pi/4 * np.array([1, 0, 0])).as_matrix() @@ -348,7 +399,7 @@ if __name__ == '__main__': # Test Cylinder fig = plt.figure(num=1, clear=True) - ax = Axes3Ds(fig) + ax = fig.add_subplot(projection='3d') cylinder = Cylinder(ax, radius=0.2, height=0.2) cylinder.transform(position=np.array([[0, 0, 0]]), rotation=np.identity(3)) rotation = Rotation.from_rotvec(np.pi/4 * np.array([1, 0, 0])).as_matrix() @@ -360,7 +411,7 @@ if __name__ == '__main__': # Test Quadrotor fig = plt.figure(num=2, clear=True) - ax = Axes3Ds(fig) + ax = fig.add_subplot(projection='3d') quad = Quadrotor(ax) quad.transform(position=np.array([[0.5, 0.5, 0.5]]), rotation=np.identity(3)) quad = Quadrotor(ax) @@ -372,7 +423,7 @@ if __name__ == '__main__': # Test Cuboid coloring. fig = plt.figure(num=3, clear=True) - ax = Axes3Ds(fig) + ax = fig.add_subplot(projection='3d') ax.set_xlim(-3.25,3.25) ax.set_ylim(-3.25,3.25) ax.set_zlim(-3.25,3.25) diff --git a/rotorpy/world.py b/rotorpy/world.py index 04da220..a40a444 100644 --- a/rotorpy/world.py +++ b/rotorpy/world.py @@ -289,7 +289,6 @@ if __name__ == '__main__': import argparse from pathlib import Path import matplotlib.pyplot as plt - from rotorpy.axes3ds import Axes3Ds parser = argparse.ArgumentParser(description='Display a map file in a Matplotlib window.') parser.add_argument('filename', help="Filename for map file json.") @@ -299,7 +298,7 @@ if __name__ == '__main__': world = World.from_file(file) fig = plt.figure(f"{file.name}") - ax = Axes3Ds(fig) + ax = fig.add_subplot(projection='3d') world.draw(ax) plt.show() diff --git a/setup.py b/setup.py index 21f6976..9ca9a05 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from os.path import isdir from itertools import product # Gather our flightsim and any projXX packages that happen to exist. -all_packages = ['rotorpy', 'rotorpy.wind-dynamics'] +all_packages = ['rotorpy'] packages = list(filter(isdir, all_packages)) setup( @@ -12,7 +12,7 @@ setup( version='1.0.1', install_requires=[ 'cvxopt', - 'matplotlib == 3.2.2', + 'matplotlib', 'filterpy == 1.4.5', 'numpy', 'scipy',