Winter 2021
This assignment’s purpose is threefold. You will (1) reacquaint yourself with and apply some basic linear algebra concepts that we’ll be using heavily in this course; (2) get some basic familiarity with the Julia programming language; and (3) draw traingles!
Your goal in this assignment is to write a function to rasterize a triangle: in other words, take an ideal triangle repesented analytically as three vertices and draw it into a raster image by turning on the correct pixels.
Julia is installed on Linux in all of the CS the labs. You can you can also download and install Julia on your own computer from https://julialang.org/downloads/. The assignments in this course are tested using Julia 1.5.
To use the CS environment remotely, you can ssh
into a lab machine (e.g. using labs-last) and run the julia
REPL in a terminal. Viewing images remotely is inconvenient - if you’re using bare SSH, I recommend setting up an rsync
command to make it easy to copy result files back to your local machine for inspection.
An alternative is to use an editor like VS Code that can give you the illusion of working locally on files on a remote host. Install VS Code and the Remote-SSH extension. I also recommend installing the Julia extension, which turns the editor into a pretty full-featured IDE. Once installed, you can find instructions for setting up the remote connection here.
Important: when connecting remotely, do not connect to the Linux CoW (i.e., linux-??.cs.wwu.edu
). These machines can’t handle the resource-intensive VS Code server, especially when multiple people try to run it. The VS Code remote plugin doesn’t make it through labs.cs
load balancer, so you’ll need to pick a specific hostname to connect to. You can find a list of valid hostnames here (scroll down a bit to find a table titled “CSCI Lab Hosts”). You can also manually ssh to labs.cs
, see which machine it sends you to, and connect to that from with in VS Code.
Clone your repository and do the following:
$ cd path/to/repo
$ julia
At the Julia prompt, press ]
to switch to the pkg
prompt:
julia> ]
(@v1.5) pkg>
Activate the WWURaster project, whose Project.toml
config file is found in the current directory (.
).
(@v1.5) pkg> activate .
Activating environment at `~/path/to/repo/WWURaster/Project.toml`
(WWURaster) pkg>
Notice the prompt changed to reflect the current environment. The instantiate
command now installs the package dependencies for this project:
(WWURaster) pkg> instantiate
Cloning default registries ...
If you get an error from instantiate
, make sure that you’re in the correct directory; pwd()
can tell you where you are. You should be in the WWURaster
directory that contains Project.toml
(not the src
directory).
Now press backspace to get out of the pkg
prompt back to the julia
prompt. We’ll now make the code in the WWURaster
module accessible for use at the REPL:
(WWURaster) pkg> [backspace]
julia> using WWURaster
[ Info: Precompiling WWURaster (...) - this might take a moment
julia>
At this point, the project should be all set up. To verify that it is, run the following:
julia> hello_vec3(1, 2, 3)
Hello, [1.0, 2.0, 3.0]
Unfortunately, Julia does not have the built-in ability to notice that your code has changed and load the latest version. Fortunately, there is a package that called Revise.jl
that helps with this. To set this up, fire up a new Julia REPL (I recommend doing this without activating the WWURaster
environment so it’s installed globally) and add the Revise
package:
$ julia
...
julia> ]
(@v1.5) pkg> add Revise
To use it, simply run using Revise
at the Julia prompt before running using WWURaster
. Then, you can make repeated calls to functions inside the WWURaster
module and your changes will be noticed. It’s important to make sure that you run using Revise
before running using WWURaster
, as shown in the steps below.
If you wish to set Julia up to automatically using Revise
every time you start Julia, you can add using Revise
to your startup.jl
file (see here for help locating it).
To fire up a REPL and begin work on the project, here’s what you need to do:
$ cd path/to/repo/WWURaster
$ julia --project
julia> using Revise
julia> using WWURaster
[Info: Precompiling WWURaster [...]
# now you can call your code at will:
julia> hello_vec3(1, 2, 3)
The --project
flag tells Julia to look in the current directory for a Project.toml
file; when found, this automatically activates the project’s environment. This works in place of running ]activate .
after starting julia; either one works.
The following is a very barebones tour of some Julia features and concepts that will be useful for this project. Many more comprehensive resources are available. Some that I’ve found useful include the following, sorted roughly in decreasing order of comprehensiveness:
You can also use documentation directly at the REPL by typing ?
, which switches you to a help search prompt. For example:
julia> ?
help?> min
search: min minmax minimum minimum! argmin Main typemin findmin findmin!
min(x, y, ...)
Return the minimum of the arguments. See also the minimum function to take the
minimum element from a collection.
Examples
≡≡≡≡≡≡≡≡≡≡
julia> min(2, 5, 1)
1
One more bit of REPL prompt magic: a semicolon converts the prompt into a shell prompt:
julia> ;
shell> ls
Manifest.toml Project.toml src
julia>
Julia natively supports Multidimensional Arrays. An example of this is canv
in the test_tri
function; this is a ch
-by-cw
2D array of a special color type (RGB{Float32}
, which represents a color in RGB format with 3 single-precision floats). Arrays such as canv
support a bunch of really nice functionality that most other languages don’t have built in:
canvas
as canvas[i,j]
to get the pixel at the i
th row and j
th column (both starting at (1,1) in the top left) of the image. The first element of a Vec3
called x
would be accessed using x[1]
. See here for full details.for val in A
), or over each of its indices (for i in eachindex(A)
). The eachindex thing even works for multi-dimensional arrays! Details here.Vec2
and Vec3
At the top of the skeleton code, I created a couple helpful common definitions we’ll use throughout the class. These will grow into a separate module in later projects, but for now we simply define types Vec2
and Vec3
, representing floating-point 2- and 3-vectors. These are aliases for SArray
s, or static arrays, which are mainly different from your standard Julia Multidmensional arrays in that they have fixed size; everything above still applies to them. You can create them (v = Vec2(3, 4)
), index into them (v[1] => 3
), and do elementwise arithmetic on them (v + v => 6, 8
).
Assignment and unpacking - convenient stuff works with tuples and arrays, including Vec2
and Vec3
:
a = 3, 4
b = Vec2(3, 4)
x, y = b
You can define functions in (at least) the following two ways:
The usual way, for longer functions:
The quick way, for short functions:
f(x) = x + h
This defines a function! I sometimes use this to make helpers inside larger functions. Notice that helpers defined inside a function have access to the outer function’s local variables; in this example, h
is imagined to defined in the enclosing scope, so the function can access it.
Some potentially useful builtin functions:
size
, min
, max
, Int
Some potentially useful functions from LinearAlgebra
(comes standard installed; just add using LinearAlgebra
to bring these into scope):
dot
, cross
Here’s one way to decide whether a point lies inside a triangle. Suppose a triangle is defined by three vertices, specified in counter-clockwise order (we will follow this convention in this class), a, b, c. Given a primitive routine that tells us whether a point lies to the left of a line defined by two points (cf. HW0 #3), we can simply use this three times. Specifically, p lies inside the triangle abc if p is to the left of all three lines ab, bc, and ca. Here’s an illustration of this - p1 is to the left of each vector and lies inside the triangle, while p2 is to the left of ab and bc but not ca, thus lies outside the triangle.
Multidimensional arrays in Julia are indexed as you would a matrix: row index (i
) first, column index (j
) second, with the top-left entry being (1, 1). If you treated these as (x, y) coordinates in a Cartesian plane, we’d be in an odd situation where (0, 0) would be above and to the left of the top left pixel, the x axis would point down, and the y axis would point right. To get things looking like our familiar friendly xy plane, we’ll need to convert from i, j to x, y. If we want the origin in the bottom left corner, this is almost as simple as x = j, y = h − i where h is the height of the array. Two related wrinkles remain: first, the discrepancy between 1-indexing and 0-indexing. Second, if we imagine our array (image) as a grid of square pixels, we’d ideally like integer array coordinates (e.g., (h, 1) to correspond to the center of a pixel’s extent, which lives half a grid cell offset from the integer x, y locations (e.g., (0.5, 0.5)). The figure below shows a pixel grid with both coordinate systems ovelaid.
When writing my code, I worked out formulas to convert from i to y and from j to x, wrapped each in a function, and then used that anytime I had array indices that needed to be converted to (x, y) coordinates. If you’re debugging, it’s always a good idea to check that you properly convert i, j for x, y when necessary, noticing in particular that the order of the coordinates flips (i, the first index relates to y, the second coordinate and similarly for x and j).
Create your assignment repository by accepting the invitation via the Github Classroom link provided in the A0 Assignment on Canvas, then clone a local copy.
When all is said and done, there’s not a ton of code to write, though you’ll want to have completed HW0 and thoroughly reviewed the Math section from above before you get started. I’ve broken the triangle drawing task down into three functions, each of which can use the previous.
line_side
returns positive, zero, or negative if a point p
is left, on, or right of a line specified by two points (Problem 3 from HW0 will be helpful here).point_in_triangle
returns whether a point p
is in a given triangle.draw_tri
takes an image and fills points inside a given triangle in with a given color.This is a small assignment, so I kept the project layout simple:
.
├── Manifest.toml
├── Project.toml
├── src
│ └── WWURaster.jl
As described above, navigate to the WWURaster
directory and start the Julia REPL and run using Revise
and using WWURaster
. At this point, you can test your functions interactively (e.g., line_side(Vec2(0, 0), Vec2(0, 10), Vec2(4, 4))
should return a negative number). The Vec
s and primary functions are export
ed at the top of the file, meaning you don’t need to qualify them with WWURaster.
to call them once you’ve run using WWURaster
. To create colors (RGB
) you’ll want to run using Images
, and likewise to save images you’ll need to using FileIO
. I also included a single barebones test function (test10_tri
) for triangle drawing at the bottom of WWURaster.jl
; if your code agrees with mine, running WWURaster.test10_tri()
should save out the following stunningly beautiful image:
You may want to write a script to build up a set of repeatable tests. I recommend simply creating a tests.jl
in the project’s base directory with calls like the above, then running include("tests.jl")
at the REPL. If you want to get fancy, check out Julia’s unit testing features; if you create a test/runtests.jl
with unit tests you can run test
from the package prompt and it will execute all the unit tests.
Once your triangle drawing function works, use it to draw something cool! The artifact is due one day after the code deadline; I’ve included an artifact
function you can fill in with code to produce your artifact, but you don’t need to include this in your code submission. This is also a good opportunity to play with a few more of Julia’s features and constructs. Artifacts will be showcased and the class will vote on their favorite. The winning artifact(s) will be shown in class and receive a small amount of extra credit. To help with inspiration, here’s my artifact:
580 students must complete at least one of the following extensions for full credit.
480 students may complete any of the following extensions for up to 5 points of extra credit.
You are also welcome to propose your own extensions, but run them by me first to confirm that you will get credit for them.
In all cases, you must thoroughly document the extensions you have completed in a readme.txt file included in your repository. Include a description of where to find the code that implements the extension, and instructions for how to run and test your code. Extensions with no tests or demo code will not receive full credit.
You’ve probably noticed that the edges of the triangles we’re drawing don’t look great, especially for small canvases and certain line angles. The technical term for the artifacts you’re seeing is jaggies. With an all-or-nothing approach to coloring pixels, there’s not much we can do about this, but we can make things look a lot better using what’s called anti-aliasing techniques. Two possible approaches to this are:
As mentioned in class, it’s usually the case in graphics that performance matters. One way to measure performance is to count FLOPs (floating-point operations). There’s a nice package GFlops.jl that can count the number of flops performed by a piece of code executed in the REPL. Here’s the output for my unoptimized implementation.
julia> a = Vec2(10, 10);
julia> b = Vec2(30, 10);
julia> c = Vec2(5, 40);
julia> color = RGB{Float32}(0.7, 0.7, 0.7);
julia> @count_ops img = WWURaster.test_tri(50, 50, a, b, c, color)
Flop Counter: 24154 flop
┌─────┬─────────┐
│ │ Float64 │
├─────┼─────────┤
│ add │ 6968 │
│ sub │ 9010 │
│ mul │ 8176 │
└─────┴─────────┘
julia>
Try to implement a version of draw_tri
that performs fewer flops. How much can you beat mine by?
A basic implementation may encounter problems if two triangles that share an edge are drawn onto the same canvas, because a pixel whose center lies exactly on the shared edge may be colored zero, one, or two times depending on how ‘ties’ (i.e., points directly on the line) are handled. This may be further complicated by slight floating-point errors. Develop test cases that highlight this problem, then try to find a way to fix it.
Our triangles are solid colored. Devise and implement a scheme to enable multi-colored triangles: for example, a caller might be allowed to specify a different color for each vertex (or for each edge?) and each pixel gets a color value interpolated from these.
Add and commit test10.png
, the output of test10_tri()
, to your repository. Submit your code by pushing your final changes to Github before the deadline, then submit the A0 Survey on Canvas.
To submit your artifact, upload your PNG file to the applicable A0 assignment on Canvas. Note the artifact deadline is one day later than the code deadline, so you have some extra time to get creative.
Here are some resources that you may find helpful in completing this project.
Points are earned for correctness and deducted for defficiencies in clarity or efficiency.
line_side
- 10 pointspoint_in_triangle
- 10 pointsdraw_tri
- 10 points