2D viewport drawing using a SceneHook

by .

If you wrote a plugin that draws stuff in the viewport before, you might remember running into some difficulties.

The problem

If your plugin is an ObjectData, you had to care about the drawpass: Which is the correct drawpass to use? Should you add your drawing to the Post pass instead? Which matrix do you have to set before drawing, and why does the rest of the scene look screwed up after you finished drawing?

If your plugin is a TagData, it’s even worse. Only recently it became possible to even find out which is the current drawpass. And because of the priority system and the hierarchical execution order in CINEMA 4D, you can never be sure when your draw code will be executed. It is almost impossible to use 2D drawing functions or to use DrawPolygon in screen space to draw 2D shapes or text in front of everything else, there are always situations when some other geometry is drawn after your draw code, effectively overlapping and hiding it. Experimenting with the Z buffer offset will not improve the situation either. In addition to that, you can’t even return a DRAWRESULT value to inform CINEMA 4D in detail about what happened during your draw process, you can only return a simple Bool.

This article proposes a different method for drawing. Instead of drawing from a TagData, we will use a SceneHookData to put all the drawing function in, and the TagData will only feed its data to the SceneHook.

An example case

In this example, our goal is to draw a small circle around the position of all child objects of the object that has the tag attached to it.

Without any drawing, our example scene looks like this. The left and right cubes are children of the cube in the middle.

The TagData approach

Here is a very simple TagData. There is only a Draw() function that does nothing yet, and a register function that has to be called from PluginStart() in main.cpp.

#include "c4d.h"
#include "c4d_symbols.h"
#include "tdrawexample.h"

#define ID_TDRAWEXAMPLE	1029388


class tDrawExample : public TagData
{
  INSTANCEOF(tDrawExample, TagData)

public:
  virtual Bool Draw(BaseTag* tag, BaseObject* op, BaseDraw* bd, BaseDrawHelp* bh);

  static NodeData *Alloc(void) { return gNew tDrawExample; }
};


Bool tDrawExample::Draw(BaseTag* tag, BaseObject* op, BaseDraw* bd, BaseDrawHelp* bh)
{
  return SUPER::Draw(tag, op, bd, bh);
}


Bool RegisterExampleDrawTagData()
{
  // decide by name if the plugin shall be registered - just for user convenience
  String name=GeLoadString(IDS_TDRAWEXAMPLE); if (!name.Content()) return TRUE;
  return RegisterTagPlugin(ID_TDRAWEXAMPLE, name, TAG_VISIBLE, tDrawExample::Alloc, "tdrawexample", AutoBitmap("tdrawexample.tif"), 0);
}

Drawing

This is the drawing function of our example plugin:

Bool tDrawExample::Draw(BaseTag* tag, BaseObject* op, BaseDraw* bd, BaseDrawHelp* bh)
{
  if (!tag || !op || !bd || !bh) return FALSE;

  BaseObject* the_obj = op->GetDown();
  Vector	pos;

  while (the_obj)
  {
    pos = the_obj->GetMg().off;
    pos = bd->WS(pos);
    bd->DrawCircle2D(LCO pos.x, LCO pos.y, RCO 20.0);

    the_obj = the_obj->GetNext();
  }

  return SUPER::Draw(tag, op, bd, bh);
}

As you can see, just because we use DrawCircle2D the draw matrix gets totally screwed up:

Also, the circles only show up when the highlights of the objects are drawn:

We can fix the draw matrix by adding BaseDraw::SetMatrix calls. The draw matrix has to be set to screen space, as we are going to draw 2D stuff. After we’re done with drawing, the draw matrix has to be set back to 3D space to prevent drawing errors on the objects’ geometry.

Bool tDrawExample::Draw(BaseTag* tag, BaseObject* op, BaseDraw* bd, BaseDrawHelp* bh)
{
  if (!tag || !op || !bd || !bh) return FALSE;

  BaseObject* the_obj = op->GetDown();
  Vector	pos;

  bd->SetMatrix_Screen();

  while (the_obj)
  {
    pos = the_obj->GetMg().off;
    pos = bd->WS(pos);
    bd->DrawCircle2D(LCO pos.x, LCO pos.y, RCO 20.0);

    the_obj = the_obj->GetNext();
  }

  bd->SetMatrix_Matrix(NULL, Matrix(), 0);

  return SUPER::Draw(tag, op, bd, bh);
}

Now the draw matrix is fine again, but still, the circles only show up when the highlights are drawn:

Let’s try to fix this by excluding drawpasses we don’t need:

Bool tDrawExample::Draw(BaseTag* tag, BaseObject* op, BaseDraw* bd, BaseDrawHelp* bh)
{
  if (!tag || !op || !bd || !bh) return FALSE;
  if (bd->GetDrawPass() != DRAWPASS_OBJECT) return TRUE;

  BaseObject* the_obj = op->GetDown();
  Vector	pos;

  bd->SetMatrix_Screen();

  while (the_obj)
  {
    pos = the_obj->GetMg().off;
    pos = bd->WS(pos);
    bd->DrawCircle2D(LCO pos.x, LCO pos.y, RCO 20.0);

    the_obj = the_obj->GetNext();
  }

  bd->SetMatrix_Matrix(NULL, Matrix(), 0);

  return SUPER::Draw(tag, op, bd, bh);
}

The code above only performs any draw actions in DRAWPASS_OBJECT. The result is that the circles don’t show up at all anymore. We can try any drawpass we want, the circles only show up in DRAWPASS_HIGHLIGHTS, which is not what we want.

It seems there is no solution for 2D drawing in a TagData. That is why we will try it with a SceneHook now.

The SceneHook approach

A scene hook is independent from the scene hierarchy. It is always executed the same way. No surprises. This is the first big advantage.
The second advantage is, that we can freely decide if and when to draw anything. Not like in an ObjectData, where DRAWPASS_HANDLES is only called when the object is selected, or DRAWPASS_HIGHLIGHTS is only called when the mouse cursor hovers over the object. We can decide.

A side effect of that is, however, that we have to take good care for our SceneHook only to do anything when it is absolutely necessary. Otherwise we risk ruining the performance of the whole application, even when our example tag is not being used at all.

Creating the basic SceneHookData

Here is the code for a simple SceneHook that does nothing yet. There is only a Draw() member function and the registration function that has to be called from PluginStart() in main.cpp.

#include "c4d.h"
#include "c4d_symbols.h"

#define ID_SHDRAWEXAMPLE	1029389


class shDrawExample : public SceneHookData
{
  INSTANCEOF(shDrawExample, SceneHookData)

public:
  virtual Bool Draw(BaseSceneHook *node, BaseDocument *doc, BaseDraw *bd, BaseDrawHelp *bh, BaseThread *bt, SCENEHOOKDRAW flags);

  static NodeData *Alloc(void) { return gNew shDrawExample; }
};


Bool shDrawExample::Draw(BaseSceneHook *node, BaseDocument *doc, BaseDraw *bd, BaseDrawHelp *bh, BaseThread *bt, SCENEHOOKDRAW flags)
{
  return TRUE;
}


Bool RegisterExampleDrawSceneHook()
{
  // decide by name if the plugin shall be registered - just for user convenience
  String name=GeLoadString(IDS_SHDRAWEXAMPLE); if (!name.Content()) return TRUE;
  return RegisterSceneHookPlugin(ID_SHDRAWEXAMPLE, name, 0, shDrawExample::Alloc, EXECUTIONPRIORITY_GENERATOR, 0);
}

Removing the draw code from the TagData

This is easy. Completely remove the declaration and definition of the Draw() function from the TagData.

Drawing

Since we can decide when to draw, we have to come up with some rules.
In this example, we will only draw the circles when the standard SceneHook drawpass is being called (and not the Highlight pass), and when a tag of the type ID_TDRAWEXAMPLE is selected (for that, we have to define the Plugin ID of the Tag again).

#define ID_TDRAWEXAMPLE	1029388

Bool shDrawExample::Draw(BaseSceneHook *node, BaseDocument *doc, BaseDraw *bd, BaseDrawHelp *bh, BaseThread *bt, SCENEHOOKDRAW flags)
{
  if (!node || !doc || !bd || !bh) return FALSE;
  if (flags != SCENEHOOKDRAW_DRAW_PASS) return TRUE;

  BaseTag* the_tag = doc->GetActiveTag();
  if (!the_tag || the_tag->GetType() != ID_TDRAWEXAMPLE) return TRUE;

  BaseObject* the_obj = the_tag->GetObject();
  if (!the_obj) return TRUE;

  the_obj = the_obj->GetDown();

  Vector	pos;

  bd->SetMatrix_Screen();

  while (the_obj)
  {
    pos = the_obj->GetMg().off;
    pos = bd->WS(pos);
    bd->DrawCircle2D(LCO pos.x, LCO pos.y, RCO 20.0);

    the_obj = the_obj->GetNext();
  }

  bd->SetMatrix_Matrix(NULL, Matrix(), 0);

  return TRUE;
}

The result is just pure beauty:

Conclusion

Even though it might seem like an unnecessary amount of extra work to write a SceneHook, it has quite some advantages over drawing from a TagData or even BaseObject: by using a SceneHook, we can skip all those problems with execution order and priorities, and we get 2D drawing that just works.
Writing the basic SceneHook is a matter of just a minute, and the draw code can mostly just be copied and pasted.

Of course, there are a lot more possibilities to make use of a SceneHook for drawing, and lots of things we would have to think about in some cases. Life is not all that easy.
For example, what if we want the SceneHook to always draw those circles, and not only if the tag is selected? At a first glance, it seems easy to just remove the code part the_tag->GetType() != ID_TDRAWEXAMPLE, but what happens if we have several of our tags in the scene and all their circles should be drawn? In that case we would have to come up with a kind of drawing system where the tags would e.g. send drawing requests to the SceneHook, and the SceneHook would keep them in a kind of queue list and then draw them all at once. It is not as difficult as it sounds, but you see there are cases where we would have to do some extra work, while – if we would still do our drawing in the TagData, where every tag takes care for its own drawing – it would just work out of the box.

Advertisements