SourceGeo tutorial — NDK Developers Guide

### Navigation

- index
- next |
- previous |
- NDK Dev Guide »
- 3D »

# 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

### Navigation

- index
- next |
- previous |
- NDK Dev Guide »
- 3D »