【はじめてのThree.js】アップロード画像をジオメトリのテクスチャにする

Three.jsとは、手軽に3Dコンテンツを制作できる商用利用可能なJavaScriptライブラリです。

って書いてありました。なんかすごそうです。

canvasの使い方すら知らない自分でも手軽にできるのか気になったので、
ユーザーがアップロードした画像をThree.jsで3Dな感じで動かしてみようと思います。

## コード

```html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="https://unpkg.com/three/build/three.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
	const file = document.getElementsByTagName('input')[0];
	const canvas = document.getElementsByTagName('canvas');
	file.addEventListener('change', async () => {
		// キャンバス初期化
		if (canvas.length) {
			canvas[0].remove();
		}
		document.body.appendChild(document.createElement('canvas'));

		// オブジェクトURL生成
		const objectURL = URL.createObjectURL(file.files[0]);

		// 球体
		const texture = new THREE.TextureLoader();
		const geometry = new THREE.SphereGeometry();
		const material = new THREE.MeshPhongMaterial({
			color: 0xffffff,
			map: await texture.loadAsync(objectURL),
		});
		const sphere = new THREE.Mesh(geometry, material);

		// ライト
		const aLight = new THREE.AmbientLight(0xffffff, 0.6);
		const dLight = new THREE.DirectionalLight(0xffffff, 0.6);

		// シーン
		const scene = new THREE.Scene();
		scene.add(sphere);
		scene.add(aLight);
		scene.add(dLight);

		// カメラ
		const camera = new THREE.PerspectiveCamera(50, innerWidth / innerHeight);
		camera.position.set(0, 0, 5);

		// レンダラー
		const renderer = new THREE.WebGLRenderer({
			canvas: canvas[0],
			antialias: true,
		});
		renderer.setSize(innerWidth, innerHeight);
		renderer.setPixelRatio(devicePixelRatio);

		// 描画
		function animate() {
			sphere.rotation.x += 0.001;
			sphere.rotation.y += 0.05;
			renderer.render(scene, camera);
			requestAnimationFrame(animate);
		}
		animate();

		// オブジェクトURL解放
		URL.revokeObjectURL(objectURL);
	});
});
</script>
</head>
<body style="margin:0;padding:0;">
	<input type="file" accept="image/png,image/jpeg,image/gif">
</body>
</html>
```
## デモンストレーション

<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="RwMNaoy" data-user="lvn-shigaki" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/lvn-shigaki/pen/RwMNaoy">
  Untitled</a> by lvn-shigaki (<a href="https://codepen.io/lvn-shigaki">@lvn-shigaki</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

ファイルを選択すると、その画像が張り付けられた球体が表示されてぐるぐる回ります。
たとえば世界地図の画像をアップロードしたら地球が回ってる感じになります。

## 解説

head内でThree.jsをCDNで読み込んでいます。
body内はファイル選択のinput要素を配置しているのみです。
JavaScript側では、そのinput要素にaddEventListenerでchangeイベントを登録してます。

以降はイベントリスナー内の解説です。

```javascript:キャンバス初期化
if (canvas.length) {
	canvas[0].remove();
}
document.body.appendChild(document.createElement('canvas'));
```
ファイル選択時にbody内にcanvas要素を追加します。
すでにcanvas要素が存在する場合（2回目以降のファイル選択時）は、先にcanvas要素を削除してます。

```javascript:オブジェクトURL生成
const objectURL = URL.createObjectURL(file.files[0]);
```
ここでは選択した画像のobjectURLを生成してます。
のちに、このobjectURLはrevokeObjectURLで解放してあげます。

```javascript:球体
const texture = new THREE.TextureLoader();
const geometry = new THREE.SphereGeometry();
const material = new THREE.MeshPhongMaterial({
	color: 0xffffff,
	map: await texture.loadAsync(objectURL),
});
const sphere = new THREE.Mesh(geometry, material);
```
ここからThree.jsの出番。ここでやってることは以下です。
・テクスチャにobjectURLを読み込み
・ビルトインジオメトリのSphereGeometryを使用
・ビルトインマテリアルのMeshPhongMaterialを使用しテクスチャを適用
・ジオメトリとマテリアルを結合しメッシュを作成

```javascript:ライト
const aLight = new THREE.AmbientLight(0xffffff, 0.6);
const dLight = new THREE.DirectionalLight(0xffffff, 0.6);
```
光源を用意します。
AmbientLightは環境光源で、3D空間全体に均等に光を当てます。
DirectionalLightは平行光源で、立体感を出すために使用してます。

```javascript:シーン
const scene = new THREE.Scene();
scene.add(sphere);
scene.add(aLight);
scene.add(dLight);
```
3D空間であるシーンにさきほど作った球体と光源を追加してます。

```javascript:カメラ
const camera = new THREE.PerspectiveCamera(50, innerWidth / innerHeight);
camera.position.set(0, 0, 5);
```
3D空間の視点となるカメラの設定をしてます。
引数の各値は適当にいじってイイ感じにした程度のものです。
カメラのことをしっかり理解できたら、いろいろ楽しくなりそうです。

```javascript:レンダラー
const renderer = new THREE.WebGLRenderer({
	canvas: canvas[0],
	antialias: true,
});
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(devicePixelRatio);
```
WebGLのレンダリングをするためのレンダラーを作成します。
キャンバスを指定し、球体の輪郭にギザギザ感を出さないようにアンチエイリアスの設定をしてます。
で、レンダラーのサイズや解像度を指定してあげてます。

```javascript:描画
function animate() {
	sphere.rotation.x += 0.001;
	sphere.rotation.y += 0.05;
	renderer.render(scene, camera);
	requestAnimationFrame(animate);
}
animate();
```
さいごに描画処理です。
フレーム毎にanimate()が実行されます。
球体を回転させたかったのでrotationをいじりつつ、
レンダラーにシーンとカメラを渡してレンダリングしてます。

## 感想
今回いろいろ調べるなか、WebGLの難易度は非常に高いと感じました。
必要な知識が多すぎて学習コストがやばい。

Three.jsはその高い敷居を大きく下げてくれる素敵なライブラリでした。

いつか仕事で使ってみたいな。