web上的OffscreenCanvas-离屏canvas使用说明(离屏渲染)

OffscreenCanvas 是一个实验中的新特性(在最新版本的 Chrome 和 Firefox 上都可以通过实验室开关打开,Chrome 的开关是 chome://flags -> Experimental Web Platform features(离屏渲染chrome86已默认支持,不需要开启),本文的例程是在 Chrome 67 Canary 上进行验证,主要用于提升 Canvas 2D/3D 绘图的渲染性能和使用体验。

OffscreenCanvas和canvas都是渲染图形的对象。 不同的是canvas只能在window环境下使用,而OffscreenCanvas既可以在window环境下使用,也可以在web worker中使用,这让不影响浏览器主线程的离屏渲染成为可能。

跟 OffscreenCanvas 关系比较紧密的还有另外两个新的 API,ImageBitmapImageBitmapRenderingContext。

使用解析

OffscreenCanvas 目前主要用于两种不同的使用场景:

  • Transfer 模式:一种是在 Worker 线程创建一个 OffscreenCanvas 做后台渲染,然后再把渲染好的缓冲区 Transfer 回主线程显示;
  • Commit 模式:一种是主线程从当前 DOM 树中的 Canvas 元素产生一个 OffscreenCanvas,再把这个 OffscreenCanvas 发送给 Worker 线程进行渲染,渲染的结果直接 Commit 到浏览器的 Display Compositor 输出到当前窗口,相当于在 Worker 线程直接更新 Canvas 元素的内容;

一、Transfer模式

应用场景:Transfer 模式主要用于后台渲染,避免耗时的渲染任务会阻塞前台线程,导致应用无法及时响应用户的操作,比如一些 2D/3D 图表,图形可视化应用,地图应用等。

Transfer Demo 运行流程大致如下:

  1. 主线程启动 Worker 线程,并请求初始化;
  2. Worker 线程创建 OffscreenCanvas;
  3. Worker 线程获取 OffscreenCanvas 的 WebGL Context 并进行绘制;
  4. Worker 线程获取 OffscreenCanvas 的缓冲区(ImageBitmap),然后 Transfer 回主线程;
  5. 主线程将 Worker 线程回传的缓冲区分别绘制在两个不同的 Canvas 上,一个 Canvas 使用 CanvasRenderingContext2D,一个 Canvas 使用 ImageBitmapRenderingContext
  6. 3 ~ 5 重复运行;

1、渲染到canvas上的两种方式:

  • 方案一:ImageBitmap 可以被当做普通的 Image 绘制在一个 2D Canvas 上;
  • 方案二:可以通过 ImageBitmapRenderingContext Transfer 到一个 Bitmap Canvas;

2、ImageBitmap介绍

  • ImageBitmap 主要是用来封装一块 GPU 缓冲区,可以被 GPU 读写,并且实现了 Transferable 的接口(transferFromImageBitmap和transferToImageBitmap),可以在不同线程之间 Transfer。
  • 跟 ImageData 不一样,ImageBitmap 并没有提供 JavaScipt API 供 CPU 进行读写,这是因为使用 CPU 读写 GPU 缓冲区的成本非常高,需要拷贝到临时缓冲区进行读写然后再写回。这也是为什么规范的制定者没有扩展 ImageData,而是提供了一个新的 ImageBitmap 的缘故。

3、渲染的具体流程:

  • (1) 当我们使用 OffscreenCanvas,通过 2D/3D 进行绘制时==》我们有一块画板,上面有一些画纸,我们可以在画纸上作画
  • (2) 调用 OffscreenCanvas.transferToImageBitmap 获取 ImageBitmap 封装的缓冲区==》我们把当前绘画的画纸取下来;
function TransferBuffer() {
  let image_bitmap = canvas.transferToImageBitmap();
  postMessage({name:"TransferBuffer", buffer:image_bitmap},
    [image_bitmap]);
}
  • (3) 把 ImageBitmap 作为 Image 绘制在一个 2D Canvas(方案一)==》我们对已经绘制好的图画在新的画纸上进行临摹
function init() {
    g_bitmap_canvas = helper.GetCanvas("bitmap");
    g_2d_canvas = helper.GetCanvas("2d");
    g_render_worker = new Worker("./render_worker.js");
    g_render_worker.onmessage = function(msg) {

    if (msg.data.name === "TransferBuffer") {
        GetTransferBuffer(msg.data.buffer);
    }
  }
}

function GetTransferBuffer(buffer) {
    let context_2d = g_2d_canvas.getContext("2d");
    context_2d.clearRect(0, 0, g_2d_canvas.width, g_2d_canvas.height);
    context_2d.save();
    context_2d.translate(g_bitmap_canvas.width / 2, g_bitmap_canvas.height / 2);
    context_2d.rotate(g_angle * Math.PI / 180);
    context_2d.scale(0.5, 0.5);
    context_2d.translate(-g_bitmap_canvas.width / 2, -g_bitmap_canvas.height / 2);                  
    context_2d.drawImage(buffer, 0, 0);
    context_2d.restore();

    g_angle += 15;
    if (g_angle > 360)
        g_angle = 0;
}
  • (4)把 ImageBitmap 通过 ImageBitmapRenderingContext.transferFromImageBitmap Transfer 给 Bitmap Canvas(方案二)==》我们把画纸放入一个画框里挂在墙上显示
function GetTransferBuffer(buffer) {
  let bitmap_context = g_bitmap_canvas.getContext("bitmaprenderer");
  bitmap_context.transferFromImageBitmap(buffer);
  console.log("end");
}

4、Transfer 总结

  •  ImageBitmap Transfer 语义实现了零拷贝 的所有权转移,不需要对缓冲区进行拷贝,性能更高,但是也限制了显示的方式。
  • 临摹意味着我们可以对临摹的副本进行旋转,缩放,位移等等,还可以在上面再绘制其它内容。
  • ImageBitmap Transfer 之后所有权发生了转移,例如“渲染具体流程中”(3)和(4)交换顺序,即调换一下两个 Canvas 的绘制顺序就会报错,因为 Transfer 之后,原来的缓冲区引用已经被置空变成一个空引用。

===》具体使用哪种方式取决于应用的场景,如果只是简单的展现就可以考虑使用性能更高ImageBitmapRenderingContext,即方案二。

二、Commit模式

Commit 模式主要用于 H5 游戏,它允许应用/游戏在 Worker 线程直接对 DOM 树里面的 Canvas 元素进行更新,浏览器在这种模式下提供了一条最短路径和最佳性能的 Canvas 渲染流水线。

1、普通 Canvas 元素更新的渲染流水线

普通 Canvas 元素更新的渲染流水线,跟其它 DOM 元素一样,Canvas 元素的更新也是走非合成器动画的渲染流水线,主要的缺点是:

  • 非合成器动画的渲染流水线比较复杂和冗长,有较多的 Overhead,页面的结构越复杂,Overhead 就越高;
  • 如果同时有其它 DOM 元素一起更新,Canvas 的更新会被其它 DOM 元素的光栅化所阻塞,导致性能下降,性能下降的幅度取决于其它 DOM 元素光栅化的耗时;

如果我们调用 Commit,并且 Commit 的 OffscreenCanvas 是跟当前 DOM 树里面的某个 Canvas 元素相关联,浏览器就会直接将 OffscreenCanvas 的当前绘制缓冲区发送给 Display Compositor,然后 Display Compositor 就会合成新的一帧输出到当前窗口,对浏览器来说这就是最短的渲染路径。

Commit模式相对普通canvas的优点:

  • 避免被主线程的其它任务所阻塞,Worker 线程可以完全专注在 Canvas 动画的运行上;
  • 通过 OffscreenCanvas 更新 Canvas 元素,浏览器走的是最短的渲染路径,避免了非合成器动画的冗长流水线和 Overhead;
  • 如果有其它 DOM 元素同时更新,不会阻塞 OffscreenCanvas 的更新,所以通过 OffscreenCanvas,的确实现了 Canvas 更新和其它 DOM 更新的并发运行
  • 如果 DOM 元素需要处理事件,这些事件处理不会被 Worker 线程所阻塞,只是处理的结果数据可能需要发送给 Worker 线程用于后续的绘制;

Commit模式的缺点:

  • OffscreenCanvas 的更新和其它 DOM 元素的更新不再是强制同步的。即使它们是同时更新,甚至都在主线程而不使用 Worker 线程,因为两者已经分别走了不同的流水线,最后呈现在屏幕的时机也可能不会完全一致。如果一定要求同步,就只能参考 Transfer Demo 的做法,将绘制后的缓冲区 Transfer 给 Bitmap Canvas 来显示,但是这样就无法发挥 Commit 模式的性能优势了。

2、commit渲染的具体流程

  • (1)主线程从当前 DOM 树中的 Canvas 元素生成 OffscreenCanvas;
  • (2)主线程启动 Worker 线程并初始化,OffscreenCanvas 作为初始化的参数被 Transfer;
var g_offscreen_canvas = null;
var g_render_worker = null;

function main() {
  g_offscreen_canvas = helper.GetCanvas("offscreen");

  g_render_worker = new Worker("./render_worker.js");

  let offscreen = g_offscreen_canvas.transferControlToOffscreen();
  g_render_worker.postMessage(
    {name:"Init", mode:"commit", canvas:offscreen}, [offscreen]);
}
  • (3)Worker 线程接收 OffscreenCanvas 后完成初始化;
function Init(mode, data) {
  if (mode === "transfer")
    canvas = new OffscreenCanvas(data.width, data.height);
  else if (mode === "commit")
    canvas = data.canvas;
}
  • (4)Worker 线程使用 WebGL 对 OffscreenCanvas 进行绘制;
  • (5)Worker 线程绘制完成后 Commit,然后等待浏览器的回调;
function renderloop() {
  render();
  gl.commit().then(renderloop);
}

renderloop();
  • (6)Worker 线程接收到到浏览器的回调后继续绘制下一帧,重复 4 ~ 6;

注意:

  • canvas对象调用了函数transferControlToOffscreen移交控制权之后,不能再获取绘制上下文,调用canvas.getContext('2d')会报错。
  • 同样,如果canvas已经获取的绘制上下文,调用transferControlToOffscreen会报错。

Demo1

1、html主线程

<!doctype html>
<html>
<head>
</head>
<body>
<style>
.items {
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
}
</style>
<html>
    <head>
    	<meta charset="utf-8">
        <title>player</title>
    </head>
    <body>
	   <canvas  id="player" width="480px" height="280px" style="border: 2px; background-color:black"></canvas>
    </body>
</html>

 <div class="items">
  <button type="button" onClick="init()">start</button>
</div>
 <script>
 
	var worker2 = null;
	var canvasBitmap=null;
	var canvas2d=null;
	var ctxBitmap=null;
	function init() {
		canvasBitmap = document.getElementById('player');
		worker2 = new Worker('./bitmap_worker.js');
		console.log("test.html create worker success");
		方法一:transfer
		//worker2.postMessage({name:"Init", mode:"transfer",width:canvasBitmap.width, height: canvasBitmap.height});
		方法二:commit
		let offscreen = canvasBitmap.transferControlToOffscreen();
		worker2.postMessage({name:'Init',mode:"commit",canvas:offscreen},[offscreen]);
		worker2.onmessage = function (e) {
			if(e.data.name==="TransferBuffer")
			{
				//方法一:transfer 2d渲染
				// ctxBitmap = canvasBitmap.getContext('2d');
				// ctxBitmap.clearRect(0,0,canvasBitmap.width,canvasBitmap.height);
				// ctxBitmap.save();
				// ctxBitmap.drawImage(e.data.buffer,0,0);
				// console.log("2d");
				// ctxBitmap.restore();

				//方法二:transfer bitmaprenderer渲染
				let bitmap_context = canvasBitmap.getContext("bitmaprenderer");//bitmaprenderer
				console.log("bitmap_context:"+bitmap_context);
				if(bitmap_context)
				{
					bitmap_context.transferFromImageBitmap(e.data.buffer);
				    console.log("end");
				}
				
			}
			
		}
	}
 </script> 
</body>
</html>

worker:

'use strict';
importScripts("helper.js");
importScripts("Vector3D.js");
importScripts("Boid.js");
importScripts("cuon-utils.js");
importScripts("webgl-utils.js");
importScripts("webgl-debug.js");
var offscreen,ctx,gl;

onmessage = function (e) {
  if(e.data.name == 'Init'){
    console.log('Window post:' + e.data.name + ", mode:" + e.data.mode);
    Init(e.data.mode,e.data);
    //draw();
  }else if(e.data.name == 'draw'){
    draw();
  }
}

function Init(mode,data) {
  
  if (mode === "transfer")
  {
    console.log("bitmap_worker width:"+data.width+",height:"+data.height);
    offscreen = new OffscreenCanvas(data.width, data.height);
  }
  else if (mode === "commit")
  {
    console.log("commit");
    offscreen = data.canvas;
  }
  ctx = offscreen.getContext("2d");
  console.log("worker new OffscreenCanvas");
  draw(mode);
}

function draw(mode) {
  if (mode === "transfer")
  {
    render();
    let imageBitmap = offscreen.transferToImageBitmap();  
    postMessage({name:"TransferBuffer", buffer:imageBitmap},[imageBitmap]);
    console.log("bitmap_worker.js transfer");
  }
  else if (mode === "commit")
  {
    render();
    console.log("bitmap_worker.js commit");
  }
  
}
function render()
{
    var grd=ctx.createLinearGradient(20,20,150,150);//create grd object
    grd.addColorStop(0,"yellow");//the start grd color;
    grd.addColorStop(1,"red");//the end grd color; 1 means end
    ctx.fillStyle=grd;//
    ctx.fillRect(140,20,100,100);//draw a rect which is filled.
    ctx.stroke();
}

Demo2:

test2.html

<!doctype html>
<html>
<head>
    <script type="text/javascript" src="./helper.js"></script>
</head>
<body>
<style>
.items {
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
}
</style>
<html>
    <head>
    	<meta charset="utf-8">
        <title>player</title>
    </head>
    <body>
	   <canvas  id="bitmap" width="480px" height="280px" style="border: 2px; background-color:black"></canvas>
	   <canvas  id="2d"  width="480px" height="280px" ></canvas>
    </body>
</html>

 <div class="items">
  <button type="button" onClick="init()">start</button>
</div>
 <script>
 
var g_bitmap_canvas = null;
var g_2d_canvas = null;
var g_render_worker = null;
var g_angle = 30;
function init() {
  g_bitmap_canvas = helper.GetCanvas("bitmap");
  g_2d_canvas = helper.GetCanvas("2d");

  let window_size = helper.GetWindowSizeInPx();
//   g_bitmap_canvas.width = window_size.width;
//   g_bitmap_canvas.height = window_size.height / 2;
  //g_bitmap_canvas.width = 640;
  //g_bitmap_canvas.height = 640;

  //g_2d_canvas.width = window_size.width;
  //g_2d_canvas.height = window_size.height / 2;
 // g_2d_canvas.width = 512;
  //g_2d_canvas.height = 512;

  g_render_worker = new Worker("./renderWorker.js");
  //g_render_worker = new Worker("./bitmap_worker.js");
  g_render_worker.onmessage = function(msg) {
    console.log('Woker post:' + msg.data.name);

    if (msg.data.name === "TransferBuffer") {
      GetTransferBuffer(msg.data.buffer);
    }
  }

  g_render_worker.postMessage(
    {name:"Init", mode:"transfer",
      width:g_bitmap_canvas.width, height: g_bitmap_canvas.height});
}

function GetTransferBuffer(buffer) {
//   let context_2d = g_2d_canvas.getContext("2d");
//   context_2d.clearRect(0, 0, g_2d_canvas.width, g_2d_canvas.height);
//   context_2d.save();
//   context_2d.translate(g_bitmap_canvas.width / 2, g_bitmap_canvas.height / 2);
//   context_2d.rotate(g_angle * Math.PI / 180);
//   context_2d.scale(0.5, 0.5);
//   context_2d.translate(-g_bitmap_canvas.width / 2, -g_bitmap_canvas.height / 2);
//   context_2d.drawImage(buffer, 0, 0);
//   context_2d.restore();

//   g_angle += 15;
//   if (g_angle > 360)
//     g_angle = 0;

  let bitmap_context = g_bitmap_canvas.getContext("bitmaprenderer");
  bitmap_context.transferFromImageBitmap(buffer);
  console.log("end");
}
 </script>
   
</body>
</html>

renderWorker.js

'use strict';

importScripts("helper.js");
importScripts("Vector3D.js");
importScripts("Boid.js");
importScripts("cuon-utils.js");
importScripts("webgl-utils.js");
importScripts("webgl-debug.js");

// Basically RPC.
var STAGE = { width:1080, height:1920 };

var gl = null;
var canvas = null;
var meter = new helper.FPSMeter();
var drawCount = 0;

if (helper.IsWorkerEnv()) {
  self.onmessage = function(msg) {
    console.log('Window post:' + msg.data.name + ", mode:" + msg.data.mode);

    if (msg.data.name === "Init") {
      Init(msg.data.mode, msg.data);
    }
  }
}

function TransferBuffer() {
  let image_bitmap = canvas.transferToImageBitmap();
  postMessage({name:"TransferBuffer", buffer:image_bitmap},
    [image_bitmap]);
}

function Init(mode, data) {
  if (mode === "transfer")
    canvas = new OffscreenCanvas(data.width, data.height);
  else if (mode === "commit")
    canvas = data.canvas;

  console.log("Render init canvas size:" + canvas.width + "x" + canvas.height);

  STAGE.width = canvas.width;
  STAGE.height = canvas.height;

  if (canvas && canvas.getContext) {
    //setup page
    // Get the rendering context for WebGL
    gl = getWebGLContext(canvas);
    if (!gl) {
      console.log('Failed to get the rendering context for WebGL');
      return;
    }

    // Initialize shaders
    if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
      console.log('Failed to intialize shaders.');
      return;
    }

    // // Get the storage location of a_Position
    a_Position = gl.getAttribLocation(gl.program, 'a_Position');
    if (a_Position < 0) {
      console.log('Failed to get the storage location of a_Position');
      return;
    }

    // Get the storage location of a_Color
    a_Color = gl.getAttribLocation(gl.program, 'a_Color');
    if (a_Color < 0) {
      console.log('Failed to get the storage location of a_Color');
      return;
    }

    // Specify the color for clearing <canvas>
    gl.clearColor(0.2, 0.2, 0.2, 1);

    //initialize test variables
    createBoids();

    // Allocate buffer for position and color
    verticesColors = new Float32Array(boids.length * 5);

    // Create a buffer object
    vertexColorBuffer = gl.createBuffer();
    if (!vertexColorBuffer) {
      console.log('Failed to create the buffer object');
      return false;
    }

    if (mode === "transfer") {
      //setInterval(() => {
        // Render buffer first
        render();
        // Transfer render buffer back to browser context
        TransferBuffer();
     // }, 200);
    } else if (mode === "commit") {
      function renderloop() {
        // Render buffer first
        render();

        // fps
        let result = meter.update();
        if (result.framerate > 0) {
          console.log("WebGL Offscreen framerate:" + result.framerate);
        }

        // Wait next begin frame to loop
        gl.commit().then(renderloop);
      }

      renderloop();
    }
  }
}

var config = {
  minForce:3,
  maxForce:6,
  minSpeed:6,
  maxSpeed:12,
  minWanderDistance:10,
  maxWanderDistance:100,
  minWanderRadius:5,
  maxWanderRadius:20,
  minWanderStep:0.1,
  maxWanderStep:0.9,
  numBoids:500
};

var drawEnabled = true;
var firstDraw = true;
var boids = [];

// Vertex shader program
var VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'attribute vec4 a_Color;\n' +
  'varying vec4 v_Color;\n' +
  'void main() {\n' +
  '  gl_Position = a_Position;\n' +
  '  gl_PointSize = 10.0;\n' +
  '  v_Color = a_Color;\n' +
  '}\n';

// Fragment shader program
var FSHADER_SOURCE =
  '#ifdef GL_ES\n' +
  'precision mediump float;\n' +
  '#endif\n' +
  'varying vec4 v_Color;\n' +
  'void main() {\n' +
  '  gl_FragColor = v_Color;\n' +
  '}\n';

var a_Position;
var a_Color;
var verticesColors;
var vertexColorBuffer;

function initVertexBuffers(gl, first) {
  // Bind the buffer object to target
  if (first) {
    gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorBuffer);
  }

  gl.bufferData(gl.ARRAY_BUFFER, verticesColors, gl.DYNAMIC_DRAW);

  if (first) {
    let FSIZE = verticesColors.BYTES_PER_ELEMENT;
    gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 5, 0);
    gl.enableVertexAttribArray(a_Position);  // Enable the assignment of the buffer object

    gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE * 5, FSIZE * 2);
    gl.enableVertexAttribArray(a_Color);  // Enable the assignment of the buffer object
  }

  // Unbind the buffer object
  // gl.bindBuffer(gl.ARRAY_BUFFER, null);
}

function createBoids(){
  for (var i = 0;i < config.numBoids; i++){
    var boid = new Boid();
    boid.color_r = Math.floor(random(100, 255)) / 255;
    boid.color_g = Math.floor(random(100, 255)) / 255;
    boid.color_b = Math.floor(random(100, 255)) / 255;
    boid.edgeBehavior = Boid.EDGE_BOUNCE;
    boid.maxForce = random(config.minForce, config.maxForce);
    boid.maxForceSQ = boid.maxForce*boid.maxForce;
    boid.maxSpeed = random(config.minSpeed, config.maxSpeed);
    boid.maxSpeedSQ = boid.maxSpeed*boid.maxSpeed;
    boid.wanderDistance = random(config.minWanderDistance, config.maxWanderDistance);
    boid.wanderRadius = random(config.minWanderRadius, config.maxWanderRadius);
    boid.wanderStep = random(config.minWanderStep, config.maxWanderStep);
    boid.boundsRadius = STAGE.width/2;
    boid.boundsCentre = new Vector3D(STAGE.width/2, STAGE.height/2, 0.0);
    boid.radius = 16;
    //add positoin and velocity
    boid.position.x = boid.boundsCentre.x + random(-100, 100);
    boid.position.y = boid.boundsCentre.y + random(-100, 100);
    boid.position.z = random(-100, 100);
    var vel = new Vector3D(random(-2, 2), random(-2, 2), random(-2, 2));
    boid.velocity.incrementBy(vel);

    boids.push(boid);
  }
}

function render() {
  for (var i = 0;i < boids.length; i++){
    let boid = boids[i];
    boid.wander(0.3);
    // Add a mild attraction to the centre to keep them on screen
    boid.seek(boid.boundsCentre, 0.1);
    // Flock
    boid.flock(boids);
    boid.update();

    verticesColors[i * 5] = (boid.position.x - canvas.width/2)/(canvas.width/2);
    verticesColors[i * 5 + 1] =
      (canvas.height/2 - boid.position.y)/(canvas.height/2);
    verticesColors[i * 5 + 2] = boid.color_r;
    verticesColors[i * 5 + 3] = boid.color_g;
    verticesColors[i * 5 + 4] = boid.color_b;
  }

  if (drawEnabled){
    // Clear <canvas>
    gl.clear(gl.COLOR_BUFFER_BIT);

    initVertexBuffers(gl, firstDraw);
    
    gl.drawArrays(gl.POINTS, 0, boids.length);
    
    firstDraw = false;
  }
}

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

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值