2
votes

So I've got a class Label that inherits from osg::Geode which I draw in the world space in OpenSceneGraph. After displaying each frame, I then want to read the screen space coordinates of each Label, so I can find out how much they overlap in the screen space. To this end, I created a class ScreenSpace which should calculate this (the interesting function is calc_screen_coords.)

I wrote a small subroutine that dumps each frame with some extra information, including the ScreenSpace box which represents what the program thinks the screen space coordinates are:

Labels with ScreenSpace that matches the Label

Now in the above picture, there seems to be no problem; but if I rotate it to the other side (with my mouse), then it looks quite different:

Labels with ScreenSpace that does not match the Label whatsoever

And that is what I don't understand.

Is my world to screen space calculation wrong? Or am I getting the wrong BoundingBox from the Drawable? Or maybe it has something to do with the setAutoRotateToScreen(true) directive that I give the osgText::Text object?

Is there a better way to do this? Should I try to use a Billboard instead? How would I do that though? (I tried and it totally didn't work for me — I must be missing something...)

Here is the source code for calculating the screen space coordinates of a Label:

struct Pixel {
    // elided methods...

    int x;
    int y;
}

// Forward declarations:
pair<Pixel, Pixel> calc_screen_coords(const osg::BoundingBox& box, const osg::Camera* cam);
void rearange(Pixel& left, Pixel& right);

class ScreenSpace {
public:
    ScreenSpace(const Label* label, const osg::Camera* cam)
    {
        BoundingBox box = label->getDrawable(0)->computeBound();
        tie(bottom_left_, upper_right_) = calc_screen_coords(box, cam);
        rearrange(bottom_left_, upper_right_);
    }

    // elided methods...

private:
    Pixel bottom_left_;
    Pixel upper_right_;
}

pair<Pixel, Pixel> calc_screen_coords(const osg::BoundingBox& box, const osg::Camera* cam)
{
    Vec4d vec (box.xMin(), box.yMin(), box.zMin(), 1.0);
    Vec4d veq (box.xMax(), box.yMax(), box.zMax(), 1.0);

    Matrixd transmat
        = cam->getViewMatrix()
        * cam->getProjectionMatrix()
        * cam->getViewport()->computeWindowMatrix();

    vec = vec * transmat;
    vec = vec / vec.w();

    veq = veq * transmat;
    veq = veq / veq.w();

    return make_pair(
        Pixel(static_cast<int>(vec.x()), static_cast<int>(vec.y())),
        Pixel(static_cast<int>(veq.x()), static_cast<int>(veq.y()))
    );
}

inline void swap(int& v, int& w)
{
    int temp = v;
    v = w;
    w = temp;
}

inline void rearrange(Pixel& left, Pixel& right)
{
    if (left.x > right.x) {
        swap(left.x, right.x);
    }
    if (left.y > right.y) {
        swap(left.y, right.y);
    }
}

And here is the construction of Label (I tried to abridge it a little):

// Forward declaration:
Geometry* createLeader(straph::Point pos, double height, Color color);

class Label : public osg::Geode {
public:
    Label(font, fontSize, text, color, position, height, margin, bgcolor, leaderColor)
    {
        osgText::Text* txt = new osgText::Text;
        txt->setFont(font);
        txt->setColor(color.vec4());
        txt->setCharacterSize(fontSize);
        txt->setText(text);

        // Set display properties and height
        txt->setAlignment(osgText::TextBase::CENTER_BOTTOM);
        txt->setAutoRotateToScreen(true);
        txt->setPosition(toVec3(position, height));

        // Create bounding box and leader
        typedef osgText::TextBase::DrawModeMask DMM;
        unsigned drawMode = DMM::TEXT | DMM::BOUNDINGBOX;
        drawMode |= DMM::FILLEDBOUNDINGBOX;
        txt->setBoundingBoxColor(bgcolor.vec4());
        txt->setBoundingBoxMargin(margin);
        txt->setDrawMode(drawMode);
        this->addDrawable(txt);

        Geometry* leader = createLeader(position, height, leaderColor);
        this->addDrawable(leader);
    }

    // elided methods and data members...
}

Geometry* createLeader(straph::Point pos, double height, Color color)
{
    Geometry* leader = new Geometry();
    Vec3Array* array = new Vec3Array();
    array->push_back(Vec3(pos.x, pos.y, height));
    array->push_back(Vec3(pos.x, pos.y, 0.0f));
    Vec4Array* colors = new Vec4Array(1);
    (*colors)[0] = color.vec4();
    leader->setColorArray(colors);
    leader->setColorBinding(Geometry::BIND_OVERALL);
    leader->setVertexArray(array);
    leader->addPrimitiveSet(new DrawArrays(PrimitiveSet::LINES, 0, 2));
    LineWidth* lineWidth = new osg::LineWidth();
    lineWidth->setWidth(2.0f);
    leader->getOrCreateStateSet()->setAttributeAndModes(lineWidth, osg::StateAttribute::ON);
    return leader;
}

Any pointers or help?

1
I also use transformations from world to screen and viceversa and I use a similar code like your calc_screen_coords. The only differences are that I don't divide the coords by the homogeneus component and that component is always set to 0.0. Also, I work directly with a Vec3d, no casting to ints involved and don't modify the world coordinates directly. In my function I return the screen coordinate.Adri C.S.
@AdriC.S. I always understood that you need to divide by the homogeneous component in the end to get actual pixel values—am I wrong here? I'd be interested in seeing your function. Also, when you say that you work directly with Vec3d, I assume you are still only interested in the x and y components.cassava
@cassava: using w=1 for the input points and dividing by w after the projection is the correct approach. One thing which is wrong is that you only use 2 points of the bounding box, at least in the general case. Is the bounding box you have actually in world space (and including that auto rotation)? If so, is it axis-aligned there?derhass
@cassava I'm using osg::Vec3d for representing model points. Then, to project them to screen space I'm using: point * view_matrix * proj_matrix * window_matrix. The z-coord of the projected point is set to 0.0. Yesterday I told you that I was setting the w-coord to 0. I was wrong; I'm ignoring that coord. The projected point type is osg::Vec3d too, so I'm supposing osg is taking care of that part for me.Adri C.S.
@derhass When representing a point from the model, should I use an Vec4d for the type, explicitly setting the w-coord to 1.0? Up until now I was working with osg::Vec3d types, ignoring the w-coord.Adri C.S.

1 Answers

2
votes

I found a solution that works for me, but is also unsatisfying, so if you have a better solution, I'm all ears.

Basically, I take different points from the Label that I know will be at certain points, and I calculate the screen space by combining this. For the left and right sides, I take the bounds of the regular bounding box, and for the top and bottom, I calculate it with the center of the bounding box and the position of the label.

ScreenSpace::ScreenSpace(const Label* label, const osg::Camera* cam)
{
    const Matrixd transmat
        = cam->getViewMatrix()
        * cam->getProjectionMatrix()
        * cam->getViewport()->computeWindowMatrix();

    auto topixel = [&](Vec3 v) -> Pixel {
        Vec4 vec(v.x(), v.y(), v.z(), 1.0);
        vec = vec * transmat;
        vec = vec / vec.w();
        return Pixel(static_cast<int>(vec.x()), static_cast<int>(vec.y()));
    };

    // Get left right coordinates
    vector<int> xs; xs.reserve(8);
    vector<int> ys; ys.reserve(8);
    BoundingBox box = label->getDrawable(0)->computeBound();
    for (int i=0; i < 8; i++) {
        Pixel p = topixel(box.corner(i));
        xs.push_back(p.x);
        ys.push_back(p.y);
    };
    int xmin = *min_element(xs.begin(), xs.end());
    int xmax = *max_element(xs.begin(), xs.end());

    // Get up-down coordinates
    int ymin = topixel(dynamic_cast<const osgText::Text*>(label->getDrawable(0))->getPosition()).y;
    int center = topixel(box.center()).y;
    int ymax = center + (center - ymin);

    bottom_left_ = Pixel(xmin, ymin);
    upper_right_ = Pixel(xmax, ymax);
    z_ = distance_from_camera(label, cam);
}