3

I have a basic scene that moves and renders planes.

  • The framerate in Chrome is 120 fps
  • The framerate in Safari is 30 fps

How can I normalise these, and get similar performance across both, ideally toward the 120fps rate?

The 30fps Safari is giving me is a killer.

So far, I have tried using:

this.renderer = new THREE.WebGLRenderer({
  canvas: this.canvas,
  powerPreference: "high-performance",
});

But the powerPreference attribute doesn't seem to make any noticeable difference, so I think it is the requestAnimationFrame timing that I need to fix.

const mod = (k, n) => ((k %= n) < 0) ? k+n : k;
const lerp = (v0, v1, t) => (1 - t) * v0 + t * v1;
const fpsElem = document.querySelector("#fps");

const ThreeCarousel = {
  clock: new THREE.Clock(),
  sizes: {
    width: window.innerWidth,
    height: window.innerHeight
  },
  slideGap: 2,
  slides: [],
  cols: [
    0xC4E7D4,
    0x998DA0,
    0xC4DACF,
    0xB9C0DA,
    0x63585E,
  ],
  p: 0,
  targetP: 0,
  currentX: 0,
  dragReduce: 0.01,
  wheelReduce: 0.001,
  s: 0.01,
  slidePosition(i){
    const max = this.slides.length * this.slideGap;
    const p = mod(this.p + (i * this.slideGap), max)
    
    return p - max / 2;
    
  },
  addObjects() {
    for(var i = 0; i < 5; i++){
      // const geometry = new THREE.BoxGeometry(1, 1, 1);
      const geometry = new THREE.PlaneGeometry(1.1, 1.5);
      const material = new THREE.MeshBasicMaterial({ color: this.cols[i] });
      const mesh = new THREE.Mesh(geometry, material);
      mesh.position.x = this.slidePosition(i);
      this.slides.push(mesh);
      this.scene.add(mesh);
    }
  },
  addCamera() {
    this.camera = new THREE.PerspectiveCamera(
      75,
      this.sizes.width / this.sizes.height,
      0.1,
      100
    );
    this.camera.position.z = 3;
    this.scene.add(this.camera);
  },
  addRenderer() {
    this.renderer = new THREE.WebGLRenderer({
      canvas: this.canvas,
      powerPreference: "high-performance",
    });
    this.renderer.setSize(this.sizes.width, this.sizes.height);
    this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
  },
  addEvents(){
    window.addEventListener("resize", this.resize.bind(this));
    this.canvas.addEventListener("pointerdown", this.startDrag.bind(this));
    this.canvas.addEventListener("pointerup", this.stopDrag.bind(this));
    this.canvas.addEventListener("pointercancel", this.stopDrag.bind(this));
    this.canvas.addEventListener("pointerout", this.stopDrag.bind(this));
    this.canvas.addEventListener("pointermove", this.drag.bind(this));
    this.canvas.addEventListener("wheel", this.wheelDrag.bind(this));
  },
  startDrag(e){
    this.dragging = true;
    this.currentX = e.screenX;
  },
  stopDrag(e){
    this.dragging = false;
  },
  drag(e){
    if(!this.dragging){
      return;
    }
    this.targetP = this.targetP - (this.currentX - e.screenX) * this.dragReduce;
    this.currentX = e.screenX;
  }, 
  wheelDrag(e){
    if(Math.abs(e.deltaY) > Math.abs(e.deltaX)){
      return;
    }
    e.preventDefault();
    e.stopPropagation();
    this.targetP += e.deltaX * this.dragReduce * -1;
  },
  resize(){
    // Update sizes
    this.sizes.width = window.innerWidth;
    this.sizes.height = window.innerHeight;

    // Update camera
    this.camera.aspect = this.sizes.width / this.sizes.height;
    this.camera.updateProjectionMatrix();

    // Update renderer
    this.renderer.setSize(this.sizes.width, this.sizes.height);
    this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
  },
  init() {
    this.canvas = document.querySelector("canvas#carousel");
    this.scene = new THREE.Scene();
    this.addObjects();
    this.addCamera();
    this.addRenderer();
    this.addEvents();
    this.then = 0;

    const tick = (now) => {
      now *= 0.001;                          // convert to seconds
      const deltaTime = now - this.then;          // compute time since last frame
      this.then = now;                            // remember time for next frame
      const fps = 1 / deltaTime;             // compute frames per second
      fpsElem.textContent = fps.toFixed(1);  // update fps display
      this.p = lerp(this.p, this.targetP, 0.06);
      const elapsedTime = this.clock.getElapsedTime();
      this.slides.forEach((slide, i) => {
        slide.position.x = this.slidePosition(i);
        slide.rotation.y = (Math.PI / 30) * slide.position.x + Math.PI / 12;
      })
      this.renderer.render(this.scene, this.camera);
      window.requestAnimationFrame(tick);
    };

    tick();
  }
};

ThreeCarousel.init();
html, body, canvas{
  height: 100%;
}

div{
  position: fixed;
  top: 0;
  left: 0;
  color: #fff;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/0.145.0/three.min.js"></script>

<canvas id="carousel"></canvas>

<div>fps: <span id="fps"></span></div>

There are older answers about throttling framerates and slowing them down, however nothing seems to work at 60fps on Safari. Here is a screenshot of one of the demos found here:

enter image description here

Djave
  • 8,595
  • 8
  • 70
  • 124
  • There's not much you can do to override the user's power preference settings. In fact, they should be respected. If I want low-power mode because my battery is running low, I don't want a site to override my wishes and consume the remainder of it. Many people have their monitor refresh rate at 60hz, so forcing 120fps would just unnecessarily render frames that won't be seen. That's why `requestAnimationFrame` exists; to respect the user's settings. – M - Oct 14 '22 at 17:17
  • @Marquizzo I don't aim this at you, but its just so strange that the above screenshot is taken on two different browsers, on the same machine. But you are right. I have separated my logic into a tick function which runs @ 60fps and a render function that runs at the specified `requestAnimationFrame` interval – Djave Oct 17 '22 at 15:40

1 Answers1

6

I'm embarrassed — looking in System Preferences > Battery > Power Adapter I found that Low Power Mode was checked. Unchecking this gives me a thoroughly unimpressive but bearable 60fps.

Not sure how I will detect if other clients are running in this mode to update my app accordingly, but will have to find a work around.

enter image description here

Djave
  • 8,595
  • 8
  • 70
  • 124
  • Sorry, but what does this question and answer have to do with coding? – Rabbid76 Oct 14 '22 at 15:33
  • 4
    The question is to do with the JavaScript function `requestAnimationFrame`. The answer shows how to make that function run at 60fps. – Djave Oct 14 '22 at 15:54
  • I see your point. However, in my opinion, this is not a proper question for Stack Overflow. – Rabbid76 Oct 14 '22 at 15:56
  • 2
    I also see your point, however after finding the (albeit non-code solution) thought it may help someone with the same issue. Maybe down vote it and we can both go about our lives. – Djave Oct 14 '22 at 15:59
  • It doesn't matter at all. We just disagree, that's all. – Rabbid76 Oct 14 '22 at 16:13