Project 1 | Rasterizer

Name: Star Li | SID: 3033789672


Overview

This project is about the partial implementation of a svg-format graphics rasterizer, which incorporates basic concepts such as triangle rasterization, antialiasing via supersampling, and texture mapping in the first two weeks of the course.

Overall, the focus centers around three core functionalities for rendering triangles with simple color, interpolated colors, and external texture. In the first two cases the goal is to develop efficient algorithm that calculate pixels within the triangle and fill them with color provided or interpolated using barycentric coordinates. Antialiasing is achieved through supersampling and then downsampling with box filter (average).

After that, much time is spent on the last part rendering textured triangles. The mapping from xy screen space to the uv texture space once again depends on barycentric coordinates; for texture sampling, two methods sample_nearest and sample_linear are implemented in addition to three ways (L_ZERO, L_NEAREST, L_LINEAR) of determining the mipmap used for each pixel in the screen space.

This is my first time writing a large project with C++, and it's exciting to see how its wordy syntax and static typing trade off with super fast execution speed (e.g. v.s. vanilla Python). Putting textbook/slides knowledge and algorithms into actual codes requires taking care of many engineering details and edge cases, but it's exactly such sophistication in implementation that brought me sense of fulfillment after finishing everything.

The only regret is that other interesting parts of a functional svg renderer, e.g. parser and svg element objects, are given in the skeleton code. This is reasonable since this course is about graphics, not software engineering or compiler. If I have more time, I would love to add support for more svg elements such as Path.


Task 1: Drawing Single-Color Triangles

Steps for rasterizing a single-color triangle

xd
basic/test4.png

Task 2: Antialiasing by Supersampling

Overview

The vanilla triangle rasterization in the last section assigns binary color to each pixel (either colored or white) and there's no in between. Naturally this gives rise to jaggies and sharpness in the border which are parts of aliasing. To overcome this, the core idea is to enable the pixels to take intermediate colors by sampling more frequently (supersampling) and then downsampling (average) so that the image size remains.

Steps and Modifications

xd
sample rate = 1
xd
sample rate = 4
xd
sample rate = 16

As we can see from above, as the sampling rate increases, the border pixels of the triangle (see inspector) takes more variety of values and the entire image looks more smoother (the difference between 4 and 16 case is not so obvious since test4.svg is a relatively simple one). Many pixels around the green triangle which are white in the original case takes on light green values as a result of supersampling. As a result the jaggies get reduced tremendously as the sudden change of color is replaced by a gradual development.


Task 3: Transforms

robot playing soccer

Additional rotations and translations are added so that the robot looks like playing soccer and about to shoot for a goal.


Task 4: Barycentric coordinates

xd
xd

Barycentric coordinates provides a basis (coordinate system) to uniquely describe each point within a triangle using the three vertices. To be more precise, the attribute (including but not limited to location, texture space coordinates, color) of each point can be expressed as a linear combination of the attributes of the three vertices, with the additional constraint that the weights of linear combination sum to 1 (and positive since within triangle). The graph on the image is an example of using barycentric coordinates to interpolate triangle colors given the colors in the three vertices (R, G, B respectively). For each point inside, the closer it is to one of the vertex (say green vertex on the left), the closer its color to that vertex's color (greener). This results in the entire graph being blended color triangle.


Task 5: "Pixel sampling" for texture mapping

Pixel sampling is the process of getting color from the texture image (uv space) corresponding to the query point (u, v), which are usually calculated using aforementioned barycentric coordinates in the screen space (xy space). The obtained color are used for texturing the output image as we have done in coloring the triangles.

There are certain edge cases for bilinear sampling. For example, if the query point is close to the rightmost edge of the texture image, it's impossible to find all 4 sample locations as mentioned before. My implementation uses nearest sampling to handle such scenario.

xd
nearest sampling, sample rate = 1
xd
bilinear sampling, sample rate = 1
xd
nearest sampling, sample rate = 16
xd
bilinear sampling, sample rate = 16

The difference between these two methods are best illustrated when there are abrupt change of colors in a certain part. In the above examples, the inspected region displays the alternating stripes around the parrot's eyes. We see that, with the same sample rate, bilinear method gives smoother gradient transition effect from one color stripe to another, while the nearest method looks sharper. This looks similar to the effect of increasing sample rate. It's not hard at all to explain this phenomenon since the bilinear sampling tries to integrate color information from neighboring pixels as well, which is exactly what we do when in supersampling (the difference is that supersampling is followed by averaging, but here bilinear interpolation is used).


Task 6: "Level sampling" with mipmaps for texture mapping

Level sampling is the process of finding the appropriate level of mipmap for each pixel based on estimating its screen pixel footprint in texture. Intuitively, from these slides we see that using a high level mipmap across pixel will make closer regions too blurry while using a low level one causes jaggies for further scenes. So a computed, adaptive algorithm for finding mipmap level is needed.

To implement level sampling, we need to obtain the uv coordinates of (x, y + 1) and (x + 1, y) for each point (x, y) in the screen space. This unavoidably leads to redundant computation if the original rasterize_textured_triangle method is used. So I add a HashMap for caching purpose. Once this step is done, the required parameters are wrapped into a SampleParams object and passed to Texture::sample, which then calls Texture::get_level to return a continuous level value based on the formula mentioned in lecture. For nearest level sampling, this continuous level is directly rounded; for linear level sampling, an additional interpolation of the results returned by two consecutive mipmaps is appplied. The edge case I encountered is getting some level value too small (< 0). Level 0 mipmap is then used in this case.

Performance Tradeoff:

generally speaking, increasing sample rate will linearly increase the memory usage and running time, while giving better antialiasing effect. For pixel sampling, bilinear sampling is also more costly in time than nearest sampling but also increases antialiasing power as described above. For level sampling, the zero sampling doesn't require any computation at all, and linear sampling is slower than nearest sampling. But the later two methods allows additional degrees of freedom using different mipmaps for different pixels, so the blurring/jaggie issues are better solved.

An example showing effects of different sampling methods:


xd
L_ZERO and P_NEAREST
xd
L_ZERO and P_LINEAR
xd
L_NEAREST and P_NEAREST
xd
L_NEAREST and P_LINEAR