In this short lab, you’ll implement the midpoint algorithm for drawing lines.
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.
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.
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)
= b + m*x
y round(y)] = white canvas[x,
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:
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.
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:
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.
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:
Anti-alias your lines by allowing pixel values between 0 and 1 to soften the edges of the line.
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.
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.