.. _writing-geo-ops:

Writing a GeoOp
===============

A GeoOp node creates or modifies 3D data. The output is visible in NUKE's
built-in 3D Viewer, can be saved out to an OBJ or FBX file via the WriteGeo
node, or rendered into a 2D image using (for example) the ``ScanlineRender``
node.

Writing a 3D Op is similar to writing a 2D Op. You need to override many
of the same functions to describe the Op to nuke: ``Class()``, ``knobs``,
``test_input``, and so on. Refer to the section on implementing 2D Ops for
details about these. The main differences are:

- Your Op inherits from a different :ref:`base class <3d-base-classes>`
- You override a different method to do the work of the Op (e.g.
  ``create_geometry`` instead of ``pixel_engine``). Obviously the
  implementation of this method is quite different to what you would
  have in a 2D Op
- As well as the overall Op hash, 3D Ops have separate hashes for each
  :ref:`data group <3d-groups>`. You'll need to implement the
  ``get_geometry_hash()`` method to calculate each of these

To write a GeoOp you should, broadly, follow these steps:

#. Pick a :ref:`base class <3d-base-classes>` to inherit from.
#. Override the appropriate method, depending on the base class you chose, to
   do the work of the Op.
#. Override the ``get_geometry_hash()`` method, to calculate a hash for each
   of the :ref:`data groups <3d-groups>`.
#. Override whichever of the standard Op functions you need. The ``Class()``
   method must always be overridden; all the others are optional. If you
   override the ``knobs()`` method, remember to call the method from the base
   class as well; otherwise, you're likely to get some crashes.

The GeoOp class has a method, ``geometry_engine``, which does the bulk of the
geometry processing work. Both SourceGeo and ModifyGeo override this method to
provide simpler interfaces for creating or modifying geometry respectively.


.. _3d-base-classes:

Base Classes for GeoOps
-----------------------

All GeoOps inherit ultimately from the ``DD::Image::GeoOp`` class. There are
two additional subclasses which you can inherit from instead,
``DD::Image::SourceGeo`` and ``DD::Image::ModifyGeo``; these take care of some
extra details for you.

Deciding which Op to inherit from depends on what you want your Op to do. Some
examples to help guide you:

+----------------------------------------------+----------------------------+
| Purpose                                      | Base Class to Inherit From |
+==============================================+============================+
| Create new geometry from scratch             | ``DD::Image::SourceGeo``   |
+----------------------------------------------+                            +
| Create new geometry based on 2D inputs       |                            |
+----------------------------------------------+----------------------------+
| Transform or distort existing geometry       | ``DD::Image::ModifyGeo``   |
+----------------------------------------------+                            +
| Produce a more finely tessellated version of |                            |
| some existing geometry                       |                            |
+----------------------------------------------+----------------------------+
| Calculations based on analysis of the whole  | ``DD::Image::GeoOp``       |
| scene, rather than just one object at a time |                            |
+----------------------------------------------+                            +
| Filter existing objects                      |                            |
+----------------------------------------------+----------------------------+

A common reason for inheriting from ``GeoOp`` rather than ``ModifyGeo`` is
that the latter provides an interface which operates on one 3D object at a
time. If you are doing calculations which depend on having access to all the
objects at once, it may be easier to inherit from ``GeoOp``. You can still
access information about other 3D objects in ``ModifyGeo``; it just comes down
to convenience.


Extending SourceGeo
-------------------

``SourceGeo`` provides a simplified interface for creating geometry. It
also provides some useful default behaviour:

- It takes a single input, which must be a subclass of ``DD::Image::Iop``.
  This gets used as the default material on all 3D objects created by this Op.
- It overrides ``get_geometry_hash``, appending the hash of the input Op to
  Group_Object. There's special handling for when the input Op is a subclass
  of ``DD::Image::Material``.
- It overrides ``geometry_engine`` to ensure that the 3D object cache is
  properly synchronized and that any created objects get filled in with some
  sensible default values.

The actual creation of 3D geometry is delegated to the ``create_geometry``
function, which you must override in your subclass:

.. cpp:function:: void DD::Image::SourceGeo::create_geometry(Scene& scene, GeometryList& out)

  Fills the ``out`` parameter with newly created geometry. This is called
  to create geometry for the first time, or to rebuild it after something has
  changed.

If you want to change the default values which are set up for each 3D object,
you can override the ``init_geoinfo_parms`` method:

.. cpp:function:: void DD::Image::SourceGeo::init_geoinfo_parms(Scene& scene, GeometryList& out)

  Sets up the default values for created objects, including the transform
  matrix and the material.

There are further things which are common to all GeoOps that you can do.
See the :ref:`geo-op-common` section for details.

The NDK includes ``Sphere.cpp`` as an example of how to implement a
``SourceGeo`` subclass.


Extending ModifyGeo
-------------------

The ``ModifyGeo`` class provides a single method that you must override:

.. cpp:function:: void DD::Image::ModifyGeo::modify_geometry(int obj, DD::Image::Scene& scene, DD::Image::GeometryList& out)

  This function is called once per GeoInfo from the input node. The ``obj``
  parameter is the index of the input object to process.

  The data for the object to process is already populated in the
  ``out`` parameter. Your implementation of this function should modify the
  data in place by calling (e.g.) ``out.writable_points(obj)`` and overwriting
  the entries in the list it returns.

There are further things which are common to all GeoOps that you can do.
See the :ref:`geo-op-common` section for details.

The NDK includes ``LogGeo.cpp`` as an example of how to implement a
``ModifyGeo`` subclass.


Extending GeoOp Directly
------------------------

The GeoOp base class does most of it's work in a single method which you can
override:

.. cpp:function:: void DD::Image::GeoOp::geometry_engine(DD::Image::Scene& scene, DD::Image::GeometryList& out)

  Creates or modifies geometry and ensures the Op's geometry cache is up to
  date.

  Your implementation of this function should ensure that it calls
  ``out.synchronize_objects()`` to get the Op's geometry cache up to date.
  Calling this more than once is relatively cheap, because it exits quickly if
  the cache is up to date. It must be called at least once after all the
  geometry creation or modification.

There are further things which are common to all GeoOps that you can/must do.
See the :ref:`geo-op-common` section for details.

The NDK includes ``UVProject.cpp`` as an example of how to implement a
``GeoOp`` subclass.


.. _geo-op-common:

Common Parts of All GeoOps
--------------------------

Generally speaking, all GeoOps should calculate suitable hashes for each of
the groups by overriding this method:

.. cpp:function:: void DD::Image::GeoOp::get_geometry_hash()

  Recalculates and updates the stored hashes for each group.

  This function is called by NUKE to get the hashes up to date, so it can use
  them to check which groups need to be rebuilt and set the appropriate
  rebuild flags.


All GeoOps have a few features in common:

- They store a separate hash for each group
- They perform caching

- Operations to implement (what, when and why)
- Geometry hashes


  Updates the stored hashes for each of the groups.


GeoOp Call Order for Rendering
------------------------------

The call order for a GeoOp is different depending on whether it's being
rendered through a ScanlineRender (or similar) node, or being viewed in the 3D
Viewer.

When rendering a GeoOp, most of the work happens inside the
renderers own ``_validate(bool)`` method (the renderer itself is a
subclass of Iop, following the usual
:ref:`Iop call sequence <iops>`):

#. ``GeoOp::_validate(bool)``
#. ``GeoOp::get_geometry_hash()`` (called by the ``_validate`` method)
#. ``GeoOp::build_scene()``
#. ``GeoOp::get_geometry()`` (called by ``build_scene``)
#. ``GeoOp::geometry_engine()`` (called by ``get_geometry``)
#. ``Scene::validate()``
#. ``GeoInfo::validate()`` for each ``GeoInfo`` in the scene
#. ``Iop::validate()`` for each material used by a ``GeoInfo`` in the scene
#. ``Scene::evaluate_lights()``

In terms of the functions you'll be overriding:

- ``get_geometry_hash()`` is called first to ensure the hashes are up to date;
- then ``geometry_engine()`` is called to produce the geometry.

In the case of a ``SourceGeo`` Op, that will instead look like:

- ``get_geometry_hash()`` is called first, as above;
- then ``create_geometry(Scene& scene, GeometryList& out)`` is called, to
  create the geometry.
- finally ``init_geoinfo_parms(Scene& scene, GeometryList& out)`` is called to
  setup some default values for various ``GeoInfo`` parameters. This includes
  providing a default material, when none was specified.

And for a ``ModifyGeo`` Op:

- ``get_geometry_hash()`` is called first, as above;
- then ``modify_geometry(int obj, Scene& scene, GeometryList& out)`` gets
  called once for each ``GeoInfo`` in the scene.


GeoOp Call Order for Viewing
----------------------------

When you're viewing the output of a GeoOp node in NUKE's 3D Viewer, the call
order is different to that shown above. Geometry is generated first, without any
materials; then textures are generated in a background thread for each of the
materials and the Viewer is updated as they complete.

Another difference between rendering 3D and viewing 3D is the amount that gets
drawn. For rendering, it's the connected inputs only; for the Viewer, it's the
connected inputs and also anything which has a panel open in the Properties
bin.

The call order in the 3D Viewer looks like this for every Op connected to the
Viewer's primary input and for every Op with an open panel in the Properties
bin:

#. ``_validate(false)``
#. ``get_geometry_hash()`` (called by the ``_validate`` method)
#. ``geometry_engine(Scene& scene, GeometryList& out)``
#. ``GeoOp::build_handles(ViewerContext* ctx)`` is called first. If this returns
   ``true``, a draw_handles callback is added for the Op
#. ``Knob::build_handles(ViewerContext* ctx)`` is called for every knob on the
   Op. If it returns ``true``, a draw_handles callback is added for the knob

This gathers the information necessary for NUKE to do its drawing. Next we do a
series of render passes; for each pass we set the ViewerContext's event to a
different value then, after drawing all the geometry, invoke the draw_handles
callbacks:

#. ``draw_handle(ViewerContext* ctx)`` with ``ctx->event() == DRAW_SOLID``
#. ``draw_handle(ViewerContext* ctx)`` with ``ctx->event() == DRAW_LINES``

If your Op doesn't draw any custom handles, ``build_handles`` should return
``false`` and you can ignore the ``draw_handle`` function. Likewise, if you're
not using any custom knobs you can ignore the knob ``build_handles`` and
``draw_handle`` functions; the built-in knobs already have suitable
implementations of these.


Creating Geometry
-----------------

To create a new 3D object, you'll need to follow these steps:

#. Add an object
#. Add points for the object
#. Add primitives
#. Set attributes

It may not always be necessary to rebuild all of the geometry data; only some
of the :ref:`data groups <3d-groups>` may have been affected. The
implementation should check the state of the rebuild flags to determine which
groups need to be rebuilt. You can do this using (for example)::

  if (rebuild(Mask_Points)) {
    ...
  }

Note that it's possible to set the rebuild flags explicitly by calling
``set_rebuild()``; but you should avoid doing this while inside your
geometry_engine, create_geometry or modify_geometry method as it could lead to
a memory leak.

Call out.add_object(obj) to make space for the new object in the geometry list.

