Kurt Lagerbier
Kurt Lagerbier

Reputation: 200

threejs - better performance for my time script

I made some experiments with the great threejs lib: https://maki-mikkelson.com/time

But the performance is very bad. Does somebody know what I could do better?

First there's my timeFont class from where I create every second (or by input) an instance of it. Below I initialize the camera, the scene, the light and the renderer. I delete the instance of it when it flies out of the scene.

I don't know what's eating more resources. Is it the fonts, is it that I update the text too much or did I just go over the limits of this lib.

Here's the source:

// timeFont class
class timeFont {

    constructor(scene = undefined, timeType = 'currentTimeString', startZPos = 0) {
        this.scene = scene;
        this.timeType = timeType;
        this.startZPos = startZPos;

        this.group, this.textMesh1, this.textGeo, this.materials;

        // set random speed
        this.speed = this.getRandomInt(10, 100);

        // set random y
        this.yOffset = this.getRandomArbitrary(-1.0, 1.0);

        // time strings
        this.timeStrings = {
            0: 'currentTimeString',
            1: 'currentMilliseconds',
            2: 'currentSeconds',
            3: 'currentMinutes',
            4: 'currentHours',
            5: 'currentDateString',
            6: 'currentDay',
            7: 'currentMonth',
            8: 'currentYear',
            9: 'currentTimestamp',
            10: 'mid'
        };

        this.currentTimeString,
        this.currentMilliseconds,
        this.currentSeconds,
        this.currentMinutes,
        this.currentHours,
        this.currentDateString,
        this.currentDay,
        this.currentMonth,
        this.currentYear,
        this.currentTimestamp,
        this.mid;

        // font settings
        this.fontMap = {
            0: "Alfa_Slab_One_Regular.json",
            1: "Comfortaa_Regular.json",
            2: "Freehand_Regular.json",
            3: "Mandali_Regular.json",
            4: "Monofett_Regular.json",
            5: "Raleway_Dots_Regular.json"
        };

        // update the clock initially
        this.updateClock();

        this.timeType = this.randomProperty(this.timeStrings);
        this.text = this.currentMilliseconds,

        this.height = this.getRandomInt(10, 100),
        this.size = this.getRandomInt(10, 100),
        this.hover = 0,

        this.curveSegments = 4,

        this.bevelThickness = 1.0,
        this.bevelSize = 1.5,

        this.font = undefined,
        this.fontName = this.randomProperty(this.fontMap);

    }

    randomProperty(obj) {
        var keys = Object.keys(obj);
        return obj[keys[ keys.length * Math.random() << 0]];
    };

    getRandomArbitrary(min, max) {
        return Math.random() * (max - min) + min;
    }

    getRandomInt(min, max) {
        return Math.floor(Math.random() * (max - min + 1)) + min;
    }

    initClass() {

        this.materials = [
            new THREE.MeshPhongMaterial( { color: 0xffffff, flatShading: true, opacity: 0.5, depthWrite: false, transparent: true } ), // front
            new THREE.MeshPhongMaterial( { color: 0xffffff, opacity: 0.5, depthWrite: false, transparent: true } ) // side
        ];

        this.group = new THREE.Group();
        this.group.position.y = 0;

        // load font
        this.loadFont();

        // add to scene
        if (this.scene) {
            this.scene.add(this.group);
        }

    }

    loadFont() {

        this.fontName = this.randomProperty(this.fontMap);

        var loader = new THREE.FontLoader();
        loader.load('assets/content/font/' + this.fontName, (response) => {
            this.font = response;
        });

    };

    createText() {

        this.textGeo = new THREE.TextGeometry(String(this.text), {

            font: this.font,

            size: this.size,
            height: this.height,
            curveSegments: this.curveSegments,

            bevelThickness: this.bevelThickness,
            bevelSize: this.bevelSize

        });

        this.textGeo.computeBoundingBox();
        this.textGeo.computeVertexNormals();

        var centerOffset = - 0.5 * ( this.textGeo.boundingBox.max.x - this.textGeo.boundingBox.min.x );

        this.textGeo = new THREE.BufferGeometry().fromGeometry(this.textGeo);

        this.textMesh1 = new THREE.Mesh(this.textGeo, this.materials);

        this.textMesh1.position.x = centerOffset;
        this.textMesh1.position.y = this.hover;
        this.textMesh1.position.z = 0;

        this.textMesh1.rotation.x = 0;
        this.textMesh1.rotation.y = 0;
        this.textMesh1.rotation.z = 0;

        this.group.add(this.textMesh1);

    };

    updateText() {
        // remove old mesh
        this.group.remove(this.textMesh1);
        // create it again
        this.createText();
    }

    // time function
    updateClock() {
        var currentTime = new Date();

        this.currentDay = currentTime.getDate();
        this.currentMonth = currentTime.getMonth() + 1;
        this.currentYear = currentTime.getFullYear();

        this.currentTimestamp = Date.now();

        this.currentHours = currentTime.getHours();
        this.currentMinutes = currentTime.getMinutes();
        this.currentSeconds = currentTime.getSeconds();
        this.currentMilliseconds = currentTime.getMilliseconds();

        // get AM / PM
        var hours = (this.currentHours+24-2)%24; 
        this.mid='am';
        if (hours==0) {
            hours=12;
        } else if (hours>12) {
            hours=hours%12;
            this.mid='pm';
        }

        // Pad the minutes with leading zeros, if required
        //currentMinutes = ( currentMinutes == 12 ) ? currentHours - 12 : currentHours;
        if (this.currentMinutes < 10) {
            this.currentMinutes = '0' + this.currentMinutes;
        }

        // Pad the seconds with leading zeros, if required
        if (this.currentSeconds < 10) {
            this.currentSeconds = '0' + this.currentSeconds;
        }

        // Compose the string for display time
        this.currentTimeString = this.currentHours + ":" + this.currentMinutes + ":" + this.currentSeconds;

        // Compose the string for display date
        this.currentDateString = this.currentDay + "-" + this.currentMonth + "-" + this.currentYear;

        // set text
        switch(this.timeType) {
            case 'currentTimeString':
                this.text = this.currentTimeString;
                break;
            case 'currentMilliseconds':
                this.text = this.currentMilliseconds;
                break;
            case 'currentSeconds':
                this.text = this.currentSeconds;
                break;
            case 'currentMinutes':
                this.text = this.currentMinutes;
                break;
            case 'currentHours':
                this.text = this.currentHours;
                break;
            case 'currentDateString':
                this.text = this.currentDateString;
                break;
            case 'currentDay':
                this.text = this.currentDay;
                break;
            case 'currentMonth':
                this.text = this.currentMonth;
                break;
            case 'currentYear':
                this.text = this.currentYear;
                break;
            case 'currentTimestamp':
                this.text = this.currentTimestamp;
                break;
            case 'mid':
                this.text = this.mid;
                break;
            default:
                this.text = this.currentTimestamp;
        }

    };

    get timeFontZ() {
        return this.group.position.z;
    }

    updatePosition() {
        //group.rotation.y += ( targetRotation - group.rotation.y ) * 0.05;

        // move the object towards the camera
        this.group.position.z += this.speed;
        this.group.position.y += this.yOffset;
        // reset the object, if it's behind the camera
        if (this.group.position.z > 4080) {
            this.scene.remove(this.group);
        }
    }

    update() {
        // update font, time and position
        this.updateClock();
        this.updatePosition();
        if (this.font) {
            this.updateText();
        }
    }

};

// global vars
var container;
var camera, cameraTarget, scene, renderer;

var windowHalfX = window.innerWidth / 2;

var instances = [];

var frameRate = 5;
document.getElementById("setFrameRate-Input").value = frameRate;

var instanceCreationTime = 1;
document.getElementById("setInstanceCreation-Input").value = instanceCreationTime;

// start
initGlobal();
animate();

document.getElementById("setFrameRate").addEventListener('click', function () {
    frameRate = document.getElementById("setFrameRate-Input").value;
}, false);

document.getElementById("setInstanceCreation").addEventListener('click', function () {
    instanceCreationTime = document.getElementById("setInstanceCreation-Input").value;
}, false);

function setDeceleratingTimeout(callback, factor, times) {
    var internalCallback = function(tick, counter) {
        return function() {
            if (--tick >= 0) {
                window.setTimeout(internalCallback, ++counter * factor);
                callback();
            }
        }
    } (times, 0);

    window.setTimeout(internalCallback, factor);
};

function initGlobal() {

    // container
    container = document.createElement('div');
    document.body.appendChild(container);

    // camera
    var cameraZ = 4000;
    camera = new THREE.PerspectiveCamera(500, window.innerWidth / window.innerHeight, 0.1, 10000);
    camera.position.set(0, 0, cameraZ);
    cameraTarget = new THREE.Vector3(0, 0, 0);

    // scene
    scene = new THREE.Scene();
    scene.background = new THREE.Color(0xf0f0f0);

    // lights
    var dirLight = new THREE.DirectionalLight(0xffffff, 0.125);
    dirLight.position.set(0, 0, 1).normalize();
    scene.add(dirLight);

    // renderer
    renderer = new THREE.WebGLRenderer( {antialias: true} );
    renderer.setPixelRatio( window.devicePixelRatio );
    renderer.setSize( window.innerWidth, window.innerHeight );
    container.appendChild( renderer.domElement );

    // add resize listener
    window.addEventListener('resize', onWindowResize, false);

    // create first instance
    createInstance();

};

function onWindowResize() {

    windowHalfX = window.innerWidth / 2;

    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();

    renderer.setSize(window.innerWidth, window.innerHeight);

};

function createInstance() {
    var newInstance = new timeFont(scene);
    newInstance.initClass();
    instances.push(newInstance);

    setDeceleratingTimeout(function() {
        createInstance();
        //console.log('create instace');
    }, 1000 / instanceCreationTime, 1);
}

function animate() {

    //requestAnimationFrame(animate);
    setTimeout(function() {
        requestAnimationFrame(animate);
    }, 1000 / frameRate);

    render();

    // update test instance
    instances.forEach(instance => {
        instance.update();

        if (instance.timeFontZ > 4120) {
            delete instance;
        }

    });

}

function render() {

    camera.lookAt(cameraTarget);

    renderer.clear();
    renderer.render(scene, camera);

}

PS: I updated the script on the link on my website. I only update the text, if there are seconds shown now. But it's still bad performance.

Upvotes: 0

Views: 204

Answers (1)

Mugen87
Mugen87

Reputation: 31026

One problem in your app is the amount of geometries you allocate over time. When removing objects from the scene, you miss to call dispose() in order to free internal resources.

You remove objects at two places:

  • In updateText() via this.group.remove(this.textMesh1);
  • In updatePosition() via this.scene.remove(this.group);

In updateText() use this code to free the material and geometry:

this.textMesh1.geometry.dispose();

if ( Array.isArray( this.textMesh1.material ) === true ) {

    for ( let material of this.textMesh1.material ) material.dispose();

} else {

    this.textMesh1.material.dispose();

} 

In updatePosition(), use this generic code:

this.group.traverse( function( object ) {

    if ( object.isMesh ) {

        object.geometry.dispose();
        // release material like above

    }

} );

You can read more about memory management in this official guide:

How to dispose of objects

Besides, try to load your fonts only once at the beginning and then reuse them. This will also avoid big frametimes since you avoid parsing overhead during your animation.

Upvotes: 1

Related Questions