Skip to content
Babylon.js Importing & Asset Management — glTF, GLB, and Optimization

Babylon.js Importing & Asset Management — glTF, GLB, and Optimization

DodaTech Updated Jun 6, 2026 13 min read

Real-world 3D applications rarely use only built-in primitives. You import complex models — characters, buildings, vehicles — created in Blender, Maya, or other 3D tools. Babylon.js provides a powerful asset pipeline for loading glTF/GLB files, managing loading progress, optimizing meshes, compressing textures, and even exporting scenes back to glTF.

What You’ll Learn

By the end of this tutorial, you’ll import glTF/GLB models with SceneLoader.ImportMesh, use AssetContainer for preloading assets without adding them to the scene immediately, track loading progress with a progress bar, enable Draco mesh compression for smaller files, merge meshes and set up LOD for performance, use KTX2 compressed textures, and export scenes to glTF/GLB.

Prerequisites: You should be comfortable with Babylon.js scenes, cameras, lights, and materials.

Why Asset Management Matters

In production 3D applications, you don’t create every object with code. Artists design detailed models in Blender, export them as glTF files, and developers load them at runtime. These models can be complex — thousands of polygons, multiple materials, skeletal animations, and high-resolution textures.

Security note: Understanding Babylonjs Importing helps build more secure applications — a core principle at DodaTech, where tools like Durga Antivirus Pro and Doda Browser rely on solid implementation practices.

Loading them efficiently matters:

  • A 50MB model with no progress bar = users think the app is broken
  • Importing without optimization = slow frame rate
  • Loading uncompressed textures = excessive GPU memory

Asset management is the bridge between your code and the artist’s work.

Real-world use: Durga Antivirus Pro’s 3D network topology viewer imports thousands of server models from glTF files. AssetContainers preload building models while showing a progress bar. Each model uses LOD to reduce polygons when zoomed out. KTX2 textures keep GPU memory usage under 300MB even with hundreds of servers visible.

Where This Fits in Your Learning Path

    flowchart LR
    A["Babylon.js Getting Started"] --> B["Materials & Textures"]
    B --> C["Animation & Physics"]
    C --> D["GUI & Interaction"]
    D --> E["**Importing & Assets**"]
    style E fill:#f97316,stroke:#c2410c,color:#fff
    style A fill:#e5e7eb,stroke:#9ca3af,color:#374151
    style E fill:#f97316,stroke:#c2410c,color:#fff
  

SceneLoader.ImportMesh — The Primary Import Method

ImportMesh loads a model file and adds it to the scene. It accepts callbacks for success, progress, and error:

BABYLON.SceneLoader.ImportMesh(
  '',                          // mesh name filter ('' = all)
  './models/',                 // root URL (where the model file is)
  'robot.glb',                 // filename
  scene,                       // target scene
  (meshes, particleSystems, skeletons, animationGroups) => {
    // Success! meshes is an array of loaded meshes
    console.log('Loaded', meshes.length, 'meshes')

    // Position the imported model
    meshes[0].position.y = 0

    // Scale if the model is too big or too small
    meshes[0].scaling.scaleInPlace(0.01)

    // Play the first animation
    if (animationGroups.length > 0) {
      animationGroups[0].start(true, 1.0)  // loop, speed
    }
  },
  (evt) => {
    // Progress callback
    const progress = (evt.loaded / evt.total) * 100
    console.log('Loading:', progress.toFixed(1) + '%')
  },
  (scene, message, exception) => {
    // Error callback
    console.error('Load failed:', message, exception)
  }
)

Understanding the parameters:

ParamDescription
meshNamesFilter by name: '' = all, 'Body' = single mesh, ['Arm_L', 'Arm_R'] = multiple
rootUrlBase path where the model and its textures are located
sceneFilenameThe model file name (.glb, .gltf, .babylon)
sceneTarget scene
onSuccessCalled with loaded meshes, particle systems, skeletons, animation groups
onProgressCalled with { loaded, total } as bytes are downloaded
onErrorCalled with error details

AssetContainer — Preloading Without Adding to Scene

AssetContainer loads all assets from a file into a container without adding them to the scene. You can inspect, modify, and then decide when to add them:

BABYLON.SceneLoader.LoadAssetContainer(
  './models/',
  'building.glb',
  scene,
  (container) => {
    // Inspect before adding
    console.log('Meshes:', container.meshes.length)
    console.log('Materials:', container.materials.length)
    console.log('Skeletons:', container.skeletons.length)
    console.log('AnimationGroups:', container.animationGroups.length)

    // Modify assets before adding to scene
    container.meshes.forEach(m => {
      m.position.x += 5
    })

    // Add ALL to the scene at once
    container.addAllToScene()

    // Or add selectively:
    // container.meshes.forEach(m => scene.addMesh(m))

    // Remove from scene later:
    // container.removeAllFromScene()
    // container.dispose()
  }
)

Use cases for AssetContainer:

  • Preloading screens: load models in the background, show progress, add them all at once when ready
  • Scene transitions: load next scene while current scene is running
  • Pooling: keep reusable assets in containers, add/remove as needed
  • Level streaming: load/unload chunks of a large world

glTF/GLB Support

glTF (GL Transmission Format) is the recommended format for Babylon.js. It’s the “JPEG of 3D” — a standard, efficient format supported by almost every 3D tool.

  • .glb — Binary format, single file (mesh + textures + scene). Easy to distribute.
  • .gltf — JSON format with separate .bin and texture files. Easier to debug.

Babylon.js auto-detects the format by file extension:

Babylon.js SceneLoader supports: .glb, .gltf, .babylon, .obj, .stl

Draco Mesh Compression

Draco compresses 3D geometry data, reducing file sizes by 90-95%:

// Register the Draco decoder before loading compressed models
BABYLON.DracoCompression.Configuration = {
  decoder: {
    wasmUrl: 'https://www.gstatic.com/draco/versioned/decoders/1.5.6/draco_decoder.wasm',
    wasmWorkerUrl: 'https://www.gstatic.com/draco/versioned/decoders/1.5.6/draco_wasm_wrapper.js',
    fallbackUrl: 'https://www.gstatic.com/draco/versioned/decoders/1.5.6/draco_decoder.js',
  }
}

Why Draco? A 10MB model with Draco becomes 500KB. The GPU decompresses it instantly with no performance penalty.


Loading Progress

Display a visual progress bar to users so they know something is happening:

BABYLON.SceneLoader.ImportMesh('', './models/', 'city.glb', scene, null,
  (evt) => {
    const pct = (evt.loaded / evt.total) * 100
    document.getElementById('progressBar').style.width = pct + '%'
    document.getElementById('progressText').textContent =
      Math.round(pct) + '% — ' + formatBytes(evt.loaded) + ' / ' + formatBytes(evt.total)
  }
)

function formatBytes(bytes) {
  if (bytes < 1024) return bytes + ' B'
  if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB'
  return (bytes / 1048576).toFixed(1) + ' MB'
}

AssetsManager — Track Multiple Loads

For loading multiple files with a single progress bar:

const loader = new BABYLON.AssetsManager(scene)

const meshTask = loader.addMeshTask('robot load', '', './models/', 'robot.glb')
meshTask.onSuccess = (task) => {
  task.loadedContainer.addAllToScene()
}
meshTask.onError = (task, message, exception) => {
  console.error('Asset task failed:', message)
}

// Global progress
loader.onProgress = (remaining, total, lastTask) => {
  const pct = ((total - remaining) / total) * 100
  updateProgressBar(pct)
}

loader.onFinish = (tasks) => {
  console.log('All', tasks.length, 'assets loaded')
  hideLoadingScreen()
}

loader.load()

Optimizing Imported Meshes

After import, optimize meshes for performance.

Merge Meshes That Share Materials

Reduces draw calls — your most important optimization:

BABYLON.Mesh.MergeMeshes(
  meshes.filter(m => m.material === sharedMat),
  true,   // disposeSource — remove the original meshes
  true,   // allow32BitsIndices
  null,   // meshToMerge
  false   // subdivideWithSubMeshes
)

Level of Detail (LOD)

Show simpler models when objects are far away:

mesh.addLODLevel(15, lodMesh1)  // show lodMesh1 when distance > 15
mesh.addLODLevel(30, lodMesh2)  // show lodMesh2 when distance > 30

Freeze Static Meshes

If a mesh never moves, freeze its world matrix for a performance boost:

mesh.freezeNormals()
mesh.freezeWorldMatrix()

Texture Compression (KTX2 / Basis)

KTX2 compressed textures use 70-85% less GPU memory than PNG:

// Configure KTX2 decoder
BABYLON.KhronosTextureContainer2.Configuration = {
  decoder: {
    url: 'https://www.babylonjs.com/assets/basis/basis_transcoder.js',
    wasmUrl: 'https://www.babylonjs.com/assets/basis/basis_transcoder.wasm',
  }
}

// Load a KTX2 texture
const ktxTex = new BABYLON.Texture('textures/armor.ktx2', scene)
FormatGPU Memory vs PNGBest For
PNG100%Simple UI, small textures
KTX2 (UASTC)~25%High-quality, all-purpose
KTX2 (ETC1S)~15%Mobile, low-end GPUs

Exporting to glTF

You can export entire scenes back to glTF/GLB:

// Export to .gltf (JSON + separate bin/textures)
BABYLON.GLTF2Export.GLTF(scene, 'scene-name.gltf', {
  shouldExportAnimation: true,
  shouldExportMaterials: true,
  shouldExportTextures: true,
  shouldExportSkin: true,
  shouldExportMorphTargets: true,
}).then((exporter) => {
  exporter.downloadFiles()
})

// Export to .glb (single binary file)
BABYLON.GLTF2Export.GLB(scene, 'scene-name.glb').then((exporter) => {
  exporter.downloadFiles()
})

Common Mistakes Beginners Make

1. Model Appears Tiny or Huge

Imported models often have different scale conventions. A building from Blender might be 100 units tall when your scene expects 1 unit.

// Scale to fit: experiment with values like 0.01, 0.1, or 10
meshes[0].scaling.scaleInPlace(0.01)

2. Textures Missing (404 Errors)

Texture paths in glTF are relative to the model file’s location. If the model is at ./models/building.glb and textures are at ./models/textures/brick.png, set rootUrl correctly:

BABYLON.SceneLoader.ImportMesh('', './models/', 'building.glb', scene, ...)

3. Animations Not Playing

// Wrong — the animation starts but doesn't loop
animationGroups[0].start()

// Correct — loop and play at normal speed
animationGroups[0].start(true, 1.0)

4. Cross-Origin Errors

You can’t load models from file:// protocol. Use a local dev server:

npx serve .   # or
python -m http.server

5. Draco-Compressed Models Fail

You must register the Draco decoder before loading:

BABYLON.DracoCompression.Configuration = { decoder: { wasmUrl: '...' } }

6. Imported Mesh Appears Black

PBR materials need an environment texture. Set one on the scene:

scene.environmentTexture = BABYLON.CubeTexture.CreateFromPrefilteredData('env.env', scene)

Practice Questions

  1. What’s the difference between ImportMesh and LoadAssetContainer? ImportMesh loads and immediately adds to the scene. LoadAssetContainer loads into a container without adding — you can inspect and modify before calling addAllToScene().

  2. What is Draco compression and why use it? Draco compresses geometry data by 90-95%, reducing file size. The GPU decompresses it instantly with no runtime performance cost.

  3. What does the progress callback’s evt.loaded and evt.total represent? Bytes downloaded so far and total bytes. Use them to calculate percentage: (loaded / total) * 100.

  4. Why would you merge meshes after import? To reduce draw calls. Each mesh is a separate draw call; merging meshes that share a material combines them into one draw call.

  5. What is the recommended 3D format for Babylon.js? glTF/GLB — it’s the industry standard, supports PBR materials, animations, skeletons, and Draco compression.

Challenge

Build a 3D asset viewer with:

  • A file input that accepts .glb, .gltf, and .babylon files
  • Drag-and-drop support on the canvas
  • A loading progress bar
  • Auto-centering and scaling of loaded models
  • Animation playback controls (play, pause, stop, frame slider)
  • Display of model metadata (mesh count, vertex count, animation count)
  • A “Load Sample” button that loads a model from a URL

FAQ

What is the difference between .glb and .gltf?

GLB is a single binary file containing mesh data, textures, and scene graph — easy to distribute. GLTF is a JSON file with separate .bin and texture files — easier to debug.

How do I load a model from a URL?

Set rootUrl to the base URL and sceneFilename to the filename. For example: rootUrl: 'https://example.com/models/' and sceneFilename: 'model.glb'.

How do I load multiple models with a single progress bar?

Use AssetsManager. Add multiple tasks with addMeshTask, then use loader.onProgress for combined progress and loader.onFinish for completion.

Why is my animation not looping?

When calling animationGroup.start(), pass true as the second parameter: animationGroup.start(true, 1.0). The first param is loop, the second is speedRatio.

How do I optimize models for Babylon.js?

  1. Use glTF/GLB format
  1. Enable Draco mesh compression
  2. Use KTX2 compressed textures
  3. Merge meshes that share materials
  4. Add LOD levels for distant objects
  5. Freeze world matrices for static objects

Try It Yourself: 3D Asset Viewer

Import models via file picker or drag-and-drop, see loading progress, and control animations.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Babylon.js — 3D Asset Viewer</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    html, body { width: 100%; height: 100%; overflow: hidden; font-family: system-ui, sans-serif; }
    #renderCanvas { width: 100%; height: 100%; display: block; }
    #ui { position: absolute; top: 0; left: 0; right: 0; z-index: 10; display: flex; flex-direction: column; pointer-events: none; }
    #toolbar { display: flex; align-items: center; gap: 12px; padding: 12px 16px; background: rgba(0,0,0,0.7); backdrop-filter: blur(4px); color: #fff; pointer-events: auto; }
    #toolbar button, #toolbar label { background: #4a6cf7; color: #fff; border: none; padding: 6px 14px; border-radius: 4px; cursor: pointer; font-size: 13px; pointer-events: auto; }
    #toolbar button:hover { background: #5f7cf7; }
    #toolbar input[type="file"] { display: none; }
    #toolbar .spacer { flex: 1; }
    #toolbar .status { font-size: 13px; opacity: 0.8; }
    #progressContainer { width: 100%; height: 4px; background: rgba(255,255,255,0.1); pointer-events: none; }
    #progressBar { height: 100%; width: 0%; background: #4a6cf7; transition: width 0.2s; }
    #dropOverlay { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(74, 108, 247, 0.15); border: 3px dashed #4a6cf7; display: none; align-items: center; justify-content: center; color: #fff; font-size: 24px; font-weight: bold; z-index: 20; pointer-events: none; backdrop-filter: blur(2px); }
    #dropOverlay.active { display: flex; }
    #info { position: absolute; bottom: 16px; left: 16px; right: 16px; display: flex; gap: 16px; pointer-events: none; color: rgba(255,255,255,0.7); font-size: 12px; }
  </style>
</head>
<body>
  <canvas id="renderCanvas"></canvas>
  <div id="ui">
    <div id="toolbar">
      <strong>3D Asset Viewer</strong>
      <label for="fileInput">Open File</label>
      <input type="file" id="fileInput" accept=".glb,.gltf,.babylon" />
      <button id="loadSample">Load Sample</button>
      <span class="spacer"></span>
      <span class="status" id="statusText">No model loaded</span>
      <button id="animPlay" disabled>Play</button>
      <button id="animPause" disabled>Pause</button>
      <button id="animStop" disabled>Stop</button>
      <label for="animSlider">Anim <input type="range" id="animSlider" min="0" max="100" value="0" disabled style="width:100px;vertical-align:middle" /></label>
    </div>
    <div id="progressContainer"><div id="progressBar"></div></div>
  </div>
  <div id="dropOverlay">Drop model file here</div>
  <div id="info"><span id="meshInfo"></span><span id="vertexInfo"></span><span id="animInfo"></span></div>
  <script src="https://cdn.babylonjs.com/babylon.js"></script>
  <script src="https://cdn.babylonjs.com/loaders/babylonjs.loaders.min.js"></script>
  <script>
    const canvas = document.getElementById('renderCanvas')
    const engine = new BABYLON.Engine(canvas, true, { stencil: true })
    const scene = new BABYLON.Scene(engine)
    scene.clearColor = new BABYLON.Color3(0.08, 0.08, 0.12)

    const envTex = BABYLON.CubeTexture.CreateFromPrefilteredData('https://www.babylonjs.com/assets/environment/environmentSpecular.env', scene)
    scene.environmentTexture = envTex; scene.environmentIntensity = 0.6

    const camera = new BABYLON.ArcRotateCamera('cam', -Math.PI / 2.5, Math.PI / 2.8, 5, new BABYLON.Vector3(0, 1, 0), scene)
    camera.lowerRadiusLimit = 1; camera.upperRadiusLimit = 50; camera.attachControl(canvas, true)

    const hemi = new BABYLON.HemisphericLight('hemi', new BABYLON.Vector3(0, 1, 0), scene); hemi.intensity = 0.5
    const dir = new BABYLON.DirectionalLight('dir', new BABYLON.Vector3(-1, -2, -1), scene)
    const ground = BABYLON.MeshBuilder.CreateGround('ground', { width: 20, height: 20 }, scene)
    const gMat = new BABYLON.StandardMaterial('gMat', scene); gMat.diffuseColor = new BABYLON.Color3(0.12, 0.12, 0.16); gMat.specularColor = BABYLON.Color3.Black(); ground.material = gMat

    let loadedMeshes = [], animationGroups = [], currentContainer = null

    function updateProgress(pct) { document.getElementById('progressBar').style.width = pct + '%' }
    function clearModel() { if (currentContainer) { currentContainer.removeAllFromScene(); currentContainer.dispose(); currentContainer = null } loadedMeshes = []; animationGroups = []; document.getElementById('statusText').textContent = 'No model loaded'; document.getElementById('meshInfo').textContent = '—'; document.getElementById('vertexInfo').textContent = '—'; document.getElementById('animInfo').textContent = '—'; disableAnimControls() }

    function onModelLoaded(container) {
      currentContainer = container; container.addAllToScene()
      loadedMeshes = container.meshes.filter(m => m.getTotalVertices() > 0)
      if (loadedMeshes.length > 0) {
        let min = loadedMeshes[0].getBoundingInfo().minimum.clone(), max = loadedMeshes[0].getBoundingInfo().maximum.clone()
        loadedMeshes.forEach(m => { const bi = m.getBoundingInfo(); min = BABYLON.Vector3.Minimize(min, bi.minimum); max = BABYLON.Vector3.Maximize(max, bi.maximum) })
        const size = max.subtract(min).length(); const center = max.add(min).scale(0.5)
        if (size > 0.001) { const scale = 3 / size; loadedMeshes.forEach(m => { m.position.subtractInPlace(center); m.position.scaleInPlace(scale); m.scaling.scaleInPlace(scale) }); camera.target = BABYLON.Vector3.Zero(); camera.radius = 5 }
      }
      animationGroups = container.animationGroups
      const totalVerts = loadedMeshes.reduce((sum, m) => sum + m.getTotalVertices(), 0)
      document.getElementById('statusText').textContent = container.meshes.length + ' objects loaded'
      document.getElementById('meshInfo').textContent = 'Meshes: ' + loadedMeshes.length
      document.getElementById('vertexInfo').textContent = 'Verts: ' + totalVerts.toLocaleString()
      if (animationGroups.length > 0) { document.getElementById('animInfo').textContent = 'Animations: ' + animationGroups.length; enableAnimControls(); animationGroups[0].start(true, 1.0) } else disableAnimControls()
    }

    function loadModel(rootUrl, filename) { clearModel(); updateProgress(0); BABYLON.SceneLoader.LoadAssetContainer(rootUrl, filename, scene, (container) => { onModelLoaded(container); updateProgress(100); setTimeout(() => updateProgress(0), 500) }, (evt) => { if (evt.total > 0) updateProgress((evt.loaded / evt.total) * 100) }, (s, msg, exc) => { console.error('Load error:', msg, exc); document.getElementById('statusText').textContent = 'Load failed'; updateProgress(0) }) }

    document.getElementById('fileInput').addEventListener('change', (e) => { const file = e.target.files[0]; if (!file) return; const url = URL.createObjectURL(file); const rootUrl = url.substring(0, url.lastIndexOf('/') + 1); const filename = url.substring(url.lastIndexOf('/') + 1); loadModel(rootUrl, filename) })
    document.getElementById('loadSample').addEventListener('click', () => { loadModel('https://www.babylonjs.com/assets/', 'Dude.glb') })

    const dropOverlay = document.getElementById('dropOverlay')
    canvas.addEventListener('dragenter', (e) => { e.preventDefault(); dropOverlay.classList.add('active') })
    canvas.addEventListener('dragover', (e) => e.preventDefault())
    canvas.addEventListener('dragleave', () => dropOverlay.classList.remove('active'))
    canvas.addEventListener('drop', (e) => { e.preventDefault(); dropOverlay.classList.remove('active'); const files = e.dataTransfer.files; if (files.length === 0) return; const file = files[0]; const ext = file.name.split('.').pop().toLowerCase(); if (!['glb', 'gltf', 'babylon'].includes(ext)) { document.getElementById('statusText').textContent = 'Unsupported format: .' + ext; return }; const url = URL.createObjectURL(file); const rootUrl = url.substring(0, url.lastIndexOf('/') + 1); const filename = url.substring(url.lastIndexOf('/') + 1); loadModel(rootUrl, filename) })

    function enableAnimControls() { ['animPlay', 'animPause', 'animStop', 'animSlider'].forEach(id => document.getElementById(id).disabled = false) }
    function disableAnimControls() { ['animPlay', 'animPause', 'animStop', 'animSlider'].forEach(id => document.getElementById(id).disabled = true); document.getElementById('animSlider').value = 0 }
    document.getElementById('animPlay').addEventListener('click', () => animationGroups.forEach(g => g.play()))
    document.getElementById('animPause').addEventListener('click', () => animationGroups.forEach(g => g.pause()))
    document.getElementById('animStop').addEventListener('click', () => { animationGroups.forEach(g => g.stop()); document.getElementById('animSlider').value = 0 })
    document.getElementById('animSlider').addEventListener('input', function () { const val = parseFloat(this.value) / 100; animationGroups.forEach(g => { g.goToFrame(g.targetedAnimations[0].animation.getHighestFrame() * val) }) })

    engine.runRenderLoop(() => {
      if (animationGroups.length > 0 && animationGroups[0].isPlaying) {
        const anim = animationGroups[0].targetedAnimations[0].animation; const currentFrame = anim.getCurrentFrame(); const maxFrame = anim.getHighestFrame()
        if (maxFrame > 0) document.getElementById('animSlider').value = ((currentFrame / maxFrame) * 100).toFixed(0)
      }
      scene.render()
    })
    window.addEventListener('resize', () => engine.resize())
  </script>
</body>
</html>

What to expect: Open a .glb file or click “Load Sample” to see a 3D model with auto-centering, animation controls, and metadata display.


What’s Next

You’ve completed the Babylon.js series. Next, explore other frontend libraries:

LibraryWhat You’ll Learn
Chart.js Getting StartedInteractive charts in the browser
GSAP Getting StartedHigh-performance web animations

Related topics: Babylon.js GUI & Interaction, 3D Modeling, WebGL.

What’s Next

Congratulations on completing this Babylonjs Importing tutorial! Here’s where to go from here:

  • Practice daily — Consistency is more important than long study sessions
  • Build a project — Apply what you learned by building something real
  • Explore related topics — Check out other tutorials in the same category
  • Join the community — Discuss with other learners and share your progress

Remember: every expert was once a beginner. Keep coding!

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro