Djave
Djave

Reputation: 9329

Safari requestAnimationFrame running at 30fps

I have a basic scene that moves and renders planes.

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

Upvotes: 5

Views: 1941

Answers (1)

Djave
Djave

Reputation: 9329

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

Upvotes: 10

Related Questions