1

I am under the impression that WebGL is much more powerful than the 2d renderer in the browser but for some reason, my WebGL code runs much slower. Are there any recommended optimization strategies I can use to make my WebGL code run a little bit faster?

Are there any code in my rect function that should be left out because I am new to WebGL and most tutorials don't cover how to make a rect function.

WebGL Code

const canvas = document.getElementById("canvas");
const vertexCode = `
precision mediump float;

attribute vec4 position;
uniform mat4 matrix;
uniform vec4 color;
varying vec4 col;

void main() {
  col = color;
  gl_Position = matrix * position;
}
`;
const fragmentCode = `
precision mediump float;

varying vec4 col;

void main() {
  gl_FragColor = col;
}
`;
const width = canvas.width;
const height = canvas.height;
const gl = canvas.getContext("webgl");
if(!gl) {
  console.log("WebGL not supported");
}
const projectionMatrix = [
  2/width, 0, 0, 0,
  0, -2/height, 0, 0,
  0, 0, 1, 0,
  -1, 1, 0, 1
];


const vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, vertexCode);
gl.compileShader(vertexShader);

const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, fragmentCode);
gl.compileShader(fragmentShader);

const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);

gl.linkProgram(program);

gl.useProgram(program);

function rect(x, y, w, h) {
  const vertex = [
    x, y, 0, 1,
    x+w, y, 0, 1,
    x, y+h, 0, 1,
    x+w, y+h, 0, 1
  ]
  const positionBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertex), gl.STATIC_DRAW);

  const positionLocation = gl.getAttribLocation(program, "position");
  gl.enableVertexAttribArray(positionLocation);
  gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
  gl.vertexAttribPointer(positionLocation, 4, gl.FLOAT, false, 0, 0);

  const projectionLocation = gl.getUniformLocation(program, `matrix`);
  gl.uniformMatrix4fv(projectionLocation, false, projectionMatrix);

  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
}
function fill(r, g, b, a) {
  const projectionLocation = gl.getUniformLocation(program, `color`);
  gl.uniform4fv(projectionLocation, [r, g, b, a]);
}
let lastTime = new Date();
function animate() {
  let currentTime = new Date();
  console.log(1000 / (currentTime.getTime() - lastTime.getTime()));
  lastTime = new Date();
  requestAnimationFrame(animate);
  for(let i=0;i<200;i++) {
    fill(1, 0, 0, 1);
    rect(random(0, 800), random(0, 600), 10, 10);
  }
}
animate();
function random(low, high) {
  return low + Math.random() * (high-low)
}

Using the normal 2D renderer

const canvas = document.getElementById("canvas");
const c = canvas.getContext("2d");
let lastTime = new Date();
function animate() {
  let currentTime = new Date();
  console.log(1000 / (currentTime.getTime() - lastTime.getTime()));
  lastTime = new Date();
  requestAnimationFrame(animate);
  c.fillStyle = "black";
  c.fillRect(0, 0, 800, 600);
  c.fillStyle = "red";
  for(let i=0;i<200;i++) {
    c.fillRect(random(0, 800), random(0, 600), 10, 10);
  }
}
animate();
function random(low, high) {
  return low + Math.random() * (high-low)
}

2 Answers2

2

There are 1000s of ways to optimized WebGL. Which one you choose depends on your needs. The more you optimize generally the less flexible.

First you should do the obvious and not create new buffers for every rectangle as your code is doing now and also move anything outside the rendering that can be moved outside (like looking up locations) which is shown in the answer by Józef Podlecki.

Another is you could move and scale the rectangle use the uniform matrix rather than uploading new vertices for each rectangle. It's unclear whether or not updating the matrix would be faster or slower for rectangles but if you were drawing something with more vertices it would definitely be faster do it by matrix. This series of articles mentions that and builds up to matrices.

Another is you can use vertex arrays though that's not important for your example since you're only drawing a single thing.

Another is if the shapes are all the same (as yours are) you can use instanced drawing to draw all 200 rectangles with one draw call

If the shapes are not all the same you can use creative techniques like storing their data in textures.

Another is you can put more than one shape in a buffer. So for example instead of putting one rectangle in the buffer put all 200 rectangles in the buffer and then draw them with one draw call. From this presentation

Also note: the callback to requestAnimationFrame is passed the time since the page loaded so there is no need to create Date objects. Creating Date objects is slower than not and the time passed to requestAnimationFrame is more accurate than Date

let lastTime = 0;
function animate(currentTime) {
  console.log(1000 / (currentTime - lastTime));
  lastTime = currentTime;
  ...
  requestAnimationFrame(animate);
}
requestAnimationFrame(animate);

It's also slow to print to the console. You'd be better off updating an element's content

let lastTime = 0;
function animate(currentTime) {
  someElement.textContent = (1000 / (currentTime - lastTime)).toFixed(1);
  lastTime = currentTime;
  ...
  requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
gman
  • 100,619
  • 31
  • 269
  • 393
1

You can reuse buffer, extract getAttribLocation,getUniformLocation outside of rect function as well as vertexAttribPointer once you bound buffer and lastly call requestAnimationFrame after rendering

const canvas = document.getElementById("canvas");
const vertexCode = `
precision mediump float;

attribute vec4 position;
uniform mat4 matrix;
uniform vec4 color;
varying vec4 col;

void main() {
  col = color;
  gl_Position = matrix * position;
}
`;
const fragmentCode = `
precision mediump float;

varying vec4 col;

void main() {
  gl_FragColor = col;
}
`;
const width = canvas.width;
const height = canvas.height;
const gl = canvas.getContext("webgl");
if(!gl) {
  console.log("WebGL not supported");
}
const projectionMatrix = [
  2/width, 0, 0, 0,
  0, -2/height, 0, 0,
  0, 0, 1, 0,
  -1, 1, 0, 1
];


const vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, vertexCode);
gl.compileShader(vertexShader);

const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, fragmentCode);
gl.compileShader(fragmentShader);

const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);

gl.linkProgram(program);

gl.useProgram(program);

const positionBuffer = gl.createBuffer();
const positionLocation = gl.getAttribLocation(program, "position");
const projectionLocation = gl.getUniformLocation(program, `matrix`);
const projectionColorLocation = gl.getUniformLocation(program, `color`);
gl.enableVertexAttribArray(positionLocation);
const vertex = [
    0, 0, 0, 1,
    0, 0, 0, 1,
    0, 0, 0, 1,
    0, 0, 0, 1
  ]
const floatArray = new Float32Array(vertex)
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.vertexAttribPointer(positionLocation, 4, gl.FLOAT, false, 0, 0);
gl.uniformMatrix4fv(projectionLocation, false, projectionMatrix);

function rect(x, y, w, h) {
  floatArray[0] = x;
  floatArray[1] = y;
  floatArray[4] = x + w;
  floatArray[5] = y;
  floatArray[8] = x;
  floatArray[9] = y + h;
  floatArray[12] = x + w;
  floatArray[13] = y + h;
  
  gl.bufferData(gl.ARRAY_BUFFER, floatArray, gl.STATIC_DRAW);    
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
  
}
function fill(r, g, b, a) { 
  gl.uniform4fv(projectionColorLocation, [r, g, b, a]);
}
let lastTime = new Date();
function animate() {
  let currentTime = new Date();
  console.log(1000 / (currentTime.getTime() - lastTime.getTime()));
  lastTime = new Date();
  for(let i=0;i<200;i++) {
    fill(1, 0, 0, 1);
    rect(random(0, 800), random(0, 600), 10, 10);
  }
  requestAnimationFrame(animate);
  
}
animate();
function random(low, high) {
  return low + Math.random() * (high-low)
}
<canvas id="canvas" width="500" height="300"></canvas>
Józef Podlecki
  • 10,453
  • 5
  • 24
  • 50
  • Thank you so much. I used your code and changed static draw to dynamic draw and the fps immediately spiked up to 60 fps from 10 fps. :D – Jeffrey Chen Jun 17 '20 at 02:20