logo

Babylon.js Market

By Lawrence

7 minutes

Understanding Assets

Before diving into our mesh loading system, it's important to understand Babylon.js's AssetContainer class. An AssetContainer is a lightweight container that holds references to assets (meshes, materials, textures, etc.) without adding them to the scene immediately.

What is an AssetContainer?

AssetContainer Example
import { AssetContainer } from "@babylonjs/core";

// An AssetContainer holds assets separately from the scene
const container = new AssetContainer(scene);

// You can add meshes, materials, textures to the container
container.meshes.push(myMesh);
container.materials.push(myMaterial);
container.textures.push(myTexture);

Key Benefits of AssetContainer

Memory Management: AssetContainers allow you to load assets without immediately consuming scene resources. This is perfect for:

  • Preloading assets for later use
  • Managing asset lifecycles independently
  • Reducing initial scene complexity

Asset Reuse: Once loaded in a container, assets can be cloned or instantiated multiple times without reloading from disk:

Asset Reuse Example
// Load once, use many times
// Create an AssetManager and add a container task
const assetsManager = new AssetsManager(scene);
const containerTask = assetsManager.addContainerTask(
  "chair",
  "",
  "",
  "/models/chair.glb",
);

// Start loading
assetsManager.load();

// Handle the loaded container
containerTask.onSuccess = (task) => {
  const containerWithChair = task.loadedContainer;

  // Create multiple instances
  for (let i = 0; i < 10; i++) {
    const chairInstance = containerWithChair.instantiateModelsToScene();
    chairInstance.rootNodes[0].position.x = i * 2;
  }
};

You can add all assets from a container to the scene at once, or selectively add specific assets:

Scene Management Example
// Add all assets to scene
container.addAllToScene();

// Or add selectively
container.meshes.forEach((mesh) => {
  if (mesh.name.includes("important")) {
    scene.addMesh(mesh);
  }
});

Understanding AssetManager

The AssetManager is Babylon.js's task-based asset loading system. It provides a powerful way to load multiple assets with progress tracking, error handling, and lifecycle management.

Core Concepts

Task-Based Loading: Instead of loading assets individually, you create tasks that describe what to load:

AssetManager Tasks
import { AssetsManager } from "@babylonjs/core";

const assetsManager = new AssetsManager(scene);

// Create different types of loading tasks
const meshTask = assetsManager.addMeshTask(
  "chair",
  "",
  "",
  "/models/chair.glb",
);
const textureTask = assetsManager.addTextureTask("wood", "/textures/wood.jpg");
const containerTask = assetsManager.addContainerTask(
  "furniture",
  "",
  "",
  "/models/furniture.glb",
);

Task Types Available

The AssetManager supports several task types for different asset loading needs:

Mesh Tasks: Load individual meshes or entire model files

Mesh Task Example
const meshTask = assetsManager.addMeshTask(
  "taskName", // Unique identifier
  "", // Mesh name (empty for all)
  "", // Root URL (empty if included in filename)
  "/models/car.glb", // Filename/path
);

Container Tasks: Load assets into an AssetContainer for later use

Container Task Example
const containerTask = assetsManager.addContainerTask(
  "vehicles",
  "",
  "",
  "/models/vehicles.glb",
);

containerTask.onSuccess = (task) => {
  // task.loadedContainer is now available
  const instances = task.loadedContainer.instantiateModelsToScene();
};

Texture Tasks: Load individual textures

Texture Task Example
const textureTask = assetsManager.addTextureTask(
  "groundTexture",
  "/textures/ground.jpg",
);

Progress Tracking and Events

AssetManager provides comprehensive progress tracking and event handling:

Progress Tracking
// Global progress tracking
assetsManager.onProgress = (remainingCount, totalCount, lastFinishedTask) => {
  const progress = ((totalCount - remainingCount) / totalCount) * 100;
  console.log(`Loading progress: ${progress.toFixed(1)}%`);
};

// Global completion
assetsManager.onFinish = (tasks) => {
  console.log("All assets loaded successfully!");
};

// Individual task events
meshTask.onSuccess = (task) => {
  console.log("Mesh loaded:", task.loadedMeshes);
};

meshTask.onError = (task, message, exception) => {
  console.error("Failed to load mesh:", message);
};

Loading and Lifecycle Management

The AssetManager follows a simple lifecycle pattern:

AssetManager Lifecycle
// 1. Create tasks (as shown above)
// 2. Start loading
assetsManager.load();

// 3. Reset for next batch (optional)
assetsManager.reset(); // Clears all tasks for reuse

// 4. Dispose when done (cleanup)
assetsManager.dispose();

Advanced Features

Auto-Hide Loading UI: Control Babylon's built-in loading screen

Loading UI Control
assetsManager.autoHideLoadingUI = true; // Hide default loading UI
assetsManager.useDefaultLoadingScreen = false; // Disable entirely

Task Dependencies: While not built-in, you can chain tasks based on success:

Task Chaining
const modelTask = assetsManager.addContainerTask("model", "", "", "/base.glb");
modelTask.onSuccess = (task) => {
  // Load additional assets after base model loads
  assetsManager.addTextureTask("modelTexture", "/textures/model_diffuse.jpg");
  assetsManager.load().reset();
};

Using AssetManager in a Entity Component System

Let's walk through building a mesh loading component system that loads 3D models for each entity in your game. We'll break down the code and explain how the load() function (which should probably be named loadAllEntities()) works to get meshes on in the game before any other system needs them.

shell
touch src/Components/Mesh.ts

First, we need to import everything we'll use from Babylon.js and our ECS framework:

src/Components/Mesh.ts
import { Mesh } from "@babylonjs/core";
import { Component } from "~/ECS";

We're pulling in Mesh for our 3D model type and the base Component class from our ECS system.

Mesh Component Options

Next, we define what data our mesh component needs when it's created:

src/Components/Mesh.ts
export interface MeshComponentInput {
  src: string;
}

Simple but effective - we just need the source path to the 3D model file we want to load.

Creating the Mesh Component

Now we build the actual component class that holds mesh data:

src/Components/Mesh.ts
export class MeshComponent extends Component {
  public src: string;
  public lastSrc: string;
  public mesh: Mesh | null = null;

  constructor(data: MeshComponentInput) {
    super(data);
    this.src = data.src;
    this.lastSrc = data.src;
  }
}

The component tracks the current source path, the last loaded source (for change detection), and holds a reference to the actual loaded mesh. This lets us know when we need to reload a mesh if the source changes.

Setting Up the System Imports

For the system, we need more Babylon.js functionality:

src/Components/Mesh.ts
import {
  AssetsManager,
  DracoCompression,
  Mesh,
  SceneLoader,
} from "@babylonjs/core";
import { System, Entity } from "~/DECS";

AssetsManager handles loading multiple assets efficiently, and DracoCompression provides compressed mesh support for better performance.

Creating the Mesh System

The system handles all the mesh loading logic. Let's start with the constructor:

src/Components/Mesh.ts
export class MeshSystem extends System {
  assetsManager: AssetsManager;

  constructor(scene: Scene) {
    super([MeshComponent], scene);
    this.assetsManager = new AssetsManager(this.scene!);
    this.assetsManager.autoHideLoadingUI = true;

We create an AssetsManager instance that will handle loading all our meshes. The autoHideLoadingUI setting prevents Babylon from showing its default loading screen.

Configuring Draco Compression

Next, we set up Draco compression for efficient mesh loading:

src/Components/Mesh.ts
DracoCompression.Configuration = {
  decoder: {
    wasmUrl: "/Draco/draco_wasm_wrapper_gltf.js",
    wasmBinaryUrl: "/Draco/draco_decoder_gltf.wasm",
    fallbackUrl: "/Draco/draco_decoder_gltf.js",
  },
};

This configures the paths to the Draco decoder files, allowing us to load compressed meshes for better performance and smaller file sizes.

Loading Individual Meshes

Here's where the actual mesh loading happens for each entity:

src/Components/Mesh.ts
loadMesh(entity: Entity) {
  const meshComponent = entity.getComponent(MeshComponent);
  if (!meshComponent) return;

  const meshTask = this.assetsManager.addMeshTask(
    meshComponent.src,
    "",
    "",
    meshComponent.src,
  );

We get the mesh component from the entity and create a mesh loading task. The addMeshTask method queues up the mesh for loading through the assets manager.

Handling Successful Mesh Loading

When a mesh loads successfully, we need to store it in the component:

src/Components/Mesh.ts
meshTask.onSuccess = (task) => {
  const mesh = task.loadedMeshes[0];
  meshComponent.mesh = mesh as Mesh;
  meshComponent.lastSrc = src;
  console.log(
    `Mesh loaded for entity: ${entity.name}, src: ${meshComponent.src}`,
  );
};

We grab the first loaded mesh, store it in our component, and update the lastSrc to track that this source has been loaded. The console log helps with debugging.

Handling Loading Errors

We also need to handle cases where mesh loading fails:

src/Components/Mesh.ts
meshTask.onError = (task, message, exception) => {
  console.error(
    `Error loading mesh for entity: ${entity.name}, src: ${meshComponent.src}`,
    message,
    exception,
  );
};

This logs detailed error information to help debug mesh loading issues.

Triggering the Load Process

Finally, we start the loading process and reset the assets manager:

src/Components/Mesh.ts
this.assetsManager.load().reset();

The load() method starts loading all queued assets, and reset() clears the queue for the next batch. This is the function that maybe should be named loadAllEntities() since it processes all queued mesh loading tasks.

Processing Entities Each Frame

The system checks each frame if any meshes need to be loaded:

src/Components/Mesh.ts
processEntity(entity: Entity, deltaTime: number): void {
  const meshComponent = entity.getComponent(MeshComponent);
  if (meshComponent.src != meshComponent.lastSrc) return;
  this.loadMesh(entity);
}

We compare the current source with the last loaded source. If they're different, it means the mesh source has changed and we need to load the new mesh. This gives us dynamic mesh swapping capabilities.

In Sum

This mesh loading system efficiently loads 3D models for your entities using Babylon's AssetManager. It supports Draco compression for performance, handles loading errors gracefully, and can dynamically swap meshes when sources change. The load() function (which could be better named loadAllEntities()) processes all queued mesh loading tasks in batches, making it efficient for loading multiple models at once.

↑↓ NavigateEnter SelectEsc CloseCtrl+K Open Search