In this lab, you will learn:
After the lab lecture, you have two weeks to modify the files in Lab4.zip to:
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.
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:
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:
// 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 }
// 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:
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...
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.
// 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 }
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.
//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!!
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()
//Place this clear color settings gl.polygonOffset(1,1); //enable or disable this setting as needed gl.enable(gl.POLYGON_OFFSET_FILL);
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:
// 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
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.
<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.
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.
With any XML requested file you either need to move your whole website to a real server, or make special changes to your web browser to permit loading external files from your local disk.
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//6Comments are in-line and are preceeded by a # symbol.
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.
Add | Mesh | 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:
Click Export .obj
obj = loadObj(gl, "relative reference to .obj file");
if (obj.loaded) { bindBuffersToShader(); //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.
function bindBuffersToShader() { gl.bindBuffer(gl.ARRAY_BUFFER, obj.vertexObject); gl.vertexAttribPointer(program.vPosition, 3, gl.FLOAT, gl.FALSE, 0, 0); gl.enableVertexAttribArray(program.vPosition); //repeat for normalObject and textureObject if your shader supports them. 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); }
Please use the files in Lab4.zip to for this lab assignment
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.
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.
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.
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:
Modify heightmapExercise.js as follows:
//---------------------------------------------------------------------------- // Gets RGBA data, width and height fromtag // 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; }
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
//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; //}