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);
}

Enter fullscreen mode Exit fullscreen mode

fragment shader

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

precision mediump float;

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

Enter fullscreen mode Exit fullscreen mode

New entry point

πŸ“„ index.html

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

Enter fullscreen mode Exit fullscreen mode

πŸ“„ src/rotating-square.js

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

Enter fullscreen mode Exit fullscreen mode

πŸ“„ webpack.config.js

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

      output: {

Enter fullscreen mode Exit fullscreen mode

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');
+ 

Enter fullscreen mode Exit fullscreen mode

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`;

Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

πŸ“„ 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,
      ];
  }


Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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();

Enter fullscreen mode Exit fullscreen mode

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);
  }


Enter fullscreen mode Exit fullscreen mode

We also need to update rotation angle each frame

πŸ“„ src/rotating-square.js

          gl.STATIC_DRAW,
      );

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


Enter fullscreen mode Exit fullscreen mode

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);
  }


Enter fullscreen mode Exit fullscreen mode

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"
    }
  }

Enter fullscreen mode Exit fullscreen mode

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);
  }

Enter fullscreen mode Exit fullscreen mode

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);
  }

Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

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)