Lab 4: Making a Mesh

Highlights of this lab:

In this lab, you will learn:

• a way to represent a mesh as convex polygonal faces and how to transform that into triangles and wireframes
• how to generate a height map of any size
• how to load external files like textures and meshes from Javascript
• how to use Blender and uofrOBJLoader to load complex models for you to render

Assignment

After the lab lecture, you have two weeks to modify the files in Lab4.zip to:

• Write some functions to translate one geometry representation to another
• Design an octahedron with a different colour for each solid face and a wire frame representation.
• Modify a function based height map into a texture based height map.
• Look Ahead/Bonus: Next lab we import at least one OBJ into a WebGL program. Maybe not with j3di.js. In the meantime, see if you can figure out how to make an OBJ work with j3di.js…

Lecture Notes

Getting More From One Hand Made Mesh

You might remember this cube from Lab 3

Figure 1: This looks an awful lot like the wire cube from lab 3.

It was represented like this:

var cubeVerts = [
[ 0.5, 0.5, 0.5, 1], //0
[ 0.5, 0.5,-0.5, 1], //1
[ 0.5,-0.5, 0.5, 1], //2
[ 0.5,-0.5,-0.5, 1], //3
[-0.5, 0.5, 0.5, 1], //4
[-0.5, 0.5,-0.5, 1], //5
[-0.5,-0.5, 0.5, 1], //6
[-0.5,-0.5,-0.5, 1], //7
];

var shapes = {
wireCube: {Start: 0, Vertices: 30},
solidCube: {Start: 30, Vertices: 36},
axes: {Start: 66, Vertices: 6}
};

//Look up patterns from cubeVerts for different primitive types
var cubeLookups = [
//Wire Cube - use LINE_STRIP, starts at 0, uses 30 vertices
0,4,6,2,0, //front
1,0,2,3,1, //right
5,1,3,7,5, //back
4,5,7,6,4, //right
4,0,1,5,4, //top
6,7,3,2,6, //bottom
//Solid Cube - use TRIANGLES, starts at 30, uses 36 vertices
0,4,6, //front
0,6,2,
1,0,2, //right
1,2,3,
5,1,3, //back
5,3,7,
4,5,7, //left
4,7,6,
4,0,1, //top
4,1,5,
6,7,3, //bottom
6,3,2,
];

Notice that to help hand design this, the points are defined first in their own array without any connections. The same points are then connected by reference to their indexes, lookups, in different ways to specify a wire and a solid version of the same object. Seperate connection definitions are necessary since drawing the wire cube with triangles looks wrong, and drawing the solid cube with lines also looks wrong. The way the lookups array was built is time consuming though, so let's see if we can make the job easier.

The ultimate goal is to get a solid and wire version from the same input automatically.

Wireframe Drawings

For efficiency, the hand built Wire Cube uses LINE_STRIP drawing primitives. This puts extra burden on the designer, though, since it can be hard to find a path that doesn't double back on itself a lot or cross through the middle of the object. It will be far easier to write an automated version that uses LINES instead.

Using LINES, the wire cube would look like this:

//Wire Cube - uses LINE, starts at ???, uses 48 vertices
0,4,  4,6,  6,2,  2,0,  //front
1,0,  0,2,  2,3,  3,1,  //right
5,1,  1,3,  3,7,  7,5,  //back
4,5,  5,7,  7,6,  6,4,  //right
4,0,  0,1,  1,5,  5,4,  //top
6,7,  7,3,  3,2,  2,6,  //bottom

The data has been organized into pairs to show each line segment. See all the duplication? The continuous line loop has been split up into individual segments by repeating vertices within each face of the cube. Each side, though, already contained a repeat to complete the face, so there's no need for a new repeat between lines.

What if all you have is a mesh made of TRIANGLES, or of polygonal faces? Making the mesh is easy! In pseudocode:

Pseudocode: TrianglesToWireframe
// TrianglesToWireframe
// Inputs:
//    vertices: array of vertices ready to draw with WebGL as
//              primitive type TRIANGLES
// Outputs:
//    returns an array of vertices that outline each triangle
//    when drawn as primitive type LINES
function TrianglesToWireframe(vertices)
{
//Declare a return array

//loop index i from [0 to vertices length), counting by 3s
{
//add vertex at index i to return array
//add two copies of vertex at index i + 1 to return array
//add two copies of vertex at index i + 2 to return array
//add vertex at index i to return array
}
//return the return array
}
Pseudocode: FacesToWireframe
// FacesToWireframe
// Inputs:
//    vertices: array of vertices in the mesh in no particular order
//    facesArray: array of "faces" where each face is an array
//                of vertex indices (lookups) in the vertices array.
//                For all faces in the array, these vertices
//                should define a convex polygon in the same order,
//                either clockwise or counterclockwise
// Outputs:
//    returns an array of vertices that outline each face
//    when drawn as primitive type LINES
function FacesToWireframe(vertices, facesArray)
{
//Declare a return array

//loop index i from [0 to facesArray length)
{
//lookup the vertex at face i index 0
//add it to the return array

//loop index v from [1 to face i's length)
{
//lookup the vertex at face i index v
//add it to the return array twice
}

//lookup the vertex at face i index 0
//add it to the return array
}

//return the return array
}

The original Solid Cube array could be used as input to TranglesToWireFrame, and the result would look like this:

Figure 2: The lines through the faces of the box just seem unnecessary...

Since we probably don't want to outline sub-triangles of each cube face like that, the original cube arrays could be modified to match FacesToWireframe as follows:

Vertices and Faces Cube Data
var cubeVerts = [
[ 0.5, 0.5, 0.5, 1], //0
[ 0.5, 0.5,-0.5, 1], //1
[ 0.5,-0.5, 0.5, 1], //2
[ 0.5,-0.5,-0.5, 1], //3
[-0.5, 0.5, 0.5, 1], //4
[-0.5, 0.5,-0.5, 1], //5
[-0.5,-0.5, 0.5, 1], //6
[-0.5,-0.5,-0.5, 1], //7
];

//Look up patterns from cubeVerts for different primitive types
var cubeFaces = [
[0,4,6,2], //front
[1,0,2,3], //right
[5,1,3,7], //back
[4,5,7,6], //right
[4,0,1,5], //top
[6,7,3,2], //bottom
];

//Load a wire frame into points array for Vertex Data Buffer,
//and store drawing information
var points = []; //Declare empty points array
var shapes = {}; //Declare empty shapes object (associative array)

//Use FacesToWireframe something like this
shapes.wireCube = {}; //Declare wireCube as an associative array
shapes.wireCube.Start = points.length;
points = points.concat(FacesToWireframe(cubeVerts, cubeFaces));
shapes.wireCube.Vertices = points.length - shapes.wireCube.Start;

//Don't forget to set up colours for your points...

Converting Faces to Triangles for Solid Drawings

Objects defined as faces cannot be directly translated to a vertex buffer unless the faces are all triangles already. While this would be ideal, is isn't always true. However, the faces described are easy to break into triangles because they are convex: pick any vertex on the face to be common to all sub-triangles, then use it and pairs going around the face in order to define the sub-triangles. The first vertex will probably be OK, but if you plan to avoid long thin triangles, look for the vertex at tip of the most obtuse angle. The following pseudocode uses the first approach.

Pseudocode: FacesToTriangles
// FacesToWireframe
// Inputs:
//    vertices: array of vertices in the mesh in no particular order
//    facesArray: array of "faces" where each face is an array
//                of vertex indices (lookups) in the vertices array.
//                For all faces in the array, these vertices
//                should define a convex polygon in the same order,
//                either clockwise or counterclockwise
// Outputs:
//    returns an array of vertices that correctly draw a filled face
//    when drawn as primitive type TRIANGLES
function FacesToTriangles(vertices, facesArray)
{
//Declare a return array

//loop index i from [0 to facesArray length)
{
//lookup the vertex at face i index 0
//store it as v0

//loop index v from [1 to face i's length - 1)
{
//add v0 to the return array

//lookup the vertex at face i index v
//add it to the return array

//lookup the vertex at face i index v + 1
//add it to the return array
}

}
//return the return array
}

Combining Wireframes and Triangles On Screen

Unless they are well colored, lit or textured, solid drawings are not very interesting:

Figure 3:Boring, sigle color solid object. Details are hard to distinguish.

Until we learn some lighting or texturing, drawing the wireframe over top is very helpful:

Figure 4:Now the edges are visible, but so are the ones behind the cube!

But unless we enable depth testing, it is as though we have x-ray vision - we can see the lines right through the cube. This can be fixed by using depth testing.

Enable depth testing
//Place this where needed, usually in your init function where you set the clear color
gl.enable(gl.DEPTH_TEST);

Now this is a bit better:

Figure 5:Beautiful!!

Fixing Thin or Missing Lines

Your lines might seem a bit thin or disappear entirely from time to time if the mesh and triangles render at the same depth. This is definitely a problem for height maps in the next section. WebGL provides a special setting - the Polygon Offset - to move triangles a little farther away from the viewer so that lines and other surface effects are not hidden due to depth test imprecision. You can find the documentation here: polygonOffset()

Sample Polygon Offset Settings
//Place this clear color settings
gl.polygonOffset(1,1);

//enable or disable this setting as needed
gl.enable(gl.POLYGON_OFFSET_FILL);

Height Maps

There are many ways to generate basic geometry. You can systematically sample the surface of a sphere or cylinder. You can also make a 2D mesh of triangles and apply heights to the shared points, like this:

Figure 3: A heightmap - a triangle mesh in the XZ plane with added height values. In this example the heights come from a sin() functions applied to the x and z coordinates, but any function, 2D array of values or image could be used.

To generate the 2D mesh, all you need is coordinates for two opposing corners of the mesh, the number of divisions along each axis, and a pair of nested loops. The loop iterators define one corner of each subrectangle in the mesh. The other corners are calculated based on the desired mesh size, and two triangles are produced per square. The code is given in heightmapExercise.js and below:

Incomplete Code: make2DMesh
// make2DMesh
// Inputs:
//    xzMin: vec2 defining x and z minimum coordinates for mesh
//    xzMax: vec2 defining x and z maximum coordinates for mesh
//    xDivs: number of columns in x direction
//    zDivs: number of rows in z direction
function make2DMesh(xzMin, xzMax, xDivs, zDivs)
{
var ret = [];
if (xzMin.type != 'vec2' || xzMax.type != 'vec2')
{
throw "make2DMesh: either xzMin or xzMax is not a vec2";
}

var dim = subtract(xzMax, xzMin);
var dx = dim[0] / (xDivs);
var dz = dim[1] / (zDivs);

for (var x = xzMin[0]; x < xzMax[0]; x+=dx)
{
for (var z = xzMin[1]; z < xzMax[1]; z+=dz)
{
//Triangle 1
//  x,z
//   |\
//   |  \
//   |    \
//   |      \
//   |        \
//   |__________\
// x,z+dz      x+dx,z+dz
ret.push(vec4(   x, 0,   z,1));
ret.push(vec4(   x, 0,z+dz,1));
ret.push(vec4(x+dx, 0,z+dz,1));

//Triangle 2
//  x,z         x+dx,z
//    \----------|
//      \        |
//        \      |
//          \    |
//            \  |
//              \|
//           x+dx,z+dz
ret.push(vec4(   x, 0,   z,1));
ret.push(vec4(x+dx, 0,z+dz,1));
ret.push(vec4(x+dx, 0,   z,1));
}
}
return ret;
}

The function to supply the height to each vertex in this sample is:

y = sin(x*1.5)/3 + sin(z)/2

How to request external files like meshes and textures from Javascript

There's two methods you can use to request an external file. You can make it part of the HTML document by design - this is really easy with pictures. Or you can make an XMLHttpRequest - this is the approach to take with data files for things like 3D objects, and this is what the OBJ loader in this used in this lab does.

For images you place a file in your webpage's folder, or somewhere near it, and use an image tag to retrieve it, but make it hidden. The image should have a unique id so you can request it from Javascript.

Embedding a hidden image in HTML for Javascript use:
<img src="mypic.png" id="pic1" hidden />

With the image placed in the HTML file, you then request it by ID and you can work with it.

Requesting an image from HTML with Javascript:
var mypic = document.getElementById("pic1");

Then you work with the Javascript variable. When we use images for textures, we just hand them over directly to WebGL to read. If you want to use an image as a height map, though, you'll have to access the data yourself. Detailed instructions for reading the image's raw unpacked colour informaton and dimensions are found in this week's exercise.

The lab exercise will also show how to load the .OBJ file. The code is a bit odd because you have to check on the .OBJ file to see if it if fully loaded before you can request it's data, load it into buffers and draw it.

To Load Files from the Local Disk:

• Chrome: add --allow-file-access-from-files to the argument list when you launch it. Make sure Chrome is shut down completely before launch.
• On a Mac, start the Terminal - type terminal in to finder to find it
• type: open Google\ Chrome.app/ --args --allow-file-access-from-files

• Firefox seems to work for me, but if it doesn't for you, go to about:config, search for security.fileuri.strict_origin_policy and set it to false.

Brief description of .OBJ file format

The .OBJ file format uses a system like the lookups arrays we've been using, but on a bigger scale. .OBJ files mostly contain lists of three different types of coordinates: vertex (position), normal (surface orientation for lighting), and texture. These lists are individually indexed by a list of faces. Each face consists of at least three sets of indices. Each set must have a vertex index, and can optionally specify separate normal and texture indices. The file can also contain references to external .MTL material descriptions.

Here's a sample from the .OBJ file for a cube:

# Blender v2.69 (sub 0) OBJ File: ''
# www.blender.org
# formatted for readability by Alex Clarke

# object name
o Cube

# vertices
v 1.000000 -1.000000 -1.000000
v 1.000000 -1.000000 1.000000
v -1.000000 -1.000000 1.000000
v -1.000000 -1.000000 -1.000000
v 1.000000 1.000000 -0.999999
v 0.999999 1.000000 1.000001
v -1.000000 1.000000 1.000000
v -1.000000 1.000000 -1.000000

# normals
vn 0.000000 -1.000000 0.000000
vn 0.000000 1.000000 0.000000
vn 1.000000 -0.000000 0.000000
vn 0.000000 -0.000000 1.000000
vn -1.000000 -0.000000 -0.000000
vn 0.000000 0.000000 -1.000000
vn 1.000000 0.000000 0.000001

s off

# faces - the // in the middle represents missing texture coordinates
#       - this cube is ready to be lit, but not textured
f 1//1 2//1 4//1
f 5//2 8//2 6//2
f 1//3 5//3 2//3
f 2//4 6//4 3//4
f 3//5 7//5 4//5
f 5//6 1//6 8//6
f 2//1 3//1 4//1
f 8//2 7//2 6//2
f 5//7 6//7 2//7
f 6//4 7//4 3//4
f 7//5 8//5 4//5
f 1//6 4//6 8//6
Comments are in-line and are preceeded by a # symbol.

Using Blender to create j3di friendly .OBJ files

Our first OBJ loader is from an ~2009 Apple WebGL helper called j3di.js. Like Dr. Angel's helper code it contains initShader() routines and WebGL context creation routines. It can load simple models, but not materials or complex scenes. Though it provides everything we will need to finish up the labs for this class, you want a more capable one. You can find one as part of the materials for WebGL Programming Guide: Interactive 3D Graphics Programming with WebGL. This is the latest in a log running and respected serias, and is as close to an official WebGL guide as you can get.

The first trick is to find a file that Blender will read. Blender supports many file formats, so this isn't too hard, but sometimes things can go wrong. You might want to try TF3DM, which often lists Blender's native .blend format as a download option.

Next, start Blender and click anywhere to dismiss the welcome screen. Blender is big and complex, so don't expect a tutorial. All I will explain is how to load a model, clean it a bit and export a useful .OBJ file.

The default scene contains a cube. This is the very one shown above. If it isn't highlighted in orange, right click on it and press x. This will pop up a delete confirmation menu. Press Enter to confirm. You can use this method to clean unwanted stuff from models you load. Or, you can click on things in the Scene Graph in the upper right corner of the screen. Press shift to make a multiple selection.

Once you have removed everything you don't want, either import the mesh file you downloaded from here:

File | Import | list of supported types...

Or add one of the built in meshes. I recommend Suzanne, the Blender monkey.

You can position her by clicking and dragging on the arrows. You can rotate her about the view axis by pressing R and moving the mouse, or about one of the x, y or z axes by clicking them then pressing R. Scaling is done by pressing E.

Position you model facing up the positive Z axis near (0,0,0) so her face is pointing to you when you load her. You may want to use "View | Top" to help with this.

Now you are ready to export. If you only want to export a couple things, select them like you did for deletion - right click in the view, then shift click to add more; or left click in the scene graph and shift click to add more.

To export go to

File | Export | Wavefront (.obj)

From here choose:

• Selection only: if you made a specific export selection
• Triangulate faces: very important!
• Include Normals: only if you plan to light the model
• Include UVs: only if you plan to texture the model and it is set up for texturing.
• Forward: my instructions expect -Z forward which is the default for .obj and for OpenGL
• Scale: adjust this if the model is the wrong size - too large or too small is common.

Click Export .obj

• Include j3di.js in your HTML file
• Put the obj file in your lab folder.
• Make a global variable called var obj;
obj = loadObj(gl, "relative reference to .obj file");

//Apply transformations/colors if necessary
//draw obj
gl.drawElements(gl.TRIANGLES, obj.numIndices, gl.UNSIGNED_SHORT, 0);
}
//Note that at this point, you might need to rebind buffers to draw other
//objects - the vertex and elements arrays are those for the obj file.

• Add this function (called by render to bind the obj file's buffer
gl.bindBuffer(gl.ARRAY_BUFFER, obj.vertexObject);
gl.vertexAttribPointer(program.vPosition, 3, gl.FLOAT, gl.FALSE, 0, 0);
gl.enableVertexAttribArray(program.vPosition);

gl.disableVertexAttribArray(program.vColor);
gl.vertexAttrib4f(program.vColor, 0.8, 0.8, 0.8, 1.0); // specify colour as necessary

gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, obj.indexObject);
}

Assignment

Please use the files in Lab4.zip to for this lab assignment

Goals of this assignment:

Experience different ways to work with and specify a mesh.

Part 1: Write Translation Functions

Add JavaScript implementations of these translation function to the end of uofrGraphics.js in your Common folder. You may use the pseudocode in the labnotes as a guide. Test them with the indicated HTML/JS files. No changes to those files are necessary for this part of the assignment.

Once you have triangles and wires, the cube should look strange - the back of the wire frame is visible and some parts of the interior look green... depth testing has not been enabled. The shader has been instructed to draw the backs of triangles with green so you can't miss them.

• Add the appropriate code to enable depth testing near the gl.clearColor() call in init.

After you add wires, the height map will also look odd - there will be lines, but the will not be the clear dark lines you see in the lab notes. Instead the are thin and sometimes disappear. This is because the lines and the surface are at very similar depths. Use the polygon offset functions discussed in the lab notes to fix this.

• Add the appropriate code to enable polygon offset and change the default polygon offset parameters to something useful near the gl.clearColor() call in init.

Part 2: Hand Modeling

Make a copy of your working cubeExercise.html/js files and name them octahedron.html and octahedron.js. Given the points below, write the lookups to describe the 8 triangle faces of an octahedron.

var octaVerts =
[
vec4( 1, 0, 0, 1), //0
vec4(-1, 0, 0, 1),//1
vec4( 0, 1, 0, 1), //2
vec4( 0,-1, 0, 1),//3
vec4( 0, 0, 1, 1), //4
vec4( 0, 0,-1, 1),//5
];

As you are testing your model pay attention to the point order you use. By default, WebGL considers a triangle to be "Front Facing" if its vertices are counter-clockwise. The shader for this exercise has a front facing test that colours the backs of triangles bright green.

Change how the code works so that the eight sides of the octahedron are each a different colour. Do not use bright green or any colour that looks too much like it. Specify the colours with vertex arrays so the the octahedron can be drawn with only one command.

Part 3: Image Based Height Map

Open heightmapExercise.html in a web browser. You should see a spinning height map of a function like the one in the lab notes.

Modify heightmapExercise.html as follows:

• Select or design a black and white .png or .bmp image that is less than 255x255 in size. This will be your height map image. There is one called GL.png in the assignment folder if you don't know what to use.
• Add an image tag for this picture as described in the section on external files

Modify heightmapExercise.js as follows:

//----------------------------------------------------------------------------
// Gets RGBA data, width and height from  tag
// in an ImageData object.
//----------------------------------------------------------------------------
function getImageData(name)
{
//Get the image from HTML
var img = document.getElementById(name);

//Draw it into a hidden canvas
var canvas = document.createElement('canvas');
var context = canvas.getContext('2d');
canvas.width = img.width;
canvas.height = img.height;
context.drawImage(img, 0, 0 );

//Extract the data from the canvas
var myData = context.getImageData(0, 0, img.width, img.height);

return myData;
}

//----------------------------------------------------------------------------
// Gets the Red channel value for a specified x,y coordinate
// from an ImageData object
//----------------------------------------------------------------------------
function dataLookup(myImg, x, y)
{
return myImg.data[x*4 + y*myImg.width*4]/255.0;
}
• Before the call to make2DMesh, call getImageData with the ID of your image as the argument. Store the result as var myImg
• Change the call to make2DMesh so that it can use your image as data:
• remove the last two arguments
• add myImg as an argument
• add a number as an argument - this will be a scaling factor to make the map taller or shorter.
I suggest trying 1 to start.
• Change the make2DMesh to allow it use use your image's data:
• remove xDivs and yDivs as parameters from the function header
• add myImg and s as parameters to the function header to match the arguments you added in the last step
• add these lines to the beginning of the function
var ix; //x coordinate for looking up image data
var iz; //y coordinate for looking up image data
var xDivs = myImg.width - 1; // Make the mesh match the width of your image
var zDivs = myImg.height - 1; // Make the mesh match the height of your image
• iz should count iterations of the inner loop, and ix should count the outer
• Before the outer for loop add ix = 0;
• Before the inner for loop add iz = 0;
• At the end of the inner for loop add iz++;
• After the inner for loop add ix++;
• For every vec4 that is pushed to ret replace the 0 value in the y coordinate with a call like this dataLookup(myImg, ix, iz)*s
• use ix+1 instead of ix if dx appears in the vec4
• use iz+1 instead of iz if dz appears in that vec4
• Comment out these lines of code after the make2DMesh call or they will overwrite the y values of your image with the original wave function:
//for (var i = 0; i < points.length; i++)
//{
//	points[i][1] = Math.sin(points[i][0]*1.5)/3 + Math.sin(points[i][2]*1)/2;
//}