How to have hundred of simultaneous small canvas with animated and interactive webgl context spread in a webpage?

StackOverflow https://stackoverflow.com/questions/22176035

  •  03-06-2023
  •  | 
  •  

Question

I have a webpage with a large number of canvas spread in a document.

<canvas type="myCanvas" width="256" height="256" config="something different for each" ></canvas>

They have to render something simple enough to be acceptable in term of performance and memory, but the amount of simultaneous webgl context is limited by the browser to a dozen.

An alternative is to keep a single context and update the canvases in sequence, similar to have a single d3d11device and multiple dxgiswapchain.

Question : Is there an efficient way ( no gpu to cpu memory transfer ) to copy multiple times a framebuffer from a unique offscreen webgl enabled canvas created by code to the visible ones in the page, and if possible, do a prior test to not refresh ones outside of the window ?

No correct solution

OTHER TIPS

My advice:

  • Use a single canvas
  • Update regions in that canvas instead
  • Canvas uses GPU for most of its methods (if available) except for getImageData / createImageData and putImageData.

Each canvas has an overhead in addition to its bitmap so using a single canvas reduce this overhead and also makes it easier for the browser to parse and update the DOM.

You should be able to draw in regions on that canvas as if they where separate canvases/images.

Not sure what you are trying to do and maybe this is what you were suggesting but,

  1. you could render to one WebGL based canvas and then copy with drawImage to several 2D based canvases. Effectively something like

    <canvas id="c1"></canvas>
    <canvas id="c2"></canvas>
    <canvas id="c3"></canvas>
    

    Then

    var webGLOffScreenCanvas = document.createElement("canvas");
    var gl = webglOffScreenCanvas = webGLOffScreenCanvas.getContext("experimental-webgl");
    
    var c1 = document.getElementById("c1");
    var c2 = document.getElementById("c2");
    var c3 = document.getElementById("c3");
    
    var ctx1 = c1.getContext("2d");
    var ctx2 = c2.getContext("2d");
    var ctx3 = c3.getContext("2d");
    
    drawSomeWebGLSceneForC1(gl);
    ctx1.drawImage(webGLOffScreenCanvas, 0, 0);
    drawSomeWebGLSceneForC2(gl);
    ctx2.drawImage(webGLOffScreenCanvas, 0, 0);
    drawSomeWebGLSceneForC3(gl);
    ctx3.drawImage(webGLOffScreenCanvas, 0, 0);
    

    AFAIK this will be a GPU based rendering (drawing a the WebGL's canvas as a texture into another canvas's framebuffer texture, at least Chrome although I'm not 100% sure about that. It's easy enough to test. Here's a snippet

// make relatively large canvases to show possible readback
var width = 2048;
var height = 2048;
var numCanvases = 3;

var webGLOffScreenCanvas = makeCanvas();
var gl = webglOffScreenCanvas = webGLOffScreenCanvas.getContext("experimental-webgl");

var contexts = [];
for (var ii = 0; ii < numCanvases; ++ii) {
    var c = makeCanvas();
    c.style.width = "64px";
    c.style.height = "64px";
    document.body.appendChild(c);
    contexts.push(c.getContext("2d"));
}

function makeCanvas() {
    var c = document.createElement("canvas"); 
    c.width = width;
    c.height = height;
    return c;
}

function drawSomeWebGLScene(gl) {
    gl.clearColor(Math.random(), Math.random(), Math.random(), 1);
    gl.clear(gl.COLOR_BUFFER_BIT);
}
     
function render() {
    contexts.forEach(function(ctx) {
      drawSomeWebGLScene(gl);
      ctx.drawImage(webGLOffScreenCanvas, 0, 0);
    });
    requestAnimationFrame(render);
}
render();
canvas { border: 1px solid black; margin: 0.5em; }

Looking at about:tracing it doesn't appear at a glance to be reading back any data.

  1. Make one canvas the size of the browser window (viewport) and put it in the background, use place holder divs where you want to draw stuff, look up those divs, get their location and draw with gl.scissor and gl.viewport set to match that location

"use strict";
twgl.setDefaults({attribPrefix: "a_"});
var m4 = twgl.m4;
var gl = twgl.getWebGLContext(document.getElementById("c"));
var programInfo = twgl.createProgramInfo(gl, ["vs", "fs"]);

var shapes = [
  twgl.primitives.createCubeBufferInfo(gl, 2),
  twgl.primitives.createSphereBufferInfo(gl, 1, 24, 12),
  twgl.primitives.createTruncatedConeBufferInfo(gl, 1, 0, 2, 24, 1),
  twgl.primitives.createCylinderBufferInfo(gl, 1, 2, 24, 2),
  twgl.primitives.createTorusBufferInfo(gl, 1, 0.4, 24, 12),
];

function rand(min, max) {
  return min + Math.random() * (max - min);
}

// Shared values
var lightWorldPosition = [1, 8, -10];
var lightColor = [1, 1, 1, 1];
var camera = m4.identity();
var view = m4.identity();
var viewProjection = m4.identity();

var tex = twgl.createTexture(gl, {
min: gl.NEAREST,
mag: gl.NEAREST,
src: [
  255, 255, 255, 255,
  192, 192, 192, 255,
  192, 192, 192, 255,
  255, 255, 255, 255,
],
});

var objects = [];
var numObjects = 100;
var list = document.getElementById("list");
var listItemTemplate = document.getElementById("list-item-template").text;
for (var ii = 0; ii < numObjects; ++ii) {
var listElement = document.createElement("div");
listElement.innerHTML = listItemTemplate;
listElement.className = "list-item";
var viewElement = listElement.querySelector(".view");
var uniforms = {
  u_lightWorldPos: lightWorldPosition,
  u_lightColor: lightColor,
  u_diffuseMult: chroma.hsv(rand(0, 360), 0.4, 0.8).gl(),
  u_specular: [1, 1, 1, 1],
  u_shininess: 50,
  u_specularFactor: 1,
  u_diffuse: tex,
  u_viewInverse: camera,
  u_world: m4.identity(),
  u_worldInverseTranspose: m4.identity(),
  u_worldViewProjection: m4.identity(),
};
objects.push({
  ySpeed: rand(0.1, 0.3),
  zSpeed: rand(0.1, 0.3),
  uniforms: uniforms,
  viewElement: viewElement,
  programInfo: programInfo,
  bufferInfo: shapes[ii % shapes.length],
});
list.appendChild(listElement);
}

function render(time) {
  time *= 0.001;
  twgl.resizeCanvasToDisplaySize(gl.canvas);

  gl.enable(gl.DEPTH_TEST);
  gl.disable(gl.SCISSOR_TEST);
  gl.clearColor(0, 0, 0, 0);
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

  gl.enable(gl.SCISSOR_TEST);
  gl.clearColor(0.8, 0.8, 0.8, 1);

  var eye = [0, 0, -8];
  var target = [0, 0, 0];
  var up = [0, 1, 0];

  m4.lookAt(eye, target, up, camera);
  m4.inverse(camera, view);

  objects.forEach(function(obj) {
    var viewElement = obj.viewElement;
    // get viewElement's position
    var rect = viewElement.getBoundingClientRect();
    if (rect.bottom < 0 || rect.top  > gl.canvas.clientHeight ||
        rect.right  < 0 || rect.left > gl.canvas.clientWidth) {
      return;  // it's off screen
    }

    var width  = rect.right - rect.left;
    var height = rect.bottom - rect.top;
    var left   = rect.left;
    var bottom = gl.canvas.clientHeight - rect.bottom - 1;

    gl.viewport(left, bottom, width, height);
    gl.scissor(left, bottom, width, height);
    gl.clear(gl.COLOR_BUFFER_BIT);

    var projection = m4.perspective(30 * Math.PI / 180, width / height, 0.5, 100);
    m4.multiply(projection, view, viewProjection);

    var uni = obj.uniforms;
    var world = uni.u_world;
    m4.identity(world);
    m4.rotateY(world, time * obj.ySpeed, world);
    m4.rotateZ(world, time * obj.zSpeed, world);
    m4.transpose(m4.inverse(world, uni.u_worldInverseTranspose), uni.u_worldInverseTranspose);
    m4.multiply(viewProjection, uni.u_world, uni.u_worldViewProjection);

    gl.useProgram(obj.programInfo.program);
    twgl.setBuffersAndAttributes(gl, obj.programInfo, obj.bufferInfo);
    twgl.setUniforms(obj.programInfo, uni);
    twgl.drawBufferInfo(gl, obj.bufferInfo);
  });

  requestAnimationFrame(render);
}
requestAnimationFrame(render);
* {
  box-sizing: border-box;
  -moz-box-sizing: border-box;
}
body {
  margin: 0;
  font-family: monospace;
}
canvas {
  display: block;
  width: 100vw;
  height: 100vh;
}
#c {
  position: fixed;
}
#outer {
  width: 100%;
  z-index: 2;
  position: absolute;
  top: 0px;
}
#content {
  margin: auto;
  padding: 2em;
}
#b {
  width: 100%;
  text-align: center;
}
.list-item {
  margin: 1em;
  padding: 1em;
  border: 1px solid #ccc;
  display: inline-block;
  width: 160px;
}
.list-item .view {
  width: 100px;
  height: 100px;
  float: left;
  margin: 0 1em 1em 0;
}
.list-item .description {
  font-family: sans-serif;
  font-size: small;
}
<canvas id="c"></canvas>
<div id="outer">
  <div id="content">
    <div id="b"><a href="http://twgljs.org">twgl.js</a> - item list</div>
    <div id="list"></div>
  </div>
</div>
  <script id="list-item-template" type="notjs">
    <div class="view"></div>
    <div class="description">bla blaaa blaba bla blabla bla</div>
  </script>
  <script id="vs" type="notjs">
uniform mat4 u_worldViewProjection;
uniform vec3 u_lightWorldPos;
uniform mat4 u_world;
uniform mat4 u_viewInverse;
uniform mat4 u_worldInverseTranspose;

attribute vec4 a_position;
attribute vec3 a_normal;
attribute vec2 a_texcoord;

varying vec4 v_position;
varying vec2 v_texCoord;
varying vec3 v_normal;
varying vec3 v_surfaceToLight;
varying vec3 v_surfaceToView;

void main() {
  v_texCoord = a_texcoord;
  v_position = (u_worldViewProjection * a_position);
  v_normal = (u_worldInverseTranspose * vec4(a_normal, 0)).xyz;
  v_surfaceToLight = u_lightWorldPos - (u_world * a_position).xyz;
  v_surfaceToView = (u_viewInverse[3] - (u_world * a_position)).xyz;
  gl_Position = v_position;
}
  </script>
  <script id="fs" type="notjs">
precision mediump float;

varying vec4 v_position;
varying vec2 v_texCoord;
varying vec3 v_normal;
varying vec3 v_surfaceToLight;
varying vec3 v_surfaceToView;

uniform vec4 u_lightColor;
uniform vec4 u_diffuseMult;
uniform sampler2D u_diffuse;
uniform vec4 u_specular;
uniform float u_shininess;
uniform float u_specularFactor;

vec4 lit(float l ,float h, float m) {
  return vec4(1.0,
              abs(l),//max(l, 0.0),
              (l > 0.0) ? pow(max(0.0, h), m) : 0.0,
              1.0);
}

void main() {
  vec4 diffuseColor = texture2D(u_diffuse, v_texCoord) * u_diffuseMult;
  vec3 a_normal = normalize(v_normal);
  vec3 surfaceToLight = normalize(v_surfaceToLight);
  vec3 surfaceToView = normalize(v_surfaceToView);
  vec3 halfVector = normalize(surfaceToLight + surfaceToView);
  vec4 litR = lit(dot(a_normal, surfaceToLight),
                    dot(a_normal, halfVector), u_shininess);
  vec4 outColor = vec4((
  u_lightColor * (diffuseColor * litR.y +
                u_specular * litR.z * u_specularFactor)).rgb,
      diffuseColor.a);
  gl_FragColor = outColor;
}
  </script>
  <script src="https://twgljs.org/dist/3.x/twgl-full.min.js"></script>
  <script src="https://twgljs.org/3rdparty/chroma.min.js"></script>

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top