WebGL 3D graphics programming in the web browser

WebGL - easy starter script

Description

This library wraps around the code for running gpu commands. With the gpu programming details abstracted the programmer can focus on drawing. All you have do to is load your position and colour data and look at the results.

About WebGL

The WebGL platform is for proramming the GPU. The GPU is designed to run programs on each element of a list of data. WebGL runs two programs; a vertex and fragment shader. The vertex shader sets the screen position and the fragment shader sets the colour. These two shaders need to compiled and loaded onto the gpu. We will be loading arrays of geometry points(x,y,z) and colour data(rgb) for our shaders.

Plotting 3D graphics is accomplished using 3D analytical geometry -matrix algebra. WebGL does not include the necessary geometry calculation functionality. You must supply the functions for calculating geometry yourself.

Web browser user input such as mouse clicks are handled in JavaScript as DOM window events. User input programming in WebGL is the same as any Web App.

About this WebGL library

You can use this library for creating interactive 3D geometries by simply entering points and colours. GLSL Shaders with matrix algebra compution, DOM mouse events, and animation programming are provided as well as a few helper tools/functions.

While coding graphics for WebGL you will encounter errors that are hard to find. One such error is mis-matched vertex and colour data. This library does a simple check of the length of the vertex and colour data arrays.

This library includes a pre-written Vertex Shader program which includes matrices for translation, scale, rotation. You do not have to code in the Shader. There are controls/settings for setting the translation, scale and rotation in JavaScript.

This library include a mouse click and drag event handler program for interactively controlling the Shader translation, scale and rotation matrices.

The WebGL platform uses a C like language for programming shaders called GLSL. The GLSL language includes many functions that are great for 3D graphics programming such as vec2() and vec3() for 2D(x,y) and 3D(x,y,z) data. In GLSL, you can easily convert a 2D vector (x,y) to 3D (x,y,z) as follows: vec3 my3dpoint = vec3(my3Dpoint, 0). This WebGL library includes a complementary JavaScript function called convert2Dto3D(). And there are complementary JavaScript functions for drawing using indices instead of arrays and drawing using draw types TRIANGLES, STRIP and FAN.

Using drawElements() you can draw a cube which requires 36 vertices with 24 of them repeated by entering just the 8 unique vertices. You would then supply a list of 36 index references to the 8 uniques vertices. It is a welcome relief for the job of data entry. But then with an array of only 8 vertices we can only supply a corresponding array of 8 colours. So the result would be a cube painted by corners instead of faces. We would not be able to distinguish the edges. We could not paint the faces separate colours. We can not colour by indices.

The genVertsByIndices.js function offers the capability of colouring by indices; you can supply 8 verts and 36 colours. The genVertsByIndices.js generates the 36 verts by a list of 12 indicies. With a list of 36 vertices you can supply a list of 36 corresponding colours and call drawArrays() as per usual.

Audience

Elementary Math and JavaScript proficiency is required to use this library. As long as you are capable of working with JavaScript Arrays[], plotting 3D(x,y,z) graph points and rgba[] colour encoding you can use this library. This is a small library with the basic feature set for 3D graphics. As a comparison, Threejs.org is a large library with a wide feature set. Using the wide feature set available in 3D programming requires an understanding of Scenes, Cameras, Lamps and much more. This library with just the basic feature set requires just an understanding of JavaScript arrays, the (x,y,z) axis system and the rgb colour encoding format.

If you are looking to get into 3D programming quickly and easily you will appreciate this offering. You only need to know about points and colours. If you are looking to gain experience in programming 3D features yourself, you will find this library includes only the necessary code that is neatly organized and easy to follow and build upon.

Usage

This library is made up of a number of files that must be loaded/included in your Web App in the correct order. The file names are prepending by sequentual ordering codes that start with the letters a-z followed by two digit numbers starting with 01. Most of the files/programs are extra functions that may be excluded if not used. The manditory files are canvasWebGL.js, load.js, backgroundViewport.js, dataEntry.js, drawFrame.js, and of course index.html. The program in canvasWebGL.js wraps the WebGL gpu commands for loading shader programs and data onto the gpu. The load.js program operates the canvasWebGL.js program and contains the data needed by the supplied Vertex Shader; namely the matrix settings for translation, scale and rotation. The Vertex and Fragment shader source code are included in the index.html file in "script" tags.

The extra unnecessary files/function add helper programs for working in WebGL such as print3D.js which prints a one dimensional array three at time. Printing three at time is convenient for viewing your three dimensional data points(x,y,z) and colours(red,green,blue).

You will be entering your data in the file named "dataentry.js". WebGL requires data in JS to be entered in one dimenional arrays. This library requires your vertex and fragment data arrays be named "verts" and "colours".

DEMO 1. Triangle

a01-triangle

//            
//       + 1,1
//      /|
//     / |
//    /  |
//   /   |
//  +----+
//  0,0   1,0
//
					

edit file: dataEntry.js

//  bottom left , right , top right
verts = [ 0,0,0 , 1,0,0 , 1,1,0 ]; 
//           red  , green , blue
colours = [ 1,0,0 , 0,1,0 , 0,0,1 ]; // red, green, blue
					

Notice the point of rotation is the origin(0,0,0).

DEMO 2. off center triangle rotation

a02-off-center-rotation

//               
//              + 1,1
//             /|
//            / |
//           /  |
//          /   |
//         +----+
//   0.5,0.5    1,0.5
//
					

edit file: dataEntry.js

verts = [ 0.5 , 0.5 , 0 , 
	  1   , 0.5 , 0 , 
	  1   , 1   , 0 ,   
];
					

Notice the point of rotation is the origin(0,0,0) on the screen and not point(0,0,0) on the triangle itself.

DEMO 3. full-screen-triangle

full-screen-triangle

//               
//              + 1,1
//             /|
//            / |
//           /  |
//          /   |
//         +----+
//     -1,-1    1,-1
//
					

edit file: dataEntry.js

verts = [  -1,-1, 0 ,
	    1,-1, 0 ,
	    1, 1, 0 ,   
];

					

The default configuration in this library has the scaling factor set to 0.5. To draw a full screen triangle we'll first set the scaling factor to 1. Enter the following code at the top or anywhere before the load.tsrp() function call:

edit file: drawFrame.js

load.scaleXYZ(1) 

Next we'll want to disable the animation. Comment out the animateXYrotation() call:

edit file: dataEntry.js

//animateXYrotation(); 

Lastly we'll change the background colour to white:

edit file: backgroundViewport.js

var background= [ 1 , 1 , 1 , 1 ]; 

The fourth number sets the alpha/opacity channel.

DEMO 4. gradient

Notice the gradient between the colours of the triangle points. WebGL automatically calculates linearly interpolated colours between the vertices of a triangle. Lets use this characteristic together with our full screen triangle to create a gradient background. Change the blue to white and the green to red for a red to white gradient.

gradient

//               
//              + 1,1
//             /|
//            / |
//           /  |
//          /   |
//         +----+
//     -1,-1    1,-1
//
					

edit file: dataEntry.js

var colours = [
	1 , 0 ,  0  ,  // red
	1 , 0 ,  0  ,  // red
	1 , 1 ,  1  ,  // white
];
					

DEMO 5. full-screen-gradient

Add another triangle to make a quad to so we can make a full screen gradient:

edit file: dataEntry.js

full-screen-gradient

//               
//    -1,1 +----+ 1,1
//         |   /|
//         |  / |
//         | /  |
//         |/   |
//         +----+
//     -1,-1    1,-1
//
var verts = [
	-1,-1, 0 ,
	 1,-1, 0 ,
	 1, 1, 0 ,

	-1,-1, 0 ,
	 1, 1, 0 ,
	-1, 1, 0 ,
];
var colours = [
	1 , 0 ,  0  ,  // red
	1 , 0 ,  0  ,  // red
	1 , 1 ,  1  ,  // white 

	1 , 0 ,  0  ,  // red
	1 , 1 ,  1  ,  // white 
	1 , 1 ,  1  ,  // white 
];

					

DEMO 6. cube-front-back

Notice the ordering of the vertices in all the examples above. We always ordered the vertices in the counter-clockwise direction. When drawing a cube you will wonder 'how does WebGL know to draw the front face in front of the back face?'. The answer is by the vertex ordering. WebGL follows the convention that front facing triangles are ordered counter-clockwise.

A mesh defined by only 3 points must be a plane. A plane has a front and back side. For a given ordering of clockwise or counter-counter clockwise on one side that same ordering will be be reversed on the opposite side. If your order of vertices 0,1,2 are counter-clockwise on the front side then if you look at that triangle from the other side those same verts 0,1,2 will be ordered clockwise. WebGL takes advantage of this intrinsic characteristic of a triangle/plane for determining what triangle faces are front facing and should be visible versus those that back facing, not visible and can be skipped. So when we enter the vertex data for the cube, we will take care to order the vertices of front facing triangles counter-clockwise and back facing triangles clockwise.

edit file: dataEntry.js

cube-front-back

// cube geometry
//      7                6
//	+--------------+
// 3    |           2  |
//  +---|----------+   |
//  |	|          |   |
//  |	|          |   |
//  |	|          |   |
//  |  	+--------------+ -Back z=+1
//  |  4           |    5
//  +--------------+ -Front z=-1 
//  0                1
// 
var verts = [
	-1,-1, -1 ,  // 0
	 1,-1, -1 ,  // 1
	 1, 1, -1 ,  // 2

	-1,-1, -1 ,  // 0
	 1, 1, -1 ,  // 2
	-1, 1, -1 ,  // 3

	-1,-1,  1 ,  // 4
	 1, 1,  1 ,  // 6
	 1,-1,  1 ,  // 5

	-1,-1,  1 ,  // 4
	-1, 1,  1 ,  // 7
	 1, 1,  1 ,  // 6

];
var vertsCnt = verts.length / 3;
var colours = [
	0.9 , 0.0 ,  0.0  ,  // red
	0.9 , 0.0 ,  0.0  ,  // red
	0.9 , 0.0 ,  0.0  ,  // red

	0.5 , 0.0 ,  0.0  ,  // red - dark
	0.5 , 0.0 ,  0.0  ,  // red - dark
	0.5 , 0.0 ,  0.0  ,  // red - dark

	0.0 , 0.9 ,  0.0  ,  // green
	0.0 , 0.9 ,  0.0  ,  // green
	0.0 , 0.9 ,  0.0  ,  // green

	0.0 , 0.5 ,  0.0  ,  // green - dark
	0.0 , 0.5 ,  0.0  ,  // green - dark
	0.0 , 0.5 ,  0.0  ,  // green - dark
];
					

DEMO 7. cube-front-back-indices

cube-front-back-indices

edit file: dataEntry.js

var vertsIn = [
	-1,-1, -1 ,  // 0
	 1,-1, -1 ,  // 1
	 1, 1, -1 ,  // 2
	-1, 1, -1 ,  // 3

	-1,-1,  1 ,  // 4
	 1,-1,  1 ,  // 5
	 1, 1,  1 ,  // 6
	-1, 1,  1 ,  // 7
];
var indices = [ 
	0,1,2 , 0,2,3 ,  // counter-clockwise
	4,7,6 , 4,6,5    // clockwise
];
var verts = [];
genVertsByIndices(indices, verts, vertsIn);
					

That was much easier data entry than the previous example.

DEMO 8. cube-front-back-2dto3d

cube-front-back-2dto3d

edit file: dataEntry.js

var verts2D = [
	-1,-1, // 0
	 1,-1, // 1
	 1, 1, // 2
	-1, 1, // 3
];
var verts3Dfront = [];
var verts3Dback = [];
convert2Dto3D(verts3Dfront, verts2D, -1);
convert2Dto3D(verts3Dback, verts2D, 1);

vertsIn = [];
vertsIn = verts3Dfront.concat(verts3Dback);

var indices = [ 
	0,1,2 , 0,2,3 ,  // counter-clockwise
	4,7,6 , 4,6,5    // clockwise
];

var verts = [];
genVertsByIndices(indices, verts, vertsIn);
					

Notice the final name of the verts array is "verts". This library expects your verts array to be named "verts" and colours array to be named "colours".

It is important to realize the potential for error with these helper functions. It worked because our front and back verts were ordered the same. The indices on the front plane were ordered 0,1,2,3 going counter-clockwise starting from the bottom left corner. The indices on the back side were numbered similarily 4,5,6,7 starting from the bottom left. When we supplied indices, we ordered our triangles counter-clockwise for the front plane which is the outside of the cube and clockwise for the back plane which is going to be the inside of the cube or the back face of that plane.

DEMO 9. cube-colours-shortcut

cube-colours-shortcut

Notice we painted each triangle the same colour. To do so we repeated the same colour in the colours array. We will want to paint all 3 verts of a triangle the same colour often. We have a helper function for a shortcut for entering the same colour 3X; genThriceColoursArray.js.

edit file: dataEntry.js

var coloursIn = [
	0.9 , 0.0 ,  0.0  ,  // red
	0.5 , 0.0 ,  0.0  ,  // red - dark
	0.0 , 0.9 ,  0.0  ,  // green
	0.0 , 0.5 ,  0.0  ,  // green - dark
];
var colours = [];
genThriceColoursArray(colours, coloursIn);
					

DEMO 10. cube-all-sides

Lets add all the rest of the sides.

edit file: dataEntry.js

cube-all-sides

var verts2D = [
	-1,-1, // 0
	 1,-1, // 1
	 1, 1, // 2
	-1, 1, // 3
];
var verts3Dfront = [];
var verts3Dback = [];
convert2Dto3D(verts3Dfront, verts2D, -1);
convert2Dto3D(verts3Dback, verts2D, 1);

var indices = [ 
 0,1,2 , 0,2,3 ,  // front : counter-clockwise
 4,7,6 , 4,6,5 ,  // back : clockwise
 1,5,6 , 1,6,2 ,  // right: counter-clockwise
 0,3,7 , 0,7,4 ,  // left: clockwise
 3,2,6 , 3,6,7 ,  // top: counter-clockwise
 0,4,5 , 0,5,1 ,  // bottom: clockwise
];

var verts = [];
genVertsByIndices(indices, verts, vertsIn);

var vertsCnt = verts.length / 3;

// colours
var coloursIn = [
	0.9 , 0.0 ,  0.0  ,  // front - red
	0.5 , 0.0 ,  0.0  ,  // red - dark

	0.0 , 0.9 ,  0.0  ,  // back - green
	0.0 , 0.5 ,  0.0  ,  // green - dark

	0.0 , 0.0 ,  0.9  ,  // right - blue 
	0.0 , 0.0 ,  0.5  ,  // blue - dark

	0.9 , 0.9 ,  0.0  ,  // left - green
	0.5 , 0.5 ,  0.0  ,  // green - dark

	0.0 , 0.9 ,  0.9  ,  // top - red
	0.0 , 0.5 ,  0.5  ,  // red - dark

	0.9 , 0.0 ,  0.9  ,  // bottom - green
	0.5 , 0.0 ,  0.5  ,  // green - dark
];

var colours = [];
genThriceColoursArray(colours, coloursIn);
					

DEMO 11. two-cubes

edit file: drawFrame.js

two-cubes

load.scaleXYZ(0.1);
//load.psvFactor = 1;
//load.tsrp();
drawFrame();

function drawFrame(){

	ngl.clear();

	load.xLoc = -0.2;
	load.zLoc =  0.5;
	load.tsrp();
	ngl.drawTriangles(vertsCnt);


	load.xLoc = +0.2;
	load.zLoc = -0.5;
	load.tsrp();
	ngl.drawTriangles(vertsCnt);

	animateXYrotation();
}
					

DEMO 12. two-cubes-perspective

two-cubes-perspective

Notice that the the zLoc was set the +0.5 for one cube and -0.5 for the other but they look exactly the same. They are the same size because we are currently using orthographic projection.

edit file: drawFrame.js

load.psvFactor = 1; 

Changing the perspective value creates the 3D effect we want.

DEMO 13. letter-A-front

letter-A-front

There is a gap. We should recalculate of points for a better fit. But lets just over lap since they are going to painted the same colour anyway.

DEMO 14. letter-A-front-overlap

letter-A-front-overlap

DEMO 15. letter-A-back

letter-A-back

DEMO 16. letter-A-sides

letter-A-sides

DEMO 17. letter-A-inside-sides

letter-A-inside-sides

We see from our work for creating the geometry for the letter A that we could benefit from a few more helper functions. Firstly, the function genThriceColursArray() should be updated to accept a count instead of defaulting to just 3 since we wanted to paint all the triangles on the front of the letter A the same colour. Another, would be an algorithm for autogenerating the side walls.

Final Notes

There are two versions of WebGL; version-1 & 2. This library is using WebGL version-1.

There are two directions of any axis. We usually choose the positive direction for example for the y axis to be up and negative to be down. And Rotation too may be be positive in the clockwise or counter-clockwise directions following the Right Hand or Left Hand rule. This library uses the Left Hand rule with the positive x going right, y going up and z going farther away.

References

tutorialspoint.com/webgl

webglfundamentals.org

Learning video series

I posted a video journal series/playlist while I went about learning WebGL using the above references. This code is very different from those.

channel: youtube.com/@nadeemelahi593

playlist: playlist?list=PLJUHxPFa4XqP0KREQlaBVST_VLUEFiitS