CSCI 480 / 580: Line Drawing Lab

In this short lab, you’ll implement the midpoint algorithm for drawing lines.

Getting Started

Collaboration: You should write your own code, but you may collaborate freely with your classmates on this lab. If you’ve finished tasks 1-3, feel free to look around for anyone else who needs help; vice versa, if you’re stuck, check in with your neighbors to see if they have any helpful suggestions.

Coordinate System: Before saving the image, I’ve flipped the i dimension and transposed the image. The result of this is to give us a familiarly-oriented coordinate system: indexing into canvas[x,y] gives us the pixel at \((x, y)\), where (1, 1) is the bottom-leftmost pixel, \(x\) points right, and \(y\) points up.

Grading: This lab is worth 10 points. Grading is pretty chill: if your task 1 output visually matches mine, you’ll get 9 points. If your task 3 output matches mine, you’ll get 10. If you finish both tasks during the lab, please flag me down and I’ll assign your grade right away.

Enhancements: There are plenty of ways to extend this lab if you have extra time. These are optional, and listed as further tasks after 1-3.

Submission: I anticipate that you’ll be able to finish this during class time, but you have until Thursday, 11/14 at 10pm to push the changes to your A0 repository to Github.

Skeleton

This lab involves making updates to the Raster module from long ago in your A0 code base. We’ll re-use the repository from A0, so just paste this additional skeleton code into Raster.jl. It includes two methods of the draw_line function and two test functions, one to test each method.

Tasks

1. Basic Line Drawing

The first method of the draw_line function takes a canvas and two points, p1 and p2 each containing the \((x,y)\) coordinates of an endpoint of the line segment to be drawn. Implment this function following this pseudocode:

for x = ceil(x0) to floor(x1)
    y = b + m*x
    canvas[x, round(y)] = white

You’ll need to calculate \(b\) and \(m\) based on p1 and p2. Recall that the slope of a line is rise over run: change in y divided by change in x. Given \(m\), you can rearrange \(y = mx + b\) to say instead \(b = y - mx\), then plug in any known value of \(x,y\) that lie on the line to find \(b\).

You can test visually by running Raster.task1test() at the REPL and opening out the t1out.png image to see the result. It should looke like this:

2. Faster Line Drawing

Assuming you computed \(b\) and \(m\) before entering the loop, the above algorithm computes \(b + mx\) and rounds the result each iteration of the loop. That’s one each of floating-point addition, multiplication, and rounding. Let’s see if we can move some stuff outside the loop.

Start by observing that if we’ve just computed \(y_i = b + mx_i\), then the value we need in the next iteration is \[ \begin{align*} y_{i+1}&= b + m x_{i+1}\\ &= b + m (x_i + 1)\\ &= b + mx_i + m\\ &= y_i + m \end{align*} \] So all we really need to do is one floating-point addition to add the slope to the current \(y\), then round it to decide where to draw. Modify your draw_line function to take advantage of this precomputation.

Check to make sure that your code still passes the task 1 test.

3. Interpolation

Often, the job of a rasterizer is not only to decide which pixels to draw, but also to interpolate values that are known only at the vertices to provide values at each pixel across the primitive. For lines, this is done using simple linear interpolation. Given endpoint values (here we’ll use colors) v1 and v2, we wish to find an interpolated v at each pixel along the line.

Here’s the thing, though: if you replaced v1 and v2 in the above sentence with y1 and y2, the \(y\) coordinates of the start and end point, you’d be describing exactly what we just did - we found values of \(y\) that varied linearly from the starting to ending value over the range of \(x\) from x1 to x2. Interpolating other values is no different than interpolating the positions!

Implement the color-interpolating draw_line; start by copying in your existing draw_line implementation. Then, modify it to set the color of the pixels along the line to interpolated color values ranging between v1 and v2.

We’re actually interpolating 3 values (red, green, and blue), but we can do the math for all three together because the arithmetic is the same for each element. In Julia, elementwise operations on array-like objects (including RGB{Float32}s) can be done by adding a dot (.) before the operator. For example, if I want to multiply two colors c1 and c2 together, I can write c1 .* c2.

You should be able to run Raster.task3test() and see an output like the following:

Optional Extra Fun

4. Fasterer Line Drawing

We can go further and eliminate the somewhat expensive rounding operation in favor of a simple sign check by incrementally keeping track of the distance from an integer \(y\) coordinate to the line. With the slope-intercept form, this looks like: \[ d = m (x+1) + b - y \] That is, \(d\) is the distance from the current integer-valued \(y\) to the value of the line at \(x+1\). If \(d > 0.5\), then \(y\) should be incremented and we should draw one pixel up in the current column. This approach can be done the slow way, by computing \(d\) fresh every iteration, or incrementally by adding \(m\) to \(d\), updating \(y\) as needed, and subtracting 1 from \(d\) if y is changed. To (likely?) speed things up even further, it may be fruitful to keep track of \(d - 0.5\) instead of \(d\) itself, because then you’ll be comparing \(d\) to zero, which should replace the more expensive comparison with a sign bit check. Feel free to use GFlops.jl and/or @time to help you benchmark.

5. Other slopes

Our current line drawing algorithm can only draw lines with slopes between 0 and 1 (\(0 < m < 1\)). We can make it work for other slopes by doing analogous reasoning for the other three cases:

6. Anti-aliased lines

Anti-alias your lines by allowing pixel values between 0 and 1 to soften the edges of the line.

7. Triangle Interpolation

Extend your A0 triangle rasterization solution to enable barycentric interpolation of colors across triangles, analogous to Task 3 above. Focus on using the same efficiency tricks as we did in lines, but on barycentric coordinates.

8. Wireframe rendering / A full rasterizer

Plug your line drawing code into the wire.jl demo from Lecture 17 to remove the dependency on Luxor.jl. Alternatively/additionally, plug your triangle rasterizing code into wire.jl to make it into a non-wireframe renderer. You’ll probably need a z buffer.