Init
This commit is contained in:
437
rotorpy/utils/shapes.py
Normal file
437
rotorpy/utils/shapes.py
Normal file
@@ -0,0 +1,437 @@
|
||||
"""
|
||||
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
|
||||
|
||||
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))
|
||||
self.normals = np.asarray(self.ax._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)
|
||||
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
|
||||
colset = self.ax._shade_colors(self.facecolors, normals)
|
||||
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)
|
||||
self.normals = np.asarray(self.ax._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)
|
||||
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
|
||||
colset = self.ax._shade_colors(self.facecolors, normals)
|
||||
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)
|
||||
self.normals = np.asarray(self.ax._generate_normals(self.verts))
|
||||
|
||||
# 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
|
||||
colset = self.ax._shade_colors(self.color, normals)
|
||||
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):
|
||||
|
||||
self.ax = ax
|
||||
|
||||
# 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)]
|
||||
self.artists = tuple(itertools.chain.from_iterable(r.artists for r in self.rotors))
|
||||
self.transform(np.zeros((3,)), np.identity(3))
|
||||
|
||||
def transform(self, position, rotation):
|
||||
position.shape = (3,1)
|
||||
for (r, pos) in zip(self.rotors, self.rotor_position.T):
|
||||
pos.shape = (3,1)
|
||||
r.transform(np.matmul(rotation,pos)+position, rotation)
|
||||
|
||||
if __name__ == '__main__':
|
||||
from axes3ds import Axes3Ds
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
# Test Face
|
||||
fig = plt.figure(num=4, clear=True)
|
||||
ax = Axes3Ds(fig)
|
||||
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)
|
||||
ax = Axes3Ds(fig)
|
||||
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)
|
||||
ax = Axes3Ds(fig)
|
||||
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)
|
||||
ax = Axes3Ds(fig)
|
||||
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)
|
||||
ax = Axes3Ds(fig)
|
||||
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()
|
||||
Reference in New Issue
Block a user