Three Js Model example

How it works

A GLB model lit by an EXR environment, turned by the scroll of the page.

The class in src/js/handler/three-model loads a model and an environment with the loaders that ship with Three.js (three on npm): GLTFLoader for the .glb and EXRLoader for the .exr. The EXR goes through a PMREMGenerator, which turns the equirectangular image into the prefiltered map a PBR material needs, and becomes scene.environment: the materials of the model reflect it, with envMapIntensity set to 1 on every material that has one. The background stays transparent. A hemisphere light and a directional light are in the scene from the start, so the model is visible before the EXR arrives.

When the model loads it is centred on its bounding box and added to the scene. From then on its rotation on y is the scroll progress of the whole document, 0 at the top and 1 at the bottom, times a full turn: the ring turns once as you scroll the page. The camera has a 45° field of view at (0, 2, 6), with sRGB output, ACES filmic tone mapping and an exposure of 1.1. OrbitControls with damping are created on the canvas; on this page the component sets pointer-events: none on the frame so the page scrolls over the canvas, which also means the controls get no pointer input. Drop that rule and the model can be dragged.

The markup

One element with the js--three-webgl-model hook and a component class that gives it a size. c--three-webgl-model-a is full width and one viewport high, hides the overflow and lets the pointer through:

<!-- The element is the frame; the class appends a canvas that fills it -->
<div class="c--three-webgl-model-a js--three-webgl-model"></div>
.c--three-webgl-model-a {
    width: 100%;
    height: 100vh;
    overflow: hidden;
    pointer-events: none;

    canvas {
        display: block;
        width: 100%;
        height: 100%!important;
    }
}

The handler

Handler.js passes the element and nothing else:

this.config = ({ element }) => ({
    element,
});

The assets are not attributes: the two paths are fixed in the constructor of ThreeGLModel.js, and both files live in public/, so they are served from the root:

// public = "/"
this.modelPath = "/Ring3.glb";
this.envPath = "/TestEnv.exr";

The scroll listener is passive and only stores the progress; nothing is computed there:

events() {
  window.addEventListener("scroll", this.onScroll, { passive: true });
  window.addEventListener("resize", this.onResize);
}

onScroll = () => {
  const scrollTop = window.scrollY || window.pageYOffset;
  const docHeight = document.documentElement.scrollHeight - window.innerHeight;
  if (docHeight <= 0) return;

  this.scrollProgress = scrollTop / docHeight;
};

The loop reads it every frame, updates the damping of the controls and renders:

animate = () => {
  this.animationId = requestAnimationFrame(this.animate);

  if (this.model) {
    const maxRotation = Math.PI * 2;
    this.model.rotation.y = this.scrollProgress * maxRotation;
  }

  if (this.controls) this.controls.update();
  if (this.renderer && this.scene && this.camera) this.renderer.render(this.scene, this.camera);
};

What the framework does for you

  • Lazy loading. An element is only built when it gets within 30px of the viewport (Boostify scroll). ThreeGLModel.js, with three and its three loaders, is imported through the Manager on first use. The model and the EXR are only requested once the instance exists.
  • Page transitions. Before swup replaces the content every instance is destroyed: the frame loop is cancelled, the scroll and resize listeners removed, the controls disposed, the model removed with its geometries, materials and textures disposed, the environment render target and the PMREM generator disposed, the scene cleared, the renderer disposed and its canvas taken out of the DOM.
  • One renderer per instance. Every element gets its own WebGLRenderer, its own WebGL context and its own copy of the environment. Browsers allow a limited number of live contexts, so one model per page is the sensible use.

Model

A ring from public/, lit by an EXR environment. Scroll to turn it.

Default /Ring3.glb/TestEnv.exr

The frame is one viewport high. The ring shows lit by the fallback lights as soon as the GLB is in, and gets its reflections when the EXR finishes loading. Its rotation follows the scroll bar of the page, not the position of this block, so it keeps turning while you read the rest.

<!-- The element is the frame; the class appends a canvas that fills it -->
<div class="c--three-webgl-model-a js--three-webgl-model"></div>
Live example
/Ring3.glb/TestEnv.exr

Options reference

No attributes: the hook is the only thing the HTML carries. The assets and the camera are values in the class.

Reference

There are no data-* attributes and the handler passes no options. To use another model or environment, change the paths in ThreeGLModel.js; the rest of the class is independent of the file.

In the classValueWhat it does
modelPath/Ring3.glbThe GLB to load, relative to public/
envPath/TestEnv.exrThe equirectangular EXR used as environment map
PerspectiveCamera(45, …, 0.1, 100), position.set(0, 2, 6)fov 45Where the model is seen from
controls.target.set(0, 0.8, 0)pointWhat the camera orbits around and looks at
maxRotationMath.PI * 2The turn on y at scroll progress 1, in animate
toneMappingExposure1.1Overall brightness of the ACES tone mapping
_applyEnvIntensity(1)1How strongly the materials reflect the environment
HemisphereLight(…, 0.6), DirectionalLight(…, 1.1)intensitiesThe fallback lights, kept after the environment loads

The size of the canvas is the size of the element. c--three-webgl-model-a is one example of sizing, not part of the handler; js--three-webgl-model is a hook and carries no styles.