Set Spline Tangents with Python Generator



  • My goal is to use a Python generator to create a spline with Bezier interpolation where each point has tangents lying in the same plane as the plane spanned by the point itself and its two adjacent points and is also parallel to the line going through the two adjacent points. C4D does this natively when you double click on a point. See the following video example:

    https://www.youtube.com/watch?v=Ug4Pxk9lfns&feature=youtu.be

    The video does exactly what I want, but now I need to figure out the mathematics of this in the Python generator. This is the code that I have so far, but I don't understand how to calculate the tangents to get exactly what I want:

    def main():
        verts = [c4d.Vector(50*ii, 50*(-1)**ii, 50*(-1)**int(ii/2)) for ii in range(10)] # just a sample
        total_points = len(verts)
        spline = c4d.SplineObject(total_points, c4d.SPLINETYPE_BEZIER)
        spline.ResizeObject(total_points, 1)
        spline.SetSegment(0, total_points, False)
    
        mag = op[c4d.ID_USERDATA,1]
        
        for pntnum, vert in enumerate(verts):
            spline.SetPoint(pntnum, vert)
            # i need to set the tangent here somehow!
    
        spline.Message(c4d.MSG_UPDATE)
        return spline
    

    The magnitude of the tangents would be a scalar value that I would want to control separately. If you could help me figure out how to calculate the tangents that are identical to what C4D created automatically in that video (besides magnitude), that would really help me out. Does anybody know the mathematics to calculate these tangents?



  • Hi,

    the common way to do this would be constructing a frame for that vertex. Normally you would also not separate the calculation of the tangent lengths, because you have everything you need right there when building the frame. Here is a narrative example. Start reading in the main() function.

    """Example for constructing smooth tangents in a bezier spline.
    
    Run this with a spline object selected that has at least three vertices. It
    will set the tangents of the second vertex of the spline in respect to its
    neighbors.
    """
    
    import c4d
    
    
    def project_on_line(p, a, b):
        """Projects the point p onto the line which contains the line segment ab.
    
        Args:
            p (c4d.Vector): The point to project.
            a (c4d.Vector): The first point of the line segment.
            b (c4d.Vector): The second point of the line segment.
    
        Returns:
            c4d.Vector: The projection of p onto the line described by the line
             segment ab.
        """
        # Not much to explain here, check a trigonometry textbook on point line
        # projections / the dot product if you are lost.
        ab = b - a
        t = ((p - a) * ab) / ab.GetLengthSquared()
        return a + ab * t
    
    
    def construct_vertex_frame(p, q, r):
        """Constructs a frame for a spline vertex and its two normalized tangent
         lengths.
    
        Args:
            p (c4d.Vector): The point of the vertex for which to construct the
             frame.
            q (c4d.Vector): The point of the vertex which is the vertex before p.
            r (c4d.Vector): The point of the vertex which is the vertex after p.
    
        Returns:
            float, float, c4d.Vector: The normalized length of the left and
             right tangent and the frame.
        """
    
        # The Vectors pointing from p to q and from p to r.
        a, b = q - p, r - p
    
        # Now we construct our frame axis, I did use axis labels instead of the
        # terms normal, binormal and tangent to keep things unambiguous.
    
        # The x-axis or tangent of the frame, i.e. the axis along which we will
        # place out tangent control points.
        x = (r - q).GetNormalized()
        # The z-axis or normal of the frame, which is the normal to the plane
        # of the three points. We just take the cross product of two vectors that
        # lie in that plane.
        z = (x % a).GetNormalized()
        # The y axis or binormal of the frame, we just build it with the two
        # other axis.
        y = (x % z).GetNormalized()
        # The finished matrix/frame. It is important to set the offset to the
        # zero vector and not p, since each tangent pair lives in its own tangent
        # space relative to their vertex.
        frame = c4d.Matrix(c4d.Vector(), x, y, z)
    
        # We are technically finished here, but we could do some more, as we
        # will have to determine the length of each tangent. We could just set
        # these to a fraction of the vectors a, b, but this will give us odd
        # results when theta, the angle between a and b is getting smaller and
        # smaller. Instead we will project both a and b onto x and return the
        # lengths of those two projections as the normalized length for the
        # respective tangent.
        origin = c4d.Vector()
        tlength_left = project_on_line(a, origin, x).GetLength()
        tlength_right = project_on_line(b, origin, x).GetLength()
    
        # Return the length of the left and right tangent and the frame.
        return tlength_left, tlength_right, frame
    
    
    def align_tangents(node, i, rel_scale_left=.5, rel_scale_right=.5):
        """Aligns the tangents of a spline at a given index.
    
        Calculates the frame for the given vertex and sets its tangents for a
        smooth interpolation in respect to its two neighbors. Will also force
        the spline into bezier mode.
    
        Note:
            This currently cannot handle the first and last vertex. It is trivial
            to implement this, but things would have gotten more bloated, so I
            kept short and (slightly) flawed.
    
        Args:
            node (c4d.SplineObject): The spline.
            i (int): The index of the vertex to align the tangents for. Has to
             satisfy "0 < i < vertex_count - 1".
            rel_scale_left (float, optional): The relative scale of the left
             tangent. Defaults to 50%.
            rel_scale_right (float, optional): The relative scale of the right
             tangent. Defaults to 50%.
    
        Raises:
            TypeError: When 'node' is not a SplineObject. 
            ValueError: When 'i' is out of bounds.
        """
        if not isinstance(node, c4d.SplineObject):
            raise TypeError("Expected SplineObject as input.")
    
        points = node.GetAllPoints()
        # Out of bounds condition for the spline vertex indices.
        if i < 1 or len(points) < i + 1:
            raise ValueError("Index out of bounds.")
    
        # The three consecutive spline vertices that form the plane. p is
        # the mid vertex for which we want to set the tangents and q and r
        # are the vertices previous and next to it.
        q, p, r = points[i-1:i+2]
        # The normalized length of the tangents and the frame.
        tlength_left, tlength_right, frame = construct_vertex_frame(p, q, r)
    
        # Constructing the tangents:
        tangent_left = c4d.Vector(-tlength_left * rel_scale_left, 0, 0) * frame
        tangent_right = c4d.Vector(tlength_right * rel_scale_right, 0, 0) * frame
    
        # Update the spline.
        if node[c4d.SPLINEOBJECT_TYPE] is not c4d.SPLINETYPE_BEZIER:
            node[c4d.SPLINEOBJECT_TYPE] = c4d.SPLINETYPE_BEZIER
        node.SetTangent(i, tangent_left, tangent_right)
        node.Message(c4d.MSG_UPDATE)
    
        # For visualization purposes we insert the frame of our vertex as a
        # null object. SHOULD BE COMMENTED OUT.
        null = c4d.BaseList2D(c4d.Onull)
        null.SetMg(frame)
        null.SetName("frame_for_vertex_" + str(i))
        doc.InsertObject(null)
    
        # Push the changes to Cinema.
        c4d.EventAdd()
    
    
    def main():
        """Entry point.
        """
        # ALign the tangents of the second vertex of the selected spline object.
        if op:
            align_tangents(op, 1)
    
    
    if __name__ == "__main__":
        main()
    
    

    Cheers,
    zipit



  • @zipit said in Set Spline Tangents with Python Generator:

    > """Example for constructing smooth tangents in a bezier spline.
    > 
    > Run this with a spline object selected that has at least three vertices. It
    > will set the tangents of the second vertex of the spline in respect to its
    > neighbors.
    > """
    > 
    > import c4d
    > 
    > 
    > def project_on_line(p, a, b):
    >     """Projects the point p onto the line which contains the line segment ab.
    > 
    >     Args:
    >         p (c4d.Vector): The point to project.
    >         a (c4d.Vector): The first point of the line segment.
    >         b (c4d.Vector): The second point of the line segment.
    > 
    >     Returns:
    >         c4d.Vector: The projection of p onto the line described by the line
    >          segment ab.
    >     """
    >     # Not much to explain here, check a trigonometry textbook on point line
    >     # projections / the dot product if you are lost.
    >     ab = b - a
    >     t = ((p - a) * ab) / ab.GetLengthSquared()
    >     return a + ab * t
    > 
    > 
    > def construct_vertex_frame(p, q, r):
    >     """Constructs a frame for a spline vertex and its two normalized tangent
    >      lengths.
    > 
    >     Args:
    >         p (c4d.Vector): The point of the vertex for which to construct the
    >          frame.
    >         q (c4d.Vector): The point of the vertex which is the vertex before p.
    >         r (c4d.Vector): The point of the vertex which is the vertex after p.
    > 
    >     Returns:
    >         float, float, c4d.Vector: The normalized length of the left and
    >          right tangent and the frame.
    >     """
    > 
    >     # The Vectors pointing from p to q and from p to r.
    >     a, b = q - p, r - p
    > 
    >     # Now we construct our frame axis, I did use axis labels instead of the
    >     # terms normal, binormal and tangent to keep things unambiguous.
    > 
    >     # The x-axis or tangent of the frame, i.e. the axis along which we will
    >     # place out tangent control points.
    >     x = (r - q).GetNormalized()
    >     # The z-axis or normal of the frame, which is the normal to the plane
    >     # of the three points. We just take the cross product of two vectors that
    >     # lie in that plane.
    >     z = (x % a).GetNormalized()
    >     # The y axis or binormal of the frame, we just build it with the two
    >     # other axis.
    >     y = (x % z).GetNormalized()
    >     # The finished matrix/frame. It is important to set the offset to the
    >     # zero vector and not p, since each tangent pair lives in its own tangent
    >     # space relative to their vertex.
    >     frame = c4d.Matrix(c4d.Vector(), x, y, z)
    > 
    >     # We are technically finished here, but we could do some more, as we
    >     # will have to determine the length of each tangent. We could just set
    >     # these to a fraction of the vectors a, b, but this will give us odd
    >     # results when theta, the angle between a and b is getting smaller and
    >     # smaller. Instead we will project both a and b onto x and return the
    >     # lengths of those two projections as the normalized length for the
    >     # respective tangent.
    >     origin = c4d.Vector()
    >     tlength_left = project_on_line(a, origin, x).GetLength()
    >     tlength_right = project_on_line(b, origin, x).GetLength()
    > 
    >     # Return the length of the left and right tangent and the frame.
    >     return tlength_left, tlength_right, frame
    > 
    > 
    > def align_tangents(node, i, rel_scale_left=.5, rel_scale_right=.5):
    >     """Aligns the tangents of a spline at a given index.
    > 
    >     Calculates the frame for the given vertex and sets its tangents for a
    >     smooth interpolation in respect to its two neighbors. Will also force
    >     the spline into bezier mode.
    > 
    >     Note:
    >         This currently cannot handle the first and last vertex. It is trivial
    >         to implement this, but things would have gotten more bloated, so I
    >         kept short and (slightly) flawed.
    > 
    >     Args:
    >         node (c4d.SplineObject): The spline.
    >         i (int): The index of the vertex to align the tangents for. Has to
    >          satisfy "0 < i < vertex_count - 1".
    >         rel_scale_left (float, optional): The relative scale of the left
    >          tangent. Defaults to 50%.
    >         rel_scale_right (float, optional): The relative scale of the right
    >          tangent. Defaults to 50%.
    > 
    >     Raises:
    >         TypeError: When 'node' is not a SplineObject. 
    >         ValueError: When 'i' is out of bounds.
    >     """
    >     if not isinstance(node, c4d.SplineObject):
    >         raise TypeError("Expected SplineObject as input.")
    > 
    >     points = node.GetAllPoints()
    >     # Out of bounds condition for the spline vertex indices.
    >     if i < 1 or len(points) < i + 1:
    >         raise ValueError("Index out of bounds.")
    > 
    >     # The three consecutive spline vertices that form the plane. p is
    >     # the mid vertex for which we want to set the tangents and q and r
    >     # are the vertices previous and next to it.
    >     q, p, r = points[i-1:i+2]
    >     # The normalized length of the tangents and the frame.
    >     tlength_left, tlength_right, frame = construct_vertex_frame(p, q, r)
    > 
    >     # Constructing the tangents:
    >     tangent_left = c4d.Vector(-tlength_left * rel_scale_left, 0, 0) * frame
    >     tangent_right = c4d.Vector(tlength_right * rel_scale_right, 0, 0) * frame
    > 
    >     # Update the spline.
    >     if node[c4d.SPLINEOBJECT_TYPE] is not c4d.SPLINETYPE_BEZIER:
    >         node[c4d.SPLINEOBJECT_TYPE] = c4d.SPLINETYPE_BEZIER
    >     node.SetTangent(i, tangent_left, tangent_right)
    >     node.Message(c4d.MSG_UPDATE)
    > 
    >     # For visualization purposes we insert the frame of our vertex as a
    >     # null object. SHOULD BE COMMENTED OUT.
    >     null = c4d.BaseList2D(c4d.Onull)
    >     null.SetMg(frame)
    >     null.SetName("frame_for_vertex_" + str(i))
    >     doc.InsertObject(null)
    > 
    >     # Push the changes to Cinema.
    >     c4d.EventAdd()
    > 
    > 
    > def main():
    >     """Entry point.
    >     """
    >     # ALign the tangents of the second vertex of the selected spline object.
    >     if op:
    >         align_tangents(op, 1)
    > 
    > 
    > if __name__ == "__main__":
    >     main()
    

    This was extremely helpful, thank you very much. My 3d geometry / Calc III is pretty rusty and seeing this really helped.


Log in to reply