Babylon.js Importing & Asset Management — glTF, GLB, and Optimization
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.
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:
| Param | Description |
|---|---|
meshNames | Filter by name: '' = all, 'Body' = single mesh, ['Arm_L', 'Arm_R'] = multiple |
rootUrl | Base path where the model and its textures are located |
sceneFilename | The model file name (.glb, .gltf, .babylon) |
scene | Target scene |
onSuccess | Called with loaded meshes, particle systems, skeletons, animation groups |
onProgress | Called with { loaded, total } as bytes are downloaded |
onError | Called 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.binand texture files. Easier to debug.
Babylon.js auto-detects the format by file extension:
Babylon.js SceneLoader supports: .glb, .gltf, .babylon, .obj, .stlDraco 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)| Format | GPU Memory vs PNG | Best For |
|---|---|---|
| PNG | 100% | 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.server5. 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
What’s the difference between
ImportMeshandLoadAssetContainer?ImportMeshloads and immediately adds to the scene.LoadAssetContainerloads into a container without adding — you can inspect and modify before callingaddAllToScene().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.
What does the progress callback’s
evt.loadedandevt.totalrepresent? Bytes downloaded so far and total bytes. Use them to calculate percentage:(loaded / total) * 100.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.
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.babylonfiles - 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
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:
| Library | What You’ll Learn |
|---|---|
| Chart.js Getting Started | Interactive charts in the browser |
| GSAP Getting Started | High-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