three.js + TypeScript: 3次元空間で立方体を回してみる

[three.js](https://threejs.org/)は、WebGLを使って3次元表現ができるライブラリです。素のWebGLでは難しくなってしまうコードが、すっきりと短くわかりやすく書けます。本稿は公式サイト「[Creating a scene](https://threejs.org/docs/index.html#manual/ja/introduction/Creating-a-scene)」(シーンの作成)の作例をもとにしたチュートリアル解説です。アプリケーションはモジュールに分け、TypeScriptも採り入れました。もっとも、TypeScriptについては型推論が働くおかげで、明示的な型づけはほとんどしていません。本稿で書くコード例のでき上がりはCodeSandboxに公開しました。

#### 本稿作例のでき上がり
https://codesandbox.io/s/three-js-typescript-simple-example-04-l1yt1v?file=/src/index.ts


# 環境をつくる
作業はすぐに始められて、簡単に試せるように[CodeSandbox](https://codesandbox.io/)で進めることにします。テンプレートはVanilla TypeScriptを用いました。依存(Dependencies)には、`three`と`@types/three`(TypeScript用)を加えてください。

ローカル環境で作業したい場合には、「[Three.js を TypeScript で使う方法 ～ 初めての Three.js 入門編 ～](https://std9.jp/articles/01g1vzvvrragrgbbk4x8s3564e/)」を参考にされるとよいでしょう。[Vite](https://ja.vitejs.dev/guide/)ビルドシステムを使っており、ファイルの構成や名前が少し異なるので、そこは読み替えてください。ご参考までに、本稿末尾にGitHubリポジトリの公開リンクを添えました。

まず、メインのHTMLドキュメント`index.html`は、つぎのように書き替えます。

#### コード001■メインのHTMLドキュメント
```html:index.html
<html>
	<head>
		<title>Three.js + TypeScript: Simple example</title>
		<meta charset="UTF-8" />
	</head>

	<body>
		<canvas id="canvas"></canvas>
		<script src="src/index.ts"></script>
	</body>
</html>
```


# 3次元空間をつくる
three.jsでまずつくるのは、つぎの3つのオブジェクトです。

* シーン(scene)
    - 3次元空間を表し、3Dオブジェクトやライトが加えられる。
* カメラ(camera)
    - 3次元空間を映し、レンダラーに送って画面に投影する。
* レンダラー(renderer)
    - カメラから受け取った画像を、画面の再描画のたびに更新する。


シーンのコンストラクタは[`Scene()`](https://threejs.org/docs/#api/en/scenes/Scene)です。カメラにはいくつか種類があります。今回使うのは、標準的な[`PerspectiveCamera`](https://threejs.org/docs/?q=Camera#api/en/cameras/PerspectiveCamera)です。Perspectiveは[透視投影](https://ja.wikipedia.org/wiki/%E9%80%8F%E8%A6%96%E6%8A%95%E5%BD%B1)(perspective projection)を意味し、対象物を人が見たのと近い表現になります。4つの引数については、このあと解説しましょう(構文001)。WebGLによる描画を担うのが[`WebGLRenderer`](https://threejs.org/docs/?q=WebGLRenderer#api/en/renderers/WebGLRenderer)です。引数オブジェクトの`canvas`プロパティには、[`<canvas>`](https://developer.mozilla.org/docs/Web/HTML/Element/canvas)要素の参照を渡してください。

#### 構文001■PerspectiveCamera()コンストラクタ

```
PerspectiveCamera(fov: Number, aspect: Number, near: Number, far: Number)
```

* `fov`: 視野(field of view)またはカメラの用語で[画角](https://ja.wikipedia.org/wiki/%E7%94%BB%E8%A7%92)。写される光景の範囲を角度(度数)で示す。数字が大きいと広角で、写る範囲は広くなるかわり、対象物は相対的に小さくなる。小さい角度は望遠に近づき、対象物が相対的に大きく写る。
* `aspect`: [アスペクト比](https://ja.wikipedia.org/wiki/%E3%82%A2%E3%82%B9%E3%83%9A%E3%82%AF%E3%83%88%E6%AF%94)で、描画する矩形領域の幅/高さ。矩形領域の比と合わなければ、画像の水平・垂直比が歪む。
* `near`: オブジェクトを描画するもっとも近い距離。これより近くのオブジェクトは画面に描かれない。
* `far`: オブジェクトを描画するもっとも遠い距離。これより遠くのオブジェクトは画面に描かれない。

距離や長さは、1を基本とした数値で表します。カメラの設定でオブジェクトは大きくも小さくも描かれるので、単位は気にせず比率と捉えて構いません。

メインのTypeScriptモジュール`src:index.ts`に、シーンとカメラおよびレンダラーをつぎのように定めましょう。これで、3次元空間のいわば舞台ができ上がりました。もっとも、映し出すオブジェクトが何もありませんので、ただの暗闇です。なお、`src/styles.css`には最低限のスタイルを書き加えました。あとに掲げるサンプル001のCodeSandbox作例でお確かめください。

```typescript:src:index.ts
import * as THREE from 'three';
import './styles.css';

function init() {
	const canvas = document.getElementById('canvas') as HTMLCanvasElement;
	const size = { width: window.innerWidth, height: window.innerHeight };
	const scene = new THREE.Scene();
	const camera = new THREE.PerspectiveCamera(
		45,
		size.width / size.height,
		0.1,
		1000
	);
	const renderer = new THREE.WebGLRenderer({ canvas });
	renderer.setSize(size.width, size.height);
}
init();
```


# 3次元空間に立方体を加える
では、3次元空間に3Dオブジェクトをつくって加えましょう。3Dオブジェクトには形状の定めとなるジオメトリと、表面素材を決めるマテリアルがなければなりません。そして、ジオメトリにマテリアルを適用して3Dオブジェクトにつくり上げるのがメッシュです。

立方体は[`BoxGeometry`](https://threejs.org/docs/?q=BoxGeometry#api/en/geometries/BoxGeometry)でつくります。3つの引数は、幅と高さ、そして奥行きです。マテリアルは、とりあえず扱いやすい[`MeshBasicMaterial`](https://threejs.org/docs/?q=MeshBasicMaterial#api/en/materials/MeshBasicMaterial)を用いましょう。引数オブジェクトの`color`プロパティには、CSSと同じかたちでカラー値を与えます。16進数の場合は、数値でも構いません。このふたつを[`Mesh`](https://threejs.org/docs/?q=Mesh#api/en/objects/Mesh)に引数として渡せば、3Dオブジェクトが返されます。シーンには`add()`メソッドで加えてください。そして、カメラの位置を定めるのは`position`プロパティです。`z`座標は手前が正になります。レンダラーの`render()`メソッドで描画するのを忘れてはいけません。引数はシーン(`scene`)とカメラ(`camera`)です。


```typescript:src:index.ts
function init() {

	const geometry = new THREE.BoxGeometry(1, 1, 1);
	const material = new THREE.MeshBasicMaterial({ color: 'aqua' });
	const cube = new THREE.Mesh(geometry, material);
	scene.add(cube);
	camera.position.z = 5;
	renderer.render(scene, camera);
}
```

これで、3次元空間の原点`(0, 0, 0)`を中心に(デフォルト)立方体が置かれたはずです。もっとも、カメラに正面向きなので、立方体か正方形平面か区別がつきません。立方体(`cube`)の`rotation`プロパティを使って、x軸とy軸で少し回転しましょう(角度の単位はラジアンです)。結果は、CodeSandboxに公開した以下のサンプル001でお確かめください。

```typescript:src:index.ts
function init() {

	cube.rotation.x += 0.5;
	cube.rotation.y += 0.5;
	renderer.render(scene, camera);
}
```

#### サンプル001■Three.js + TypeScript: Simple example 01
https://codesandbox.io/s/three-js-typescript-simple-example-01-cq1vwt?file=/src/index.ts


# 立方体に回転のアニメーションを加える
[`Window.requestAnimationFrame()`](https://developer.mozilla.org/docs/Web/API/Window/requestAnimationFrame)メソッドで、立方体に回転のアニメーションを加えましょう。描画ごとの立方体の回転角は少なめにします。`requestAnimationFrame()`の呼び出しは、再描画のたび繰り返すことにお気をつけください。モジュール`src:index.ts`の記述全体を、いったんコード002にまとめました。動きは、以下のサンプル002のCodeSandbox作例でご確認いただけます。

```typescript:src:index.ts
function init() {

	const animate = () => {
		requestAnimationFrame(animate);
		// cube.rotation.x += 0.5;
		cube.rotation.x += 0.01;
		// cube.rotation.y += 0.5;
		cube.rotation.y += 0.01;
		renderer.render(scene, camera);
	};
	animate();
}
```

#### コード002■立方体を水平・垂直軸で回転する
```typescript:src:index.ts
import * as THREE from 'three';
import './styles.css';

function init() {
	const canvas = document.getElementById('canvas') as HTMLCanvasElement;
	const size = { width: window.innerWidth, height: window.innerHeight };
	const scene = new THREE.Scene();
	const camera = new THREE.PerspectiveCamera(
		45,
		size.width / size.height,
		0.1,
		1000
	);
	const renderer = new THREE.WebGLRenderer({ canvas });
	renderer.setSize(size.width, size.height);
	const geometry = new THREE.BoxGeometry(1, 1, 1);
	const material = new THREE.MeshBasicMaterial({ color: 'aqua' });
	const cube = new THREE.Mesh(geometry, material);
	scene.add(cube);
	camera.position.z = 5;
	const animate = () => {
		requestAnimationFrame(animate);
		cube.rotation.x += 0.01;
		cube.rotation.y += 0.01;
		renderer.render(scene, camera);
	};
	animate();
}
init();
```

#### サンプル002■three.js + TypeScript: Simple example 02
https://codesandbox.io/s/three-js-typescript-simple-example-02-hbb7k6?file=/src/index.ts


# 立方体の面に陰影を与える
立方体の面の表現に、気になるところがあるかもしれません。面に陰影がないことです。これは、用いたマテリアル`MeshBasicMaterial`に原因があります。3次元空間は何もしなければ本来暗闇です。けれど、`MeshBasicMaterial`に与えたカラーは、光がなくても見える、いわば発光する性質をもっています。カラーを光として放っているのですから、陰影のつきようがありません。

そこで、マテリアルを[`MeshPhongMaterial`](https://threejs.org/docs/?q=MeshPhongMaterial#api/en/materials/MeshPhongMaterial)に差し替えます。表面は鏡面反射する光沢があるものの、カラーは発光しません。引数オブジェクトの`color`プロパティの定め方は同じです。これで立方体は闇に閉ざされます。

```typescript:src:index.ts
function init() {

	// const material = new THREE.MeshBasicMaterial({ color: 'aqua' });
	const material = new THREE.MeshPhongMaterial({ color: 'aqua' });

}
```

光源として加えるのは、まず[`DirectionalLight`](https://threejs.org/docs/#api/en/lights/DirectionalLight)です。太陽光のようにひとつの方向から、光が平行に差し込みます(「[平行光源](https://wgld.org/d/webgl/w021.html)」)。引数には与えるのはカラーです。`white`(`0xffffff`)では陰影のコントラストが少し強いので、明るさを抑えました。光が差してくる方向を定めるのはライト(`directionalLight`)の`position.set()`メソッドです。原点に向かって差し込みます。引数値`(1, 1, 1)`は、原点`(0, 0, 0)`と立方体の初期位置の前面右上の座標`(0.5, 0.5, 0.5)`を結んだ直線の方向です(右斜め上手前)。光源(`directionalLight`)は、シーン(`scene`)に`add()`メソッドで加えてください。

```typescript:src:index.ts
function init() {

	const directionalLight = new THREE.DirectionalLight('gray');
	directionalLight.position.set(1, 1, 1);
	scene.add(directionalLight);

}
```

なお、光源の明るさは第2引数で、1を100%とした比率で調整することもできます。

```typescript
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.9);
```

ただ、このままですと、光の当たらない面が黒く潰れます。そこで、3次元空間内を等しく照らすために使うのが[`AmbientLight`](https://threejs.org/docs/?q=AmbientLight#api/en/lights/AmbientLight)(環境光)です。こちらも、`white`(`0xffffff`)では陰影が飛んでしまうので、明るさを抑えました。以下のサンプル003のCodeSandbox作例で、立方体の陰影が確かめられます。

```typescript:src:index.ts
function init() {

	const ambientLight = new THREE.AmbientLight('gray');
	scene.add(ambientLight);

}
```

#### サンプル003■three.js + TypeScript: Simple example 03
https://codesandbox.io/s/three-js-typescript-simple-example-03-p5g8ju?file=/src/index.ts


# OrbitControlsで簡単にカメラを制御する
[`OrbitControls`](https://threejs.org/docs/#examples/en/controls/OrbitControls)は、マウス操作で簡単にカメラを制御するためのクラスです。一定の座標を中心にして、マウスドラッグでカメラを周回させられます。

CodeSandboxの依存(Dependencies)には、`three-orbitcontrols-ts`を加えてください。`OrbitControls()`コンストラクタの引数は、カメラ(`camera`)とWebGLが描くDOM要素です。レンダラーのインスタンス(`renderer`)作成時に与えたDOM要素は、`domElement`プロパティで取り出せます。コントロール(`controls`)の`target.set()`メソッドの引数が、カメラを動かす中心座標です。コントロールは`update()`メソッドで起動してください。立方体に向けカメラを、マウスドラッグで周回できるようになったでしょう。

```typescript:src:index.ts
import { OrbitControls } from 'three-orbitcontrols-ts';

function init() {

	const controls = new OrbitControls(camera, renderer.domElement);
	controls.target.set(0, 0, 0);
	controls.update();

}
```

なお、ローカルに開発環境を構築した場合、`OrbitControls`はつぎのパスから`import`します。

```typescript:src:index.ts
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
```

実は、`OrbitControls`でできるカメラのマウス操作は、あとふたつあります。

* 右クリックでドラッグ: カメラのパン。カメラは固定したまま、フレーミングを水平・垂直方向に移動させる。
* マウスホイール: カメラのズーミング。カメラを向けた方向に、映像を拡大・縮小する。

残念ながらCodeSandbox(サンプル004)では、つぎのような警告が出て操作できません。

> WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.

> WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled. 

#### サンプル004■three.js + TypeScript: Simple example 04
https://codesandbox.io/s/three-js-typescript-simple-example-04-l1yt1v?file=/src/index.ts

ローカルの開発環境であれば、上記ふたつの操作も可能です。Viteで構築したGitHubのリポジトリをご参考までに公開します。冒頭に述べたとおり、ファイルの構成や名前が少し異なるので、その点はお気をつけください。

#### GitHub: `FumioNonaka/three-typescript`
https://github.com/FumioNonaka/three-typescript