ninethehacker.xyz 3d Graphics in the Browser with three.js

3d Graphics in the Browser with three.js

ridin' spinners, we ridin spinners (we don't stop)


I don’t know why I’ve been sitting on this demo for two months. Actually I do, I was using the indices of a three dimensional array to store the cube position and it was slow as hell. Now that I’ve corrected it I’ve had to fight bizarre html edge cases to embed it, but here we go. You can check out a fullscreen version of the effect here.


boring programmer stuff

The demo is written in javascript using the excellent three.js. The library does a huge amount of the heavy lifting letting even a beginner get started right away.

The first big mistake I made was using multidimensional arrays and nested for loops to populate and then render the scene. This proved to be far to heavy for the animation loop, and was excessively taxing on memory and processor. I improved this by flattening the arrays into a single list that holds a reference to the mesh alongside the cube position in world space, I believe this is faster than pulling the world space co-ordinates of a mesh 5^3 times every frame. I will have to investigate java classes to see if there’s a cleaner way to write this though. I’m glad I pasted the nested loops because if I’d done something stupid like write a function this wouldn’t have untangled half as easy.

Another issue I had was keeping the camera orbiting and facing the center of the cube, this is because I’m stupid and bad at math, I kept trying to write the algebraic inversion of x=sin(t);y=cos(t); but the javascript Math library has IBM’s fast atan2 function. Way smarter.

After that it was mostly just messing around with HTML5 and CSS to get it onto the internet. The same code runs the embedded version and the fullpage demo with no extra JS shims or cheekyness. This was not easy or fun, but I’m glad I didn’t compromise as I learned a lot of weird edge behaviour that will probably come in handy later.

Regrets, I got some. One the code is kind of raw with some dumb assumptions and spaghetti. For example I could probably reorient the geometry to 0,0,0. Secondly I wrote the JS quite a bit like python, and having learned more idiomatic JS since starting I began to cringe fixing it for publication. I kind of wish I wrote some code to mess with the colors more, I also kind of wish I wrote a function to walk the array calculating distance and set the alpha down to zero on a curve so you don’t notice the camera clipping. Another idea was turning off and on he cubes to make voxel patters, like most voxel displays I think that would require more voxels to actually look decent.

On a technical note, if you refresh the page repetitively the references to the renderer in the resize event handler cause the GL context to be held after the browser spawns a new one decreasing performance. This maxes out somewhere between 8 and 16 contexts on modern browsers, but it’s still a bug I’d like to look into for any future demos.

I think I’m done with cubes, going forward I want to do learn some shaders, or look into procedural meshes.

embedding

To get the demo up and running we need to load up the framework and then start my own code. It’s not necessary to target a html5 canvas, but in order to use three.js as part of a layout it’s highly recommended, otherwise your JS needs to handle this and your code becomes less portable.

<div style="width:100%;height:30vh;">
<canvas id="demo"></canvas>
<script src="/media/repetitionandmutation/three.js"></script>
<script src="/media/repetitionandmutation/demo.js"></script>
</div>
<br>

the actual code

//
// scene config
//

var scene = new THREE.Scene();
scene.background = new THREE.Color(0xffffff);

var c = document.getElementById("demo");

var renderer = new THREE.WebGLRenderer({canvas: c});
var canvas = renderer.domElement;
renderer.setSize(
    canvas.parentElement.clientWidth,
    canvas.parentElement.clientHeight
);

var camera = new THREE.PerspectiveCamera(
    75,
    canvas.clientWidth / canvas.clientHeight,
    0.1,
    1000
);

//
// lighting config
//

var point_light = new THREE.PointLight(0xFFFFFF);
point_light.position.set(20, 20, 20);
scene.add(point_light);

var fill_light = new THREE.PointLight(0x888888);
fill_light.position.set(-20, -20, -20);
scene.add(fill_light);

var orbit_light = new THREE.PointLight(0xFFFF00);
scene.add(orbit_light);

var orbit_fill = new THREE.PointLight(0x0000FF);
scene.add(orbit_light);

//
// geometry config
//

const EDGE_LENGTH = 5;
const NUM_CUBES = Math.pow(EDGE_LENGTH, 3);

cube = [];
var i;
i = 0
var x;
for (x=0; x<EDGE_LENGTH; x++) {
    var y;
    for (y=0; y<EDGE_LENGTH; y++) {
        var z;
        for (z=0; z<EDGE_LENGTH; z++) {
            var geometry = new THREE.BoxGeometry();
            var material =  new THREE.MeshLambertMaterial(
                {color:new THREE.Color(
                    x/EDGE_LENGTH,
                    y/EDGE_LENGTH,
                    z/EDGE_LENGTH
                )}
            );
            var mesh = new THREE.Mesh( geometry, material );
            mesh.position.set(x, y, z);
            scene.add (mesh);
            cube.push({mesh:mesh, x:x, y:y, z:z});
            i+=1;
        }
    }
}

function orbit(object, counter, origin_x, origin_y, origin_z, radius, speed) {
    object.position.set(
        origin_x + radius * Math.sin(counter * speed),
        origin_y,
        origin_z + radius * Math.cos(counter * speed)
    );
    object.rotation.y = Math.atan2(
        object.position.x - origin_x,
        object.position.z - origin_z
    );
}

function rotate(object, counter, rot_x, rot_y, rot_z) {
    object.rotation.x += counter * rot_x;
    object.rotation.y += counter * rot_y;
    object.rotation.z += counter * rot_z;
}

function oscillate(counter, rate, distance) {
    return Math.sin(counter * rate) * distance;
}

function animate() {
    requestAnimationFrame( animate );
    counter += 0.01;

    //
    // camera animation
    //

    orbit(camera, counter, 2, 2, 2, 9 + oscillate(counter, 0.6, 8), 1);

    //
    // light animation
    //

    orbit(orbit_light, counter, 2, 2, 2, 8, 4);
    orbit(orbit_fill, counter + 0.5, 2, 2, 2, 8, 4);

    //
    // cube animation
    //

    var i;
    for (i=0; i<NUM_CUBES; i++) {
        var x = cube[i].x;
        var y = cube[i].y;
        var z = cube[i].z;
        rotate(cube[i].mesh, 0.01, x-2, y-2, z-2);
        cube[i].mesh.scale.set(
            oscillate(counter, 0.1 + (x+y+z) * 0.06, 1),
            oscillate(counter, 0.1 + (x+y+z) * 0.06, 1),
            oscillate(counter, 0.1 + (x+y+z) * 0.06, 1)
        );
    }

    renderer.render ( scene, camera );
}

//
// event handlers
//

function onWindowResize() {
    camera.aspect = canvas.clientWidth / canvas.clientHeight;
    camera.updateProjectionMatrix();

    renderer.setSize(
        canvas.parentElement.clientWidth,
        canvas.parentElement.clientHeight
    );
}

window.addEventListener( 'resize', onWindowResize, false );

counter = 0;
animate();

there are no comments yet


all comments are manually reviewed before publication