WebGL Month. Day 10. Multiple textures

lesnitsky

Andrei Lesnitsky

Posted on July 10, 2019

WebGL Month. Day 10. Multiple textures

Multiple textures

This is a series of blog posts related to WebGL. New post will be available every day

GitHub stars
Twitter Follow

Join mailing list to get new posts right to your inbox

Source code available here

Built with

Git Tutor Logo


Hey šŸ‘‹ Welcome back to WebGL month.
We already know how to use a single image as a texture, but what if we want to render multiple images?

We'll learn how to do this today.

First we need to define another sampler2D in fragment shader

šŸ“„ src/shaders/texture.f.glsl

  precision mediump float;

  uniform sampler2D texture;
+ uniform sampler2D otherTexture;
  uniform vec2 resolution;

  vec4 inverse(vec4 color) {

Enter fullscreen mode Exit fullscreen mode

And render 2 rectangles instead of a single one. Left rectangle will use already existing texture, right ā€“ new one.

šŸ“„ src/texture.js

  gl.linkProgram(program);
  gl.useProgram(program);

- const vertexPosition = new Float32Array(createRect(-1, -1, 2, 2));
+ const vertexPosition = new Float32Array([
+     ...createRect(-1, -1, 1, 2), // left rect
+     ...createRect(-1, 0, 1, 2), // right rect
+ ]);
  const vertexPositionBuffer = gl.createBuffer();

  gl.bindBuffer(gl.ARRAY_BUFFER, vertexPositionBuffer);
  gl.enableVertexAttribArray(attributeLocations.position);
  gl.vertexAttribPointer(attributeLocations.position, 2, gl.FLOAT, false, 0, 0);

- const vertexIndices = new Uint8Array([0, 1, 2, 1, 2, 3]);
+ const vertexIndices = new Uint8Array([
+     // left rect
+     0, 1, 2, 
+     1, 2, 3, 
+     
+     // right rect
+     4, 5, 6, 
+     5, 6, 7,
+ ]);
  const indexBuffer = gl.createBuffer();

  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);

Enter fullscreen mode Exit fullscreen mode

We'll also need a way to specify texture coordinates for each rectangle, as we can't use gl_FragCoord any longer, so we need to define another attribute (texCoord)

šŸ“„ src/shaders/texture.v.glsl

  attribute vec2 position;
+ attribute vec2 texCoord;

  void main() {
      gl_Position = vec4(position, 0, 1);

Enter fullscreen mode Exit fullscreen mode

The content of this attribute should be coordinates of 2 rectangles. Top left is 0,0, width and height are 1.0

šŸ“„ src/texture.js

  gl.linkProgram(program);
  gl.useProgram(program);

+ const texCoords = new Float32Array([
+     ...createRect(0, 0, 1, 1), // left rect
+     ...createRect(0, 0, 1, 1), // right rect
+ ]);
+ const texCoordsBuffer = gl.createBuffer();
+ 
+ gl.bindBuffer(gl.ARRAY_BUFFER, texCoordsBuffer);
+ gl.bufferData(gl.ARRAY_BUFFER, texCoords, gl.STATIC_DRAW);
+ 
  const vertexPosition = new Float32Array([
      ...createRect(-1, -1, 1, 2), // left rect
      ...createRect(-1, 0, 1, 2), // right rect

Enter fullscreen mode Exit fullscreen mode

We also need to setup texCoord attribute in JS

šŸ“„ src/texture.js


  const attributeLocations = {
      position: gl.getAttribLocation(program, 'position'),
+     texCoord: gl.getAttribLocation(program, 'texCoord'),
  };

  const uniformLocations = {
  gl.enableVertexAttribArray(attributeLocations.position);
  gl.vertexAttribPointer(attributeLocations.position, 2, gl.FLOAT, false, 0, 0);

+ gl.bindBuffer(gl.ARRAY_BUFFER, texCoordsBuffer);
+ 
+ gl.enableVertexAttribArray(attributeLocations.texCoord);
+ gl.vertexAttribPointer(attributeLocations.texCoord, 2, gl.FLOAT, false, 0, 0);
+ 
  const vertexIndices = new Uint8Array([
      // left rect
      0, 1, 2, 

Enter fullscreen mode Exit fullscreen mode

and pass this data to fragment shader via varying

šŸ“„ src/shaders/texture.f.glsl

      );
  }

+ varying vec2 vTexCoord;
+ 
  void main() {
-     vec2 texCoord = gl_FragCoord.xy / resolution;
+     vec2 texCoord = vTexCoord;
      gl_FragColor = texture2D(texture, texCoord);

      gl_FragColor = sepia(gl_FragColor);

Enter fullscreen mode Exit fullscreen mode

šŸ“„ src/shaders/texture.v.glsl

  attribute vec2 position;
  attribute vec2 texCoord;

+ varying vec2 vTexCoord;
+ 
  void main() {
      gl_Position = vec4(position, 0, 1);
+ 
+     vTexCoord = texCoord;
  }

Enter fullscreen mode Exit fullscreen mode

Ok, we rendered two rectangles, but they use the same texture. Let's add one more attribute which will specify which texture to use and pass this data to fragment shader via another varying

šŸ“„ src/shaders/texture.v.glsl

  attribute vec2 position;
  attribute vec2 texCoord;
+ attribute float texIndex;

  varying vec2 vTexCoord;
+ varying float vTexIndex;

  void main() {
      gl_Position = vec4(position, 0, 1);

      vTexCoord = texCoord;
+     vTexIndex = texIndex;
  }

Enter fullscreen mode Exit fullscreen mode

So now fragment shader will know which texture to use

DISCLAMER: this is not the perfect way to use multiple textures in a fragment shader, but rather an example of how to acheive this

šŸ“„ src/shaders/texture.f.glsl

  }

  varying vec2 vTexCoord;
+ varying float vTexIndex;

  void main() {
      vec2 texCoord = vTexCoord;
-     gl_FragColor = texture2D(texture, texCoord);

-     gl_FragColor = sepia(gl_FragColor);
+     if (vTexIndex == 0.0) {
+         gl_FragColor = texture2D(texture, texCoord);
+     } else {
+         gl_FragColor = texture2D(otherTexture, texCoord);
+     }
  }

Enter fullscreen mode Exit fullscreen mode

tex indices are 0 for the left rectangle and 1 for the right

šŸ“„ src/texture.js

  gl.bindBuffer(gl.ARRAY_BUFFER, texCoordsBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, texCoords, gl.STATIC_DRAW);

+ const texIndicies = new Float32Array([
+     ...Array.from({ length: 4 }).fill(0), // left rect
+     ...Array.from({ length: 4 }).fill(1), // right rect
+ ]);
+ const texIndiciesBuffer = gl.createBuffer();
+ 
+ gl.bindBuffer(gl.ARRAY_BUFFER, texIndiciesBuffer);
+ gl.bufferData(gl.ARRAY_BUFFER, texIndicies, gl.STATIC_DRAW);
+ 
  const vertexPosition = new Float32Array([
      ...createRect(-1, -1, 1, 2), // left rect
      ...createRect(-1, 0, 1, 2), // right rect

Enter fullscreen mode Exit fullscreen mode

and again, we need to setup vertex attribute

šŸ“„ src/texture.js

  const attributeLocations = {
      position: gl.getAttribLocation(program, 'position'),
      texCoord: gl.getAttribLocation(program, 'texCoord'),
+     texIndex: gl.getAttribLocation(program, 'texIndex'),
  };

  const uniformLocations = {
  gl.enableVertexAttribArray(attributeLocations.texCoord);
  gl.vertexAttribPointer(attributeLocations.texCoord, 2, gl.FLOAT, false, 0, 0);

+ gl.bindBuffer(gl.ARRAY_BUFFER, texIndiciesBuffer);
+ 
+ gl.enableVertexAttribArray(attributeLocations.texIndex);
+ gl.vertexAttribPointer(attributeLocations.texIndex, 1, gl.FLOAT, false, 0, 0);
+ 
  const vertexIndices = new Uint8Array([
      // left rect
      0, 1, 2, 

Enter fullscreen mode Exit fullscreen mode

Now let's load our second texture image

šŸ“„ src/texture.js

  import { createRect } from './shape-helpers';

  import textureImageSrc from '../assets/images/texture.jpg';
+ import textureGreenImageSrc from '../assets/images/texture-green.jpg';

  const canvas = document.querySelector('canvas');
  const gl = canvas.getContext('webgl');
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
  gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, vertexIndices, gl.STATIC_DRAW);

- loadImage(textureImageSrc).then((textureImg) => {
+ Promise.all([
+     loadImage(textureImageSrc),
+     loadImage(textureGreenImageSrc),
+ ]).then(([textureImg, textureGreenImg]) => {
      const texture = gl.createTexture();

      gl.bindTexture(gl.TEXTURE_2D, texture);

Enter fullscreen mode Exit fullscreen mode

As we'll have to create another texture ā€“ we'll need to extract some common code to separate helper functions

šŸ“„ src/gl-helpers.js


      return p;
  }
+ 
+ export function createTexture(gl) {
+     const texture = gl.createTexture();
+     
+     gl.bindTexture(gl.TEXTURE_2D, texture);
+     
+     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.LINEAR);
+     gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
+ 
+     return texture;
+ }
+ 
+ export function setImage(gl, texture, img) {
+     gl.bindTexture(gl.TEXTURE_2D, texture);
+ 
+     gl.texImage2D(
+         gl.TEXTURE_2D,
+         0,
+         gl.RGBA,
+         gl.RGBA,
+         gl.UNSIGNED_BYTE,
+         img,
+     );
+ }

Enter fullscreen mode Exit fullscreen mode

šŸ“„ src/texture.js

      loadImage(textureImageSrc),
      loadImage(textureGreenImageSrc),
  ]).then(([textureImg, textureGreenImg]) => {
-     const texture = gl.createTexture();
- 
-     gl.bindTexture(gl.TEXTURE_2D, texture);
- 
-     gl.texImage2D(
-         gl.TEXTURE_2D,
-         0,
-         gl.RGBA,
-         gl.RGBA,
-         gl.UNSIGNED_BYTE,
-         textureImg,
-     );
- 
-     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.LINEAR);
-     gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
+ 

      gl.activeTexture(gl.TEXTURE0);
      gl.uniform1i(uniformLocations.texture, 0);

Enter fullscreen mode Exit fullscreen mode

Now let's use our newely created helpers

šŸ“„ src/texture.js

  import vShaderSource from './shaders/texture.v.glsl';
  import fShaderSource from './shaders/texture.f.glsl';
- import { compileShader, loadImage } from './gl-helpers';
+ import { compileShader, loadImage, createTexture, setImage } from './gl-helpers';
  import { createRect } from './shape-helpers';

  import textureImageSrc from '../assets/images/texture.jpg';
      loadImage(textureImageSrc),
      loadImage(textureGreenImageSrc),
  ]).then(([textureImg, textureGreenImg]) => {
+     const texture = createTexture(gl);
+     setImage(gl, texture, textureImg);

+     const otherTexture = createTexture(gl);
+     setImage(gl, otherTexture, textureGreenImg);

      gl.activeTexture(gl.TEXTURE0);
      gl.uniform1i(uniformLocations.texture, 0);

Enter fullscreen mode Exit fullscreen mode

get uniform location

šŸ“„ src/texture.js


  const uniformLocations = {
      texture: gl.getUniformLocation(program, 'texture'),
+     otherTexture: gl.getUniformLocation(program, 'otherTexture'),
      resolution: gl.getUniformLocation(program, 'resolution'),
  };


Enter fullscreen mode Exit fullscreen mode

and set necessary textures to necessary uniforms

to set a texture to a uniform you should specify

  • active texture unit in range [gl.TEXTURE0..gl.TEXTURE31] (number of texture units depends on GPU and can be retreived with gl.getParameter)
  • bind texture to a texture unit
  • set texture unit "index" to a sampler2D uniform

šŸ“„ src/texture.js

      setImage(gl, otherTexture, textureGreenImg);

      gl.activeTexture(gl.TEXTURE0);
+     gl.bindTexture(gl.TEXTURE_2D, texture);
      gl.uniform1i(uniformLocations.texture, 0);

+     gl.activeTexture(gl.TEXTURE1);
+     gl.bindTexture(gl.TEXTURE_2D, otherTexture);
+     gl.uniform1i(uniformLocations.otherTexture, 1);
+ 
      gl.uniform2fv(uniformLocations.resolution, [canvas.width, canvas.height]);

      gl.drawElements(gl.TRIANGLES, vertexIndices.length, gl.UNSIGNED_BYTE, 0);

Enter fullscreen mode Exit fullscreen mode

That's it, we can now render multiple textures

See you tomorrow šŸ‘‹


This is a series of blog posts related to WebGL. New post will be available every day

GitHub stars
Twitter Follow

Join mailing list to get new posts right to your inbox

Source code available here

Built with

Git Tutor Logo

šŸ’– šŸ’Ŗ šŸ™… šŸš©
lesnitsky
Andrei Lesnitsky

Posted on July 10, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related