Generated banner for Build Threejs Scroll Worlds
Generated logo for Build Threejs Scroll Worlds

Build Threejs Scroll Worlds

Meng ToCommunityCreated by Meng To

  • Skill
  • Workflow
  • Software development
  • MIT
  • Medium risk
  • Has scripts

From the author

Build rich, scroll-controlled real-time Three.js experiences as one persistent 3D world whose camera, lighting, atmosphere, materials, objects, DOM story, and interactions evolve across authored chapters. Use for 3D scrollytelling, scroll-driven WebGL worlds, camera journeys, interactive portfolios, product stories, exhibitions, explainers, game or film microsites, spatial narratives, and multi-scene websites where native scroll should travel through one continuous place. Not limited to landing pages.
— the description by Meng To

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

This skill provides a structured approach to creating real-time 3D experiences where native document scroll acts as the deterministic conductor for a persistent Three.js world. It emphasizes maintaining spatial continuity while evolving camera composition, lighting, atmosphere, and interactions across defined story beats. The workflow focuses on data-driven scene management, using a central ledger to coordinate state changes rather than scattering logic throughout the render loop.

What it does

  • Maps native scroll position to a fractional chapter value for deterministic state control.
  • Defines a persistent WebGL scene architecture with separate layers for environment, architecture, and interactives.
  • Provides a ledger-based system to manage camera, lighting, and material states per chapter.
  • Establishes a workflow for progressive asset loading and performance budgeting.
  • Integrates semantic DOM content with 3D spatial narratives.
  • Includes a QA framework for verifying transitions, responsive composition, and interaction stability.

When to use it

  • When building 3D scrollytelling or scroll-driven WebGL worlds.
  • When creating interactive portfolios or product stories that require a continuous 3D space.
  • When objects must remain interactive and spatially consistent while the camera moves through multiple compositions.
  • When a project requires a deterministic, reversible scroll-controlled journey.

Advantages

  • Ensures spatial continuity by keeping the renderer and scene graph alive.
  • Decouples story state from scroll events using a centralized conductor.
  • Provides a clear structure for managing complex 3D scenes through data ledgers.
  • Supports responsive design by allowing chapter-specific camera and layout overrides.
  • Facilitates consistent performance through defined asset and draw call budgets.

Usage examples

“I need to build a 3D product showcase where the user scrolls through different parts of a machine. How do I keep the camera movement smooth?”
The agent will guide you to define a chapter ledger with camera endpoints and use a scroll-conductor to interpolate between them, ensuring smooth, deterministic movement.
“How should I structure my Three.js scene for a multi-chapter scrollytelling site?”
The agent will provide a scene architecture template, including a worldRoot for persistent objects and chapterSets for localized props, ensuring efficient culling and loading.

Install

  1. Download the skill as a ZIP:

    https://autogineer.com/agent-skills/mengto/build-threejs-scroll-worlds/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip build-threejs-scroll-worlds.zip -d .agents/skills

Documentation by Meng To

Build Three.js Scroll Worlds

Build one detailed real-time world and use native document scroll as its deterministic conductor. Keep the renderer, scene graph, and spatial continuity alive while camera composition, light, fog, animation, copy, and interaction focus move through authored chapters.

The mechanism is one persistent Three.js world + one normalized reversible scroll state. If removing either makes the experience collapse into stacked sections, this skill applies.

The exact Kage demo proves the quality bar; it is staging, not a mandatory subject or layout. Use its detailed anatomy only when the requested direction benefits from it: references/kage-anatomy.md.

Route the request correctly

  • Use threejs for a single interactive scene with no scroll-authored journey.

  • Use scroll-world-storytelling when deciding between real-time 3D, pre-rendered video, and DOM-first storytelling.

  • Use scroll-scrubbed-visual-sequence for a video or image sequence whose time is scrubbed by scroll.

  • Use cinematic-scroll-storytelling for DOM-first GSAP/Lenis choreography.

  • Use this skill when objects must remain truly spatial and interactive while scroll moves through several compositions in one live WebGL world.

Do not disguise a video as Three.js. The public oso95/scroll-world project, reviewed at commit 71cc36d, is a strong reference for intake, scene ledgers, budget gates, mobile-specific composition, config-driven playback, and seam QA, but its renderer is a pre-generated video chain. This skill adopts those structural strengths while retaining real geometry, materials, lighting, raycasting, and camera control.

Read the build references

Before implementing a new world, read:

Copy references/scroll-conductor.js when a project needs a portable native-scroll conductor rather than a framework-specific implementation.

Start with the output, not the template

Determine what the world is for: a landing page, portfolio, museum chapter, product explanation, game teaser, editorial essay, data story, or another spatial experience. Do not force every request into a hero-plus-CTA layout.

Discover only what is unknown:

  • the subject, audience, and ordered story beats;

  • the art direction and emotional arc;

  • whether the world is continuous, portal-connected, or composed as adjacent sets;

  • the target devices and minimum acceptable hardware;

  • how interactive the world should be beyond scrolling;

  • available models, textures, brand assets, audio, and copy;

  • the initial-load and total-download budget.

If the user supplied an approved reference or working scene, inspect it first and carry its real geometry, materials, textures, light ratios, camera values, and motion constants into the ledger. Do not replace measured details with adjectives.

Write the world bible before code

Define one reusable visual grammar:

  • scale system and unit convention;

  • silhouette language and level of stylization;

  • palette and per-chapter accent progression;

  • material families and wear logic;

  • key, fill, rim, practical, and emissive light roles;

  • fog, haze, particles, weather, and post-processing;

  • typography, DOM composition, and near-plane foreground rules;

  • motion grammar for camera, ambient objects, and interactions.

Every visible detail must support the same world. Random noise, unrelated texture packs, arbitrary particle effects, and one-off materials create complexity without cohesion.

Author a scene ledger

Use 4–8 chapters for most experiences. Store the full contract as data rather than scattering thresholds through CSS and the render loop:

const chapters = [
  {
    id: "threshold",
    scrollWeight: 1.25,
    copy: {
      eyebrow: "Chapter 01",
      title: "Enter the archive",
      body: "A spatial collection revealed through motion."
    },
    camera: {
      position: [0, 3.8, 13.5],
      target: [0, 2.4, -8],
      fov: 38,
      mobile: { position: [0, 4.8, 18], fov: 46 }
    },
    world: {
      key: 1.0,
      practicals: 0.35,
      fog: 0.018,
      particles: 0.25,
      grade: "cold"
    },
    focus: ["gate", "lantern-left"],
    interactions: ["inspect-gate"],
    assets: ["gate-shell", "stone-1k", "mist-atlas"]
  }
];

For each chapter record the story beat, landmark, camera endpoint, occluders, light/fog state, active animation clips, interaction targets, DOM beat, foreground treatment, asset dependencies, and responsive override. Reject a chapter that differs only by copy.

Build one persistent spatial architecture

Create these systems once:

WebGL canvas
  worldRoot
    environment       sky, terrain, distant silhouettes
    architecture      persistent landmarks and paths
    chapterSets       local props grouped for culling and loading
    interactives      raycast targets and animation state
    atmosphere        fog volumes, particles, weather
    nearPlane3D       optional camera-relative depth accents

DOM above canvas
  semantic chapters  headings, copy, links, media, fallback order
  fixed interface    progress, chapter navigation, controls
  fixed cut-outs      optional alpha foregrounds at viewport edges

Use one renderer and normally one scene. Use layers or render passes when transparency, post-processing, or interaction isolation requires them. Do not instantiate one renderer per chapter or rebuild the world at seams.

Choose a topology deliberately:

  • Continuous geography: landmarks share one navigable space; strongest presence, highest world-building cost.

  • Connected sets: rooms or islands are joined by tunnels, gates, fog banks, terrain folds, or darkness; easiest to art-direct and stream.

  • Layered reveal: the same place changes through time, scale, or state; animate materials, visibility, and light instead of moving to unrelated coordinates.

Hide unavoidable discontinuities behind occlusion, darkness, dense atmosphere, an interior threshold, or a close foreground pass. Never let an object visibly teleport in open space.

Give the world real detail

Geometry
  • Establish the large silhouette first, then medium forms, then small accents. A dense scatter of tiny props cannot rescue a weak silhouette.

  • Give architectural edges believable thickness, bevels, joints, caps, reveals, and contact points. Paper-thin roofs and floating props expose the fake immediately.

  • Use modular kits, instancing, merged static meshes, and LODs instead of duplicating unique geometry.

  • Break repetition with authored clusters, scale/rotation ranges, decals, and material variation—not unbounded randomness.

  • Keep collision and raycast proxies simpler than render meshes.

Materials and textures

Use a coherent PBR surface stack where it improves the image:

maprolecommon failure
base colormaterial identity and broad variationbaked highlights fight live lighting
normalsmall directional reliefstrength too high makes rubber or foil
roughnesscontrols highlight breakup and ageflat values make every object plastic
AOcontact and crevice groundingmultiplied too heavily makes dirty seams
metalnessseparates conductors from dielectricsgray values everywhere create implausible mud
emissivepractical lights, screens, runesreplaces light but does not illuminate nearby forms
alpha/transmissionfoliage, cloth, glass, mistsorting, overdraw, and depth artifacts
  • Set color textures to sRGB and keep normal, roughness, AO, metalness, and data textures linear.

  • Keep texel density consistent within a camera range. Reserve 2K–4K maps for surfaces that genuinely fill the frame; use 512–1K atlases for most props.

  • Prefer KTX2/Basis compression, Meshopt/Draco geometry compression, atlases, trim sheets, decals, and baked AO/light where appropriate.

  • Add anisotropy only to shallow-angle surfaces that need it. Setting every texture to the maximum wastes bandwidth.

  • Make wear causal: exposed edges polish, recesses collect grime, water leaves vertical or pooling traces, and paths compress vegetation. Uniform grunge reads as a filter.

  • For foliage, use alpha-tested cards or instanced low-poly clusters before transparent blended planes. Transparency overdraw is often more expensive than geometry.

Lighting and atmosphere
  • Start with one authored key direction, a restrained environment/fill, and practical emissive sources. Add lights only when they create a visible relationship.

  • Bake static bounce and contact where possible. Keep shadow-casting dynamic lights to roughly 1–2 on mobile and 2–4 on desktop unless profiling proves more.

  • Use fog and haze to separate depth planes, not merely darken the scene. Keep the focal landmark inside the contrast window.

  • Match emissive geometry, glow sprites, actual light intensity, and bloom threshold so lamps feel luminous without bleaching the frame.

  • Use post-processing after the unprocessed frame is composed. Bloom, vignette, grain, chromatic separation, and depth of field must not hide weak materials or framing.

Record the detailed surface and light plan in the ledgers from references/world-bible.md.

Map native scroll to deterministic state

Measure stable section anchors only after fonts and critical media settle. Convert scrollY into a fractional chapter value such as 2.35.

Keep separate values:

rig.target = progressFromScroll(scrollY);       // exact reproducible story state
rig.smooth = reduceMotion
  ? rig.target
  : damp(rig.smooth, rig.target, 5.2, dt);      // cinematic render state

Use exact progress for navigation, URLs, accessibility, foreground ownership, and interaction gating. Use smoothed progress for camera and visual interpolation only. The same scroll position must recreate the same state forward, backward, after a fast jump, and after reload.

Use references/scroll-conductor.js as the baseline implementation. Keep native scroll as the source of truth; never integrate wheel delta into story position.

Author the camera as cinematography

Compose every chapter endpoint at desktop and mobile before interpolating.

  • Store position, target, FOV, optional roll, lens shift, and responsive overrides.

  • Use Catmull–Rom curves for broad continuous travel and segment interpolation for deliberate turns.

  • Inspect the curve for wall penetration, ground clipping, target flips, speed spikes, and accidental close passes.

  • Parameterize by chapter progress, not raw curve arc length, so story pacing remains intentional.

  • Use scrollWeight to give important views more dwell; do not distort the camera path just to slow a section.

  • Add restrained pointer parallax only after the base composition works. Clamp it and blend it out near precise transitions.

  • On tall viewports, author a pullback/FOV override instead of accepting an arbitrary center crop.

Camera motion must expose new spatial relationships: approach, reveal, passage, scale change, inspection, horizon, departure. Six dolly-ins aimed at the same center are not six scenes.

Interpolate world state from the same conductor

Resolve the adjacent chapters once per frame and interpolate their declared values:

const { a, b, t } = segmentState(rig.smooth);
moon.scale.setScalar(lerp(a.world.moon, b.world.moon, t));
key.intensity = lerp(a.world.key, b.world.key, t);
scene.fog.density = lerp(a.world.fog, b.world.fog, t);

Prefer continuous physical change: occlusion, material blend, animation mixer weight, light, fog, scale, and transform. When swapping assets, crossfade only within an occluded or atmospherically dense interval and keep both states loaded until the transition completes.

Add interactions without fighting scroll

Scroll owns macro movement. Pointer, touch, and keyboard interactions own local response.

  • Use raycasting against named proxy meshes, not every decorative triangle.

  • Define idle, hover, focused, active, and unavailable states for each interactive object.

  • Let hover warm light, shift material response, reveal a label, or trigger a short animation; do not move the scroll camera away from its path.

  • Pause or soften pointer parallax while the user drags a control or activates a hotspot.

  • Gate interactions by exact chapter visibility and depth; hidden objects must not capture events.

  • Mirror every essential hotspot with a DOM button or link in document order. Give it a visible focus state and synchronize its state with the 3D object.

  • On touch, use tap-to-focus then tap-to-activate when accidental activation would be costly. Keep hit targets at least 44 CSS px in the DOM proxy.

  • Make decorative particle trails and cursor effects optional, camera-relative where appropriate, and disabled for coarse pointers or reduced motion.

Write the interaction matrix before implementation; use the pattern in references/world-bible.md.

Keep the DOM story semantic

Keep headings, body copy, links, controls, and the footer in real HTML above or beside the canvas. The 3D world creates place; the DOM carries meaning and fallback order.

  • Reveal display headings by word only when it supports the rhythm; preserve the complete accessible label.

  • Reveal eyebrow, heading, body, media, proof, and CTA as independent beats.

  • Keep the reading block stable while the camera makes its largest move.

  • Use optional fixed alpha cut-outs as near-plane scenery, not as a requirement of every design. When used, keep them fully opaque while active and fade/blur them out after ownership changes.

Load progressively

Make the first authored frame complete before loading the entire journey.

  1. Inline or preload the critical shell, camera, first-scene geometry, smallest environment, and fallback poster.

  2. Load the next chapter before it can enter; prefetch one or two chapters ahead.

  3. Decode textures off the main interaction path and reveal a chapter only when its required assets are ready.

  4. Keep a designed poster and semantic DOM visible if WebGL, a model, or a texture fails.

  5. Report meaningful progress by required asset weight or completed groups, not an arbitrary timer.

Do not hide a 40 MB world behind a decorative percentage. Record the load plan in the asset ledger.

Hold a measurable performance budget

Start with these budgets, then profile on the actual target device:

budgetmobile targetdesktop target
DPR cap1.25–1.51.5–2
visible triangles150k–300k500k–1.2m
draw calls50–9090–160
shadowed lights1–22–4
simultaneously blended full-screen layers23
critical initial transfer3–6 MB5–10 MB
steady frame time≤16.7 ms ideal, ≤25 ms fallback≤16.7 ms

These are starting envelopes, not success claims. Measure CPU, GPU, texture memory, shader compilation, long tasks, and first-interactive frame.

  • Cap dt near 1/30 second after stalls.

  • Pause on document.hidden and when the experience is not visible.

  • Use a quality governor that lowers DPR and optional effects before deleting authored landmarks.

  • Render secondary cameras, reflections, cloth simulations, and expensive particles only while relevant.

  • Dispose geometries, materials, textures, render targets, observers, events, animation mixers, and RAF during teardown.

Respect motion, access, and failure

  • Preserve native reversible scroll; do not trap the wheel or force a custom scrollbar.

  • For reduced motion, snap camera/state to the nearest composed chapter, stop ambient loops and stagger/blur, and retain the complete ordered DOM story.

  • Keep a real heading hierarchy, landmarks, links, form controls, visible focus, and reachable footer.

  • Provide a composed poster or chapter stills when WebGL is unavailable.

  • Maintain contrast when the world changes behind copy; use authored scrims or local contrast management, not an opaque blanket over the whole scene.

Verify the whole journey

Follow references/quality-and-qa.md. At minimum verify:

  • every camera endpoint at 1440×900, 768×1024, and 390×844;

  • slow, fast, reverse, scrollbar-drag, anchor navigation, reload-at-depth, and resize between chapters;

  • material scale, UVs, texture color space, roughness response, contact grounding, shadow stability, fog depth, and post-processing;

  • hover, focus, tap, activation, chapter gating, and interaction recovery after scrolling away and back;

  • first-load poster, progressive chapter loading, failed asset, context loss, hidden-tab resume, and teardown;

  • reduced motion, WebGL fallback, keyboard order, readable DOM, reachable footer, and a clean console;

  • target-device frame time, draw calls, triangles, texture memory, transfer size, and shader compilation.

Use the Codex browser for visual and interaction verification. Build/DOM checks are not visual proof.

Deliver

Return:

  • the world bible and art-direction constants;

  • chapter, camera, material/texture, interaction, asset, and performance ledgers;

  • a layer and scene-graph map;

  • the working experience and local runtime assets;

  • desktop, tablet, and mobile evidence from representative chapters;

  • measured performance and loading results;

  • fallback/reduced-motion evidence;

  • remaining visual, interaction, or device gaps stated plainly.

Keep the bundled Kage demo unchanged unless the user explicitly asks to revise the reference itself.

Files

references/realtime-architecture.md

Real-time Three.js scroll-world architecture

Use this reference when implementing the persistent renderer, scene graph, scroll conductor, camera path, world-state interpolation, asset streaming, interactions, quality profiles, and teardown.

Contents

  1. Runtime contract

  2. Scene graph

  3. Renderer and color pipeline

  4. Asset pipeline

  5. Scroll conductor

  6. Camera rig

  7. World-state interpolation

  8. Chapter streaming and culling

  9. Interaction system

  10. DOM synchronization

  11. Responsive composition

  12. Quality governor

  13. Lifecycle and failure

  14. Debug surfaces

1. Runtime contract

Keep one authoritative configuration:

const worldSpec = {
  units: "meters",
  initialChapter: "threshold",
  quality: {
    mobile: { dpr: 1.35, shadows: 1024, particles: 0.45, post: "lite" },
    desktop: { dpr: 1.75, shadows: 2048, particles: 1, post: "full" }
  },
  chapters,
  groups: {
    critical: ["shell", "threshold"],
    approach: ["path", "garden"],
    deep: ["archive", "reactor"]
  }
};

Drive all subsystems from the same fractional progress object:

{
  exact: 2.35,
  smooth: 2.28,
  index: 2,
  next: 3,
  localExact: 0.35,
  localSmooth: 0.28,
  direction: 1
}

Do not let the camera, DOM, foreground host, navigation, particles, and interactions compute their own chapter rules independently.

2. Scene graph

Use named groups that match production responsibility:

const worldRoot = new THREE.Group();
worldRoot.name = "world";

const environment = new THREE.Group();
environment.name = "environment";

const landmarks = new THREE.Group();
landmarks.name = "landmarks";

const chapterSets = new THREE.Group();
chapterSets.name = "chapter-sets";

const interactives = new THREE.Group();
interactives.name = "interactives";

const atmosphere = new THREE.Group();
atmosphere.name = "atmosphere";

worldRoot.add(environment, landmarks, chapterSets, interactives, atmosphere);
scene.add(worldRoot);

Inside each loaded model, normalize useful names once:

gltf.scene.traverse((node) => {
  node.frustumCulled = true;
  if (node.isMesh) {
    node.castShadow = shouldCast(node.name);
    node.receiveShadow = shouldReceive(node.name);
    if (node.userData.interactive) registerInteractive(node);
  }
});

Avoid scene traversal in the render loop. Build registries for animated objects, lights, materials, mixers, particles, and interactive proxies during loading.

3. Renderer and color pipeline

Configure the renderer before validating materials:

const renderer = new THREE.WebGLRenderer({
  canvas,
  antialias: quality.antialias,
  alpha: false,
  powerPreference: "high-performance"
});

renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.0;
renderer.setPixelRatio(Math.min(devicePixelRatio, quality.dpr));
renderer.shadowMap.enabled = quality.shadows > 0;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;

Material QA is meaningless until output color space, tone mapping, exposure, environment intensity, and post-processing order are stable.

Use explicit texture roles:

baseColor.colorSpace = THREE.SRGBColorSpace;
emissiveMap.colorSpace = THREE.SRGBColorSpace;

normalMap.colorSpace = THREE.NoColorSpace;
roughnessMap.colorSpace = THREE.NoColorSpace;
metalnessMap.colorSpace = THREE.NoColorSpace;
aoMap.colorSpace = THREE.NoColorSpace;

If the Three.js version predates colorSpace, use the equivalent encoding API and pin that version locally. Do not mix examples from different Three.js releases.

PBR setup checks
  • Duplicate the primary UV set into uv2 when an AO map requires it and the asset lacks one.

  • Process HDR environments through PMREMGenerator and dispose the source texture and generator after assignment.

  • Keep normalScale conservative, often .25–.8 depending on authoring strength.

  • Use alphaTest for foliage and fences when soft transparency is unnecessary.

  • Use polygonOffset, render order, or a decal library deliberately; random z offsets eventually flicker.

  • Keep transmission/refraction surfaces rare and small. They may require the scene color and multiply full-frame cost.

4. Asset pipeline

Prefer glTF/GLB with Meshopt or Draco where it materially reduces transfer, and KTX2/Basis for GPU-ready textures.

const ktx2 = new KTX2Loader()
  .setTranscoderPath("/basis/")
  .detectSupport(renderer);

const draco = new DRACOLoader().setDecoderPath("/draco/");

const loader = new GLTFLoader()
  .setKTX2Loader(ktx2)
  .setDRACOLoader(draco)
  .setMeshoptDecoder(MeshoptDecoder);

Pin loader versions to the Three.js runtime version. A mismatched loader and core build can fail only on compressed assets, making the problem look like corrupted media.

Load by groups
class AssetGroup {
  constructor(id, urls) {
    this.id = id;
    this.urls = urls;
    this.state = "idle";
    this.assets = new Map();
  }
}

Maintain idle → queued → loading → ready | failed → retired. Retire only disposable chapter-local assets; never retire a landmark still visible from later frames.

Decode and compile before reveal:

await loadGroup(group);
scene.add(group.root);
await renderer.compileAsync(scene, camera);
group.state = "ready";

Where compileAsync is unavailable, render a hidden or occluded warm-up frame. Avoid first-use shader compilation during a visible scroll seam.

Texture residency
  • Keep one material/texture instance per shared asset.

  • Release object URLs after loaders consume them.

  • Dispose chapter-local render targets and textures when their group retires.

  • Do not dispose shared resources from a single mesh destructor; reference-count them or dispose at world teardown.

5. Scroll conductor

Use native scroll position as the exact state. Copy scroll-conductor.js or implement the same contract.

Measure after layout stabilizes:

await document.fonts?.ready;
await Promise.all(criticalImages.map(waitForImage));
conductor.measure();

Observe later layout changes with ResizeObserver. Re-measure after content insertion, font swap, orientation change, or a width-changing resize. Ignore touch-browser URL-bar height noise when width and orientation are unchanged.

Use exponential damping independent of frame rate:

function damp(current, target, lambda, dt) {
  return THREE.MathUtils.lerp(current, target, 1 - Math.exp(-lambda * dt));
}

Clamp dt after a pause:

dt = Math.min(clock.getDelta(), 1 / 30);

The exact state must update immediately on scroll, pageshow, hashchange, history restoration, and programmatic anchor navigation. The damped state may catch up visually.

6. Camera rig

Separate the authored camera from local input offsets:

cameraRig
  pathRig          authored position and orientation
    inputRig       pointer/device micro-parallax
      camera       projection and post effects

Do not add pointer offsets directly to waypoint data.

Position and target curves
const positionCurve = new THREE.CatmullRomCurve3(
  chapters.map(c => new THREE.Vector3(...c.camera.position)),
  false,
  "centripetal",
  0.5
);

const targetCurve = new THREE.CatmullRomCurve3(
  chapters.map(c => new THREE.Vector3(...c.camera.target)),
  false,
  "centripetal",
  0.5
);

Map each segment deliberately. A single global curve.getPoint(progress / lastIndex) gives every chapter equal parametric space even when scrollWeight differs; that is fine when the conductor already expresses fractional chapter progress. Sample per segment if chapters need different curve types or easings.

Avoid interpolating Euler angles. Interpolate position and target, then call lookAt; or author quaternions and use slerp when roll is intentional.

Responsive camera

Resolve a camera endpoint from the active breakpoint before curve construction. Do not linearly blend from a desktop curve to a mobile curve during a normal resize; rebuild the curve and preserve the exact chapter progress.

For an emergency tall-screen correction:

const tall = Math.max(0, innerHeight / innerWidth - 1.15);
resolvedPosition.addScaledVector(viewDirection, -tall * pullback);
resolvedFov += tall * fovOpen;

Author explicit overrides for hero and close-interior frames. Formula-only corrections cannot know where the copy or landmark sits.

Collision guard

Sample each camera segment offline or in debug mode. Cast a sphere or several rays from the previous sample to the next against coarse environment proxies. Flag points whose clearance falls below the camera near-plane plus a design margin.

Do not run dense world collision every frame for a fixed authored route.

7. World-state interpolation

Use named state channels:

const channels = {
  key: value => { key.intensity = value; },
  fog: value => { scene.fog.density = value; },
  core: value => {
    coreMaterial.emissiveIntensity = value;
    coreGlow.material.opacity = smoothstep(0.2, 1, value);
  },
  foliage: value => { foliageWindTarget = value; }
};

Resolve adjacent chapter values and interpolate once. Do not let every object compare raw progress to arbitrary thresholds.

Use the correct interpolation type:

  • scalar/vector: linear or smoothstep;

  • color: interpolate in the intended working space, checking luminance through the grade;

  • quaternion: spherical interpolation;

  • visibility: opacity or an occluded swap window;

  • animation: mixer weight crossfade;

  • discrete interaction availability: exact progress range, no damping.

Keep update order stable:

  1. read exact scroll and input targets;

  2. update damped progress and local input rigs;

  3. resolve camera and world state;

  4. update animation mixers, simulations, and particles;

  5. update interaction proxies and DOM state;

  6. render post-processing passes;

  7. sample performance.

8. Chapter streaming and culling

Use a prefetch window based on exact progress and scroll direction:

const near = Math.round(state.exact);
ensureLoaded(near);
ensureLoaded(near + state.direction);
ensureLoaded(near + state.direction * 2);

Keep the previous chapter resident until the transition is complete and reverse scroll is safe. Do not unload the only asset needed to reproduce the state just above the current seam.

Use multiple strategies:

  • frustum culling for ordinary meshes;

  • chapter-group visibility for distant or impossible sets;

  • LODs for persistent landmarks;

  • instancing for repeated props;

  • animation and particle suspension outside the relevance window;

  • reduced shadow casting outside the focal chapter.

Turning a group invisible does not free its GPU memory. Use that distinction deliberately.

9. Interaction system

Raycast against proxy layers:

raycaster.layers.set(INTERACTIVE_LAYER);
camera.layers.enable(INTERACTIVE_LAYER);

const hits = raycaster.intersectObjects(activeProxies, false);
const hit = hits.find(h => interactionAvailable(h.object.userData.id, exactProgress));

Build a finite state machine per interaction:

unavailable → idle ↔ hover/focus → active → idle
                         ↘ chapter-exit → unavailable

Synchronize pointer and DOM focus through the same state setter. Do not create separate hover and keyboard visual code paths.

function setInteractionState(id, next, source) {
  const item = registry.get(id);
  item.state = next;
  item.apply3D(next);
  item.dom?.toggleAttribute("data-active", next === "active");
  item.dom?.setAttribute("aria-expanded", String(next === "active"));
}

Throttle pointer raycasts to one per animation frame and skip them when the pointer is unchanged, the tab is hidden, reduced motion disables the effect, or no interactions are available.

When an active detail panel should stop macro motion, do not block scrolling. Close or gracefully retire the panel when exact progress leaves its range.

10. DOM synchronization

Keep DOM updates event-based. On chapter or interaction change, toggle stable classes/attributes rather than writing every style on every frame.

Use exact state for:

  • active navigation and aria-current;

  • chapter URL/hash;

  • DOM reading ownership;

  • interactive button availability;

  • foreground stage ownership;

  • analytics milestones.

Use smooth state for:

  • visual word/element entrances;

  • local opacity and translate interpolation;

  • camera-matched scrim intensity.

Preserve complete accessible text when visually splitting headings:

<h2 aria-label="The archive wakes beneath us">
  <span aria-hidden="true"><span>The</span> <span>archive</span> …</span>
</h2>

11. Responsive composition

Treat mobile as a second composition of the same world, not a cropped desktop render.

  • Resolve mobile camera endpoints and safe copy zones.

  • Reduce foreground density, not landmark identity.

  • Prefer a simpler post stack and lower DPR before removing the world.

  • Keep small interactive objects reachable through DOM controls even if their 3D proxy is too small.

  • Use 100dvh where supported for fixed DOM, while keeping scroll-anchor math based on measured elements.

  • Ignore height-only touch-browser resizes; rebuild on width/orientation change.

12. Quality governor

Choose a tier from hardware and measured frame time, then allow conservative runtime downgrades.

const tiers = {
  high:   { dpr: 1.75, shadows: 2048, particles: 1.0, reflections: true, post: "full" },
  medium: { dpr: 1.35, shadows: 1024, particles: 0.55, reflections: false, post: "lite" },
  low:    { dpr: 1.0,  shadows: 0,    particles: 0.2, reflections: false, post: "grade" }
};

Sample a rolling frame-time window after shader warm-up. Downgrade only after sustained misses, for example 120 frames above 22 ms. Do not oscillate tiers; require a reload or a long stable interval before upgrading.

Use the cheapest high-impact lever first: DPR, full-resolution post passes, reflections, shadow update frequency, transparent particles, then geometry LOD.

13. Lifecycle and failure

Pause and resume cleanly:

document.addEventListener("visibilitychange", () => {
  running = !document.hidden;
  if (running) lastTime = performance.now();
});

Handle WebGL context loss:

canvas.addEventListener("webglcontextlost", (event) => {
  event.preventDefault();
  running = false;
  showFallback("The 3D world paused. Restore or continue with the story below.");
});

If restoration is supported, rebuild GPU resources from retained asset sources and reapply exact scroll state. Do not reload the page without user intent.

Dispose once:

  • cancel RAF and timers;

  • disconnect ResizeObserver, IntersectionObserver, and media-query listeners;

  • remove scroll, pointer, keyboard, history, and visibility listeners;

  • stop audio and animation mixers;

  • dispose geometries, unique materials, textures, skeleton helpers, render targets, PMREM targets, controls, and renderer;

  • revoke object URLs;

  • clear DOM classes and fixed foreground ownership.

Track ownership so shared resources are not disposed twice.

14. Debug surfaces

Ship a development-only overlay that can show:

  • exact/smooth progress and direction;

  • active and prefetched chapter groups;

  • camera position, target, FOV, and waypoint labels;

  • path and target curves;

  • interaction proxy bounds and hit results;

  • renderer triangles, calls, points, lines, textures, and programs;

  • current quality tier and rolling frame time;

  • asset group state and transfer size;

  • toggles for fog, post, shadows, textures, wireframe, and fallback.

The debug overlay prevents subjective guessing. Remove or gate it in production, but keep the instrumentation callable.

Requirements and compatibility

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Needs: Needs Node.js

Security

What our scanner noticed· Medium risk

  • Large base64-encoded blobs
  • Scripts that your agent may run

This skill includes scripts that your agent may run. Read them before you use it.

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Created by
Meng To
Publisher
Meng To (Community)
Source
MengTo/Skills · agent-skills/web-design/build-threejs-scroll-worlds · commit 5ea1661
Licence
MIT License (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by Meng To or Meng To.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download build-threejs-scroll-worlds.zip