Drawing text in the viewport

by .

In this article we show a handy function for drawing text in the viewport.

On PluginCafé, this has been a frequent question. Since there is no BaseDraw::DrawText() function, and since it’s not obvious, here’s how to do it.

How it works

As there is no way, to draw draw text in the viewport, we have to implement a workaround: We will create a clipmap, draw the background and text into it, and then get a bitmap from it. Then we set up a simple polygon with UV coordinates, map the bitmap onto it, and use BaseDraw::DrawTexture() to draw it into the viewport.

The function

// Set the draw color of a GeClipMap from a color vector
void ClipMapSetColor(GeClipMap* clip, const Vector& col, LONG alpha)
{
  if (clip) clip->SetColor((LONG)(col.x * 255), (LONG)(col.y * 255), (LONG)(col.z * 255), alpha);
}

// Draw text into the viewport
Bool DrawText(BaseDraw* bd, GeClipMap* clip, const String &text, LONG xpos, LONG ypos, const Vector& textcol, const Vector& bgcol, LONG margin)
{
  if (!bd || !clip) return false;

  LONG i = 0;
  LONG width = 0;
  LONG height = 0;
  Vector padr[4];
  Vector uvadr[4];
  Vector cpos = Vector(RCO xpos, RCO ypos, RCO 0.0);

  // Init a 32-bit clipmap with 0x0 pixels
  if(clip->Init(0, 0, 32) != IMAGERESULT_OK) return false;

  // Find out required size of the clipmap
  clip->BeginDraw();
    width = clip->GetTextWidth(text) + margin * 2;
    height = clip->GetTextHeight() + margin * 2;
  clip->EndDraw();
  clip->Destroy();

  // Init a 32-bit clipmap with the correct size
  if(clip->Init(width, height, 32) != IMAGERESULT_OK) return false;

  // Draw background to clipmap
  // Draw text to clipmap
  clip->BeginDraw();
    ClipMapSetColor(clip, bgcol, 64);
    clip->FillRect(0, 0, width, height);
    ClipMapSetColor(clip, textcol, 255);
    clip->TextAt(margin, margin, text);
  clip->EndDraw();

  // Set draw matrix
  bd->SetMatrix_Screen();

  // Deactivate lights
  bd->SetLightList(BDRAW_SETLIGHTLIST_NOLIGHTS);

  // Set up points and UV map
  cpos += Vector(RCO 6.0, RCO 6.0, RCO 0.0);
  padr[0] = cpos + Vector(RCO 0.0, RCO 0.0, RCO 0.0);
  padr[1] = cpos + Vector(RCO width, RCO 0.0, RCO 0.0);
  padr[2] = cpos + Vector(RCO width, RCO height, RCO 0.0);
  padr[3] = cpos + Vector(RCO 0.0, RCO height, RCO 0.0);

  for (i=0; i<4; i++)
  {
    padr[i].x = (LONG)Clamp(-RCO 10000.0, RCO 10000.0, padr[i].x);
    padr[i].y = (LONG)Clamp(-RCO 10000.0, RCO 10000.0, padr[i].y);
    padr[i].z = -MAX_Z;
  }

  uvadr[0] = Vector(RCO 0.0);
  uvadr[1] = Vector(RCO 1.0, RCO 0.0, RCO 0.0);
  uvadr[2] = Vector(RCO 1.0, RCO 1.0, RCO 0.0);
  uvadr[3] = Vector(RCO 0.0, RCO 1.0, RCO 0.0);

  // Get bitmap from clipmap
  BaseBitmap *cmbmp = clip->GetBitmap();
  if(!cmbmp) return false;

  bd->DrawTexture(cmbmp, padr, NULL, NULL, uvadr, 4, DRAW_ALPHA_NORMAL, DRAW_TEXTUREFLAGS_INTERPOLATION_NEAREST);

  return true;
}

Usage

As an example, here’s how to use DrawText() in the Draw() function of an ObjectData, using the Handles drawpass:

DRAWRESULT ExampleObject::Draw(BaseObject* op, DRAWPASS drawpass, BaseDraw* bd, BaseDrawHelp* bh)
{
  if (drawpass != DRAWPASS_HANDLES) return DRAWRESULT_SKIP;

  AutoAlloc<GeClipMap>clipmap;
  DrawText(bd, clipmap, "Hello World!", 100, 100, Vector(RCO 1.0), Vector(RCO  0.0), 4);
  bd->SetMatrix_Matrix(NULL, Matrix());

  return DRAWRESULT_OK;
}

Comments on the code

Why is the clipmap initialized twice?

To set up a clipmap with the correct size to fit our string, we need to know how high and wide the string is going to be when drawn into the clipmap. The thing is, we don’t know that until we ask the clipmap to calculate height and width for us. And the clipmap won’t do that, before it has been properly initialized and before its BeginDraw() function has been called.

Therefore, we have to init the clipmap once and pretend to start drawing, to be able to get the width and height of the string. After that, we can use those values to re-initialize the clipmap with a reasonable size.

Set draw matrix

As we’re drawing in screen space, the draw matrix has to be set accordingly. If you already know you’re going to call DrawText() more often, it might be a good idea to remove the SetMatrix_Screen() call and put it before calling DrawText().

ClipMapSetColor

It is most comfortable to use Vectors for color values. Since the GeClipMap only allows us to use colors as three separate LONG values (ranging from 0 to 255), we use this function to convert and set the color vector.

Clipmap as class member

The performance can be improved by making ‘clipmap’ a member of class ‘ExampleObject’, instead of allocating it just before calling DrawText().

Further thoughts

Of course, this is just a simple implementation. It could be visually improved to look more appealing, e.g. by adding rounded corners to the background.

Problems with Z depth?

If you’re having the problem of the text hiding behind objects in the scene, consider doing the drawing from a SceneHook.

Advertisements