This is the single largest MP in the course, with both core and elective points. The core parts are core for two reasons.
In the short term, we’re about to learn WebGL2. WebGL2 is simpler than some other 3D APIs, but it is still quite complicated, needing to coordinate between multiple processors, memory pools, and languages. This assignment has you implement (and thus learn and internalize) a portion of that API a piece at a time in a language you’re familiar with without the complexity of the Browser-CPU-GPU communication component of WebGL2.
In the long term, I want to show you there’s no magic in graphics by revealing how each piece of current graphics systems work. You remember what you do for far longer than you remember what you’ve merely been told, so implementing is key to that. An essential piece of graphics is how triangles are rendered and interpolated across, so that’s core. The various electives are all also common and useful, though less essential.
Implement the core parts of a simplified version of the WebGL API that we’ll be using for most other MPs. In particular,
Write a program that reads a .txt
file and produces
a .png
file. It will be invoked as e.g. ./yourprogram exampleInput.txt
,
via a makefile.
Handle the input file keywords png
, position 4 ...
, color 3 ...
, and drawArraysTriangles
. See
our notes on program state for tips on doing this
well.
Implement the DDA algorithm, and the scanline algorithm which consists of repeated invocations of DDA. These algorithms are defined down to the pixel in almost all contexts, and should match the provided input files and their outputs very closely. Almost all successful submissions follow our DDA pseudocode and scanline diagram closely.
Implement division by w and viewport transformations. See our notes on algorithms for tips on doing this well.
You may stop after implementing the core parts. Elective parts may be implemented in any set of MPs, and getting 0 electives here is fine if you do extra electives elsewhere.
The first four elective components complete the core functionality; they are prerequisite to the other electives in this MP.
Pt | Keywords | Prereqs | Test cases |
---|---|---|---|
1 | elements and drawElementsTriangles |
rast-elements.txt | |
2 | depth |
rast-depth.txt | |
1 | sRGB |
rast-sRGB.txt and rast-gammabox.txt | |
2 | hyp |
depth and sRGB |
rast-perspective.txt |
The other electives are mostly independent of one another, with the first four electives as their only prereqs. However, there are a few exceptions where additional prereqs are needed:
Pt | Keywords | Additional Prereqs | Test cases |
---|---|---|---|
2 | frustum |
rast-frustum.txt and rast-manyclip.txt | |
0.5 | cull |
rast-cull.txt | |
1 | fsaa |
rast-fsaa2.txt and rast-fsaa8.txt | |
2 | color 4 |
rast-alpha.txt | |
2 | uniformMatrix |
rast-matrix.txt | |
0.5 | position 3 and
position 2 |
uniformMatrix |
rast-2d3d.txt |
3 | texture and texcoord |
rast-textures.txt | |
2 | pointsize and drawArraysPoints |
texture |
rast-points1.txt and rast-points2.txt |
1 | decal |
texture and texcoord |
rast-decals.txt |
For this MP you submit one program, in any language of your choosing, that implements all of the core and any elective functionality you choose. The program will be executed as follows:
make build
make run file=rast-grey.txt
make run file=rast-smallgap.txt
# ...
make run file=rast-points2.txt
See the associated warm-up for more on how to set up a Makefile and generate PNG images.
It is tedious to grade output files for inputs you haven’t
implemented. Because of that you’ll be asked to submit a file named
implemented.txt
which lists the optional parts you
implemented; in particular, it should be a subset of the following
elements
depth
gamma
hyperbolic
frustum
cull
anti-alias
alpha
matrix
position23
textures
points
decal
Submitting a file that says you implemented something you didn’t may result in a small professionalism penalty for wasting grader time.
All test input files, reference output files, and supporting files can be downloaded as a zip
Input | Output | Notes |
---|---|---|
rast-gray.txt | Comments inside input file give intermediate computation results. | |
rast-smallgap.txt | Checks especially for boundary via the gap between triangles and the initial offsets via the lack of horizontal bands in the color of the left triangle. | |
rast-smoothcolor.txt | Various interpolation errors can be detected because in this image they’ll create a color difference on the front triangle. | |
rast-checkers.txt | More than a hundred adjacent 1-pixel-wide
triangles to test alignment. Note especially the white row on the
top. Some pixels DDA generates will be off-screen. You can simply ignore (not draw) those pixels. You may wish to try this with a higher-res output (up the png size several fold on the
first line) to see how triangles make this image. |
Input | Output | Notes |
---|---|---|
rast-depth.txt | Created by a combination of (a) depth
interpolation to each pixel and (b) comparison with a per-pixel depth
value; if you plot the depth of each pixel as a gray-scale color you
should get something like
this: |
|
rast-elements.txt |
Input | Output | Notes |
---|---|---|
rast-sRGB.txt | rast-smoothcolor with sRGB;
note that adding sRGB makes it much brighter overall. |
|
rast-gammabox.txt | rast-checkers with sRGB; if
your monitor is properly calibrated, squinting should make this look
uniformly gray. |
|
rast-perspective.txt | The left wall is split in the middle; the right wall is not; they should have the same overall color, and match the color of the horizontal bar where it crosses them. | |
rast-frustum.txt | Includes both zero and negative w values; if you don’t clip it probably won’t run at all. | |
rast-manyclip.txt | Two intersecting triangles extending both behind and in front of camera clipped by several frustum walls. |
Input | Output | Notes |
---|---|---|
rast-textures.txt | Note that straight lines in the textures
look straight because the file uses hyp ; without that it
would have had changes in angle where the two triangles of each face
met, like
this:There are multiple places in texture code where rounding can be done in several ways; rather than enumerate them all, we accept images where the textures are shifted up to one full texel from our reference images. |
|
rast-matrix.txt | This is an RGB/CMY cube rendered twice using matrices we’ll learn to construct later in the course. | |
rast-decals.txt |
Input | Output | Notes |
---|---|---|
rast-2d3d.txt | Cube uses position 3 ;
rectangle uses position 2 ; both use
uniformMatrix to verify that they are converted to
4-vectors internally. |
|
rast-cull.txt | Both cubes have the same 12 triangles,
though one has them at half the size of the other; but the specification
order differs. For example, one cube has triangle 0 1 2 and
the other 1 0 2 , leading one to be clockwise and the other
counter-clockwise when rendered. |
Input | Output | Notes |
---|---|---|
rast-alpha.txt | There are many different things that can
go wrong here; the most common erros are: • Using premultiplied alpha formulas. • Using sRGB for one color and linear for the other. • Trying to apply sRGB to alpha channel. • Cumulative rounding errors from repeated conversion to bytes and back to floats. • Not storing the resulting alpha in the frame buffer. |
|
rast-fsaa2.txt | re-colored rast-depth with
2×2 subpixels for 4 levels of opacity along borders. |
|
rast-fsaa8.txt | re-colored rast-depth with
8×8 subpixels for 256 levels of opacity along borders.You may wish to try adding fsaa 8 to the rast-gammabox.txt
and see if you can explain what comes out and where the new horizontal
bands come from |
|
rast-points1.txt | Points, some colored and some textured, overlapping with triangles. | |
rast-points2.txt | The same as rast-points1 but
with different image dimensions, emphasizing that point sizes are
specified in pixels and do not expand if placed in a bigger image. |
In this assignment you will implement a subset of the GPU operation of WebGL2 and related rendering libraries. The commands in the text files we provide are inspired by the WebGL2 API, and will work best if you mimic in your code the GPU state that WebGL2 expects.
You’ll have four broad kinds of state:
is the depth buffer on?
The attribute buffers in WebGL2 are assumed to be variable-length arrays of 4-vectors. WebGL2 lets you specify many such buffers and use them in programmable ways using vertex shaders; in this assignment we have just a few specific attributes:
position
, with coordinates (x,y,z,w)color
, with coordinates (r,g,b,a)texcoord
, with coordinates (s,t)pointsize
, with coordinates (p)WebGL2 lets you specify attributes with different numbers of coordinates than the internal state would suggest. It fills in missing coordinates as (0,0,0,1):
The element array buffer is just a list of integers.
Most lines of the input file will be manipulating state, after which will be a drawing command. The drawing command tells you which indices in the attribute buffers to connect into primitives. You’ll make those connections and then draw each primitive.
GPUs generally use the Bresenham algorithm to draw triangles, modified with optimizations for cache locality like tiles, optimizations for parallelism like stamps, and optimizations of depth buffers like HiZ. Bresenham is very efficient in hardware as it can avoid some of the complexities of floating-point arithmetic, but you’re writing code to run on the CPU with full floating-point support so we recommend using the simpler but functionally equivalent DDA algorithm instead. We also don’t recommend adding any of the hardware-oriented optimizations.
DDA works on vectors. You should definitely code it with vectors, probably with long vectors including all of the attribute values at a point together in one (i.e. (x,y,z,w,r,g,b,a,s,t) if you implement all the elective parts). The only non-vector operations are:
That’s it. Every other part of DDA uses vectors, and if you find
yourself writing something[2]
or something.z
anywhere in your DDA code you did something wrong.
The result of DDA is interpolated values at each pixel. Here you’ll access individual coordinates: x and y to be the pixel coordinate, z for the depth buffer, s and t for texture lookups, a for alpha blending, and (r,g,b) for color.
The file may have four types of keywords:
png
keyword will always be firstWhen a drawing command is encountered, it draws with the data provided so far.
Suppose a file has something like
position
, color
, and
texcoord
texture
and new position
Then the first draw uses the first positions and no texture; the second draw uses the new positions, old texcoord, and texture.
We recommend reading the file once, line by line, modifying state for each non-draw command and rendering based on the current state for each draw command.
png
width height
filename.png
similar to creating a <canvas>
element in WebGL2
depth
similar to gl.enable(gl.DEPTH)
in WebGL2
sRGB
always enabled in WebGL2
hyp
always enabled in WebGL2
fsaa
levelsimilar to getContext('webgl2', {antialias:true})
when creating a WebGL2 rendering context; enabled by default on some
browsers, not in others
cull
similar to gl.enable(gl.CULL_FACE)
and gl.cullFace(gl.BACK)
in WebGL2
decals
uniform sampler2D theImage;
in vec4 vertcolor;
in vec2 texcoord;
out vec4 color
void main() {
vec4 texcolor = texture(theImage, vec2(s,t));
= vec4(texcolor.rgb*texcolor.a + vertcolor.rgb*(1-texcolor.a),
color .a + vertcolor.a - texcolor.a*vertcolor.a);
texcolor}
frustum
always enabled in WebGL2
texture
filename.png
and always be
the filename of an existing PNG file on the grading server during
testing
let img = new Image()
.src = filename
img.addEventListener('load', event => {
imglet texture = gl.createTexture()
.activeTexture(gl.TEXTURE0)
gl.bindTexture(gl.TEXTURE_2D, texture)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img)
gl
})let bindPoint = gl.getUniformLocation(program, 'samplerNameInFragmentShader')
.uniform1i(bindPoint, 0) gl
uniformMatrix
n0 n1 n2 …
n14 n15gl.uniformMatrix4fv(m, false, new Gloat32Array([n0, n0, ... n14, n15])
in WebGL2 where the vertex shader includes:
in layout(location=0) position;
uniform mat4 m;
void main() {
gl_Position = m * position;
}
and m
is returned by gl.getUnformLocation(program, 'm')
position
size num0 num1
num2 …size is either 2
, 3
, or
4
there are a size multiple of numbers after size (e.g. if size is 3 there will be 3 or 6 or 9 or 12 or … additional numbers)
the numbers are the coordinates of position vectors for vertices, with missing z being implicitly 0 and missing w being implicitly 1.
position 2 4 1 8 0.4 0.1 0.8
provides 6 numbers paired for 3 vectors: \big[ (4,1), (8,0.4), (0.1,0.8) \big] or, filling in the missing coordinates to make 4-vectors, \big[ (4,1,0,1), (8,0.4,0,1), (0.1,0.8,0,1) \big]
coordinates are provided in normalized device coordinates, with x=-1 being the left edge of the screen and +1 the right edge; y=-1 the top edge of the screen and +1 the bottom edge, etc.
let buffer = gl.createBuffer()
.bindBuffer(gl.ARRAY_BUFER, buffer)
gl.bufferData(gl.ARRAY_BUFER, new Float32Array(num0, num1, num2, ...), gl.STATIC_DRAW)
gl.vertexAttribPointer(7, size, gl.FLOAT, false, 0, 0)
gl.enableVertexAttribArray(7) gl
where the vertex shader has in layout(location=7) position;
color
size num0 num1
num2 …3
, or 4
position
, but giving RGBA colors
instead of XYZW positionssimilar to how WebGL2 blends if you call gl.enable(gl.BLEND)
and gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
texcoord
size num0 num1
num2 …2
position
, but giving ST texture
coordinates instead of XYZW positionspointsize
size num0 num1
num2 …1
position
, but giving point sizes
instead of XYZW positionselements
i0 i1 i2 …let buffer = gl.createBuffer()
.bindBuffer(gl.ELEMENT_ARRAY_BUFER, buffer)
gl.bufferData(gl.ELEMENT_ARRAY_BUFER, new Uint16Array(i0, i1, i2, ...), gl.STATIC_DRAW) gl
drawArraysTriangles
first countcount
will be a multiple of 3 (this is not required
in WebGL2)
draws a triangle with vertices position[first+0]
,
position[first+1]
, position[first+2]
and
corresponding color
and texcoord
s
draws a triangle with vertices position[first+3]
,
position[first+4]
, position[first+5]
and
corresponding color
and texcoord
s
…
draws a triangle with vertices position[first+count-3]
,
position[first+count-2]
,
position[first+count-1]
and corresponding
color
and texcoord
s
similar to gl.drawArrays(gl.TRIANGLES, first, count)
in WebGL2
drawElementsTriangles
count
offsetcount
will be a multiple of 3 (this is not required
in WebGL2)
draws a triangle with vertices
position[elements[offset+0]]
,
position[elements[offset+1]]
,
position[elements[offset+2]]
and corresponding
color
and texcoord
s
draws a triangle with vertices
position[elements[offset+3]]
,
position[elements[offset+4]]
,
position[elements[offset+5]]
and corresponding
color
and texcoord
s
… and so on up to
position[element[offset+count-1]]
similar to gl.drawElements(gl.TRIANGLES, count, gl.UNSIGNED_SHORT, offset)
in WebGL2
drawArraysPoints
first countdraws a square centered on position[first+0]
with
diameter pointsize[first+0]
pixels and color
color[first+0]
draws a square centered on position[first+1]
with
diameter pointsize[first+1]
pixels and color
color[first+0]
… and so on up to position[first+count-1]
each square has texture coordinates varying from (0,0) in its top-left corner to (1,1) in its bottom-right corner; this is
similar to the built-in gl_PointCoord
in
WebGL2
similar to gl.drawArrays(gl.POINTS, first, count)
in WebGL2