March 28, 2022 ยท Dev Diary

Tuning Three.js for Lightweight 3D

The spinning 3D assets on this site are a fun detail, here is how I implemented them.

Three.js UofT logo - same as shown on the home page

Right-Sizing GLTF Assets

I created my own .gltf file in blender using a UofT jpg as a sample.

The scene normalizer recenters the model and enforces a consistent scale. That keeps rotations predictable when swapping models.


function resizeCanvasToDisplaySize(renderer, camera) {
    const canvas = renderer.domElement;
    const width = canvas.clientWidth;
    const height = canvas.clientHeight;
    if (canvas.width !== width ||canvas.height !== height) {
        // you must pass false here or three.js sadly fights the browser
        renderer.setSize(width, height, false);
        camera.aspect = width / height;
        camera.updateProjectionMatrix();

        // set render target sizes here
    }
}

function uoft() {
    const canvasElement = document.querySelector(".logo.uoft canvas");
    if (!canvasElement) {
        console.log("UofT canvas not found, skipping initialization");
        return;
    }

    const renderer = new THREE.WebGLRenderer({
        canvas: canvasElement,
        alpha: true,
        antialias: true
    });

    renderer.setClearColor( 0x000000, 0 ); // the default
    // There's no reason to set the aspect here because we're going
    // to set it every frame anyway so we'll set it to 2 since 2
    // is the the aspect for the canvas default size (300w/150h = 2)
    const camera = new THREE.PerspectiveCamera(43, 1, 5, 200);
    camera.position.set( 1, 90, 0 );
    camera.lookAt( 0, 0, 0 );

    const scene = new THREE.Scene();

    var loader = new GLTFLoader();
    let model;
    loader.load( '../assets/objects/uoft/uoft.gltf', function ( gltf ) {
        model = gltf.scene
        var box = new THREE.Box3().setFromObject( model );
        var center = new THREE.Vector3();
        box.getCenter( center );
        model.position.sub( center ); // center the model
        model.rotation.y = Math.PI/2;   // rotate the model
        scene.add( model );
    }, undefined, function ( error ) {
        console.error( error );
    } );

    const light1 = new THREE.PointLight(0xffffff, 2, 500);
    light1.position.set(0, 150, 0);
    scene.add(light1);

    function animate(time) {
        time *= 0.001;  // seconds

        resizeCanvasToDisplaySize(renderer, camera);

        if (model) {
            model.rotation.x = -Math.PI/2 - 0.4 + (time * 0.5);
        }

        renderer.render(scene, camera);
        requestAnimationFrame(animate);
    }

    setTimeout(() => {
        requestAnimationFrame(animate)
    },10);
}

uoft();
}

Limiting the Render Loop

Animations pause when the tab loses focus thanks to requestAnimationFrame. For extra savings on low-power devices, I gate the animation until the canvas is visible by checking IntersectionObserver events.

The goal is a steady 60fps on desktop and a graceful 30fps on older phones.

With these tweaks, Lighthouse keeps reporting "Minimal main-thread work" and the WebGL experience feels snappy everywhere.