Three.js not rendering correctly when canvas has display: none

I’m using MediaPipe to apply face blendshape scores to a 3D avatar’s morph targets in Three.js.
The avatar is rendered to a canvas element, and I use canvas.captureStream() to extract the video stream from that canvas.

However, the stream I receive sometimes shows rendering issues, like in the screenshot below. The face mesh appears distorted or broken.

After testing, I found that this issue only occurs on certain machines and seems to be directly related to the canvas having display: none or the Scene.background being set to a color.

When I change the canvas to display: block or set the background to an image, the issue completely disappears and the rendered output in the stream appears correctly.

I’m not sure what the root cause of the issue is.

Use opacity:0 and maybe pointer-events:none;

1 Like

Sometimes display: none act as if the element has been removed from the DOM. Try with visibility: hidden or opacity: 0; or both, if the canvas is taking space, create a .hidden css class with position to absolute, and moves it off-screen.

@Fennec @Chaser_Code I tried using opacity: 0, visibility: hidden, or setting position to absolute and moving it off-screen. However, none of these methods solved the problem.

Can you share a minimal code, or a reproduction of the issue.

1 Like

Below is the core implementation of my avatarController:

export class GlbController {
  constructor() {
    this.scene = new THREE.Scene();
    this.renderer = null;
    this.camera = null;
    this.controls = null;
    this.backgroundLoader = new THREE.TextureLoader();
    this.avatar = undefined;
  }

  initThree(canvas) {
    if (!canvas) {
      throw new Error("Canvas element is required to initialize Three.js.");
    }
    this.renderer = new THREE.WebGLRenderer({ canvas, alpha: true, antialias: true });
    this.renderer.setSize(canvas.width, canvas.height);
    this.renderer.setPixelRatio(window.devicePixelRatio);

    this.camera = new THREE.PerspectiveCamera(
      30.0,
      canvas.width / canvas.height,
      0.1,
      20.0
    );


    // Light
    const light = new THREE.DirectionalLight(0xffffff, Math.PI);
    light.position.set(0, 1.0, 2.0).normalize();
    this.scene.add(light);

    this.renderer.outputEncoding = THREE.sRGBEncoding;

    this.scene.background = new THREE.Color(0x87ceeb);
  }

  animateAvatar(results, flipped) {
    if (!results.faceBlendshapes) return;

    const blendShapes = results.faceBlendshapes[0]?.categories;
    if (!blendShapes) return;

    this.avatar?.traverse((obj) => {
      if ("morphTargetDictionary" in obj && "morphTargetInfluences" in obj) {
        const morphTargetDictionary = obj.morphTargetDictionary;
        const morphTargetInfluences = obj.morphTargetInfluences;

        for (const { score, categoryName } of blendShapes) {
          let updatedCategoryName = categoryName;
          if (flipped && categoryName.includes("Left")) {
            updatedCategoryName = categoryName.replace("Left", "Right");
          } else if (flipped && categoryName.includes("Right")) {
            updatedCategoryName = categoryName.replace("Right", "Left");
          }
          const index = morphTargetDictionary[updatedCategoryName];
          morphTargetInfluences[index] = score;
        }
      }
    });
    this.renderer.render(this.scene, this.camera);
  }
}

And here’s where the animateAvatar function is invoked:

  startFaceLandmarker(video) {
    const callback = (results) => {
      this.avatarController?.animateAvatar(results, false);
    }
    if (!this.faceLandmarker) {
      throw new Error("FaceLandmarker not initialized. Call init() first.");
    }
    this.video = video;
    this.streaming = true;
    const processVideo = async () => {
      if (!this.streaming) return;
      const results = await this.faceLandmarker.detectForVideo(this.video, performance.now());
      callback(results);
      this.animationId = requestAnimationFrame(processVideo);
    };
    processVideo();
  }

It would help you get better answers if you put your code in a jsfiddle or similar so that people don’t have to do it for you.