Cookbook recipe #3

Generating a dynamic spline in an ObjectData plugin

If you want to generate a spline object in an ObjectData plugin, there are two functions you can use: GetContour() or GetVirtualObjects(). For generating a static spline – one that isn’t going to change on a frame-by-frame basis – you can use either.

Let’s say that you decide to use GetContour. This is fine for splines which don’t change between frames. But suppose you want to generate a spline which increases in length with each frame in the animation. This is easy enough to do, but when you use GetContour to do it, you find that the generated spline doesn’t appear to change between frames, even though your code does change it. A little investigation shows that – and this the problem – GetContour is not called on a per-frame basis. In fact it isn’t called at all after the spline is initially generated unless something changes in the plugin’s parameters in the attributes manager – which is no good for an animation.

The obvious choice then is to use GetVirtualObjects instead, which is called on a per-frame basis. And in some cases this works without a problem. It all depends on what you want to do with the spline. Say you want to use the Hair renderer to render the spline. You soon find that Hair ignores a spline generated by GetVirtualObjects, but happily accepts one from GetContour. This is due to a crucial difference between these functions.

GetVirtualObjects returns a BaseObject*, while GetContour returns a SplineObject*. Much of the time this doesn’t matter; if you want to generate a static spline, you can often use either. However, there are certain inbuilt C4D objects which require the use of GetContour. Notably, this includes the Hair renderer, and Sketch & Toon; presumably these objects check that the input object is of type Ospline, and a spline which is of type Obase – such as that returned by GetVirtualObjects – won’t work. Others objects, such as the Mograph Tracer, are adult enough not to care.

So you see the problem: you need a spline which is recreated each frame (GetVirtualObjects but not GetContour) and which is accepted by things such as Hair or S&T (GetContour but not GetVirtualObjects). How do you solve this riddle?

Forcing GetContour to be called each frame

It’s a given that you have to use GetContour if you want to feed your spline into an object which demands a SplineObject (type Ospline). The question then becomes how to force GetContour to be called at least once per frame?

We know that GetContour will be called if one of the plugin’s parameters is changed. What we need to do is to change something in the parameters, which won’t affect the generated spline, and which is done every frame. Fortunately there is a way: we can use the Execute() function of the ObjectData plugin.

There are several hoops to jump through to make this work.

1. Get a unique identifer

First, you will need a unique ID value from Plugin Cafe, This is because you will use this as a unique identifier for the parameter you’re going to change. Once you have that ID value, you should store something into the object’s parameters, in the Init() function, like this:

Bool MyObjectDataPlugin::Init(GeListNode *node)
{
        BaseObject *op = (BaseObject*)node;
        LONG i;
        GeData param;
        const DescID id = DescID(MyUniqueID);

        // 'oldframe' is a class-level variable that will let us check if the current frame is different
        // from the one when the Execute() function was last called
        oldframe = 0;

        // set up the initial parameter for the Execute function
        i = 0;
        param.SetLong(i);
        op->SetParameter(id, param, DESCFLAGS_SET_0);
        return TRUE;
}

2. Ensure the Execute() function is called each frame

Secondly, you have to make sure that Execute() is called each frame. It won’t be unless you tell Cinema to do it. In your plugin, override the virtual functions Execute() and AddToExecution():

virtual Bool AddToExecution(BaseObject *op, PriorityList *list);
virtual EXECUTIONRESULT Execute(BaseObject *op, BaseDocument *doc, BaseThread *bt, LONG priority, EXECUTIONFLAGS flags);

You can see how AddToExecution works in the SDK documentation. Here's the code:

Bool MyObjectDataPlugin::AddToExecution(BaseObject *op, PriorityList *list)
{
    list->Add(op, EXECUTIONPRIORITY_GENERATOR, EXECUTIONFLAGS_ANIMATION);

    return TRUE;
}

If you don’t do this, AddToExecution() is never called and so Execute() is never called either.

3. Implement the Execute() function

In the Execute() function, you need to change the parameter with the unique ID. The code is very simple:

EXECUTIONRESULT MyObjectDataPlugin::Execute(BaseObject *op, BaseDocument *doc, BaseThread *bt, LONG priority, EXECUTIONFLAGS flags)
{
    GeData param;
    const DescID id = DescID(MyUniqueID);
    LONG i, fps, frame;

    fps = doc->GetFps();
    frame = doc->GetTime().GetFrame(fps);

    // ensure that this only happens once per frame or GetContour may be called multiple times
    if(frame == 0 || frame != oldFrame)
    {
        // hack to force GetContour to update
        op->GetParameter(id, param, DESCFLAGS_GET_0);
        i = param.GetLong();
        i++;
        param.SetLong(i);
        op->SetParameter(id, param, DESCFLAGS_SET_0);
        oldFrame = frame;
    }

    return EXECUTIONRESULT_OK;
}

All the function does is read the value, increment it, and store it back again. That one simple change is enough to force GetContour to be called again, and since Execute() is called each frame, so too is GetContour.

That’s it. Now you can use GetContour to generate dynamic splines that the older parts of Cinema will accept. And yes, it’s way past time that Maxon fixed this. All they need to do is cause GetContour to be called in the same way that  GetVirtualObjects is. Can’t be too difficult, can it?

Page last updated June 23rd 2021