LoadingManager

Progress Tracking with LoadingManager

A LoadingManager passed to several loaders counts every file they request, including files a loader fetches on its own, and calls onStart, onProgress(url, loaded, total), onLoad and onError; drive a progress bar from loaded / total. loadCovers() accepts one, and the listing also loads the fox (Fox model by PixelMannen (CC0), rigging and animation by tomkranis (CC BY 4.0), glTF conversion by @AsoboStudio and @scurest (CC BY 4.0), via the Khronos glTF Sample Assets):

One manager tracking the six covers and the foxJavaScript
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { loadCovers } from './bookshelf.js';
const manager = new THREE.LoadingManager();
const short = (url) => (url.startsWith('blob:') ? 'blob: (embedded image)' : url.slice(-20));
manager.onProgress = (url, loaded, total) => console.log(`${loaded}/${total}`, short(url));
manager.onLoad = () => console.log('all loaded');
manager.onError = (url) => console.log('failed:', url);
await Promise.all([loadCovers(manager), new GLTFLoader(manager).loadAsync('fox.glb')]);
Output
1/8 booknest-cover-1.png
2/8 booknest-cover-2.png
3/8 booknest-cover-3.png
4/8 booknest-cover-4.png
5/8 booknest-cover-5.png
6/8 booknest-cover-6.png
7/8 fox.glb
8/9 blob: (embedded image)
9/9 fox.glb
all loaded

The total grew from 8 to 9: GLTFLoader registers the .glb once to download it and again while it parses, and the texture embedded in the file arrives as a blob: URL only after parsing starts. A progress bar therefore must tolerate a total that rises, and should hide on onLoad, not when loaded first equals total.

A LoadingManager driving a progress bar while six cover textures arriveHTMLLive
<!doctype html>
<style>
  body { margin: 0; background: #f5f1e8; font: 13px system-ui, sans-serif; color: #263238; }
  #stage { position: relative; width: 600px; max-width: 100%; }
  canvas { display: block; max-width: 100%; }
  #bar { position: absolute; left: 20px; right: 20px; top: 12px; height: 10px; background: #e0d8c8; border-radius: 5px; transition: opacity 0.5s; }
  #fill { height: 100%; width: 0; background: #e09a10; border-radius: 5px; transition: width 0.2s; }
  #log { margin: 4px 8px; font: 12px monospace; white-space: pre; }
</style>
<script type="importmap">
{ "imports": {
  "three": "https://cdn.jsdelivr.net/npm/three@0.186.1/build/three.module.js",
  "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.186.1/examples/jsm/" } }
</script>
<div id="stage"><div id="bar"><div id="fill"></div></div></div>
<button id="again">Load again</button>
<div id="log"></div>
<script type="module">
import * as THREE from 'three';

const colors = ['#1f5f8b', '#5b3f99', '#e09a10', '#3f7d3a', '#b5452f', '#2a9d8f'];
const titles = ['Harbor', 'Deep Web', 'Saffron', 'Summits', 'Clockmaker', 'Gardens'];
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.setSize(600, 220);
document.getElementById('stage').prepend(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color('#fbf8f2');
const camera = new THREE.PerspectiveCamera(35, 600 / 220, 0.1, 50);
camera.position.set(0, 0, 3.4);
scene.add(new THREE.HemisphereLight('#ffffff', '#8d6e63', 2));
const books = colors.map((color, i) => {
  const book = new THREE.Mesh(new THREE.PlaneGeometry(0.42, 0.6), new THREE.MeshStandardMaterial({ color: '#d8d0c0' }));
  book.position.x = (i - 2.5) * 0.5;
  scene.add(book);
  return book;
});

// Generated covers as data URLs (a real site would request booknest-cover-N.png)
const coverURL = (i) => {
  const c = Object.assign(document.createElement('canvas'), { width: 128, height: 180 });
  const g = c.getContext('2d');
  g.fillStyle = colors[i];  g.fillRect(0, 0, 128, 180);
  g.fillStyle = '#fff3e0';  g.font = 'bold 20px Georgia';  g.fillText(titles[i], 10, 40, 108);
  return c.toDataURL();
};

const fill = document.getElementById('fill'), bar = document.getElementById('bar'), log = document.getElementById('log');
const manager = new THREE.LoadingManager();
manager.onStart = () => { bar.style.opacity = 1; };
manager.onProgress = (url, loaded, total) => {
  fill.style.width = `${loaded / total * 100}%`;          // total may grow: never assume it is fixed
  log.textContent += `${loaded}/${total}  ${url.slice(0, 30)}…\n`;
};
manager.onLoad = () => { bar.style.opacity = 0; log.textContent += 'all loaded'; };  // hide here
manager.onError = (url) => { log.textContent += `failed: ${url}\n`; };

// A loader reports to its manager with itemStart(url) when a request goes out and
// itemEnd(url) when it is done. This one waits a while first, to imitate a slow network.
function loadSlowly(url, delay, onLoad) {
  manager.itemStart(url);
  setTimeout(() => new THREE.TextureLoader().load(url, (texture) => {
    onLoad(texture);
    manager.itemEnd(url);
  }), delay);
}

function loadAll() {
  log.textContent = '';
  books.forEach((book, i) => {
    book.material.map = null;  book.material.color.set('#d8d0c0');  book.material.needsUpdate = true;
    loadSlowly(coverURL(i), 300 + i * 300, (texture) => {
      texture.colorSpace = THREE.SRGBColorSpace;
      book.material.map = texture;
      book.material.color.set('#ffffff');
      book.material.needsUpdate = true;
    });
  });
}
document.getElementById('again').onclick = loadAll;
loadAll();
renderer.setAnimationLoop(() => renderer.render(scene, camera));
</script>