1. Anders Ruud
  2. love
  3. Issues


Issue #725 wontfix

Solving the pixel-perfect line issue: split line drawing functions

Jason McKesson
created an issue

There are two conflicting issues with line drawing in Love2D. These come from two different groups who want to use them:

  1. One group wants line drawing to be pixel perfect. To draw a horizontal line, they want to say love.graphics.line(40, 50, 80, 50) And if the line-width is one, they want a line to appear on the screen exactly 40 pixels from the top, and that line should be exactly one pixel in width.

    This would also include rectangle drawing. The command love.graphics.rectangle("line", 40, 40, 20, 20) with a line-width of 1 should draw a rectangular outline that is exactly 20 pixels tall. The pixels on the half-open range [40, 60) should be set; pixel 60 should not be set.

  2. The second group wants non-integer transforms to properly affect line drawing. They want to zoom in, scale around, rotate, translate, and have everything look "correct", all on non-integer transforms.

In older versions of Love2D, Group 1 were the ones who (more or less) got what they wanted. The resolution to bug #153 effectively gave group 2 what they wanted at the expense of group 1. And recent WONTFIX's like #365 suggest that the Love2D developers have sided with group 2 for the near future.

It seems obvious to me that neither side can both get what they want from the same functions. So... why not give the users the choice?

Leave the current functions as they are, but add pixelRectangle, pixelLine, and so forth functions. These functions will:

  1. Use the OpenGL-provided line-width and line rendering code.

  2. Perform any +0.5 offsetting that is needed to ensure that lines with a thickness 1 are properly respected.

  3. For pixelRectangle, perform any rectangle size shrinkage needed to ensure that pixels outside of the rectangle are not filled in (if the line-width is 1. If it's larger, all bets are off). The other pixel functions should not attempt to perform such shrinkage.

This way, the behavior of every function is well-understood. Each group can get at the functionality they need.

Comments (7)

  1. Jason McKesson reporter

    Scaling shouldn't be a problem because, since you asked for pixel-perfect line drawing, you shouldn't be doing any scaling at all. Once you scale, rotate, sheer, or apply non-integer translation, you've lost the ability to get reasonable results from pixel-perfect line drawing.

    Alternatively, one could get something not entirely unlike that ability back easily enough by the use of a vertex shader that does the pixel clamping/offset post-transform. It's not particularly hard. It would look something like this:

    void main()
      vec4 postTransform = gl_ModelViewProjection * gl_Vertex;
      vec2 windowCoord = postTransform.xy * viewport.xy + viewport.zw;
      windowCoord = floor(windowCoord) + 0.5;
      postTransform.xy = windowCoord * viewportInv.xy + viewport.zw;

    viewport is a vec4 uniform computed from the 4 parameters passed to glViewport. The .xy and .zw components are values to multiply and add a NDC-space coordinate to that compute a window-space coordinate. It would be computed like this (in C++ code, using the GLM math library):

    vec4 GetViewportFromParams(int x, int y, int width, int height)
      glm::vec4 viewport;
      viewport.x = width / 2.0f;
      viewport.y = height / 2.0f;
      viewport.z = viewport.x + x;
      viewport.w = viewport.y + y;
      return viewport;

    The viewportInv are similar parameters, but they go backwards, from window space to NDC space. They're the "inverse" of the viewport transform. They would be computed like this:

    vec4 GetViewportInvFromParams(int x, int y, int width, int height)
      glm::vec4 viewportInv;
      viewport.x = 2.0f / width;
      viewport.y = 2.0f / height;
      viewport.z = viewport.x * x + 1.0f;
      viewport.w = viewport.y * y + 1.0f;
      return viewport;

    So no, it's not particularly tricky. Now granted, it guarantees precisely nothing about being pixel perfect when you start scaling, but it should do reasonable things.

  2. hryx

    I had the recent misfortune of learning this distinction (#595). +0.5px adjusting is a real pain (not to mention not obvious) and I'd really love a second set of functions dedicated to pixel-perfect lines. +1

  3. Alex Szpakowski

    It's worth noting that, in OpenGL 3.2 core profile in OS X (which LÖVE doesn't support right now, but it might some day), wide lines via GL_LINES and friends are not supported - the maximum line width is 1.

  4. Alex Szpakowski

    Wide lines via explicit line primitives (the equivalent of GL_LINES) range from optional to unsupported in the new graphics APIs (Metal, DX12, Vulkan). The implementations of those lines also tend to be not extremely well defined, especially when it comes to line corners.

    I'm not really excited about adding support for something that's actually deprecated / not fully supported in modern graphics APIs, so I'm marking this as wontfix for now.

  5. Log in to comment