I’m trying to use a web-worker to render a scene with threejs. I want to render some dynamic font using a CanvasTexture. But I find that when I use canvas in web-worker from transferControlToOffscreen, if I change the text, the render loop will stop, but this doesn't happen with new OffscreenCanvas().
//if use new OffscreenCanvas, render loop will work fine.
//if not, canvas2 is transferd from main thread, when change the text, render loop will stop sometimes
//canvas2=new OffscreenCanvas(200,200);
canvas2.height = 200;
canvas2.width = 200;
let context = canvas2.getContext('2d');
context.font = "40px Helvetica";
context.fillStyle = 'red';
context.fillText("123", canvas2.width / 2, canvas2.height / 2);
this.sprite = new THREE.Sprite(new THREE.SpriteMaterial({map: new THREE.CanvasTexture(canvas2)}));
this.sprite.scale.set(50, 50, 50);
this.scene.add(this.sprite);
If I declare a canvas in the HTML and simply hide it, after transfered to worker, the problem doesn’t occur.
<canvas id="canvas3"style="display: none;" ></canvas>
// spriteCanvas2 = document.createElement('canvas').transferControlToOffscreen();
spriteCanvas2=document.getElementById('canvas3').transferControlToOffscreen();
The live sample is here.The top canvas run in main thread and the bottom run in worker. If click the bottom canvas, it will stop render sometimes. Some additional information is in the forum.
If run in local not codepen, it will trigger consistently in three clicks. If not trigger, please refresh the browser.
Could anyone tell the difference between them? Thanks in advance.
This is a Chrome bug, caused by the Garbage Collection of the spriteCanvas2's original <canvas>.
Indeed, when you do spriteCanvas2=document.createElement('canvas').transferControlToOffscreen();, the intermediary <canvas> returned by document.createElement('canvas') isn't held by anything, and thus, it's marked as being garbage collectable.
The Worker's side animation frame provider will lock onto the last draw commands have been passed to the placeholder canvas's buffer (the one in the main thread). Since it doesn't exist anymore, it's stuck.
This has nothing to do with Three.js and can be reproduced in a more minimal test-case:
const workerScript = `
// If we didn't run rAF in the last second, the renderer is locked
const notifyDeath = () => setTimeout(() => postMessage("dead"), 1000);
self.onmessage = ({ data: canvas }) => {
const ctx = canvas.getContext("2d");
let timer = notifyDeath();
const anim = () => {
clearTimeout(timer);
timer = notifyDeath();
ctx.clearRect(0, 0, 300, 150);
requestAnimationFrame(anim);
}
requestAnimationFrame(anim);
};
`;
const workerURL = URL.createObjectURL(new Blob([workerScript], { type: "text/javascript" }));
const worker = new Worker(workerURL);
worker.onerror = console.error;
// get a WeakRef from the <canvas> element
const weakCanvas = new WeakRef(document.createElement("canvas"));
const offscreen = weakCanvas.deref()
.transferControlToOffscreen();
worker.postMessage(offscreen, [offscreen]);
//
worker.onmessage = ({ data }) => {
console.log("message from worker:", { data }); // died
document.body.style.background = "red";
console.log("is still reffed?", weakCanvas.deref() !== undefined); // undefined
}
// Force garbage production
onclick = () => {
const a = Array.from({ length: 10e5 }, () => ({ a: Math.random() }));
}
You may have to wait quite some time before the object is dereferenced.<br>
You may click anywhere in this frame to try to force garbage production.<br>
Beware, switching tab might trigger the check too, since rAF is throttled in that case.
So to workaround it, you could keep a reference to the <canvas>, or use an OffscreenCanvas directly since you won't ever render that <canvas> anyway.