Search

Categories

WebGL Using 2 or More Textures

This article is a continuation of WebGL Image Processing. If you haven’t read that I suggest you start there.

Now might be a good time to answer the question, “How do I use 2 more more textures?”

It’s pretty simple. Let’s go back a few lessons to our first shader that draw a single image and update it for 2 images.

The first thing we need to do is change our code so we can load 2 images. This is not really a WebGL thing, it’s a HTML5 JavaScript thing, but we might as well tackle it. Images are loaded asynchronously which can take a little getting used to.

There are basically 2 ways we could handle it. We could try to structure our code so that it runs with no textures and as the textures are loaded the program updates. We’ll save that method for a latter article.

In this case we’ll wait for all the images to load before we draw anything.

First let’s change the code that loads an image into a function. It’s pretty straight forward. It creates a new image object, sets the URL to load, and sets a callback to be called when the image finishes loading.

function loadImage(url, callback) {
  var image = new Image();
  image.src = url;
  image.onload = callback;
  return image;
}

Now let’s make a function that loads an array of urls and generates an array of images. First we set `imagesToLoad` to the number of images we’re going to load. Then we make the callback we pass to `loadImage` decrement `imagesToLoad`. When `imagesToLoad` goes to 0 all the images have been loaded and we pass the array of images to a callback.

function loadImages(urls, callback) {
  var images = [];
  var imagesToLoad = urls.length;

  // Called each time an image finished
  // loading.
  var onImageLoad = function() {
    --imagesToLoad;
    // If all the images are loaded call the callback.
    if (imagesToLoad == 0) {
      callback(images);
    }
  };

  for (var ii = 0; ii < imagesToLoad; ++ii) {
    var image = loadImage(urls[ii], onImageLoad);
    images.push(image);
  }
}

Now we call loadImages like this

function main() {
  loadImages([
    "resources/leaves.jpg",
    "resources/star.jpg",
  ], render);
}

Next we change the shader to use 2 textures. In this case we'll multiply 1 texture by the other.

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

// our textures
uniform sampler2D u_image0;
uniform sampler2D u_image1;

// the texCoords passed in from the vertex shader.
varying vec2 v_texCoord;

void main() {
   vec4 color0 = texture2D(u_image0, v_texCoord);
   vec4 color1 = texture2D(u_image1, v_texCoord);
   gl_FragColor = color0 * color1;
}
</script>

We need to create 2 WebGL texture objects.

  // create 2 textures
  var textures = [];
  for (var ii = 0; ii < 2; ++ii) {
    var texture = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, texture);

    // Set the parameters so we can render any size image.
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);

    // Upload the image into the texture.
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, images[ii]);

    // add the texture to the array of textures.
    textures.push(texture);
  }

WebGL has something called "texture units". You can think of it as an array of references to textures. You tell the shader which texture unit to use for each sampler.

  // lookup the sampler locations.
  var u_image0Location = gl.getUniformLocation(program, "u_image0");
  var u_image1Location = gl.getUniformLocation(program, "u_image1");

  ...

  // set which texture units to render with.
  gl.uniform1i(u_image0Location, 0);  // texture unit 0
  gl.uniform1i(u_image1Location, 1);  // texture unit 1

Then we have to bind a texture to each of those texture units.

  // Set each texture unit to use a particular texture.
  gl.activeTexture(gl.TEXTURE0);
  gl.bindTexture(gl.TEXTURE_2D, textures[0]);
  gl.activeTexture(gl.TEXTURE1);
  gl.bindTexture(gl.TEXTURE_2D, textures[1]);

The 2 images we're loading look like this

And here's the result if we multiply them together using WebGL.


click here to open in a separate window

Some things I should go over.

The simple way to think of texture units is something like this: All of the texture functions work on the "active texture unit". The "active texture unit" is just a global variable that's the index of the texture unit you want to work with. Each texture unit has 2 targets. The TEXTURE_2D target and the TEXTURE_CUBE_MAP target. Every texture function works with specified target on the current active texture unit. If you were to implement WebGL in JavaScript it would look something like this

var getContext = function() {
  var textureUnits = [];
  var activeTextureUnit = 0;

  var activeTexture = function(unit) {
    // convert the unit enum to an index.
    var index = unit - gl.TEXTURE0;
    // Set the active texture unit
    activeTextureUnit = index;
  };

  var bindTexture = function(target, texture) {
    // Set the texture for the target of the active texture unit.
    textureUnits[activeTextureUnit][target] = texture;
  };

  var texImage2D = function(target, ... args ...) {
    // Call texImage2D on the current texture on the active texture unit
    var texture = textureUnits[activeTextureUnit][target];
    texture.image2D(...args...);
  };

  // return the WebGL api
  return {
    activeTexture: activeTexture,
    bindTexture: bindTexture,
    texImage2D: texImage2D,
  }
};

The shaders take indices into the texture units. Hopefully that makes these 2 lines clearer.

  gl.uniform1i(u_image0Location, 0);  // texture unit 0
  gl.uniform1i(u_image1Location, 1);  // texture unit 1

One thing that to be aware of, when setting the uniforms you use indices for the texture units but when calling gl.activeTexture you have to pass in special constants gl.TEXTURE0, gl.TEXTURE1 etc. Fortunately the constants are consecutive so instead of this

  gl.activeTexture(gl.TEXTURE0);
  gl.bindTexture(gl.TEXTURE_2D, textures[0]);
  gl.activeTexture(gl.TEXTURE1);
  gl.bindTexture(gl.TEXTURE_2D, textures[1]);

We could have done this

  for (var ii = 0; ii < 2; ++ii) {
    gl.activeTexture(gl.TEXTURE0 + ii);
    gl.bindTexture(gl.TEXTURE_2D, textures[ii]);
  }

Hopefully this small step helps explain how to use mutliple textures in a single draw call in WebGL.

  • http://twitter.com/troy_mmx Troy Dawson

    wow, brief.exe flashback!

    I’m no DHTML guru, but the sequencing of:

    image.src = url;
    image.onload = callback;

    gives me the willies : )

    Recently I’ve been getting back into active mode again after laying out for several years and was happy to remember that you are the WebGL go-to guy! (I used to add random comments here a while back).

    Getting into it, I see WebGL is the OOP-wrapper that Apple should have put together in the 1997-2000 period, instead of their thin pass-through of the Conix code. There’s a story there, sigh.

    I think WebGL is going to get big mindshare this year as several important pieces are falling into place. Underneath it, we’ve got more mature rendering stacks and more market penetration of non-sucky GPUs. Bigger LCDs, too : )

    And above WebGL we’ve got better tooling. One of my biggest frustrations working in Unity3D was the dodgy debug cycle. Man that sucked. Micros~1 had a good thing going in the IDE area with their XNA/VS Express combo, but they actually killed it last year! Inconceivable! I’m still in denial/anger over that.

    I assume Google and others are working on capable IDEs for Dart and whatnot, but I’m really digging TypeScript’s approach, and thanks to an IE browser extension from Russia (!) I can debug my code in VS (F5, F10, shift-F11 FTW!)

    http://i.imgur.com/7BANzK0.png

    the beauty of this is that while I have to boot into goofy Windows 8, I can code in Visual Studio and deploy the content to Android, Mac, and iOS (the latter via Ejecta which recently added WebGL support).

    Everything’s really coming together now! Back in 1996 I was doing PowerVR coding on a 200Mhz PPro. PVR taking over the world has certainly surprised me, but it was a cool design back in the day.