Reputation: 319
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
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
tick 2. 50ms
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