ObjectTextDisplay         Displaying 3D Object Text in 2D Without Using Billboards

See Displaying non-overlapping Text Overlays above Ogre MovableObjects for an enhanced version.

Introduction

Commonly, you will want to draw text above or near an object in your 3D scene, perhaps to display its name and/or faction, or to show some other information about the object. One method is to employ billboards for this purpose, but billboards have their own caveats and gotchas when used for this purpose; for example, maintaining text legibility as distance increases, or actually drawing the text for that matter. In many cases, it would be simple enough to use a TextArea OverlayElement, if only you knew how to get it to follow your object around the screen. This article demonstrates how to do just that.

The View Matrix

The important part of this equation is the view matrix; this is the matrix that controls the transformation of your object's vertices from world space to camera space (from the viewpoint of the camera, in other words). Since the computer display is just a viewport in which the camera renders that viewpoint, we can use the view transform to obtain the relative screen coordinates that we need to be able to place text above an object.

The Code

(Standard disclaimer and rant)
The following class is copyright "me", but you can do whatever you please with it (try to avoid passing it off as your own, but feel free to use it however you need in whatever capacity, including closed-source commercial apps). Change it, pass it around, use printouts of it for bin liners; it's entirely up to you. This code comes with no warranty, period. If it works for you, great; if it doesn't, feel free to fix it and maybe even adjust this article with the fixes. If it manages to burn down your house (or at least make you spend hours of needless debugging), that's on you, not me — that's what "no warranty" means. ;) This code may not even be correct — if it is not, please, just fix it so that it is, instead of emailing me with how clever you are and stupid I am. ;) The readers of this article are the only ones who care anyway; make your notes in the text if you want.
(End disclaimer and rant)

That said, the ObjectTextDisplay class takes an Ogre MovableObject and a Camera, and performs the needed Overlay management to make text move with your object around the screen. This class was created for demonstration purposes; it is not robust and there are plenty of other things that can be done with it — maybe someday I'll come back and make it more complete, but until then don't hold your breath.

How It Works

The class works by computing the screen-space extent of your object's world AABB (axis-aligned bounding box), as the means by which it knows where to draw text. We are working only with the view transform here, as that is sufficient for our needs. We first get the list of 8 corners of the world AABB, and expand a screen-space rectangle that defines the screen-space bounds of the object. We normalize the X and Y coordinates by dividing them both by Z (depth) and we make relative screen coordinates out of them by adding them to 0.5 (screen center in a 0,0...1,1 coordinate system). These coordinates are enough to be able to place the text above the object.

We manage that by resizing the OverlayContainer to match the width of the screen-space bounding-box and position it above the screen-space box. The program's main loop would call the "update()" method each frame to adjust the position of the overlay to match any changes in that of the object. You can set the text to be displayed with the "setText()" method at any time. And that's all there is to it.

class ObjectTextDisplay {

public:
    ObjectTextDisplay(const Ogre::MovableObject* p, const Ogre::Camera* c) {
        m_p = p;
        m_c = c;
        m_enabled = false;
        m_text = "";

        // create an overlay that we can use for later
        m_pOverlay = Ogre::OverlayManager::getSingleton().create("shapeName");
        m_pContainer = static_cast<Ogre::OverlayContainer*>(Ogre::OverlayManager::getSingleton().createOverlayElement(
                  "Panel", "container1"));

        m_pOverlay->add2D(m_pContainer);

        m_pText = Ogre::OverlayManager::getSingleton().createOverlayElement("TextArea", "shapeNameText");
        m_pText->setDimensions(1.0, 1.0);
        m_pText->setMetricsMode(Ogre::GMM_PIXELS);
        m_pText->setPosition(0, 0);

        m_pText->setParameter("font_name", "blue16");
        m_pText->setParameter("char_height", "16");
        m_pText->setParameter("horz_align", "center");
        m_pText->setColour(Ogre::ColourValue(1.0, 1.0, 1.0));

        m_pContainer->addChild(m_pText);
        m_pOverlay->show();
    }

        virtual ~ObjectTextDisplay() {

                // overlay cleanup -- Ogre would clean this up at app exit but if your app 
                // tends to create and delete these objects often it's a good idea to do it here.

        m_pOverlay->hide();
        Ogre::OverlayManager *overlayManager = Ogre::OverlayManager::getSingletonPtr();
        m_pContainer->removeChild("shapeNameText");
        m_pOverlay->remove2D(m_pContainer);
        overlayManager->destroyOverlayElement(m_pText);
        overlayManager->destroyOverlayElement(m_pContainer);
        overlayManager->destroy(m_pOverlay);
        }

    void enable(bool enable) {
        m_enabled = enable;
        if (enable)
            m_pOverlay->show();
        else
            m_pOverlay->hide();
    }

    void setText(const Ogre::String& text) {
        m_text = text;
        m_pText->setCaption(m_text);
    }

    void update();

protected:
    const Ogre::MovableObject* m_p;
    const Ogre::Camera* m_c;
    bool m_enabled;
    Ogre::Overlay* m_pOverlay;
    Ogre::OverlayElement* m_pText;
    Ogre::OverlayContainer* m_pContainer;
    Ogre::String m_text;
};

void ObjectTextDisplay::update()  {
    if (!m_enabled)
        return;

    // get the projection of the object's AABB into screen space
    const Ogre::AxisAlignedBox& bbox = m_p->getWorldBoundingBox(true);
    Ogre::Matrix4 mat = m_c->getViewMatrix();

    const Ogre::Vector3* corners = bbox.getAllCorners();

    float min_x = 1.0f, max_x = 0.0f, min_y = 1.0f, max_y = 0.0f;

        // expand the screen-space bounding-box so that it completely encloses 
        // the object's AABB
    for (int i=0; i<8; i++) {
        Ogre::Vector3 corner = corners[i];

                // multiply the AABB corner vertex by the view matrix to 
                // get a camera-space vertex
        corner = mat * corner;

                // make 2D relative/normalized coords from the view-space vertex
                // by dividing out the Z (depth) factor -- this is an approximation
        float x = corner.x / corner.z + 0.5;
        float y = corner.y / corner.z + 0.5;

        if (x < min_x) 
            min_x = x;

        if (x > max_x) 
            max_x = x;

        if (y < min_y) 
            min_y = y;

        if (y > max_y) 
            max_y = y;
    }

    // we now have relative screen-space coords for the object's bounding box; here
    // we need to center the text above the BB on the top edge. The line that defines
    // this top edge is (min_x, min_y) to (max_x, min_y)

    //m_pContainer->setPosition(min_x, min_y);
    m_pContainer->setPosition(1-max_x, min_y);  // Edited by alberts: This code works for me
    m_pContainer->setDimensions(max_x - min_x, 0.1); // 0.1, just "because"
}


Code that uses this class might look like the following:

Ogre::Entity* ogrehead = pSceneMgr->createEntity("ogrehead", "ogrehead.mesh");
    Ogre::SceneNode* node = pSceneMgr->getRootSceneNode()->createChildSceneNode();
    node->attachObject(ogrehead);

    ObjectTextDisplay* text = new ObjectTextDisplay(ogrehead, pCamera);
    text->enable(true);
        text->setText("Ogre Head");

    // simple render loop -- ALT-F4 (or stop debugging) or kill the window to exit this loop
    while (!pWindow->isClosed()) {

                // NOTE: this is Eihort (CVS HEAD, 1.3) specific -- if you are using Dagon or
                // older, you need to do Ogre::PlatformManager::getSingleton().messagePump() instead
        Ogre::WindowEventUtilities::messagePump();

                // update the object text position and size
        text->update();

        pRoot->renderOneFrame();
    }

    delete text;


Make sure to call "enable(true);" before you start trying to render this — it defaults to "off". If you plan to cut-n-paste this code you need to make sure that all of the needed resources are available to your app or expect it to crash (or at least render the ogre head in white — see your Ogre.log for things that are amiss if you have problems).

The rendered output should look like this:

ShapeNameHud.png

Enjoy!

The Code for Python-Ogre

I use Python-Ogre, so I translated this to Python, and thought I'd save others the work. In my application, I use a function
tempName()
to generate object names. You may want to replace it with your own naming system. I include the code for tempName here so you can run my code without modification:

NEXTID = 1

def tempName():
    global NEXTID
    id = NEXTID
    NEXTID += 1
    return 't%d'%id


Here is the Python class:

# Copyright (c) 2007, David Mandelin
# All rights reserved.
# Use and redistribute freely.
#
# Translated from a C++ version authored by and copyright "Xavier".
# (He did all the hard work.)
#
# This code is provided as is, without warranty, use at your own risk.

import Ogre as ogre

class OgreText(object):
    """Class for displaying text in Ogre above a Movable."""
    def __init__(self, movable, camera, text=''):
        self.movable = movable
        self.camera = camera
        self.text = ''
        self.enabled = True

        ovm = ogre.OverlayManager.getSingleton()
        self.overlay = ov = ovm.create(tempName())
        self.container = c = ovm.createOverlayElement('Panel', tempName())
        ov.add2D(c)
        self.textArea = t = ovm.createOverlayElement('TextArea', tempName())
        t.setDimensions(1.0, 1.0)
        t.setMetricsMode(ogre.GMM_PIXELS)
        t.setPosition(0, 0)
        t.setParameter('font_name', 'BlueHighway')
        t.setParameter('char_height', '16')
        t.setParameter('horz_align', 'center')
        t.setColour(ogre.ColourValue(1.0, 1.0, 1.0))
        c.addChild(t)
        ov.show()

        self.setText(text)

    def __del__(self):
        self.destroy()

    def destroy(self):
        if hasattr(self, 'dead'): return
        self.dead = True
        self.overlay.hide()
        ovm = ogre.OverlayManager.getSingleton()
        self.container.removeChild(self.textArea.name)
        self.overlay.remove2D(self.container)
        ovm.destroyOverlayElement(self.textArea.name)
        ovm.destroyOverlayElement(self.container.name)
        ovm.destroy(self.overlay.name)

    def enable(self, f):
        self.enabled = f
        if f:
            self.overlay.show()
        else:
            self.overlay.hide()

    def setText(self, text):
        self.text = text
        self.textArea.setCaption(ogre.UTFString(text))

    def update(self):
        if not self.enabled : return

        # get the projection of the object's AABB into screen space
        bbox = self.movable.getWorldBoundingBox(True);
    mat = self.camera.getViewMatrix();
    corners = bbox.getAllCorners();

        min_x, max_x, min_y, max_y = 1.0, 0.0, 1.0, 0.0
        # expand the screen-space bounding-box so that it completely encloses 
        # the object's AABB
        for corner in corners:
            # multiply the AABB corner vertex by the view matrix to 
            # get a camera-space vertex
            corner = mat * corner;
            # make 2D relative/normalized coords from the view-space vertex
            # by dividing out the Z (depth) factor -- this is an approximation
            x = corner.x / corner.z + 0.5
            y = corner.y / corner.z + 0.5

            if x < min_x: min_x = x
            if x > max_x: max_x = x
            if y < min_y: min_y = y
            if y > max_y: max_y = y
            
        # we now have relative screen-space coords for the
        # object's bounding box; here we need to center the
        # text above the BB on the top edge. The line that defines
        # this top edge is (min_x, min_y) to (max_x, min_y)

    # self.container.setPosition(min_x, min_y);
        # Edited by alberts: This code works for me
        self.container.setPosition(1-max_x, min_y);
        # 0.1, just "because"
    self.container.setDimensions(max_x - min_x, 0.1);


Author: Xavier

Bug Fixes

(rkeene) The above code assumes only one instance of the ObjectTextDisplay. Our game has many instances so you want to have unique names for each container, and only one instance of the overlay. Also if the object is behind the camera it projects forward and the text shows.

So here is my version. Tested on widescreen and regular aspect ratio. (Sorry about the TDL specific headers.)

ObjectDisplayText.h

#ifndef OBJECT_TEXT_DISPLAY
#define OBJECT_TEXT_DISPLAY

#pragma once

#include "TDLCommonClient.h"

// From the Ogre forums.  This class displays text above or next to on screen entities.
class ObjectTextDisplay {
 
public:
    static Ogre::Overlay* g_pOverlay;
 
    ObjectTextDisplay(const Ogre::MovableObject* p, const Ogre::Camera* c) {
        m_p = p;
        m_c = c;
        m_enabled = false;
        m_text = "";
 
        // create an overlay that we can use for later
		if(g_pOverlay == NULL)
		{
			g_pOverlay = Ogre::OverlayManager::getSingleton().create("floatingTextOverlay");
			g_pOverlay->show();
		}

		char buf[15];
		sprintf(buf, "c_%s", p->getName().c_str());
		m_elementName = buf;
        m_pContainer = static_cast<Ogre::OverlayContainer*>(Ogre::OverlayManager::getSingleton().createOverlayElement(
                  "Panel", buf));
 
        g_pOverlay->add2D(m_pContainer);
 
		sprintf(buf, "ct_%s", p->getName().c_str());
		m_elementTextName = buf;
        m_pText = Ogre::OverlayManager::getSingleton().createOverlayElement("TextArea", buf);
		ASSERT_VALID_PTR(m_pText);
        m_pText->setDimensions(1.0, 1.0);
        m_pText->setMetricsMode(Ogre::GMM_PIXELS);
        m_pText->setPosition(0, 0);
 
        m_pText->setParameter("font_name", "LucidaSans");
        m_pText->setParameter("char_height", "16");
        m_pText->setParameter("horz_align", "center");
        m_pText->setColour(Ogre::ColourValue(1.0, 1.0, 1.0));
 
        m_pContainer->addChild(m_pText);
		m_pContainer->setEnabled(false);
    }
 
        virtual ~ObjectTextDisplay() {
 
            // overlay cleanup -- Ogre would clean this up at app exit but if your app 
            // tends to create and delete these objects often it's a good idea to do it here.
 
			Ogre::OverlayManager *overlayManager = Ogre::OverlayManager::getSingletonPtr();
			m_pContainer->removeChild(m_elementTextName);
			g_pOverlay->remove2D(m_pContainer);
			overlayManager->destroyOverlayElement(m_pText);
			overlayManager->destroyOverlayElement(m_pContainer);
        }
 
    void enable(bool enable) {
        m_enabled = enable;
        if (enable)
		{
            m_pContainer->show();
		}
        else
		{
            m_pContainer->hide();
		}
    }
 
    void setText(const Ogre::String& text) {
        m_text = text;
        m_pText->setCaption(m_text);
    }
 
    void update();
 
protected:
    const Ogre::MovableObject* m_p;
    const Ogre::Camera* m_c;
    bool m_enabled;
    Ogre::OverlayElement* m_pText;
    Ogre::String m_text;
	Ogre::String m_elementName;
	Ogre::String m_elementTextName;
    Ogre::OverlayContainer* m_pContainer;
};


#endif


ObjectDisplayText.cpp

#include "ObjectDisplayText.h"
 
Ogre::Overlay * ObjectTextDisplay::g_pOverlay = NULL;

void ObjectTextDisplay::update()  {
    if (!m_enabled)
        return;
 
    const Ogre::AxisAlignedBox& bbox = m_p->getWorldBoundingBox(true);
	Ogre::Matrix4 mat = m_c->getViewMatrix();
 
	bool behind = false;
 
	// We want to put the text point in the center of the top of the AABB Box.
	Ogre::Vector3 topcenter = bbox.getCenter();
	// Y is up.
	topcenter.y += bbox.getHalfSize().y;
	topcenter = mat * topcenter;
	// We are now in screen pixel coords and depth is +Z away from the viewer.
	behind = (topcenter.z > 0.0);

	if(behind)
	{
		// Don't show text for objects behind the camera.
		m_pContainer->setPosition(-1000, -1000);
	}
	else
	{
		// Not in pixel coordinates, in screen coordinates as described above.
		// We convert to screen relative coord by knowing the window size.
		// Top left screen corner is 0, 0 and the bottom right is 1,1
		// The 0.45's here offset alittle up and right for better "text above head" positioning.
		// The 2.2 and 1.7 compensate for some strangeness in Ogre projection?
		// Tested in wide screen and normal aspect ratio.
		m_pContainer->setPosition(0.45f - topcenter.x / (2.2f * topcenter.z), 
			topcenter.y / (1.7f * topcenter.z) + 0.45f);
		// Sizse is relative to screen size being 1.0 by 1.0 (not pixels size)
		m_pContainer->setDimensions(0.1f, 0.1f);
	}
}

Bug Fixes 2


(madmage) Unfortunately, the formula to compute the position of the overlay element is completely wrong: it lacks the multiplication by the projection matrix (this is the reason why the original author needed the weird 2.2f and 1.7f coefficients, as described in the comments). Here is an bugfixed version of the update() function:

void ObjectTextDisplay::update()  {
    if (!m_enabled) return;
 
    const Ogre::AxisAlignedBox& bbox = m_p->getWorldBoundingBox(true);
    Ogre::Matrix4 mat = m_c->getProjectionMatrix() * m_c->getViewMatrix();
 
    // We want to put the text point in the center of the top of the AABB Box.
    Ogre::Vector3 topcenter = bbox.getCenter();
//    topcenter.z += bbox.getHalfSize().z;    // in world coordinates, z is world up
    topcenter = mat * topcenter;
    bool behind = (topcenter.z < 0.0);      // in view and clip coordinates, z is towards the screen

    if (behind) {
        // Don't show text for objects behind the camera.
        m_pContainer->setPosition(-1000, -1000);
    }   
    else {
        // After multiplying the topcenter vector by the projection matrix and view matrix, 
        // we have the coordinates in "clip space", i.e., screen coordinates clipped to (-1, 1) (where -1 is left and top, +1 is right and bottom)
        // since setPosition requires coordinates in (0, 1), we need some more math.
        m_pContainer->setPosition(topcenter.x / 2 + 0.5, -topcenter.y / 2 + 0.5);

        // Size is relative to screen size being 1.0 by 1.0 (not pixels size)
        m_pContainer->setDimensions(0.1f, 0.1f);
        //m_pContainer->setMaterialName("RedTransparent");
    }   
}