基本構成

3D アプリの土台。4つの主要オブジェクト(Renderer / Scene / Camera / Mesh)と レンダリングループを正しく組み立てれば、あとは枝葉を生やしていくだけで何でも作れる。

4つの主要オブジェクト

WebGLRenderer

ブラウザの WebGL(または最近は WebGPU)と話す描画エンジンcanvas 要素を内部で 作り、renderer.render(scene, camera) でその時点のシーンを 1 フレーム描く。

const renderer = new THREE.WebGLRenderer({
  antialias: true,         // ジャギーを抑える(軽い MSAA)
  alpha: false,            // 背景透明にしたい時は true
  powerPreference: "high-performance",
})
renderer.setSize(window.innerWidth, window.innerHeight)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
document.body.appendChild(renderer.domElement)

Scene

シーングラフのルートObject3D のツリーが scene.add() でぶら下がる。

const scene = new THREE.Scene()
scene.background = new THREE.Color("#0b0d12") // 単色背景
// scene.fog = new THREE.Fog("#0b0d12", 5, 30) // 距離フォグ

PerspectiveCamera

現実の遠近感を再現するカメラ。FOV(視野角)が大きいほど広角に、小さいほど望遠に。

const camera = new THREE.PerspectiveCamera(
  50,                                       // fov(度)
  window.innerWidth / window.innerHeight,   // aspect
  0.1,                                      // near
  100,                                      // far
)
camera.position.set(3, 2, 5)
camera.lookAt(0, 0, 0)

Mesh

形(geometry)+ 見た目(material)を組み合わせた、画面に見えるオブジェクトの基本単位。

const mesh = new THREE.Mesh(
  new THREE.BoxGeometry(1, 1, 1),
  new THREE.MeshStandardMaterial({ color: "orange", roughness: 0.4 }),
)
mesh.position.set(0, 0, 0)
mesh.rotation.set(0, Math.PI / 4, 0)
mesh.castShadow = true
mesh.receiveShadow = true
scene.add(mesh)

レンダリングループ

毎フレーム描画したいなら setAnimationLoop にコールバックを渡す。 requestAnimationFrame を直接書くよりWebXR 対応が楽。

renderer.setAnimationLoop(() => {
  mesh.rotation.y += 0.01
  renderer.render(scene, camera)
})

止めるには renderer.setAnimationLoop(null)
静的シーン(変化がない場合)は1回だけ render すれば十分

delta を使う(フレームレート非依存)

position.x += 0.01 のような固定値は 30fps と 120fps で速度が4倍違う。 THREE.Clock で前フレームからの経過秒数を取る。

const clock = new THREE.Clock()

renderer.setAnimationLoop(() => {
  const dt = clock.getDelta()         // 前フレームからの秒数
  const t  = clock.getElapsedTime()    // 開始からの累計秒数

  mesh.rotation.y += dt * 1.0          // 1秒で 1 ラジアン
  mesh.position.y = Math.sin(t * 2)    // 累計時間で振動

  renderer.render(scene, camera)
})

Object3D ヒエラルキー

Mesh / Light / Camera / Group など、シーンに乗るものはほぼ全部 Object3D の派生。 共通 API(position / rotation / scale / add / remove / traverse)はここで定義されている。

API意味
positionVector3。座標。position.set(x,y,z)
rotationEuler。XYZオーダーが既定。rotation.y = Math.PI
quaternionQuaternion。回転を直接持つ表現(補間が安全)。
scaleVector3。大きさ。scale.setScalar(2) で全方向2倍。
visibleboolean。子も含めて非表示にできる。
matrix / matrixWorld変換行列。普通は触らない。
add(...children) / remove(child)子要素の出し入れ。
traverse(cb)自分含む全子孫を巡回。
getWorldPosition(target)ワールド座標を target に書き出す。
lookAt(x, y, z)指定点を向くよう quaternion を更新。

Group でまとめる

論理的に「単位」になるオブジェクトを Group(= 中身のない Object3D)にまとめると、 位置・回転・スケールを一括で動かせる

const robot = new THREE.Group()
robot.add(body, head, leftArm, rightArm)
robot.position.set(0, 0, 0)
robot.rotation.y = Math.PI / 4
scene.add(robot)

リサイズ対応

ウィンドウサイズが変わったらカメラの aspect とレンダラの setSize を更新する。 これを忘れると画面が伸びる

window.addEventListener("resize", () => {
  const w = window.innerWidth, h = window.innerHeight
  camera.aspect = w / h
  camera.updateProjectionMatrix()
  renderer.setSize(w, h)
})

カラー設定(重要)

Three.js r152 以降はデフォルトで sRGB 色管理。古いチュートリアルどおりに書くと色が薄く/濃く見えることがある。

クリーンアップ

Three.js は明示的に dispose() しないとメモリが残る。 ページ遷移や SPA で破棄するとき:

function disposeAll(root) {
  root.traverse((o) => {
    if (o.isMesh) {
      o.geometry?.dispose()
      const mats = Array.isArray(o.material) ? o.material : [o.material]
      for (const m of mats) {
        for (const k of Object.keys(m)) {
          const v = m[k]
          if (v && v.isTexture) v.dispose()
        }
        m.dispose()
      }
    }
  })
  renderer.dispose()
  renderer.domElement.parentNode?.removeChild(renderer.domElement)
}

詳細は 注意点・パフォーマンス