Building 3D Worlds in the Browser

James Milner

@JamesLMilner

jmilner@esriuk.com

loxodrome.io

The web was very different growing up...

The Web has Matured

 

The standards and technologies have changed and we build such experiences differently

  • Open GL ES 2.0 for the web

  • Allows us to create 2D & 3D graphics 

  • JavaScript API

  • Integrates into HTML page via canvas

  • No file formats etc.

  • No markup

 

Increasingly a common choice is WebGL

Why WebGL?

*NPAPI now disabled in Chrome 45

More Plugins = More Attack Vectors

Plugins are Frustrating

Credit: xkcd.com

Some are Platform Specific 

... but many of them were very valuable and served their purpose

A Plugin Free World

Source: Microsoft

HTML5 vs ActionScript

  • WebGL is a separate spec from HTML5 but requires canvas support

 

 

 

  • WebGL also depends Typed Arrays (ECMAScript 2016) : an array-like view of an underlying binary data buffer 

Transitioning to a WebGL World

What About Support?

"WebGL is already a standard"

Ricardo Cabello (Three.js)

WebGL is a Low Level API

"low-level means that WebGL commands are expressed in terms that map relatively directly to how a GPU actually works" - Robert Nyman

"what many people don't [realise] is that WebGL is a rasterization API, not a 3D API"

 

Greg Tavares

Constructing Things in WebGL

Moving Data Around: Buffers

// Vertices for a square
gl.bufferData( gl.ARRAY_BUFFER,
               new Float32Array([
                -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.STATIC_DRAW
             );

// Some few lines of code later

gl.drawArrays(gl.TRIANGLES, 0, squareVertexPositionBuffer.numItems);
  • Buffer - a block of memory allocated to storing a specific data

  • Pass vertices using Typed Arrays

  • Stateful and implicit; uses last buffer referenced

Alongside our WebGL JavaScript API we require shaders 

Shaders?

  • Shader - "a program that tells a computer how to draw something in a specific and unique way"

 

  • Vertex Shader - generates scene coordinates

  • Fragment Shader - provide colour for current pixel

Using Shaders 

Vertex and Fragment Shaders

  1. Create Shader  
  2. Set Source  
  3. Compile  
  1. Create Program  
  2. Attach Shaders 
  3. Link Program 
  4. Use Program

A Simplified Pipeline

Source: Mozilla

Introducing GLSL ES

Graphics Library Shader Language

<!-- Drawing a green square -->
<script id="2d-vertex-shader" type="x-shader/x-vertex">
    attribute vec2 a_position; 
    // Storage qualifer - type - variable name
     
    void main() {
      gl_Position = vec4(a_position, 0, 1); // X, Y, Z, W
      // gl_Position is a built in variable
    }
</script>
 
<script id="2d-fragment-shader" type="x-shader/x-fragment">
    void main() {
      gl_FragColor = vec4(0, 1, 0, 1);  //  R, G, B, A (Green)
      // gl_FragColor is a built in variable
    }
</script>

Vertex Shaders : Sorry What?

(Buffer)

(Screen)

Fragment Shaders: Again Please?

  • Storage qualifiers e.g. attributes, uniforms

  • Stored in a string, a script tag, or separate file

  • Typed, C like language

  • Has vectors and matrices as primitives

  • gl_ prefix dictates built in variables

GLSL? What is this sorcery in my web page?

Hurry up and show me some JavaScript!

<html>

<head>
<title>WebGL - Black and White Square</title>
<meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">

<script type="text/javascript" src="glMatrix-0.9.5.min.js"></script>

<script id="shader-fs" type="x-shader/x-fragment">
    precision mediump float;

    void main(void) {
        gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
    }
</script>

<script id="shader-vs" type="x-shader/x-vertex">
    attribute vec3 aVertexPosition;
    // more info on these matrices at: 
    http://www.opengl-tutorial.org/beginners-tutorials/tutorial-3-matrices/
    uniform mat4 uMVMatrix;
    uniform mat4 uPMatrix;

    void main(void) {
        gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
    }
</script>


<script type="text/javascript">

    var gl;
    function initGL(canvas) {
        // Initialise the WebGL context on the canvas element
        try {
            var gl = canvas.getContext("experimental-webgl");
            if (!gl) {
              gl = canvas.getContext("webgl");
            }
            gl.viewportWidth = canvas.width;
            gl.viewportHeight = canvas.height;
        } catch (e) {
        }
        if (!gl) {
            alert("Could not initialise WebGL, sorry :-(");
        }
    }


    function getShader(gl, id) {
        
        // Get the shader from the script tag
        var shaderScript = document.getElementById(id);
        if (!shaderScript) {
            return null; // No shader script with that id
        }

        var str = ""; //concatenate the whole shader together
        var k = shaderScript.firstChild;
        while (k) {
            if (k.nodeType == 3) { //text will have nodeType property will return 3.
                str += k.textContent;
            }
            k = k.nextSibling;
        }

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

        gl.shaderSource(shader, str); // Link type and source
        gl.compileShader(shader); // Compile 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(); // create shader program
        gl.attachShader(shaderProgram, vertexShader); // add vertex shader
        gl.attachShader(shaderProgram, fragmentShader); // add vertex shader
        gl.linkProgram(shaderProgram); // Link the two to program

        if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
            alert("Could not initialise shaders");
        }

        gl.useProgram(shaderProgram); // Use the shader program
    
        // Explicitly say which variable is the vertex position in vertex shader
        shaderProgram.vertexPositionAttribute = gl.getAttribLocation(shaderProgram, "aVertexPosition");
        gl.enableVertexAttribArray(shaderProgram.vertexPositionAttribute); // Enable it

        shaderProgram.pMatrixUniform = gl.getUniformLocation(shaderProgram, "uPMatrix");
        shaderProgram.mvMatrixUniform = gl.getUniformLocation(shaderProgram, "uMVMatrix");
    }


    var mvMatrix = mat4.create(); // glMatrix functions
    var pMatrix = mat4.create();

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


    var triangleVertexPositionBuffer;
    var squareVertexPositionBuffer;

    function initBuffers() {

        //Triangle
        triangleVertexPositionBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);
        var vertices = [
             0.0,  1.0,  0.0,
            -1.0, -1.0,  0.1,
             1.0, -1.0,  0.0
        ];
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
        triangleVertexPositionBuffer.itemSize = 3;
        triangleVertexPositionBuffer.numItems = 3;

        //Square
        squareVertexPositionBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
        vertices = [
             1.0,  1.0,  0.0,
            -1.0,  1.0,  0.0,
             1.0, -1.0,  0.0,
            -1.0, -1.0,  0.0
        ];
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
        squareVertexPositionBuffer.itemSize = 3;
        squareVertexPositionBuffer.numItems = 4;
    }


    function drawScene() {
        gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);
        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
        // Clears the color and depth buffers

        mat4.perspective(45, gl.viewportWidth / gl.viewportHeight, 0.1, 100.0, pMatrix);
        // Field of View = 45 , width-to-height ratio of canvas, min view distance, max view distance
        mat4.identity(mvMatrix);
        

        // DRAW THE TRIANGLE
        mat4.translate(mvMatrix, [-1.5, 0.0, -7.0]);
        gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);
        gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, triangleVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
        setMatrixUniforms();
        gl.drawArrays(gl.TRIANGLES, 0, triangleVertexPositionBuffer.numItems);
        
        // DRAW THE SQUARE
        mat4.translate(mvMatrix, [3.0, 0.0, 0.0]);
        gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
        gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, squareVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
        setMatrixUniforms();
        gl.drawArrays(gl.TRIANGLE_STRIP, 0, squareVertexPositionBuffer.numItems);
    }


    function webGLStart() {
        var canvas = document.getElementById("lesson01-canvas"); //Get Canvas
        initGL(canvas);
        initShaders();
        initBuffers();

        gl.clearColor(0.0, 0.0, 0.0, 1.0); //Black
        gl.enable(gl.DEPTH_TEST);

        drawScene();
    }

</script>


</head>

<body onload="webGLStart();">
    <a href="http://learningwebgl.com/blog/?p=28"><< Back to Lesson 1</a><br />

    <canvas id="lesson01-canvas" style="border: none;" width="500" height="500"></canvas>
</body>

<!-- Adapted from: http://learningwebgl.com/blog/?p=28 -->

</html>

This is pretty verbose... we have to be explicit so many things!

Frameworks Help us Move Faster

Things We're (Generally) Immediately Concerned With

Pure WebGL

Three.js, Babylon.js, etc.

Vertices

Buffers

Shaders

Dot Products

Cross Products

 

 

Camera

Scene

Objects

Lighting

 

 

Matrices

Clipspace Coordinates

Pixel Coordinates

Raw WebGL

TWGL

PhiloGL

 Three.js

Babylon.js

Scene.js

OSG.js

GLGE

CopperLicht

WebGL Studio

Increasing Abstraction

Enter JavaScript Libraries

"It  shouldn't be underestimated how much WebGL adoption has been driven by Three.js."

Steven Wittens

Let's see some examples

Obviously these are fairly trivial...

It's not just fantasy worlds benefiting from WebGL...

WebGL Powered Vector Map Tiles

WebGL is currently the weapon of choice for 3D Mapping on the web

Why 3D?

Queen Flight Visualisation

3D Edition!

Flight Simulator

Physics Intergration

  • Plugins are dying 
  • WebGL + HTML5 are powerful replacements
  • ... but WebGL is low level and verbose 
  • Frameworks can help (Three.js, Babylon.js)
  • We can now build immersive 3D apps 
  • They are not just limited to video games
  • WebGL 2 is in the works...

Thanks!

@JamesLMilner

jmilner@esriuk.com

WebGL Links

Content Links

Building 3D Worlds in the Browser

By James Milner

Building 3D Worlds in the Browser

A talk on building 3D worlds in the browser with WebGL (or more specifically abstraction libraries for WebGL!)

  • 1,872