How can matplotlib 2D patches be transformed to 3D with arbitrary normals?

后端 未结 3 1400
天命终不由人
天命终不由人 2020-12-01 16:52

Short question

How can matplotlib 2D patches be transformed to 3D with arbitrary normals?

Long question

I would like to plot Patches in axes with

3条回答
  •  温柔的废话
    2020-12-01 17:27

    Short answer

    Copy the code below into your project and use the method

    def pathpatch_2d_to_3d(pathpatch, z = 0, normal = 'z'):
        """
        Transforms a 2D Patch to a 3D patch using the given normal vector.
    
        The patch is projected into they XY plane, rotated about the origin
        and finally translated by z.
        """
    

    to transform your 2D patches to 3D patches with arbitrary normals.

    from mpl_toolkits.mplot3d import art3d
    
    def rotation_matrix(d):
        """
        Calculates a rotation matrix given a vector d. The direction of d
        corresponds to the rotation axis. The length of d corresponds to 
        the sin of the angle of rotation.
    
        Variant of: http://mail.scipy.org/pipermail/numpy-discussion/2009-March/040806.html
        """
        sin_angle = np.linalg.norm(d)
    
        if sin_angle == 0:
            return np.identity(3)
    
        d /= sin_angle
    
        eye = np.eye(3)
        ddt = np.outer(d, d)
        skew = np.array([[    0,  d[2],  -d[1]],
                      [-d[2],     0,  d[0]],
                      [d[1], -d[0],    0]], dtype=np.float64)
    
        M = ddt + np.sqrt(1 - sin_angle**2) * (eye - ddt) + sin_angle * skew
        return M
    
    def pathpatch_2d_to_3d(pathpatch, z = 0, normal = 'z'):
        """
        Transforms a 2D Patch to a 3D patch using the given normal vector.
    
        The patch is projected into they XY plane, rotated about the origin
        and finally translated by z.
        """
        if type(normal) is str: #Translate strings to normal vectors
            index = "xyz".index(normal)
            normal = np.roll((1.0,0,0), index)
    
        normal /= np.linalg.norm(normal) #Make sure the vector is normalised
    
        path = pathpatch.get_path() #Get the path and the associated transform
        trans = pathpatch.get_patch_transform()
    
        path = trans.transform_path(path) #Apply the transform
    
        pathpatch.__class__ = art3d.PathPatch3D #Change the class
        pathpatch._code3d = path.codes #Copy the codes
        pathpatch._facecolor3d = pathpatch.get_facecolor #Get the face color    
    
        verts = path.vertices #Get the vertices in 2D
    
        d = np.cross(normal, (0, 0, 1)) #Obtain the rotation vector    
        M = rotation_matrix(d) #Get the rotation matrix
    
        pathpatch._segment3d = np.array([np.dot(M, (x, y, 0)) + (0, 0, z) for x, y in verts])
    
    def pathpatch_translate(pathpatch, delta):
        """
        Translates the 3D pathpatch by the amount delta.
        """
        pathpatch._segment3d += delta
    

    Long answer

    Looking at the source code of art3d.pathpatch_2d_to_3d gives the following call hierarchy

    1. art3d.pathpatch_2d_to_3d
    2. art3d.PathPatch3D.set_3d_properties
    3. art3d.Patch3D.set_3d_properties
    4. art3d.juggle_axes

    The transformation from 2D to 3D happens in the last call to art3d.juggle_axes. Modifying this last step, we can obtain patches in 3D with arbitrary normals.

    We proceed in four steps

    1. Project the vertices of the patch into the XY plane (pathpatch_2d_to_3d)
    2. Calculate a rotation matrix R that rotates the z direction to the direction of the normal (rotation_matrix)
    3. Apply the rotation matrix to all vertices (pathpatch_2d_to_3d)
    4. Translate the resulting object in the z-direction (pathpatch_2d_to_3d)

    Sample source code and the resulting plot are shown below.

    from mpl_toolkits.mplot3d import proj3d
    from matplotlib.patches import Circle
    from itertools import product
    
    ax = axes(projection = '3d') #Create axes
    
    p = Circle((0,0), .2) #Add a circle in the yz plane
    ax.add_patch(p)
    pathpatch_2d_to_3d(p, z = 0.5, normal = 'x')
    pathpatch_translate(p, (0, 0.5, 0))
    
    p = Circle((0,0), .2, facecolor = 'r') #Add a circle in the xz plane
    ax.add_patch(p)
    pathpatch_2d_to_3d(p, z = 0.5, normal = 'y')
    pathpatch_translate(p, (0.5, 1, 0))
    
    p = Circle((0,0), .2, facecolor = 'g') #Add a circle in the xy plane
    ax.add_patch(p)
    pathpatch_2d_to_3d(p, z = 0, normal = 'z')
    pathpatch_translate(p, (0.5, 0.5, 0))
    
    for normal in product((-1, 1), repeat = 3):
        p = Circle((0,0), .2, facecolor = 'y', alpha = .2)
        ax.add_patch(p)
        pathpatch_2d_to_3d(p, z = 0, normal = normal)
        pathpatch_translate(p, 0.5)
    

    Resulting plot

提交回复
热议问题