CSCI 480 / 580 - Assignment 0: Hello, Triangle!

Scott Wehrwein

Winter 2021

Introduction

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 - Environment

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.

Remote - Simple

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.

Remote - Fancy

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.

Project Setup - Once Per Assignment

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]
Setting up Revise.jl

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).

Project Setup - Once Per Session

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.

Julia - Language

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>
Multidimensional Arrays

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:

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 SArrays, 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).

Other Basics

Assignment and unpacking - convenient stuff works with tuples and arrays, including Vec2 and Vec3:

You can define functions in (at least) the following two ways:

Some potentially useful builtin functions:

Some potentially useful functions from LinearAlgebra (comes standard installed; just add using LinearAlgebra to bring these into scope):

The Math

Point-in-triangle

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.

Array Indices and Coordinate Systems

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).

Getting Started

Github Classroom

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.

Tasks

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.

  1. 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).
  2. point_in_triangle returns whether a point p is in a given triangle.
  3. draw_tri takes an image and fills points inside a given triangle in with a given color.

Workflow and Testing

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 Vecs and primary functions are exported 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.

Artifact

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:

Extensions

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.

Submission

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.

Resources

Here are some resources that you may find helpful in completing this project.

Julia

Rubric

Points are earned for correctness and deducted for defficiencies in clarity or efficiency.