Winter 2020
In this assignment you will learn about the most widely used way to represent surfaces for graphics: triangle meshes. A triangle mesh is just a collection of triangles in 3D, but the key thing that makes it a mesh rather than just a bag of triangles is that the triangles are connected to one another to form a seamless surface. The textbook and lecture notes discuss the data structures used for storing and manipulating triangle meshes, and in this assignment we will work with the simplest kind of structure: an indexed triangle mesh.
Your job in this assignment is to write a simple mesh generation and processing utility that is capable of building triangle meshes to approximate some simple curved surfaces, and also can add some information to existing meshes. It reads and writes meshes stored in the popular OBJ file format, a text format with one line for each vertex and face in the mesh.
Suppose we wish to generate a mesh for a loaded die as shown below:
4 different views of our loaded die, represented as a triangle mesh.
The cube extends from -1 to 1 in each of the x, y, and z directions. Each face has the following texture applied:
The texture to apply to each face of the cube. In uv-space, the lower left corner of the image is the origin, and the top right corner of the image is the point (1,1). The u direction extends horizontally across the image, the v direction extends vertically.
We could represent this mesh by listing out each triangle. Each triangle would be specified by 3 points in 3D space (i.e., the locations of each of its vertices), 3 points in 2D space (i.e., the uv-space texture coordinates of each of its vertices), and 3 unit-length 3D vectors (i.e., the normals of the surface at each vertex). This is sufficient to represent this mesh, but you may notice that we are repeating some information; for instance, the position of each corner of the cube is shared by at least 3 different triangles. Additionally, each triangle represents a flat surface, and thus the normals at each of its vertices are all equivalent. (Later, we will use triangles to approximate curved surfaces where this is not the case.) We can reduce this repetition by introducing an indexing scheme.
The OBJ file format is one such indexing scheme, and is the one we’ll be using in this assignment. In this format, the positions of all vertices are listed first, and then triangles are specified by providing 3 integers that index into this list of positions. Texture coordinates and normals are abstracted similarly; thus, each triangle is specified by 3 position indices, 3 texture coordinate indices, and 3 normal indices.
Vertex Position: A vertex position is specified on a single line by the letter “v” followed by 3 floating point numbers that represent the x, y, and z coordinates of the point.
Texture Coordinate: A texture coordinate is specified by the letters “vt” followed by 2 floating point numbers that represent the u and v coordinates respectively.
Vertex Normal: A vertex normal is specified by the letters “vn” followed by 3 floating point numbers that represent the normal vector. (The OBJ file format does not require that these be unit length, but we will require it for this assignment.)
Triangle Faces: Triangles are specified with the letter “f” followed by 3 groups of indices. Groups of indices can be a single integer (indexing the vertex position), two integers separated by a “/” (indexing the vertex position and texture coordinate respectively), two integers separated by “//” (indexing the vertex position and vertex normal respectively), and three integers each separated with a “/” (indexing the position, texture coordinates, and normals). Note that indices in the OBJ file format are 1-based! Vertices should be specified in counter-clockwise order, assuming you are looking down at the outer surface of the triangle (i.e., the triangle’s normal is pointed towards you).
Given all of this information, we can specify the loaded die above with the following OBJ file:
v 1.0 -1.0 -1.0
v 1.0 -1.0 1.0
v -1.0 -1.0 1.0
v -1.0 -1.0 -1.0
v 1.0 1.0 -1.0
v 1.0 1.0 1.0
v -1.0 1.0 1.0
v -1.0 1.0 -1.0
vt 1.0 1.0
vt 0.0 1.0
vt 0.0 0.0
vt 1.0 0.0
vn 1.0 0.0 0.0
vn -1.0 0.0 0.0
vn 0.0 1.0 0.0
vn 0.0 -1.0 0.0
vn 0.0 0.0 1.0
vn 0.0 0.0 -1.0
f 1/1/4 2/2/4 3/3/4
f 1/1/4 3/3/4 4/4/4
f 1/4/1 5/1/1 6/2/1
f 1/4/1 6/2/1 2/3/1
f 2/4/5 6/1/5 7/2/5
f 2/4/5 7/2/5 3/3/5
f 3/2/2 7/3/2 8/4/2
f 3/2/2 8/4/2 4/1/2
f 4/2/6 8/3/6 5/4/6
f 4/2/6 5/4/6 1/1/6
f 5/1/3 8/2/3 7/3/3
f 5/1/3 7/3/3 6/4/3
Note that even though there are 12 total triangles, there are only 8 unique locations for each triangle vertex, so there is no point in listing them multiple times. Similarly, each vertex has only 1 of 4 total texture coordinates, and each vertex has 1 of 6 total normals. This scheme allows us to eliminate the redundancy of listing these values once for each triangle. Also note that many vertices can share a position, but each can have different texture coordinates and normals; for instance, each corner of the mesh is shared by at least 3 triangles, but each may face a different direction and therefore may have its own normal.
Create your assignment repository by accepting the invitation via the Github Classroom link provided in the A1 Assignment on Canvas. Clone your repository.
Julia is installed on Linux in all of the CS the labs. You can you can download and install Julia on your own computer from https://julialang.org/downloads/. Type julia
at the terminal to fire up the REPL (read-eval-print loop, an interactive shell that runs Julia code).
You’ll need to install a few Julia package dependencies for this project. Julia makes this pretty easy! At the REPL, type ]
and you should see the prompt change to pkg>
. You can get back to the julia>
prompt by pressing backspace. At the pkg>
prompt, enter the following command to install the dependencies:
add ArgParse FileIO StaticArrays
You should now be able to go into the src
directory of the repository and run julia meshgen.jl
at the command line without any errors. You can also run it with the --help
flag to see the auto-generated usage message.
In this assignment, you will complete three tasks:
meshgen.jl
that provides a command-line interface to the mesh processing functionality that you’ll implement in OBJMeshes.jl
.The skeleton code provided defines some useful data structures and includes code to read and write OBJ meshes to and from files. Below is a quick tour of the skeleton files and the functionality provided therein.
GfxBase.jl contains two type aliases that give us convenient names for 2- and 3-vectors of floating-point numbers. In this assignment, the vertices and normals are represented as Vec3
s, while texture coordinates are represented using Vec2
s. These types are easy to use:
v = Vec3(2, 4, 1) # create a Vec3 object containing 1, 2, and 3
v[1] # returns 2.0; recall Julia is 1-indexed, and Vec3 stores Float64
OBJMeshes.jl
is where you’ll be implementing the bulk of this assignment. Included at the top of this file are the mesh data structures that are used to represent the mesh in memory, as well as functions that write to and read from from disk.
The mesh data structure mirrors the OBJ file format closely, containing arrays of the four components described above:
positions
is an Array of Vec3
, each of which is a vertex position.uvs
is an Array of Vec2
, each of which is a texture coordinate.normals
is an Array of Vec3
, each of which is a vertex normal.triangles
is an Array of OBJTriangle
s describing each face in the mesh.mutable struct OBJMesh
positions::Array{Vec3, 1} # all vertex positions
uvs::Array{Vec2, 1} # all texture coordinates
normals::Array{Vec3, 1} # all vertex normals
triangles::Array{OBJTriangle, 1} # the OBJTriangles belonging to the mesh
end
The OBJTriangle
type stores the information for a single triangle:
mutable struct OBJTriangle
positions::Array{Int, 1} # vertex position indices
uvs::Array{Int, 1} # vertex texture coordinate indices
normals::Array{Int, 1} # normal vector indices
end
Recall that the triangles are defined using indices of positions, texture coordinates, and normals. Here, these are represented using 3 Arrays of Int
. In this assignment, the positions
array must have 3 values, and the uvs
and normals
arrays may have either 0 or 3 values, depending on whether the mesh has texture coordinates and/or normals defined.
read_obj(obj_filename)
takes a filename as a string and loads the OBJ mesh into an OBJMesh struct.
write_obj(obj_filename, mesh)
takes a filename and an OBJMesh and writes the mesh contained in mesh
to the file obj_filename
.
The cube_mesh
function is provided to you as an example of creating a simple mesh. This creates an OBJMesh representing the cube described in the previous section. You will write the two analogous functions below this one; the first generates a cylinder mesh and the second generates a sphere mesh.
This is a small main program that provides a command-line tool to expose the functionality in OBJMeshes.jl. The command line argument parsing has been done for you, and the file should be set up so that you can directly call functions that have been exported from OBJMeshes.jl.
meshgen
Command Line ToolComplete the implementation of the main program in meshgen
. The program should support the following usages:
(1) julia meshgen.jl -g <cube|sphere|cylinder> [-n divisionsU=32] [-m divisonsV=16] <outfile.obj>
(2) julia meshgen.jl -i <infile.obj> <outfile.obj>
In usage (1), a mesh the given geometry is generated and saved in OBJ
format to <outfile.obj>. When generating mesh geometry,
-n specifies the number of divisions in the u direction
(this is ignored by cube)
-m speicifes the number of divisions in the v direction
(this is ignored by cylinder and cube)
In usage (2), a mesh is loaded from <infile.obj>, any existing vertex
normals are discareded, and new vertex normals are estimated based on the
triangle geometry.
The presence of the -i flag overrides -g, -n, and -u; if an input file is
given, ignore all other arguments and perform normal estimation on the mesh
read from the given file, saving the result to <outfile.obj>.
The code to perform command line argument parsing has been done for you. I recommend starting with a basic implementation of this program that only supports the cube
geometry, making use of the cube_mesh()
function provided in OBJMeshes.jl. As you implement each of the remaining tasks, update your meshgen
program to support the newly implemented functionalisty.
The cube_mesh()
function in OBJMeshes.jl provides an example of how to create a triangle mesh using the provided data structures. In this task you will implement the functions cylinder_mesh(divisionsU)
and sphere_mesh(divisionsU, divisionsV)
, which compute and store the vertices, texture coordinates, vertex normals, and triangle faces and store them in an OBJMesh object.
Cylinders and spheres are different from cubes in that we can’t model them exactly with triangles, because they have curved surfaces. A triangle mesh approximating a smooth surface should have normal vectors stored at the vertices that indicate the direction normal to the exact surface at that point. When generating the shapes, you will generate points that are on the surface, and for each you should also calculate the normal vector that is perpendicular to the surface at that point. Additionally, the generated meshes will have texture coordinates; the details of these are described below for each geometry.
Note that you should take advantage of the indexed storage scheme where possible: for full credit, you should make sure that if two triangles meet at a position in space, that position should only be specified once in the resulting OBJ file. Similarly, if two vertices have both the same location and texture coordinates, those texture coordinates should only be specified once; if they share the same location and normal, then the normal should only be specified once. (Duplicate normals and texture coordinates are allowed, but only as long as they are used at different places on the mesh.)
When the method sphere_mesh(divisionsU, divisionsV)
is correctly implemented, you will get an OBJ file of a sphere (32x16) by running meshgen.jl
with arguments -g sphere -n 32 -m 16 sphere.obj
. To help you debug your implementation, we also provide a visualization tool; you can view the generated mesh by dragging the OBJ file to the window, and can select an image to apply a texture to it. (see below under Testing your implementation for details of this tool).
An example result is shown below:
The cylinder has radius 1 and height 2 and is centered at the origin; its longitudinal axis is aligned with the y-axis. It is tessellated with n divisions arranged radially around the outer surface. The two ends of the cylinder are closed by disc-shaped caps parallel to the *x**z*-plane.
The vertices around the rims of the cylinder share 2 or more normals and texture coordinates, though they share the same positions. Each cap consists of n vertices arranged in a circle as well as a single point where the cap intersects the y-axis. This point is incorporated into each triangle that makes up the cap.
Along the cylinder’s shell (i.e., excluding its caps), texture coordinates in the u dimension run from 0 to 1 in a counterclockwise direction as viewed from the +y direction. There is a texture seam (where u=0 meets u=1) along vertices that have a z-coordinate of −1. Multiple texture coordinates occur at the same position in space to allow this discontinuity. Coordinates run from 0 to 0.5 in the v dimension, increasing in the +y direction. The texture coordinates for the two caps are circles inscribed in the upper-left (for the −y cap) and upper-right (for the +y cap) quadrants of the unit square in the uv-plane, with the +u direction corresponding to the +x direction in 3D space, and the +v direction corresponding to the −z direction for the top cap, and the +z direction for the bottom cap. The -m
flag is ignored for the cylinder.
Specs illustration for the cylinder (for the case of n = 32 and m = 16)
The sphere has radius 1 and is centered at the origin in 3D coordinates. It is tessellated in latitude-longitude fashion, with n divisions around the equator and m divisions from pole to pole along each line of longitude. Note that m divisions requires m+1 vertices from pole to pole. The North pole is at (0,1,0), the South pole at (0,−1,0), and points on the Greenwich meridian have coordinates (0,y,z) with z>0.
The mesh is generated with vertex normals that are normal to the exact sphere, and with texture coordinates (u,v) where u depends only on longitude, with u=0 at longitude 180 degrees West and u=1 at 180 degrees East, and where v depends only on latitude, with v=0 at the South Pole and v=1 at the North pole. Each quadrilateral formed by two adjacent longitude lines and two adjacent latitude lines is divided on the diagonal to form two triangles.
The texture coordinates along the 180th meridian are duplicated: one texture coordinate has u=0 and the other has u=1, to enable the discontinuity required for correct wrapping of a image texture across the seam. The pole has n different texture coordinates, to enable nearly-appropriate texture in the row of triangles adjacent to the pole. Every other triangle around each pole becomes degenerate, i.e., collapses into a line; these degenerate triangles should be omitted.
Specs illustration for the sphere (for the case of n = 32 and m = 16)
If you have your sphere and cylinder correctly implemented, you could attach texture to your mesh in our view tool. Here is an example result for cylinder and sphere:
Recall the second use case for the meshgen
program:
julia meshgen.jl -i <infile.obj> <outfile.obj>
In this mode,, the user provides an input OBJ mesh file, which the program reads in. The mesh is assumed to have no normals (if normals are included in the input file, they are ignored). The program then generates approximate normals at each vertex as described below, and writes the resulting mesh to the user-provided output file.
Since the original surface which the mesh approximates is forgotten (if there even was one), we need some way to make up plausible normals. There are a number of ways to do this, and we’ll use a simple one for this assignment: the normal at a vertex is the average of the geometric normals of the triangles that share this vertex.
Your first thought might be to do this as a loop over vertices, with an inner loop over the triangles that share that vertex:
for each vertex v
normal[v] = (0,0,0)
for each triangle t around v
normal[v] += normal of triangle
normal[v].normalize()
With the appropriate data structures, this is possible, but in our case there’s no efficient way to do the inner loop: our data structure tells us what vertices belong to a triangle, but the only way to find triangles that belong to a vertex is to search through the whole list of triangles. This is possible but would be quadratic in the mesh size, which is bad news for large meshes.
However, it’s simple to do it with the loops interchanged:
for each vertex v
normal[v] = (0,0,0)
for each triangle t
for each vertex v around t
normal[v] += normal of triangle t
for each vertex v
normal[v].normalize()
This way the inner loop can efficiently visit just the necessary vertices. Nifty!
Since your program just writes a file full of inscrutable numbers, you need some way to look at the results. We have provided a mesh visualization tool that works in your browser, available here. To view a mesh, click “Select OBJ File” (or just drag an OBJ file onto the window). If the mesh has texture coordinates, you can upload an image texture by clicking “Select Texture File” (or, again, just drag the image file onto the window). The viewer optionally shows the wireframe structure and vertex normals of the mesh. Coordinate axes may also be displayed; +x is red, +y is green, and +z is blue. If triangle faces are wound in the wrong direction, they appear yellow and will not be textured.
There are a number of other programs you can use to visualize meshes, for example Blender, ObjViewer, MeshLab, or p3d.in. Be careful, though! Some of these programs add normals to your meshes if they don’t already exist, or if they’re malformed.
To help you verify that your output is correct, you are provided reference meshes produced using default settings the sphere and cylinder geometry (sphere-reference.obj
and cylinder-reference.obj
). Be sure to compare your output to the reference one with textures, paying close attention to tricky areas such as the poles of the sphere.
Also included are two meshes without normals (a bunny and a horse), as well as examples of what the meshes should look like when normals have been estimated. Be patient with the horse example, as it contains many triangles and may take a while to process.
In addition to the completed code, please generate the following outputs and place them in your repo’s results
directory:
cylinder.obj
, a cylinder generated with default settingscylinder_5.obj
, a cylinder generated with -n 5
sphere.obj
, a sphere generated with default settingssphere_5.obj
, a sphere generated with -n 5
and -m 16
(the default m
)sphere_4_5.obj
, a sphere generated with -n 4
and -m 5
bunny.obj
, the output of estimating normals for data/bunny-nonorms.obj
hours.txt
as follows:
To submit, simply push your finished code and results to Github sometime before the deadline.
The following extensions are grouped into two categories. Category 1 items should be somewhat easier than Category 2 items.
480 students may complete any of the following extensions for extra credit.
580 students must complete either
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. If applicable, include any test data in your repository that will help convince me that your feature works.
A torus is a doughnut-shaped surface defined by a major radius, affecting the size of the hole, and a minor radius, affecting the thickness of the ring. Add torus to the list of geometries generated by your program. Your code should create a torus with major radius 1 and minor radius r (controlled by an additional -r
flag with a default of 0.25). Its u coordinates are like the sphere, and the v coordinate runs from 0 to 1 around the inside of the torus, with the direction arranged so that the texture is right-reading from the outside (i.e., the texture is not flipped when mapped to the surface). Like the sphere, it has a seam on the −z half of the yz-plane, and it has a similar seam around the inner surface of the doughnut hole; vertices along each seam share a pair of texture coordinates, and single vertex, at the position (0,0,r−1) where the seams meet, shares 4 texture coordinates.
Write a function that compares two OBJMesh objects to determine whether they are equivalent. Use the following definition of equivalency:
In addition to the two meshes, your function should take two arguments, verbose
and epsilon
. If verbose
is true, print a message describing each found violation of the above properties. Consider floating-point values equal if they are within epsilon
of each other.
The OBJ data format allows the reuse of positions, vertex coordinates, and normals through indexing. However, this reuse is not enforced. Extend your meshgen
utility with an option to take in an uncompressed OBJ file and remove duplicate positions, texture coordinates, and normals, adjusting vertex indices as necessary.
The OBJ specification allows arbitrary polygons, not just triangles. Make a separate version of the file-reading code that can load meshes with arbitrary polygons. That is, when reading the mesh, you should take all faces that have 4 or more vertices and split them into triangles before adding the resulting triangles to the OBJMesh structure.
Many algorithms that operate on triangle meshes assume that the mesh is well-behaved; that is, it has some structure that is more predictable than just a soup of arbitrary triangles. One class of well-behaved mesh is known as a manifold mesh. Manifold meshes satisfy the following properties:
Extend your meshgen
utility to check if an input triangle mesh is manifold or not. As a hint, it may be useful to create a new data structure which, given the index of a triangle, allows you to quickly find that triangle’s neighboring faces.
You may have noticed that for the sphere, the triangles get compressed near the poles. Provide an alternative tessellation approach to create a more even mesh; see icosphere as an example. This feature should be added as another geometry option in addition to the sphere specification described above.
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.
Deductions:
hours.txt
not filled in