【Three.js】入門してみた🐥

# Three.jsとは

手軽にWebサイト上で3Dコンテンツの制作ができるJSライブラリです。
ライブラリなしで3D表現をしようとした場合、高度な技術が必要になりますがThree.jsライブラリを導入することで、手軽に3D表現ができるようになります🌏


## インストール方法

色んな方法がありますが、今回はViteを使用してインストールします🚀

https://ja.vitejs.dev/guide/

`yarn create vite プロジェクト名`

フレームワークの使用や言語を聞かれるので回答。
私はVanilla、JavaScriptで作成しました。

`cd プロジェクト名`

`npm i three`

`npm run dev`でローカルサーバーが立ち上がります。

main.jsを下記のように編集し、Three.jsのモジュールが正常に読み込まれていれば環境構築は終了です🛠

```js
import * as THREE from "three";
console.log(THREE);
```

![スクリーンショット 2023-06-27 10.42.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2609332/0a061e5b-0938-6c32-caf8-2e843c0c386e.png)

## 3Dグラフィックス構築に必要な機能

シーン、カメラ、レンダラーなど、3Dグラフィックスを構築するためには様々な機能が必要です。

### シーンの追加

シーン（Scene）は、3Dオブジェクトや光源などの要素を含む仮想的な空間を表します🖼️
カメラで撮影するためのステージのようなものです。

```js
import * as THREE from "three";

// シーンの追加
const scene = new THREE.Scene();
```

### カメラの追加

カメラ（Camera）は、シーンのどの部分を表示するかを定義します🎥
`PerspectiveCamera()`は4つの引数をとります。

`new THREE.PerspectiveCamera(視野角, アスペクト比, 開始距離, 終了距離)`

```js
// カメラの追加
const camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 1000);
```

### レンダラーの追加

シーンを描画して実際の画面に表示するための機能を提供します📺

```js
// レンダラーの追加
const renderer = new THREE.WebGLRenderer({
  alpha: true,
});
// サイズ調整
renderer.setSize(window.innerWidth, window.innerHeight);
// レンダリングしたいDOM要素と紐付け
document.body.appendChild(renderer.domElement);
```

### ジオメトリの追加

ジオメトリ（Geometry）とは、Three.jsで3Dオブジェクトの形状を定義するためのクラスです。
Three.jsには、さまざまな形状を作成するための多くのジオメトリクラスがあります。
その一つである`SphereGeometry()`は球体の形状を定義し、3次元空間に球を作成するための関数です🌏
[Three.js SphereGeometry公式ドキュメント](https://threejs.org/docs/index.html?q=SphereGeometry#api/en/geometries/SphereGeometry)で7つのパラメーターとれることが確認できます。
下記コードは、radius、widthSegments、heightSegmentsを設定しています。

```js
// ジオメトリ作成
const ballGeometry = new THREE.SphereGeometry(100, 64, 32);
```

### マテリアルの追加

マテリアル（Material）とは、Three.jsの3Dオブジェクトが画面にレンダリングされるときの外観や質感を決定するものです。
こちらも色々なメソッドが用意されていますが、その一つである`MeshPhysicalMateria()`は物理ベースのレンダリング（PBR）をサポートするマテリアルで、粗さや金属製など、現実の物質のような設定ができます。
パラメーターでは色や、mapというテクスチャの指定ができ、画像をジオメトリに貼り付けたりできます。

```js
// マテリアル作成
const ballMaterial = new THREE.MeshPhysicalMaterial();
```

### メッシュの追加

メッシュ（Mesh）とは、ジオメトリ（形状）とマテリアル（材質）の組み合わせからなります。
これにより、形状と外観が定義された3Dオブジェクトが作成されます。
`Mesh()`を用いることでメッシュの追加ができます。
`Mesh()`はパラメーターとして、ジオメトリとマテリアルをとります。

```js
// メッシュ化
const ballMesh = new THREE.Mesh(ballGeometry, ballMaterial);
```

### シーンに追加

メッシュ化したものをシーンに追加します。

```js
scene.add(ballMesh);
```

### レンダリング関数実行

そして最後に、「レンダラーの追加」で追加したレンダリング関数を実行し、ようやく画面にレンダリングされます🌸
引数には設定したシーンと、カメラを渡します。

```js
// レンダリング関数
renderer.render(scene, camera);
```

...と思いきやブラウザを確認すると何も映ってないです。
どうやらカメラ位置の設定が必要なようで、現状カメラは原点位置になっていて球が見えない状態になっています。
下記コードを`PerspectiveCamera`インスタンスの下部分に追加して、カメラ位置を調整します。
引数は、x軸、y軸、z軸になっており、ここではz軸方向に+500させます。

`camera.position.set(0, 0, 500);`

このようにカメラを球から適切な距離に移動させると見えるようになりました！

![スクリーンショット 2023-06-27 16.50.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2609332/f277e1b4-a329-784a-c9bd-b6c83cfcd177.png)

`MeshPhysicalMaterial()`で色やテクスチャを設定していないので真っ黒な球体のままです。


#### ここまでのコード

```js
import * as THREE from "three";

// シーンの追加
const scene = new THREE.Scene();

// カメラの追加
const camera = new THREE.PerspectiveCamera(50, window.innerWidth / innerHeight, 0.1, 1000);
camera.position.set(0, 0, 500);

// レンダラーの追加
const renderer = new THREE.WebGLRenderer({
  alpha: true,
});
// サイズ調整
renderer.setSize(window.innerWidth, window.innerHeight);
// レンダリングしたいDOM要素と紐付け
document.body.appendChild(renderer.domElement);

// ジオメトリ作成
const ballGeometry = new THREE.SphereGeometry(100, 64, 32);

// マテリアル作成
const ballMaterial = new THREE.MeshPhysicalMaterial();

// メッシュ化
const ballMesh = new THREE.Mesh(ballGeometry, ballMaterial);
scene.add(ballMesh);

// レンダリング関数
renderer.render(scene, camera);

```

cssで調整や背景画像などを設置するといい感じになります。

```css
body {
  margin: 0;
  width: 100%;
  height: 100%;
  background: url("./images/space.jpg") no-repeat center center / cover;
}
```

![スクリーンショット 2023-06-27 16.53.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2609332/35fa6431-dce5-4b5f-4cc6-6739244c57f5.png)

### 平行光源の追加

`DirectionalLight()`はThree.jsで提供されているライトの一種で、無限遠光源（平行光源）を模倣します。
太陽の光など、非常に遠くの光源からの光を表現するのに適しています。パラメーターは16新数(0x部分)でカラーと、光の強さを渡せます。

:::note warn
以降のコードは全て下記レンダリング関数より前に記述します。
```js
// レンダリング関数
renderer.render(scene, camera);
```
:::

```js
// 平行光源の追加
const directionalLight = new THREE.DirectionalLight(0xffffff, 2);
// 光の当たる位置調整
directionalLight.position.set(1, 1, 1); 
// シーンに追加
scene.add(directionalLight);
```

下記のように、x軸1, y軸1, z軸1の位置に0xffffff, 2の光が表現できているかと思います。
![スクリーンショット 2023-06-27 17.16.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2609332/34cff096-fb86-264d-fd27-bc3ed347038a.png)

### ポイント光源を追加

`PointLight()`もThree.jsで提供されているライトの一種で特定の点から全方向へ等しく光を放つ光源を模倣します。
これは、実際の物理的な光源、例えば電球や蝋燭のように振る舞います🔦
とれるパラメーターは`DirectionalLight()`と同じです。
下記コードはポイント光源の追加と位置調整、シーンの追加をしています。

```js
// ポイント光源を追加
const pointLight = new THREE.PointLight(0xffffff, 1);
pointLight.position.set(-200, -200, -200);
scene.add(pointLight);
```

![スクリーンショット 2023-06-27 17.36.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2609332/b0222077-724d-e189-4b2d-8a89fe168b48.png)

:::note

光源位置の特定にはそれぞれ用意されているHelper関数を使用することで特定可能です
```js
// ポイント光源特定の場合
const lightHelper = new THREE.PointLightHelper(pointLight, 30);
scene.add(lightHelper);
```
![スクリーンショット 2023-06-27 17.40.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2609332/8b5ed22b-b653-3b2d-4a14-a50b60d5b7e4.png)
:::

### ポイント光源を動かすアニメーション

`pointLight.position.set()`で光源位置を指定しましたが、この光源位置を動的に変更し、フレーム毎にレンダリングすることによって光源が移動しているアニメーションを再現できます。

```js
// ポイント光源のアニメーション
function animate() {
  // ポジションを動的に指定
  pointLight.position.set(200 * Math.sin(Date.now() / 500), 200 * Math.sin(Date.now() / 1000), 200 * Math.cos(Date.now() / 500));
  // フレーム単位で関数実行
  requestAnimationFrame(animate);

  // この関数内でレンダリング関数実行
  renderer.render(scene, camera);
}
animate(); // アニメーション関数の実行
```

![タイトルなし.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2609332/7c64f95d-7993-8568-8a8b-ebf792168d83.gif)

### マウスで動かす

`OrbitControls()`はユーザーインターフェースコントロールの一つで、ユーザーはマウスやタッチジェスチャーを使用してカメラを回転させ、ズームイン/アウト、パン（水平または垂直に移動）を行うことができます。
引数にはカメラとDOMエレメントをとります。

viteで環境構築を行いましたが`OrbitControls()`はThree.jsのコアライブラリには含まれていませんので、別途インポートする必要があります。

`import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";`

```js
// マウス操作
const Controls = new OrbitControls(camera, renderer.domElement);
```
![タイトルなし.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2609332/3a40e2fa-4add-1b2a-b564-84fa823339a4.gif)

### テクスチャの追加

画像をダウンロードし、マテリアルにてテクスチャ設定をしてみます。
`TextureLoader()`を使用します。

```js
// テクスチャの追加
const texture = new THREE.TextureLoader().load("./textures/earth.jpg");
```

マテリアルの追加の章で説明した通り、`MeshPhysicalMateria()`はパラメーターでテクスチャの設定ができますので
この`TextureLoader()`の返却値が格納された`texture`をmapプロパティのvalueとして渡します。

> パラメーターでは色や、mapというテクスチャの指定ができ、画像をジオメトリに貼り付けたりできます。

```js
// マテリアル作成
const ballMaterial = new THREE.MeshPhysicalMaterial({ map: texture });
```

![スクリーンショット 2023-06-27 19.04.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2609332/198bd429-5c4f-13b7-0fbf-46a72c686751.png)

## 完成！

コードのリファクタリングや、リサイズ対応などを少しして完成です！

:::note

現状、球体の境目がギザギザしていて画質が荒いですが下記のように`setPixelRatio()`を使用すると改善されます。

```js
// 画質調整
renderer.setPixelRatio(window.devicePixelRatio);
```

:::

### 最終的なコード

```js
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";

window.addEventListener("load", init);

function init() {
  // シーンの追加
  const scene = new THREE.Scene();

  // カメラの追加
  const camera = new THREE.PerspectiveCamera(50, window.innerWidth / innerHeight, 0.1, 1000);
  camera.position.set(0, 0, 500);

  // レンダラーの追加
  const renderer = new THREE.WebGLRenderer({
    alpha: true,
  });
  // サイズ調整
  renderer.setSize(window.innerWidth, window.innerHeight);
  // 画質調整
  renderer.setPixelRatio(window.devicePixelRatio);
  // レンダリングしたいDOM要素と紐付け
  document.body.appendChild(renderer.domElement);

  // テクスチャの追加
  const texture = new THREE.TextureLoader().load("./textures/earth.jpg");

  // ジオメトリ作成
  const ballGeometry = new THREE.SphereGeometry(100, 64, 32);

  // マテリアル作成
  const ballMaterial = new THREE.MeshPhysicalMaterial({ map: texture });

  // メッシュ化
  const ballMesh = new THREE.Mesh(ballGeometry, ballMaterial);
  scene.add(ballMesh);

  // 平行光源の追加
  const directionalLight = new THREE.DirectionalLight(0xffffff, 2);
  // 光の当たる位置調整
  directionalLight.position.set(1, 1, 1);
  // シーンに追加
  scene.add(directionalLight);

  // ポイント光源を追加
  const pointLight = new THREE.PointLight(0xffffff, 1);
  pointLight.position.set(-200, -200, -200);
  scene.add(pointLight);

  // マウス操作
  const Controls = new OrbitControls(camera, renderer.domElement);

  // ポイント光源のアニメーション
  function animate() {
    // ポジションを動的に指定
    pointLight.position.set(
      200 * Math.sin(Date.now() / 500),
      200 * Math.sin(Date.now() / 1000),
      200 * Math.cos(Date.now() / 500)
    );
    // フレーム単位で関数実行
    requestAnimationFrame(animate);

    // レンダリング関数
    renderer.render(scene, camera);
  }

  // ブラウザリサイズ対応
  function onWindowResize() {
    renderer.setSize(window.innerWidth, window.innerHeight);
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
  }

  window.addEventListener("resize", onWindowResize);
  animate();
}

```

簡単に触れてみましたがマテリアルやシーン、ジオメトリなど、様々な機能の上で初めて、3Dオブジェクトがレンダリングされることがわかりました！



参考

https://www.udemy.com/course/threejs-beginner/