Welcome to my second WebGL tutorial! This time around we’re going to take a look at how to get colour into the scene. It’s based on number 3 in the NeHe OpenGL tutorials.

Here’s what the lesson looks like when run on a browser that supports WebGL:
A static picture of this lesson's results

Click here and you’ll see the live WebGL version, if you’ve got a browser that supports it; here’s how to get one if you don’t.

More on how it all works below…

A quick warning: these lessons are targeted at people with a reasonable amount of programming knowledge, but no real experience in 3D graphics; the aim is to get you up and running, with a good understanding of what’s going on in the code, so that you can start producing your own 3D Web pages as quickly as possible. If you haven’t read the first tutorial already, you should do so before reading this one — here I will only explain the differences between the code for that one and the new code.

As before, there may be bugs and misconceptions in this tutorial. If you spot anything wrong, let me know in the comments and I’ll correct it ASAP.

There are two ways you can get the code for this example; just “View Source” while you’re looking at the live version, or if you use GitHub, you can clone it (and the other lessons) from the repository there. Either way, once you have the code, load it up in your favourite text editor and take a look.

Most of it should look pretty similar from the first tutorial. Running through from top to bottom, we:

  • Define vertex and fragment shaders, using HTML <script> tags with types "x-shader/x-vertex" and "x-shader/x-fragment"
  • Initialise a WebGL context in initGL
  • Load the shaders into a WebGL program object using getShader and initShaders.
  • Define the model-view matrix mvMatrix and the projection matrix pMatrix, along with the function setMatrixUniforms for pushing them over the JavaScript/WebGL divide so that the shaders can see them.
  • Load up buffers containing information about the objects in the scene using initBuffers
  • Draw the scene itself, in the appropriately-named drawScene.
  • Define a function webGLStart to set everything up in the first place
  • Finally, we provide the minimal HTML required to display it all.

The only things that have changed in this code from the first lesson are the shaders, initBuffers, and the drawScene function. In order to explain how the changes work, you need to know a little about the WebGL rendering pipeline. Here’s a diagram:

Simplified diagram of the WebGL rendering pipelineThe diagram shows, in a very simplified form, how the data passed to JavaScript functions in drawScene is turned into pixels displayed in the WebGL canvas on the screen. It only shows the steps needed to explain this lesson; we’ll look at more detailed versions in future lessons.

At the highest level, the process works like this: each time you call a function like drawArrays, WebGL processes the data that you have previously given it in the form of attributes (like the buffers we used for vertices in lesson 1) and uniform variables (which we used for the projection and the model-view matrices), and passes it along to the vertex shader.

It does this by calling the vertex shader once for each vertex, each time with the attributes set up appropriately for the vertex; the uniform variables are also passed in, but as their name suggests, they don’t change from call to call. The vertex shader does stuff with this data — in lesson 1, it applied the projection and model-view matrices so that the vertices would all be in perspective and moved around according to our current model-view state — and puts its results into things called varying variables. It can output a number of varying variables; one particular one is obligatory, gl_Position, which contains the coordinates of the vertex once the shader has finished messing around with it.

Once the vertex shader is done, WebGL does the magic required to turn the 3D image from these varying variables into a 2D image, and then it calls the fragment shader once for each pixel in the image. (In some 3D graphics systems you’ll hear fragment shaders referred to as pixel shaders for that reason.) Of course, this means that it’s calling the fragment shader for those pixels that don’t have vertices in them — that is, the ones in between the pixels on which the vertices wind up. For these, it fills in points into the positions between the vertices via a process called linear interpolation — for the vertex positions that make up our triangle, this process “fills in” the space delimited by the vertices with points to make a visible triangle. The purpose of the fragment shader is to return the colour for each of these interpolated points, and it does this in a varying variable called gl_FragColor.

Once the fragment shader is done, its results are messed around with a little more by WebGL (again, we’ll get into that in a future lesson) and they are put into the frame buffer, which is ultimately what is displayed on the screen.

Hopefully, by now it’s clear that the most important trick that this lesson teaches is how to get the colour for the vertices from the JavaScript code all the way over to the fragment shader, when we don’t have direct access from one to the other.

The way we do this is to make use of the fact that we can pass a number of varying variables out of the vertex shader, not just the position, and can then retrieve them in the fragment shader. So, we pass the colour to the vertex shader, which can then put it straight into a varying variable which the fragment shader will pick up.

Conveniently, this gives us gradients of colours for free. All varying variables set by the vertex shader are linearly interpolated when generating the fragments between vertices, not just the positions. Linear interpolation of the colour between the vertices gives us smooth gradients, like those you can see in the triangle in the image above.

Let’s look at the code; we’ll work through the changes from lesson 1. Firstly, the vertex shader. It has changed quite a lot, so here’s the new code:

  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;
  }

What this is saying is that we have two attributes — inputs that vary from vertex to vertex — called aVertexPosition and aVertexColor, two non-varying uniforms called uMVMatrix and uPMatrix, and one output in the form of a varying variable called vColor.

In the body of the shader, we calculate the gl_Position (which is implicitly defined as a varying variable for every vertex shader) in exactly the same way as we did in lesson 1, and all we do with the colour is pass it straight through from the input attribute to the output varying variable.

Once this has been executed for each vertex, the interpolation is done to generate the fragments, and these are passed on to the fragment shader:

  precision mediump float;

  varying vec4 vColor;

  void main(void) {
    gl_FragColor = vColor;
  }

Here, after the floating-point precision boilerplate, we take the input varying variable vColorcontaining the smoothly blended colour that has come out of the linear interpolation, and just return it immediately as the colour for this fragment — that is, for this pixel.

That’s all of the differences in the shaders between this lesson and the last. There are two other changes. The first is very small; in initShaders we are now getting references to two attributes rather than one; the extra lines are highlighted in red below:

  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");
  }

This code to get the attribute locations, which we glossed over to a certain degree in the first lesson, should now be pretty clear: they are how we get a reference to the attributes that we want to pass to the vertex shader for each vertex. In lesson 1, we just got the vertex position attribute. Now, obviously enough, we get the colour attribute as well.

The remainder of the changes in this lesson are in initBuffers, which now needs to set up buffers for both the vertex positions and the vertex colours, and in drawScene, which needs to pass both of these up to WebGL.

Looking at initBuffers first, we define new global variables to hold the colour buffers for the triangle and the square:

  var triangleVertexPositionBuffer;
  var triangleVertexColorBuffer;
  var squareVertexPositionBuffer;
  var squareVertexColorBuffer;

Then, just after we’ve created the triangle’s vertex position buffer, we specify its vertex colours:

  function initBuffers() {
    triangleVertexPositionBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);
    var vertices = [
         0.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);
    triangleVertexPositionBuffer.itemSize = 3;
    triangleVertexPositionBuffer.numItems = 3;

    triangleVertexColorBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexColorBuffer);
    var colors = [
        1.0, 0.0, 0.0, 1.0,
        0.0, 1.0, 0.0, 1.0,
        0.0, 0.0, 1.0, 1.0
    ];
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
    triangleVertexColorBuffer.itemSize = 4;
    triangleVertexColorBuffer.numItems = 3;

So, the values we provide for the the colours are in a list, one set of values for each vertex, just like the positions. However, there is one interesting difference between the two array buffers: while the vertices’ positions are specified as three numbers each, for X, Y and Z coordinates, their colours are specified as four elements each — red, green, blue and alpha. Alpha, if you’re not familiar with it, is a measure of opaqueness (0 is transparent, 1 totally opaque) and will be useful in later lessons. This change in the number of elements per item in the buffer necessitates a change to the itemSize that we associate with it.

Next, we do the the equivalent code for the square; this time, we’re using the same colour for every vertex, so we generate the values for the buffer using a loop:

    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;

    squareVertexColorBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexColorBuffer);
    colors = []
    for (var i=0; i < 4; i++) {
      colors = colors.concat([0.5, 0.5, 1.0, 1.0]);
    }
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
    squareVertexColorBuffer.itemSize = 4;
    squareVertexColorBuffer.numItems = 4;

Now we have all of the data for our objects in a set of four buffers, so the next change is to make drawScene use the new data. The new code is in red again, and should be easy to understand:

  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, -7.0]);
    gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);
    gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, triangleVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

    gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexColorBuffer);
    gl.vertexAttribPointer(shaderProgram.vertexColorAttribute, triangleVertexColorBuffer.itemSize, gl.FLOAT, false, 0, 0);

    setMatrixUniforms();
    gl.drawArrays(gl.TRIANGLES, 0, triangleVertexPositionBuffer.numItems);

    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);

    gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexColorBuffer);
    gl.vertexAttribPointer(shaderProgram.vertexColorAttribute, squareVertexColorBuffer.itemSize, gl.FLOAT, false, 0, 0);

    setMatrixUniforms();
    gl.drawArrays(gl.TRIANGLE_STRIP, 0, squareVertexPositionBuffer.numItems);
  }

And the next change… hang on, there is no next change! That was all that was necessary to add colour to our WebGL scene, and hopefully you are now also comfortable with the basics of shaders and how data is passed between them.

That’s it for this lesson — hopefully it was easier going than the first! If you have any questions, comments, or corrections, please do leave a comment below.

Next time, we’ll add code to animate the scene by rotating the triangle and the square.

<< Lesson 1Lesson 3 >>

Acknowledgments: working out exactly what was going on in the rendering pipeline was made much easier by reference to the OpenGL ES 2.0 Programming Guide, which Jim Pick recommended on his WebGL blog. As ever, I’m deeply in debt to NeHe for his OpenGL tutorial for the script for this lesson.

'WEB > WEBGL' 카테고리의 다른 글

jQuery Ripples  (0) 2018.03.07
gl.BLEND  (0) 2018.03.07
WebGL Lesson 1 – A triangle and a square  (0) 2018.03.03

jQuery Ripples Plugin

By the powers of WebGL, add a layer of water to your HTML elements which will ripple by cursor interaction!

Important: this plugin requires the WebGL extension OES_texture_float (and OES_texture_float_linear for a better effect) and works only with same-origin images (see this link for more information on using cross-origin requested images).

Click here for a demo and to see how to use it.

Usage

Include the script at the end of your page after including jQuery, or when you are using bundling tools such as Webpack or Browserify, simply import it into your bundle.

The quickest way to use this plugin on an element is to ensure that the element has a background-image set (currently only URLs are supported), then initialize the plugin as follows:

$(selector).ripples();

Optionally you can tweak the behavior and appearance by initializing it with options (See the options secton for the full list of options):

$(selector).ripples({
  dropRadius: ...,
  perturbance: ...,
  ...
});

The plugin also has several methods to programmatically add drops, show, hide or remove the effects among other things. See the methods section for more details.

Options

NameTypeDefaultDescription
imageUrlstringnullThe URL of the image to use as the background. If absent the plugin will attempt to use the value of the computed background-image CSS property instead. Data-URIs are accepted as well.
dropRadiusfloat20The size (in pixels) of the drop that results by clicking or moving the mouse over the canvas.
perturbancefloat0.03Basically the amount of refraction caused by a ripple. 0 means there is no refraction.
resolutioninteger256The width and height of the WebGL texture to render to. The larger this value, the smoother the rendering and the slower the ripples will propagate.
interactivebooltrueWhether mouse clicks and mouse movement triggers the effect.
crossOriginstring""The crossOrigin attribute to use for the affected image. For more information see MDN.

Methods

drop

Call $(selector).ripples('drop', x, y, radius, strength) to manually add a drop at the element's relative coordinates (x, y). radius controls the drop's size and strength the amplitude of the resulting ripple.

destroy

Call $(selector).ripples('destroy') to remove the effect from the element.

hide / show

Call .ripples('hide') and .ripples('show') to toggle the effect's visibility. Hiding it will also effectively pause the simulation.

pause / play

Call $(selector).ripples('pause') and .ripples('play') to toggle the simulation's state.

set

Call $(selector).ripples('set', name, value) to update properties of the effect. The properties that can be updated are:

  • dropRadius
  • perturbance
  • interactive
  • imageUrl (setting the image URL will update the background image used for the effect, but the background-image CSS property will be untouched)
  • crossOrigin (setting this won't have any effect until imageUrl is changed)

updateSize

The effect resizes automatically when the width or height of the window changes. When the dimensions of the element changes, you need to call $(selector).ripples('updateSize') to update the size of the effect accordingly.



https://github.com/sirxemic/jquery.ripples

'WEB > WEBGL' 카테고리의 다른 글

WebGL Lesson 2 – Adding colour  (0) 2018.03.07
gl.BLEND  (0) 2018.03.07
WebGL Lesson 1 – A triangle and a square  (0) 2018.03.03
  1. Texturing a png so alpha channel part is transparent?

    Hello I have a PNG with an alpha channel I would like it so the transparent parts of the image
    are transparent when I texture to a quad. Right now they just show up as white. Considering the results I'm getting I'm guessing its not as easy as giving the WebGL API a png with an alpha channel. If anyone knows how to do this I would love to hear how you did it.
  2. #2
    Senior Member
    Join Date
    May 2010
    Posts
    208

    Re: Texturing a png so alpha channel part is transparent?

    Did you remember to enable blending?

    gl.enable ( gl.BLEND ) ;

    Does your shader copy the alpha channel of the texture into the alpha channel of the output?
  3. #3
    Junior Member
    Join Date
    Sep 2010
    Posts
    15

    Re: Texturing a png so alpha channel part is transparent?

    Thanks for the reply. I do not have blend enabled. I will do that.

    My shader looks like this:

    #ifdef GL_ES
    precision highp float;
    #endif
    varying vec2 vTextureCoord;
    uniform sampler2D uSampler;
    void main(void)
    {
    gl_FragColor = texture2D(uSampler, vec2(vTextureCoord.s, vTextureCoord.t));
    }

    Is what I'm doing here copying the alpha channel to the output?
  4. #4
    Junior Member
    Join Date
    Sep 2010
    Posts
    15

    Re: Texturing a png so alpha channel part is transparent?

    Using the above shader and adding these two lines of code got parts of my sprite to be transparent exactly as I wanted.

    gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
    gl.enable(gl.BLEND);

     That was suprisingly easy!


'WEB > WEBGL' 카테고리의 다른 글

WebGL Lesson 2 – Adding colour  (0) 2018.03.07
jQuery Ripples  (0) 2018.03.07
WebGL Lesson 1 – A triangle and a square  (0) 2018.03.03

<< Lesson 0Lesson 2 >>

Welcome to my first WebGL tutorial! This first lesson is based on number 2 in the NeHe OpenGL tutorials, which are a popular way of learning 3D graphics for game development. It shows you how to draw a triangle and a square in a web page. Maybe that’s not terribly exciting in itself, but it’s a great introduction to the foundations of WebGL; if you understand how this works, the rest should be pretty simple…

Here’s what the lesson looks like when run on a browser that supports WebGL:
A static picture of this lesson's results

Click here and you’ll see the live WebGL version, if you’ve got a browser that supports it; here’s how to get one if you don’t.

More on how it all works below…

A quick warning: These lessons are targeted at people with a reasonable amount of programming knowledge, but no real experience in 3D graphics; the aim is to get you up and running, with a good understanding of what’s going on in the code, so that you can start producing your own 3D Web pages as quickly as possible. I’m writing these as I learn WebGL myself, so there may well be (and probably are) errors; use at your own risk. However, I’m fixing bugs and correcting misconceptions as I hear about them, so if you see anything broken then please let me know in the comments.

There are two ways you can get the code for this example; just “View Source” while you’re looking at the live version, or if you use GitHub, you can clone it (and future lessons) from the repository there.  Either way, once you have the code, load it up in your favourite text editor and take a look.  It’s pretty daunting at first glance, even if you’ve got a nodding acquaintance with, say, OpenGL.  Right at the start we’re defining a couple of shaders, which are generally regarded as relatively advanced… but don’t despair, it’s actually much simpler than it looks.

Like many programs, this WebGL page starts off by defining a bunch of lower-level functions which are used by the high-level code at the bottom.  In order to explain it, I’ll start at the bottom and work my way up, so if you’re following through in the code, jump down to the bottom.

You’ll see the following HTML code:

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

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

  <br/>
  <a href="http://learningwebgl.com/blog/?p=28">&lt;&lt; Back to Lesson 1</a><br />
</body>

This is the complete body of the page — everything else is in JavaScript (though if you got the code using “View source” you’ll see some extra junk needed by my website analytics, which you can ignore). Obviously we could put more normal HTML inside the <body> tags and build our WebGL image into a normal web page, but for this simple demo we’ve just got the links back to this blog post, and the <canvas> tag, which is where the 3D graphics live.   Canvases are new for HTML5 — they’re how it supports new kinds of JavaScript-drawn elements in web pages, both 2D and (through WebGL) 3D.  We don’t specify anything more than the simple layout properties of the canvas in its tag, and instead leave all of the WebGL setup code to a JavaScript function called webGLStart, which you can see is called once the page is loaded.

Let’s scroll up to that function now and take a look at it:

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

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

    drawScene();
  }

It calls functions to initialise WebGL and the shaders that I mentioned earlier, passing into the former the canvas element on which we want to draw our 3D stuff, and then it initialises some buffers using initBuffers; buffers are things that hold the details of the the triangle and the square that we’re going to be drawing — we’ll talk more about those in a moment. Next, it does some basic WebGL setup, saying that when we clear the canvas we should make it black, and that we should do depth testing (so that things drawn behind other things should be hidden by the things in front of them). These steps are implemented by calls to methods on a gl object — we’ll see how that’s initialised later. Finally, it calls the function drawScene; this (as you’d expect from the name) draws the triangle and the square, using the buffers.

We’ll come back to initGL and initShaders later on, as they’re important in understanding how the page works, but first, let’s take a look at initBuffers and drawScene.

initBuffers first; taking it step by step:

  var triangleVertexPositionBuffer;
  var squareVertexPositionBuffer;

We declare two global variables to hold the buffers. (In any real-world WebGL page you wouldn’t have a separate global variable for each object in the scene, but we’re using them here to keep things simple, as we’re just getting started.)

Next:

  function initBuffers() {
    triangleVertexPositionBuffer = gl.createBuffer();

We create a buffer for the triangle’s vertex positions. Vertices (don’t you just love irregular plurals?) are the points in 3D space that define the shapes we’re drawing. For our triangle, we will have three of them (which we’ll set up in a minute). This buffer is actually a bit of memory on the graphics card; by putting the vertex positions on the card once in our initialisation code and then, when we come to draw the scene, essentially just telling WebGL to “draw those things I told you about earlier”, we can make our code really efficient, especially once we start animating the scene and want to draw the object tens of times every second to make it move. Of course, when it’s just three vertex positions as in this case, there’s not too much cost to pushing them up to the graphics card — but when you’re dealing with large models with tens of thousands of vertices, it can be a real advantage to do things this way. Next:

    gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);

This line tells WebGL that any following operations that act on buffers should use the one we specify. There’s always this concept of a “current array buffer”, and functions act on that rather than letting you specify which array buffer you want to work with. Odd, but I’m sure there a good performance reasons behind it…

    var vertices = [
         0.0,  1.0,  0.0,
        -1.0, -1.0,  0.0,
         1.0, -1.0,  0.0
    ];

Next, we define our vertex positions as a JavaScript list. You can see that they’re at the points of an isosceles triangle with its centre at (0, 0, 0).

    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);

Now, we create a Float32Array object based on our JavaScript list, and tell WebGL to use it to fill the current buffer, which is of course our triangleVertexPositionBuffer. We’ll talk more about Float32Arrays in a future lesson, but for now all you need to know is that they’re a way of turning a JavaScript list into something we can pass over to WebGL for filling its buffers.

    triangleVertexPositionBuffer.itemSize = 3;
    triangleVertexPositionBuffer.numItems = 3;

The last thing we do with the buffer is to set two new properties on it. These are not something that’s built into WebGL, but they will be very useful later on. One nice thing (some would say, bad thing) about JavaScript is that an object doesn’t have to explicitly support a particular property for you to set it on it. So although the buffer object didn’t previously have itemSize and numItemsproperties, now it does. We’re using them to say that this 9-element buffer actually represents three separate vertex positions (numItems), each of which is made up of three numbers (itemSize).

Now we’ve completely set up the buffer for the triangle, so it’s on to the 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;
  }

All of that should be pretty obvious — the square has four vertex positions rather than 3, and so the array is bigger and the numItems is different.

OK, so that was what we needed to do to push our two objects’ vertex positions up to the graphics card. Now let’s look at drawScene, which is where we use those buffers to actually draw the image we’re seeing. Taking it step-by-step:

  function drawScene() {
    gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);

The first step is to tell WebGL a little bit about the size of the canvas using the viewport function; we’ll come back to why that’s important in a (much!) later lesson; for now, you just need to know that the function needs calling with the size of the canvas before you start drawing. Next, we clear the canvas in preparation for drawing on it:

    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

…and then:

    mat4.perspective(45, gl.viewportWidth / gl.viewportHeight, 0.1, 100.0, pMatrix);

Here we’re setting up the perspective with which we want to view the scene.  By default, WebGL will draw things that are close by the same size as things that are far away (a style of 3D known as orthographic projection).  In order to make things that are further away look smaller, we need to tell it a little about the perspective we’re using.  For this scene, we’re saying that our (vertical) field of view is 45°, we’re telling it about the width-to-height ratio of our canvas, and saying that we don’t want to see things that are closer than 0.1 units to our viewpoint, and that we don’t want to see things that are further away than 100 units.

As you can see, this perspective stuff is using a function from a module called mat4, and involves an intriguingly-named variable called pMatrix. More about these later; hopefully for now it is clear how to use them without needing to know the details.

Now that we have our perspective set up, we can move on to drawing some stuff:

    mat4.identity(mvMatrix);

The first step is to “move” to the centre of the 3D scene.  In OpenGL, when you’re drawing a scene, you tell it to draw each thing you draw at a “current” position with a “current” rotation — so, for example, you say “move 20 units forward, rotate 32 degrees, then draw the robot”, the last bit being some complex set of “move this much, rotate a bit, draw that” instructions in itself.  This is useful because you can encapsulate the “draw the robot” code in one function, and then easily move said robot around just by changing the move/rotate stuff you do before calling that function.

The current position and current rotation are both held in a matrix; as you probably learned at school, matrices can represent translations (moves from place to place), rotations, and other geometrical transformations.  For reasons I won’t go into right now, you can use a single 4×4 matrix (not 3×3) to represent any number of transformations in 3D space; you start with the identity matrix— that is, the matrix that represents a transformation that does nothing at all — then multiply it by the matrix that represents your first transformation, then by the one that represents your second transformation, and so on.   The combined matrix represents all of your transformations in one. The matrix we use to represent this current move/rotate state is called the model-view matrix, and by now you have probably worked out that the variable mvMatrix holds our model-view matrix, and the mat4.identity function that we just called sets it to the identity matrix so that we’re ready to start multiplying translations and rotations into it.  Or, in other words, it’s moved us to an origin point from which we can move to start drawing our 3D world.

Sharp-eyed readers will have noticed that at the start of this discussion of matrices I said “in OpenGL”, not “in WebGL”.  This is because WebGL doesn’t have this stuff built in to the graphics library. Instead, we use a third-party matrix library — Brandon Jones’s excellent glMatrix — plus some nifty WebGL tricks to get the same effect. More about that niftiness later.

Right, let’s move on to the code that draws the triangle on the left-hand side of our canvas.

    mat4.translate(mvMatrix, [-1.5, 0.0, -7.0]);

Having moved to the centre of our 3D space with by setting mvMatrix to the identity matrix, we start the triangle  by moving 1.5 units to the left (that is, in the negative sense along the X axis), and seven units into the scene (that is, away from the viewer; the negative sense along the Z axis).  (mat4.translate, as you might guess, means “multiply the given matrix by a translation matrix with the following parameters”.)

The next step is to actually start drawing something!

    gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);
    gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, triangleVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

So, you remember that in order to use one of our buffers, we call gl.bindBuffer to specify a current buffer, and then call the code that operates on it. Here we’re selecting our triangleVertexPositionBuffer, then telling WebGL that the values in it should be used for vertex positions. I’ll explain a little more about how that works later; for now, you can see that we’re using the itemSize property we set on the buffer to tell WebGL that each item in the buffer is three numbers long.

Next, we have:

    setMatrixUniforms();

This tells WebGL to take account of our current model-view matrix (and also the projection matrix, about which more later).   This is required because all of this matrix stuff isn’t built in to WebGL.  The way to look at it is that you can do all of the moving around by changing the mvMatrix variable you want, but this all happens in JavaScript’s private space.  setMatrixUniforms, a function that’s defined further up in this file, moves it over to the graphics card.

Once this is done, WebGL has an array of numbers that it knows should be treated as vertex positions, and it knows about our matrices.   The next step tells it what to do with them:

    gl.drawArrays(gl.TRIANGLES, 0, triangleVertexPositionBuffer.numItems);

Or, put another way, “draw the array of vertices I gave you earlier as triangles, starting with item 0 in the array and going up to the numItemsth element”.

Once this is done, WebGL will have drawn our triangle.   Next step, draw the square:

    mat4.translate(mvMatrix, [3.0, 0.0, 0.0]);

We start by moving our model-view matrix three units to the right.  Remember, we’re currently already 1.5 to the left and 7 away from the screen, so this leaves us 1.5 to the right and 7 away.  Next:

    gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
    gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, squareVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

So, we tell WebGL to use our square’s buffer for its vertex positions…

    setMatrixUniforms();

…we push over the model-view and projection matrices again (so that we take account of that last mvTranslate), which means that we can finally:

    gl.drawArrays(gl.TRIANGLE_STRIP, 0, squareVertexPositionBuffer.numItems);

Draw the points.  What, you may ask, is a triangle strip?  Well, it’s a strip of triangles :-)   More usefully, it’s a strip of triangles where the first three vertices you give specify the first triangle, then the last two of those vertices plus the next one specify the next triangle, and so on.  In this case, it’s a quick-and-dirty way of specifying a square.  In more complex cases, it can be a really useful way of specifying a complex surface in terms of the triangles that approximate it.

Anyway, once that’s done, we’ve finished our drawScene function.

  }

If you’ve got this far, you’re definitely ready to start experimenting.  Copy the code to a local file, either from GitHub or directly from the live version; if you do the latter, you need index.html and glMatrix-0.9.5.min.js. Run it up locally to make sure it works, then try changing some of the vertex positions above; in particular, the scene right now is pretty flat; try changing the Z values for the square to 2, or -3, and see it get larger or smaller as it moves back and forward.  Or try changing just one or two of them, and watch it distort in perspective.  Go crazy, and don’t mind me.  I’ll wait.

Right, now that you’re back, let’s take a look at the support functions that made all of the code we just went over possible. As I said before, if you’re happy to ignore the details and just copy and paste the support functions that come above initBuffers in the page, you can probably get away with it and build interesting WebGL pages (albeit in black and white — colour’s the next lesson).  But none of the details are difficult to understand, and by understanding how this stuff works you’re likely to write better WebGL code later on.

Still with me?  Thanks :-)   Let’s get the most boring of the functions out of the way first; the first one called by webGLStart, which is initGL.  It’s near the top of the web page, and here’s a copy for reference:

  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 :-( ");
    }
  }

This is very simple.  As you may have noticed, the initBuffers and drawScene functions frequently referred to an object called gl, which clearly referred to some kind of core WebGL “thing”.  This function gets that “thing”, which is called a WebGL context, and does it by asking the canvas it is given for the context, using a standard context name.  (As you can probably guess, at some point the context name will change from “experimental-webgl” to “webgl”; I’ll update this lesson and blog about it when that happens. Subscribe to the RSS feed if you want to know about that — and, indeed, if you want at-least-weekly WebGL news.) Once we’ve got the context, we again use JavaScript’s willingness to allow us to set any property we like on any object to store on it the width and height of the canvas to which it relates; this is so that we can use it in the code that sets up the viewport and the perspective at the start of drawScene. Once that’s done, our GL context is set up.

After calling initGLwebGLStart called initShaders. This, of course, initialises the shaders (duh ;-) .  We’ll come back to that one later, because first we should take a look at our model-view matrix, and the projection matrix I also mentioned earlier.  Here’s the code:

  var mvMatrix = mat4.create();
  var pMatrix = mat4.create();

So, we define a variable called mvMatrix to hold the model-view matrix and one called pMatrixfor the projection matrix, and then set them to empty (all-zero) matrices to start off with. It’s worth saying a bit more about the projection matrix here. As you will remember, we applied the glMatrix function mat4.perspective to this variable to set up our perspective, right at the start of drawScene. This was because WebGL does not directly support perspective, just like it doesn’t directly support a model-view matrix.  But just like the process of moving things around and rotating them that is encapsulated in the model-view matrix, the process of making things that are far away look proportionally smaller than things close up is the kind of thing that matrices are really good at representing.  And, as you’ve doubtless guessed by now, the projection matrix is the one that does just that.  The mat4.perspective function, with its aspect ratio and field-of-view, populated the matrix with the values that gave use the kind of perspective we wanted.

Right, now we’ve been through everything apart from the setMatrixUniforms function, which, as I said earlier, moves the model-view and projection matrices up from JavaScript to WebGL, and the scary shader-related stuff.  They’re inter-related, so let’s start with some background.

Now, what is a shader, you may ask?  Well, at some point in the history of 3D graphics they may well have been what they sound like they might be — bits of code that tell the system how to shade, or colour, parts of a scene before it is drawn.  However, over time they have grown in scope, to the extent that they can now be better defined as bits of code that can do absolutely anything they want to bits of the scene before it’s drawn.  And this is actually pretty useful, because (a) they run on the graphics card, so they do what they do really quickly and (b) the kind of transformations they can do can be really convenient even in simple examples like this.

The reason that we’re introducing shaders in what is meant to be a simple WebGL example (they’re at least “intermediate” in OpenGL tutorials) is that we use them to get the WebGL system, hopefully running on the graphics card, to apply our model-view matrix and our projection matrix to our scene without us having to move around every point and every vertex in (relatively) slow JavaScript.   This is incredibly useful, and worth the extra overhead.

So, here’s how they are set up.  As you will remember, webGLStart called initShaders, so let’s go through that step-by-step:

  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);

As you can see, it uses a function called getShader to get two things,  a “fragment shader” and a “vertex shader”, and then attaches them both to a WebGL thing called a “program”.  A program is a bit of code that lives on the WebGL side of the system; you can look at it as a way of specifying something that can run on the graphics card.  As you would expect, you can associate with it a number of shaders, each of which you can see as a snippet of code within that program; specifically, each program can hold one fragment and one vertex shader. We’ll look at them shortly.

    shaderProgram.vertexPositionAttribute = gl.getAttribLocation(shaderProgram, "aVertexPosition");
    gl.enableVertexAttribArray(shaderProgram.vertexPositionAttribute);

Once the function has set up the program and attached the shaders, it gets a reference to an “attribute”, which it stores in a new field on the program object called vertexPositionAttribute. Once again we’re taking advantage of JavaScript’s willingness to set any field on any object; program objects don’t have a vertexPositionAttribute field by default, but it’s convenient for us to keep the two values together, so we just make the attribute a new field of the program.

So, what’s the vertexPositionAttribute for? As you may remember, we used it in drawScene; if you look now back at the code that set the triangle’s vertex positions from the appropriate buffer, you’ll see that the stuff we did associated the buffer with that attribute. You’ll see what that means in a moment; for now, let’s just note that we also use gl.enableVertexAttribArray to tell WebGL that we will want to provide values for the attribute using an array.

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

The last thing initShaders does is get two more values from the program, the locations of two things called uniform variables. We’ll encounter them soon; for now, you should just note that like the attribute, we store them on the program object for convenience.

Now, let’s take a look at getShader:

  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;
  }

This is another one of those functions that is much simpler than it looks.  All we’re doing here is looking for an element in our HTML page that has an ID that matches a parameter passed in, pulling out its contents, creating either a fragment or a vertex shader based on its type (more about the difference between those in a future lesson) and then passing it off to WebGL to be compiled into a form that can run on the graphics card.  The code then handles any errors, and it’s done! Of course, we could just define shaders as strings within our JavaScript code and not mess around with extracting them from the HTML — but by doing it this way, we make them much easier to read, because they are defined as scripts in the web page, just as if they were JavaScript themselves.

Having seen this, we should take a look at the shaders’ code: 

<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;

  uniform mat4 uMVMatrix;
  uniform mat4 uPMatrix;

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

The first thing to remember about these is that they are not written in JavaScript, even though the ancestry of the language is clearly similar.  In fact, they’re written in a special shader language — called GLSL — that owes a lot to C (as, of course, does JavaScript). 

The first of the two, the fragment shader, does pretty much nothing; it has a bit of obligatory boilerplate code to tell the graphics card how precise we want it to be with floating-point numbers (medium precision is good because it’s required to be supported by all WebGL devices — highpfor high precision doesn’t work on all mobile devices), then simply specifies that everything that is drawn will be drawn in white.  (How to do stuff in colour is the subject of the next lesson.)

The second shader is a little more interesting.   It’s a vertex shader — which, you’ll remember, means that it’s a bit of graphics-card code that can do pretty much anything it wants with a vertex.  Associated with it, it has two uniform variables called uMVMatrix and uPMatrix.  Uniform variables are useful because they can be accessed from outside the shader — indeed, from outside its containing program, as you can probably remember from when we extracted their location in initShaders, and from the code we’ll look at next, where (as I’m sure you’ve realised) we set them to the values of the model-view and the projection matrices.  You might want to think of the shader’s program as an object (in the object-oriented sense) and the uniform variables as fields. 

Now, the shader is called for every vertex, and the vertex is passed in to the shader code as aVertexPosition, thanks to the use of the vertexPositionAttribute in the drawScene, when we associated the attribute with the buffer.  The tiny bit of code in the shader’s main routine just multiplies the vertex’s position by the model-view and the projection matrices, and pushes out the result as the final position of the vertex.

So, webGLStart called initShaders, which used getShader to load the fragment and the vertex shaders from scripts in the web page, so that they could be compiled and passed over to WebGL and used later when rendering our 3D scene.

After all that, the only remaining unexplained code is setMatrixUniforms, which is easy to understand once you know everything above :-)

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

So, using the references to the uniforms that represent our projection matrix and our model-view matrix that we got back in initShaders, we send WebGL the values from our JavaScript-style matrices.

Phew!  That was quite a lot for a first lesson, but hopefully now you (and I) understand all of the groundwork we’re going to need to start building something more interesting — colourful, moving, properly three-dimensional WebGL models. To find out more, read on for lesson 2.

<< Lesson 0Lesson 2 >>

Acknowledgments: obviously I’m deeply in debt to NeHe for his OpenGL tutorial for the script for this lesson, but I’d also like to thank Benjamin DeLillo and Vladimir Vukićević for their WebGL sample code, which I’ve investigated, analysed, probably completely misunderstood, and eventually munged together into the code on which I based this post :-) . Thanks also to Brandon Jones for glMatrix. Finally, thanks to James Coglan, who wrote the general-purpose Sylvester matrix library; the first versions of this lesson used it instead of the much more WebGL-centric glMatrix, so the current version would never have existed without it.

'WEB > WEBGL' 카테고리의 다른 글

WebGL Lesson 2 – Adding colour  (0) 2018.03.07
jQuery Ripples  (0) 2018.03.07
gl.BLEND  (0) 2018.03.07

+ Recent posts