Minecraftで学ぶ半透明テクスチャ

<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102207/42627cf2-8d50-92e9-c19a-bc35f9dd0ce5.png width="160">

テクスチャを使った半透明の表現はやってみるとなかなか難しいです。
Three.jsでは単純にアルファチャンネルを持つPNGを直接テクスチャに指定しても半透明にはなりません。
マイクラのリソースパックをいじる感覚でやると、かなり勝手が違っていて悩みます。

## Three.jsでテクスチャの半透明レンダリング

Three.jsでは半透明を表現するにはいくつかの方法があります。
``Material.opacity``を使う方法。
``Material.alphaMap``を使う方法です。

### Material.opacity
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102207/85c85be2-dc65-74b1-b29d-a8f8344229f3.png" width="160">
opacityはそのマテリアルが使われているジオメトリに対して透明度を指定できます。
これはアルファチャンネルを持たないテクスチャを使用する場合に適しています。
css の opacityと同様に、0 から 1 の浮動小数点で指定します。
これはテクスチャ全体に均一な半透明処理を行うだけで、マイクラの透明処理とは異なります。

### Material.alphaMap
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102207/acb08d5e-23b9-564e-7ed9-6c58bf97aa2c.png" width="160">

alphaMap は ``Material.map``に対して部分的な透明処理を施したい場合に適しています。
これはPNG画像でいうところのアルファチャンネルにあたり、グレイスケール画像を指定する必要があります。
ピクセルが黒(#000000)ならば透明に、ピクセルが白(#FFFFFF)ならば不透過になります。
``Material.transaprent`` に true を設定していないと無視されてしまうので注意。

Canvasオブジェクトの[context2d.getImageData](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/getImageData) を使って自力でアルファMapを作成するのもいいですが、PNGからアルファチャンネルをImageとして取得できるPNGライブラリを使うと楽かもしれません。

## 半透明テクスチャを使うときのハマリポイント

### Material.Map の罠
``Material.map`` はアルファチャンネル付きのPNG画像をそのまま表示することはできません。

例えば``Material.transparent = false``の時、Material.alphaTest プロパティに 1 未満の値がセットされていた場合、半透明部分はすべて透明と認識されてしまいます。デフォルト値は 0 であるため、全体が半透明なテクスチャ画像を指定すると、完全に見えなくなります。

また``Material.transparent = true``の時、Material.map に半透明な画像を指定するとPNG画像のカラーチャンネルとアルファチャンネルは #FFFFFF をbackground-colorとして合成されて色味が薄くなってしまいます。
したがって、``Material.map`` にテクスチャをセットする時は予めアルファチャンネルを除去しておく必要があります。

### Material.depthWrite の罠
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102207/bcc58efe-deb6-ae60-96d0-976965106ac5.png" width="160"><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102207/1548af57-678d-b7f3-cfe5-ff72c3e5d9db.png" width="160">

``Material.depthWrite``プロパティは各オブジェクトの座標と大きさで奥行き判別して高速にオブジェクトの重なり描画を行う機能です。Three.jsではこれがデフォルトで true になっています。

しかしこれは不透過メッシュの描画を高速化するための機能であるため、半透明オブジェクトの重なり合いが発生すると正しく描画されません。

スライムなどの半透明オブジェクトを組み合わせたオブジェクトは、デフォルトのままだと中身が正しく描画されませんので false に設定しておく必要があります。

### face と Mesh の半透明処理の違い
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102207/764ce4d1-3312-6d07-9fb8-a0f0414ddad1.png" width="160"><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102207/1c79a888-0ad0-2e84-ee01-2c4ef580baa9.png" width="160">

Meshオブジェクト同士は透明処理とともにライティングも正しく影響しますが、同一Mesh内のポリゴン(``Geometry.faces[]``)同士は正しい合算が行われないようです。各faceの表裏(cullface)がfaceごとに影響し合わないのか、すべての面を含めたGeometryで作成したメッシュを描画した場合では、重なった面のライティングが不自然になることがあります。

これはGeometry内のポリゴンがすべて同じGeometryの中心座標を起点としている為で、カメラからの深度が同一とみなされのが原因のようです。
Opaqueオブジェクトのレンダリングを最適化するdepthWrite, depthTestプロパティをfalseにしても、その保証対象はMesh単位でしかないようで、私は半透明オブジェクトのレンダリングの際はMeshの構成単位はなるべく細かくしてTHREE.Object3Dなどでグルーピングするようにしています。

## おわり
半透明レンダリングは面倒です。
AOマッピングやシェーダーなどが組み合わさるとさらに厄介になりそうです。

ゲーム中のスライムブロックの描画を見ても、角度によってオブジェクトの表示/非表示を切り替えているようで、その苦労が伺えます。
これはTHREE.js/WebGLというよりも元となったOpenGLの技術的な仕様のようで、まだまだ発展途上なのかなと感じます。