Coding the spline generator itself

The code below is the code which does the actual work of the plugin. I'll list the entire code first, then discuss what the various functions do.

The plugin code

// CrossMaker
// crossmaker.cpp

// includes
#include "c4d.h"
#include "crossmaker.h"
#include "Ocrossmaker.h"
#include "c4d_symbols.h"
#include "ge_math.h"

// CrossMaker class
Bool CrossMaker::Init(GeListNode *node)
{
    ResetValues(node);
    return true;
}

void CrossMaker::ResetValues(GeListNode *node)
{
    BaseObject *op = (BaseObject*)node;
    BaseContainer *data = op->GetDataInstance();

    data->SetFloat(CROSS_WIDTH, 200.0);
    data->SetFloat(CROSS_HEIGHT, 200.0);
    data->SetInt32(PRIM_PLANE, 0);
    data->SetBool(PRIM_REVERSE, false);
    data->SetInt32(SPLINEOBJECT_INTERPOLATION, SPLINEOBJECT_INTERPOLATION_LINEAR);
    data->SetInt32(SPLINEOBJECT_SUB, 8);
    data->SetFloat(SPLINEOBJECT_ANGLE, Rad(5.0));
    data->SetFloat(SPLINEOBJECT_MAXIMUMLENGTH, 5.0);
}

Bool CrossMaker::Message(GeListNode *node, Int32 type, void *data)
{
    BaseDocument *doc;
    DescriptionCommand *desc;
    BaseContainer *nodebc;

    switch(type)
    {
        case MSG_MENUPREPARE:
        doc = (BaseDocument*)data;
        ((BaseObject*)node)->GetDataInstance()->SetInt32(PRIM_PLANE, doc->GetSplinePlane());
        break;

        case MSG_DESCRIPTION_COMMAND:
        doc = node->GetDocument();
        desc = (DescriptionCommand*) data;
        nodebc = ((BaseObject*)node)->GetDataInstance();

        switch(desc->id[0].id)
        {
            case B_RESET_CROSS:
            doc->StartUndo();
            doc->AddUndo(UNDOTYPE_CHANGE, node);
            ResetValues(node);
            doc->EndUndo();
            node->Message(MSG_UPDATE);
            break;
        }
        break;
    }

    return true;
}

SplineObject* CrossMaker::GetContour(BaseObject *op, BaseDocument *doc, Float lod, BaseThread *bt)
{
    cCross cs;
    BaseContainer *bc = op->GetDataInstance();

    cs.crossHeight = bc->GetFloat(CROSS_HEIGHT);
    cs.crossWidth = bc->GetFloat(CROSS_WIDTH);
    SplineObject *so = GenerateCross(cs);
    if(!so) return nullptr;

    // get the base container of our new spline
    BaseContainer *bb = so->GetDataInstance();
    // now set its parameters using the parameters set for the plugin overall
    bb->SetInt32(SPLINEOBJECT_INTERPOLATION, bc->GetInt32(SPLINEOBJECT_INTERPOLATION));
    bb->SetInt32(SPLINEOBJECT_SUB, bc->GetInt32(SPLINEOBJECT_SUB));
    bb->SetFloat(SPLINEOBJECT_ANGLE, bc->GetFloat(SPLINEOBJECT_ANGLE));
    bb->SetFloat(SPLINEOBJECT_MAXIMUMLENGTH, bc->GetFloat(SPLINEOBJECT_MAXIMUMLENGTH));

    OrientObject(so, bc->GetInt32(PRIM_PLANE), bc->GetBool(PRIM_REVERSE));
    return so;
}

SplineObject* CrossMaker::GenerateCross(cCross ACross)
{
    Int32 pnts, i;
    Vector *padr;
    Float w, h;

    pnts = 12;
    SplineObject *op = SplineObject::Alloc(pnts, SPLINETYPE_LINEAR);
    if(!op)
        return nullptr;

    op->GetDataInstance()->SetBool(SPLINEOBJECT_CLOSED, true);
    padr = op->GetPointW();
    w = ACross.crossWidth;
    h = ACross.crossHeight;
    i = 0;
    padr[i++] = Vector((w/8) * -1, h/2, 0); // point 0
    padr[i++] = Vector((w/8), h/2, 0); // point 1
    padr[i++] = Vector((w/8), h/8, 0); // point 2
    padr[i++] = Vector((w/2), h/8, 0); // point 3
    padr[i++] = Vector((w/2), (h/8) * -1, 0); // point 4
    padr[i++] = Vector((w/8), (h/8) * -1, 0); // point 5
    padr[i++] = Vector((w/8), (h/2) * -1, 0); // point 6
    padr[i++] = Vector((w/8) * -1, (h/2) * -1, 0); // point 7
    padr[i++] = Vector((w/8) * -1, (h/8) * -1, 0); // point 8
    padr[i++] = Vector((w/2) * -1, (h/8) * -1, 0); // point 9
    padr[i++] = Vector((w/2) * -1, (h/8), 0); // point 10
    padr[i++] = Vector((w/8) * -1, (h/8), 0); // point 11

    op->Message(MSG_UPDATE);
    return op;
}

void CrossMaker::OrientObject(SplineObject *op, Int32 plane, Bool reverse)
{
    Vector *padr;
    Int32 pcnt, i;

    padr = op->GetPointW();
    pcnt = op->GetPointCount();
    if(!padr) return;

    if(plane >= 1) // ZY or XZ planes
    {
        switch(plane)
        {
        case 1: // ZY
            for(i = 0; i < pcnt; i++)
            {
                padr[i] = Vector(-padr[i].z, padr[i].y, padr[i].x);
            }
            break;

        case 2: // XZ
            for(i = 0; i < pcnt; i++)
            {
                padr[i] = Vector(padr[i].x, -padr[i].z, padr[i].y);
            }
            break;
        }
    }

    if (reverse)
    {
        Vector p;
        Int32 to = pcnt/2;
        if (pcnt%2) to++;
        for (i=0; i<to; i++)
        {
            p = padr[i]; padr[i] = padr[pcnt-1-i]; padr[pcnt-1-i] = p;
        }
    }

    op->Message(MSG_UPDATE);
}

// register the plugin
Bool RegisterCrossMaker(void)
{
    String plugname = GeLoadString(IDS_CROSSMAKER);

    return RegisterObjectPlugin(ID_CROSSMAKER, plugname, OBJECT_GENERATOR|OBJECT_ISSPLINE, CrossMaker::Alloc, "Ocrossmaker", AutoBitmap( "crossmaker.tif"), 0);
}

Code walk-through

As you can see, it's not a lot of code. So what do these functions do?

Init() and ResetValues()

Init() simply calls ResetValues(). This in turn resets the various parameters of the cross object to their defaults. Changing these values will cause Cinema to call the GetContour() function to recrete the spline. The first two parameters (CROSS_WIDTH and CROSS_HEIGHT) are those we added to our interface, and both are reset to a value of 200 units. The remaining six are those you see with any spline primitive. They are all pretty self-explanatory if you look at any inbuilt spline primitive in Cinema.

Message()

This function handles messages sent from C4D to the plugin. There are numerous such messages, but we only have to respond to two of them.

MSG_MENUPREPARE is called whenever the user selects the plugin from the Plugins menu in Cinema. You can then do any preparatory work you like before the plugin is executed. In this case all we need to do is get from the document what plane the spline should be created in. Normally this is XY, but in case it's been changed, you can get the required value by calling doc->GetSplinePlane(). This value is then stored in the base container for the object, where we can access it when we create (or re-create) the spline object.

MSG_DESCRIPTION_COMMAND is called when the user clicks a button element in the description. The first thing we have to do is determine which description element has been activated. In this case the data passed to Message() in the 'data' parameter is a structure of type DescriptionCommand. The ID of the command is stored in a DescID class, which in turn is a stack of DescLevel structures. The 'id' member of the first DescLevel in the stack contains the command ID. First, we cast the 'data' object to a DescriptionCommand. Having done that, desc->id[0] gives us the first DescLevel, and desc->id[0].id is the ID of the command. It looks more complex than it is because of the way the objects are structured. Think about it enough and it'll come to you.

We're only interested in one ID, which is B_RESET_CROSS, our button to reset the cross values. If this button is clicked, all we do is call ResetValues(), wrapping the call in a set of begin and end undos in case the user changes his mind, then send an update message to the object so that Cinema knows it's been changed and has to be redrawn.

GetContour()

This is a function which Cinema will call whenever the cross is created or modified in some way, for example when parameters such as width or height or the plane of the spline is changed.

First, we set up a structure to hold the data needed to produce the cross - the width and height, in this case. Then we just pass this structure to the function GenerateCross() which does the actual work. That function must return a spline object, because that's what GetContour() is expected to return. If we get a nullptr pointer back, we just exit, passing a nullptr back to Cinema to show that something went wrong.

If we do get a spline, we need to set the default values of some of the standard parameters of spline primitives before returning the object to Cinema. You will recognise these from the attribute manager in C4D. They are all easy to set - we get the base container of the new object, then set the parameters into that. The only tricky one is the orientation (the plane the spline is on - XY, YZ, or XZ) and to get that we use a separate function, OrientObject().

GenerateCross()

This is surprisingly simple. We need to create a new spline object and pass it back to GetContour(). To create the spline object, we first need to know how many points it must have. If you look at the diagram below, you can see we need 12 points, numbered 0 to 11:

CrossMaker points

If you add additional parameters to this plugin, you may end up with cross designs which need more or less than 12 points, so you would need to expand the first part of this function to decide how many points you need in each case. Then you would set the variable 'pnts' accordingly. Having done that, we allocate a spline object using the Alloc() function of the SplineObject class. This needs two parameters: the number of points required and the type of spline (linear, Bezier, cubic, etc.). We only need a linear spline, so the constant 'Tlinear' is passed. The other spline types can be found in the SDK documentation. If something goes wrong and Alloc() doesn't return a valid spline object, we just pass a nullptr back to the calling function.

Assuming we get a valid spline, we first get its base container and set the Bool which indicates to Cinema that it should draw a closed spline. If we don't do this, our spline will be left open, which we don't want in this case. Then all we have to do is set the position in the 3D world of each point in the spline.

To do this, we first get the array of points in the spline using the spline object's GetPointW() call. I'm being lazy here in assuming we know that there are 12 points (ideally we should get the number of points from the spline object) and we iterate through them setting each point in turn. But how do we know what their positions should be?

Well, we know that the point positions are relative to the centre of the spline object, so a point right in the centre of the object will have X, Y, and Z position values of 0, 0, 0. In this case we will also make the assumption that the width of each arm of the cross is 25% of the width of the entire cross object. So that, if the cross is 200 units wide, the vertical arm will be 200/4 = 50 units in width. (It needn't be so, of course. You could add a parameter to allow the user to set the arm width either in absolute units or as a percentage of the object width. Here we'll keep things simple and make the arm width always 25% of the object's width.)

Referring to the above diagram, we see that point 0 is to the left of and above the object's centre. The X-position is therefore negative in respect of the centre and the Y-position is positive. The Z-position will be zero since we will always start out by adopting the C4D convention that new spline primitives lie on the XY axis. The X-position is therefore the object width divided by 8 (since the corresponding point 1 will also have an X-position of the object width divided by 8 but in the positive X-axis, giving a total width of the arm equal to object width/4 - which is what we want). The Y-position is the total height of the object divided by 2. This may seem a little complex but if you study the above diagram and work out what each point's X and Y values must be, then it will all become clear. The object's other 11 points are calculated in the same way. Each set of X, Y, and Z values are converted into a Vector, since that's how Cinema holds point positions, and stored back in the array.

Once this is done, we tell Cinema that the object has been updated, and return the new spline object to the calling function.

OrientObject()

This function looks more complex than it really is. Its job is to handle the orientation of the spline (which plane it lies on) and whether the order of the spline points should be reversed. Note that checking the 'Reverse' box in the attribute manager doesn't make any difference as long as the spline remains a primitive. It does though if you make it editable - try it and see what happens to the point order.

When we enter this function, we have already created the spline as though it lies on the XY axis. In GetContour() we pass the desired orientation to OrientObject(), having got it and the value of the 'Reverse' parameter from the base container. A 'plane' parameter of zero is the XY plane, which the object is already on, so we only need to do anything if the spline is to be on one of the other planes. If the plane is ZY, we don't need to do anything about the Y-position of each point because it won't change. All we need to do is swap the X and Z values, also multiplying the Z value by -1 when we do so (I'll leave it to you to test this out and see why that is necessary!). Of course, we know that the Z value is always zero, so we could just set the X values all to zero, but in future versions of the plugin maybe the Z value might initially be something other than zero - so we do it this way now, and we won't have to change it later.

If the plane is XZ, the X values don't change, but we swap the Y and Z values in exactly the same way.

Once this is done for all the points we can move on to the reverse parameter. I won't go into detail about the algorithm - it's very simple and all it does is reverse the point positions, so that point 0 gets the position of point 11 while point 11 gets point 0's position, then point 1 and point 10 are swapped, and so on for each pair of points.

Once all this is done we just tell C4D that the object has been updated, and return.

RegisterCrossMaker()

This is the last function in the code. It does much the same as the equivalent function to register a channel shader plugin. The main difference is that we tell Cinema that this plugin generates spline objects (the flags OBJECT_GENERATOR|OBJECT_ISSPLINE do this) and provide the name of an icon ('crossmaker.tif') which will appear in the plugins menu next to the plugin name, and in the object manager when we invoke the plugin. If we don't provide an icon, Cinema adds a white question mark instead.

Conclusion

So there you have a very simple object generator plugin. Feel free to modify this is as much as you like. The complete source code, plus resource files, can be downloaded from the link below.

Download file Download source file (12K, .zip file)

Page last updated June 23rd 2021