Viewing and Projections
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:
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...
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.
// 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.
//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()
//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:
// 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 module 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 module 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.
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
- On Windows, make a shortcut and add the argument there, or follow these instructions from Stack Overflow
- 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.Extended Resources