Getting started with BabylonJS

A minimal base template using TypeScript and Vite

Getting started with BabylonJS

BabylonJS is a comprehensive JavaScript library for 3D graphics in the web browser. Requiring just a recent browser, it allows shipping 3D graphics applications to a wide range of users and their platforms easily. You can already make quick experiments with BabylonJS from within the browser at the Playground or by including the compiled library in your HTML document, see the documentation. But for more serious development TypeScript in combination with Vite offers a better setup. You get compile-time checking, cleaner code, and optimized builds for shipping your application.

Unfortunately, the JavaScript and front-end development landscape can be rather confusing. There is a plethora of languages and dialects (e.g. evolving ECMAScript standards, TypeScript, or CoffeeScript), as well as development tools (e.g. NPM, Bower, webpack, or Vite). As a beginning front-end developer or someone coming from another language ecosystem, the front-end tooling can be overwhelming to choose from. It is also not very obvious how all these tools connect.

This post aims to provide an easy-to-start-with minimal base template for writing BabylonJS applications in TypeScript and building them using Vite. For this, we will only need NPM—the Node Package Manager—installed. At the end, we will have a rotating textured cube. Our template is based on this guide and this render loop timing control.

Installing dependencies

First, we create a project directory and use NPM to install BabylonJS, and our development tools TypeScript, and Vite.

mkdir babylon-start
cd babylon-start
npm install --save @babylonjs/core
npm install --save-dev typescript vite

The @babylonjs package scope provides the ES6 (ECMAScript 2015) module versions of the library, which are now widely supported and preferred over the older ES5 babylonjs/core build.

Configuring the TypeScript compiler

Before we can compile any code, we need to configure the TypeScript compiler. The tsconfig.json` file tells the compiler where to find our source code, which libraries we use, and how to output the transpiled JavaScript.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{
  "compilerOptions": {
    "target": "es6", // widely supported ECMAScript 2015
    "lib": ["dom", "esnext"],
    "useDefineForClassFields": true, // use upcoming ECMA runtime behavior
    "module": "esnext", // use latest syntax for import statements
    "moduleResolution": "bundler", // find installed NPM packages
    "removeComments": true, // remove comments to save space
    "esModuleInterop": true, // allow for ES5 modules
  },
  "include": ["src"] // location of our TypeScript code
}

The HTML base document

To serve our app, we need an HTML document to display in a web browser—for example, index.html shown below. It must contain a canvas element for BabylonJS to render into (line 26), and it needs to load our application code as well (line 28). We additionally ensure the app runs at the full window size (lines 5–21).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<html>
  <head>
    <title>BabylonJS Minimal Base</title>

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

      #renderCanvas {
        width: 100%;
        height: 100%;
        display: block;
        font-size: 0;
      }
    </style>
  </head>

  <body>

    <canvas id="renderCanvas" touch-action="none"></canvas>

    <script type="module" src="./src/app.ts"></script>

  </body>
</html>

The BabylonJS code

Now we come to our actual BabylonJS application code. We first create the src/ subdirectory and save the code below as src/app.ts. With a programming background, the code is fairly straightforward, and the many comment lines provide additional explanation. Pay particular attention to the rendering loop, which implements fixed‑timestep logic updates combined with variable‑frame‑rate rendering.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// import all needed classes
import { Scene, Engine, Color3, Vector3 } from "@babylonjs/core";
import { UniversalCamera, DirectionalLight } from "@babylonjs/core";
import { Mesh, MeshBuilder } from "@babylonjs/core/Meshes";
import { StandardMaterial, Texture } from "@babylonjs/core/Materials";

class App {

    TIMESTEP = 1/60;    // 60 updates per second
    box: Mesh;          // has to be accessible in update()

    constructor() {
        // get canvas element to render to
        var canvas = document.getElementById("renderCanvas") as HTMLCanvasElement;

        // initialize babylon scene and engine
        var engine = new Engine(canvas, true);
        var scene = new Scene(engine);

        // setup a camera looking at the origin
        var camera:UniversalCamera = new UniversalCamera("camera1", new Vector3(0, 0, -3), scene);
        camera.setTarget(Vector3.Zero());

        // add a directional light source along camera viewing direction
        var light: DirectionalLight = new DirectionalLight("light1", new Vector3(0, 0, 1), scene);

        // create a cube mesh as wooden crate
        this.box = MeshBuilder.CreateBox("box1", {size: 1}, scene);

        // use the standard material, but disable specularity, wood isn't shiny
        var material:StandardMaterial = new StandardMaterial("material1", scene);
        material.specularColor = new Color3(0.0, 0.0, 0.0);

        // fetch and apply the texture map
        var texture:Texture = new Texture("crate-texture.jpg", scene);
        material.diffuseTexture = texture;
        this.box.material = material;

        // initialize timing control
        var accumulator = 0.0;
        var previousTime = performance.now() / 1000;

        // render loop with consistent timing and updates
        engine.runRenderLoop(() => {

            // compute time passed since last frame draw
            const currentTime = performance.now() / 1000;
            const deltaTime = currentTime - previousTime;
            previousTime = currentTime;

            // how many updates are needed for the time that has passed?
            accumulator += Math.min(deltaTime, 0.1);

            // perform the required amount of updates, keep remainder for next cycle
            while (accumulator >= this.TIMESTEP) {
                this.update(this.TIMESTEP);
                accumulator -= this.TIMESTEP;
            }

            // now render with variable frame rate
            scene.render();
        });
    }

    // update logic
    update(dt: number) {
        // rotate box a bit further each time (dt radians per update)
        // amounts to one radian per second or roughly 10 rpm
        this.box.rotation.x += dt;
        this.box.rotation.y += dt;
    }
}

// run our app
new App();

A crate texture map

For the rotating crate to be textured, download the texture map below and place it in the public/ subdirectory. This is where all static assets are stored and packaged during bundling.

Wooden crate texture (by Filter Forge— Attribution 2.0 Generic (CC BY 2.0) license)

Wooden crate texture (by Filter Forge— Attribution 2.0 Generic (CC BY 2.0) license)

Final directory structure

Finally, our project directory should look as follows. The node_modules/ directory and the package*.json files were created automatically when we installed our dependencies. All the other files we created manually in the previous steps.

.
├── index.html
├── node_modules
├── package.json
├── package-lock.json
├── public
│   └── crate-texture.jpg
├── src
│   └── app.ts
└── tsconfig.json

Compiling and deploying

We can now run and package our BabylonJS application.

Using the TypeScript compiler tsc we compile our code and check it for errors.

npx tsc

When the code compiles without problems, we run and view our application using Vite. Fortunately, Vite works out of the box and no further configuration is necessary. We just run

npx vite

to start a development web server to view our application. To package our application we run

npx vite build

This bundles our application together with all of its dependencies into the dist/ subdirectory. For deployment, we simply upload this directory to a public web server in order to make it accessible.

In Action

This is how our application finally looks like in action inside a smaller render canvas.

Hopefully, this walkthrough will help you get started building impressive 3D graphics for the web. Happy coding!