DEV Community

Andrei Lesnitsky
Andrei Lesnitsky

Posted on • Updated on

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

Top comments (0)