Files
rotor_py_control/rotorpy/utils/shapes.py

500 lines
18 KiB
Python
Raw Normal View History

2023-03-15 15:38:14 -04:00
"""
Parametric 3D shapes for spatial plots and animations. Shapes are drawn on an
Axes3D axes, and then can be moved using .transform(). They can return a list of
artists to support blitting in animations.
TODO:
There is a fair amount of code duplication here; a superclass may be warranted.
"""
import itertools
import numpy as np
from mpl_toolkits.mplot3d import art3d
import matplotlib.colors as mcolors
from scipy.spatial.transform import Rotation
2023-12-15 11:00:30 -05:00
"""
Necessary functions for visualization
From original mplot3d version by John Porter (Created: 23 Sep 2005)
Parts fixed by Reinier Heeres <reinier@heeres.eu>
Minor additions by Ben Axelrod <baxelrod@coroware.com>
Significant updates and revisions by Ben Root <ben.v.root@gmail.com>
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
2023-03-15 15:38:14 -04:00
class Face():
def __init__(self, ax, corners, *,
shade=True,
alpha=1.0,
facecolors=None,
edgecolors=None,
linewidth=0,
antialiased=True):
"""
Parameters
ax, Axes3D to contain new shape
corners, shape=(N,3)
shade, shade faces using default lightsource, default is True
linewidth, width of lines, default is 0
alpha, transparency value in domain [0,1], default is 1.0
edgecolors, color of edges
facecolors, color of faces
antialiased, smoother edge lines, default is True
"""
self.shade = shade
self.facecolors = facecolors
self.ax = ax
if self.facecolors is None:
self.facecolors = self.ax._get_lines.get_next_color()
self.facecolors = np.array(mcolors.to_rgba(self.facecolors))
# Precompute verticies and normal vectors in reference configuration.
self.verts = np.reshape(corners, (1, -1, 3))
2023-12-15 11:00:30 -05:00
self.normals = np.asarray(_generate_normals(self.verts))
2023-03-15 15:38:14 -04:00
# Instantiate and add collection.
self.polyc = art3d.Poly3DCollection(self.verts, linewidth=linewidth, antialiased=antialiased, alpha=alpha, edgecolors=edgecolors, facecolors=self.facecolors)
self.artists = (self.polyc,)
self.transform(np.zeros((3,)), np.identity(3))
self.ax.add_collection(self.polyc)
def transform(self, position, rotation=np.identity(3)):
position = np.array(position)
position.shape = (3,1)
# The verts array is indexed as (i_face, j_coordinate, k_point).
verts = np.swapaxes(self.verts, 1, 2)
new_verts = np.matmul(rotation, verts) + position
self.polyc.set_verts(np.swapaxes(new_verts, 1, 2))
if self.shade:
normals = np.matmul(rotation, self.normals.T).T
2023-12-15 11:00:30 -05:00
colset = _shade_colors(self.facecolors, normals)
2023-03-15 15:38:14 -04:00
else:
colset = self.facecolors
self.polyc.set_facecolors(colset)
class Cuboid():
def __init__(self, ax, x_span, y_span, z_span, *,
shade=True,
alpha=1.0,
facecolors=None,
edgecolors=None,
linewidth=0,
antialiased=True):
"""
Parameters
ax, Axes3D to contain new shape
x_span, width in x-direction
y_span, width in y-direction
z_span, width in z-direction
shade, shade faces using default lightsource, default is True
linewidth, width of lines, default is 0
alpha, transparency value in domain [0,1], default is 1.0
edgecolors, color of edges
facecolors, color of faces
antialiased, smoother edge lines, default is True
"""
self.shade = shade
self.facecolors = facecolors
self.ax = ax
if self.facecolors is None:
self.facecolors = self.ax._get_lines.get_next_color()
self.facecolors = np.array(mcolors.to_rgba(self.facecolors))
# Precompute verticies and normal vectors in reference configuration.
self.verts = self.build_verts(x_span, y_span, z_span)
2023-12-15 11:00:30 -05:00
self.normals = np.asarray(_generate_normals(self.verts))
2023-03-15 15:38:14 -04:00
# Instantiate and add collection.
self.polyc = art3d.Poly3DCollection(self.verts, linewidth=linewidth, antialiased=antialiased, alpha=alpha, edgecolors=edgecolors, facecolors=self.facecolors)
self.artists = (self.polyc,)
self.transform(np.zeros((3,)), np.identity(3))
self.ax.add_collection(self.polyc)
def transform(self, position, rotation=np.identity(3)):
position = np.array(position)
position.shape = (3,1)
# The verts array is indexed as (i_face, j_coordinate, k_point).
verts = np.swapaxes(self.verts, 1, 2)
new_verts = np.matmul(rotation, verts) + position
self.polyc.set_verts(np.swapaxes(new_verts, 1, 2))
if self.shade:
normals = np.matmul(rotation, self.normals.T).T
2023-12-15 11:00:30 -05:00
colset = _shade_colors(self.facecolors, normals)
2023-03-15 15:38:14 -04:00
else:
colset = self.facecolors
self.polyc.set_facecolors(colset)
def build_verts(self, x_span, y_span, z_span):
"""
Input
x_span, width in x-direction
y_span, width in y-direction
z_span, width in z-direction
Returns
verts, shape=(6_faces, 4_points, 3_coordinates)
"""
# Coordinates of each point.
(x, y, z) = (x_span, y_span, z_span)
bot_pts = np.array([
[0, 0, 0],
[x, 0, 0],
[x, y, 0],
[0, y, 0]])
top_pts = np.array([
[0, 0, z],
[x, 0, z],
[x, y, z],
[0, y, z]])
pts = np.concatenate((bot_pts, top_pts), axis=0)
# Indices of points for each face.
side_faces = [(i, (i+1)%4, 4+((i+1)%4), 4+i) for i in range(4)]
side_faces = np.array(side_faces, dtype=int)
bot_faces = np.arange(4, dtype=int)
bot_faces.shape = (1,4)
top_faces = 4 + bot_faces
all_faces = np.concatenate((side_faces, bot_faces, top_faces), axis=0)
# Vertex list.
xt = pts[:,0][all_faces]
yt = pts[:,1][all_faces]
zt = pts[:,2][all_faces]
verts = np.stack((xt, yt, zt), axis=-1)
return verts
class Cylinder():
def __init__(self, ax, radius, height, n_pts=8, shade=True, color=None):
self.shade = shade
self.ax = ax
if color is None:
color = self.ax._get_lines.get_next_color()
self.color = np.array(mcolors.to_rgba(color))
# Precompute verticies and normal vectors in reference configuration.
self.verts = self.build_verts(radius, height, n_pts)
2023-12-15 11:00:30 -05:00
self.normals = np.asarray(_generate_normals(self.verts))
2023-03-15 15:38:14 -04:00
# Instantiate and add collection.
self.polyc = art3d.Poly3DCollection(self.verts, color='b', linewidth=0, antialiased=False)
self.artists = (self.polyc,)
self.transform(np.zeros((3,)), np.identity(3))
self.ax.add_collection(self.polyc)
def transform(self, position, rotation):
position.shape = (3,1)
# The verts array is indexed as (i_triangle, j_coordinate, k_point).
verts = np.swapaxes(self.verts, 1, 2)
new_verts = np.matmul(rotation, verts) + position
self.polyc.set_verts(np.swapaxes(new_verts, 1, 2))
if self.shade:
normals = np.matmul(rotation, self.normals.T).T
2023-12-15 11:00:30 -05:00
colset = _shade_colors(self.color, normals)
2023-03-15 15:38:14 -04:00
else:
colset = self.color
self.polyc.set_facecolors(colset)
def build_verts(self, radius, height, n_pts):
"""
Input
radius, radius of cylinder
height, height of cylinder
n_pts, number of points used to describe rim of cylinder
Returns
verts, [n_triangles, 3_points, 3_coordinates]
"""
theta = np.linspace(0, 2*np.pi, n_pts, endpoint=False)
delta_theta = (theta[1]-theta[0])/2
# Points around the bottom rim, top rim, bottom center, and top center.
bot_pts = np.zeros((3, n_pts))
bot_pts[0,:] = radius * np.cos(theta)
bot_pts[1,:] = radius * np.sin(theta)
bot_pts[2,:] = np.full(n_pts, -height/2)
top_pts = np.zeros((3, n_pts))
top_pts[0,:] = radius * np.cos(theta + delta_theta)
top_pts[1,:] = radius * np.sin(theta + delta_theta)
top_pts[2,:] = np.full(n_pts, height/2)
bot_center = np.array([[0], [0], [-height/2]])
top_center = np.array([[0], [0], [height/2]])
pts = np.concatenate((bot_pts, top_pts, bot_center, top_center), axis=1)
# Triangle indices for the shell.
up_triangles = np.stack((
np.arange(0, n_pts, dtype=int),
np.arange(1, n_pts+1, dtype=int),
np.arange(n_pts+0, n_pts+n_pts, dtype=int)))
up_triangles[1,-1] = 0
down_triangles = np.stack((
np.arange(0, n_pts, dtype=int),
np.arange(n_pts, n_pts+n_pts, dtype=int),
np.arange(n_pts-1, n_pts+n_pts-1, dtype=int)))
down_triangles[2,0] = n_pts+n_pts-1
shell_triangles = np.concatenate((up_triangles, down_triangles), axis=1)
# Triangle indices for the bottom.
bot_triangles = np.stack((
np.arange(0, n_pts, dtype=int),
np.arange(1, n_pts+1, dtype=int),
np.full(n_pts, 2*n_pts, dtype=int)))
bot_triangles[1,-1] = 0
top_triangles = np.stack((
np.arange(n_pts+0, n_pts+n_pts, dtype=int),
np.arange(n_pts+1, n_pts+n_pts+1, dtype=int),
np.full(n_pts, 2*n_pts+1, dtype=int)))
top_triangles[1,-1] = n_pts
all_triangles = np.concatenate((shell_triangles, bot_triangles, top_triangles), axis=1)
xt = pts[0,:][all_triangles.T]
yt = pts[1,:][all_triangles.T]
zt = pts[2,:][all_triangles.T]
verts = np.stack((xt, yt, zt), axis=-1)
return verts
class Quadrotor():
def __init__(self, ax,
arm_length=0.125, rotor_radius=0.08, n_rotors=4,
shade=True, color=None, wind=True, wind_scale_factor=5):
2023-03-15 15:38:14 -04:00
self.ax = ax
2023-04-05 15:37:52 -04:00
self.wind_bool = wind
self.wind_scale_factor = wind_scale_factor
2023-03-15 15:38:14 -04:00
# Apply same color to all rotor objects.
if color is None:
color = self.ax._get_lines.get_next_color()
self.color = np.array(mcolors.to_rgba(color))
# Precompute positions and rotations in the reference configuration.
theta = np.linspace(0, 2*np.pi, n_rotors, endpoint=False)
theta = theta + np.mean(theta[:2])
self.rotor_position = np.zeros((3, n_rotors))
self.rotor_position[0,:] = arm_length*np.cos(theta)
self.rotor_position[1,:] = arm_length*np.sin(theta)
# Instantiate.
self.rotors = [Cylinder(ax,
rotor_radius,
0.1*rotor_radius,
shade=shade,
color=color) for _ in range(n_rotors)]
2023-04-05 15:37:52 -04:00
artists = [r.artists for r in self.rotors]
if self.wind_bool:
self.wind_vector = [self.ax.quiver(0,0,0,0,0,0, color='k')]
artists.append(self.wind_vector)
self.artists = tuple(itertools.chain.from_iterable(artists))
self.transform(np.zeros((3,)), np.identity(3), np.zeros((3,)))
def transform(self, position, rotation, wind=np.array([1,0,0])):
2023-03-15 15:38:14 -04:00
position.shape = (3,1)
2023-04-05 15:37:52 -04:00
wind.shape = (3,1)
2023-03-15 15:38:14 -04:00
for (r, pos) in zip(self.rotors, self.rotor_position.T):
pos.shape = (3,1)
r.transform(np.matmul(rotation,pos)+position, rotation)
2023-04-05 15:37:52 -04:00
if self.wind_bool:
self.wind_vector[0].remove()
self.wind_vector = [self.ax.quiver(position[0], position[1], position[2], wind[0]/self.wind_scale_factor, wind[1]/self.wind_scale_factor, wind[2]/self.wind_scale_factor, color='r', linewidth=1.5)]
2023-03-15 15:38:14 -04:00
if __name__ == '__main__':
import matplotlib.pyplot as plt
# Test Face
fig = plt.figure(num=4, clear=True)
2023-12-15 11:00:30 -05:00
ax = fig.add_subplot(projection='3d')
2023-03-15 15:38:14 -04:00
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')
x_plus_face.transform(
position=(0,0,0),
rotation=Rotation.from_rotvec(np.pi/2 * np.array([0, 1, 0])).as_matrix())
y_plus_face = Face(ax, corners=corners, facecolors='g')
y_plus_face.transform(
position=(0,0,0),
rotation=Rotation.from_rotvec(np.pi/2 * np.array([-1, 0, 0])).as_matrix())
ax.set_xlim(-2,2)
ax.set_ylim(-2,2)
ax.set_zlim(-2,2)
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('z')
# Test Cuboid
fig = plt.figure(num=0, clear=True)
2023-12-15 11:00:30 -05:00
ax = fig.add_subplot(projection='3d')
2023-03-15 15:38:14 -04:00
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()
cuboid = Cuboid(ax, x_span=1, y_span=2, z_span=3)
cuboid.transform(position=np.array([[2.0, 1, 1]]), rotation=rotation)
ax.set_xlim(-1,3)
ax.set_ylim(-1,3)
ax.set_zlim(-1,3)
# Test Cylinder
fig = plt.figure(num=1, clear=True)
2023-12-15 11:00:30 -05:00
ax = fig.add_subplot(projection='3d')
2023-03-15 15:38:14 -04:00
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()
cylinder = Cylinder(ax, radius=0.2, height=0.2)
cylinder.transform(position=np.array([[1.0, 0, 0]]), rotation=rotation)
ax.set_xlim(-1,1)
ax.set_ylim(-1,1)
ax.set_zlim(-1,1)
# Test Quadrotor
fig = plt.figure(num=2, clear=True)
2023-12-15 11:00:30 -05:00
ax = fig.add_subplot(projection='3d')
2023-03-15 15:38:14 -04:00
quad = Quadrotor(ax)
quad.transform(position=np.array([[0.5, 0.5, 0.5]]), rotation=np.identity(3))
quad = Quadrotor(ax)
rotation = Rotation.from_rotvec(np.pi/4 * np.array([1, 0, 0])).as_matrix()
quad.transform(position=np.array([[0.8, 0.8, 0.8]]), rotation=rotation)
ax.set_xlim(-1,1)
ax.set_ylim(-1,1)
ax.set_zlim(-1,1)
# Test Cuboid coloring.
fig = plt.figure(num=3, clear=True)
2023-12-15 11:00:30 -05:00
ax = fig.add_subplot(projection='3d')
2023-03-15 15:38:14 -04:00
ax.set_xlim(-3.25,3.25)
ax.set_ylim(-3.25,3.25)
ax.set_zlim(-3.25,3.25)
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('z')
# No shading.
x = (-1, 0, 1)
z = -3.25
cuboid = Cuboid(ax, x_span=0.5, y_span=0.5, z_span=0.5, shade=False)
cuboid.transform(position=(x[0], 0, z))
cuboid = Cuboid(ax, x_span=0.5, y_span=0.5, z_span=0.5, shade=False)
cuboid.transform(position=(x[1], 0, z))
cuboid = Cuboid(ax, x_span=0.5, y_span=0.5, z_span=0.5, shade=False, facecolors='b')
cuboid.transform(position=(x[2], 0, z))
# Shading.
z = z + 1
cuboid = Cuboid(ax, x_span=0.5, y_span=0.5, z_span=0.5)
cuboid.transform(position=(x[0], 0, z))
cuboid = Cuboid(ax, x_span=0.5, y_span=0.5, z_span=0.5)
cuboid.transform(position=(x[1], 0, z))
cuboid = Cuboid(ax, x_span=0.5, y_span=0.5, z_span=0.5, facecolors='b')
cuboid.transform(position=(x[2], 0, z))
# Transparency.
z = z + 1
cuboid = Cuboid(ax, x_span=0.5, y_span=0.5, z_span=0.5, alpha=0.5)
cuboid.transform(position=(x[0], 0, z))
cuboid = Cuboid(ax, x_span=0.5, y_span=0.5, z_span=0.5, alpha=0.5)
cuboid.transform(position=(x[1], 0, z))
cuboid = Cuboid(ax, x_span=0.5, y_span=0.5, z_span=0.5, alpha=0.5, facecolors='b')
cuboid.transform(position=(x[2], 0, z))
# No shading, edge lines.
z = z + 1
cuboid = Cuboid(ax, x_span=0.5, y_span=0.5, z_span=0.5, shade=False, linewidth=1)
cuboid.transform(position=(x[0], 0, z))
cuboid = Cuboid(ax, x_span=0.5, y_span=0.5, z_span=0.5, shade=False, linewidth=1, edgecolors='k')
cuboid.transform(position=(x[1], 0, z))
cuboid = Cuboid(ax, x_span=0.5, y_span=0.5, z_span=0.5, shade=False, linewidth=1, facecolors='b', edgecolors='k')
cuboid.transform(position=(x[2], 0, z))
# Shading, edge lines.
z = z + 1
cuboid = Cuboid(ax, x_span=0.5, y_span=0.5, z_span=0.5, linewidth=1)
cuboid.transform(position=(x[0], 0, z))
cuboid = Cuboid(ax, x_span=0.5, y_span=0.5, z_span=0.5, linewidth=1, edgecolors='k')
cuboid.transform(position=(x[1], 0, z))
cuboid = Cuboid(ax, x_span=0.5, y_span=0.5, z_span=0.5, linewidth=1, facecolors='b', edgecolors='k')
cuboid.transform(position=(x[2], 0, z))
# Transparency, edge lines.
z = z + 1
cuboid = Cuboid(ax, x_span=0.5, y_span=0.5, z_span=0.5, alpha=0.5, linewidth=1)
cuboid.transform(position=(x[0], 0, z))
cuboid = Cuboid(ax, x_span=0.5, y_span=0.5, z_span=0.5, alpha=0.5, linewidth=1, edgecolors='k')
cuboid.transform(position=(x[1], 0, z))
cuboid = Cuboid(ax, x_span=0.5, y_span=0.5, z_span=0.5, alpha=0.5, linewidth=1, facecolors='b', edgecolors='k')
cuboid.transform(position=(x[2], 0, z))
# Transparent edges.
z = z + 1
cuboid = Cuboid(ax, x_span=0.5, y_span=0.5, z_span=0.5, alpha=0, linewidth=1, edgecolors='k', antialiased=False)
cuboid.transform(position=(x[1], 0, z))
cuboid = Cuboid(ax, x_span=0.5, y_span=0.5, z_span=0.5, alpha=0, linewidth=1, edgecolors='k', antialiased=True)
cuboid.transform(position=(x[2], 0, z))
# Draw all figures.
plt.show()