Three.js faceVertexUvsを利用したテクスチャのアニメーション

この記事は[Three.jsテクスチャの部分切り抜きや回転・反転・伸縮](https://qiita.com/Urushibara01/items/d828e853fc5c4626647a) の続編になります。Three.jsやWebGLのテクスチャの仕組みは前回に詳しく書いていますので一読してみてください。

![animated.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102207/1482c73e-22fa-5fab-f629-85dda0cac1f4.png)

マイクラのアニメーションするブロックのテクスチャファイルを見ると、縦にタイル状に並んだテクスチャが使われているのがわかります。
![smoker_front_on.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102207/75d6f8d2-4215-32cb-2dfb-b1199228684a.png)
こういった画像を使ってアニメーションする場合、物理的に切り貼りしてテクスチャイメージを動的に読み直したりはせずに、CSSの background-image のoffset のように、見せたい部分だけをずらして表示させるとメモリ軽減になります。

<p class="codepen" data-height="456" data-theme-id="light" data-default-tab="js,result" data-user="pneuma01" data-slug-hash="RwwQQRW" style="height: 456px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="Minecraft Smoker Animation">
  <span>See the Pen <a href="https://codepen.io/pneuma01/pen/RwwQQRW">
  Minecraft Smoker Animation</a> by Urushibara (<a href="https://codepen.io/pneuma01">@pneuma01</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>
[CodePen - Minecraft Smorker Animation](https://codepen.io/pneuma01/pen/RwwQQRW)

ここで表示させているのは、実際にMinecraftで使われている models/smoker.json (読込用にBlockbenchで変換)を使って生成した3Dオブジェクトです。
3Dオブジェクトは独自クラス化したコードで作成したものなので省きますが、基本的にはTHREE.BoxGeometryなどと同じ構造だと思ってください。

##faceVertexUvs のVector2の値をずらすだけ
見てほしいのは、このアニメーションを実現させている箇所です。

```javascript:呼び出し元

	let animate = (mesh) => {
		vectorAdd(mesh.geometry.faceVertexUvs[0][10], 16, 48, 0, 16);
		vectorAdd(mesh.geometry.faceVertexUvs[0][11], 16, 48, 0, 16);
		mesh.geometry.uvsNeedUpdate = true;
	}
```

```javascript:Vector2を指定ピクセルでずらす関数
function vectorAdd(face, width, height, x, y)
{
	//Vector2をリアル座標系に変換してから座標を加算する関数
	let add = vector => {
		let xx = vector.x * width + x;
		let yy = (1 - vector.y) * height + y;

		return {x: xx, y: yy};
	}

	//すべての座標をリアル座標になおして加算
	let vexs = []; //Vector2の配列
	let maxX = 0, maxY = 0;
	face.forEach(vector => {
		let pos = add(vector)
		vexs.push(pos)
		//はみ出しチェック用
		maxX = Math.max(maxX, pos.x)
		maxY = Math.max(maxY, pos.y)
	});

	//最大幅よりもはみ出してたらマイナスする
	let minusX = maxX > width ? width: 0;
	let minusY = maxY > height ? height: 0;

	//座標系を変換して実際のfaceVertexUvsに代入
	for(let i=0; i < face.length; i++){
		face[i].x = (vexs[i].x - minusX) / width
		face[i].y = 1 - (vexs[i].y - minusY) / height
	}
}
```

少し複雑に見えるかもしれませんが、実際にやっていることはこんな感じです。

- テクスチャ座標系からリアル座標に戻す(実際のテクスチャのWidth/Heightを掛ける)
- 指定されたピクセルを加算
- テクスチャ座標系にもどす(実際のテクスチャのWidth/Heightで割る)
- faceVertexUvsに代入

前回に説明しましたが、テクスチャ座標系の場合はy軸が上下逆になるので注意。

##クロスフェード？
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102207/329ac0ec-d0c2-573d-f25b-eb8232f9980b.gif" width=150/>
マイクラでは、これを複数レイヤーで使ってクロスフェードまでやっています。
~~Three.jsはどうもレイヤーの概念が無い？(faceVertexUvs[Layers]は存在するがマテリアルを個別指定できない)のでどうやっているのかちょっとわかりません。~~ →下記参照
同じ形状のGeometryを重ねて、2つのジオメトリのOpacity(透明度)をアニメーションして同じことができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102207/14d79ff2-b744-fd5f-6e81-13410db14c45.png)
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102207/6814fbe1-7bf0-4851-0404-cf68b87ee2a4.gif" width=150> + <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102207/ea274335-e81e-b70c-a029-9dc32a48f768.gif" width=150> merge <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102207/a4d36245-1ad3-585f-6933-74dca69b047c.gif" width=150>

追記:
複数のテクスチャの合成は現在(rev120時点)のTHREE.jsが提供する機能では再現できないようです。
これを実現するには Canvasオブジェクトにwebglで2枚の画像を指定した値で合成するfragment shaderを書いて、THREE.CanvasTextureでテクスチャに割り当てる方法が考えられます。
THREE.ShaderMaterialでもfragment shaderを扱えますが、こちらはライティング処理も自力で書く必要があるためテクスチャの合成を行うだけの場合には向いてなさそうです。