I am trying to draw an object defined by several vertices and then rotate it by defining a face and a vector that I want that face to be normal to. It works fine if I choose a vector that is parallel to the x, y, or z axis but anything different than that makes it skewed. In my code below, line 104 is the vector that makes it skewed if you want to test. Does anyone know why this is happening? I feel there is some offset that needs to be applied but I have no idea how to calculate it. import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D from mpl_toolkits.mplot3d.art3d import Poly3DCollection import numpy as np # Create a new figure with 3D projection fig = plt.figure() ax = fig.add_subplot(111, projection='3d') def rotate_shape_to_normal(vertices, face_indices, target_normal): """ Rotate vertices so that the face specified by face_indices becomes normal to target_normal. aram vertices: List or numpy array of 3D points defining the shape aram face_indices: Indices of the vertices that form the face to align aram target_normal: The vector to which the face should be normal :return: Rotated vertices """ vertices = np.array(vertices) # Convert to numpy array if it isn't already face_vertices = vertices[face_indices] # Calculate the normal vector of the face v1 = face_vertices[1] - face_vertices[0] v2 = face_vertices[2] - face_vertices[0] face_normal = np.cross(v1, v2) print(face_normal) ax.quiver(*face_vertices[0], *face_normal, color='r', length=2) ax.quiver(*face_vertices[0], *target_normal, color='b', length=2) face_normal = face_normal / np.linalg.norm(face_normal) # Normalize # Normalize target_normal for consistency target_normal = target_normal / np.linalg.norm(target_normal) # Compute the rotation axis (perpendicular to both vectors) rotation_axis = np.cross(face_normal, target_normal) rotation_axis = rotation_axis / np.linalg.norm(rotation_axis)# if np.linalg.norm(rotation_axis) > 0 else np.array([1, 0, 0]) # Default to x-axis if parallel print("rotation_axis", rotation_axis) # Compute the angle between the vectors cos_theta = np.dot(face_normal, target_normal) print("cos_theta", np.degrees(cos_theta)) theta = np.arccos(np.clip(cos_theta, -1.0, 1.0)) # Clip to avoid floating-point issues print("theta", np.degrees(theta)) # Check if vectors are already aligned or opposite if np.isclose(theta, 0) or np.isclose(theta, np.pi): return vertices # No rotation needed # Rodrigues' rotation formula K = np.array([ [0, -rotation_axis[2], rotation_axis[1]], [rotation_axis[2], 0, -rotation_axis[0]], [-rotation_axis[1], rotation_axis[0], 0] ]) rotation_matrix = np.sin(theta) * K + (1 - np.cos(theta)) * np.outer(rotation_axis, rotation_axis) # Apply the rotation to all vertices return np.dot(vertices, rotation_matrix.T) # Define the vertices for a simple satellite model: vertices = np.array([ # Main body (cube) (0, 0, 0), # 0 - Base, front-left (1, 0, 0), # 1 - Base, front-right (1, 1, 0), # 2 - Base, back-right (0, 1, 0), # 3 - Base, back-left (0, 0, 1), # 4 - Top, front-left (1, 0, 1), # 5 - Top, front-right (1, 1, 1), # 6 - Top, back-right (0, 1, 1), # 7 - Top, back-left # Solar panels # Left panel (0.5, .25, -1.5), # 8 (0.5, .75, -1.5), # 9 (0.5, .75, 0), # 10 (0.5, .25, 0), # 11 # Right panel (0.5, .25, 2.5), # 12 (0.5, .75, 2.5), # 13 (0.5, .75, 1), # 14 (0.5, .25, 1) # 15 ]) # Translate spacecraft to origin vertices = [(x - .5, y - .5, z - .5) for x, y, z in vertices] # Original points ax.scatter(vertices[5][0], vertices[5][1],vertices[5][2], color='green', s=25) ax.scatter(vertices[1][0], vertices[1][1],vertices[1][2], color='green', s=25) ax.scatter(vertices[4][0], vertices[4][1],vertices[4][2], color='green', s=25) # Rotate spacecraft to point at specified vector face_to_align = [5, 1, 4] # Indexes of points that define the face I want to be normal to the specified vector (-Y) #vector = np.array([0, 0, 1]) # This rotation doesn't skew anything vector = np.array([0.56167836, 0.76075023, 0.32523301]) # This rotation skews everything vertices = rotate_shape_to_normal(vertices, face_to_align, vector) # Rotated points ax.scatter(vertices[5][0], vertices[5][1],vertices[5][2], color='blue', s=25) ax.scatter(vertices[1][0], vertices[1][1],vertices[1][2], color='blue', s=25) ax.scatter(vertices[4][0], vertices[4][1],vertices[4][2], color='blue', s=25) # Faces for the cube cube_faces = [ [vertices[0], vertices[1], vertices[5], vertices[4]], # (-Y) [vertices[1], vertices[2], vertices[6], vertices[5]], # (+X) [vertices[2], vertices[3], vertices[7], vertices[6]], # (+Y) [vertices[3], vertices[0], vertices[4], vertices[7]], # (-X) [vertices[0], vertices[1], vertices[2], vertices[3]], # (-Z) [vertices[4], vertices[5], vertices[6], vertices[7]], # (+Z) ] # Solar panel faces - note these are just rectangles left_panel = [vertices[8], vertices[9], vertices[10], vertices[11]] right_panel = [vertices[12], vertices[13], vertices[14], vertices[15]] # Combine all faces faces = cube_faces + [left_panel, right_panel] # Create Poly3DCollection poly3d = Poly3DCollection(faces, alpha=0.7) poly3d.set_edgecolor('k') # Set face color for different parts poly3d.set_facecolor([[1, 0, 0], [.7, .7, .7], [.7, .7, .7], [.7, .7, .7], [.7, .7, .7], [.7, .7, .7], [0, 0.5, 1], [0, 0.5, 1]]) ax.add_collection3d(poly3d) # Set the aspect ratio to ensure it looks like a cube ax.set_box_aspect((1, 1, 1)) # Remove axes for a cleaner look #ax.set_axis_off() # Set limits to see everything ax.set_xlim(-1, 2) ax.set_ylim(-1, 2) ax.set_zlim(-1, 2) # Origin ax.scatter(0, 0, 0, color='red', s=25) ax.set_xlabel('X') ax.set_ylabel('Y') ax.set_zlabel('Z') # Adjust view for better visualization ax.view_init(elev=20., azim=-45) plt.show() Continue reading...