Search code examples
javascriptclassthree.jsthisrequestanimationframe

Animation extremely slow for threejs animation when calling requestAnimationFrame from within class


The best way to describe this issue is to checkout the JSFiddle. The animation will be very choppy when you try to move around the mouse or do anything. I've narrowed it down to having something to do with classing this.requestAnimationFrame(this.animation) which didn't work at first, but after looking on stackoverflow I discovered a fix to say this.animate = this.animate.bind(this); This made it work but extremely slow! How can I make this faster while retaining the class?

JSFiddle:

https://jsfiddle.net/gentleman_goat66/o5wn3bpf/100/

Here is my code below:

HTML

<script type="importmap">
{
"imports": 
  {
    "three": "https://unpkg.com/three@0.151.3/build/three.module.js",
    "OrbitControls": "https://unpkg.com/three@0.151.3/examples/jsm/controls/OrbitControls.js"
  }
}
</script>
<div id="container">
</div>

JS

import * as THREE from 'three';
import { OrbitControls } from 'OrbitControls';

class Heatmap {
  constructor(){
    console.log("Heatmap - constructor()");
    
    //ThreeJS Variables
    this.camera = null;
    this.scene = null;
    this.renderer = new THREE.WebGLRenderer({antialias: true});
    this.orbital_controls = null;
    this.ambientLight = null;
    this.heatmap = new THREE.Object3D(); //This will hold all the meshes that make up the heatmap
    this.animate = this.animate.bind(this);
    
    //Animation Related Variables
    this.clock = new THREE.Clock();
    this.delta = 0;
    this.fps = 60; //60 fps
        this.interval = 1;
        this.seconds = 2; //seconds for animation
        this.timeSoFar = 0;
        this.targetTime = 3;
    
    //Setup
    this.scene = new THREE.Scene();
    this.setUpCamera();
    this.setUpRenderer();
    this.setUpLighting();
    this.setUpOrbitalControls();
    
    //Add optional GridHelper for Debug
    this.scene.add(new THREE.GridHelper());
    
    //TODO: Create the heatmap here
    this.createDataRectangle();
    
    //Add the finished heatmap to the scene
    this.scene.add(this.heatmap);
    this.render();
    this.animate();
  }
  setUpRenderer(){
    console.log("Heatmap - setUpRenderer()");
    let width = $('#container').width();
    let height = $('#container').height();
    this.renderer.setPixelRatio(window.devicePixelRatio);
    this.renderer.setSize(width,height);
    this.renderer.setClearColor(0x404040);
 
    $("#container").html(this.renderer.domElement);
    
    window.addEventListener("resize", event => {
      this.camera.aspect = innerWidth / innerHeight;
      this.camera.updateProjectionMatrix();
      this.renderer.setSize(innerWidth, innerHeight);
    });
  }
  setUpCamera(){
    console.log("Heatmap - setUpCamera()");
    this.camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.01, 100);
    this.camera.position.z = 4;
  }
  setUpLighting(){
    console.log("Heatmap - setUpLighting()");
    this.ambientLight = new THREE.AmbientLight(0xffffff, 1);
    this.scene.add(this.ambientLight);
  }
  setUpOrbitalControls(){
    console.log("Heatmap - setUpOrbitalControls()");
    this.orbital_controls = new OrbitControls( this.camera, this.renderer.domElement );
    this.orbital_controls.target.set(0, 0, 0);
    this.orbital_controls.enableDamping = true;
    this.orbital_controls.dampingFactor = 0.025;
    this.orbital_controls.maxPolarAngle = Math.PI/2.0;
    this.orbital_controls.minDistance = 2.0;
    this.orbital_controls.maxDistance = 80.0;
    this.orbital_controls.update();
    
    this.orbital_controls.addEventListener( 'change', function(){
      if (this.target.y < 0){
          this.target.y = 0;
          /* camera.position.y = -1; */
      } else if (this.target.y > 1){
          this.target.y = 1;
          /* camera.position.y = 1; */
      }
    });
  }
  createDataRectangle(){ //TODO: Make this create at a position specified
    console.log("Heatmap - createDataRectangle()");
    const geometry = new THREE.BufferGeometry();
    const vertices = [
      -2, 0, 1,//0, floor, bottom left
      -1, 0, 1,//1, floor, bottom right
      -1, 0, -1,//2, floor, top right
      -2, 0, -1,//3 floor, top left
      -2, 0, 1,//4 roof, bottom left
      -1, 0, 1,//5 roof, bottom right
      -1, 0, -1,//6 roof, top right
      -2, 0, -1//7 roof, top left
    ];
    const indices = [
      0, 1, 2,//floor, first triangle
      2, 3, 0,//floor, second triangle
      4, 5, 6,//roof, first triangle
      6, 7, 4,//roof, second triangle
      3, 0, 4,//west wall, first triangle
      3, 4, 7,//west wall, second triangle
      0, 1, 5,//south wall, first triangle
      0, 5, 4,//south wall, first triangle
      1, 2, 6,//east wall, first triangle
      1, 6, 5,//east wall, second triangle
      2, 3, 7,//north wall, first triangle
      2, 7, 6,//north wall, second triangle
    ];
    const colors = [
      2,0,0, // bottom left
      0,2,0, // bottom right
      0,2,0, // top right
      2,0,0,  // top left
      2,0,0, // bottom left
      0,2,0, // bottom right
      0,2,0, // top right
      2,0,0  // top left
    ];
    geometry.setIndex(indices);
    geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
    geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
    geometry.computeVertexNormals();
    const material = new THREE.MeshStandardMaterial({side:THREE.FrontSide,vertexColors:true,color: 0xFFFFFF });
    const sample_rectangle = new THREE.Mesh(geometry, material);
    this.heatmap.add(sample_rectangle);
  }
  animate(){
    requestAnimationFrame( this.animate);

    this.orbital_controls.update();//Required for Damping on OrbitControls

    this.delta += this.clock.getDelta();

    if (this.delta  > this.interval) {
      //this.timeSoFar = this.timeSoFar + (1 * morphDirection);

      this.render();
      this.delta = this.delta % this.interval;
    }
  }
  render(){
    this.renderer.render(this.scene, this.camera);
  }
}

$(document).ready(function(){
  console.log("DOM ready!");
  new Heatmap();
});

CSS

body,html{
  margin: 0;
  width: 100%;
  height: 100%;
}
#container {
  width: 100%;
  height: 100%;
  background-color: black;
  margin: 0;
}

Solution

  • I'm not sure what were you expecting the

    this.delta += this.clock.getDelta();
    if (this.delta  > this.interval) {
    

    part would do. But this is your issue, this.interval = 1 here means that the part in the if block will get executed only once every second.
    Maybe you wanted to limit to some FPS, in which case you should have had this.interval = 1 / this.fps; but I don't see a good reason to do this, since limiting to a fixed FPS is VERY hard to get right (you must choose a multiple of the active monitor's native refresh rate, and with variable rate monitors...)
    So the best is probably to get rid of this check entirely and run as fast as the monitor and browser are willing to go.

    body,html{
      margin: 0;
      width: 100%;
      height: 100%;
    }
    #container {
      width: 100%;
      height: 100%;
      background-color: black;
      margin: 0;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <script type="importmap">
    {
    "imports": 
      {
        "three": "https://unpkg.com/three@0.151.3/build/three.module.js",
        "OrbitControls": "https://unpkg.com/three@0.151.3/examples/jsm/controls/OrbitControls.js"
      }
    }
    </script>
    <div id="container">
    </div>
    <script type="module">
    import * as THREE from 'three';
    import { OrbitControls } from 'OrbitControls';
    
    class Heatmap {
      constructor(){
        console.log("Heatmap - constructor()");
        
        //ThreeJS Variables
        this.camera = null;
        this.scene = null;
        this.renderer = new THREE.WebGLRenderer({antialias: true});
        this.orbital_controls = null;
        this.ambientLight = null;
        this.heatmap = new THREE.Object3D(); //This will hold all the meshes that make up the heatmap
        this.animate = this.animate.bind(this);
        
            this.seconds = 2; //seconds for animation
            this.timeSoFar = 0;
            this.targetTime = 3;
        
        //Setup
        this.scene = new THREE.Scene();
        this.setUpCamera();
        this.setUpRenderer();
        this.setUpLighting();
        this.setUpOrbitalControls();
        
        //Add optional GridHelper for Debug
        this.scene.add(new THREE.GridHelper());
        
        //TODO: Create the heatmap here
        this.createDataRectangle();
        
        //Add the finished heatmap to the scene
        this.scene.add(this.heatmap);
        this.render();
        this.animate();
      }
      setUpRenderer(){
        console.log("Heatmap - setUpRenderer()");
        let width = $('#container').width();
        let height = $('#container').height();
        this.renderer.setPixelRatio(window.devicePixelRatio);
        this.renderer.setSize(width,height);
        this.renderer.setClearColor(0x404040);
     
        $("#container").html(this.renderer.domElement);
        
        window.addEventListener("resize", event => {
          this.camera.aspect = innerWidth / innerHeight;
          this.camera.updateProjectionMatrix();
          this.renderer.setSize(innerWidth, innerHeight);
        });
      }
      setUpCamera(){
        console.log("Heatmap - setUpCamera()");
        this.camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.01, 100);
        this.camera.position.z = 4;
      }
      setUpLighting(){
        console.log("Heatmap - setUpLighting()");
        this.ambientLight = new THREE.AmbientLight(0xffffff, 1);
        this.scene.add(this.ambientLight);
      }
      setUpOrbitalControls(){
        console.log("Heatmap - setUpOrbitalControls()");
        this.orbital_controls = new OrbitControls( this.camera, this.renderer.domElement );
        this.orbital_controls.target.set(0, 0, 0);
        this.orbital_controls.enableDamping = true;
        this.orbital_controls.dampingFactor = 0.025;
        this.orbital_controls.maxPolarAngle = Math.PI/2.0;
        this.orbital_controls.minDistance = 2.0;
        this.orbital_controls.maxDistance = 80.0;
        this.orbital_controls.update();
        
        this.orbital_controls.addEventListener( 'change', function(){
          if (this.target.y < 0){
              this.target.y = 0;
              /* camera.position.y = -1; */
          } else if (this.target.y > 1){
              this.target.y = 1;
              /* camera.position.y = 1; */
          }
        });
      }
      createDataRectangle(){ //TODO: Make this create at a position specified
        console.log("Heatmap - createDataRectangle()");
        const geometry = new THREE.BufferGeometry();
        const vertices = [
          -2, 0, 1,//0, floor, bottom left
          -1, 0, 1,//1, floor, bottom right
          -1, 0, -1,//2, floor, top right
          -2, 0, -1,//3 floor, top left
          -2, 0, 1,//4 roof, bottom left
          -1, 0, 1,//5 roof, bottom right
          -1, 0, -1,//6 roof, top right
          -2, 0, -1//7 roof, top left
        ];
        const indices = [
          0, 1, 2,//floor, first triangle
          2, 3, 0,//floor, second triangle
          4, 5, 6,//roof, first triangle
          6, 7, 4,//roof, second triangle
          3, 0, 4,//west wall, first triangle
          3, 4, 7,//west wall, second triangle
          0, 1, 5,//south wall, first triangle
          0, 5, 4,//south wall, first triangle
          1, 2, 6,//east wall, first triangle
          1, 6, 5,//east wall, second triangle
          2, 3, 7,//north wall, first triangle
          2, 7, 6,//north wall, second triangle
        ];
        const colors = [
          2,0,0, // bottom left
          0,2,0, // bottom right
          0,2,0, // top right
          2,0,0,  // top left
          2,0,0, // bottom left
          0,2,0, // bottom right
          0,2,0, // top right
          2,0,0  // top left
        ];
        geometry.setIndex(indices);
        geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
        geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
        geometry.computeVertexNormals();
        const material = new THREE.MeshStandardMaterial({side:THREE.FrontSide,vertexColors:true,color: 0xFFFFFF });
        const sample_rectangle = new THREE.Mesh(geometry, material);
        this.heatmap.add(sample_rectangle);
      }
      animate(){
        requestAnimationFrame(this.animate);
    
        this.orbital_controls.update();//Required for Damping on OrbitControls
        this.render();
      }
      render(){
        this.renderer.render(this.scene, this.camera);
      }
    }
    
    $(document).ready(function(){
      console.log("DOM ready!");
      new Heatmap();
    });
    </script>