WebGL Month. Day 11. Reducing WebGL boilerplate

lesnitsky

Andrei Lesnitsky

Posted on July 11, 2019

WebGL Month. Day 11. Reducing WebGL boilerplate

Reducing boilerplate

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

Yesterday we've learned how to use multiple textures. This required a shader modification, as well as javascript, but this changes might be partially done automatically

There is a package glsl-extract-sync which can get the info about shader attributes and uniforms

Install this package with

npm i glsl-extract-sync
Enter fullscreen mode Exit fullscreen mode

šŸ“„ package.json

      "url-loader": "^2.0.1",
      "webpack": "^4.35.2",
      "webpack-cli": "^3.3.5"
+   },
+   "dependencies": {
+     "glsl-extract-sync": "0.0.0"
    }
  }

Enter fullscreen mode Exit fullscreen mode

Now let's create a helper function which will get all references to attributes and uniforms with help of this package

šŸ“„ src/gl-helpers.js

+ import extract from 'glsl-extract-sync';
+ 
  export function compileShader(gl, shader, source) {
      gl.shaderSource(shader, source);
      gl.compileShader(shader);
          img,
      );
  }
+ 
+ export function setupShaderInput(gl, program, vShaderSource, fShaderSource) {
+ 
+ }

Enter fullscreen mode Exit fullscreen mode

We need to extract info about both vertex and fragment shaders

šŸ“„ src/gl-helpers.js

  }

  export function setupShaderInput(gl, program, vShaderSource, fShaderSource) {
- 
+     const vShaderInfo = extract(vShaderSource);
+     const fShaderInfo = extract(fShaderSource);
  }

Enter fullscreen mode Exit fullscreen mode

šŸ“„ src/texture.js

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

  import textureImageSrc from '../assets/images/texture.jpg';
  gl.bindBuffer(gl.ARRAY_BUFFER, vertexPositionBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, vertexPosition, gl.STATIC_DRAW);

+ console.log(setupShaderInput(gl, program, vShaderSource, fShaderSource));
+ 
  const attributeLocations = {
      position: gl.getAttribLocation(program, 'position'),
      texCoord: gl.getAttribLocation(program, 'texCoord'),

Enter fullscreen mode Exit fullscreen mode

Only vertex shader might have attributes, but uniforms may be defined in both shaders

šŸ“„ src/gl-helpers.js

  export function setupShaderInput(gl, program, vShaderSource, fShaderSource) {
      const vShaderInfo = extract(vShaderSource);
      const fShaderInfo = extract(fShaderSource);
+ 
+     const attributes = vShaderInfo.attributes;
+     const uniforms = [
+         ...vShaderInfo.uniforms,
+         ...fShaderInfo.uniforms,
+     ];
  }

Enter fullscreen mode Exit fullscreen mode

Now we can get all attribute locations

šŸ“„ src/gl-helpers.js

          ...vShaderInfo.uniforms,
          ...fShaderInfo.uniforms,
      ];
+ 
+     const attributeLocations = attributes.reduce((attrsMap, attr) => {
+         attrsMap[attr.name] = gl.getAttribLocation(program, attr.name);
+         return attrsMap;
+     }, {});
  }

Enter fullscreen mode Exit fullscreen mode

and enable all attributes

šŸ“„ src/gl-helpers.js

          attrsMap[attr.name] = gl.getAttribLocation(program, attr.name);
          return attrsMap;
      }, {});
+ 
+     attributes.forEach((attr) => {
+         gl.enableVertexAttribArray(attributeLocations[attr.name]);
+     });
  }

Enter fullscreen mode Exit fullscreen mode

We should also get all uniform locations

šŸ“„ src/gl-helpers.js

      attributes.forEach((attr) => {
          gl.enableVertexAttribArray(attributeLocations[attr.name]);
      });
+ 
+     const uniformLocations = uniforms.reduce((uniformsMap, uniform) => {
+         uniformsMap[uniform.name] = gl.getUniformLocation(program, uniform.name);
+         return uniformsMap;
+     }, {});
  }

Enter fullscreen mode Exit fullscreen mode

and finally return attribute and uniform locations

šŸ“„ src/gl-helpers.js

          uniformsMap[uniform.name] = gl.getUniformLocation(program, uniform.name);
          return uniformsMap;
      }, {});
+ 
+     return {
+         attributeLocations,
+         uniformLocations,
+     }
  }

Enter fullscreen mode Exit fullscreen mode

Ok, let's get advantage of our new sweet helper

šŸ“„ src/texture.js

  gl.bindBuffer(gl.ARRAY_BUFFER, vertexPositionBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, vertexPosition, gl.STATIC_DRAW);

- console.log(setupShaderInput(gl, program, vShaderSource, fShaderSource));
+ const programInfo = setupShaderInput(gl, program, vShaderSource, fShaderSource);

- const attributeLocations = {
-     position: gl.getAttribLocation(program, 'position'),
-     texCoord: gl.getAttribLocation(program, 'texCoord'),
-     texIndex: gl.getAttribLocation(program, 'texIndex'),
- };
- 
- const uniformLocations = {
-     texture: gl.getUniformLocation(program, 'texture'),
-     otherTexture: gl.getUniformLocation(program, 'otherTexture'),
-     resolution: gl.getUniformLocation(program, 'resolution'),
- };
- 
- gl.enableVertexAttribArray(attributeLocations.position);
- gl.vertexAttribPointer(attributeLocations.position, 2, gl.FLOAT, false, 0, 0);
+ gl.vertexAttribPointer(programInfo.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);
+ gl.vertexAttribPointer(programInfo.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);
+ gl.vertexAttribPointer(programInfo.attributeLocations.texIndex, 1, gl.FLOAT, false, 0, 0);

  const vertexIndices = new Uint8Array([
      // left rect

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

      gl.activeTexture(gl.TEXTURE1);
      gl.bindTexture(gl.TEXTURE_2D, otherTexture);
-     gl.uniform1i(uniformLocations.otherTexture, 1);
+     gl.uniform1i(programInfo.uniformLocations.otherTexture, 1);

-     gl.uniform2fv(uniformLocations.resolution, [canvas.width, canvas.height]);
+     gl.uniform2fv(programInfo.uniformLocations.resolution, [canvas.width, canvas.height]);

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

Enter fullscreen mode Exit fullscreen mode

Looks quite like a cleanup šŸ˜Ž

One more thing that we use often are buffers.
Let's create a helper class

šŸ“„ src/GLBuffer.js

export class GLBuffer {
    constructor(gl, target, data) {

    }
}

Enter fullscreen mode Exit fullscreen mode

We'll need data, buffer target and actual gl buffer, so let's assign everything passed from outside and craete a gl buffer.

šŸ“„ src/GLBuffer.js

  export class GLBuffer {
      constructor(gl, target, data) {
- 
+         this.target = target;
+         this.data = data;
+         this.glBuffer = gl.createBuffer();
      }
  }

Enter fullscreen mode Exit fullscreen mode

We didn't assign a gl to instance because it might cause a memory leak, so we'll need to pass it from outside

Let's implement an alternative to a gl.bindBuffer

šŸ“„ src/GLBuffer.js

          this.data = data;
          this.glBuffer = gl.createBuffer();
      }
+ 
+     bind(gl) {
+         gl.bindBuffer(this.target, this.glBuffer);
+     }
  }

Enter fullscreen mode Exit fullscreen mode

and a convenient way to set buffer data

šŸ“„ src/GLBuffer.js

      bind(gl) {
          gl.bindBuffer(this.target, this.glBuffer);
      }
+ 
+     setData(gl, data, usage) {
+         this.data = data;
+         this.bind(gl);
+         gl.bufferData(this.target, this.data, usage);
+     }
  }

Enter fullscreen mode Exit fullscreen mode

Now let's make a data argument of constructor and add a usage argument to be able to do everything we need with just a constructor call

šŸ“„ src/GLBuffer.js

  export class GLBuffer {
-     constructor(gl, target, data) {
+     constructor(gl, target, data, usage) {
          this.target = target;
          this.data = data;
          this.glBuffer = gl.createBuffer();
+ 
+         if (typeof data !== 'undefined') {
+             this.setData(gl, data, usage);
+         }
      }

      bind(gl) {

Enter fullscreen mode Exit fullscreen mode

Cool, now we can replace texCoords buffer with our thin wrapper

šŸ“„ src/texture.js


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

  const canvas = document.querySelector('canvas');
  const gl = canvas.getContext('webgl');
  gl.linkProgram(program);
  gl.useProgram(program);

- const texCoords = new Float32Array([
+ const texCoordsBuffer = new GLBuffer(gl, gl.ARRAY_BUFFER, 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);
+ ]), gl.STATIC_DRAW);

  const texIndicies = new Float32Array([
      ...Array.from({ length: 4 }).fill(0), // left rect

  gl.vertexAttribPointer(programInfo.attributeLocations.position, 2, gl.FLOAT, false, 0, 0);

- gl.bindBuffer(gl.ARRAY_BUFFER, texCoordsBuffer);
+ texCoordsBuffer.bind(gl);
  gl.vertexAttribPointer(programInfo.attributeLocations.texCoord, 2, gl.FLOAT, false, 0, 0);

  gl.bindBuffer(gl.ARRAY_BUFFER, texIndiciesBuffer);

Enter fullscreen mode Exit fullscreen mode

Do the same for texIndices buffer

šŸ“„ src/texture.js

      ...createRect(0, 0, 1, 1), // right rect
  ]), gl.STATIC_DRAW);

- const texIndicies = new Float32Array([
+ const texIndiciesBuffer = new GLBuffer(gl, gl.ARRAY_BUFFER, 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);
+ ]), gl.STATIC_DRAW);

  const vertexPosition = new Float32Array([
      ...createRect(-1, -1, 1, 2), // left rect
  texCoordsBuffer.bind(gl);
  gl.vertexAttribPointer(programInfo.attributeLocations.texCoord, 2, gl.FLOAT, false, 0, 0);

- gl.bindBuffer(gl.ARRAY_BUFFER, texIndiciesBuffer);
+ texIndiciesBuffer.bind(gl);
  gl.vertexAttribPointer(programInfo.attributeLocations.texIndex, 1, gl.FLOAT, false, 0, 0);

  const vertexIndices = new Uint8Array([

Enter fullscreen mode Exit fullscreen mode

vertex positions

šŸ“„ src/texture.js

      ...Array.from({ length: 4 }).fill(1), // right rect
  ]), gl.STATIC_DRAW);

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

- gl.bindBuffer(gl.ARRAY_BUFFER, vertexPositionBuffer);
- gl.bufferData(gl.ARRAY_BUFFER, vertexPosition, gl.STATIC_DRAW);

  const programInfo = setupShaderInput(gl, program, vShaderSource, fShaderSource);

+ vertexPositionBuffer.bind(gl);
  gl.vertexAttribPointer(programInfo.attributeLocations.position, 2, gl.FLOAT, false, 0, 0);

  texCoordsBuffer.bind(gl);

Enter fullscreen mode Exit fullscreen mode

and index buffer

šŸ“„ src/texture.js

  texIndiciesBuffer.bind(gl);
  gl.vertexAttribPointer(programInfo.attributeLocations.texIndex, 1, gl.FLOAT, false, 0, 0);

- const vertexIndices = new Uint8Array([
+ const indexBuffer = new GLBuffer(gl, gl.ELEMENT_ARRAY_BUFFER, 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);
- gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, vertexIndices, gl.STATIC_DRAW);
+ ]), gl.STATIC_DRAW);

  Promise.all([
      loadImage(textureImageSrc),

      gl.uniform2fv(programInfo.uniformLocations.resolution, [canvas.width, canvas.height]);

-     gl.drawElements(gl.TRIANGLES, vertexIndices.length, gl.UNSIGNED_BYTE, 0);
+     gl.drawElements(gl.TRIANGLES, indexBuffer.data.length, gl.UNSIGNED_BYTE, 0);
  });

Enter fullscreen mode Exit fullscreen mode

Now we are able to work with shaders being more productive with less code!

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 11, 2019

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

Sign up to receive the latest update from our blog.

Related