WebGL & Three.js

Three.js入門|回転する立方体をViteで表示する最小チュートリアル

Three.js初心者向けに、npmとViteの準備から回転する立方体の表示まで解説します。Scene、Camera、Renderer、Geometry、Material、Meshの役割、レスポンシブ対応、アニメーション、後片付けまで一つずつ確認できます。

この記事の目次
  1. このチュートリアルで作るもの
  2. Three.jsをnpmとViteでインストールする
  3. ファイルを3つ作る
  4. HTMLにcanvasとJavaScriptを読み込む
  5. canvasの表示サイズをCSSで決める
  6. 完成版のJavaScriptを書く
  7. Scene・Camera・Rendererを作る
  8. PerspectiveCameraの4つの引数
  9. GeometryとMaterialからMeshを作る
  10. canvasを画面サイズに追従させる
  11. setAnimationLoopで立方体を回転させる
  12. Viteの開発サーバーで確認する
  13. npmを使わずCDNで試す方法
  14. よくあるエラーと確認順
  15. Failed to resolve module specifier “three”
  16. 画面が真っ暗で何も表示されない
  17. 立方体が横長または縦長になる
  18. スマートフォンで動作が重い
  19. 使い終わったリソースを解放する
  20. 次に学ぶ内容
  21. よくある質問
  22. Three.jsを始める前にWebGLを学ぶ必要はありますか?
  23. JavaScriptだけでThree.jsを使えますか?
  24. ライトを作っていないのに立方体が見えるのはなぜですか?
  25. requestAnimationFrameではだめですか?
  26. まとめ

Three.jsを初めて使う人に向けて、回転する立方体をブラウザへ表示するところまでを一つずつ解説します。

この記事ではnpmとViteを使います。完成までの流れは次のとおりです。

  1. Three.jsとViteをインストールする
  2. canvasを用意する
  3. Scene、Camera、Rendererを作る
  4. GeometryとMaterialからMeshを作る
  5. MeshをSceneへ追加する
  6. アニメーションループで描画する

Geometry(形)+ Material(見た目)= Mesh(描画する物体)を作り、Scene(3D空間)へ置き、Camera(視点)から見た結果をRendererがcanvasへ描く、と考えると全体を理解しやすくなります。

WebGLとの違いから確認したい場合は、先にWebGLとThree.jsの違いを読んでください。

このチュートリアルで作るもの

作るのは、濃い背景の中央で緑色の立方体が回転する最小構成です。画像、3Dモデル、ライト、外部コントロールは使いません。

項目 内容
実行環境 ブラウザ、Node.js、npm、Vite
ライブラリ Three.js
ファイル index.htmlmain.jsstyle.css
3Dオブジェクト BoxGeometry + MeshBasicMaterial

HTMLやJavaScriptの基礎、ターミナルでコマンドを実行する方法が分かれば進められます。シェーダーや行列計算の知識はまだ必要ありません。

スポンサーリンク

Three.jsをnpmとViteでインストールする

Three.js公式マニュアルでは、一般的な開発方法としてnpmとbuild toolを使う手順を案内しています。ここでは公式例と同じくViteを使います。

任意の場所でプロジェクト用フォルダを作り、移動します。

mkdir threejs-first-app
cd threejs-first-app
npm init -y

Three.jsとViteをインストールします。

npm install three
npm install --save-dev vite

この入門ではThree.js以外の実行時ライブラリは不要です。Parcel、GSAP、glslify、GUIライブラリなどをまとめて入れる必要はありません。

ファイルを3つ作る

プロジェクト直下を次の構成にします。

threejs-first-app/
├── index.html
├── main.js
├── style.css
└── package.json

Viteはルートにあるindex.htmlを入口として読み込みます。

HTMLにcanvasとJavaScriptを読み込む

index.htmlへ次の内容を書きます。

<!doctype html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Three.js first app</title>
    <link rel="stylesheet" href="/style.css" />
  </head>
  <body>
    <canvas id="three-canvas"></canvas>
    <script type="module" src="/main.js"></script>
  </body>
</html>

Three.jsはES Modulesとしてimportするため、script要素へtype="module"を指定します。Rendererへ既存のcanvasを渡すので、HTML側でcanvasを一つ用意しています。

canvasの表示サイズをCSSで決める

style.cssへ次の内容を書きます。

* {
  box-sizing: border-box;
}

html,
body {
  width: 100%;
  height: 100%;
  margin: 0;
}

body {
  overflow: hidden;
  background: #111827;
}

#three-canvas {
  display: block;
  width: 100%;
  height: 100%;
}

canvasの見た目の大きさはCSSで管理し、JavaScript側では表示サイズに合わせて描画バッファを調整します。

完成版のJavaScriptを書く

main.jsへ次のコードを書きます。各処理の意味は後から順番に説明します。

import * as THREE from "three";

const canvas = document.querySelector("#three-canvas");

const renderer = new THREE.WebGLRenderer({
  canvas,
  antialias: true,
});

const scene = new THREE.Scene();
scene.background = new THREE.Color(0x111827);

const camera = new THREE.PerspectiveCamera(45, 2, 0.1, 100);
camera.position.z = 4;

const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshBasicMaterial({ color: 0x34d399 });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);

function resizeRendererToDisplaySize() {
  const pixelRatio = Math.min(window.devicePixelRatio, 2);
  const width = Math.floor(canvas.clientWidth * pixelRatio);
  const height = Math.floor(canvas.clientHeight * pixelRatio);
  const needsResize = canvas.width !== width || canvas.height !== height;

  if (needsResize) {
    renderer.setSize(width, height, false);
  }

  return needsResize;
}

function animate(time) {
  if (resizeRendererToDisplaySize()) {
    camera.aspect = canvas.clientWidth / canvas.clientHeight;
    camera.updateProjectionMatrix();
  }

  const seconds = time * 0.001;
  cube.rotation.x = seconds * 0.4;
  cube.rotation.y = seconds * 0.7;

  renderer.render(scene, camera);
}

renderer.setAnimationLoop(animate);

Scene・Camera・Rendererを作る

最初にThree.jsを読み込み、描画の土台となる3つのオブジェクトを作ります。

import * as THREE from "three";

const canvas = document.querySelector("#three-canvas");
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(45, 2, 0.1, 100);
オブジェクト 役割
Scene Mesh、Light、Cameraなどを管理する3D空間
PerspectiveCamera 遠近感のある視点。どこから何を見るかを決める
WebGLRenderer SceneをCameraの視点からcanvasへ描画する

antialias: trueは輪郭のギザギザを軽減する指定です。端末負荷との交換条件があるため、重いシーンでは必要性を確認してください。

PerspectiveCameraの4つの引数

const camera = new THREE.PerspectiveCamera(45, 2, 0.1, 100);

引数は順に、垂直方向の画角、アスペクト比、近くの描画限界、遠くの描画限界です。初期値のアスペクト比2は仮の値で、描画時にcanvasの実寸へ更新します。

Cameraは作っただけでは原点にあります。立方体も原点へ置くため、Cameraをz軸方向へ移動して物体との距離を作ります。

camera.position.z = 4;

GeometryとMaterialからMeshを作る

表示する物体は、形状を表すGeometryと、見た目を表すMaterialをMeshへ組み合わせて作ります。

const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshBasicMaterial({ color: 0x34d399 });
const cube = new THREE.Mesh(geometry, material);

scene.add(cube);

BoxGeometry(1, 1, 1)は幅・高さ・奥行きが1の立方体です。Geometryの種類や頂点データはThree.jsのGeometryの種類と変更方法で詳しく解説しています。

MeshBasicMaterialはライトの影響を受けず、指定色をそのまま表示しやすいMaterialです。入門コードでライトを省けるため採用しています。材質の違いはThree.jsのMaterialの種類と変更方法を参照してください。

最後のscene.add(cube)を忘れると、Meshを作っても画面には表示されません。

canvasを画面サイズに追従させる

canvasには、CSS上の表示サイズと、実際にピクセルを描く描画バッファサイズがあります。CSSだけを変更すると、引き伸ばされてぼやけたり、Cameraの縦横比と合わずに物体がつぶれたりします。

次の関数はcanvasの表示サイズを読み、必要なときだけRendererをリサイズします。

function resizeRendererToDisplaySize() {
  const pixelRatio = Math.min(window.devicePixelRatio, 2);
  const width = Math.floor(canvas.clientWidth * pixelRatio);
  const height = Math.floor(canvas.clientHeight * pixelRatio);
  const needsResize = canvas.width !== width || canvas.height !== height;

  if (needsResize) {
    renderer.setSize(width, height, false);
  }

  return needsResize;
}

高密度ディスプレイの倍率を無制限に使うと、描画するピクセル数とGPU負荷が急増します。この例では最大2へ抑えています。品質と速度の優先度に合わせて調整してください。

Rendererをリサイズしたら、Cameraのアスペクト比も更新します。

if (resizeRendererToDisplaySize()) {
  camera.aspect = canvas.clientWidth / canvas.clientHeight;
  camera.updateProjectionMatrix();
}

setAnimationLoopで立方体を回転させる

Three.jsのWebGLRenderer公式ドキュメントは、手動でrequestAnimationFrame()を再帰する代わりにsetAnimationLoop()を使うよう案内しています。

function animate(time) {
  const seconds = time * 0.001;

  cube.rotation.x = seconds * 0.4;
  cube.rotation.y = seconds * 0.7;

  renderer.render(scene, camera);
}

renderer.setAnimationLoop(animate);

コールバックのtimeはミリ秒です。秒へ変換して回転角を計算すると、端末のフレームレートが違っても時間基準でほぼ同じ速さにできます。

renderer.render(scene, camera)が、SceneをCameraの視点から描画する命令です。物体の位置や回転を変えた後に呼びます。

Viteの開発サーバーで確認する

ターミナルで次のコマンドを実行します。

npx vite

表示されたローカルURLをブラウザで開きます。立方体が表示されて回転すれば完成です。

HTMLファイルをFinderやExplorerから直接開くのではなく、ローカルサーバー経由で確認してください。ES Modules、3Dモデル、テクスチャなどはブラウザのセキュリティ制約によりfile://で正しく読み込めない場合があります。

公開用ファイルを作るときは次を実行します。

npx vite build

通常はdistフォルダへ出力されたファイルをWebサーバーへ配置します。

npmを使わずCDNで試す方法

小さな検証なら、import mapを使ってCDNから読み込む方法もあります。<version>は利用するThree.jsのバージョンに置き換え、three本体とaddonsは同じバージョン・同じCDNへそろえてください。

<script type="importmap">
{
  "imports": {
    "three": "https://cdn.jsdelivr.net/npm/three@<version>/build/three.module.js",
    "three/addons/": "https://cdn.jsdelivr.net/npm/three@<version>/examples/jsm/"
  }
}
</script>

<script type="module" src="./main.js"></script>

CDN方式でもローカルサーバーは必要です。依存関係が増える制作では、更新管理とbuildを行いやすいnpm方式をおすすめします。

よくあるエラーと確認順

Failed to resolve module specifier “three”

import * as THREE from "three"threeをブラウザが解決できていません。npm方式ならVite経由で開いているか、CDN方式ならimport mapがscriptより前にあるかを確認します。

画面が真っ暗で何も表示されない

次の順番で確認します。

  1. ブラウザのConsoleにエラーがないか
  2. canvasの幅と高さが0になっていないか
  3. Meshをscene.add()したか
  4. CameraとMeshが同じ位置にないか
  5. renderer.render(scene, camera)を呼んでいるか
  6. Materialがライトを必要とする種類ではないか

立方体が横長または縦長になる

canvasをリサイズした後にcamera.aspectcamera.updateProjectionMatrix()を更新しているか確認します。

スマートフォンで動作が重い

描画バッファのピクセル数、物体数、影、透明表現、ポストプロセスを見直します。このサンプルではdevice pixel ratioを最大2に制限していますが、速度優先なら1へ下げる方法もあります。

使い終わったリソースを解放する

Three.jsのGeometry、Material、Textureが確保するGPU関連リソースは、オブジェクトへのJavaScript参照を外すだけでは即座に解放されません。

ページ遷移のないアプリでシーンを破棄したり作り直したりする場合は、アニメーションを止め、不要なリソースへdispose()を呼びます。

function disposeThreeScene() {
  renderer.setAnimationLoop(null);
  scene.remove(cube);
  geometry.dispose();
  material.dispose();
  renderer.dispose();
}

Textureを使っている場合はTextureも個別にdispose()します。SPAの画面切り替えや、同じページ内で3D表示を何度も生成する実装では特に重要です。

次に学ぶ内容

立方体を表示できたら、次の順番で機能を増やすと理解しやすくなります。

  1. Geometryの種類と頂点データを学ぶ
  2. Materialの種類と見た目の違いを学ぶ
  3. Lightと影を追加する
  4. OrbitControlsでマウス・タッチ操作を追加する
  5. TextureやglTF形式の3Dモデルを読み込む
  6. 必要になった段階でShaderMaterialとGLSLを学ぶ

最初から複数のライブラリや高度なシェーダーを追加せず、Scene、Camera、Renderer、Meshの関係を保ったまま一つずつ増やすのが近道です。

よくある質問

Three.jsを始める前にWebGLを学ぶ必要はありますか?

基本的な3D表示から始めるなら必須ではありません。Three.jsでScene、Camera、Geometry、Material、Meshを触り、独自シェーダーやパフォーマンス改善が必要になった時点でWebGLやGLSLへ進めます。

JavaScriptだけでThree.jsを使えますか?

使えます。Three.js本体はJavaScriptライブラリです。ただし、npm方式では依存関係管理と開発サーバーのためにNode.jsとViteなどのbuild toolを使います。

ライトを作っていないのに立方体が見えるのはなぜですか?

この例のMeshBasicMaterialはライトの影響を受けないためです。MeshStandardMaterialなど光を計算するMaterialへ変える場合は、LightもSceneへ追加します。

requestAnimationFrameではだめですか?

一般的なcanvasアニメーションでは使えますが、Three.jsのWebGLRendererは互換性を考慮してsetAnimationLoop()を使うよう案内しています。本記事では現行の推奨に合わせています。

まとめ

Three.jsの最小構成は、Scene、Camera、Rendererを作り、GeometryとMaterialから作ったMeshをSceneへ追加し、Rendererで描画する流れです。

今回のコードは、npm + Vite、ES Modules、レスポンシブなcanvas、時間基準の回転、setAnimationLoop()、リソース解放まで含んでいます。まずこの構成をそのまま動かし、Geometry、Material、Light、操作機能を一つずつ追加してください。

スポンサーリンク