For a project I have been working on, the ability to draw lines with a gradient (I.E. they change color over the interval they are drawn) would be very useful. I have an algorithm for this, as I will paste below, but it turns out to be DREADFULLY slow. I'm using the Bresenham algorithm to find each point, but I fear that I have reached the limits of software rendering. I've been using SDL2 thus far, and my line drawing algorithm appears 200x slower than SDL_RenderDrawLine. This is an estimate, and gathered from comparing the two functions' times to draw 10,000 lines. My function would take near 500ms, and SDL_RenderDrawLine did it in 2-3ms on my machine. I even tested the functions with horizontal lines to ensure it wasn't just a botched Bresenham algorithm, and similar slowness hatched. Unfortunately, SDL doesn't have an API for drawing lines with a gradient (or if it does, I'm blind). I knew that any software rendering would be significantly slower than hardware, but the shear magnitude of slowness caught me by surprise. Is there a method that can be used to speed this up? Have I just botched the drawing system beyond reason? I've considered saving an array of the pixels I wish to draw and then shoving them to the screen all at once, but I don't know how to do this with SDL2 and I can't seem to find the API in the wiki or documentation that allows for this. Would that even be faster?
Thanks for the consideration!
void DRW_LineGradient(SDL_Renderer* rend, SDL_Color c1, int x1, int y1, SDL_Color c2, int x2, int y2){
Uint8 tmpr, tmpg, tmpb, tmpa;
SDL_GetRenderDrawColor(rend, &tmpr, &tmpg, &tmpb, &tmpa);
int dy = y2 - y1;
int dx = x2 - x1;
/* Use doubles for a simple gradient */
double d = (abs(x1 - x2) > abs(y1 - y2) ? abs(x1 - x2) : abs(y1 - y2));
double dr = (c2.r - c1.r) / d;
double dg = (c2.g - c1.g) / d;
double db = (c2.b - c1.b) / d;
double da = (c2.a - c1.a) / d;
double r = c1.r, g = c1.g, b = c1.b, a = c1.a;
/* The line is vertical */
if (dx == 0) {
int y;
if (y2 >= y1) {
for (y = y1; y <= y2; y++) {
SDL_SetRenderDrawColor(rend, r, g, b, a);
SDL_RenderDrawPoint(rend, x1, y);
r += dr;
g += dg;
b += db;
a += da;
}
return;
}
else{
for (y = y1; y >= y2; y--) {
SDL_SetRenderDrawColor(rend, r, g, b, a);
SDL_RenderDrawPoint(rend, x1, y);
r += dr;
g += dg;
b += db;
a += da;
}
return;
}
}
/* The line is horizontal */
if (dy == 0) {
int x;
if (x2 >= x1) {
for (x = x1; x <= x2; x++) {
SDL_SetRenderDrawColor(rend, r, g, b, a);
SDL_RenderDrawPoint(rend, x, y1);
r += dr;
g += dg;
b += db;
a += da;
}
return;
}
else{
for (x = x1; x >= x2; x--) {
SDL_SetRenderDrawColor(rend, r, g, b, a);
SDL_RenderDrawPoint(rend, x, y1);
r += dr;
g += dg;
b += db;
a += da;
}
return;
}
}
/* The line has a slope of 1 or -1 */
if (abs(dy) == abs(dx)) {
int xmult = 1, ymult = 1;
if (dx < 0) {
xmult = -1;
}
if (dy < 0) {
ymult = -1;
}
int x = x1, y = y1;
do {
SDL_SetRenderDrawColor(rend, r, g, b, a);
SDL_RenderDrawPoint(rend, x, y);
x += xmult;
y += ymult;
r += dr;
g += dg;
b += db;
a += da;
} while (x != x2);
return;
}
/* Use bresenham's algorithm to render the line */
int checky = dx >> 1;
int octant = findOctant((Line){x1, y1, x2, y2, dx, dy});
dy = abs(dy);
dx = abs(dx);
x2 = abs(x2 - x1) + x1;
y2 = abs(y2 - y1) + y1;
if (octant == 1 || octant == 2 || octant == 5 || octant == 6) {
int tmp = dy;
dy = dx;
dx = tmp;
}
int x, y = 0;
for (x = 0; x <= dx; x++) {
SDL_SetRenderDrawColor(rend, r, g, b, a);
switch (octant) {
case 0:
SDL_RenderDrawPoint(rend, x + x1, y + y1);
break;
case 1:
SDL_RenderDrawPoint(rend, y + x1, x + y1);
break;
case 2:
SDL_RenderDrawPoint(rend, -y + x1, x + y1);
break;
case 3:
SDL_RenderDrawPoint(rend, -x + x1, y + y1);
break;
case 4:
SDL_RenderDrawPoint(rend, -x + x1, -y + y1);
break;
case 5:
SDL_RenderDrawPoint(rend, -y + x1, -x + y1);
break;
case 6:
SDL_RenderDrawPoint(rend, y + x1, -x + y1);
break;
case 7:
SDL_RenderDrawPoint(rend, x + x1, -y + y1);
break;
default:
break;
}
checky += dy;
if (checky >= dx) {
checky -= dx;
y++;
}
r += dr;
g += dg;
b += db;
a += da;
}
SDL_SetRenderDrawColor(rend, tmpr, tmpg, tmpb, tmpa);
}
SIDE NOTE:
I am reluctant to just move on to using OpenGL 3.0+ (Which I hear SDL2 has support for) because I don't know how to use it. Most tutorials I have found have explained the process of setting up the contexts with SDL and then coloring the screen one solid color, but then stop before explaining how to draw shapes and such. If someone could offer a good place to start learning about this, that would also be extremely helpful.
A lot of the overhead of your function is in repeated calls to SDL_RenderDrawPoint. This is (most likely) a generic function which needs to do the following operations:
check if x and y are in range for your current surface;
calculate the position inside the surface by multiplying y with surface->pitch and x with surface->format->BytesPerPixel;
check the current color model of the surface using SDL_PixelFormat;
convert the provided color to the correct format for this color model.
All of the above must be done for each single pixel. In addition, calling a function in itself is overhead -- small as it may be, it still needs to be done for each separate pixel, even if it is not visible.
You can:
omit x and y range checking if you are sure the line start and end points are always visible;
omit the convert-to-address step by calculating it once for the start of the line, then updating it by adding BytesPerPixel and pitch for a horizontal or vertical movement;
omit the convert-to-color model step by calculating the correct RGB values once (well, for a single color line, at least -- it's a bit harder for a gradient);
omit the function call by inlining the code to set a single pixel inside the line routine.
Another -- smaller -- issue: you call your own routine "Bresenham's ... but it isn't. Bresenham's optimization is actually that it avoids double calculations entirely (and its strongest point is that it still gives the mathematically correct output; something I would not count on when using double variables...).
The following routine does not check for range, color model, color values, or (indeed) if the surface should be locked. All of these operations should be ideally done outside the tight drawing loop. As it is, it assumes a 24-bit RGB color screen, with the Red byte first. [*]
I wrote this code for my current SDL environs, which is still SDL-1.0, but it should work for newer versions as well.
It is possible to use Bresenham's calculations for the delta-Red, delta-Green, and delta-Blue values as well, but I deliberately omitted them here :) They would add a lot of extra variables -- at a guess, three per color channel --, extra checks, and, not least of all, not really a visibly better quality. The difference between two successive values for Red, say 127 and 128, are usually too small to notice in a single pixel wide line. Besides, this small step would only occur if your line is at least 256 pixels long and you cover the entire range of Red from 0 to 255 in the gradient.
[*] If you are 100% sure you are targeting a specific screen model with your own program, you can use this (adjusted for that particular screen model, of course). It's feasible to target a couple of different screen models as well; write a customized routine for each, and use a function pointer to call the correct one.
Most likely this is how SDL_RenderDrawLine is able to squeeze out every millisecond of performance. Well worth writing all that code for a library (which will be used on a wide variety of screen set-ups), but most likely not for a single program such as yours. Notice I commented out a single range check, which falls back to a plain line routine if necessary. You could do the same for unusual or unexpected screen set-ups, and in that case simply call your own, slower, drawing routine. (Your routine is more robust as it uses SDL's native routines.)
The original line routine below was copied from The Internet more than a single decade ago, as I have been using it for ages. I'd gladly attribute it to someone; if anybody recognizes the comments (they are mostly as appeared in the original code), do post a comment.
void gradient_line (int x1,int y1,int x2,int y2,
int r1,int g1, int b1,
int r2,int g2, int b2)
{
int d; /* Decision variable */
int dx,dy; /* Dx and Dy values for the line */
int Eincr,NEincr; /* Decision variable increments */
int t; /* Counters etc. */
unsigned char *ScrPos;
int LineIncr;
int rd,gd,bd;
if (x1 < 0 || y1 < 0 || x2 < 0 || y2 < 0 ||
x1 >= SCREEN_WIDE || x2 >= SCREEN_WIDE ||
y1 >= SCREEN_HIGH || y2 >= SCREEN_HIGH)
{
line (x1,y1, x2,y2, (r1<<16)+(g1<<8)+b1);
return;
}
rd = (r2-r1)<<8;
gd = (g2-g1)<<8;
bd = (b2-b1)<<8;
dx = x2 - x1;
if (dx < 0)
dx = -dx;
dy = y2 - y1;
if (dy < 0)
dy = -dy;
if (dy <= dx)
{
/* We have a line with a slope between -1 and 1
*
* Ensure that we are always scan converting the line from left to
* right to ensure that we produce the same line from P1 to P0 as the
* line from P0 to P1.
*/
if (x2 < x1)
{
t = x2; x2 = x1; x1 = t; /* Swap X coordinates */
t = y2; y2 = y1; y1 = t; /* Swap Y coordinates */
/* Swap colors */
r1 = r2;
g1 = g2;
b1 = b2;
rd = -rd;
gd = -gd;
bd = -bd;
}
r1 <<= 8;
g1 <<= 8;
b1 <<= 8;
if (y2 > y1)
{
LineIncr = screen->pitch;
} else
{
LineIncr = -screen->pitch;
}
d = 2*dy - dx; /* Initial decision variable value */
Eincr = 2*dy; /* Increment to move to E pixel */
NEincr = 2*(dy - dx); /* Increment to move to NE pixel */
ScrPos = (unsigned char *)(screen->pixels+screen->pitch*y1+x1*screen->format->BytesPerPixel);
rd /= dx;
gd /= dx;
bd /= dx;
/* Draw the first point at (x1,y1) */
ScrPos[0] = r1 >> 8;
ScrPos[1] = g1 >> 8;
ScrPos[2] = b1 >> 8;
r1 += rd;
g1 += gd;
b1 += bd;
/* Incrementally determine the positions of the remaining pixels */
for (x1++; x1 <= x2; x1++)
{
if (d < 0)
{
d += Eincr; /* Choose the Eastern Pixel */
} else
{
d += NEincr; /* Choose the North Eastern Pixel */
ScrPos += LineIncr;
}
ScrPos[0] = r1>>8;
ScrPos[1] = g1>>8;
ScrPos[2] = b1>>8;
ScrPos += screen->format->BytesPerPixel;
r1 += rd;
g1 += gd;
b1 += bd;
}
} else
{
/* We have a line with a slope between -1 and 1 (ie: includes
* vertical lines). We must swap our x and y coordinates for this.
*
* Ensure that we are always scan converting the line from left to
* right to ensure that we produce the same line from P1 to P0 as the
* line from P0 to P1.
*/
if (y2 < y1)
{
t = x2; x2 = x1; x1 = t; /* Swap X coordinates */
t = y2; y2 = y1; y1 = t; /* Swap Y coordinates */
/* Swap colors */
r1 = r2;
g1 = g2;
b1 = b2;
rd = -rd;
gd = -gd;
bd = -bd;
}
r1 <<= 8;
g1 <<= 8;
b1 <<= 8;
if (x2 > x1)
{
LineIncr = screen->format->BytesPerPixel;
} else
{
LineIncr = -screen->format->BytesPerPixel;
}
d = 2*dx - dy; /* Initial decision variable value */
Eincr = 2*dx; /* Increment to move to E pixel */
NEincr = 2*(dx - dy); /* Increment to move to NE pixel */
rd /= dy;
gd /= dy;
bd /= dy;
/* Draw the first point at (x1,y1) */
ScrPos = (unsigned char *)(screen->pixels+screen->pitch*y1+x1*screen->format->BytesPerPixel);
ScrPos[0] = r1 >> 8;
ScrPos[1] = g1 >> 8;
ScrPos[2] = b1 >> 8;
r1 += rd;
g1 += gd;
b1 += bd;
/* Incrementally determine the positions of the remaining pixels
*/
for (y1++; y1 <= y2; y1++)
{
ScrPos += screen->pitch;
if (d < 0)
{
d += Eincr; /* Choose the Eastern Pixel */
} else
{
d += NEincr; /* Choose the North Eastern Pixel */
ScrPos += LineIncr; /* (or SE pixel for dx/dy < 0!) */
}
ScrPos[0] = r1 >> 8;
ScrPos[1] = g1 >> 8;
ScrPos[2] = b1 >> 8;
r1 += rd;
g1 += gd;
b1 += bd;
}
}
}
.. and this is a section of a screenful of random lines with random colors, with on the right a close-up:
I did not time the difference between "native" SDL line drawing, your naive method, a pure solid color Bresenham's implementation and this one; then again, this ought not be very much slower than an SDL native line.
Related
I've found this Generalized Bresenham's Line Drawing Algorithm and I'm having a hard time understanding what the while is doing here.
Any help is greatly appreciated.
the code:
#define sign(x) ((x > 0)? 1 : ((x < 0)? -1: 0))
x = x1;
y = y1;
dx = abs(x2 - x1);
dy = abs(y2 - y1);
s1 = sign(x2 - x1);
s2 = sign(y2 - y1);
swap = 0;
if (dy > dx) {
temp = dx;
dx = dy;
dy = temp;
swap = 1;
}
D = 2*dy - dx;
for (i = 0; i < dx; i++) {
display_pixel (x, y);
while (D >= 0) {
D = D - 2*dx;
if (swap)
x += s1;
else
y += s2;
}
D = D + 2*dy;
if (swap)
y += s2;
else
x += s1;
}
D is the scaled distance from the line to the candidate x, y coordinate.
Better: D is scaled difference of the distance from the line to the candidates x+1, y+0 and x+1, y+1.
That is, as x increments, the sign of D indicates should the closer y increment by 0 or 1?
(The role of x, y interchanges depending on which octant the algorithm is applied.)
I expected while (D >= 0) { as if (D >= 0) {. Bresenham's line algorithm
Note that OP's code is subject to overflow in abs(x2 - x1) and 2*dy - dx. Alternates exist that do not rely on wider math.
I'm trying to write out pixles on a LCD. I'm plotting them at X, Y coordinates and I'm using this code:
void SSD1306_draw_line(uint8_t x0, uint8_t x1, uint8_t y0, uint8_t y1){
uint8_t k = (x1 - x0)/(y1 - y0);
uint8_t y = y0;
for(uint8_t x = x0; x < x1; x++){
pixel(x, y, true);
y += k;
}
}
The problem where is that if k becomes a decimal number e.g 1.98, then k will still 1. If k = 2.01, then k = 2 due to the uint8_t datatype.
Assume that we are going to plot the line (0,0), (40, 20) {x,y}.
Sure, now k will be 2. That works!
But assume that if we plot the line (0,0), (35, 20) {x,y}.
Now k will be a float number of 1.75. This will not work for me.
Is there a way to find a better k?
I have tried this, but the line does not follow the coordinates.
void SSD1306_draw_line(uint8_t x0, uint8_t x1, uint8_t y0, uint8_t y1){
float k = 0;
if(y1 > x1){
k = (y1 - y0)/(x1 - x0);
}else{
k = (x1 - x0)/(y1 - y0);
}
float y = y0;
for(uint8_t x = x0; x < x1; x++){
pixel(x, (uint8_t) y, true);
y += k;
}
}
Instead of attempting FP math, research Bresenham's line algorithm for an all integer solution.
Untested code:
void SSD1306_draw_line(uint8_t x0, uint8_t x1, uint8_t y0, uint8_t y1) {
int dx = x1 - x0;
int dy = y1 - y0;
int D = 2*dy - dx;
int y = y0;
// code still needs work when x0 > x1 or |dy| > |dx|
for (int x = x0; x <= x1; x++) {
pixel(x,y,true);
if (D > 0) {
y++;
D = D - 2*dx;
}
D = D + 2*dy;
}
}
You were on the right path with changing k and y to data type float.
But in addition to that, you need to make sure that the right hand side of
k = (y1 - y0)/(x1 - x0);
will be a float value, too. Currently, since only integral values are involved, the result of the expression on the right hand side will be integral. So k will never receive any fractional part.
To "enforce" floating point division, it is sufficient that one of the operands is of type float. You can achieve this by an explicit cast. Write, for example:
k = ((float)(y1 - y0))/(x1 - x0);
In my program I am currently rendering circles using the Midpoint Circle Algorithm with the following code.
void drawcircle(int x0, int y0, int radius)
{
int x = radius-1;
int y = 0;
int dx = 1;
int dy = 1;
int err = dx - (radius << 1);
while (x >= y)
{
putpixel(x0 + x, y0 + y);
putpixel(x0 + y, y0 + x);
putpixel(x0 - y, y0 + x);
putpixel(x0 - x, y0 + y);
putpixel(x0 - x, y0 - y);
putpixel(x0 - y, y0 - x);
putpixel(x0 + y, y0 - x);
putpixel(x0 + x, y0 - y);
if (err <= 0)
{
y++;
err += dy;
dy += 2;
}
if (err > 0)
{
x--;
dx += 2;
err += (-radius << 1) + dx;
}
}
}
But my question is, is it possible to have this function work the same way, but split the circle into 4 separate sections? i.e. so rather than rendering a normal circle, it would look somewhat like this
Putting holes in circles
The implementation you had had some overlapping pixels where the 8 subsections meet. Also the circle was the wrong diameter (should always be even because radius * 2) the cause of some of the overlap.
To fix the size and remove the overlap I move half the circle out (to the right down) by one pixel and prevented half the last iteration pixels being rendered.
Also the statement if(err <= 0) is not needed as at that point err will always meet that condition. The y offset steps up 1 pixel each iteration, the err is to find when x needs to step.
Easy solution
With that the solution to your question regarding putting holes in the circle. You just need a counter to count down the number of pixels you want to skip. While that counter is > 0 don't draw any pixels.
I added it to the snippet below as a 4th argument hole. An int that is half the size of the hole at the top, bottom, left and right of the circle.
So if you want a 20 pixel hole then the last argument is 10.
See right circles in image below.
Limit and geek stuff
Note that the max size of hole is sin(PI / 4) * radius if you make it bigger, no pixels will be drawn. As a side note the number of pixel drawn (without the holes) is approx sin(PI / 4) * radius * 8 - 4 which is almost 10% less than the circumference.
Solution for answer
void drawcircle(int x0, int y0, int radius, int hole) {
int x = radius-1;
int y = 0;
int dx = 1;
int dy = 1;
int err = dx - (radius << 1);
int x1 = x0 + 1;
int y1 = x0 + 1;
while (x >= y) {
if(hole == 0){
putpixel(x1 + x, y1 + y);
putpixel(x0 - x, y0 - y);
putpixel(x0 - y, y1 + x);
putpixel(x1 + x, y0 - y);
if (x > y) {
putpixel(x1 + y, y1 + x);
putpixel(x0 - x, y1 + y);
putpixel(x0 - y, y0 - x);
putpixel(x1 + y, y0 - x);
}
} else {
hole--;
}
y++;
err += dy;
dy += 2;
if (err > 0) {
x--;
dx += 2;
err += (-radius << 1) + dx;
}
}
}
Also as I geeked out on this function as it makes some good high performance shapes like rounded cross and boxes with only very minor changes that I thought worth sharing. See image
To get other shapes...
Add back the y err test and then change the delta X, and delta y error change to values other than 2.
if(err <= 0){ // needed
y++;
err += dy;
dy += 8; << change this
}
if (err > 0) {
x--;
dx += 8; << change this
err += (-radius << 1) + dx;
}
// See image in answer
// 8 made bottom right cross,
// 18 top right
// 28 top left
// 0.8 bottom left
The image shows the shapes and the result of the holes
I'm trying to write a program that writes a digital car speedo onto an LCD screen (ST7735) with a Tiva C (Tm4C123GXL). The attached code is the draw line function that should draw a straight line between the two distances. if I put in (speed_x1, speed_y1, 80, 60, ST7735_WHITE) into the function, up until 45 degrees, the line drawn is horizontal, not angled like it should be. After 45 degrees up until 90, the drawing is fine, then after 90 it breaks again.
speed_x1 = 80 - 55 * cos((PI / 180) * (speed * 1.8))
speed_y1 = 60 - 55 * sin((PI / 180) * (speed * 1.8))
(I want the speedo to max out at 100, hence speed * 1.8 is 1.8 degress/km/hr)
Any help in solving my problem here would be greatly appreciated. Thanks :)
void ST7735_DrawLine(short x1, short y1, short x2, short y2, unsigned short color) {
// unsigned char hi = color >> 8, lo = color;
//int x=x1;
//int y=y1;
int dy = y2 - y1;
int dx = x2 - x1;
double m = dy / dx;
double c = y1 - m * x1;
if ((x1 >= _width) || (y1 >= _height) || (x2 >= _width) || (y2 >= _height) ) return;
setAddrWindow(x1, y1, x1 + x2 - 1, y2);
while(x1 <= x2)
{
if (m <= 1)
{
x1 = x1 + 1;
y1 = m * x1 + c;
ST7735_DrawPixel(x1,y1,color);
}
else
{
y1 = y1 + 1;
x1 = (y1 - c) / m;
ST7735_DrawPixel(x1,y1,color);
}
}
}
void ST7735_DrawPixel(short x, short y, unsigned short color) {
if ((x < 0) || (x >= _width) || (y < 0) || (y >= _height))
return;
setAddrWindow(x,y,x+1,y+1);
pushColor(color);
}
Typecast problem. M results in 0 when dy/dx is below one. Typecast them to floats to get a float as a result.
If that would be a bresenham line drawing algorithm note that it only works within 45 deg. (Always wonder why ppl not even have a look at WP first.)
Not sure, but as your surname has a german origin: you will also find this in german on WP.
For other angles you would have to swap/reorder coordinates. It might even be better to have seperate drawing algorithms for hor/vert and diagonal lines, if performance is an issue (but as you use double, it is obviously none at all).
Does anyone know that if there is a modified version of Bresehham's line algorithm that uses feedback control with PID filters? Basically, the algorithm is just a P-Feedback Control for the error term amplified by half. I looked at the Graphics Gems series, Abrash's book,..etc can't find any yet
Here it is: From Here. This uses the P parameter (see steps in pseudo code shown at top of link)
The provided example code was written in an old environment (references to TurboC in code) and contains functions that will have to be written by you. eg. initgraph(), putpixel(), etc. But the algorithm appears to be complete.
#include <stdio.h>
#include <conio.h>
#include <graphics.h>
#include <math.h>
#include <dos.h>
int main() {
/* request auto detection */
int gdriver = DETECT, gmode;
int x1 = 0, y1 = 0, x2, y2;
int err, x, y, dx, dy, dp, xEnd;
int twody, twodxdy;
/* initialize graphic driver */
initgraph(&gdriver, &gmode, "C:/TURBOC3/BGI");
err = graphresult();
if (err != grOk) {
/* error occurred */
printf("Graphics Error: %s\n",
grapherrormsg(err));
return 0;
}
/* max position in x and y axis */
x2 = getmaxx();
y2 = getmaxy();
/* draws line from (0, 0) to (x2, y2) */
dx = x2 - x1;
dy = y2 - y1;
twody = 2 * dy;
twodxdy = 2 * (dy - dx);
dp = twody - dx;
if (x1 > x2) {
x = x2;
y = y2;
xEnd = x1;
} else {
x = x1;
y = y1;
xEnd = x2;
}
/* put a dot at the position (x, y) */
putpixel(x, y, WHITE);
/* calculate x and y successor and plot the points */
while (x < xEnd) {
x = x + 1;
if (dp < 0) {
dp = dp + twody;
} else {
y = y + 1;
dp = dp + twodxdy;
}
/* put a dot at the given position(x, y) */
putpixel(x, y, WHITE);
/* sleep for 50 milliseconds */
delay(50);
}
getch();
/* deallocate memory allocated for graphic screen */
closegraph();
return 0;
}