[Python] Matplotlib polygons: Polygons are skewed after rotation

Discussão em 'Python' iniciado por Stack, Fevereiro 9, 2025.

  1. Stack

    Stack Membro Participativo

    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.

    :param vertices: List or numpy array of 3D points defining the shape
    :param face_indices: Indices of the vertices that form the face to align
    :param 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...

Compartilhe esta Página