three.jsでスクロールに追従してカメラを上下させる

three.jsでスクロールに追従してカメラを上下させることで、以下のようにcanvas内の3DをDOMと連動しているように見せたいと思います。

![ThreeScrollWithCameraMoveSample.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61569/6fcf6da6-9e8f-315e-83ca-e112e9f736a4.gif)

サンプルは以下に置いてあります。
[aadebdeb/ThreeScrollWithCameraMoveSample](https://github.com/aadebdeb/ThreeScrollWithCameraMoveSample)

基本的なHTMLとCSSはこのようになります。canvasを背景としてウィンドウいっぱいに表示して、その上にテキストを表示するようにします。

```html
<style>
  body {
    margin: 0;
  }
  p {
    color: white;
    font: 5rem bold;
    margin: 0;
  }
  canvas {
    margin: 0;
    position: fixed;
    top: 0;
    left: 0;
    z-index: -1;
  }
</style>
<div style="height: 5000px">
    <p style="position: absolute; top: 0px;">0: Red Sphere</p>
    <p style="position: absolute; top: 1000px;">1000: Green Sphere</p>
    <p style="position: absolute; top: 2000px;">2000: Blue Sphere</p>
    <p style="position: absolute; top: 3000px;">3000: White Sphere</p>
    <p style="position: absolute; top: 4000px;">4000: Black Sphere</p>
</div>
<canvas id="canvas"></canvas>
```

スクリプトは以下のようになります。レンダリングループ内でスクロールの情報をもとにカメラのY座標を上下させることでDOMと連動させます。

```js
const canvas = document.getElementById('canvas');
const renderer = new THREE.WebGLRenderer({ canvas });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(new THREE.Color(0x999999), 1.0);

const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 500);
const scene = new THREE.Scene();

const sphereGeom = new THREE.SphereBufferGeometry();
const redSphere = new THREE.Mesh(sphereGeom, new THREE.MeshBasicMaterial({ color: new THREE.Color(0xff0000) }));
const greenSphere = new THREE.Mesh(sphereGeom, new THREE.MeshBasicMaterial({ color: new THREE.Color(0x00ff00) }));
const blueSphere = new THREE.Mesh(sphereGeom, new THREE.MeshBasicMaterial({ color: new THREE.Color(0x0000ff) }));
const whiteSphere = new THREE.Mesh(sphereGeom, new THREE.MeshBasicMaterial({ color: new THREE.Color(0xffffff) }));
const blackSphere = new THREE.Mesh(sphereGeom, new THREE.MeshBasicMaterial({ color: new THREE.Color(0x000000) }));
scene.add(redSphere);
scene.add(greenSphere);
scene.add(blueSphere);
scene.add(whiteSphere);
scene.add(blackSphere);  

const interval = 10.0;
// オブジェクトの位置設定
// y=0 をカメラが映す上端として各位置の比率をDOMと合わせておく
redSphere.position.y = -0.5 * interval;
greenSphere.position.y = -1.5 * interval;
blueSphere.position.y = -2.5 * interval;
whiteSphere.position.y = -3.5 * interval;
blackSphere.position.y = -4.5 * interval;
// カメラが映す範囲
// 最後までスクロールにしたときにカメラが映す下端が y=-viewVerticalRange になる
const viewVerticalRange = 5.0 * interval;

const animate = () => {
  requestAnimationFrame(animate);

  // カメラが縦方向に写すべき範囲
  const cameraVerticalRange =  viewVerticalRange * window.innerHeight / document.body.clientHeight;
  // カメラのZ座標を調整して縦方向に写る範囲がcameraVerticalRangeになるようにする
  camera.position.z = cameraVerticalRange / Math.tan(0.5 * camera.fov * Math.PI / 180.0) * 0.5;
  // (上2行は実際にはリサイズ時のみにおこなえばよい)

  // スクロール位置を正規化して[0, 1]にする
  const scrollRate = window.pageYOffset / (document.body.clientHeight - window.innerHeight);
  // スクロール位置をもとにカメラのy座標を調整する
  camera.position.y = -0.5 * cameraVerticalRange - scrollRate * (viewVerticalRange - cameraVerticalRange);

  renderer.render(scene, camera);
};
animate();
```

手書きで分かりづらいですが、こんな感じの計算をしています。
![ThreeScrollWithCameraMove.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61569/af6e8ec3-741a-dde8-7d5b-e5365237b5d5.jpeg)