An Introduction to WebGL – with Three.js – So what is WebGL?



An Introduction to WebGL – with Three.js – So what is WebGL?

0 1


WebGL-As-Easy-As-One-Two-Three.js-Slides

WebGL: As Easy As One, Two, Three.js Slides

On Github jpdurham / WebGL-As-Easy-As-One-Two-Three.js-Slides

An Introduction to WebGL

with Three.js

Josh Durham

Aim

  • Cover the basic concpets you will need to get started on your WebGL and three.js journey.
  • Demonstate three.js is a powerful tool any web developer can use to render high-quality graphics.

So what is WebGL?

  • WebGL (Web Graphics Library) is a JavaScript API for rendering interactive 3D graphics and 2D graphics within any compatible web browser without the use of plug-ins
  • WebGL is integrated completely into all the web standards of the browser allowing GPU accelerated usage of physics and image processing and effects as part of the web page canvas
  • WebGL elements can be mixed with other HTML elements and composited with other parts of the page or page background
  • WebGL programs consist of control code written in JavaScript and shader code that is executed on a computer's Graphics Processing Unit (GPU)

Browser Support

The Graphics Pipeline Overview

The Graphics Pipeline

Step-By-Step

Vertex Transformation

In here a vertex is a set of attributes such as its location in space, as well as its color, normal, texture coordinates, amongst others. The inputs for this stage are the individual vertices attributes. Some of the operations performed by the fixed functionality at this stage are:

  • Vertex position transformation
  • Lighting computations per vertex
  • Generation and transformation of texture coordinates

Primitive Assembly and Rasterization

The inputs for this stage are the transformed vertices, as well as connectivity information. This latter piece of data tells the pipeline how the vertices connect to form a primitive. It is in here that primitives are assembled.

This stage is also responsible for clipping operations against the view frustum, and back face culling.

More on Primative Assembly and Rasterization

Rasterization determines the fragments, and pixel positions of the primitive. A fragment in this context is a piece of data that will be used to update a pixel in the frame buffer at a specific location. A fragment contains not only color, but also normals and texture coordinates, amongst other possible attributes, that are used to compute the new pixel's color. The output of this stage is twofold:

  • The position of the fragments in the frame buffer
  • The interpolated values for each fragment of the attributes computed in the vertex transformation stage

Fragment Texturing and Coloring

Interpolated fragment information is the input of this stage. A color has already been computed in the previous stage through interpolation, and in here it can be combined with a texel (texture element) for example. Texture coordinates have also been interpolated in the previous stage. Fog is also applied at this stage. The common end result of this stage per fragment is a color value and a depth for the fragment.

Raster Operations

The inputs of this stage are:

  • The pixels location
  • The fragments depth and color values

More on Rasterizing

The last stage of the pipeline performs a series of tests on the fragment, namely:

  • Scissor test
  • Alpha test
  • Stencil test
  • Depth test

If successful the fragment information is then used to update the pixel's value according to the current blend mode. Notice that blending occurs only at this stage because the Fragment Texturing and Coloring stage has no access to the frame buffer. The frame buffer is only accessible at this stage.

For more details, see openglinsights.csom.

Graphics Pipeline Recap

A quick example without three.js

Demo Breakdown

Initialization

function webGLStart() {
	var canvas = document.getElementById("lesson04-canvas");
	initGL(canvas);
	initShaders()
	initBuffers();
	
	gl.clearColor(0.0, 0.0, 0.0, 1.0);
	gl.enable(gl.DEPTH_TEST);
	
	tick();
}
		
var gl;

function initGL(canvas) {
	try {
	    gl = canvas.getContext("experimental-webgl");
	    gl.viewportWidth = canvas.width;
	    gl.viewportHeight = canvas.height;
	} catch (e) {
	}
	if (!gl) {
	    alert("Could not initialise WebGL, sorry :-(");
	}
}
		
    function getShader(gl, id) {
        var shaderScript = document.getElementById(id);
        if (!shaderScript) {
            return null;
        }

        var str = "";
        var k = shaderScript.firstChild;
        while (k) {
            if (k.nodeType == 3) {
                str += k.textContent;
            }
            k = k.nextSibling;
        }

        var shader;
        if (shaderScript.type == "x-shader/x-fragment") {
            shader = gl.createShader(gl.FRAGMENT_SHADER);
        } else if (shaderScript.type == "x-shader/x-vertex") {
            shader = gl.createShader(gl.VERTEX_SHADER);
        } else {
            return null;
        }

        gl.shaderSource(shader, str);
        gl.compileShader(shader);

        if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
            alert(gl.getShaderInfoLog(shader));
            return null;
        }

        return shader;
    }
			
			
var shaderProgram;

function initShaders() {
	var fragmentShader = getShader(gl, "shader-fs");
	var vertexShader = getShader(gl, "shader-vs");
	
	shaderProgram = gl.createProgram();
	gl.attachShader(shaderProgram, vertexShader);
	gl.attachShader(shaderProgram, fragmentShader);
	gl.linkProgram(shaderProgram);
	
	if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
	    alert("Could not initialise shaders");
	}
	
	gl.useProgram(shaderProgram);
	
	shaderProgram.vertexPositionAttribute = gl.getAttribLocation(shaderProgram, "aVertexPosition");
	gl.enableVertexAttribArray(shaderProgram.vertexPositionAttribute);
	
	shaderProgram.vertexColorAttribute = gl.getAttribLocation(shaderProgram, "aVertexColor");
	gl.enableVertexAttribArray(shaderProgram.vertexColorAttribute);
	
	shaderProgram.pMatrixUniform = gl.getUniformLocation(shaderProgram, "uPMatrix");
	shaderProgram.mvMatrixUniform = gl.getUniformLocation(shaderProgram, "uMVMatrix");
}
			
			
var mvMatrix = mat4.create();
var mvMatrixStack = [];
var pMatrix = mat4.create();

function mvPushMatrix() {
var copy = mat4.create();
mat4.set(mvMatrix, copy);
mvMatrixStack.push(copy);
}

function mvPopMatrix() {
if (mvMatrixStack.length == 0) {
    throw "Invalid popMatrix!";
}
mvMatrix = mvMatrixStack.pop();
}


function setMatrixUniforms() {
gl.uniformMatrix4fv(shaderProgram.pMatrixUniform, false, pMatrix);
gl.uniformMatrix4fv(shaderProgram.mvMatrixUniform, false, mvMatrix);
}


function degToRad(degrees) {
return degrees * Math.PI / 180;
}
			
			
var pyramidVertexPositionBuffer;
var pyramidVertexColorBuffer;
var cubeVertexPositionBuffer;
var cubeVertexColorBuffer;
var cubeVertexIndexBuffer;
		
	
function initBuffers() {
	pyramidVertexPositionBuffer = gl.createBuffer();
	gl.bindBuffer(gl.ARRAY_BUFFER, pyramidVertexPositionBuffer);
	var vertices = [
	    // Front face
	     0.0,  1.0,  0.0,
	    -1.0, -1.0,  1.0,
	     1.0, -1.0,  1.0,
	
	    // Right face
	     0.0,  1.0,  0.0,
	     1.0, -1.0,  1.0,
	     1.0, -1.0, -1.0,
	
	    // Back face
	     0.0,  1.0,  0.0,
	     1.0, -1.0, -1.0,
	    -1.0, -1.0, -1.0,
	
	    // Left face
	     0.0,  1.0,  0.0,
	    -1.0, -1.0, -1.0,
	    -1.0, -1.0,  1.0
	];
	gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
	pyramidVertexPositionBuffer.itemSize = 3;
	pyramidVertexPositionBuffer.numItems = 12;
		
	
        pyramidVertexColorBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, pyramidVertexColorBuffer);
        var colors = [
            // Front face
            1.0, 0.0, 0.0, 1.0,
            0.0, 1.0, 0.0, 1.0,
            0.0, 0.0, 1.0, 1.0,

            // Right face
            1.0, 0.0, 0.0, 1.0,
            0.0, 0.0, 1.0, 1.0,
            0.0, 1.0, 0.0, 1.0,

            // Back face
            1.0, 0.0, 0.0, 1.0,
            0.0, 1.0, 0.0, 1.0,
            0.0, 0.0, 1.0, 1.0,

            // Left face
            1.0, 0.0, 0.0, 1.0,
            0.0, 0.0, 1.0, 1.0,
            0.0, 1.0, 0.0, 1.0
        ];
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
        pyramidVertexColorBuffer.itemSize = 4;
        pyramidVertexColorBuffer.numItems = 12;
			
			
        cubeVertexPositionBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexPositionBuffer);
        vertices = [
            // Front face
            -1.0, -1.0,  1.0,
             1.0, -1.0,  1.0,
             1.0,  1.0,  1.0,
            -1.0,  1.0,  1.0,

            // Back face
            -1.0, -1.0, -1.0,
            -1.0,  1.0, -1.0,
             1.0,  1.0, -1.0,
             1.0, -1.0, -1.0,

            // Top face
            -1.0,  1.0, -1.0,
            -1.0,  1.0,  1.0,
             1.0,  1.0,  1.0,
             1.0,  1.0, -1.0,

            // Bottom face
            -1.0, -1.0, -1.0,
             1.0, -1.0, -1.0,
             1.0, -1.0,  1.0,
            -1.0, -1.0,  1.0,

            // Right face
             1.0, -1.0, -1.0,
             1.0,  1.0, -1.0,
             1.0,  1.0,  1.0,
             1.0, -1.0,  1.0,

            // Left face
            -1.0, -1.0, -1.0,
            -1.0, -1.0,  1.0,
            -1.0,  1.0,  1.0,
            -1.0,  1.0, -1.0
        ];
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
        cubeVertexPositionBuffer.itemSize = 3;
        cubeVertexPositionBuffer.numItems = 24;
			
        cubeVertexColorBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexColorBuffer);
        colors = [
            [1.0, 0.0, 0.0, 1.0], // Front face
            [1.0, 1.0, 0.0, 1.0], // Back face
            [0.0, 1.0, 0.0, 1.0], // Top face
            [1.0, 0.5, 0.5, 1.0], // Bottom face
            [1.0, 0.0, 1.0, 1.0], // Right face
            [0.0, 0.0, 1.0, 1.0]  // Left face
        ];
        var unpackedColors = [];
        for (var i in colors) {
            var color = colors[i];
            for (var j=0; j < 4; j++) {
                unpackedColors = unpackedColors.concat(color);
            }
        }
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(unpackedColors), gl.STATIC_DRAW);
        cubeVertexColorBuffer.itemSize = 4;
        cubeVertexColorBuffer.numItems = 24;
			
	cubeVertexIndexBuffer = gl.createBuffer();
	gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cubeVertexIndexBuffer);
	var cubeVertexIndices = [
	    0, 1, 2,      0, 2, 3,    // Front face
	    4, 5, 6,      4, 6, 7,    // Back face
	    8, 9, 10,     8, 10, 11,  // Top face
	    12, 13, 14,   12, 14, 15, // Bottom face
	    16, 17, 18,   16, 18, 19, // Right face
	    20, 21, 22,   20, 22, 23  // Left face
	];
	gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(cubeVertexIndices), gl.STATIC_DRAW);
	cubeVertexIndexBuffer.itemSize = 1;
	cubeVertexIndexBuffer.numItems = 36;
}
			
		
			
var rPyramid = 0;
var rCube = 0;
		
	
function drawScene() {
	gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);
	gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
	
	mat4.perspective(45, gl.viewportWidth / gl.viewportHeight, 0.1, 100.0, pMatrix);
	
	mat4.identity(mvMatrix);
	
	mat4.translate(mvMatrix, [-1.5, 0.0, -8.0]);
	
	mvPushMatrix();
	mat4.rotate(mvMatrix, degToRad(rPyramid), [0, 1, 0]);
	
	gl.bindBuffer(gl.ARRAY_BUFFER, pyramidVertexPositionBuffer);
	gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, pyramidVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
	
	gl.bindBuffer(gl.ARRAY_BUFFER, pyramidVertexColorBuffer);
	gl.vertexAttribPointer(shaderProgram.vertexColorAttribute, pyramidVertexColorBuffer.itemSize, gl.FLOAT, false, 0, 0);
	setMatrixUniforms();
	gl.drawArrays(gl.TRIANGLES, 0, pyramidVertexPositionBuffer.numItems);
	
	mvPopMatrix();
	
	
	mat4.translate(mvMatrix, [3.0, 0.0, 0.0]);
	
	mvPushMatrix();
	mat4.rotate(mvMatrix, degToRad(rCube), [1, 1, 1]);
	
	gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexPositionBuffer);
	gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, cubeVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
	
	gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexColorBuffer);
	gl.vertexAttribPointer(shaderProgram.vertexColorAttribute, cubeVertexColorBuffer.itemSize, gl.FLOAT, false, 0, 0);
	
	gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cubeVertexIndexBuffer);
	setMatrixUniforms();
	gl.drawElements(gl.TRIANGLES, cubeVertexIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0);
	
	mvPopMatrix();

}
			
		
	
var lastTime = 0;

function animate() {
	var timeNow = new Date().getTime();
	if (lastTime != 0) {
	    var elapsed = timeNow - lastTime;
	
	    rPyramid += (90 * elapsed) / 1000.0;
	    rCube -= (75 * elapsed) / 1000.0;
	}
	lastTime = timeNow;							
}
			
			
function tick() {
	requestAnimFrame(tick);
	drawScene();
	animate();
}
		

Shaders

    //vertex shader
    attribute vec3 aVertexPosition;
    attribute vec4 aVertexColor;

    uniform mat4 uMVMatrix;
    uniform mat4 uPMatrix;

    varying vec4 vColor;

    void main(void) {
        gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
        vColor = aVertexColor;
    }

		
    //fragment shader
    precision mediump float;

    varying vec4 vColor;

    void main(void) {
        gl_FragColor = vColor;
    }

			

Further Instruction writing WebGL directly

So that sucked

There has to be a better way...

There is with three.js!

three.js

3D Javascript Library

Renderers: WebGL, <canvas>, <svg>, CSS3D / DOM, and more

Scenes, Cameras, Geometry, 3D Model Loaders, Lights, Materials,Shaders, Particles, Animation, Math Utilities

                    
    <html>
        <head>
            <title>Basic Three.js App</title>
            <style>
            html, body { margin: 0; padding: 0; overflow: hidden; }
            </style>
        </head>
        <body>
            <script src="js/three.min.js"></script>
            <script>
            // Javascript will go here.
            </script>
        </body>
    </html>
                    
                
                    
<script>
    var scene = new THREE.Scene();
    var camera = new THREE.PerspectiveCamera( 75, window.innerWidth/window.innerHeight, 0.1, 1000 );

    var renderer = new THREE.WebGLRenderer();
    renderer.setSize( window.innerWidth, window.innerHeight );
    document.body.appendChild( renderer.domElement );

    var geometry = new THREE.BoxGeometry( 1, 1, 1 );
    // var material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
    var material = new THREE.MeshNormalMaterial()
    var cube = new THREE.Mesh( geometry, material );
    scene.add( cube );

    camera.position.z = 5;

    var render = function () {
        requestAnimationFrame( render );

        cube.rotation.x += 0.1;
        cube.rotation.y += 0.1;

        renderer.render(scene, camera);
    };

    render();
</script>
                    
                

The result

Object3D Transforms

mesh.position.x = 0
mesh.position.x = -100
mesh.scale.set(2,2,2)
mesh.rotation.y = Math.PI / 4
mesh.rotation.y = Math.PI * 5 / 4

Unit Circle

mesh.rotation.y = THREE.Math.degToRad(45);

Unit Circle

mesh.position.x = Math.cos( time );
mesh.position.y = Math.sin( time );

Cameras

cam = new THREE.PerspectiveCamera( fov, aspect, near, far )
cam = new THREE.PerspectiveCamera( fov, aspect, near, far )
cam = new THREE.PerspectiveCamera( fov, aspect, near, far )
camera.fov = 15
camera.fov = 60
camera.far = 1000
camera.far = 3000
camera = new THREE.OrthographicCamera( left, right, top, bottom, near, far );

Camera Controls

/three.js/examples/js/controls/OrbitControls.js

<script src="path/to/OrbitControls.js"></script>

controls = new THREE.OrbitControls( camera );

function render() {
  requestAnimationFrame( render );
  controls.update();
  renderer.render( scene, camera );
}
controls.enablePan = false;
controls.enableZoom = false;
controls.enableRotate = false;

controls.minDistance
controls.maxDistance

controls.minPolarAngle
controls.maxPolarAngle

Geometry

Geometry

var geo = new THREE.BoxGeometry( width, height, depth );
var geo = new THREE.SphereGeometry( 60, 24, 16 );
var geo = new THREE.CylinderGeometry( ... );
var geo = new THREE.TorusGeometry( ... );

Materials

Materials

var material = new THREE.MeshBasicMaterial({ ... });
var material = new THREE.MeshLambertMaterial({ ... });
var material = new THREE.MeshPhongMaterial({ ... });
var material = new THREE.MeshNormalMaterial({ ... });
var material = new THREE.MeshNormalMaterial({ ... });

Material Properties

shading: THREE.SmoothShading
shading: THREE.FlatShading
shading: THREE.FlatShading // face normals
shading: THREE.FlatShading // face normals
shading: THREE.SmoothShading // vertex normals
color: 0xaaaaaa
color: 0x3794cf
shininess: 40
shininess: 80
wireframe: true
transparent: true, opacity: 0.5

Texture Mapping

var loader = new THREE.TextureLoader();
var texture = loader.load("color-map.jpg");
map: texture
normalMap: texture
specularMap: texture
map: colorMap, specularMap: specMap, normalMap: normalMap
var material = new THREE.MeshPhongMaterial({
  color: 0xaaaaaa,
  specular: 0x333333,
  shininess: 15,
  map: colorMap,
  specularMap: specMap,
  normalMap: normalMap
});

Lights

light = new THREE.DirectionalLight( 0xdddddd, 0.8 );
light.position.set( -80, 80, 80 );
light.position.x = 80;
light.target.position = 160;
light.position.x = -80;
light = new THREE.DirectionalLight( 0xdddddd, 0.8 );
light = new THREE.DirectionalLight( 0xb4e7f2, 0.8 );
light = new THREE.DirectionalLight( 0xb4e7f2, 0.2 );
light = new THREE.DirectionalLight( 0xb4e7f2, 1.5 );
light = new THREE.DirectionalLight( 0xb4e7f2, 0.8 );
light = new THREE.PointLight( 0xb4e7f2, 0.8 );
light = new THREE.PointLight( 0xb4e7f2, 0.8 );
light = new THREE.SpotLight( 0xb4e7f2, 0.8 );
light.angle = Math.PI / 9;
light.angle = Math.PI / 5;
light.penumbra = 0.1;
light.penumbra = 0;
light.penumbra = 0.2;
light = new THREE.AmbientLight( 0x444444 );
light = new THREE.AmbientLight( 0x000000 );
light = new THREE.AmbientLight( 0x444444 );

Model Converter

OBJ to JSON converter python tool /three.js/utils/converters/obj/convert_obj_three.py

python convert_obj_three.py -i teapot.obj -o teapot.js

Model Loader

var loader = new THREE.JSONLoader();

loader.load("teapot.js", function( geometry, materials ) {
  material = new THREE.MultiMaterial( materials );
  mesh = new THREE.Mesh( geometry, material );
  scene.add( mesh );
});

Interaction

Physics

Physijs makes physics simulations just as easy to run as three.js. In fact, there are just five easy steps that must be taken to make a 3D scene come alive

Physijs is built on top of ammo.js (although there is also a cannon.js branch) and runs the physics simulation in a separate thread (via web worker) to avoid impacting in your application's performance and taking up your 3D rendering time

Questions or Comments?

Slides and code available at https://jpdurham.github.io/presentations

Josh Durham

An Introduction to WebGL with Three.js Josh Durham