three.js: 50行以下でテクスチャつきの箱をマウス操作によるカメラワークで自由に捉える

[three.js](http://threejs.org/)は、HTMLの3D技術[WebGL](https://ja.wikipedia.org/wiki/WebGL)を使って3次元の表現ができる無償のJavaScriptライブラリです。GPUによる本格的な3D表現をプラグインなしでつくれます。本稿は、three.jsをはじめて使う方が対象です。50行足らずのJavaScriptコードで3次元空間に立方体をつくり、テクスチャを貼ってみます。さらには、オービット/ズーム/パンといったマウス操作によるカメラワークまで仕込んでしまおうというのです。

でき上がったサンプルは[jsdo.it](http://jsdo.it/FumioNonaka/ucuf)に掲げましたので、コードを開いてお試しいただけます。マウスによるカメラ操作はつぎのとおりです。なお、オービットというのは被写体を中心にして周回軌道を描くように映すことです。

| カメラワーク | マウス操作 |
|:-----------|:---------|
| オービット | ドラッグ |
| ズーム | ホイール |
| パン | 右ボタンドラッグ |

[![1812004_003.png](https://qiita-image-store.s3.amazonaws.com/0/262045/b11cbd38-f943-3519-2ade-198f7cc4632c.png)
\>> jsdo.itへ](http://jsdo.it/FumioNonaka/ucuf)

# theree.jsライブラリを使う
ライブラリはthree.jsサイトのリンク[download]で、ZIPファイルがローカルに保存できます。[build](https://github.com/mrdoob/three.js/tree/dev/build)フォルダの中に、開発用の`three.js`と公開用の`three.min.js`が納められていますので、用途に応じていずれかをお使いください。

![1812004_004.png](https://qiita-image-store.s3.amazonaws.com/0/262045/adf15023-71d6-390d-4b47-297f949962c0.png)

[CDN](https://cdnjs.com/libraries/three.js/)も公開されています。今回はこちらを使いましょう。

```html
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/99/three.js"></script>
```


# シーンとカメラを定める
3次元空間であるシーンは、コンストラクタ[`Scene()`](https://threejs.org/docs/index.html#api/en/scenes/Scene)でつくります。引数は要りません。シーンの中には、3次元のオブジェクトや光源、それらを映しだすためのカメラが加えられます。カメラを定めるのはコンストラクタ[`PerspectiveCamera()`](https://threejs.org/docs/index.html#api/en/cameras/PerspectiveCamera)です。引数はつぎの4つを渡します。

```js
new THREE.PerspectiveCamera(視野角, 縦横比, 撮影開始距離, 撮影終了距離)
```

![FN1704010_001.png](http://www.fumiononaka.com/TechNotes/Images/FN1704010_001.png)

視野角は画角とも呼ばれ、撮影する範囲の広さのほか遠近法の強さにも影響します(「[レンズと画角のちがいを知り、表現に活かす　CG制作者のための画角講座](https://cgworld.jp/interview/201604-camera.html)」参照)。画面の縦横比は幅/高さです。そして、奥行きに対して撮影する範囲の始まりと終わりを与えます。

シーンとカメラはつぎのように定め、カメラのz軸方向の位置は[`Object3D.position`](https://threejs.org/docs/index.html#api/en/core/Object3D.position)プロパティで少し下げました(デフォルト値0)。なお、JavaScriptコードから`<body>`要素を参照することになるので、[`DOMContentLoaded`](https://developer.mozilla.org/docs/Web/Events/DOMContentLoaded)イベントのリスナーで、これら初期化の処理(`init()`)を行います。

```js
let camera;
let scene;

function init() {
	const windowWidth = window.innerWidth;
	const windowHeight = window.innerHeight;

	camera = new THREE.PerspectiveCamera(70, windowWidth / windowHeight, 1, 1000);
	camera.position.z = 400;
	scene = new THREE.Scene();
}
document.addEventListener('DOMContentLoaded', init);
```


# レンダラーで\<canvas>要素をつくってページに加える
カメラの映したシーンを描くのがレンダラーです。コンストラクタ[`WebGLRenderer()`](https://threejs.org/docs/index.html#api/renderers/WebGLRenderer)で`<canvas>`要素をつくります。引数は省きましょう。メソッド[`setPixelRatio()`](https://threejs.org/docs/index.html#api/en/renderers/WebGLRenderer.setPixelRatio)がデバイスピクセル比、[`WebGLRenderer.setSize()`](https://threejs.org/docs/index.html#api/renderers/WebGLRenderer.setSize)が幅と高さを決めます(デフォルト値300×150ピクセル)。`<canvas>`要素はページに加えなければなりません。[`WebGLRenderer.domElement`](https://threejs.org/docs/index.html#api/renderers/WebGLRenderer.domElement)プロパティで参照できるので、`<body>`要素に差し込みました。これらの処理は関数(`createRenderer()`)として定め、戻り値のレンダラーは変数に納めます(`renderer`)。

```js
let renderer;

function init() {

	renderer = createRenderer(windowWidth, windowHeight);
}
function createRenderer(width, height) {
	const renderer = new THREE.WebGLRenderer({antialias: true});
	renderer.setPixelRatio(window.devicePixelRatio);
	renderer.setSize(width, height);
	document.body.appendChild(renderer.domElement);
	return renderer;
}
```


# 立方体をつくってシーンに加える
立体は`Mesh`クラスのオブジェクトとしてつくります。メッシュには、かたちの幾何学情報となるジオメトリーと素材のマテリアルを与えなければなりません。直方体のジオメトリーをつくるのは、[`BoxBufferGeometry()`](https://threejs.org/docs/index.html#api/en/geometries/BoxBufferGeometry)メソッドです。幅と高さと奥行きを引数に渡します。[`MeshNormalMaterial()`](https://threejs.org/docs/index.html#api/materials/MeshNormalMaterial)でつくる素材は少し変わっていて、面の向きによって色が決まるのです。でき上がったジオメトリー(`geometry`)とマテリアル(`material`)のオブジェクトは、コンストラクタ[`Mesh()`](https://threejs.org/docs/index.html#api/objects/Mesh)の引数にします。こうして関数(`createCube()`)から返された立体(`mesh`)は、[`Object3D.add()`](https://threejs.org/docs/index.html#api/core/Object3D.add)メソッドでシーンに加えてください。

```js
function init() {

	const side = Math.min(windowWidth, windowHeight) / 4;

	const mesh = createCube(side, side, side);
	scene.add(mesh);

	renderer.render(scene, camera);
}

function createCube(width, height, depth) {
	const geometry = new THREE.BoxBufferGeometry(width, height, depth);
	const material = new THREE.MeshNormalMaterial();
	const cube = new THREE.Mesh(geometry, material);
	return cube;
}
```

カメラ(`camera`)で映したシーン(`scene`)の画像は、レンダラー(`renderer`)で`<canvas>`要素に描きます。そのために呼び出すのが、[`WebGLRenderer.render()`](https://threejs.org/docs/index.html#api/en/renderers/WebGLRenderer.render)メソッドです。引数にはシーンとカメラを渡します。これで、3次元空間に立方体が描かれます。もっとも、真正面向きですので、立方体か正方形かわかりません。ひとまず、ここまでのJavaScriptコードを以下にまとめました。

![1812004_001.png](https://qiita-image-store.s3.amazonaws.com/0/262045/abc464ca-063a-a438-1307-4fc14e77a6ff.png)

```js
let camera;
let scene;
let renderer;

function init() {
	const windowWidth = window.innerWidth;
	const windowHeight = window.innerHeight;
	const side = Math.min(windowWidth, windowHeight) / 4;

	camera = new THREE.PerspectiveCamera(70, windowWidth / windowHeight, 1, 1000);
	camera.position.z = 400;
	scene = new THREE.Scene();

	renderer = createRenderer(windowWidth, windowHeight);

	const mesh = createCube(side, side, side);
	scene.add(mesh);

	renderer.render(scene, camera);
}
function createRenderer(width, height) {
	const renderer = new THREE.WebGLRenderer({antialias: true});
	renderer.setPixelRatio(window.devicePixelRatio);
	renderer.setSize(width, height);
	document.body.appendChild(renderer.domElement);
	return renderer;
}
function createCube(width, height, depth) {
	const geometry = new THREE.BoxBufferGeometry(width, height, depth);
	const material = new THREE.MeshNormalMaterial();
	const cube = new THREE.Mesh(geometry, material);
	return cube;
}
document.addEventListener('DOMContentLoaded', init);
```


# OrbitControlsでカメラをマウス操作する
ここまでで30行ほど書きました。あと20行でカメラワークとテクスチャ貼りまで済ませなければなりません。そこで、カメラのコントロールについては少々卑怯な手を使います。`OrbitControls`というライブラリです。three.js標準ライブラリには含まれていません。実は、ダウンロードしたライブラリファイル群の[`examples/js/controls/`](https://github.com/mrdoob/three.js/tree/dev/examples/js/controls)にひっそりと収められているのです。このライブラリ`OrbitControls.js`を別途`<script>`要素に読み込んでおきます(以下はライブラリファイルをフォルダ`lib`に納めた場合です)。

```html
<script src="lib/OrbitControls.js"></script>
```

`OrbitControls`の使い方はとても簡単で、コンストラクタにつぎのように`Camera`オブジェクトを渡すだけです。 `OrbitControls.autoRotate`プロパティに`true`を与えると、カメラは立体を中心にゆっくりと周回します。アニメーションは再描画を繰り返さなければなりません。そこで再描画の関数(`update()`)を定め、その中からカメラ操作更新の`OrbitControls.update()`メソッドを呼び出します。そして、[`window.requestAnimationFrame()`](https://developer.mozilla.org/ja/docs/Web/API/Window/requestAnimationFrame)メソッドで描画を繰り返せばよいでしょう。

```js
let controls;

function init() {

	controls = new THREE.OrbitControls(camera);
	controls.autoRotate = true;
	// renderer.render(scene, camera);
	update();
}

function update() {
	controls.update();
	requestAnimationFrame(update);
	renderer.render(scene, camera);
}
```

冒頭でご紹介したとおり、カメラはつぎのようにマウスで操作できます。

- オービット: ドラッグ
- ズーム: ホイール
- パン: 右ボタンドラッグ

![1812004_002.png](https://qiita-image-store.s3.amazonaws.com/0/262045/b7198179-f82e-f891-822b-d006c11d5a13.png)


# 立方体にテクスチャを加える
残るはテクスチャです。three.jsライブラリの[`examples/textures/crate.gif`](https://github.com/mrdoob/three.js/blob/dev/examples/textures/crate.gif)を使わせてもらいます。

![crate.gif](https://qiita-image-store.s3.amazonaws.com/0/262045/083b5cf0-6b67-d6ef-1b3a-74c86b86b674.gif)

テクスチャを読み込むために用いるのは、[`TextureLoader`](https://threejs.org/docs/index.html#api/loaders/TextureLoader)クラスです。[`TextureLoader.load()`](https://threejs.org/docs/index.html#api/loaders/TextureLoader.load)メソッドに、テクスチャのURLを渡してロードします。テクスチャを加えるには、マテリアルのクラスは切り替えなければなりません。[`MeshBasicMaterial()`](https://threejs.org/docs/api/en/materials/MeshBasicMaterial.html)コンストラクタでつくり、引数のオブジェクトには`map`プロパティでテクスチャを与えます([`MeshBasicMaterial.map`](https://threejs.org/docs/#api/en/materials/MeshBasicMaterial.map)に定められます)。テクスチャの読み込み待ちはしなくても大丈夫です。

```js
function init() {

	const texture = new THREE.TextureLoader().load('textures/crate.gif');
	// const mesh = createCube(side, side, side);
	const mesh = createCube(side, side, side, texture);

}

function createCube(width, height, depth, texture) {

	// const material = new THREE.MeshNormalMaterial();
	const material = new THREE.MeshBasicMaterial({map: texture});

}
```

これで、立方体にテクスチャが貼られ、マウス操作でカメラワークもできるようになりました。以下にJavaScriptコードをまとめます。また、[jsdo.itのサンプル](http://jsdo.it/FumioNonaka/ucuf)もお試しください。

![1812004_003.png](https://qiita-image-store.s3.amazonaws.com/0/262045/82b29053-a933-ba6d-37ba-6b715c3a052e.png)


```js
let camera;
let scene;
let renderer;
let controls;

function init() {
	const windowWidth = window.innerWidth;
	const windowHeight = window.innerHeight;
	const side = Math.min(windowWidth, windowHeight) / 4;

	camera = new THREE.PerspectiveCamera(70, windowWidth / windowHeight, 1, 1000);
	camera.position.z = 400;
	scene = new THREE.Scene();

	renderer = createRenderer(windowWidth, windowHeight);

	const texture = new THREE.TextureLoader().load('textures/crate.gif');
	const mesh = createCube(side, side, side, texture);
	scene.add(mesh);

	controls = new THREE.OrbitControls(camera);
	controls.autoRotate = true;
	update();
}
function createRenderer(width, height) {
	const renderer = new THREE.WebGLRenderer({antialias: true});
	renderer.setPixelRatio(window.devicePixelRatio);
	renderer.setSize(width, height);
	document.body.appendChild(renderer.domElement);
	return renderer;
}
function createCube(width, height, depth, texture) {
	const geometry = new THREE.BoxBufferGeometry(width, height, depth);
	const material = new THREE.MeshBasicMaterial({map: texture});
	const cube = new THREE.Mesh(geometry, material);
	return cube;
}
function update() {
	controls.update();
	requestAnimationFrame(update);
	renderer.render(scene, camera);
}
document.addEventListener('DOMContentLoaded', init);
```