Code Walkthrough

Rough overview of animation

image

The following is probably completely wrong.

The core of rendering multiple 3D objects seems to be done by the painters algorithm or raytracing, which traces light from the perspective of camera. It uses a data structure called BSP trees to store all visible entities on screen. You still need 2D for UI because 2D in 3D is effected by lighting. To animate anything you need a 3D model, a skin and skeleton. Skin is also called texture, model is also called a mesh and skeleton is also called a rig. A 3D model is a bunch of triangles stored in an array. Shader is something that can be used instead of a texture or on top of it. More complex animations are composed by joints and contraints.

In the initial resting position both the 3d model and the skeleton share the same center of mass. If you move the skeleton the animation engine figures out how the triangles are also moved wrt the common center. The skin is stretched appropriately. Attaching an object to a joint is how you compose multiple objects together. Now all this is done in blender which allows you to create animations and store them as keyframes along with any modifications to each frame. The blender model is exported as a gltf file or an fbx file.

Babylon

The most basic thing three.js / babylon.js does is to setup the camera, lighting, the entities in the scene and then attach event handlers.

Model Import

Captures some variables

BABYLON.SceneLoader.ImportMesh("", 
    "/test_model4/",
    "scene.gltf",
    scene, function (newMeshes, particleSystems, skeletons, animationGroups) {
    hero = newMeshes[0];
        
    hero.scaling.scaleInPlace(1);
    hero.rotation = new BABYLON.Vector3(0, Math.PI / 8, 0);
    // hero.position.z = -10;
        
    //Lock camera on the character
    camera1.target = hero;
    animating = true;
    walkAnim = scene.getAnimationGroupByName("Take0001");

    @[ _["Event Handlers Actor"] @]

    demo();
});

Camera Setup

var camera1 = new BABYLON.ArcRotateCamera("camera1", Math.PI / 2, Math.PI / 4, 10, new BABYLON.Vector3(0, -5, 0), scene);
scene.activeCamera = camera1;
scene.activeCamera.attachControl(canvas, true);
camera1.lowerRadiusLimit = 2;
camera1.upperRadiusLimit = 10;
camera1.wheelDeltaPercentage = 0.01;

var light = new BABYLON.HemisphericLight("light1", new BABYLON.Vector3(0, 1, 0), scene);
light.intensity = 1;
light.diffuse = new BABYLON.Color3(1, 1 , 1);
light.specular = new BABYLON.Color3(1, 1, 1);

Event Handlers Camera

Todo

Add start/stop and fullscreen buttons, possibly with a countdown.

// Keyboard events
var inputMap = {};
scene.actionManager = new BABYLON.ActionManager(scene);
scene.actionManager.registerAction(new BABYLON.ExecuteCodeAction(BABYLON.ActionManager.OnKeyDownTrigger, function (evt) {
    inputMap[evt.sourceEvent.key] = evt.sourceEvent.type == "keydown";
}));
scene.actionManager.registerAction(new BABYLON.ExecuteCodeAction(BABYLON.ActionManager.OnKeyUpTrigger, function (evt) {
    inputMap[evt.sourceEvent.key] = evt.sourceEvent.type == "keydown";
}));

Event Handlers Actor

Animation Script

Called soon after model is loaded

let start, previousTimeStamp;

function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}


function step(timestamp) {
    if (start === undefined)
        start = timestamp;
    const elapsed = timestamp - start;

    if (previousTimeStamp !== timestamp) {
        const count = Math.min(0.1 * elapsed, 200);
        hero.moveWithCollisions(hero.forward.scaleInPlace(heroSpeed));
        // walkAnim.start(true, 1.0, walkAnim.from, walkAnim.to, false);
    }

    if (elapsed < 2000) { // Stop the animation after 2 seconds
        previousTimeStamp = timestamp
        window.requestAnimationFrame(step);
    } else {
        // walkAnim.stop();
        animating = false;
    }
}

async function demo() {
        window.requestAnimationFrame(step);
        await sleep(100);
        $("#main-text").show();
        await sleep(3000);
        document.getElementById("main-text").innerText = "World!";
       await sleep(3000);
       $("#main-text").hide();
}

HTML

<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />

        <title>Virtuator</title>

        <!-- Babylon.js -->
        <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.6.2/dat.gui.min.js"></script>
        <script src="https://preview.babylonjs.com/ammo.js"></script>
        <script src="https://preview.babylonjs.com/cannon.js"></script>
        <script src="https://preview.babylonjs.com/Oimo.js"></script>
        <script src="https://preview.babylonjs.com/earcut.min.js"></script>
        <script src="https://preview.babylonjs.com/babylon.js"></script>

        <script src="https://preview.babylonjs.com/materialsLibrary/babylonjs.materials.min.js"></script>
        <script src="https://preview.babylonjs.com/proceduralTexturesLibrary/babylonjs.proceduralTextures.min.js"></script>
        <script src="https://preview.babylonjs.com/postProcessesLibrary/babylonjs.postProcess.min.js"></script>
        <script src="https://preview.babylonjs.com/loaders/babylonjs.loaders.js"></script>
        <script src="https://preview.babylonjs.com/serializers/babylonjs.serializers.min.js"></script>
        <script src="https://preview.babylonjs.com/gui/babylon.gui.min.js"></script>
        <script src="https://preview.babylonjs.com/inspector/babylon.inspector.bundle.js"></script>

        <style>
            html, body {
                overflow: hidden;
                width: 100%;
                height: 100%;
                margin: 0;
                padding: 0;
            }

            #renderCanvas {
                width: 100%;
                height: 100%;
                touch-action: none;
            }

            .speech-bubble {
                z-index: 1000;
                font-size: 16px;
                position: absolute;
                top: 75px;
                left: 550px;
                background: #f7f7f7;
                border: 1px solid black;
                border-radius: .4em;
                padding: 16px;
                border-radius: .4em;
            }

            .speech-bubble:after {
                content: '';
                position: absolute;
                bottom: 0;
                left: 50%;
                width: 0;
                height: 0;
                border: 10px solid transparent;
                border-top-color: black;
                border-bottom: 0;
              	border-right: 0;
                margin-left: -10px;
                margin-bottom: -10px;
            }

        </style>
    </head>
<body>
    <canvas id="renderCanvas"></canvas>
    <hgroup id="main-text" class="speech-bubble" style="display: none">
        Hello!
    </hgroup>
    <script>
        var canvas = document.getElementById("renderCanvas");

        var engine = null;
        var scene = null;
        var sceneToRender = null;

        var hero;
        var animating;
        var walkAnim;
        var heroSpeed = 0.03;
        var heroSpeedBackwards = 0.01;
        var heroRotationSpeed = 0.1;

        var createDefaultEngine = function() { 
            return new BABYLON.Engine(canvas, true, { preserveDrawingBuffer: true, stencil: true,  disableWebGL2Support: false});
        };

        @[ _["Animation Script"] @]

        var createScene = function () {
            engine.enableOfflineSupport = false;
            var scene = new BABYLON.Scene(engine);
            // Green Screen
            scene.clearColor = new BABYLON.Color3(0.2, 0.97, 0.2);

            @[ _["Camera"] @]
            @[ _["Event Handlers Camera"] @]
            @[ _["Model Import"] @]

            return scene;
        }

        window.initFunction = async function() {

            var asyncEngineCreation = async function() {
                try {
                    return createDefaultEngine();
                } catch(e) {
                    console.log("the available createEngine function failed. Creating the default engine instead");
                    return createDefaultEngine();
                }
            }

            window.engine = await asyncEngineCreation();
            if (!engine) throw 'engine should not be null.';
            window.scene = createScene();

        };

        initFunction().then(() => {
            sceneToRender = scene
            engine.runRenderLoop(function () {
                if (sceneToRender && sceneToRender.activeCamera) {
                    sceneToRender.render();
                }
            });
        });

        window.addEventListener("resize", function () {
            engine.resize();
        });
    </script>
</body>
</html>