Install any skill in seconds. Free to start, no credit card required.
Get Started Free →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
.claude/skills/mengto-build-threejs-scroll-worlds/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-08 | ✗→✓ | ▲ Improved | 102% | 0% |
| case-06 | ✓→✓ | = Same ✓ | 75% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 142% | 0% |
| case-05 | ✓→✓ | = Same ✓ | 103% | 0% |
| case-07 | ✓→✓ | = Same ✓ | 130% | 0% |
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.
threejs for a single interactive scene with no scroll-authored journey.scroll-world-storytelling when deciding between real-time 3D, pre-rendered video, and DOM-first storytelling.scroll-scrubbed-visual-sequence for a video or image sequence whose time is scrubbed by scroll.cinematic-scroll-storytelling for DOM-first GSAP/Lenis choreography.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.
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.
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:
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.
Define one reusable visual grammar:
Every visible detail must support the same world. Random noise, unrelated texture packs, arbitrary particle effects, and one-off materials create complexity without cohesion.
Use 4–8 chapters for most experiences. Store the full contract as data rather than scattering thresholds through CSS and the render loop:
jsconst 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.
Create these systems once:
textWebGL 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:
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.
Use a coherent PBR surface stack where it improves the image:
| map | role | common failure | | --- | --- | --- | | base color | material identity and broad variation | baked highlights fight live lighting | | normal | small directional relief | strength too high makes rubber or foil | | roughness | controls highlight breakup and age | flat values make every object plastic | | AO | contact and crevice grounding | multiplied too heavily makes dirty seams | | metalness | separates conductors from dielectrics | gray values everywhere create implausible mud | | emissive | practical lights, screens, runes | replaces light but does not illuminate nearby forms | | alpha/transmission | foliage, cloth, glass, mist | sorting, overdraw, and depth artifacts |
Record the detailed surface and light plan in the ledgers from references/world-bible.md.
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:
jsrig.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.
Compose every chapter endpoint at desktop and mobile before interpolating.
scrollWeight to give important views more dwell; do not distort the camera path just to slow a section.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.
Resolve the adjacent chapters once per frame and interpolate their declared values:
jsconst { 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.
Scroll owns macro movement. Pointer, touch, and keyboard interactions own local response.
idle, hover, focused, active, and unavailable states for each interactive object.Write the interaction matrix before implementation; use the pattern in references/world-bible.md.
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.
Make the first authored frame complete before loading the entire journey.
Do not hide a 40 MB world behind a decorative percentage. Record the load plan in the asset ledger.
Start with these budgets, then profile on the actual target device:
| budget | mobile target | desktop target | | --- | ---: | ---: | | DPR cap | 1.25–1.5 | 1.5–2 | | visible triangles | 150k–300k | 500k–1.2m | | draw calls | 50–90 | 90–160 | | shadowed lights | 1–2 | 2–4 | | simultaneously blended full-screen layers | 2 | 3 | | critical initial transfer | 3–6 MB | 5–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.
dt near 1/30 second after stalls.document.hidden and when the experience is not visible.Follow references/quality-and-qa.md. At minimum verify:
Use the Codex browser for visual and interaction verification. Build/DOM checks are not visual proof.
Return:
Keep the bundled Kage demo unchanged unless the user explicitly asks to revise the reference itself.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-06 | pass→pass | 24,194 | 17,375 | -28% | 1 | 1 | 0% | 4,178 | 7,304 | +75% | 0 | 0 | — |
case-01 | fail→fail | 43,128 | 47,082 | +9% | 1 | 1 | 0% | 8,300 | 12,412 | +50% | 0 | 0 | — |
case-02 | fail→fail | 46,207 | 51,456 | +11% | 1 | 1 | 0% | 8,301 | 12,413 | +50% | 0 | 0 | — |
case-03 | fail→fail | 44,739 | 43,338 | -3% | 1 | 1 | 0% | 8,287 | 12,399 | +50% | 0 | 0 | — |
case-04 | pass→pass | 22,653 | 27,764 | +23% | 1 | 1 | 0% | 3,451 | 8,340 | +142% | 0 | 0 | — |
case-05 | pass→pass | 21,709 | 20,076 | -8% | 1 | 1 | 0% | 3,817 | 7,761 | +103% | 0 | 0 | — |
case-07 | pass→pass | 19,901 | 20,596 | +3% | 1 | 1 | 0% | 3,669 | 8,449 | +130% | 0 | 0 | — |
case-08 | fail→pass | 21,137 | 17,094 | -19% | 1 | 1 | 0% | 3,389 | 6,856 | +102% | 0 | 0 | — |
case-09 | pass→pass | 12,753 | 11,241 | -12% | 1 | 1 | 0% | 2,361 | 6,193 | +162% | 0 | 0 | — |
case-10 | pass→pass | 17,455 | 19,454 | +11% | 1 | 1 | 0% | 2,932 | 7,309 | +149% | 0 | 0 | — |
case-11 | pass→pass | 21,866 | 14,004 | -36% | 1 | 1 | 0% | 2,942 | 6,257 | +113% | 0 | 0 | — |
case-12 | pass→pass | 18,405 | 18,405 | 0% | 1 | 1 | 0% | 2,698 | 6,917 | +156% | 0 | 0 | — |
case-13 | pass→pass | 14,902 | 14,744 | -1% | 1 | 1 | 0% | 2,246 | 6,517 | +190% | 0 | 0 | — |
case-14 | pass→pass | 16,014 | 14,503 | -9% | 1 | 1 | 0% | 2,478 | 6,412 | +159% | 0 | 0 | — |
case-15 | pass→pass | 16,479 | 16,878 | +2% | 1 | 1 | 0% | 2,576 | 6,908 | +168% | 0 | 0 | — |
case-16 | pass→pass | 21,451 | 22,768 | +6% | 1 | 1 | 0% | 3,007 | 7,624 | +154% | 0 | 0 | — |
case-17 | pass→pass | 17,010 | 14,635 | -14% | 1 | 1 | 0% | 2,258 | 6,298 | +179% | 0 | 0 | — |
case-18 | pass→pass | 17,038 | 16,060 | -6% | 1 | 1 | 0% | 2,550 | 6,327 | +148% | 0 | 0 | — |
case-19 | pass→pass | 18,312 | 19,000 | +4% | 1 | 1 | 0% | 3,262 | 7,548 | +131% | 0 | 0 | — |
case-20 | pass→pass | 14,569 | 16,328 | +12% | 1 | 1 | 0% | 2,045 | 6,611 | +223% | 0 | 0 | — |
case-21 | pass→pass | 17,695 | 15,374 | -13% | 1 | 1 | 0% | 2,978 | 6,855 | +130% | 0 | 0 | — |
case-22 | pass→pass | 15,304 | 13,023 | -15% | 1 | 1 | 0% | 2,008 | 6,055 | +202% | 0 | 0 | — |
case-23 | pass→pass | 19,305 | 20,263 | +5% | 1 | 1 | 0% | 2,737 | 7,252 | +165% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted. The headline lift of +4 percentage points is the difference between those two pass rates over the 23 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.