DEV Community

Andrei Lesnitsky
Andrei Lesnitsky

Posted on • Updated on

WebGL Month. Simple animation

Day 13. Simple animation

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 to WebGL month.

All previous tutorials where based on static images, let's add some motion!

We'll need a simple vertex shader

πŸ“„ src/shaders/rotating-square.v.glsl

attribute vec2 position;
uniform vec2 resolution;

void main() {
    gl_Position = vec4(position / resolution * 2.0 - 1.0, 0, 1);
}

fragment shader

πŸ“„ src/shaders/rotating-square.f.glsl

precision mediump float;

void main() {
    gl_FragColor = vec4(1, 0, 0, 1);
}

New entry point

πŸ“„ index.html

    </head>
    <body>
      <canvas></canvas>
-     <script src="./dist/texture.js"></script>
+     <script src="./dist/rotating-square.js"></script>
    </body>
  </html>

πŸ“„ src/rotating-square.js

import vShaderSource from './shaders/rotating-square.v.glsl';
import fShaderSource from './shaders/rotating-square.f.glsl';

πŸ“„ webpack.config.js

      entry: {
          'week-1': './src/week-1.js',
          'texture': './src/texture.js',
+         'rotating-square': './src/rotating-square.js',
      },

      output: {

Get WebGL context

πŸ“„ src/rotating-square.js

  import vShaderSource from './shaders/rotating-square.v.glsl';
  import fShaderSource from './shaders/rotating-square.f.glsl';
+ 
+ const canvas = document.querySelector('canvas');
+ const gl = canvas.getContext('webgl');
+ 

Make canvas fullscreen

πŸ“„ src/rotating-square.js

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

+ const width = document.body.offsetWidth;
+ const height = document.body.offsetHeight;
+ 
+ canvas.width = width * devicePixelRatio;
+ canvas.height = height * devicePixelRatio;
+ 
+ canvas.style.width = `${width}px`;
+ canvas.style.height = `${height}px`;

Create shaders

πŸ“„ src/rotating-square.js

  import vShaderSource from './shaders/rotating-square.v.glsl';
  import fShaderSource from './shaders/rotating-square.f.glsl';
+ import { compileShader } from './gl-helpers';

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

  canvas.style.width = `${width}px`;
  canvas.style.height = `${height}px`;
+ 
+ const vShader = gl.createShader(gl.VERTEX_SHADER);
+ const fShader = gl.createShader(gl.FRAGMENT_SHADER);
+ 
+ compileShader(gl, vShader, vShaderSource);
+ compileShader(gl, fShader, fShaderSource);

Create program

πŸ“„ src/rotating-square.js


  compileShader(gl, vShader, vShaderSource);
  compileShader(gl, fShader, fShaderSource);
+ 
+ const program = gl.createProgram();
+ 
+ gl.attachShader(program, vShader);
+ gl.attachShader(program, fShader);
+ 
+ gl.linkProgram(program);
+ gl.useProgram(program);

Get attribute and uniform locations

πŸ“„ src/rotating-square.js

  import vShaderSource from './shaders/rotating-square.v.glsl';
  import fShaderSource from './shaders/rotating-square.f.glsl';
- import { compileShader } from './gl-helpers';
+ import { setupShaderInput, compileShader } from './gl-helpers';

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

  gl.linkProgram(program);
  gl.useProgram(program);
+ 
+ const programInfo = setupShaderInput(gl, program, vShaderSource, fShaderSource);

Create vertices to draw a square

πŸ“„ src/rotating-square.js

  import vShaderSource from './shaders/rotating-square.v.glsl';
  import fShaderSource from './shaders/rotating-square.f.glsl';
  import { setupShaderInput, compileShader } from './gl-helpers';
+ import { createRect } from './shape-helpers';
+ import { GLBuffer } from './GLBuffer';

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

  const programInfo = setupShaderInput(gl, program, vShaderSource, fShaderSource);
+ 
+ const vertexPositionBuffer = new GLBuffer(gl, gl.ARRAY_BUFFER, new Float32Array([
+     ...createRect(canvas.width / 2 - 100, canvas.height / 2 - 100, 200, 200),
+ ]), gl.STATIC_DRAW);

Setup attribute pointer

πŸ“„ src/rotating-square.js

  const vertexPositionBuffer = new GLBuffer(gl, gl.ARRAY_BUFFER, new Float32Array([
      ...createRect(canvas.width / 2 - 100, canvas.height / 2 - 100, 200, 200),
  ]), gl.STATIC_DRAW);
+ 
+ gl.vertexAttribPointer(programInfo.attributeLocations.position, 2, gl.FLOAT, false, 0, 0);

Create index buffer

πŸ“„ src/rotating-square.js

  ]), gl.STATIC_DRAW);

  gl.vertexAttribPointer(programInfo.attributeLocations.position, 2, gl.FLOAT, false, 0, 0);
+ 
+ const indexBuffer = new GLBuffer(gl, gl.ELEMENT_ARRAY_BUFFER, new Uint8Array([
+     0, 1, 2, 
+     1, 2, 3, 
+ ]), gl.STATIC_DRAW);

Pass resolution and setup viewport

πŸ“„ src/rotating-square.js

      0, 1, 2, 
      1, 2, 3, 
  ]), gl.STATIC_DRAW);
+ 
+ gl.uniform2fv(programInfo.uniformLocations.resolution, [canvas.width, canvas.height]);
+ 
+ gl.viewport(0, 0, canvas.width, canvas.height);

And finally issue a draw call

πŸ“„ src/rotating-square.js

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

  gl.viewport(0, 0, canvas.width, canvas.height);
+ gl.drawElements(gl.TRIANGLES, indexBuffer.data.length, gl.UNSIGNED_BYTE, 0);

Now let's think of how can we rotate this square

Actually we can fit in in the circle and each vertex position might be calculated with radius, cos and sin and all we'll need is add some delta angle to each vertex

Rotation

Let's refactor our createRect helper to take angle into account

πŸ“„ src/rotating-square.js

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

  const vertexPositionBuffer = new GLBuffer(gl, gl.ARRAY_BUFFER, new Float32Array([
-     ...createRect(canvas.width / 2 - 100, canvas.height / 2 - 100, 200, 200),
+     ...createRect(canvas.width / 2 - 100, canvas.height / 2 - 100, 200, 200, 0),
  ]), gl.STATIC_DRAW);

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

πŸ“„ src/shape-helpers.js

- export function createRect(top, left, width, height) {
+ const Pi_4 = Math.PI / 4;
+ 
+ export function createRect(top, left, width, height, angle = 0) {
+     const centerX = width / 2;
+     const centerY = height / 2;
+ 
+     const diagonalLength = Math.sqrt(centerX ** 2 + centerY ** 2);
+ 
+     const x1 = centerX + diagonalLength * Math.cos(angle + Pi_4);
+     const y1 = centerY + diagonalLength * Math.sin(angle + Pi_4);
+ 
+     const x2 = centerX + diagonalLength * Math.cos(angle + Pi_4 * 3);
+     const y2 = centerY + diagonalLength * Math.sin(angle + Pi_4 * 3);
+ 
+     const x3 = centerX + diagonalLength * Math.cos(angle - Pi_4);
+     const y3 = centerY + diagonalLength * Math.sin(angle - Pi_4);
+ 
+     const x4 = centerX + diagonalLength * Math.cos(angle - Pi_4 * 3);
+     const y4 = centerY + diagonalLength * Math.sin(angle - Pi_4 * 3);
+ 
      return [
-         left, top, // x1 y1
-         left + width, top, // x2 y2
-         left, top + height, // x3 y3
-         left + width, top + height, // x4 y4
+         x1 + left, y1 + top,
+         x2 + left, y2 + top,
+         x3 + left, y3 + top,
+         x4 + left, y4 + top,
      ];
  }


Now we need to define initial angle

πŸ“„ src/rotating-square.js

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

  gl.viewport(0, 0, canvas.width, canvas.height);
- gl.drawElements(gl.TRIANGLES, indexBuffer.data.length, gl.UNSIGNED_BYTE, 0);
+ 
+ let angle = 0;

and a function which will be called each frame

πŸ“„ src/rotating-square.js

  gl.viewport(0, 0, canvas.width, canvas.height);

  let angle = 0;
+ 
+ function frame() {
+     requestAnimationFrame(frame);
+ }
+ 
+ frame();

Each frame WebGL just goes through vertex data and renders it. In order to make it render smth different we need to update this data

πŸ“„ src/rotating-square.js

  let angle = 0;

  function frame() {
+     vertexPositionBuffer.setData(
+         gl, 
+         new Float32Array(
+             createRect(canvas.width / 2 - 100, canvas.height / 2 - 100, 200, 200, angle)
+         ), 
+         gl.STATIC_DRAW,
+     );
+ 
      requestAnimationFrame(frame);
  }


We also need to update rotation angle each frame

πŸ“„ src/rotating-square.js

          gl.STATIC_DRAW,
      );

+     angle += Math.PI / 60;
+ 
      requestAnimationFrame(frame);
  }


and issue a draw call

πŸ“„ src/rotating-square.js


      angle += Math.PI / 60;

+     gl.drawElements(gl.TRIANGLES, indexBuffer.data.length, gl.UNSIGNED_BYTE, 0);
      requestAnimationFrame(frame);
  }


Cool! We now have a rotating square! πŸŽ‰

Rotating circle gif

What we've just done could be simplified with rotation matrix

Don't worry if you're not fluent in linear algebra, me neither, there is a special package πŸ˜‰

πŸ“„ package.json

      "webpack-cli": "^3.3.5"
    },
    "dependencies": {
+     "gl-matrix": "^3.0.0",
      "glsl-extract-sync": "0.0.0"
    }
  }

We'll need to define a rotation matrix uniform

πŸ“„ src/shaders/rotating-square.v.glsl

  attribute vec2 position;
  uniform vec2 resolution;

+ uniform mat2 rotationMatrix;
+ 
  void main() {
      gl_Position = vec4(position / resolution * 2.0 - 1.0, 0, 1);
  }

And multiply vertex positions

πŸ“„ src/shaders/rotating-square.v.glsl

  uniform mat2 rotationMatrix;

  void main() {
-     gl_Position = vec4(position / resolution * 2.0 - 1.0, 0, 1);
+     gl_Position = vec4((position / resolution * 2.0 - 1.0) * rotationMatrix, 0, 1);
  }

Now we can get rid of vertex position updates

πŸ“„ src/rotating-square.js

  import { setupShaderInput, compileShader } from './gl-helpers';
  import { createRect } from './shape-helpers';
  import { GLBuffer } from './GLBuffer';
+ import { mat2 } from 'gl-matrix';

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

  gl.viewport(0, 0, canvas.width, canvas.height);

- let angle = 0;
+ const rotationMatrix = mat2.create();

  function frame() {
-     vertexPositionBuffer.setData(
-         gl, 
-         new Float32Array(
-             createRect(canvas.width / 2 - 100, canvas.height / 2 - 100, 200, 200, angle)
-         ), 
-         gl.STATIC_DRAW,
-     );
- 
-     angle += Math.PI / 60;

      gl.drawElements(gl.TRIANGLES, indexBuffer.data.length, gl.UNSIGNED_BYTE, 0);
      requestAnimationFrame(frame);

and use rotation matrix instead

πŸ“„ src/rotating-square.js

  const rotationMatrix = mat2.create();

  function frame() {
+     gl.uniformMatrix2fv(programInfo.uniformLocations.rotationMatrix, false, rotationMatrix);
+ 
+     mat2.rotate(rotationMatrix, rotationMatrix, -Math.PI / 60);

      gl.drawElements(gl.TRIANGLES, indexBuffer.data.length, gl.UNSIGNED_BYTE, 0);
      requestAnimationFrame(frame);

Conclusion

What seemed a complex math in our shape helper refactor turned out to be pretty easy doable with matrix math. GPU performs matrix multiplication very fast (it has special optimisations on hardware level for this kind of operations), so a lot of transformations can be made with transform matrix. This is very improtant concept, especcially in 3d rendering world.

That's it for today, 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)