This is a modified version of a lab written by Alex Clarke at the University of Regina Department of Computer Science for their course, CS315. Any difficulties with the lab are no doubt due to my modifications, not to Alex's original!
This lab provides an introduction to WebGL programming. You will see:
Block Diagram of Typical Application / OpenGL / OS interactions.
OpenGL is the definitive cross-platform 3D graphics library. with a long history. While inspired by Microsoft's Direct3D (D3D) API, both evolved together from supporting fixed function graphics accelerators to supporting modern programmable hardware. The modern OpenGL style, and the one most like recent versions of (D3D), is known as Core Profile. A simplified predecessor to OpenGL's Core Profile features inspired OpenGL ES 2.0 - by far the most popular low level 3D API for mobile devices. OpenGL ES 2.0 was, in turn used as the basis for WebGL. The most recent version of WebGL is 2.0, which "exposes" (i.e., is written on top of) OpenGL ES 3.0.
By learning WebGL, you will learn a low level graphics programming style that is common to all modern OpenGL flavours. Especially if you stick to the same programming language, switching between versions is not too hard, and porting code can be easy if you modularize properly.
Getting to the point where you can begin writing OpenGL code is a bit trickier as the process differs from operating system to operating system. Fortunately there are cross platform libraries that can make your code extremely simple to port. The above diagram shows the general case for getting a modern "Core" OpenGL environment. Regardless of how you choose to set up :
When you write a WebGL program, many of these details are hidden from you. In fact, your WebGL calls may not even be executed by an OpenGL driver. On Macs, where OpenGL support is built-in to the OS, WebGL calls are translated directly into their OpenGL equivalents. On Windows, DirectX/D3D 9 or better has been built-in to the OS since Vista, so a special layer called ANGLE — developed by Google specifically for WebGL and used by Chrome, Firefox, IE11 and Edge — is used to translate WebGL API commands into equivalent D3D9 or D3D11 commands. On Linux, driver support is EXTREMELY important — most browsers only support nVidia's official drivers.
"Ideal" WebGL Application / OpenGL / OS interactions. |
Windows WebGL Application / "OpenGL" / OS interactions. |
ANGLE's translation from WebGL to D3D is good, but not perfect. If you want a "pure" OpenGL experience on Windows, and you think your graphics drivers are up to it, learn how to disable ANGLE at Geeks3D.
Try one of Dr. Angel's samples to see if WebGL is working for you. If it isn't, then try following the advice in Learning WebGL Lesson 0.
We will not go into detail on exactly how everything works in this week's lab. Instead we will focus on getting to the point where you can begin drawing, then use HTML UI elements to interact with the drawing.
Object-oriented programming was developed, in part, to aid with modularising the components of Model-View-Controller (MVC) architectured programs. Most modern user interface APIs are written in an object oriented language.
You will be structuring your programs to handle the three aspects of an MVC program. In the context of OpenGL/WebGL this will mean that your....Though WebGL is the API of choice for this course, we will always use textbook libraries to make certain parts of the program easier to write. Follow the instructions below to learn how to set up a textbook style WebGL program.
Before starting these instructions, make sure your browser is WebGL capable. I prefer Chrome for this lab, but Firefox will do. See the end of Section A for links to tests and instructions. Also, make sure you are comfortable with an HTML and Javascript source editor on your computer. If you want to start simple, I suggest TextWrangler on Mac or Notepad++ on Windows. If you want to try something more powerful, look at Brackets or NetBeans.
The HTML file for a WebGL application will link in necessary javascript files and resources. Some resources, such as small shaders, can be defined in-line. The following is a commented minimal HTML file for a textbook style WebGL application:
<!DOCTYPE html> <html> <head> <title>WebGL Template</title> <!-- This in-line script is a vertex shader resource Shaders can be linked from an external file as well. --> <script id="vertex-shader" type="x-shader/x-vertex"> // All vertex shaders require a position input or "attribute". // The name can be changed. // Other attributes can be added. attribute vec4 vPosition; void main() { // gl_Position is a built-in vertex shader output or "varying". // Its value should always be set by the vertex shader. // The simplest shaders just copy the position attribute straight to // gl_Position gl_Position = vPosition; } </script> <!-- This in-line script is a fragment shader resource. Shaders can be linked from an external file as well. --> <script id="fragment-shader" type="x-shader/x-fragment"> // Sets default precision for floats. // Required, since fragment shaders have no default precision. // Choices are lowp and mediump. precision mediump float; void main() { // gl_FragColor is a built-in fragment shader output // In general it should be set, but this is not required. // The default gl_FragColor is undefined gl_FragColor = vec4(0,0,0,1); } </script> <!-- These are external javascript files. The first three are the textbook libraries. The last one is your own javascript code. Make sure to change the name to match your javascript file. --> <script type="text/javascript" src="../Common/webgl-utils.js"></script> <script type="text/javascript" src="../Common/initShaders.js"></script> <script type="text/javascript" src="../Common/MV.js"></script> <script type="text/javascript" src="yourWebGLJavascript.js"></script> </head> <body> <!-- This is the canvas - the only HTML element that can render WebGL graphics. You can have more than one WebGL canvas on a web page, but that gets tricky. Stick to one per page for now. --> <canvas id="gl-canvas" width="512" height="512"> Oops ... your browser doesn't support the HTML5 canvas element </canvas> </body> </html>
Using your favorite editor, create a new HTML file called Lab1Demo.html in the Lab1 folder and paste the above code into it. Make a copy of this file, calling it template.html. In Lab1Demo.html change "yourWebGLJavascript" to be the name of the Javascript file ( Lab1Demo.js ) that we're going to create in the next step...
The javascript file will breathe life into your WebGL application. It sets up the WebGL rendering context, does the drawing and defines responses to various events. The simplest WebGL javascript program will set up the rendering context after the HTML has been loaded by defining an action for window.onload event. It can also do the rendering, if no animation is needed.
The following javascript defines a very minimalistic template WebGL program:
// This variable will store the WebGL rendering context var gl; window.onload = function init() { // Set up a WebGL Rendering Context in an HTML5 Canvas var canvas = document.getElementById("gl-canvas"); gl = WebGLUtils.setupWebGL(canvas); if (!gl) { alert("WebGL isn't available"); } // Configure WebGL // eg. - set a clear color // - turn on depth testing // Load shaders and initialize attribute buffers var program = initShaders(gl, "vertex-shader", "fragment-shader"); gl.useProgram(program); // Set up data to draw // Load the data into GPU data buffers // Associate shader attributes with corresponding data buffers // Get addresses of shader uniforms // Either draw as part of initialization //render(); // Or draw just before the next repaint event //requestAnimFrame(render()); }; function render() { // clear the screen // draw }
Using your favorite editor, create a new javascript file called Lab1Demo.js in the Lab1 folder and paste the above code into it. Make a copy of this file, calling it template.js
Load the HTML file ( Lab1Demo.html ) into your WebGL capable web browser. You will see nothing. This is the correct behaviour. Let's add the code to draw a blended colour triangle. We'll starting with the necessary javascript, which will yield a black triangle because we're still using the default template shaders. We'll then move to updating the shaders to get coloration.
In this part, add or change the highlighted code in Lab1Demo.js.
// This variable will store the WebGL rendering context var gl; window.onload = function init() { // Set up a WebGL Rendering Context in an HTML5 Canvas var canvas = document.getElementById("gl-canvas"); gl = WebGLUtils.setupWebGL(canvas); if (!gl) { alert("WebGL isn't available"); } // Configure WebGL // eg. - set a clear color // - turn on depth testing// This light gray clear colour will help you see your canvas gl.clearColor(0.9, 0.9, 0.9, 1.0);// Load shaders and initialize attribute buffers var program = initShaders(gl, "vertex-shader", "fragment-shader"); gl.useProgram(program); // Set up data to draw// Here, 2D vertex positions and RGB colours are loaded into arrays. var vertices = [ -0.5, -0.5, // point 1 0.5, -0.5, // point 2 0.0, 0.5 // point 2 ]; var colors = [ 1, 0, 0, // red 0, 1, 0, // green 0, 0, 1 // blue ];// Load the data into GPU data buffers// The vertex array is copied into one buffer var vertex_buffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, vertex_buffer); gl.bufferData(gl.ARRAY_BUFFER, flatten(vertices), gl.STATIC_DRAW); // The colour array is copied into another var color_buffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, color_buffer); gl.bufferData(gl.ARRAY_BUFFER, flatten(colors), gl.STATIC_DRAW);// Associate shader attributes with corresponding data buffersvar vPosition = gl.getAttribLocation(program, "vPosition"); gl.bindBuffer(gl.ARRAY_BUFFER, vertex_buffer); gl.vertexAttribPointer(vPosition, 2, gl.FLOAT, false, 0, 0); gl.enableVertexAttribArray(vPosition); var vColor = gl.getAttribLocation(program, "vColor"); gl.bindBuffer(gl.ARRAY_BUFFER, color_buffer); gl.vertexAttribPointer(vColor, 3, gl.FLOAT, false, 0, 0); gl.enableVertexAttribArray(vColor);// Get addresses of shader uniforms// None in this program...//Either draw as part of initializationrender();//Or draw just before the next repaint event //requestAnimFrame(render()); }; function render() { // clear the screen// Actually, the WebGL automatically clears the screen before drawing. // This command will clear the screen to the clear color instead of white. gl.clear(gl.COLOR_BUFFER_BIT);// draw// Draw the data from the buffers currently associated with shader variables // Our triangle has three vertices that start at the beginning of the buffer. gl.drawArrays(gl.TRIANGLES, 0, 3);}
Reload the HTML page. The result should look like this:
What your project should look like. If you see nothing, make sure your HTML is referencing your javascript file instead of the template's dummy one.
Now we're getting somewhere! The reason the triangle is black is that the shaders in our HTML file are hardcoded to paint everything black. We need to modify them to make them aware of the colour buffer. Make the following changes to the vertex and fragment shaders in Lab1Demo.html:
// All vertex shaders require a position input or "attribute". // The name can be changed. // Other attributes can be added. attribute vec4 vPosition;attribute vec4 vColor;// This "varying" output is interpolated between the vertices in the // primitive we are drawing before being sent to a varying with the // same name in the fragment shader varying vec4 varColor;void main() { // gl_Position is a built-in vertex shader output or "varying". // Its value should always be set by the vertex shader. // The simplest shaders just copy the position attribute straight to // gl_Position gl_Position = vPosition;varColor = vColor;}
// Sets default precision for floats. // Required, since fragment shaders have no default precision. // Choices are lowp and mediump. precision mediump float;varying vec4 varColor;void main() { // gl_FragColor is a built-in fragment shader output // In general it should be set, but this is not required. // The default gl_FragColor is undefinedgl_FragColor = varColor;}
Much more colourful. Yay!
Click here to see a page with the working WebGL result instead of a picture.
The triangle is OK, but let's modify the example to provide 3-D graphics. I do not expect you to understand everything that's going on here, but you should be able to understand enough to use this as the basis for your exercise this week.
attribute vec4 vPosition; attribute vec4 vNormal; attribute vec3 vColor; uniform mat4 p; uniform mat4 mv; uniform vec4 lightPosition; varying vec4 varColor; float shininess; vec4 ambientProduct; vec4 diffuseProduct; vec4 specularProduct; vec4 mvPosition; void main() { //initialize variables shininess = 5.0; ambientProduct = vec4(0.2 * vColor, 1); diffuseProduct = vec4(0.8 * vColor,1); specularProduct = vec4(0.3); //Transform the point mvPosition = mv*vPosition; gl_Position = p*mvPosition; //Set up Normal, Light, Eye and Half vectors vec3 N = normalize((mv*vNormal).xyz); vec3 L = normalize(lightPosition.xyz - mvPosition.xyz); if (lightPosition.w == 0.0) L = normalize(lightPosition.xyz); vec3 E = -normalize(mvPosition.xyz); vec3 H = normalize(L+E); //Calculate diffuse coefficient float Kd = max(dot(L,N), 0.0); //Calculate Blinn-Phong specular coefficient float Ks = pow(max(dot(N,H), 0.0), shininess); //Calculate lit colour for this pixel varColor = Kd * diffuseProduct + Ks * specularProduct + ambientProduct; }
var gl; //WebGL State Management //////////////////////// var mvIndex; //Shader Positioning Input var projIndex; //Shader Projection Input var mv; //Local Positioning Matrix var p; //Local Projection Matrix //Model Control Variables ///////////////////////// var colors = { 'red': new vec4(1, 0, 0, 1), 'blue': new vec4(0, 0, 1, 1), 'green': new vec4(0, 1, 0, 1), 'yellow': new vec4(1, 1, 0, 1), 'cyan': new vec4(0, 1, 1, 1), 'magenta': new vec4(1, 0, 1, 1) }; var objectColor = colors['red']; var rotAngle = 0; var rotChange = 0.5;
// Set up a WebGL Rendering Context in an HTML5 Canvas var canvas = document.getElementById("gl-canvas"); gl = WebGLUtils.setupWebGL(canvas); if (!gl) { alert("WebGL isn't available"); } // Configure WebGL gl.clearColor(0.9, 0.9, 0.9, 1.0); gl.enable(gl.DEPTH_TEST); // Load shaders and initialize attribute buffers var program = initShaders(gl, "vertex-shader", "fragment-shader"); gl.useProgram(program); // Get locations of transformation matrices from shader mvIndex = gl.getUniformLocation(program, "mv"); projIndex = gl.getUniformLocation(program, "p"); // Send a perspective transformation to the shader var p = perspective(50.0, canvas.width/ canvas.height, 0.5, 20.0); gl.uniformMatrix4fv(projIndex, gl.FALSE, flatten(p)); // Get locations of lighting uniforms from shader var uLightPosition = gl.getUniformLocation(program, "lightPosition"); // Set default light direction in shader. gl.uniform4f(uLightPosition, 0.0, 0.0, 1.0, 0.0); // Configure uofrGraphics object urgl = new uofrGraphics(); urgl.connectShader(program, "vPosition", "vNormal", "vColor"); // Begin an animation sequence requestAnimFrame(render);
// Clear the canvas with the clear color instead of plain white, // and also clear the depth buffer gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); // Draw previous model state // Notice we modularized the work a bit... PreRenderScene(); RenderStockScene(); RenderScene(); // Update the model and request a new animation frame rotAngle += rotChange; requestAnimFrame(render);
// Use this to perform view transforms or other tasks // that will affect both stock scene and detail scene function PreRenderScene() { // select a default viewing transformation // of a 20 degree rotation about the X axis // then a -5 unit transformation along Z mv = mat4(); mv = mult(mv, translate(0.0, 0.0, -5.0)); mv = mult(mv, rotate(20.0, 1, 0, 0)); //Allow variable controlled rotation around local y axis. mv = mult(mv, rotate(rotAngle, 0, 1, 0)); } // Function: RenderStockScene // Purpose: // Draw a stock scene that looks like a // black and white checkerboard function RenderStockScene() { var delta = 0.5; // define four vertices that make up a square. var v1 = vec4(0.0, 0.0, 0.0, 1.0); var v2 = vec4(0.0, 0.0, delta, 1.0); var v3 = vec4(delta, 0.0, delta, 1.0); var v4 = vec4(delta, 0.0, 0.0, 1.0); var color = 0; // define the two colors var color1 = vec4(0.9, 0.9, 0.9, 1); var color2 = vec4(0.05, 0.05, 0.05, 1); //Make a checkerboard var placementX = mv; var placementZ; placementX = mult(placementX, translate(-10.0 * delta, 0.0, -10.0 * delta)); for (var x = -10; x <= 10; x++) { placementZ = placementX; for (var z = -10; z <= 10; z++) { urgl.setDrawColour((color++) % 2 ? color1 : color2); gl.uniformMatrix4fv(mvIndex, gl.FALSE, flatten(placementZ)); urgl.drawQuad(v1, v2, v3, v4); placementZ = mult(placementZ, translate(0.0, 0.0, delta)); } placementX = mult(placementX, translate(delta, 0.0, 0.0)); } } // Function: RenderScene // Purpose: // Your playground. Code additional scene details here. function RenderScene() { // draw a red sphere inside a light blue cube // Set the drawing color to red urgl.setDrawColour(objectColor); // Move the "drawing space" up by the sphere's radius // so the sphere is on top of the checkerboard // mv is a transformation matrix. It accumulates transformations through // right side matrix multiplication. mv = mult(mv, translate(0.0, 0.5, 0.0)); // Rotate drawing space by 90 degrees around X so the sphere's poles // are vertical. Arguments are angle in degrees, // and a three part rotation axis with x, y and z components. mv = mult(mv, rotate(90.0, 1, 0, 0)); //Send the transformation matrix to the shader gl.uniformMatrix4fv(mvIndex, gl.FALSE, flatten(mv)); // Draw a sphere. // Arguments are Radius, Slices, Stacks // Sphere is centered around current origin. urgl.drawSolidSphere(0.5, 20, 20); // when we rotated the sphere earlier, we rotated drawing space // and created a new "local frame" // to move the cube up or down we now have to refer to the z-axis mv = mult(mv, translate(0.0, 0.0, 0.5)); //Send the transformation matrix to the shader gl.uniformMatrix4fv(mvIndex, gl.FALSE, flatten(mv)); // set the drawing color to light blue // arguments to vec4 are red, green, blue and alpha (transparancy) urgl.setDrawColour(vec4(0.5, 0.5, 1.0, 1.0)); // Draw the cube. // Argument refers to length of side of cube. // Cube is centered around current origin. urgl.drawSolidCube(1.0); }
<br /> <select id="colorMenu" > <option value="red">red</option> <option value="blue">blue</option> <option value="green">green</option> <option value="yellow">yellow</option> <option value="cyan">cyan</option> <option value="magenta">magenta</option> </select>
// You can't work with HTML elements until the page is loaded, // So init is a good place to set up event listeners setupEventListeners();
function setupEventListeners() { //Request an HTML element var m = document.getElementById("colorMenu"); //Setup a listener - notice we are defining an anonymous function from inside a function m.onchange = function(event) { //acquire the menu entry number var index = m.selectedIndex; //learn the value of the selected option. This need not match the text... var colorName = m.options[index].value; //change the object color by looking up the value in our associate array objectColor = colors[colorName]; } }
If your result looks and works like this, congratulations!
You are now ready to begin the exercise.
Make sure you learn how to publish your work before proceeding.
Good luck!
If your program runs correctly, you might be wondering how exactly the picture is drawn. That is, you might want to understand how each function works. Well, you do not have to worry too much about this in the first lab. Over the rest of the semester we will go through these subjects one-by-one in detail. Of course, we will learn some even more advanced functions and features too.
Part of submitting your work for some labs will be to publish your results on your pages at people.emich.edu. If you worked with people.emich.edu yet, there's a good mini-guide to using FileZilla to copy your html and js files onto the web server there. You'll have to duplicate your lab's directory structure on people.emich.edu (i.e., create a Lab1 and Common directory and place your .html and .js files in Lab1, etc.)
To be completed and submitted to Canvas by the beginning of class one week after this lab.
Try to make these changes to the RenderScene function. This is a taste of things to come. For help with the function calls read the comments carefully and consult with your lab instructor.
You have seen a lot of stuff so far and you may be confused about all the libraries used in this lab's program. Take a moment to learn about them.