Minecraftっぽいアンビエントオクルージョン(AO)

#アンビエントオクルージョン(AO)とは
遮蔽や環境光の反射などを模して陰影をつけることでより自然な影を演出する効果のこと。

AO無し　/　AOあり
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102207/9c226624-2dcb-18e0-39b0-fffeaa4f51f3.png" width=300> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102207/fbe3f88d-4070-10c3-8574-02aa1703f098.png" width=300>

#MinecraftにおけるAO
本来AOは曲線などの複雑な影を計算する必要がありますが、Minecraftではキューブ状の地形なので、これを単純なアルゴリズムで再現することができます。
この図はキューブの各頂点の明るさを4段階に分類したものです。
Minecraftでは他にも各ブロックに対する明るさレベルがありますが、このような分類のAOマップで成り立っています。
![aovoxel2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102207/aa7c5ff6-4403-cced-efaa-8858a0bed60a.png)

Minecraftでは完全にはこれに当てはまらず、各面2枚の3角形ポリゴンに対してそれぞれAO計算を行っており、それによる不具合もありますが、ここでは省きます。

#AOテクスチャの作成
では先程の頂点の明るさの分類を元にテクスチャを作成してみます。
各頂点の側面と角の3方向の空気/障害物を0/1とする値として、以下の関数を使ってこの明度値を取得します。

```javascript
function vertexAO(side1, side2, corner) {
  if(side1 && side2) {
    return 0
  }
  return 3 - (side1 + side2 + corner)
}
```

それぞれ4つの頂点の明るさを取得したら、各頂点を1ピクセルとするグレイスケールのAO画像を作成します。

```javascript
// 4x4のcanvasを作成
let canvas = document.createElement("canvas");
canvas.setAttribute("width", 2);
canvas.setAttribute("height", 2);
let ctx = canvas.getContext('2d');

//明度曲線テーブル
const cols = [0.4*255, 0.6*255, 0.75*255, 1.0*255];

//light_level: 頂点の明るさ

let col = cols[light_level[0]];
ctx.fillStyle = "rgb(" + col + ", " + col + ", " + col + ")";
ctx.fillRect(0, 0, 1, 1);

col = cols[light_level[1]];
ctx.fillStyle = "rgb(" + col + ", " + col + ", " + col + ")";
ctx.fillRect(1, 0, 1, 1);

col = cols[light_level[2]];
ctx.fillStyle = "rgb(" + col + ", " + col + ", " + col + ")";
ctx.fillRect(1, 1, 1, 1);

col = cols[light_level[3]];
ctx.fillStyle = "rgb(" + col + ", " + col + ", " + col + ")";
ctx.fillRect(0, 1, 1, 1);

let texture = new THREE.CanvasTexture(canvas);
texture.magFilter = THREE.LinearFilter; //アンチエイリアスで拡大させる(省略可)

//メッシュに適用させる
mesh.material.aoMap = texture;
```
作成されるAOテクスチャの例
アンチエイリアス無し / アンチエイリアス有り
![ao_texture.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102207/8f43be50-b0c7-9361-9a7e-5ffeb8acfaa7.png)　![scaled_ao.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102207/dcd2f664-7d64-1008-2c34-0e492dc47fa3.png)

通常のテクスチャのみ
![texture_only.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102207/aba3dc87-9f1c-2052-3b02-0290d9e3cf59.png)

AOマップ適用後
![bombed.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102207/a0799434-4b06-701f-59a7-8f1790c0f8ae.png)

#Three.jsにおけるAOマップの注意
先程のソースはMeshのMaterialにそのまま適用させましたが、Three.jsではそのままでは正しく表示されません。
なぜかというと、テクスチャマップである、GeometryのfaceVertexUVsにはデフォルトではAOマップ用のレイヤーが無いからです。
ただ、これは普通のテクスチャマップのコピーを追加させるだけで良いので難しくはありません。

THREE.Geometry.jsのソースの一部を拝借して作成した関数。

```Javascript
// faceVertexUvsにAOマップ用のレイヤーを追加する
function addAOVertexUVs(mesh){
	mesh.geometry.faceVertexUvs[1] = [];
	
	for(let j=0; j<mesh.geometry.faceVertexUvs[0].length; j++){

		let uvs = mesh.geometry.faceVertexUvs[0][j];
		let uvsCopy = [];

		for(let k=0; k<uvs.length; k++){

			uvsCopy.push( uvs[k].clone() );

		}
		mesh.geometry.faceVertexUvs[1].push( uvsCopy );
	}
}
```
#サンプル
「Ignite」ボタンで発火してね！
<p class="codepen" data-height="573" data-theme-id="light" data-default-tab="result" data-user="pneuma01" data-slug-hash="BazpgEe" style="height: 573px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="Minecraft: TNT Explosion">
  <span>See the Pen <a href="https://codepen.io/pneuma01/pen/BazpgEe">
  Minecraft: TNT Explosion</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>

#参考元ページ
以下のページの一部画像を使用、内容を参考にさせていただきました。
[0 FPS - Ambient occlusion for Minecraft-like worlds](https://0fps.net/2013/07/03/ambient-occlusion-for-minecraft-like-worlds/)