Three.js From Zero · Article s7-09

S7-09 Scene Transitions

Season 7 · Article 09

Scene Transitions & Loading States

Scene-to-scene changes. Fade. Dissolve via shader. Skeleton UI while model loads. Never show a raw "undefined" or a jump cut.

1. The loading phases

  1. Empty: before load starts — skeleton UI, don't show raw canvas.
  2. Loading: progress 0-100%, keep user informed.
  3. Ready: model present, dissolve/fade from skeleton.
  4. Switching: when swapping scenes, hide the gap.

2. Fade transition

// Fullscreen overlay with CSS transition
.overlay { position: absolute; inset: 0; background: #000;
           opacity: 1; transition: opacity 0.4s; }
.overlay.fade-out { opacity: 0; }

async function switchScene(newScene) {
  overlay.classList.remove('fade-out');          // fade in (black)
  await wait(400);
  unloadOldScene();
  await loadNewScene();
  overlay.classList.add('fade-out');             // fade out (show)
}

3. Shader dissolve

// Noise threshold fragment shader over fullscreen quad
uniform float uProgress;  // 0 → hidden, 1 → full
void main() {
  float noise = texture(uNoiseTex, vUv * 4.0).r;
  float alpha = smoothstep(0.0, 0.1, uProgress - noise);
  gl_FragColor = vec4(sceneColor, alpha);
}

Organic dissolve-in. Great for game level transitions.

4. Wipe

// Reveal based on screen-space position
float mask = step(uProgress, vUv.x);
gl_FragColor = vec4(newScene, 1.0 - mask);

5. Skeleton UI during loading

<div class="skeleton">
  <div class="skel-circle"></div>
  <div class="skel-bar" style="width: 60%"></div>
  <div class="skel-bar" style="width: 40%"></div>
</div>

.skel-bar { height: 12px; background: #333;
  background-image: linear-gradient(90deg, #333 0%, #555 50%, #333 100%);
  background-size: 200% 100%; animation: shimmer 1.2s infinite; }
@keyframes shimmer { 100% { background-position: -200% 0; } }

6. Three.js LoadingManager

const mgr = new THREE.LoadingManager();
mgr.onProgress = (url, loaded, total) => {
  progressBar.style.width = (loaded / total * 100) + '%';
};
mgr.onLoad = () => hideSkeleton();
const loader = new GLTFLoader(mgr);
loader.load(...);

7. Live demo — dissolve between two scenes

Click Switch to dissolve from Scene A (spheres) to Scene B (cubes).

8. Loading order best practices

  • Show skeleton first (0ms).
  • Fetch all critical assets in parallel.
  • Progress bar fires at 10% increments, not every byte.
  • Swap from skeleton to scene over 0.4-0.6s fade.
  • Never show partial scenes — wait for full load.

9. Sound cues

Each transition gets a subtle whoosh / ping. Human ear expects audio with visual changes. See S9 Audio articles.

10. Takeaways

  • Never jump-cut. Always transition.
  • Fade = 80% of cases, 0.4s duration.
  • Shader dissolve = 10%, for drama.
  • Skeleton UI during any wait > 200ms.
  • LoadingManager gives you progress + completion events.
  • Throttle progress bar to avoid DOM thrash.