[A5] al_draw_rectangle bug? rectangle is drawn by 1 pixel off.
axilmar

I am using the function al_draw_rectangle to create a rectangle from (200, 150) to (449, 349). The rectangle's width is 250 pixels, and the height is 200 pixels. Here is the drawing code:

//test draw
void draw() {
    al_draw_filled_rectangle(0, 0, 640, 480, al_map_rgb(255, 255, 255));
    al_draw_rectangle(200, 150, 200+250-1, 150+200-1, al_map_rgb(0, 0, 0), 1);
}

However, when I zoom into the picture, the rectangle starts from (199, 149) (the cursor is over the red pixel):

{"name":"Wvqcv.png","src":"\/\/djungxnpq2nug.cloudfront.net\/image\/cache\/6\/a\/6a6255b35cf08de2c944c948838070f7.png","w":1280,"h":998,"tn":"\/\/djungxnpq2nug.cloudfront.net\/image\/cache\/6\/a\/6a6255b35cf08de2c944c948838070f7"}Wvqcv.png

The A5 manual says:

Quote:

The basic rule that determines which pixels are associated with which shape is then as follows: a pixel is treated to belong to a shape if the pixel's center is located in that shape.

However, the center of pixel (199, 149) is not inside the shape. The center of this pixel is (199.5, 149.5), which is outside of the range (200.0-201.0, 150.0-151.0).

What gives? is this normal? is it an A5 bug?

If I use thickness = 0, then the rectangle still starts from the pixel (199, 149), with the exception that the pixel at that specific location is not drawn.

SiegeLord

Outlined versions need the 0.5 offsets. See the second diagram + code here: http://www.allegro.cc/manual/5/primitives.html.

axilmar

So, when I draw an outlined shape, I need to add 0.5 to all the coordinates.

What about filled shapes? I draw a filled rectangle from (200,150) to (449, 349), but the rectangle is drawn up to (448, 348):

{"name":"eupaR.png","src":"\/\/djungxnpq2nug.cloudfront.net\/image\/cache\/e\/2\/e2d2235e40e6dfff9015881b421ef495.png","w":1280,"h":998,"tn":"\/\/djungxnpq2nug.cloudfront.net\/image\/cache\/e\/2\/e2d2235e40e6dfff9015881b421ef495"}eupaR.png

So I guess at for filled shapes, I need to add the offset 0.5 to the right and bottom coordinates. Am I correct?

Elias

No, there is no offset for filled shapes. And outlined shapes also are filled - they simply specify the center and not the outer outline. So if you use a thickness of 1 that means the filled shape extends 0.5 in all directions and the offset is only to counter that - but in general there never is any extra offset.

SiegeLord

I think it is best to imagine what happens exactly instead of memorising the offsets. Study the second diagram + code in the link I gave you and it should be apparent why the output you get is what you get.

The short version is that integer pixel coordinates refer to the top-left corner of the pixel and not its centre.

axilmar
SiegeLord said:

I think it is best to imagine what happens exactly instead of memorising the offsets. Study the second diagram + code in the link I gave you and it should be apparent why the output you get is what you get.

I have studied both diagrams and read the relevant documentation page many times, but I can't seem to pinpoint the exact rules of drawing.

So, let me write down the complete set of rules of drawing:

  • if the shape is outlined, then the coordinates refer to the pixel center.

  • if the shape is not outlined, then the coordinates refer to the pixel top-left corner.

  • a pixel is drawn if its center is within the shape coordinates.

Am I correct? are the above the only rules I need to remember for drawing?

Elias

They always refer to the exact position (with 0.0/0.0 being the top-left corner of the top-most and left-most pixel). But when you draw an outlined shape, say the red line in the diagram from (2, 1) to (6, 1) then you need to take the thickness into account. It has a thickness of 2, so you really draw a rectangle from (2, 1 - 1) to (6, 1 + 1) so a rectangle from (2, 0) to (6, 2). Any any pixels who's pixel centers are within that mathematical shape will be lit.

[edit: thickness of al_draw_line only extends along the normal, fixed my numbers :P]

SiegeLord
axilmar said:

Am I correct? are the above the only rules I need to remember for drawing?

No. Understanding what pixels are drawn given the inputs you give to the functions is a two step process.

  1. Understanding how the actual polygon is formed

  2. Understanding what pixels are drawn given the actual polygon

Combining the two steps into one rule is possible, but only after you understand the two steps above.

For a filled rectangle, the actual polygon is formed exactly from the coordinates you pass to the function:

al_draw_filled_rectangle(0, 0, 100, 100, c)

will produce a rectangular polygon that spans from (0, 0) to (100, 100). Now given that the coordinate system is such that integer coordinates refer to the top left corner of the pixel, this means that pixels 0 through 99 will be shaded in a horizontal line. Pixel 100 has its center at 100.5 which is outside the polygon.

For an outlined rectangle, you have to imagine two borders being formed, the outer one and the inner one: the only pixels that will be drawn are those inside the outer border and outside the inner one. Both borders are in effect offset from the rectangle you pass by thickness/2. I.e. this call:

al_draw_rectangle(0, 0, 100, 100, c, 1)

Will have an inner border of (0.5, 0.5) to (99.5, 99.5) and an outer border of (-0.5, -0.5, 100.5, 100.5). Those 0.5's are bad because it means that the borders lie on the centers of the pixels. What happens in that case is implementation defined. To avoid that, you introduce an offset:

al_draw_rectangle(0.5, 0.5, 99.5, 99.5, c, 1)

This call will have these borders: inner: (1, 1, 99, 99), outer: (0, 0, 100, 100). Now you can see that the pixels that'll get drawn (again in a horizontal line) are pixel 0 (center at (0.5, 0.5) falls between 0 and 1) and pixel 99 (center at (99.5, 99.5) falls between 99 and 100).

So yeah... those are the "rules". Makes sure you get what would happen if your thickness was 2 instead of 1.

axilmar
SiegeLord said:

So yeah... those are the "rules".

To which rules are you referring to? to the ones you wrote or to the ones I wrote?

SiegeLord

The ones I wrote. They are in quotes because they are not at the same level of rules that the rules you wrote were.

axilmar

So, are these the complete set of rules?

  • if the shape is outlined, then the coordinates refer to the line center.

  • if the shape is not outlined, then the coordinates refer to the pixel top-left corner.

  • a pixel is drawn if its center is within the shape coordinates.

I changed the 'pixel center' to 'line center' in the first rule, in order to account for the line thickness, as per your comment.

SiegeLord

Well, the second rule is not quite right. What does it predict when you do this:

al_draw_filled_rectangle(0.25, 0.25, 10.25, 10.25) ?

axilmar
SiegeLord said:

Well, the second rule is not quite right. What does it predict when you do this:

It draws a filled rectangle from (0,0) to (9, 9).

The center of the pixel at the coordinate 0.25 is within the shape, and therefore the pixel 0 is drawn.

The center of the pixel at the coordinate 10.25 is not within the shape, and therefore the pixel 10 is not included into the shape.

How would you write the 2nd rule?

SiegeLord

Well, thing is... the coordinate system A5 uses always has integer coordinates correspond to the top-left corner of a pixel. I'd guess the second rule would be better put as coordinates refer to the shape edge, or something.

axilmar

Thanks!

Mark Oates

I came in late, but the first point I'll make is that it's very helpful to turn on sub-pixel rendering. It makes it a lot clearer to see what's going on under there. Not having sub-pixel rendering can really obfuscate what's happening, especially because it's at the unit level, and can lead to much confusion.

The second point, and the real key in my opinion, is that when drawing outlines, you're drawing with a stroke aligned to the center of the coordinates.

A filled rectangle, drawn at (3, 3) to (17, 16):
{"name":"603584","src":"\/\/djungxnpq2nug.cloudfront.net\/image\/cache\/2\/7\/27f1091587815b3ccf5c41f143a389f5.png","w":532,"h":504,"tn":"\/\/djungxnpq2nug.cloudfront.net\/image\/cache\/2\/7\/27f1091587815b3ccf5c41f143a389f5"}603584

The outline, using the same coordinates. The stroke has a pixel width of 1 and is centered on the coordinates. The is what allegro does when drawing strokes:
{"name":"603585","src":"\/\/djungxnpq2nug.cloudfront.net\/image\/cache\/3\/b\/3ba01788136cfe2eca1eebfe58072d10.png","w":532,"h":504,"tn":"\/\/djungxnpq2nug.cloudfront.net\/image\/cache\/3\/b\/3ba01788136cfe2eca1eebfe58072d10"}603585

Here is an example of a different stroke alignment, where the stroke is inside the shape (stroke with of 1.5):
{"name":"603586","src":"\/\/djungxnpq2nug.cloudfront.net\/image\/cache\/8\/6\/861866a517208fede80ea51858341e76.png","w":532,"h":504,"tn":"\/\/djungxnpq2nug.cloudfront.net\/image\/cache\/8\/6\/861866a517208fede80ea51858341e76"}603586

This is what people typically assume will happen. In this case, it is a top and left aligned stroke:
{"name":"603587","src":"\/\/djungxnpq2nug.cloudfront.net\/image\/cache\/1\/0\/10682a9252e67b554d7c981fce6b4b3a.png","w":532,"h":504,"tn":"\/\/djungxnpq2nug.cloudfront.net\/image\/cache\/1\/0\/10682a9252e67b554d7c981fce6b4b3a"}603587

Since the alignment is centered with allegro, putting a +0.5 on your coordinates (when drawing with a stroke-width of 1px) will tuck the centered stroke inside a whole pixel when plotted to the screen, assuming you have no transformations. The result is that it will behave more like a pixel-coordinate rendering system rather than a position-coordinate.

And finally, my third point - The fact that the position-coordinates are also in pixel widths can erroneously make it seem like you are plotting directly to pixel-coordinates. Don't let that confuse you.

That's the way I see it, anyway. :P

Arthur Kalliokoski

I thought the reason for all this was because particular video cards have some differences in how they render a line, and these "rules" are trying to achieve some sort of compromise that will do what you expected. ???

Elias

I thought the reason for all this was because particular video cards have some differences in how they render a line, and these "rules" are trying to achieve some sort of compromise that will do what you expected.

No, sub-pixel rendering just looks way better (and is not possible with integer-only coordinates of course). Filled shapes are always rendered exactly no matter which video card draws them (except when the outline of your shape happens to hit the exact center of a pixel).

Mark Oates
Trezker

I'm bookmarking that page. I need to go over my GUI rendering code...

Neil Roy

After quite a bit of searching I haven't found anything explaining how to enable sub-pixel rendering. Given how lines are drawn now, I would think it would be on by default.

SiegeLord

It's not supported by all cards, so enabling it by default seems tenuous. Anyway, you can look at 'ex_multisample' to see how it is done. I don't know why wiki uses the 'sub-pixel rendering' term... it'd be easier to relate to the actual feature if it referred to it as multi-sampling.

Neil Roy

Ah okay, I was thinking about multisampling, but I wasn't sure if there was some feature that I just wasn't seeing.

Arthur Kalliokoski

The term "sub pixel" refers to antialiasing (drawing screen pixels according to how close they are to where they "should" be) and multisampling is "sub texel" which refers to grabbing source texels (bitmap pixels) according to how close, right?

axilmar

I think Allegro should also offer a primitives add-on that draws pixel coordinates. It's hard to remember all these details. I had to read this thread again in order to be reminded of what I should do in each case, and perhaps I am not the only one. The primitives add-on is good for games, where it pixel coordinates do not matter, but it is not so appropriate for drawing UIs, where discrete coordinates are required. It would be quite helpful to a lot of people, I think.

Matthew Leverton

I created macros for myself: algui_draw_hline(), etc.

But I'm not sure that they really belong in Allegro.

Neil Walker

This is harder than trying to understand the IE6 box model ;)

Is all this complication due to the way opengl renders or something?

Matthew Leverton

Read Mark's article. It's supposed to clear this up.

Neil Roy

I have to agree with axilmar on this. Having some sort of toggle would be nice. Selecting what type of system you wish to use.

Although writing your own would be no big deal I guess. I may write a few of my own functions for pixel co-ordinates. I haven't used A5 that much yet, just getting started, there is a function to plot a pixel right? So I could use that and create my own for lines, circles etc.

Edgar Reynaldo

I would also like to see compatibility functions for 2D drawing. Simple wrappers for the new al_draw calls that mimic the behaviour of A4's drawing functions. Once my GUI library is ready to be ported to A5, I would have to write them anyway, as that would be much simpler than replacing all the drawing function calls.

Neil Roy

Just created a nice line drawing function. It's pretty fast if anyone wants to use it. :)

I looked up Bresenham's line algorithm online and adopted what I found. I plan on adding more to this for circles (using similar algorithm's as I find 'em).

There's nothing here specifying a particular bitmap or screen to draw on yet.

A5_PixelDraw.h...

#SelectExpand
1#ifndef _A5_PixelDraw_h_ 2#define _A5_PixelDraw_h_ 3 4extern void a5_line(int start_x, int start_y, int dest_x, int dest_y, ALLEGRO_COLOR colour); 5 6#endif

A5_PixelDraw.cpp...

#SelectExpand
1#include <cstdlib> // abs() 2#include <allegro5/allegro.h> 3#include "A5_PixelDraw.h" 4 5 6void a5_line(int start_x, int start_y, int dest_x, int dest_y, ALLEGRO_COLOR colour) 7{ 8 // Bresenham's line algorithm 9 int x0 = start_x; 10 int x1 = dest_x; 11 int y0 = start_y; 12 int y1 = dest_y; 13 int sx, sy, err, e2; 14 15 int dx = abs(x1-x0); 16 int dy = abs(y1-y0); 17 18 if(x0 < x1) sx = 1; else sx = -1; 19 if(y0 < y1) sy = 1; else sy = -1; 20 err = dx-dy; 21 22 while(x0 != x1 and y0 != y1) { 23 al_put_pixel(x0, y0, colour); 24 e2 = 2*err; 25 if(e2 > -dy) { 26 err -= dy; 27 x0 += sx; 28 } 29 if(e2 < dx) { 30 err += dx; 31 y0 += sy; 32 } 33 } 34 35 return; 36}

Mark Oates
Neil Roy said:

I haven't used A5 that much yet, just getting started, there is a function to plot a pixel right? So I could use that and create my own for lines, circles etc.

Just be sure to lock your bitmap first before drawing the pixels, then unlock it when you're done (drawing the pixels). Otherwise you'll get a really slow render.

Neil Roy

Oh yeah!! I forgot about that, thanks Mark. I'll modify my function. I had that in mind before I started on this function and got so wrapped up in making it that I forgot about locking the bitmap! ;D.

Hmmm, I don't require a bitmap in my function... I s'pose I should so I can lock it?

It runs REALLY fast, at least in a simple test I done as is.

Matthew Leverton

Either lock the target bitmap or tell the programmer to do it before calling any of the functions.

Mark Oates

You're always drawing to a bitmap. If you haven't explicitly defined a bitmap then it will be the ALLEGRO_BITMAP* you can get from al_get_backbuffer().

Either lock the target bitmap or tell the programmer to do it before calling any of the functions.

Yes, that.

Neil Roy

I overloaded the function so that you can pass a bitmap, display or nothing. If you pass a display or bitmap, it will lock it. If you pass nothing, you have to lock it. I like this. Should be fun to do up some more. ;)

deleted old code

Edit: Fixed. The above code now has both a working line and rect function, all overloaded and working properly. When I tried the functions normally, without locking any bitmaps or displays, it still drew so many lines that my screen was almost solid colour from them after only a few seconds.

axilmar

While such an add-on does not technically belong to the Allegro library itself, I think it would be very helpful if it is in the core distribution, and I don't think it would be such a great effort. A couple of days, perhaps, for a veteran Allegro developer.

SiegeLord

Why not copy the functions from Allegro 4, if the goal is to emulate the behaviour of Allegro 4... Also, there is no overloading in C.

And thirdly, anyone advocating a mode switch please provide a floating point versions of both line drawing and circle drawing functions. I literally spent months trying to get them working, and failed. Separate functions are fine as long as they take integer coordinates though.

Another thing to check is if it is any faster to draw these things using ALLEGRO_PRIM_POINTS.

EDIT: Also, all rectangular shapes (rectangles and horizontal + vertical lines) should be implemented using al_draw calls. There's way too much FUD about the current al_draw functions.

Elias

Wouldn't it be faster to just do all rendering to a memory bitmap instead? The A5 software line resterizer with width set to 0 is likely close to how A4 lines look anyway. Something like:

al_set_new_bitmap_flags(ALLEGRO_MEMORY_BITMAP);
screen = al_create_bitmap(320, 200);
al_set_target_bitmap(screen);
...
yay, i can use software rasterization for everything
...

And then at the end of each frame just draw that bitmap. It means there wouldn't be any locking or texture up and down loading. You'd even have direct pixel access to the screen, just like in A4.

Neil Roy

I just created those on the fly as I read this topic actually. I have no desire to redo all the Allegro 4 functions. I'll probably create a few useful functions for myself. Using C++ for a change because I do like overloading for this type of thing.

I may just rename them so they can be interchanged with A4 functions. Would make porting some of my own stuff easier. I may try my hand at wrapping them around the A5 line() functions etc... <shrug> gives me something to do. ;)

Edit: after more thinking, I may just redo these in C and leave the locking etc... up to the user. Would make my life easier. ;)

Okay, I redone them in 'C'. There are 3 functions that all take a bitmap perimeter, they all lock the bitmap and draw very fast. For example, I tested the a5_rectfill() from 10,10 - 790-590 and it was instant, at least on my machine. To draw to the display and not a bitmap, you simply get the bitmap from the backbuffer with ALLEGRO_BITMAP *bitmap = al_get_backbuffer(display);. This seems to be a nice compromise and matches the Allegro 4 functions. I don't think I have the skill to duplicate Allegro 4 functions, but I can make some simple drawing functions to plot pixels like A4 did.

A5_PixeDraw.h#SelectExpand
1#ifndef _A5_PixelDraw_h_ 2#define _A5_PixelDraw_h_ 3 4#ifdef __cplusplus 5 extern "C" { 6#endif 7 8extern void a5_line(ALLEGRO_BITMAP *bitmap, int start_x, int start_y, int dest_x, int dest_y, ALLEGRO_COLOR colour); 9extern void a5_rect(ALLEGRO_BITMAP *bitmap, int start_x, int start_y, int dest_x, int dest_y, ALLEGRO_COLOR colour); 10extern void a5_rectfill(ALLEGRO_BITMAP *bitmap, int start_x, int start_y, int dest_x, int dest_y, ALLEGRO_COLOR colour); 11 12#ifdef __cplusplus 13 } 14#endif 15 16#endif /*_A5_PixelDraw_h_ */

A5_PixeDraw.c#SelectExpand
1#include <stdlib.h> /* abs() */ 2#include <allegro5/allegro.h> 3#include "A5_PixelDraw.h" 4 5 6void a5_line(ALLEGRO_BITMAP *bitmap, int start_x, int start_y, int dest_x, int dest_y, ALLEGRO_COLOR colour) 7{ 8 /* Bresenham's line algorithm */ 9 int x0 = start_x; 10 int x1 = dest_x; 11 int y0 = start_y; 12 int y1 = dest_y; 13 int sx, sy, err, e2; 14 int dx = abs(x1-x0); 15 int dy = abs(y1-y0); 16 int x,y; 17 18 if(x0 < x1) sx = 1; else sx = -1; 19 if(y0 < y1) sy = 1; else sy = -1; 20 err = dx-dy; 21 22 al_lock_bitmap(bitmap, al_get_bitmap_format(bitmap), ALLEGRO_LOCK_WRITEONLY); 23 al_set_target_bitmap(bitmap); 24 25 if (x0 == x1) { 26 for (y=y0; y!=y1; y+=sy) al_put_pixel(x0, y, colour); 27 al_put_pixel(x1, y1, colour); 28 al_unlock_bitmap(bitmap); 29 return; 30 } 31 if (y0 == y1) { 32 for (x=x0; x!=x1; x+=sx) al_put_pixel(x, y0, colour); 33 al_put_pixel(x1, y1, colour); 34 al_unlock_bitmap(bitmap); 35 return; 36 } 37 38 while(x0 != x1 && y0 != y1) { 39 al_put_pixel(x0, y0, colour); 40 e2 = 2*err; 41 if(e2 > -dy) { 42 err -= dy; 43 x0 += sx; 44 } 45 if(e2 < dx) { 46 err += dx; 47 y0 += sy; 48 } 49 } 50 al_unlock_bitmap(bitmap); 51 52 return; 53} 54 55/***************************************/ 56 57void a5_rect(ALLEGRO_BITMAP *bitmap, int start_x, int start_y, int dest_x, int dest_y, ALLEGRO_COLOR colour) 58{ 59 if (start_x==dest_x || start_y==dest_y) { 60 a5_line(bitmap, start_x, start_y, dest_x, dest_y, colour); 61 return; 62 } 63 64 int sx, sy, dx, dy; 65 if (start_x < dest_x) { 66 sx = start_x; 67 dx = dest_x; 68 } 69 else { 70 sx = dest_x; 71 dx = start_x; 72 } 73 74 if (start_y < dest_y) { 75 sy = start_y; 76 dy = dest_y; 77 } 78 else { 79 sy = dest_y; 80 dy = start_y; 81 } 82 83 a5_line(bitmap, sx, sy, dx, sy, colour); 84 a5_line(bitmap, dx, sy, dx, dy, colour); 85 a5_line(bitmap, dx, dy, sx, dy, colour); 86 a5_line(bitmap, sx, dy, sx, sy, colour); 87 88 return; 89} 90 91/***************************************/ 92 93void a5_rectfill(ALLEGRO_BITMAP *bitmap, int start_x, int start_y, int dest_x, int dest_y, ALLEGRO_COLOR colour) 94{ 95 int temp, y; 96 97 /* if start x is greater than destination x, swap them. */ 98 if (start_x > dest_x) { 99 temp = start_x; 100 start_x = dest_x; 101 dest_x = temp; 102 } 103 104 /* if start y is greater than destination y, swap them. */ 105 if (start_y > dest_y) { 106 temp = start_y; 107 start_y = dest_y; 108 dest_y = temp; 109 } 110 111 if (start_x==dest_x || start_y==dest_y) { 112 a5_line(bitmap, start_x, start_y, dest_x, dest_y, colour); 113 return; 114 } 115 116 for (y=start_y; y<dest_y; y++) { 117 a5_line(bitmap, start_x, y, dest_x, y, colour); 118 } 119 120 return; 121}

Thread #606662. Printed from Allegro.cc