javascriptbrowserthree.jswebgl

The fps reduced to half when I set the encoding of a realtime updated texture to sRGB in Threejs


I have a threejs application that has to update a texture in each frame, the output encoding of the THREE.WebGLRenderer is sRGB.

When I set the texture encoding to sRGB, the rendering result is right, and the fps on the Mac Chrome is about 26~29, and 30~40 on HUAWEI Meta40; if I don't set the texture encoding, the rendering result is wrong, and the fps is 60 and stable both on Mac Chrome and HUAWEI Meta40.

I want to know what happened when I set the texture encoding to sRGB which slowed down the fps? Can it be possible to improve the fps and keep the result right?

Here is the demo code:

<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>sRGB Demo</title>
  <style>
    body {
      /* overflow: hidden; */
      border: none;
      margin: 0;
      background-color: #f7f5f4;
    }

    #container {
      position: absolute;
      left: 0;
      top: 0;
      width: 100vw;
      height: 100vh;
    }
    #canvas {
      position: absolute;
      left: 0;
      top: 0;
      width: 100%;
      height: 100%;
    }
  </style>
</head>

<body>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/0.149.0/three.min.js"></script>
  <div id="container">
    <canvas id="canvas"></canvas>
  </div>
  <script>
    class App {
      constructor(container, image) {
        this.image = image;

        this.fpsDom = document.createElement('div');
        this.fpsDom.style.position = 'fixed';
        this.fpsDom.style.left = '20px';
        this.fpsDom.style.top = '20px';
        this.fpsDom.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
        this.fpsDom.style.color = '#fff';
        this.fpsDom.style.padding = '5px';
        this.fpsDom.innerText = '0.0'
        document.body.appendChild(this.fpsDom);
        this.frameCount = 0;
        this.lastFpsTime = 0;

        this.webglCanvas = document.getElementById('canvas');
        const rect = this.webglCanvas.getBoundingClientRect();
        this.webglCanvas.width = rect.width * window.devicePixelRatio;
        this.webglCanvas.height = rect.height * window.devicePixelRatio;
        // console.log(this.webglCanvas.width, this.webglCanvas.height);

        this.initThree();

        const render = () => {
          const tm = performance.now();

          requestAnimationFrame(render);

          ++this.frameCount;
          const now = performance.now();
          if (now - this.lastFpsTime>1000) {
            this.fpsDom.innerText = (this.frameCount*1000/(now-this.lastFpsTime)).toFixed(1);
            this.frameCount = 0;
            this.lastFpsTime = now;
          }

          if (!this.videoMesh.material.map) {
            this.videoMesh.material.map = new THREE.CanvasTexture(this.image);
            this.videoMesh.material.map.generateMipmaps = false;
            this.videoMesh.material.map.minFilter = THREE.LinearFilter;
            this.videoMesh.material.map.magFilter = THREE.LinearFilter;
            this.videoMesh.material.map.encoding = THREE.sRGBEncoding;
          } else {
            this.videoMesh.material.map.needsUpdate = true;
          }
          this.renderer.render(this.scene, this.camera);

          // console.log('render', (performance.now() - tm).toFixed(1));
        }

        render();

      }

      initThree() {
        this.renderer = new THREE.WebGLRenderer({
          alpha: false,
          antialias: false,
          canvas: this.webglCanvas,
        });

        this.renderer.setPixelRatio(1);
        this.renderer.setSize(this.webglCanvas.width, this.webglCanvas.height, false);
        this.renderer.outputEncoding = THREE.sRGBEncoding;

        this.camera = new THREE.PerspectiveCamera();
        this.camera.position.set(0, 0, 1);

        const geometry = new THREE.PlaneGeometry( 1, 1 );
        const material = new THREE.MeshBasicMaterial();
        this.videoMesh = new THREE.Mesh( geometry, material );
        this.videoMesh.position.set(0, 0, 0);

        this.scene = new THREE.Scene();
        this.scene.add(this.videoMesh);
      }
    }

    const container = document.getElementById('container');
    const img = new Image();
    img.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAMAAADXqc3KAAAB+FBMVEUAAAA/mUPidDHiLi5Cn0XkNTPmeUrkdUg/m0Q0pEfcpSbwaVdKskg+lUP4zA/iLi3msSHkOjVAmETdJSjtYFE/lkPnRj3sWUs8kkLeqCVIq0fxvhXqUkbVmSjwa1n1yBLepyX1xxP0xRXqUkboST9KukpHpUbuvRrzrhF/ljbwaljuZFM4jELaoSdLtElJrUj1xxP6zwzfqSU4i0HYnydMtUlIqUfywxb60AxZqEXaoifgMCXptR9MtklHpEY2iUHWnSjvvRr70QujkC+pUC/90glMuEnlOjVMt0j70QriLS1LtEnnRj3qUUXfIidOjsxAhcZFo0bjNDH0xxNLr0dIrUdmntVTkMoyfL8jcLBRuErhJyrgKyb4zA/5zg3tYFBBmUTmQTnhMinruBzvvhnxwxZ/st+Ktt5zp9hqota2vtK6y9FemNBblc9HiMiTtMbFtsM6gcPV2r6dwroseLrMrbQrdLGdyKoobKbo3Zh+ynrgVllZulTsXE3rV0pIqUf42UVUo0JyjEHoS0HmsiHRGR/lmRz/1hjqnxjvpRWfwtOhusaz0LRGf7FEfbDVmqHXlJeW0pbXq5bec3fX0nTnzmuJuWvhoFFhm0FtrziBsjaAaDCYWC+uSi6jQS3FsSfLJiTirCOkuCG1KiG+wSC+GBvgyhTszQ64Z77KAAAARXRSTlMAIQRDLyUgCwsE6ebm5ubg2dLR0byXl4FDQzU1NDEuLSUgC+vr6urq6ubb29vb2tra2tG8vLu7u7uXl5eXgYGBgYGBLiUALabIAAABsElEQVQoz12S9VPjQBxHt8VaOA6HE+AOzv1wd7pJk5I2adpCC7RUcHd3d3fXf5PvLkxheD++z+yb7GSRlwD/+Hj/APQCZWxM5M+goF+RMbHK594v+tPoiN1uHxkt+xzt9+R9wnRTZZQpXQ0T5uP1IQxToyOAZiQu5HEpjeA4SWIoksRxNiGC1tRZJ4LNxgHgnU5nJZBDvuDdl8lzQRBsQ+s9PZt7s7Pz8wsL39/DkIfZ4xlB2Gqsq62ta9oxVlVrNZpihFRpGO9fzQw1ms0NDWZz07iGkJmIFH8xxkc3a/WWlubmFkv9AB2SEpDvKxbjidN2faseaNV3zoHXvv7wMODJdkOHAegweAfFPx4G67KluxzottCU9n8CUqXzcIQdXOytAHqXxomvykhEKN9EFutG22p//0rbNvHVxiJywa8yS2KDfV1dfbu31H8jF1RHiTKtWYeHxUvq3bn0pyjCRaiRU6aDO+gb3aEfEeVNsDgm8zzLy9egPa7Qt8TSJdwhjplk06HH43ZNJ3s91KKCHQ5x4sw1fRGYDZ0n1L4FKb9/BP5JLYxToheoFCVxz57PPS8UhhEpLBVeAAAAAElFTkSuQmCC';
    img.onload = () => {
      const c = document.createElement('canvas');
      c.width = 1080;
      c.height = 1920;
      c.getContext('2d').drawImage(img, 0, 0, c.width, c.height);
      const app = new App(container, c);
    }
  </script>
</body>

</html>

UPDATE:

I also found that the render function itself runs very fast(about 1ms), but the requestAnimationFrame calling period becomes very long when I set the texture encoding to sRGB, so I think the GPU must be doing something that blocks the browser from updating the next frame.


Solution

  • I found an issue with this problem: https://github.com/mrdoob/three.js/issues/26183

    So I transcode the texture from sRGB to Linear manually in the shader to solve this problem.