27

Worker中的OffscreenCanvas渲染实践与浅析 | ¥ЯႭ1I0

 4 years ago
source link: https://yrq110.me/post/front-end/offscreen-canvas-practice/?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Tl;DR:

  1. OffscreenCanvas可以让你在Worker线程中渲染图形,支持多种RenderingContext
  2. 两种使用方式:同步的Transfer模式与异步的Control模式
  3. 将Canvas的逻辑计算与渲染分离,避免UI线程阻塞

产生的契机:用户在交互时的Canvas逻辑与渲染在同一线程内执行,动画产生的卡顿可能会影响用户体验。若在后台渲染,则可以避免耗时的渲染任务阻塞主线程。

使用OffscreenCanvas与Worker结合的方式可以将渲染任务放在子线程中,有效提升用户交互时的界面流畅度。

两种使用方式

Transfer模式Control模式

自己起的名字,参考了这篇文章。

Transfer模式

  1. worker线程中创建OffscreenCanvas对象并执行渲染,给主线程返回结果(缓冲区图像或其它数据)
  2. 主线程使用缓冲区数据渲染Canvas元素

worker线程

let offscreen = new OffscreenCanvas(w,h);
let ctx = offscreen.getContext('2d');
// 一些渲染操作...
let image = offscreen.transferToImageBitmap();
self.postMessage({ image }, [image]);

主线程

renderWorker.onmessage = msg => {
  let imageBuffer = msg.data.image;
  let bitmapContext = canvas.getContext("bitmaprenderer");
  bitmapContext.transferFromImageBitmap(imageBuffer);
}

这种方式可以用于H5游戏的精灵加载,文本渲染、生成海报等固定的渲染任务。

Control模式

  1. 主线程中移交Canvas元素的控制权
  2. 在worker线程执行所有的渲染操作,无需图像数据的传递即可更新Canvas元素

在该模式下不需要transfer的相关操作,内部直接对绑定的dom元素进行更新。

主线程

const offscreen = document.querySelector('canvas').transferControlToOffscreen();
worker.postMessage({ canvas: offscreen }, [offscreen]);

worker线程

onmessage = e => {
  let canvas = e.data.canvas
  let ctx = canvas.getContext('2d');
  // 一些渲染操作...
  // 这里的渲染操作会在Canvas元素上同步绘制的图像
}

加载worker文件

在使用webpack进行构建的项目中,对于worker文件需要进行一些额外的处理。

目前生态中主要有三种处理web worker的loader: worker-loader,workerize-loadercomlink-loader

关于这几种loader的介绍可以看看这篇后面的loader部分。

经测试,由于workerize-loader目前没有对postMessage方法中的Transferable参数序列进行处理,因此无法将主线程的OffscreenCanvas对象传入worker中,详情见这个issueworker-loader表现正常

在worker中使用图像数据与主线程中有所不同。

主线程

const loadImage = imgPath => {
  return new Promise((resolve, reject) => {
    let img = new Image();
    img.setAttribute("crossOrigin", "anonymous"); // to solve "Tainted canvases may not be exported" error
    img.onload = () => { resolve(img); };
    img.onerror = e => { reject(new Error(e));};
    img.src = imgPath;
  });
};

const image = await loadImage(url)
// use image...

worker线程

const response = await fetch(url);
const blob = await response.blob();
const image = await createImageBitmap(blob);
// use image...

在worker线程中执行动画有两种方式:

  1. Timer - setTimeout, setInterval等
  2. rAF - rAF在DedicatedWorker中已经实现,与Window中的行为一致。

了解了OffscreenCanvas与worker的相关特性,不如动手尝试一下,以一个蒙版合成的渲染任务为例。

在主线程中执行绘制

/* 0. 获取Canvas元素 */
let mainCanvas = document.querySelector('#canvas');
let mainCtx = mainCanvas.getContext('2d');
/* 1. 准备Image对象 */
let img = await loadImage(imgPath);  
...
/* 2. 创建一个Canvas来合成结果图像 */
let maskLayer = document.createElement("canvas");
maskLayer.width = width;
maskLayer.height = height;
const maskCtx = maskLayer.getContext("2d");
maskCtx.drawImage(img, 0, 0);
let maskData = maskCtx.getImageData(0, 0, width, height);
for (let i = 0; i < width * height; i++) {
  if (values[i] !== 255) {
    maskData.data[(i + 1) * 4 - 1] = mask[i];
  }
}
maskCtx.putImageData(maskData, 0, 0);
...
/* 3. 绘制到Canvas元素上 */
mainCtx.drawImage(maskLayer, 0, 0);

在worker线程中执行绘制

主线程

import CanvasWorker from "worker-loader!@/workers/canvas.worker.js";
...
/* 0. 获取Canvas元素 */
let canvas = document.querySelector('#canvas');
let offscreenCanvas = canvas.transferControlToOffscreen();
let canvasWorker = new CanvasWorker();
// 将绑定的offscreenCanvas实例传递到worker线程中
canvasWorker.postMessage({ canvas: offscreenCanvas, event: "init" }, [offscreenCanvas]);
...
/* 2.发送绘制事件 */
let img = this.resultImg || this.img;
canvasWorker.postMessage({
  event: "draw"
  payload: JSON.stringify({ width, height, imgSrc, mask }) // 由于结构化克隆算法的限制,这里对参数对象进行JSON序列化后赋值
});

worker线程

let canvas, ctx;
onmessage = async e => {
  const data = e.data;
  const { payload, event } = data;
  switch (event) {
    case "init": {
      // 保存传入的OffscreenCanvas实例
      canvas = data.canvas;
      ctx = canvas.getContext("2d");
      break;
    }
    case "draw": {
      /* 0. 解析参数 */
      let { width, height, mask } = JSON.parse(payload);
      ...
      /* 1. 下载图片并获取的ImageBitmap数据 */
      const response = await fetch(imgSrc);
      const blob = await response.blob();
      const imageBitmap = await createImageBitmap(blob);
      ...
      /* 2. 创建一个新的OffscreenCanvas来合成结果图像 */
      let maskLayer = new OffscreenCanvas(width, height);
      const maskCtx = maskLayer.getContext("2d");
      maskCtx.drawImage(imageBitmap, 0, 0); // 使用ImageBitMap绘制图片
      let maskData = maskCtx.getImageData(0, 0, width, height);
      for (let i = 0; i < width * height; i++) {
        if (values[i] !== 255) {
          maskData.data[(i + 1) * 4 - 1] = mask[i];
        }
      }
      maskCtx.putImageData(maskData, 0, 0);
      ...
      /* 3. Canvas元素上会更新绘制效果 */
      ctx.drawImage(maskLayer, 0, 0);
      break;
    }
  }
};

极简Chromium渲染流水线:

blink —(Main Frame)—> Layer Compositor —(Compositor Frame)—> Display Compositor —(GL/UI Frame)—> Window

  • 在OffscreenCanvas中渲染省去了Main Frame中的部分计算任务。
  • Control模式中OffscreenCanvas对Canvas元素的更新不再与主线程中的其他元素保持同步,因为它们通过不同的渲染流水线。
  • OffscreenCanvas类中提供了一些Layer Compositor阶段的执行方法:Commit, BeginFramePushFrame等。处理后的数据直接交给Display Compositor渲染,走最短的渲染路径。
  • 使用Transfer模式可以实现Canvas元素与其它元素的同步更新。

blink中offscreen_canvas与html_canvas_element的主类源码:

OffscreenCanvasHTMLCanvasElement共同继承的类:

  • ImageBitmapSource - ImageBitmapSource API
  • CanvasRenderingContextHost - 文档中的描述:the base class for all elements that can host a rendering context,包含通用的数据转换、尺寸设置、属性获取等方法

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK