Vertago
Vertago

Reputation: 319

Lagging FPS in animation with requestAnim or TimeOut

I'm trying to get a good animation with constant fps, but nothing works. I'm using threejs, webgl to render the scene and for the animation loop I found two ways (is there a third?), which is either requestAnimationFrame(...) or by setTimeOut(). Both don't guarantee that the fps is constant, but I'm fixing it by updating the object position by the timedelta of window.performance.now(). But I still have lagspikes which one can clearly see. So how can I fix this? It's obviously possibly because there are games like doom which don't lag.

My example with full src-code can be found here:

http://sc2tube.com:8080/test/three.html

the relevant code:

function animate() {

requestAnimationFrame( animate );

// calculate how long the last frame was 
var timefix = (window.performance.now() - last)/(1000/30);

last = window.performance.now();

var oldX = object.position.x;
// calculate updateX including the timefix
var updateX = oldX + (10 / 30 * 100) * dx * timefix;

// update the position of the object
object.position.x = updateX;  
// render the scene
renderer.render(scene, camera);

}

worker.js:

self.addEventListener('message', function(e) {

setInterval(function(){ 

    now = self.performance.now()
    timefix = (now - last)/(1000/100);
    last = now;

    x += 5*timefix*dx;

    self.postMessage(x);

}, 1000/100);

}, false);

var test;  
	var dx = 1, dy = 0;
	var speed = 0.5;

	var activeKey = 0;
    // Set up the scene, camera, and renderer as global variables.
    var scene, camera, renderer;

    init();
    animate();

    // Sets up the scene.
    function init() {

      // Create the scene and set the scene size.
      scene = new THREE.Scene();
      var WIDTH = window.innerWidth - 50,
          HEIGHT = 500;

      // Create a renderer and add it to the DOM.
      renderer = new THREE.WebGLRenderer({antialias:true});
      renderer.setSize(WIDTH, HEIGHT);
      document.body.appendChild(renderer.domElement);

      camera = new THREE.OrthographicCamera( 0, WIDTH, 200, -HEIGHT, 1, 1000 );
      
      camera.position.set(0,0,100);
      scene.add(camera);
      console.log(WIDTH);
      
      
      window.addEventListener('resize', function() {
    	  var WIDTH = window.innerWidth - 50,
          HEIGHT = window.innerHeight - 50;
        renderer.setSize(WIDTH, HEIGHT);
        camera.aspect = WIDTH / HEIGHT;
        camera.updateProjectionMatrix();
        
      });
      renderer.setClearColor();

      var loader = new THREE.ObjectLoader();
  	  
      loader.parse({
    "metadata" : {
        "type" : "Object",
        "version" : 4.3,
        "generator" : "Blender Script"
    },
    "object" : {
        "name" : "red_cube.Material",
        "type" : "Mesh",
        "uuid" : "6071e8f2-79ae-5660-8d2b-aa675c566703",
        "matrix" : [1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1],
        "geometry" : "5d6cbd93-cf58-58a9-b0a7-5be9e5794547",
        "material" : "5e847bd4-84a9-5d4b-a8fb-c567e27f7561"
    },
    "geometries" : [{
            "name" : "red_cube.Material",
            "type" : "BufferGeometry",
            "uuid" : "5d6cbd93-cf58-58a9-b0a7-5be9e5794547",
            "data" : {
                "attributes" : {
                    "position" : {
                        "type" : "Float32Array",
                        "itemSize" : 3,
                        "array" : [0.79906648,-0.73424673,-0.87263167,0.79906648,-0.73424661,1.1273682,-1.2009337,-0.73424661,1.1273681,-1.2009332,-0.73424673,-0.87263215,0.79906696,1.2657533,-0.87263131,-1.2009335,1.2657533,-0.87263179,-1.2009339,1.2657533,1.1273677,0.79906583,1.2657533,1.1273688,0.79906648,-0.73424673,-0.87263167,0.79906696,1.2657533,-0.87263131,0.79906583,1.2657533,1.1273688,0.79906648,-0.73424661,1.1273682,0.79906648,-0.73424661,1.1273682,0.79906583,1.2657533,1.1273688,-1.2009339,1.2657533,1.1273677,-1.2009337,-0.73424661,1.1273681,-1.2009337,-0.73424661,1.1273681,-1.2009339,1.2657533,1.1273677,-1.2009335,1.2657533,-0.87263179,-1.2009332,-0.73424673,-0.87263215,0.79906696,1.2657533,-0.87263131,0.79906648,-0.73424673,-0.87263167,-1.2009332,-0.73424673,-0.87263215,-1.2009335,1.2657533,-0.87263179]
                    },
                    "normal" : {
                        "type" : "Float32Array",
                        "itemSize" : 3,
                        "array" : [-1.0658141e-14,-1,5.9604645e-08,-1.0658141e-14,-1,5.9604645e-08,-1.0658141e-14,-1,5.9604645e-08,-1.0658141e-14,-1,5.9604645e-08,0,1,0,0,1,0,0,1,0,0,1,0,1,4.4703416e-08,2.8312209e-07,1,4.4703416e-08,2.8312209e-07,1,4.4703416e-08,2.8312209e-07,1,4.4703416e-08,2.8312209e-07,-2.9802322e-07,-5.9604723e-08,1,-2.9802322e-07,-5.9604723e-08,1,-2.9802322e-07,-5.9604723e-08,1,-2.9802322e-07,-5.9604723e-08,1,-1,-1.1920929e-07,-2.3841858e-07,-1,-1.1920929e-07,-2.3841858e-07,-1,-1.1920929e-07,-2.3841858e-07,-1,-1.1920929e-07,-2.3841858e-07,2.3841858e-07,1.7881393e-07,-1,2.3841858e-07,1.7881393e-07,-1,2.3841858e-07,1.7881393e-07,-1,2.3841858e-07,1.7881393e-07,-1]
                    },
                    "index" : {
                        "type" : "Uint32Array",
                        "itemSize" : 1,
                        "array" : [0,1,2,2,3,0,4,5,6,6,7,4,8,9,10,10,11,8,12,13,14,14,15,12,16,17,18,18,19,16,20,21,22,22,23,20]
                    }
                }
            }
        }],
    "materials" : [{
            "name" : "Material",
            "type" : "MeshBasicMaterial",
            "uuid" : "5e847bd4-84a9-5d4b-a8fb-c567e27f7561",
            "transparent" : false,
            "opacity" : 1,
            "color" : 10682379
        }]
}, function(object){
       		test = object;
       		object.scale.set(50,50,50);
       		scene.add(object)
       		
      });
      
  	document.addEventListener('keydown', function(e) {
  	    if (activeKey == e.keyCode) return;
  	    activeKey = e.keyCode;
  	    
  	    //left
  	    if (e.keyCode == 37) {
  	        dx = -1;
  	    }
  	    //top
  	    else if (e.keyCode == 38) {
  	        dy = 1;
  	    }
  	    //right
  	    else if (e.keyCode == 39) {
  	        dx = 1;
  	    }
  	    //bottom
  	    else if (e.keyCode == 40) {
  	        dy = -1;
  	    }
  	});
  	document.addEventListener('keyup', function(e) {
  	    switch (e.keyCode) {
  	        case 37: // left
  	        case 39: // right
  	            dx = 0;
  	            break;
  	            
  	        case 38: // up
  	        case 40: // down
  	            dy = 0;
  	            break;
  	    }
  	    
  	    activeKey = 0;
  	});

    }
    
    var start;
    var last;

    function animate() {

        requestAnimationFrame( animate );
                
    	if(start == null) {
    		start = window.performance.now();
    		last = start;
    	}
    	
    	var timefix = (window.performance.now() - last)/(1000/30);
    	
    	last = window.performance.now();
    	
        if(test != null) {
       	    var oldX = test.position.x;
       	    var oldY = test.position.y;
       	    
       	    var updateX = oldX + (10 / 30 * 100) * dx * speed * timefix;
       	    var updateY = oldY + (10 / 30 * 100) * dy * speed * timefix;  
       	    
       	    if(updateX > 1800 ) {
       	    	dx = -1;
       	    } else if(updateX < 100) {       	    	
       	    	dx = 1;
       	    }
       	    
       	    test.position.x = updateX;  
       	    test.position.y =  oldY + (10 / 30 * 100) * dy * speed * timefix;  
       	    
       	 	var text = document.getElementById('panel');
       	 	text.innerHTML =  timefix;
       		renderer.render(scene, camera);
       	}

    }
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/84/three.js"></script>
<body style="margin: 0;">

<div id="panel">TEST
</div>
<br>

</body>

Upvotes: 4

Views: 1214

Answers (1)

Tschallacka
Tschallacka

Reputation: 28722

The problem is something called game ticks.

What you need is threads. one rendering thread. one game thread.

For the game thread I advice a webworker:

https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers

You let the game thread run every 50ms to update game logic. it should 'sleep" inbetween. You post stuff back to rendering thread, which updates everything and interprolates the trajectories for currentpos to next pos in 50 ms.

  • tick 1. 0MS

    • game thread:
      • spawn block at 0.0
      • push gamestate to rendering thread
    • rendering thread
      • gather entities.
      • update entities for location if moving
      • draw entities at pos
  • tick 2. 50ms

    • game thread:
      • get entities
      • trigger update functions
        • red block moves left 20
        • push gamestate to rendering thread
    • rendering thread
      • gather entities.
      • update entities for location if moving
        • red block moves from 0 to 20
        • avg frames per tick 4
        • interprolated movement per tick, 5 px.
        • update red to 5
      • draw entities at pos

edit Added example code of how to utilise render threads.

Basically you have the same objects in the game thread(webworker) as in the render thread. Only difference is, render thread has has render instructions(onRender) and game loop has Update instructions(on update)

So they are the same, but also different.

Take a look.

function getInlineJS() {
    var js = document.querySelector('[type="javascript/worker"]').textContent;
    var blob = new Blob([js], {"type": "text\/plain"});
    return URL.createObjectURL(blob);
}



var RedCube = function(id) {
    this.cube = null;
    this.type = 'redcube';
    if(typeof id === undefined) {
       this.entityId = generateId();
    }
    else {
        this.entityId = id;
    }
    this.lastX = 0;
    this.x = 0;
}
RedCube.prototype.getType = function() {
   return this.type;
}
RedCube.prototype.onUpdate = function() {
    this.x += 20;
}
RedCube.prototype.loadCube = function(scene, renderer) {
var that = this;
       
        var loader = new THREE.ObjectLoader();
  	        loader.parse({
    "metadata" : {
        "type" : "Object",
        "version" : 4.3,
        "generator" : "Blender Script"
    },
    "object" : {
        "name" : "red_cube.Material",
        "type" : "Mesh",
        "uuid" : "6071e8f2-79ae-5660-8d2b-aa675c566703",
        "matrix" : [1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1],
        "geometry" : "5d6cbd93-cf58-58a9-b0a7-5be9e5794547",
        "material" : "5e847bd4-84a9-5d4b-a8fb-c567e27f7561"
    },
    "geometries" : [{
            "name" : "red_cube.Material",
            "type" : "BufferGeometry",
            "uuid" : "5d6cbd93-cf58-58a9-b0a7-5be9e5794547",
            "data" : {
                "attributes" : {
                    "position" : {
                        "type" : "Float32Array",
                        "itemSize" : 3,
                        "array" : [0.79906648,-0.73424673,-0.87263167,0.79906648,-0.73424661,1.1273682,-1.2009337,-0.73424661,1.1273681,-1.2009332,-0.73424673,-0.87263215,0.79906696,1.2657533,-0.87263131,-1.2009335,1.2657533,-0.87263179,-1.2009339,1.2657533,1.1273677,0.79906583,1.2657533,1.1273688,0.79906648,-0.73424673,-0.87263167,0.79906696,1.2657533,-0.87263131,0.79906583,1.2657533,1.1273688,0.79906648,-0.73424661,1.1273682,0.79906648,-0.73424661,1.1273682,0.79906583,1.2657533,1.1273688,-1.2009339,1.2657533,1.1273677,-1.2009337,-0.73424661,1.1273681,-1.2009337,-0.73424661,1.1273681,-1.2009339,1.2657533,1.1273677,-1.2009335,1.2657533,-0.87263179,-1.2009332,-0.73424673,-0.87263215,0.79906696,1.2657533,-0.87263131,0.79906648,-0.73424673,-0.87263167,-1.2009332,-0.73424673,-0.87263215,-1.2009335,1.2657533,-0.87263179]
                    },
                    "normal" : {
                        "type" : "Float32Array",
                        "itemSize" : 3,
                        "array" : [-1.0658141e-14,-1,5.9604645e-08,-1.0658141e-14,-1,5.9604645e-08,-1.0658141e-14,-1,5.9604645e-08,-1.0658141e-14,-1,5.9604645e-08,0,1,0,0,1,0,0,1,0,0,1,0,1,4.4703416e-08,2.8312209e-07,1,4.4703416e-08,2.8312209e-07,1,4.4703416e-08,2.8312209e-07,1,4.4703416e-08,2.8312209e-07,-2.9802322e-07,-5.9604723e-08,1,-2.9802322e-07,-5.9604723e-08,1,-2.9802322e-07,-5.9604723e-08,1,-2.9802322e-07,-5.9604723e-08,1,-1,-1.1920929e-07,-2.3841858e-07,-1,-1.1920929e-07,-2.3841858e-07,-1,-1.1920929e-07,-2.3841858e-07,-1,-1.1920929e-07,-2.3841858e-07,2.3841858e-07,1.7881393e-07,-1,2.3841858e-07,1.7881393e-07,-1,2.3841858e-07,1.7881393e-07,-1,2.3841858e-07,1.7881393e-07,-1]
                    },
                    "index" : {
                        "type" : "Uint32Array",
                        "itemSize" : 1,
                        "array" : [0,1,2,2,3,0,4,5,6,6,7,4,8,9,10,10,11,8,12,13,14,14,15,12,16,17,18,18,19,16,20,21,22,22,23,20]
                    }
                }
            }
        }],
    "materials" : [{
            "name" : "Material",
            "type" : "MeshBasicMaterial",
            "uuid" : "5e847bd4-84a9-5d4b-a8fb-c567e27f7561",
            "transparent" : false,
            "opacity" : 1,
            "color" : 10682379
        }]
}, function(object){
       		that.cube = object;
       		object.scale.set(50,50,50);
       		scene.add(object)
       		
      });
}
RedCube.prototype.onRender = function(scene, renderer) {
   if(this.cube === null) {
       this.loadCube(scene, renderer);
   }
   
   // Some interprolation logic here to move from lastpos to next pos in average frames
   // per tick.
   this.cube.position.x = this.x;
}

RedCube.prototype.getType = function() {
   return type;
}

RedCube.prototype.generateSyncPacket = function() {
   return {
            type: this.getType(),
            x : this.x
          };
}

RedCube.prototype.parseSyncPacket = function(syncpacket) {
   this.setPosition(syncpacket.x);
}

RedCube.prototype.generateId = function() {
    var d = new Date().getTime();
    if (typeof performance !== 'undefined' && typeof performance.now === 'function'){
        d += performance.now(); //use high-precision timer if available
    }
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
        var r = (d + Math.random() * 16) % 16 | 0;
        d = Math.floor(d / 16);
        return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
    });

}


RedCube.prototype.getEntityId = function() {
    return this.entityId;
}
RedCube.prototype.beforeDeath = function() {
}

RedCube.prototype.die = function() {
}

RedCube.prototype.setPosition = function(newpos) {
    this.lastX = this.x;
    this.x = newpos;
}

RedCube.prototype.getPosition = function() {
    return this.x;
}
RedCube.prototype.getLastX = function() {
    return this.lastX;
}

var EntityRegistry = function() {
   this.entities = {};
   this.types = {};
}

EntityRegistry.prototype.register = function(entity) {
   this.entities[entity.getEntityId()] = entity;
   
}
EntityRegistry.prototype.callUpdate = function() {
  for(entityId in this.entities) {
      if(this.entities.hasOwnProperty(entityId)) {
          this.entities[entityId].onUpdate();
      }
   }
}
EntityRegistry.prototype.callOnRender = function(scene, renderer) {
  for(entityId in this.entities) {
      if(this.entities.hasOwnProperty(entityId)) {
          this.entities[entityId].onRender(scene, renderer);
      }
   }
}
EntityRegistry.prototype.remove = function(entity) {
   entity.beforeDeath();
   delete this.entities[entity.getEntityId()]
   entity.die();
}

EntityRegistry.prototype.registerType = function(name, entityClass) {
    this.types[name] = entityClass;
}
EntityRegistry.prototype.startEntity = function(syncpacket, entityId) {
    var entity = new this.types[syncpacket.type](entityId);
    entity.parseSyncPacket(syncpacket);
    this.register(entity);
}

EntityRegistry.prototype.getSyncData = function() {
   var syncpacket = {};
   for(entityId in this.entities) {
      if(this.entities.hasOwnProperty(entityId)) {
          syncpacket[entityId] = this.entities[entityId].generateSyncPacket();
      }
   }
   return syncpacket;
}

EntityRegistry.prototype.parseSyncData = function(syncpacket) {
   for(entityId in syncpacket) {
      
      if(this.entities.hasOwnProperty(entityId)) {
          this.entities[entityId].parseSyncPacket(syncpacket[entityId]);
      }
      else {
          this.startEntity(syncpacket[entityId], entityId);
      }
   }
   return syncpacket;
}
var REGISTRY = new EntityRegistry();
REGISTRY.registerType('redcube', RedCube);

 	var test = "d";  
	var dx = 1, dy = 0;
	var speed = 0.5;

	var activeKey = 0;
    // Set up the scene, camera, and renderer as global variables.
    var scene, camera, renderer;

    var worker = new Worker(getInlineJS());
    worker.postMessage("dasd");
  
    worker.addEventListener('message', function(e) {
    	  REGISTRY.parseSyncData(e.data);
    	}, false);
    
    
    console.log("asd " + test);
    init();
    animate();

    // Sets up the scene.
    function init() {

      // Create the scene and set the scene size.
      scene = new THREE.Scene();
      var WIDTH = window.innerWidth - 50,
          HEIGHT = 500;

      // Create a renderer and add it to the DOM.
      renderer = new THREE.WebGLRenderer({antialias:true});
      renderer.setSize(WIDTH, HEIGHT);
      document.body.appendChild(renderer.domElement);

      camera = new THREE.OrthographicCamera( 0, WIDTH, 200, -HEIGHT, 1, 1000 );
      
      camera.position.set(0,0,100);
      scene.add(camera);
      console.log(WIDTH);
      
      
      window.addEventListener('resize', function() {
    	  var WIDTH = window.innerWidth - 50,
          HEIGHT = window.innerHeight - 50;
        renderer.setSize(WIDTH, HEIGHT);
        camera.aspect = WIDTH / HEIGHT;
        camera.updateProjectionMatrix();
        
      });
      renderer.setClearColor();

     
      
  	document.addEventListener('keydown', function(e) {
  	    if (activeKey == e.keyCode) return;
  	    activeKey = e.keyCode;
  	    
  	    //left
  	    if (e.keyCode == 37) {
  	        dx = -1;
  	    }
  	    //top
  	    else if (e.keyCode == 38) {
  	        dy = 1;
  	    }
  	    //right
  	    else if (e.keyCode == 39) {
  	        dx = 1;
  	    }
  	    //bottom
  	    else if (e.keyCode == 40) {
  	        dy = -1;
  	    }
  	});
  	document.addEventListener('keyup', function(e) {
  	    switch (e.keyCode) {
  	        case 37: // left
  	        case 39: // right
  	            dx = 0;
  	            break;
  	            
  	        case 38: // up
  	        case 40: // down
  	            dy = 0;
  	            break;
  	    }
  	    
  	    activeKey = 0;
  	});

    }
    
    var start;
    var last;

    var timefix, oldX,oldY, updateX,updateY,text;
    
    function animate() {
    	
    	  REGISTRY.callOnRender(scene, renderer);
        renderer.render(scene, camera);   	   
        requestAnimationFrame( animate );

    }
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/84/three.js"></script>
<body style="margin: 0;">

<div id="panel">TEST
</div>
<br>
<script type="javascript/worker">
var RedCube = function(id) {
    this.direction = false;
    this.type = 'redcube';
    if(typeof id === 'undefined') {
       this.entityId = this.generateId();
    }
    else {
        this.entityId = id;
    }
    this.lastX = 0;
    this.x = 0;
}
RedCube.prototype.getType = function() {
   return this.type;
}

RedCube.prototype.generateSyncPacket = function() {
   return {
            type: this.getType(),
            x : this.x
          };
}

RedCube.prototype.parseSyncPacket = function(syncpacket) {
   this.setPosition(syncpacket.x);
}

RedCube.prototype.generateId = function() {
    var d = new Date().getTime();
    if (typeof performance !== 'undefined' && typeof performance.now === 'function'){
        d += performance.now(); //use high-precision timer if available
    }
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
        var r = (d + Math.random() * 16) % 16 | 0;
        d = Math.floor(d / 16);
        return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
    });

}

RedCube.prototype.getEntityId = function() {
    return this.entityId;
}

RedCube.prototype.beforeDeath = function() {
}

RedCube.prototype.die = function() {
}

RedCube.prototype.setPosition = function(newpos) {
    this.lastX = this.x;
    this.x = newpos;
}

RedCube.prototype.getPosition = function() {
    return this.x;
}
RedCube.prototype.getLastX = function() {
    return this.lastX;
}

var EntityRegistry = function() {
   this.entities = {};
   this.types = {};
}

RedCube.prototype.onUpdate = function() {
    if(this.x > 500) {
       this.direction = true;
    }
    if(this.x <= 0) {
       this.direction = false;
    }
    this.x += !this.direction ? 20 : -20;
}
RedCube.prototype.onRender = function(scene, renderer) {
/// this is not a rendering thread. leave it empty
}
EntityRegistry.prototype.register = function(entity) {   
   this.entities[entity.getEntityId()] = entity;
   
}

EntityRegistry.prototype.remove = function(entity) {
   entity.beforeDeath();
   delete this.entities[entity.getEntityId()]
   entity.die();
}

EntityRegistry.prototype.registerType = function(name, entityClass) {
    this.types[name] = entityClass;
}
EntityRegistry.prototype.startEntity = function(entityId, syncpacket) {
    var entity = this.types[syncpacket.type](entityId);
    entity.parseSyncPacket(syncpacket);
    this.register(entity);
}

EntityRegistry.prototype.getSyncData = function() {
   var syncpacket = {};
   
   for(entityId in this.entities) {
      
      if(this.entities.hasOwnProperty(entityId)) {
          syncpacket[entityId] = this.entities[entityId].generateSyncPacket();
      }
   }
   return syncpacket;
}
EntityRegistry.prototype.callUpdate = function() {
  for(entityId in this.entities) {
      if(this.entities.hasOwnProperty(entityId)) {
          this.entities[entityId].onUpdate();
      }
   }
}
EntityRegistry.prototype.callOnRender = function(scene, renderer) {
  for(entityId in this.entities) {
      if(this.entities.hasOwnProperty(entityId)) {
          this.entities[entityId].onRender(scene, renderer);
      }
   }
}
EntityRegistry.prototype.parseSyncData = function(syncpacket) {
   for(entityId in syncpacket) {
      if(this.entities.hasOwnProperty(entityId)) {
          this.entities[entityId].parseSyncPacket(syncpacket[entityid]);
      }
      else {
          this.startEntity(syncpacket, entityId);
      }
   }
   return syncpacket;
}
var REGISTRY = new EntityRegistry();
var little_red = new RedCube();

REGISTRY.register(little_red);    

var x = 0;
var timefix = 0;
var last = 0;
var dx = 1;
var loopInterval = 0;
loopInterval = setInterval(function(){ 			
    REGISTRY.callUpdate()
		var msg = REGISTRY.getSyncData();
		self.postMessage(msg);
		
	}, 1000/60);
  
self.addEventListener('message', function(e) {
	  
	

}, false);
</script>
</body>

Upvotes: 1

Related Questions