PDA

View Full Version : 2d orthographic projection bug???



HollanErno
03-24-2008, 11:33 AM
Hello everyone I know that claiming a bug is the last thing that a serious programmer should do... but anyway I would like you to have a look at this function that draws a simple square:

static void draw_screen( void )
{
glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

GLint viewport[] = {0,0,0,0};
glGetIntegerv(GL_VIEWPORT, viewport);

glMatrixMode(GL_MODELVIEW);
glLoadIdentity();

glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluOrtho2D( 0, viewport[2]-1, 0, viewport[3]-1 );

// float eps = 0.001937f;
float eps = 0.0f;
glBegin(GL_LINE_LOOP);
glColor3f(1,1,0);
glVertex2f(eps, eps);
glVertex2f(100, eps);
glVertex2f(100, 100);
glVertex2f(eps, 100);
glEnd();

SDL_GL_SwapBuffers();
}

The function above simply draws a 100x100 square with the bottom/left corner set to 0,0 that is the bottom/left of the screen. With my NVIDIA 8600 GT driver the bottom and left edge of the square are not visible unless you add a little "epsilon" as in the example. Instead with the Microsoft GDI driver the square is correctly drawn even without the "epsilon".

If you cut and past the function in the SDL OpenGL sample from the SDL documentation you can immadiately try it out.

What do you think? it's a bug or it's a feature?

Thanks

Bob
03-24-2008, 12:24 PM
If your viewport dimensions are W by H pixels, then the correct setup for the projection matrix is gluOrtho2D(0, W, 0, H). Don't subtract 1 from the width and height.

It's easy to realize that. If your viewport is W pixels wide, and you want a one to one relationship between coordinates and pixels, then the view volume must be W units wide aswell. Your viewport, however, is W-1 units wide.

HollanErno
03-24-2008, 03:29 PM
We are getting a bit off topic but anyway... since this is actually a common misconception that many programmers do, I will explain it here:

glOrtho(GLdouble left, GLdouble right, GLdouble bottom, GLdouble top, GLdouble near, GLdouble far);

"...(left,bottom,-near) and (right,top,-near) are points on the near clipping plane that are mapped to the lower left and upper right corners of the viewport window, respectivey." [OpenGL Programmer's Guide 6th ed.]

So if I have a 640 x 480 pixel screen my coordinates have to go from 0 to 639 on the x and from 0 to 479 on the y, respectively mapped to the lower left and upper right corner of my viewport, as the specs say (and the program shows). If my coordinates go from 0 to 640 and from 0 to 480 what I get is a projection matrix for a 641x481 screen, which is not what we are looking for.

A simple test will wipe out any doubt, let's try to draw a box on the borders of our viewport:

The following is wrong since it leaves a pixel on the top and right borders.
gluOrtho2D(0, W, 0, H)
glVertex2f(0, 0);
glVertex2f(W-1, 0);
glVertex2f(W-1, W-1);
glVertex2f(eps, W-1);

The following is correct:
gluOrtho2D(0, W-1, 0, H-1)
glVertex2f(...);
...

Also some times graphics programmers fall in the trap of mixing up distances in pixel spaces and in "real" spaces. For example a BOX whose opposite corners are at (0,0)-(10,10) is 10 units wide in "real" coordinates (for example a box in the real world, or in a 3D scene). But, if that box is in a 2D scene, so we are counting in pixels, that box is 11 pixels wide not 10. Infact the cardinality of the following set { 0,1,2,3,4,5,6,7,8,9,10 } is 11, one for each pixel covered by the 2D box.

Hope this cleared up the background theory :)

stephen diverdi
03-24-2008, 04:03 PM
I think what you're seeing is an artifact of how lines are rasterized. When you specify gluOrtho(0,w,0,h), the exact edges of the screen (not the centers of the border pixels, but the outer edges of those pixels) are at x=0,w and y=0,h. So when you draw a line from (0,0) to (100,0), that line is exactly on the border of the image, and gets rasterized (apparently) to the pixels below and to the left. It's a degenerate case. If you want to exactly color pixels with a line, you need to specify that the line goes through the _centers_ of the pixels, which would be at an offset of 0.5 (for a correctly set up pixel projection).

On that last point, for a one to one pixel mapping, what you should set up is an ortho of (0,w,0,h), with a viewport of (0,0,w,h). This gives you a situation where (0.5,0.5) is the center of the lower left pixel, and (w-0.5,h-0.5) is the center of the upper-right-most pixel. Drawing a quad from (0,0) to (w,h) will exactly fill the screen (because quads are rasterized based on the _interior_ of the specified geometry), while a line that defines the border of the screen should be from (0.5,0.5) to (w-0.5,h-0.5).

I've heard that this behavior differs between OpenGL and Direct3D, but I'm not sure about that. No D3D experience.

Bob
03-24-2008, 04:08 PM
The misconception and lack of understanding about the rasterization rules employed by OpenGL is, actually, on you here. The coordinate system in OpenGL, which you specify with gluOrtho2D, is a continuous coordinate system, not a discrete system measured in discrete pixels.

Take a look at this diagram, illustrating the problem. It's a viewport being 5 pixels wide. Same with height, so just going for a one-dimensional example here.


|--x--|--x--|--x--|--x--|--x--|
0 1 2 3 4 5

This is the whole viewport. | represents pixel boundaries, x represents pixel centers, and - representes the continuous axis. Notice how the viewport is 5 pixel wide, the very left edge is coordinate 0, and the very right edge is coordinage 5, not 4 as you're arguing for. Note also that pixel centers are located at integer plus one half offset, and that integer coordinates are located on the boundaries between adjacent pixels.

Rasterization rules in OpenGL states that a pixel is rasterized (for a filled primitive) if it's center is located inside the primitive being drawn. Now draw a "quad" from 1 to 4, and it will cover the pixel centers at 1.5, 2.5, and 3.5. The quad is therefore 3 pixels wide when rasterized, and it was specified as being 3 units wide (from 1 to 4). Everything is consistent.

Bob
03-24-2008, 04:15 PM
And I just noticed that you're drawing lines, not filled primitives. While the theory for the continuous axis I provided is correct and still applies, the rasterization rule for quads I mentioned is not really relevant to your post. What stephen diverdi said about drawing lines from/to half-integer coordinates are correct. Different rules applies for lines and points that for filled primitives.

-NiCo-
03-24-2008, 05:04 PM
Here (http://www.opengl.org/discussion_boards/ubbthreads.php?ubb=showflat&Number=235566#Post235566)'s a link to another thread on this forum discussing this problem.

Like Stephen already mentioned, this behaviour differs between OpenGL and D3D as you can see here (http://msdn2.microsoft.com/en-us/library/bb219690.aspx). The second figure shows that D3D has its origin in the center of the corner pixel while OpenGL defines the origin as the corner of the corner pixel.

HollanErno
03-24-2008, 05:37 PM
First of all thank you guys for your attention.

Yes, drawing lines and drawing polygons leads to very different behaviours because polygons have to be drawn in a way that they don't overwrite each other when they are drawn one next to the other.

With respect to the "misconception and lack of understanding" topic I think that the documentation states it very clearly, you cannot really go that much wrong: review the part about what maps to what. I also invite you to take your time and try the simple tests I mentioned above, and see it working. My experience shows very clearly that what you state in theory don't match reality. The "drawing" above shows the "misconception" I was talking about or if you like it more, that's not the 2D I'm looking for, which is a mapping from glVector2f to the pixel on the screen (m still talking bout lines and points), which means that if you want to draw on the last pixel of a 640 wide screen I would like to use glVector2f/i(639,y) as in any other 2D program and not glVector2f/i(640,y).

Further note about pixel and centers (also very easy to test) and possible explaination of the originally repoted problem:

Established that gluOrtho2D(0, W-1, 0, H-1) produces the expected resuts (as you might have tested by now) we can see that the coordinate glVector2f(0,0) is __inside__ the viewing frustum, to be more precise is at the very bottom left border of the bottom left pixel. On the other hand, the coordinate glVector2f(W-1,H-1) is __inside__ the viewing volume as expected, more precisely at the very top/right edge of the top/right pixel.

This might mean that in my NVIDIA driver (and not with the GDI one and another Radeon I just tested) numerical instability creep in and makes the (0,0) coordinates snap to the previous pixel, thus contraddicting what according to the documentation should happen, that is, coordinate (0,0) __should__ be mapped to pixel (0,0).

Cheers

stephen diverdi
03-24-2008, 05:50 PM
If you want to use points and lines and have integer coordinates exactly specify pixel centers, the correct way to do that would be to use an ortho of (-0.5,w-0.5,-0.5,h-0.5). Then (0,0) would exactly map to the center of the lower left pixel, and (w-1,h-1) would exactly map to the center of the upper right pixel.

The problem with your examples is they are not thorough enough - you are testing degenerate conditions. When you put a point or line on (0,0) with an ortho of (0,w,0,h) or (0,w-1,0,h-1), your geometry falls exactly on the corner of four pixels - which one should be turned on? It's implementation dependent, and subtle rounding issues can influence it.

One thing is certain - if you set up an ortho of (0,w-1,0,h-1) and draw a quad from (0,0) to (w-1,h-1) it will exactly fill the scene. Similarly, if you set up an ortho of (0,1,0,1) and draw a quad from (0,0) to (1,1) it will also exactly fill the screen. But neither of those will give you exact pixel addressability. Your (0,w-1,0,h-1) may give you the appearance of exact pixel coordinates because of rounding and nearest sampling. Try using anti-aliasing or linear texture filtering and compare exact bit values in the resulting images and you will see errors.

This is a common problem in GPGPU work and has been hashed out very carefully for this reason. If you have clear documentation that directly contradicts the above, please post a link to it.

HollanErno
03-24-2008, 05:52 PM
Thank you NiCo!
the link on the MSDN is indeed very interesting and explains also some other "artefacts" about 2D and texturing I was getting on another project...

HollanErno
03-24-2008, 06:11 PM
Thank you Stephen,
in my opinion you said one thing right and one not.

The right thing, that I also just realized based on what I said in my previous post about pixels and coordinates, is that an ortho (-0.5,w-0.5,-0.5,h-0.5) will give me a better, more stable projection.

The non right thing is that for example the coordinate (0,0) is not in the middle of four pixels but is at the bottom left of the bottom left pixel on the viewport. This is also what the documentation quoted by me above from the Red Book states:

glOrtho(GLdouble left, GLdouble right, GLdouble bottom, GLdouble top, GLdouble near, GLdouble far);

"...(left,bottom,-near) and (right,top,-near) are points on the near clipping plane that are mapped to the lower left and upper right corners of the viewport window, respectivey." [OpenGL Programmer's Guide 6th ed.]

So, in our case, an OpenGL compliant implementaton has to map a glVertex2f/i(0,0) to the pixel (0,0). If it doesn't the driver is wrong, it's not a degenerate case at all. I think the Red Book couldn't be clearer.

To sum up: glOrtho(-0.5,w-0.5,-0.5,h-0.5) solves my problems so, for once, am happy :)

stephen diverdi
03-24-2008, 06:19 PM
Ah, hmm, I see what you're saying. Say the pixel coordinate (1,1) then - that is on the corner of the lower left most four pixels. (0,0) is in the same position - on the corner - but three adjacent pixels don't exist because they are outside of the viewport. However, I believe they are rasterized with the same rules as pixels not on the border (or else there would be complicated special casing for border pixels, something OpenGL has largely stayed away from, with good reason). Therefore, if a point drawn at (1,1) (on the corner) were to be rasterized by turning on the pixel to the lower left, then a point drawn at (0,0) would be rasterized the same way...by "turning on" a pixel that's off the screen - hence the behavior you originally posted about, where two lines of your square did not appear. It is a degenerate case because the rules of rasterization are not defined for geometry that exists exactly on the corners or edges of pixels, whether those are border pixels or interior pixels.

As the redbook explains, (0,0) is the lower left corner of the viewport, which is as you say, the lower left corner of the lower left pixel. So (0,0) is not a pixel center, but a pixel corner - hence the degenerate case.

HollanErno
03-24-2008, 06:42 PM
I am sorry but I couldn't follow your reasoning, how did you get that a point on coordinates (1,1) is on the same position of one at coordinates (0,0), anyway... also I don't get how could you possibly imagine a - special management - for border pixels...

Actually I see where we don't agree, on a very simple point, that is: the lower left corner of the pixel (0,0) as mapped from glOrtho is included in the pixel or is - theoretically - exactly between 4 pixels? as I saw it in practice I have been led to agree with the first option, you instead agree with the second. I must say that I cannot say who is right with this respect and that's why I go for the glOrtho(-0.5,w-0.5,-0.5,h-0.5) option :)

Bob
03-25-2008, 01:16 AM
To sum up: glOrtho(-0.5,w-0.5,-0.5,h-0.5) solves my problems so, for once, am happy :)


And the funny thing about it, if you remove the 0.5 offset, you get the following projection matrix: glOrtho(0, w, 0, h). Which is what I have been saying is the correct one all the time.

But as you said I have been testing it, I'm gonna see if you can explain this, if you still don't believe it. Concider this piece of code. I will explain the purpose of it, what it's supposes to do, what it gives, and you will explain why that is.


#include <GL/glut.h>

void display()
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glEnable(GL_BLEND);
glEnable(GL_LINE_SMOOTH);

glBegin(GL_LINES);
glVertex2f(150, 100);
glVertex2f(150, 200);
glVertex2f(100, 150);
glVertex2f(200, 150);
glEnd();

glFlush();
}

void reshape(int w, int h)
{
glViewport(0, 0, w, h);

glMatrixMode(GL_MODELVIEW);
glLoadIdentity();

glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glOrtho(0, w, 0, h, -1, 1);
glTranslatef(-0.5, -0.5, 0);
}

int main(int argc, char** argv)
{
glutInit(&amp;argc, argv);
glutCreateWindow(argv[0]);
glutDisplayFunc(display);
glutReshapeFunc(reshape);
glutMainLoop();
}

The code sets up a coordinate system according to my specifications. No -1, and a 0.5 offset. In the display function I enable line smoothing and blending. The reason for this is to see how close to the pixel centers I actually hit with my coordinates. If I hit the exact center, I get a solid white line. If I hit the edge between two pixels, I get 50% coverage on both pixels, and so I get a 50% grey line covering two pixels. Hitting something bweteen the centers and the edges gives me something inbetween.

What do I get with the following code? A solid white line, meaning exact pixel center lines. No matter where I place it (on integer coordinates, of course), I get a solid white line. Change the projection matrix to subtract one, and I get 50% coverage for lines in the center. As the lines are draws towards the edges, the lines become more and more solid white. I say this is because my viewport is correct, I get the solid white line all the time. You get a greyish line, approaching white towards the edges, because your viewport is actually one unit too small. A W wide viewport is only W-1 units across. Also, if I change the size of the window, with your setup, the coverage of the lines changes. This is reasonable as the error in your mapping depends on the size of the viewport (one unit over the whole viewport, which means the error cgnages with changes in the viewport).

This does not happen with my set up. Can you explain this, if your matrix setup is correct?

The quote from the Red Book you post is indeed very clear on this, but you're misinterpreting it. It does say left is the left edge of the viewport, but since pixels are not discrete entities, but located on a continuous axis, it's not the leftmost pixel that's on the left edge. To the very left you find the LEFT EDGE of the leftmost pixel. To the right you find the RIGHT EDGE of the rightmost pixel.

So the viewport stretches from the left edge of the left pixel to the right edge of the right pixel. All according to my diagram previously posted.

HollanErno
03-25-2008, 02:48 AM
Yes and if you shrink the settings by 0.5 you also get mine... that's a meaningless sentence actually. Also you might have noticed in your source code a glTranslatef(-0.5, -0.5, 0) which makes the final matrix equivalent to a glOrtho(-0.5, w-0.5, -0.5, h-0.5, -1, 1), so also the sentence "The code sets up a coordinate system according to my specifications. No -1, and a 0.5 offset." is false. But anyway that's not important we already agreed that glOrtho(-0.5, w-0.5, -0.5, h-0.5, -1, 1) is the way to go.

Just as a final note: it seems you prefear to think that glOrtho(-0.5, w-0.5, -0.5, h-0.5, -1, 1) is a glOrtho(0, w, 0, h, -1, 1) shifted by -0.5, instead I prefer to see it as a glOrtho(0, w-1, 0, h-1, -1, 1) to which we add a 0.5 border around. The resulting matrix is the same but in my opinion the second is a more correct way of thinking. But hey, every one is free to think the way one wants as long a one gets the same results.

Lindley
03-25-2008, 06:51 AM
I think of it in terms of graph paper. The lines on the paper represent integers, and the squares represent pixels.

Thinking of it this way, you can clearly see that C-style addressing of 0 to n-1 isn't actually correct in OpenGL. What you really want is .5 to n-.5, and you get that by setting a projection of 0 to n.

painterb
03-25-2008, 07:00 AM
Hi HollanErno, I'm glad you found a solution that is satisfying to you ... although I too agree with Bob and stephen on their justifications.

No matter though ... I'm just wondering if someone has a pointer to what the actual OpenGL rasterization rules are for points/lines/polygons in the Redbook (preferred), but any source would be useful. Especially for the degenerate cases that are mentioned above, although they certainly seem to be left to the implementation.

I've never seen them spelled out in the Redbook (maybe I just missed it). I suspect they are detailed in D3D, and I do remember they were clearly documented in the old 3dfx API.

HollanErno
03-25-2008, 07:56 AM
Lindely, you are right when you say that you think bout it in terms of graph paper, that's correct for a 0 to n projection. But that projection doesn't lead me to what I want that is glVertex(x,y) map to the center of the x-th, y-th pixel.
Have a look at Bob's original example, slightly modified to show the viewport borders.



view port borders
|
| |
V V

]|--x--|--x--|--x--|--x--|--x--|[
0 1 2 3 4 5


where the | are integer coordinates and x are pixel centers.

You see? glVertex2f(2,0) with line smoothing actived will draw on the second and the third pixel, that's not what we wanted. Again, what we wanted is a projection that maps glVertex2f(x,y) to the center of the x-th and y-th pixel. This is why you (plural) are wrong when you say that you need a "0 to n" projection. And that's why Bob fixed this by adding the glTranslation(-0.5,-0.5,-0.5) on is last example just after gluOrtho2D, so that when you write glVertex2f(2,0) the OpenGL actually gets a glVertex2f(2-0.5,0-0.5).

As we already established, what you need is a "-0.5 to n-0.5" or as i like it more "-0.5 to n-1+0.5" projection, look:



. x . x . x . x . x . x .
]--|--.--|--.--|--.--|--.--|--.--|--[
-0.5 0 . 1 . 2 . 3 . 4 . N-1 N-1+0.5


The dots mark the pixel borders. And ] and [ the viewport boundaries. You see how the "x" (pixel centers) now perfectly match the integer coordinates passed by glVertex*? Which is exactly what we wanted. No glTranslate hacks needed, perfect with smoothing activated, flawless with "C-style" or as I like it more "2D style" pixel addressing. All the rest is only, well, "justifications".

Thank you very much.

Bob
03-25-2008, 09:01 AM
Yes and if you shrink the settings by 0.5 you also get mine... that's a meaningless sentence actually. Also you might have noticed in your source code a glTranslatef(-0.5, -0.5, 0) which makes the final matrix equivalent to a glOrtho(-0.5, w-0.5, -0.5, h-0.5, -1, 1), so also the sentence "The code sets up a coordinate system according to my specifications. No -1, and a 0.5 offset." is false. But anyway that's not important we already agreed that glOrtho(-0.5, w-0.5, -0.5, h-0.5, -1, 1) is the way to go.

The quote was not meningless. Your initial claim was that glOrtho(0, w-1, 0, h, -1, 1) was the correct one. But no matter how you look at it, and no matter how much you try, the projection matrix is too narrow. You insisted that the projection matrix should be with one smaller than the number of pixels, and the one that you was happy with turned out to be the one I mentioned first, although shifted (didn't mention the shift first becuse I though you where dealing with filled primitives, which was a slight mistake on my side).

By shifting it -0.5, as I did, I'm preserving the size. If you narrow it by 0.5 as you just said, you make the projection matrix narrower, and you cannot make it work perfectly no matter what you do. That's a big difference, and why my comment certainly isn't meaningless.

Anyway, your way of viewing it suffers from the translation hack just as much as mine. You have to adjust it by 0.5 to get the desired result, and that's what I have to do also. I shift the whole viewport, you shift the edges in opposite directions. And I don't consider it a hack at all, I consider it a necessary operation to conform with the rasterization rules specified by OpenGL to guarantee correct results.

HollanErno
03-25-2008, 09:33 AM
Of course we could argue for ages about what is considered a hack and what is not. I Am sure one can obtain the same results in many different ways adding, dividing, scaling, subtracting matrices as much as one wanted, I am not here to judge one programmer's taste, I just believe that glOrtho2D(-0.5,w-0.5,-0.5,h-0.5) looks like the neater and cleaner solution to me and that a glOrtho2D(0,w,0,h) is simply wrong by itself unless you fix your coordinates in some way.
I just wanted to make clear one final point:
glOrtho2D(-0.5,w-0.5,-0.5,h-0.5) doesn't lack any glTranslation "hack", you just trow glVertex*i() and it simply works.

Thank you for your attention, I was a very constructive discussion indeed.
Cheers

stephen diverdi
03-25-2008, 11:03 AM
The two setups you are both talking about, ortho(-0.5,w-0.5,-0.5,h-0.5), vs. ortho(0,w,0,h) followed by translate(-0.5,-0.5,0), both create identical projection matrices. If you don't believe me, use glGet to check. So, I would say they're equally "hack-y", which is to say, not at all hack-y. It's what you need to do to get the results you want and it's the only way to do it within the defined functionality. How can you call that a hack?

As to if using ortho by itself or ortho + translate is more hacky than the other, well that's silly. There are many ways to specify matrices in OpenGL. The options are available for you to use the one you're most comfortable with. For some people, that's constructing a matrix by hand and passing it in with glLoadMatrix. For others, it's passing the final params in to ortho, and for yet others, it's passing basic params in to ortho and using matrix manipulation calls to adjust it until it's right. None of this is a hack.

stephen diverdi
03-25-2008, 11:28 AM
No matter though ... I'm just wondering if someone has a pointer to what the actual OpenGL rasterization rules are for points/lines/polygons in the Redbook (preferred), but any source would be useful. Especially for the degenerate cases that are mentioned above, although they certainly seem to be left to the implementation.

You can check it out in the OpenGL 1.1 spec at:

http://opengl.org/documentation/specs/version1.1/glspec1.1/node41.html

Specifically, sections 3.4.1 (line rasterization) and 3.5.1 (polygon rasterization). Or for a more recent version (in PDF format), take a look at the OpenGL 2.1 spec at:

http://www.opengl.org/registry/doc/glspec21.20061201.pdf

However, I don't believe the particulars of rasterization have changed in the interim.

HollanErno
03-25-2008, 11:33 AM
Stephen, I surely belive you, as you can deduce from my previous post, so as I see it there isn't so much space left to argue.

About the "what can be considered a hack and what is not" bit, again, as I said above, that's not an interesting topic to me.

Bob
03-25-2008, 12:12 PM
Thank you for your attention, I was a very constructive discussion indeed.
Cheers
Sure was, I suppose. You're welcome anyway.

Relic
03-26-2008, 01:47 PM
You could have made your life much easier when considering what the formulas are for a 1x1 viewport.
For that you setup an ortho exactly on the corners of that single pixel: gluOrtho2D(0, 1, 0, 1), that's it, not fudge factors, nothing.
For pixel aligned rendering of filled geometry you run on coordinates on corners of pixels, so simple integer coordinates, e.g. glRecti(0,0,1,1).
For lines rendering the diamond exit rule applies, so you need to run on pixel centers, that is your integer coordinates + 0.5 sent as float.
Same for points, render on pixel centers.
That's all folks.