SOLVED Draw editable spline in the viewport

Hi,
I would like to draw a spline similar way it's drawing in the editor, with gradient from white to blue, and the points.
As well as I'd like the points to be selectable.

I've came to this for now:

import c4d

class SplineTest(c4d.plugins.ObjectData):
    def Init(self, op):
        self.LINE_DRAW = None
        return True

    def GetVirtualObjects(self, op, hh):
        hierarchyCloneRes = op.GetAndCheckHierarchyClone(hh, op.GetDown(), c4d.HIERARCHYCLONEFLAGS_ASSPLINE, False) if op.GetDown() else None
        spline = c4d.SplineObject(0, c4d.SPLINETYPE_LINEAR)
        if hierarchyCloneRes:
            if not hierarchyCloneRes["dirty"]:
                return hierarchyCloneRes["clone"]
            if hierarchyCloneRes["clone"].IsInstanceOf(c4d.Ospline) or hierarchyCloneRes["clone"].IsInstanceOf(c4d.Oline):
                spline = hierarchyCloneRes["clone"]
        sh = c4d.utils.SplineHelp()
        if sh.InitSplineWith(spline, c4d.SPLINEHELPFLAGS_RETAINLINEOBJECT):
            self.LINE_DRAW = sh.GetLineObject().GetClone()
            sh.FreeSpline()
        return spline

    def Draw(self, op, drawpass, bd, bh):
        doc = bh.GetDocument()
        if drawpass != c4d.DRAWPASS_OBJECT or self.LINE_DRAW is None or self.LINE_DRAW.GetPointCount() == 0 or not bh.IsActive() or not doc.IsEditMode():
            return c4d.DRAWRESULT_SKIP

        # this draw is ignored
        bd.DrawPolygonObject(bh, self.LINE_DRAW, c4d.DRAWOBJECT_USE_CUSTOM_COLOR, None, c4d.GetViewColor(c4d.VIEWCOLOR_SPLINESTART),
        )

        if doc.GetMode() == c4d.Mpoints:
            opPoints = self.LINE_DRAW.GetAllPoints()
            pSelection = self.LINE_DRAW.GetPointS().GetAll(len(opPoints))
            selected = [opPoints[index] for index, isSelected in enumerate(pSelection) if isSelected]
            nonselected = [opPoints[index] for index, isSelected in enumerate(pSelection) if not isSelected]

            bd.SetMatrix_Matrix(op, bh.GetMg())
            bd.SetPointSize(6)
            bd.LineZOffset(2)

            if len(nonselected):
                bd.SetPen(c4d.GetViewColor(c4d.VIEWCOLOR_INACTIVEPOINT))
                bd.DrawPoints(nonselected)

            if len(selected):
                bd.SetPen(c4d.GetViewColor(c4d.VIEWCOLOR_ACTIVEPOINT))
                bd.DrawPoints(selected)

        return c4d.DRAWRESULT_OK

if __name__ == "__main__":
    c4d.plugins.RegisterObjectPlugin(id = 1000001, str = 'SplineTest', g = SplineTest, description = "osplinetest", icon = None, info = c4d.OBJECT_GENERATOR | c4d.OBJECT_INPUT | c4d.OBJECT_ISSPLINE)

Issue is object lines are not being drawn with custom color.
Instead this object is drawn as maybe with c4d.VIEWCOLOR_ACTIVEBOX (visually its drawn as orange in R25)

  1. Is it possible to draw spline similar to editor with existing toolset, and If not, what approach should be used here?
  2. And is it possible to let points be selected in viewport, but not using c4d.OBJECT_POINTOBJECT as generator flag (in that case generator become Opoint type, and it's not treated as spline anymore.

Hello @baca,

Thank you for reaching out to us. I will answer in bullet points.

  1. You are overwriting the wrong method. When setting the ObjectData flag OBJECT_ISSPLINE, one should overwrite GetContour and not GetVirtualObjects. The former is the method to implement spline object generators, while the latter is the method for polygon object generators (and other things).
  2. Even when one flags a generator plugin as OBJECT_POINTOBJECT or OBJECT_POLYGONOBJECT, one must do all the document mode dependent drawing and logic oneself. These flags are merely indicators for Cinema 4D that a plugin implements such logic. There is no free lunch here 😞
  3. Drawing a spline in such gradient fashion is possible, but there are no automatisms for it. One must draw multiple line segments to realize drawing a line segment with a gradient. As with most drawing operations, Python is not the ideal language for this, and one might run into slowdowns quickly.
  4. While we have internally quite a few cases which mimic point/polygon objects in this fashion, like for example the Bezier object, we have none for splines. I do not see anything right away which would prevent you from implementing such custom spline object, but you are still entering unexplored territory here. You might encounter hard to solve or even unsolvable problems.
  5. While it is technically possible to write this yourself, it should be pointed out that the internal implementations all rely on PointObject::DisplayPoints, a non-public method of the PointObject exposed in the public API, to draw the vertices of an object, including selection states and transform gizmos.

So, your code is 'correct' for the most part, at least for a polygon object. You return something in GVO to let Cinema 4D handle the polygon shading and then draw vertices and edges on top of that (which is what you are supposed to do). However, what at least I would not do, is storing the point data in self.LINE_DRAW. I would suggest accessing the cache of the node instead.

In the end it is also not quite clear what you want to do. I understand that you want to draw the vertices and 'gradient' for the line segments of a spline. What is not quite clear to me is if you actually need an object (the stated could also be a tool) and if the user should be able to select and manipulate vertices. I would be best if you could clarify this.

Especially regarding the point of wanting to draw a gradient of line segments, I would strongly suggest moving on to C++, as you will have much better chances to implement such a custom thing in that language.

Cheers,
Ferdinand

@ferdinand huge thanks for detailed reply, as always.

  1. I didn't shared whole code here, but in GVO I'm also caching the spline as self._spline property and in GetSpline method I'm returning self._spline.GetClone()
    When dealing with child input object GetAndCheckHierarchyClone has no alternatives except weird combination of CSTO + Touch()
  2. //
  3. So solution might be to draw tiny polygons with gradient in vertex colors? Any suggestion for draw pas in that case - DRAWPASS_OBJECT or DRAWPASS_HANDLER?
    Current issue is that my object is drawn automatically, and I have no idea if I can prevent automatic drawing. I can render custom color only in case, if my object is not selected.
  4. //
  5. PointObject::DisplayPoints is only available under C++? Didn't found any references in both Py and Cpp documentation
  6. Yeah, thanks for suggestion to not store source line, as it's not reflecting further deformer changes. But I found that Draw is called so frequent, and converting spline to line takes sufficient time, so it becomes uncomfortable to work with.
    Maybe I need to check self DIRTYFLAGS_CACHE before rebuilding line object, not sure.
  7. I wanted to render vertices and spline direction not for manipulation, but for visual check.
    And thought to implement point selection feature for selection tag creating, maybe.

Hey @baca,

  1. I didn't shared whole code here, but in GVO I'm also caching the spline as self._spline property and in GetSpline method I'm returning self._spline.GetClone()
    When dealing with child input object GetAndCheckHierarchyClone has no alternatives except weird combination of CSTO + Touch()

Hm, that is sometimes true, but at least from your example code this was not obvious. And I would make if this is really the only way to go. There come penalties with implementing a spline in this manner, the major one being, that you will always have another layer of caches, which in turn will impact other things. A node which implements a spline via GetVirtualObjects will return a SplineObject as its cache (or part of its cache). This SplineObject will then a have a LineObject cache. A node which implements a spline via GetContour, will return a LineObject directly.

  1. So, the solution might be to draw tiny polygons with gradient in vertex colors? Any suggestion for draw pass in that case - DRAWPASS_OBJECT or DRAWPASS_HANDLER?
    Current issue is that my object is drawn automatically, and I have no idea if I can prevent automatic drawing. I can render custom color only in the case my object is not selected.

Yes, sort of. I was thinking more of line segments than polygons, but that is the general idea. I would also not draw in object space, but in screen space, to be as efficient and smooth as possible. When you draw in object space, and you choose the spacing to wide, the gradient will be visibly segmented when projected to pixel screen space. When you choose the spacing to tightly in object space, you will end up with many unnecessary drawing operations which are not going to be visible in screen space anyways.

The drawpass in which you should be drawing depends on what you want to do, but usually DRAWPASS_OBJECT is the one to go. The more important information is here the z-depth for drawing the line segments, which should be 4 or greater, so that the line segments are drawn over shaded polygons and other stuff.

But I would here point again that Python is not a good match for this task due to performance restrictions. If your splines are simple and drawing a gradient for them just means <= 10,000 draw calls in screen space, you will be fine in Python. But when your splines are complex, and drawing a gradient for them means drawing 100,000s of pixels (i.e., draw calls), Python will bottleneck you hard. The old viewport API is not the fastest even in C++, so such complex tasks should really be done there. I would suggest drawing only the vertices in Python and expressing the direction of the spline by colouring the vertices.

  1. PointObject::DisplayPoints is only available under C++? Didn't found any references in both Py and Cpp documentation

As I said, this method is non-public. Some of the public interfaces/types have non-public methods. I simply pointed out that we use this method internally, and that it does a lot of things regarding displaying vertices in the viewport. So, when you implement a OBJECT_POINTOBJECT ObjectData plugin, it is somewhat intended from the internal perspective that you use then ::DisplayPoints. I was simply informing you that the inaccessibility of DisplayPoints is one of the obstacles you must overcome.

  1. Yeah, thanks for suggestion to not store source line, as it's not reflecting further deformer changes. But I found that Draw is called so frequent, and converting spline to line takes sufficient time, so it becomes uncomfortable to work with.

When you implement your spline in GetContour this will be more straight forward, as the cache will then be directly the line-object, but you can do more or less the same withGetVirtuaalObjects, you only must deal with a slightly more complex cache then. But it should beat storing and retrieving the data manually in any case.

  1. I wanted to render vertices and spline direction not for manipulation, but for visual check.
    And thought to implement point selection feature for selection tag creating, maybe.

I have provided a very simple pattern for that at the end. I did not implement the selecting, creating, and moving vertices stuff, as this would be quite a bit of work. I also went the route of shading the vertices and not the line segments, as this is much simpler to do 🙂

Cheers,
Ferdinand

The result:
spline_stuff.gif

The code:

import c4d

class EditableSplineData(c4d.plugins.ObjectData):

    def Draw(self, op, drawpass, bd, bh):
        """
        """
        if drawpass != c4d.DRAWPASS_OBJECT:
            super().Draw(op, drawpass, bd, bh)

        # Only display the vertices when the object is selected.
        if (not op.GetBit(c4d.BIT_ACTIVE)):
            return c4d.DRAWRESULT_OK

        cache = op.GetCache()
        if not isinstance(cache, c4d.LineObject):
            return c4d.DRAWRESULT_OK

        # Draw in object space with a zoffset of 4 over most things Cinema 4D will draw as polygons,
        # edges, and vertices.
        bd.SetMatrix_Matrix(op, op.GetMg(), zoffset=4)

        # Now we simply will draw a dot for each vertex in the LineObject, with a gradient going 
        # from the first vertex to the last vertex. Note that we are drawing the vertices of the
        # LineObject, not the ones of the SplineObject. I.e., we are not drawing the control 
        # vertices of the spline, but the actual vertices which have been interpolated between the
        # control vertices. In this case we will draw 3 * 64 vertices, since we define 4 control 
        # points of a non-closed spline (i.e., 3 segments between them) and a uniform interpolation
        # of 64.
        count = cache.GetPointCount()
        cStart = c4d.Vector(1, 0, 0)
        cEnd = c4d.Vector(0, 0, 1)
        colors = []

        # We are going to use a single draw call for doing this, DrawPoints, which also allows for
        # drawing each point in a different color. But it expects the colors in a bit annoying 
        # format as an array of floats:  [r, g, b, r, g, b, r, g, b,  ...]. 
        for i in range(count):
            ci = c4d.utils.MixVec(cStart, cEnd, i / count)
            colors += [ci.x, ci.y, ci.z]

        # Draw all points in one go, this is very performant.
        bd.SetPointSize(4)
        bd.DrawPoints(vp=cache.GetAllPoints(), vc=colors, colcnt=3)

        return super().Draw(op, drawpass, bd, bh)


    def GetContour(self, op, doc, lod, bt):
        """
        """
        # I am not going to comment the geometry construction I am doing here. For details on
        # polygon and spline generation, see https://bit.ly/3lxBBlt
        node = c4d.SplineObject(4, c4d.SPLINETYPE_LINEAR)

        size = 100.0
        node.SetAllPoints([c4d.Vector(0, 0, 0), 
                             c4d.Vector(size, 0, 0),
                             c4d.Vector(size, size, 0),
                             c4d.Vector(0, size, 0)])
        node.Message(c4d.MSG_UPDATE)

        # I am just going to be lazy and not tie all the interpolation parameters in and instead
        # set them manually here.
        node[c4d.SPLINEOBJECT_INTERPOLATION] = c4d.SPLINEOBJECT_INTERPOLATION_UNIFORM
        node[c4d.SPLINEOBJECT_SUB] = 64

        return node

if __name__ == "__main__":
    c4d.plugins.RegisterObjectPlugin(
        id = 1000001, 
        str = 'Editable Spline', 
        g = EditableSplineData, 
        description = "oeditspline", 
        icon = None, 
        info = c4d.OBJECT_GENERATOR | c4d.OBJECT_ISSPLINE)

@ferdinand thanks for explanations and example.