ruhig brauner
ruhig brauner

Reputation: 963

Improving performance with Three.js

I am currently writing my first Three.js / WebGL application and it runs very well on my PC (Chrome). Sadly, on many other PCs, the framerate often drops bellow 30 frames per second.

Since the application actually isn't to complex, I wanted to ask for some tips related to the application to improve the performance. A version of the app can be found here:

www.wrodewald.de/StackOverflowExample/

The application contains a dynamic (morphing) plane using 64² vertices. A matrix is used to store a static heightmap and a wavemap. The wavemap is updated every frame to recalculate itself, some filters are used to "even out" every vertex compared to their neightbors. So every frame the plane has to be updated (colors and vertex position) which could be a reason for the performance problem

The second object (rhombus) shouldn't be a problem, static, moving around a bit but nothing special.

There are three lights (ambient, directional, spherical), no shadows, a tilt shift shader and a vignette shader.

Here are functions which are called per frame:

var render = function() {
    requestAnimationFrame( render );
    var time = clock.getDelta();

    world.updateWorld(time);
    diamond.rotate(time);
    diamond.update(time);
    control.updateCamera(camera, time);
    composer.render();      
    stats.update();
}

this is what world.updateWorld(time) does

//in world.updateWorld(time) where
// accmap: stores acceleration and wavemap stores position
// this.mapSize stores the size of the plane in vertices (64)

// UPDATE ACCELERATION MAP
for(var iX = 1; iX < (this.mapSize-1); iX++) {
    for(var iY = 1; iY < (this.mapSize-1); iY++) {
        accmap[iX][iY] -=  dT * (wavemap[iX][iY]) * Math.abs(wavemap[iX][iY]);
    }   
}


// SMOOTH ACCELERATION MAP
for(var iX = 1; iX < (this.mapSize-1); iX++) {
    for(var iY = 1; iY < (this.mapSize-1); iY++) {
        tempmap[iX][iY] =     accmap[iX-1][iY-1]    * 0.0625
                            + accmap[iX-1][iY  ]    * 0.125
                            + accmap[iX-1][iY+1]    * 0.0625
                            + accmap[iX  ][iY-1]    * 0.125
                            + accmap[iX  ][iY  ]    * 0.25
                            + accmap[iX  ][iY+1]    * 0.125
                            + accmap[iX+1][iY-1]    * 0.0625
                            + accmap[iX+1][iY  ]    * 0.125
                            + accmap[iX+1][iY+1]    * 0.0625;

        accmap[iX][iY] = tempmap[iX][iY];
    }
}


// UPDATE WAVE MAP
for(var iX = 1; iX < (this.mapSize-1); iX++) {
    for(var iY = 1; iY < (this.mapSize-1); iY++) {
        wavemap[iX][iY] += dT * accmap[iX][iY];
    }   
}

for(var i = 0; i < this.mapSize; i++) {
    for(var k = 0; k < this.mapSize; k++) {
        geometry.vertices[ i * this.mapSize + k ].y =  wavemap[i][k] + heightmap[i][k];
    }   
}

for(var i = 0; i < geometry.faces.length; i++) {

    var vertexA = geometry.vertices[geometry.faces[i].a];
    var vertexB = geometry.vertices[geometry.faces[i].b];
    var vertexC = geometry.vertices[geometry.faces[i].c];

    var val = (vertexA.y + vertexB.y + vertexC.y) / 3.0;
    val = (val / 200.) + 0.5;

    geometry.faces[i].color.r = val;
    geometry.faces[i].color.g = val;
    geometry.faces[i].color.b = val;
}

geometry.colorsNeedUpdate = true;
geometry.verticesNeedUpdate = true;

These are the "diamond"-functions

this.rotate = function(dT) {
    counter += 0.5 * dT;
    counter % 1;
    var x = 0.0025 * (Math.sin((counter) * 2 * Math.PI));
    var y = 0.0025 * (Math.cos((counter) * 2 * Math.PI));
    this.mesh.rotateOnAxis(new THREE.Vector3(1,0,0), x);
    this.mesh.rotateOnAxis(new THREE.Vector3(0,0,1), y);
}

this.update = function(dT) {
    for(var i = 0; i < geometry.faces.length; i++) {    
        geometry.faces[i].color.lerp(color, dT*(0.9));
    }

    geometry.colorsNeedUpdate = true;
}

Do you spot any reason for the framerate to be so inconsistent?

Upvotes: 4

Views: 6617

Answers (1)

Entity Black
Entity Black

Reputation: 3491

EDIT:

I have found 2 major things you have to improve:

Planes updates with GPU

speedup: high

Lets pick your code from plane.js

    timer += dT;
    if(timer > 0.1) {           
        var x = 2 + Math.floor(Math.random() * (this.mapSize - 4));
        var y = 2 + Math.floor(Math.random() * (this.mapSize - 4));
        //accmap[x][y] += 30000 * Math.random () - 15000
    }


    // UPDATE ACCELERATION MAP
    for(var iX = 1; iX < (this.mapSize-1); iX++) {
        for(var iY = 1; iY < (this.mapSize-1); iY++) {
            accmap[iX][iY] -=  dT * (wavemap[iX][iY]) * Math.abs(wavemap[iX][iY]);
        }   
    }

So you have 4096 vertices you would like to update every 17 ms with CPU. Notice you didnt use any GPU advantage. How it should be done:

  • First you create buffers, for vertex position, for normals, for texture cordinates, indices ... . This together is called mesh.
  • Then you create a model. Model is composed from one or more meshes. And modelViewMatrix. This is super important part, matrix 4x4 is algebraic representation of position, rotation and scale of this model.
  • With each render you do exactly this in vertex shader:

    "gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );",

    it is from your cg/shaders/VerticalTiltShiftShader.js

  • If you want to rotate your plane, you don't multiply each vertex, but you only multiply your model matrix once (js with THREE.js function):

    projectionMatrix.makeRotationY(dT);

    Then each vertex is multiplied in vertex shader with this matrix, which is like much more faster.

Javascript style

speedup: none - medium, but it will allow you to code faster

Lets pick your plane.js as example.

// this is your interface
function PlaneWorld () {
    this.init = function() {};
    this.updateVertices = function() {};
    this.updateWorld = function(dT) {};
    // ... and more
}

// and somewhere else:
var world = new PlaneWorld();

In case you have only one plane in your project, you can consider this as singleton and implementation is ~ok. But if you would like to create 2 or more planes, all functions are recreated again for every instance (new PlaneWorld()). Correct way how to do this is:

function PlaneWorld () {
    ...
}

PlaneWorld.prototype.init = function() {};    
PlaneWorld.prototype.updateVertices = function() {};    
PlaneWorld.prototype.updateWorld = function(dT) {};  
// ... and more

var world = new PlaneWorld();

// calling methods works same
world.updateVertices();

or more complicated version with anonymous function:

var PlaneWorld = (function() {

    // something like private static variables here

    var PlaneWorld = function () {
        ...
    }

    PlaneWorld.prototype = {
        init: function() {},
        updateVertices: function() {},
        updateWorld: function(dT) {} 
        // ... and more
    }

    return PlaneWorld();
})();

var world = new PlaneWorld();
// calling methods works same
world.updateVertices();

Then new instance cost is lowered. Now the thing which might be connected, every instance should share same mesh, but has its own modelViewMatrix.

Upvotes: 5

Related Questions