33

I'm working on a 2d engine. It already works quite good, but I keep getting pixel-errors.

For example, my window is 960x540 pixels, I draw a line from (0, 0) to (959, 0). I would expect that every pixel on scan-line 0 will be set to a color, but no: the right-most pixel is not drawn. Same problem when I draw vertically to pixel 539. I really need to draw to (960, 0) or (0, 540) to have it drawn.

As I was born in the pixel-era, I am convinced that this is not the correct result. When my screen was 320x200 pixels big, I could draw from 0 to 319 and from 0 to 199, and my screen would be full. Now I end up with a screen with a right/bottom pixel not drawn.

This can be due to different things: where I expect the opengl line primitive is drawn from a pixel to a pixel inclusive, that last pixel just is actually exclusive? Is that it? my projection matrix is incorrect? I am under a false assumption that when I have a backbuffer of 960x540, that is actually has one pixel more? Something else?

Can someone please help me? I have been looking into this problem for a long time now, and every time when I thought it was ok, I saw after a while that it actually wasn't.

Here is some of my code, I tried to strip it down as much as possible. When I call my line-function, every coordinate is added with 0.375, 0.375 to make it correct on both ATI and nvidia adapters.

int width = resX();
int height = resY();

for (int i = 0; i < height; i += 2)
    rm->line(0, i, width - 1, i, vec4f(1, 0, 0, 1));
for (int i = 1; i < height; i += 2)
    rm->line(0, i, width - 1, i, vec4f(0, 1, 0, 1));

// when I do this, one pixel to the right remains undrawn

void rendermachine::line(int x1, int y1, int x2, int y2, const vec4f &color)
{
    ... some code to decide what std::vector the coordinates should be pushed into
    // m_z is a z-coordinate, I use z-buffering to preserve correct drawing orders
    // vec2f(0, 0) is a texture-coordinate, the line is drawn without texturing
    target->push_back(vertex(vec3f((float)x1 + 0.375f, (float)y1 + 0.375f, m_z), color, vec2f(0, 0)));
    target->push_back(vertex(vec3f((float)x2 + 0.375f, (float)y2 + 0.375f, m_z), color, vec2f(0, 0)));
}

void rendermachine::update(...)
{
    ... render target object is queried for width and height, in my test it is just the back buffer so the window client resolution is returned
    mat4f mP;
    mP.setOrthographic(0, (float)width, (float)height, 0, 0, 8000000);

    ... all vertices are copied to video memory

    ... drawing
    if (there are lines to draw)
        glDrawArrays(GL_LINES, (int)offset, (int)lines.size());

    ...
}

// And the (very simple) shader to draw these lines

// Vertex shader
    #version 120
    attribute vec3 aVertexPosition;
    attribute vec4 aVertexColor;
    uniform mat4 mP;
    varying vec4 vColor;
    void main(void) {
        gl_Position = mP * vec4(aVertexPosition, 1.0);
        vColor = aVertexColor;
    }

// Fragment shader
    #version 120
    #ifdef GL_ES
    precision highp float;
    #endif
    varying vec4 vColor;
    void main(void) {
        gl_FragColor = vColor.rgb;
    }
scippie
  • 2,011
  • 1
  • 26
  • 42
  • I'm beginning to think that it's the "point exclusive" thing I'm talking about. If you use QT or C++ Builder/Delphi or even windows API to draw lines, the pixels are drawn excluding the end-point. So it's possible that the same is done in opengl, and probably also when drawing triangles? When you draw a rectangle with QT, builder, ..., you also get the bottom right corner excluded... – scippie Apr 06 '12 at 08:48
  • The following test shows agreement with what I said above: when I use GL_POINT's to fill the pixels at all the corners of the window, these coordinates are as I expect them to be: (0,0), (width-1,height-1). This shows me that my way of drawing is correct and that line (and possibly triangle) will just not include the end-point. Can someone concur? – scippie Apr 06 '12 at 09:11
  • related http://stackoverflow.com/questions/5467218/opengl-2d-hud-over-3d – Ciro Santilli OurBigBook.com Apr 13 '16 at 15:03
  • related but more generic: http://stackoverflow.com/questions/7922526/opengl-deterministic-rendering-between-gpu-vendor – Ciro Santilli OurBigBook.com Jul 13 '16 at 12:50

2 Answers2

18

In OpenGL, lines are rasterized using the "Diamond Exit" rule. This is almost the same as saying that the end coordinate is exclusive, but not quite...

This is what the OpenGL spec has to say: http://www.opengl.org/documentation/specs/version1.1/glspec1.1/node47.html

Also have a look at the OpenGL FAQ, http://www.opengl.org/archives/resources/faq/technical/rasterization.htm, item "14.090 How do I obtain exact pixelization of lines?". It says "The OpenGL specification allows for a wide range of line rendering hardware, so exact pixelization may not be possible at all."

Many will argue that you should not use lines in OpenGL at all. Their behaviour is based on how ancient SGI hardware worked, not on what makes sense. (And lines with widths >1 are nearly impossible to use in a way that looks good!)

  • Thanks, this answer is very helpful! The lines are for testing purposes, not for final product coolness. So I am very happy with the GL_LINES at this point. Can I assume that triangles have the same result? I mean, if I draw a filled rectangle by drawing two triangles, can I expect the same result? – scippie Apr 06 '12 at 10:09
  • 3
    Sorry: 14.120: Filled primitives and line primitives follow different rules for rasterization. – scippie Apr 06 '12 at 19:44
  • >"And lines with widths >1 are nearly impossible to use in a way that looks good!" One can draw them as thin rectangles and then do some magic in fragment shader to make them look as line with almost any visual properties. But this may be expensive. – Display Name May 26 '13 at 08:49
7

Note that OpenGL coordinate space has no notion of integers, everything is a float and the "centre" of an OpenGL pixel is really at the 0.5,0.5 instead of its top-left corner. Therefore, if you want a 1px wide line from 0,0 to 10,10 inclusive, you really had to draw a line from 0.5,0.5 to 10.5,10.5.

This will be especially apparent if you turn on anti-aliasing, if you have anti-aliasing and you try to draw from 50,0 to 50,100 you may see a blurry 2px wide line because the line fell in-between two pixels.

Lie Ryan
  • 62,238
  • 13
  • 100
  • 144
  • 1
    I completely understand what you are talking about but that's not an issue here. By using the correct projection matrix and adding 0.375 to every pixel, both ATI and nvidia will round the numbers so that coordinates are exact pixels. How else would for example mac os draw pixel perfect windows if they weren't pixel perfect? – scippie Apr 06 '12 at 08:46
  • @scippie: You actually should add 0.5. 0.5 is the middle of the pixel. 0.375 is not. – SigTerm Apr 06 '12 at 10:04
  • 4
    @SigTerm: this is not true, you should try it out on both ATI and NVidia hardware and the nightmare begins. ATI and NVidia round differently. By using 0.375, the rounding is the same on both and the results are perfect (although it might not be correct theoretically) – scippie Apr 06 '12 at 10:12
  • 4
    @scippie: As soon as you enable anti-aliasing, 0.375 is going to cause big problems. SigTerm is right. Your problems arise not from use of 0.5, but because you're using the projection matrix which applies it to both dimensions and to both quads and lines. It should only be applied to lines, and only in the axis perpendicular to the line. – Ben Voigt May 12 '13 at 21:07