Skip to content

Commit 7991350

Browse files
committed
fix(display): rewrite line routine to use implicit function
The old approach was using distance based error, the implicit function approach yields slightly different lines which are more agreeable with the WIP triangle branch.
1 parent 0ea03f7 commit 7991350

File tree

3 files changed

+90
-76
lines changed

3 files changed

+90
-76
lines changed

display/rendering/canvas.c

Lines changed: 40 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -127,88 +127,54 @@ DEFN_RENDER(circle)
127127

128128
DEFN_RENDER(line)
129129
{
130-
const struct color color = line->c;
131-
132-
// I think this is essentially Bresenham's line algorithm, as in, that
133-
// is where the ideas came from, but with the caveat that I didn't want
134-
// a separate vertical and horizontal drawing function.
135-
//
136-
// ## Terms
137-
//
138-
// Since this is octant-agnostic, we generalize the notion of a step
139-
// between pixels.
130+
// This approach is guided by https://zingl.github.io/Bresenham.pdf
131+
// (A Rasterizing Algorithm for Drawing Curves, by Alois Zingl).
132+
// The math is based on the implicit function for a line,
140133
//
141-
// Considering the vector (dx, dy), one component will have greater or
142-
// equal magnitude than the other, so at every iteration, the pixel
143-
// position will be incremented along that component. I refer to this
144-
// increment vector as the step.
134+
// > f(x, y) = (y - y0)(x1 - x0) - (x - x0)(y1 - y0)
145135
//
146-
// The pixel position may also be incremented along the component with
147-
// lesser magnitude, but this happens a fraction of the time (a fraction
148-
// in the range [0, 1]). I refer to this as the lesser step.
136+
// which for all points on the line equals zero. Since the slope of a
137+
// line remains constant, we can test the "error" (evaluating the
138+
// function) at the next possible points and use a cheap comparison to
139+
// decide which to move to with the least error.
149140
//
150-
// ## Process for deriving
141+
// For lines, the errors for directly adjacent pixels can be expressed
142+
// in terms of the error for the diagonal pixel and the total distances
143+
// `dy` and `dx`. This means we only need one error variable.
151144
//
152-
// 1. Determine whether to step in the lesser direction.
153-
// - Given the two possible future points (either cur + step or cur +
154-
// step + lstep), and the point on the line which lies between
155-
// them, which future point is closer to the line? This calculation
156-
// is made with real numbers, approximated with floats.
157-
// 2. Scale the two distances such that the comparison is still
158-
// meaningful, while eliminating floating point division. Now, we can
159-
// use integers!
160-
// 3. Avoid recomputing the distances from scratch each iteration.
161-
// - Determine the initial value, and the difference between the
162-
// (i+1)th and ith values for each branch.
163-
164-
const int32_t dx = (int32_t)line->x1 - (int32_t)line->x0;
165-
const int32_t dy = (int32_t)line->y1 - (int32_t)line->y0;
166-
167-
const uint16_t steps = (uint16_t)max(abs(dx), abs(dy));
168-
169-
if (steps == 0) {
170-
draw_point(c, line->x0, line->y0, color);
171-
return;
172-
}
173-
174-
// step_x and step_y are both in the range [-1, 1], and
175-
// at least one is -1 or 1
176-
const int32_t step_x = dx / steps;
177-
const int32_t step_y = dy / steps;
145+
// The next optimization is to track the initial value and difference
146+
// per iteration, rather than recomputing the error each time.
178147

179-
const int32_t lstep_x = step_x == 0 ? (dx != 0 ? dx / abs(dx) : 0) : 0;
180-
const int32_t lstep_y = step_y == 0 ? (dy != 0 ? dy / abs(dy) : 0) : 0;
148+
const int32_t dx = abs(line->x1 - line->x0);
149+
const int32_t dy = abs(line->y1 - line->y0);
150+
const int32_t sx = line->x0 < line->x1 ? 1 : -1;
151+
const int32_t sy = line->y0 < line->y1 ? 1 : -1;
181152

182-
uint16_t x = line->x0;
183-
uint16_t y = line->y0;
153+
int32_t x = line->x0;
154+
int32_t y = line->y0;
184155

185-
int32_t d0_x = steps * step_x;
186-
int32_t d0_y = steps * step_y;
187-
int32_t d1_x = -steps * (step_x + lstep_x);
188-
int32_t d1_y = -steps * (step_y + lstep_y);
189-
190-
for (int32_t i = 0; i <= steps; i++) {
191-
draw_point(c, x, y, color);
192-
193-
const int32_t px = abs(d0_x) - abs(d1_x);
194-
const int32_t py = abs(d0_y) - abs(d1_y);
195-
196-
x += step_x;
197-
d0_x += steps * step_x - dx;
198-
d1_x -= steps * step_x - dx;
199-
if (px >= 0) {
200-
x += lstep_x;
201-
d0_x += steps * lstep_x;
202-
d1_x -= steps * lstep_x;
156+
// We track the error for the next diagonal pixel (x + sx, y + sy).
157+
//
158+
// This relies on the assumption that the slope is positive, but we took
159+
// the absolute value for `dy` and `dx`, and this lie is self contained
160+
// in the mathy state and accounted for by `sx` and `sy`.
161+
int32_t e = dx - dy;
162+
163+
while (true) {
164+
draw_point(c, x, y, line->c);
165+
166+
// Check if we reached the other end.
167+
if (x == line->x1 && y == line->y1)
168+
break;
169+
170+
const int32_t test = 2 * e;
171+
if (test > -dy) {
172+
x += sx;
173+
e -= dy;
203174
}
204-
205-
y += step_y;
206-
d0_y += steps * step_y - dy;
207-
d1_y -= steps * step_y - dy;
208-
if (py >= 0) {
209-
y += lstep_y;
210-
d0_y += steps * lstep_y;
211-
d1_y -= steps * lstep_y;
175+
if (test < dx) {
176+
y += sy;
177+
e += dx;
212178
}
213179
}
214180
}

display/testing.c

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ static void run_these_tests(
3333
static void test_rects(struct canvas *c);
3434
static void test_circles(struct canvas *c);
3535
static void test_lines_burst(struct canvas *c);
36+
static void test_lines_array(struct canvas *c);
3637
static void test_copy_rect(struct canvas *c);
3738
static void test_bezier2(struct canvas *c);
3839

@@ -60,6 +61,13 @@ void run_tests(const char *dump_dir)
6061
.width = 32,
6162
.height = 32,
6263
},
64+
{
65+
.fill_color = BG,
66+
.draw_fn = test_lines_array,
67+
.output_path = "lines-array.data",
68+
.width = 128,
69+
.height = 128,
70+
},
6371
{
6472
.fill_color = BG,
6573
.draw_fn = test_copy_rect,
@@ -229,6 +237,45 @@ static void test_lines_burst(struct canvas *c)
229237
}
230238
}
231239

240+
static void test_lines_array(struct canvas *c)
241+
{
242+
const int32_t cols = 7;
243+
const int32_t rows = 13;
244+
const int32_t gap = 1;
245+
246+
const int32_t col_len = c->width / cols;
247+
const int32_t row_len = c->height / rows;
248+
249+
for (int32_t col = 0; col < cols; col++) {
250+
for (int32_t row = 0; row < rows; row++) {
251+
int32_t col_start = col * c->width / cols;
252+
int32_t row_start = row * c->height / rows;
253+
int32_t cx = col_start + col_len / 2;
254+
int32_t cy = row_start + row_len / 2;
255+
256+
float p = (float)(cols * row + col) /
257+
((float)cols * (float)rows);
258+
259+
rendering_draw_line(c,
260+
&(struct line) {
261+
.x0 = cx +
262+
roundf(cosf(p * TAU) *
263+
((float)col_len / 2 - gap)),
264+
.y0 = cy +
265+
roundf(sinf(p * TAU) *
266+
((float)row_len / 2 - gap)),
267+
.x1 = cx +
268+
roundf(cosf(p * TAU + PI) *
269+
((float)col_len / 2 - gap)),
270+
.y1 = cy +
271+
roundf(sinf(p * TAU + PI) *
272+
((float)row_len / 2 - gap)),
273+
.c = FG,
274+
});
275+
}
276+
}
277+
}
278+
232279
static void test_copy_rect(struct canvas *c)
233280
{
234281
// Fill the screen with something.
@@ -308,8 +355,8 @@ static void test_bezier2(struct canvas *c)
308355

309356
for (int32_t col = 0; col < cols; col++) {
310357
for (int32_t row = 0; row < rows; row++) {
311-
int32_t col_start = col * col_len;
312-
int32_t row_start = row * row_len;
358+
int32_t col_start = col * c->width / cols;
359+
int32_t row_start = row * c->height / rows;
313360
int32_t dx = col * col_len / cols;
314361
int32_t dy = row * row_len / rows;
315362

display/tests/expect-lines-array.data

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)