2

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?

genpfault
  • 51,148
  • 11
  • 85
  • 139
cassava
  • 578
  • 6
  • 14
  • 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. Sep 23 '14 at 16:21
  • @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 Sep 23 '14 at 17:07
  • @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 Sep 23 '14 at 18:23
  • @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. Sep 24 '14 at 08:07
  • @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. Sep 24 '14 at 08:09
  • @AdriC.S.: No, you already use `Vec4d` for `vec` and `veq`, and explicitely set `w` to 1. So that is not the issue. – derhass Sep 25 '14 at 15:51
  • @derhass I think you have mistaken the users. It's cassava the one who posted the question... :) – Adri C.S. Sep 25 '14 at 15:53
  • @AdriC.S.: Oh, sorry. You are right. Well, I don't know enoguh about osg to answer your comment from yesterday. – derhass Sep 25 '14 at 15:54
  • @derhass That's a pity. I've already changed my code to use the 4 dimensions and `w`. I was asking because event though I explicitly ignored `w`, it still worked. – Adri C.S. Sep 25 '14 at 15:59
  • @derhass (Hey sorry for the wait!) I also realized just as you mentioned it that taking the two points seemed to not be enough. But if I take all the points, I get a screenspace box that is bigger than the label shown sometimes. So that's a different problem. Will update the question shortly. – cassava Sep 26 '14 at 12:46
  • @derhass I think the bounding box is in world space? Shouldn't it be visible from the code. What do you mean, is it axis-aligned? I assume so—otherwise I wouldn't be having this phenomenon in the first place, right? (Like I already said, I realized the root of this problem, but I don't know how to solve it... :-/) – cassava Sep 26 '14 at 12:48
  • One thing. Are you sure the bounding box is in world space? Shouldn't it be in model space? – Adri C.S. Oct 02 '14 at 11:34
  • @AdriC.S. It is in world space right? Otherwise the calculation I am doing would not work even a little bit. – cassava Oct 07 '14 at 11:25
  • @cassava Well, now I'm not sure :D As I see in the OSG Beginner's book, chapter 7, each model has its own coord system. When multiplying the point by the *model-view matrix*, you get the *world coords*. And if you do: `point * modelViewMat * projMat * windowMat` you get the screen coords. I asked because I'm curious, as you said your calculations worked and I also want to understand this better. – Adri C.S. Oct 07 '14 at 15:43
  • @AdriC.S. Ahh I see; you are probably right then. Makes sense to me. Thanks for the info! – cassava Oct 08 '14 at 06:19
  • @cassava You're welcome! Try it and update us with the results!! :) – Adri C.S. Oct 08 '14 at 07:26

1 Answers1

2

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);
}
cassava
  • 578
  • 6
  • 14