SourceGeo tutorial

This tutorial will walk you through implementing a minimal op which creates 3D geometry by subclassing DD::Image::SourceGeo. The Op creates a triangle which you can manipulate the corners of in the viewer.

It demonstrates how to:

  • create an object
  • add points to it
  • create a primitive which references the points
  • assign UVs to the vertices
  • calculate geometry hashes
  • use the rebuild flags to skip unnecessary work

Basic setup: includes and namespaces

We’ll be using assert statements in our code to catch coding errors, so we need the definition of the assert() function:

#include <cassert>

Next, we’ll need to include the relevant parts of the NDK:

#include <DDImage/SourceGeo.h>
#include <DDImage/Scene.h>
#include <DDImage/Triangle.h>

We need DDImage/SourceGeo.h because that’s where our base class is defined. It includes most of the other headers we’ll need for our plugin. The other include we’ll need is DDImage/Triangle.h, which declares the specific type of primitive we’ll be creating.

Most of the NDK classes are in the DD::Image namespace. To save us having to prefix every reference to them, we provide a using statement:

using namespace DD::Image;

It’s good practice to put your own code inside a namespace as well. This helps prevent conflicts with symbols defined elsewhere. For this example we’ll use a namespace called tri:

namespace tri {

  // All futher code in this tutorial will go inside this block.

} // namespace tri

Constants

All NUKE ops need to provide a class name. Following general good programming practice, we declare that as a constant:

static const char* kTriangleClass = "Triangle";

That’s the only constant we need for this example.

Declarations

Now we get to the more important part, where we declare the Op class and it’s members. We’ll call the class TriangleOp, since it will be creating a triangle. There’s a class in the NDK which is already called Triangle, so adding the Op suffix makes it easier to distinguish between the two.

The declaration looks like this:

class TriangleOp : public SourceGeo {
public:
  TriangleOp(Node* node);

  virtual const char* Class() const;
  virtual void knobs(Knob_Callback f);

  static Op* Build(Node* node);

  static const Op::Description description;

protected:
  virtual void create_geometry(Scene& scene, GeometryList& out);
  virtual void get_geometry_hash();

private:
  Vector3 _aPos; // Position of corner A of the triangle
  Vector3 _bPos; // Position of corner B of the triangle
  Vector3 _cPos; // Position of corner C of the triangle
};

The first thing we declare is the constructor; Ops need to have a constructor which takes a pointer to a Node. The Class() method is required on every custom Op class. We’ll also be adding some knobs to the node, so we need to override the knobs() method.

The static Build method is what NUKE will use to create instances of our plugin when (e.g.) you add a Triangle node to the DAG. This method can actually be called anything you like, so long as it keeps the same signature. It can also be a standalone function instead of a static method, but having it as a static method is recommended for the sake of clarity in your code.

The static description member tells NUKE what the Op is called and how to create it. This is required on every custom Op class. As well as a class name string for the Op, it also stores a pointer to the function for creating the op (the Build function mentioned above, in our case).

The standard SourceGeo method for creating geometry is called create_geometry; we’ll be overriding that to create our triangle. The geometry we create will depend on the values of our knobs, so we need to override the get_geometry_hash method as well.

Finally, we declare three member variables: _aPos, _bPos and _cPos. These are the locations of the triangle’s corners. We’ll be creating knobs which use these as storage, so that we can manipulate them inside NUKE.

The easy bits

The implementations of the constructor, Class() and Build() methods are fairly straightforward so we won’t spend much time on them:

TriangleOp::TriangleOp(Node* node) :
  SourceGeo(node),
  _aPos(0, 0, 0),
  _bPos(1, 0, 0),
  _cPos(0, 1, 0)
{}


const char* TriangleOp::Class() const
{
  return kTriangleClass;
}


Op* TriangleOp::Build(Node* node)
{
  return new TriangleOp(node);
}

The constructor simply calls the base class constructor, passing through the node, and initialises the data members. As an aside: the DD::Image::Vector3 class doesn’t do any initialisation in its no-arg constructor; assuming that it zeroes the vector is a common mistake.

The Class() method just returns the constant we defined earlier. And the Build() method creates a new TriangleOp instance and returns it.

Note that there’s no need to define a destructor for this example. The default one generated by the compiler is sufficient for this class.

Adding some knobs

A node with no output controls is not particularly useful, so we override the knobs() method to add some. In this case we’re adding three XYZ_Knobs which can be used to adjust the position of each corner of the triangle independently:

void TriangleOp::knobs(Knob_Callback f) {
  SourceGeo::knobs(f); // Set up the common SourceGeo knobs.
  XYZ_knob(f, &_aPos[0], "point_a", "a");
  XYZ_knob(f, &_bPos[0], "point_b", "b");
  XYZ_knob(f, &_cPos[0], "point_c", "c");
}

Note the call to the SourceGeo::knobs() method. It’s important to call this because you’re likely to get crashes at runtime if you don’t.

Generating the geometry

The create_geometry method is the meat of a SourceGeo subclass: it creates the geometry that gets passed down through the graph. The out parameter is where we put the geometry we create; NUKE takes care of the rest.

In our implementation we create a single Triangle primitive, then provide the locations of the corner points and finally set the texture coordinates for each corner:

void TriangleOp::create_geometry(Scene& scene, GeometryList& out)
{
  int obj = 0;

  if (rebuild(Mask_Primitives)) {
    out.delete_objects();
    out.add_object(obj);
    out.add_primitive(obj, new Triangle());
  }

  if (rebuild(Mask_Points)) {
    PointList& points = *out.writable_points(obj);
    points.resize(3);

    points[0] = _aPos;
    points[1] = _bPos;
    points[2] = _cPos;
  }

  if (rebuild(Mask_Attributes)) {
    Attribute* uv = out.writable_attribute(obj, Group_Vertices, "uv", VECTOR4_ATTRIB);
    assert(uv != NULL);
    uv->vector4(0).set(0, 0, 0, 1);
    uv->vector4(1).set(1, 0, 0, 1);
    uv->vector4(2).set(0, 1, 0, 1);
  }
}

In order to speed things up, NUKE tries to avoid recomputing values when it doesn’t have to. As part of the processing sequence for geometry ops it will check the hashes for each group (see the get_geometry_hash() discussion below) and set flags to indicate which parts of the geometry need to be rebuilt. We test these flags using the rebuild() function to see whether we need to rebuild a specific part.

If we need to recreate primitives, the first thing we do is call out.delete_objects() to ensure that the output list is empty. Then we add a single object to hold our triangle (an object is a collection of points, primitives, vertices and attributes). Finally we add a single Triangle primitive. Note that we haven’t specified any positions or attributes yet.

To (re)create points, we need to obtain a PointList object that we can add our points to. The out.writable_points() method gives us this. We only ever have three points, so we resize the list to 3 and set the values to match our _aPos, _bPos and _cPos members respectively.

Texture coordinates in NUKE’s 3D system are stored in a vertex attribute (Group_Vertices) named “uv”. If the rebuild flag for attributes is set, we’ll need to recreate them. The usual pattern for updating any attribute is: - call out.writable_attribute() to get an object you can store the

attribute values in. The returned attribute will already have a slot allocated for each item in the group.
  • Use the accessor methods of the relevant type (e.g. uv->vector4(1) above) to set values for each item.

The get_geometry_hash() method

In order to minimse the amount of recomputation when you change something in the graph, NUKE keeps separate hashes for various aspects of the geometry and uses them to set the appropriate rebuild flags (these are the flags we used in the create_geometry method above).

The get_geometry_hash() calculates the current value for each of these hashes:

void TriangleOp::get_geometry_hash()
{
  SourceGeo::get_geometry_hash();

  // Add the three point positions to the hash for the Points group.
  _aPos.append(geo_hash[Group_Points]);
  _bPos.append(geo_hash[Group_Points]);
  _cPos.append(geo_hash[Group_Points]);
}

The SourceGeo class provides a default implementation of this method which incorporates hashing of the material input, so we call that first.

The three knobs will affect the point locations on the geometry we create, but that’s all, so we only need to hash them into the points group.

There’s nothing else that affects the geometry that we’ll produce, so our work here is done.

The complete code

#include <cassert>

#include <DDImage/Knobs.h>
#include <DDImage/SourceGeo.h>
#include <DDImage/Triangle.h>

using namespace DD::Image;

namespace tri {

  //
  // Constants
  //

  static const char* kTriangleClass = "Triangle";


  //
  // Declarations
  //

  class TriangleOp : public SourceGeo {
  public:
    TriangleOp(Node* node);

    virtual const char* Class() const;
    virtual void knobs(Knob_Callback f);

    static Op* Build(Node* node);

    static const Op::Description description;

  protected:
    virtual void create_geometry(Scene& scene, GeometryList& out);
    virtual void get_geometry_hash();

  private:
    Vector3 _aPos; // Position of corner A of the triangle
    Vector3 _bPos; // Position of corner B of the triangle
    Vector3 _cPos; // Position of corner C of the triangle
  };


  //
  // Implementations
  //

  TriangleOp::TriangleOp(Node* node) :
    SourceGeo(node),
    _aPos(0, 0, 0),
    _bPos(1, 0, 0),
    _cPos(0, 1, 0)
  {}


  const char* TriangleOp::Class() const
  {
    return kTriangleClass;
  }


  void TriangleOp::knobs(Knob_Callback f) {
    SourceGeo::knobs(f); // Set up the common SourceGeo knobs.
    XYZ_knob(f, &_aPos[0], "point_a", "a");
    XYZ_knob(f, &_bPos[0], "point_b", "b");
    XYZ_knob(f, &_cPos[0], "point_c", "c");
  }


  Op* TriangleOp::Build(Node* node)
  {
    return new TriangleOp(node);
  }


  void TriangleOp::create_geometry(Scene& scene, GeometryList& out)
  {
    int obj = 0;

    if (rebuild(Mask_Primitives)) {
      out.delete_objects();
      out.add_object(obj);
      out.add_primitive(obj, new Triangle());
    }

    if (rebuild(Mask_Points)) {
      PointList& points = *out.writable_points(obj);
      points.resize(3);

      points[0] = _aPos;
      points[1] = _bPos;
      points[2] = _cPos;
    }

    if (rebuild(Mask_Attributes)) {
      Attribute* uv = out.writable_attribute(obj, Group_Vertices, "uv", VECTOR4_ATTRIB);
      assert(uv != NULL);
      uv->vector4(0).set(0, 0, 0, 1);
      uv->vector4(1).set(1, 0, 0, 1);
      uv->vector4(2).set(0, 1, 0, 1);
    }
  }


  void TriangleOp::get_geometry_hash()
  {
    SourceGeo::get_geometry_hash();

    // Add the three point positions to the hash for the Points group.
    _aPos.append(geo_hash[Group_Points]);
    _bPos.append(geo_hash[Group_Points]);
    _cPos.append(geo_hash[Group_Points]);
  }

    
  const Op::Description TriangleOp::description(kTriangleClass, TriangleOp::Build);

} // namespace tri