[Three.js]Web上でMinecraftのスキンを表示してみる

# はじめに
日曜工作でWeb上に3Dモデルを取り込んだりしたときの覚え書きです。
ふと「マイクラのスキンとか使えたらいいな～」と思ったので、作ってみました。

# 完成品
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2883146/f431bdfd-67fa-aa30-55c0-94c83e99d0b3.png)

# 環境
OSはWindows11、プロジェクトはNode.js + vite + Three.jsをつかって構築してます。
Blockbenchでモデル作成、VSCodeでコーディングしてます。

# やりかた
## スキンをglTF形式に変換する
まずはWeb上でスキンを扱うために、**glTF形式**というものに変換します。

聞きなれない形式ですが、**glTF（GL Transmission Format）** とは3Dモデルのファイル形式のひとつで、主にWebブラウザ上で動作するコンテンツを作る際に用いられるフォーマットらしいです。
一度OBJファイルで試してみたんですが、手順的にこちらのほうがより簡単で扱いやすかったので採用しました。

---

今回は**Blockbench**というツールを使って変換します。
早い話がMinecraft用のデザイン・モデリングツールですね。凝ったテクスチャなんかはこれを使って書くことが多いです。

https://web.blockbench.net/

開いたら「Minecraft Skin」を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2883146/8d7fee22-ce21-3990-0bc8-b42efd5bf43f.png)

「新規モデル」を選択。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2883146/8d6f1633-3c55-6677-7d7e-880ceedabac5.png)

そしたらこのような画面が出てきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2883146/ca356676-2bfc-2e85-a921-f91ae9ea1c31.png)

各選択肢については以下の通り。
<dl>
<dt>Model</dt>
<dd>テクスチャ（スキン）の形式です。今回は上2つのうちどれかになります

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2883146/a213051a-d9ce-c1cb-bc0c-b75d556e1b23.png)

実はマイクラスキンには二種類あり、肩幅4マスの**Wide**と、肩幅3マスの**Slim**があります。
ここはスキンによって違うので、スキンに合ったフォーマットを選択してください

参考までに、デフォルトスキンであるSteveはWide、AlexはSlimです

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2883146/a83aacfa-8d9b-e26c-cb04-82fc0aacb972.png)
</dd>

<dt>ピクセル</dt>
<dd>解像度です。今回はデフォルトを選択。</dd>

<dt>テクスチャ</dt>
<dd>ここに今回使用するスキンの画像ファイルを選択します。</dd>

<dt>ポーズ</dt>
<dd>スキンにポーズをつけたい場合はチェック。</dd>

<dt>レイヤーテクスチャ</dt>
<dd>これはよくわかりません。なくても大丈夫かも。</dd>
</dl>

入力したら「決定」をクリック。
そしたらこんな感じで、オーバーレイが剥げた状態のスキンがロードされます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2883146/9d0b6d9e-f1d7-cb8d-8c44-1fa96b81b33b.png)

このままだとかわいくないので、右下から非表示になっているレイヤーを表示させます。
右下の左から二番目のアイコンをクリックすると、非表示になっているレイヤーを表示させます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2883146/f45d5677-d4db-39d5-77e2-05fda78a1ca7.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2883146/d7955b38-58c3-74ad-f622-4894a6eaa696.png)

できました。かわいい。
そしたら、これをgiTF形式でエクスポートしていきます。

右上の「File」から「書き出し」、「glTF Model」を選択。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2883146/02297b9f-e76c-d0df-e981-4e0e7aa75989.png)

そしたらこんな感じのポップアップが出ます。
特に変える部分はないので「決定」。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2883146/6b8dee72-0bc5-7130-b6d6-ca0f6e274616.png)

そしたらglTFファイルがダウンロードされます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2883146/b8b55566-b431-c7a4-3d02-71a783cc4f7e.png)

これは後で使うので、ひとまず安置。

## プロジェクトを初期化する
まずは適当な場所にプロジェクトフォルダを作りましょう。名前は何でもいいです
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2883146/50a805cf-495c-2258-267b-014678ea2d0c.png)

そしたらプロジェクトフォルダをターミナルで開きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2883146/58e4cb76-ce92-f81e-a0a3-778054d42a5e.png)

これで準備OK。

また、ここからはNode.jsでライブラリの管理をしていきます。
環境構築がまだの場合は先に済ませておきましょう↓

https://prog-8.com/docs/nodejs-env-win

### package.jsonを作る
プロジェクトフォルダで`npm init`します。
いろいろ聞かれますが、特に変える部分もないので全部EnterでOK。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2883146/ced902c7-db21-7f3e-8a9c-284cd97ccf1a.png)

プロジェクトフォルダに`package.json`ができてたらOK。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2883146/e8362214-8131-7195-0d27-59ec74d8905d.png)

### vite, Three.jsを導入
必要なライブラリをそろえていきます。

```console
$ npm i -g npm
$ npm i vite
$ npm i three
```

最後に`npm list`してライブラリが表示されればOK。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2883146/9fe5853c-bcb0-5018-a9f6-b305f012459b.png)


## Three.jsの準備
そしたらコーディングしていきます。
コンソールから`code .`を叩いてVSCodeを起動。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2883146/cac5322e-8a40-7623-6883-7ffb2804f239.png)

`index.html`と`script/index.js`を作ります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2883146/c7d25215-3538-4914-29d4-4d29881050a7.png)

`index.html`はこんな感じ。
```index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>test</title>
</head>
<body>
  <!-- index.jsの呼び出し -->
  <script type="module" src="script/index.js"></script>
</body>
</html>
```

そしたら`index.js`のほうをいじっていきます。
`Three.js`をインポートして...

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

シーン、カメラ、レンダラーを追加して...
```js
// シーンを作成
const scene = new THREE.Scene();

// カメラを作成
const camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  1000
);
camera.position.z = 1;  // ちょっとずらしておく（重なり回避）

// レンダラーを作成
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0xbaf9ff, 1);
document.body.appendChild(renderer.domElement);
```

陰影をいい感じにしてくれる自然光を追加して...
```js
// 自然光を追加
const light = new THREE.AmbientLight(0xffffff, 1);
scene.add(light);
```

最後に、再帰的な描画関数を作成してレンダリングさせます
```js
// オブジェクトを描画
function animate() {
  requestAnimationFrame(animate);
  renderer.render(scene, camera);
}
animate();
```

これでひとまず基本的な`Three.js`の準備はOK。
```index.js
import * as THREE from 'three';


// シーンを作成
const scene = new THREE.Scene();

// カメラを作成
const camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  1000
);
camera.position.z = 1;

// レンダラーを作成
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0xbaf9ff, 1);
document.body.appendChild(renderer.domElement);

// 自然光を追加
const light = new THREE.AmbientLight(0xffffff, 1);
scene.add(light);

// オブジェクトを描画
function animate() {
  requestAnimationFrame(animate);
  renderer.render(scene, camera);
}
animate();
```

このままだといまいちよくわからないので、試しに立方体でも浮かせてみましょう。
描画関数の前に以下のコードを追加します。

```js
// 立方体を作成
const geometry = new THREE.BoxGeometry(0.2, 0.2, 0.2);
const material = new THREE.MeshNormalMaterial();
const cube = new THREE.Mesh(geometry, material);
cube.rotation.x = Math.PI / 4;
cube.rotation.y = Math.PI / 4;
scene.add(cube);
```

全体像はこんな感じ。
```index.js
import * as THREE from 'three';


// シーンを作成
const scene = new THREE.Scene();

// カメラを作成
const camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  1000
);
camera.position.z = 1;

// レンダラーを作成
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0xbaf9ff, 1);
document.body.appendChild(renderer.domElement);

// 自然光を追加
const light = new THREE.AmbientLight(0xffffff, 1);
scene.add(light);

// 立方体を作成
const geometry = new THREE.BoxGeometry(0.2, 0.2, 0.2);
const material = new THREE.MeshNormalMaterial();
const cube = new THREE.Mesh(geometry, material);
cube.rotation.x = Math.PI / 4;
cube.rotation.y = Math.PI / 4;
scene.add(cube);

// オブジェクトを描画
function animate() {
  requestAnimationFrame(animate);
  renderer.render(scene, camera);
}
animate();
```

そしたらデバッグしてみます。
コンソールを開いて`npx vite`します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2883146/31080537-fef5-35f2-a8d5-6c2bd52d234c.png)

そしたら「localhost:5173でWebページ開いたやで！」という感じのメッセージが来るので、実際にWebブラウザで見てみると...

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2883146/6986c27c-853e-711e-5a40-3caaa2c07fe3.png)

おー。いい感じに表示されてますね。

## スキンを描画する
まずはプロジェクトフォルダにglTFファイルを持ってきます。
今回は`assets`フォルダの中にでも入れておきましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2883146/11fbdb6c-9449-ad17-2918-1b7664f9d162.png)

そしたら表示していきます。

Three.jsでglTF形式のファイルを扱う場合は`GLTFLoader`を使うっぽいです。

https://threejs.org/docs/#examples/en/loaders/GLTFLoader

まずはインポート。
```js
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
```

使うときはインスタンスを生成します。
```js
const loader = new GLTFLoader();
```

で、読みこむときは`loader.load()`を使うっぽいです。
ドキュメントには以下の通り。
```ts
.load ( url : String, onLoad : Function, onProgress : Function, onError : Function ) : undefined
```

ざっくり説明すると、
| 引数名 | 説明 |
|:--|:--|
| url | glTFファイルの場所を示すurl。 |
| onLoad | ファイルの読み込みが終わった後に呼び出す関数。|
| onProgress（任意） | ファイル読み込み中に呼び出される関数。 |
| onError（任意） | エラーが発生したときに呼び出される関数。 |

って感じです。順番がややこいので注意。

という感じで、以下のように実装してみました。
```js
loader.load('../assets/model.gltf', 
  function (gltf) {
    gltf.scene.scale.set(0.3, 0.3, 0.3);
    gltf.scene.rotation.y = 150 * (MATH.PI / 180);
    gltf.scene.position.y -= 0.5;
    scene.add(gltf.scene);
});
```

これを`index.js`に組み込んで、全文はこんな感じ。
```index.js
import * as THREE from 'three';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';

// シーンを作成
const scene = new THREE.Scene();

// カメラを作成
const camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  1000
);
camera.position.z = 1;

// レンダラーを作成
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0xbaf9ff, 1);
document.body.appendChild(renderer.domElement);

// 自然光を追加
const light = new THREE.AmbientLight(0xffffff, 1);
scene.add(light);

// スキンを描画
const loader = new GLTFLoader();
loader.load('../assets/model.gltf',
  function (gltf) {
    gltf.scene.scale.set(0.3, 0.3, 0.3);
    gltf.scene.rotation.y = 150 * (Math.PI / 180);
    gltf.scene.position.y -= 0.5;
    scene.add(gltf.scene);
  });

// オブジェクトを描画
function animate() {
  requestAnimationFrame(animate);
  renderer.render(scene, camera);
}
animate();
```

最後にプレビューを更新してみると...
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2883146/97a403c6-220b-cd18-1c91-ed7436f6316e.png)

お～！
いい感じに表示されました。

# おわりに
軽い気持ちで触れたThree.jsでしたが、案外いい感じに使えてよかったです。学び。
ポートフォリオサイトとかにも使えそう。