ドット絵を立体に!! PixelArtGeometry 作ってみたよ

#Three.jsには画像を立体化させるジオメトリがない？
Three.jsには[TextGemetry](https://threejs.org/docs/#api/en/geometries/TextGeometry)というフォントを立体化するジオメトリがあって便利ですよね。
でも2D画像をマインクラフトのアイテムみたいにシンプルに立体化させるジオメトリがないかとググってみましたが見つかりませんでした。

#ないならば作ればいいじゃない
いままで3Dプログラミングにあまり触れたことがなかったので、難しい用語に四苦八苦しながら作ってみたのがこちら。

![Heart_bg.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102207/7e0ee1aa-0877-66b3-2f70-6b8e2a34ffa9.png)　これが
↓　↓　↓　↓　↓　↓
![rendered_heart.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102207/104f70e4-0a01-312f-8294-801bfff4bccc.png)　こうなる！

#使い方
Three.jsとPixelArtGeometry.jsをあらかじめロードしておきます。

```html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8" />
  <script src="three.js"></script>
  <script src="OrbitControls.js"></script>
  <script src="PixelArtGeometry.js"></script>
  <style>
	  body {
		  background-color: black;
	  }
  </style>
</head>
<body>
  <canvas id="myCanvas"></canvas> //キャンバス
  <img id="test" src="./textures/heart.png" style="display:none"/> //表示したい画像
</body>
</html>
```

```javascript:index.js
document.addEventListener("DOMContentLoaded", function(e){
	const width = 400;
	const height = 400;
 
	// レンダラの設定
	const renderer = new THREE.WebGLRenderer({
	  canvas: document.querySelector('#myCanvas'),
	  antialias: false
	});
	renderer.setClearColor(0xFFFFFF, 1);
	renderer.setPixelRatio(window.devicePixelRatio);
	renderer.setSize(width, height);
	// renderer.gammaInput = true;  //色味がおかしくなるので使わない事
	// renderer.gammaOutput = true; //

	// シーンの初期化
	const scene = new THREE.Scene();

	// カメラの初期化
	var viewSize = 25.4;
	var aspectRatio = width / height;
	const camera = new THREE.OrthographicCamera(-aspectRatio * viewSize / 2, aspectRatio * viewSize / 2, viewSize / 2, -viewSize / 2, -1000, 1000);
	camera.position.set( viewSize, viewSize * 0.8168, viewSize ); // 0.8168: Isometric projection
	camera.lookAt(scene.position);

	// オービットコントロール(お好みで)
	controls = new THREE.OrbitControls(camera, renderer.domElement);

	//-------------------------------------------------------------------------------------
	//canvasに画像を読み込んで貼り付けます。透過ピクセルを持つ、PNG, GIF, SVGなどを読み込んでください。
	var img = document.querySelector("#heart");
	var texture_canvas = document.createElement("canvas");
	canvas.setAttribute("width", img.width);
	canvas.setAttribute("height", img.height);
	var context = canvas.getContext('2d');
	context.imageSmoothingEnabled  = false;
	context.beginPath();
	context.clearRect(0, 0, img.width, img.height);
	context.drawImage(img, 0, 0, img.width, img.height, 0, 0, img.width, img.height);

	//-------------------------------------------------------------------------------------
	//THREE.Mesh のコンストラクタの第一引数にセットして、好きなマテリアルを第二引数にセットしてください。
	// マテリアルに vertexColors: THREE.FaceColors を指定します。指定しない場合、テクスチャを使ってください。
	const Heart = new THREE.Mesh(
		// 引数： キャンバス、奥行き(ピクセル)、vertexColorsを使う場合true
		new THREE.PixelArtGeometry(canvas, 1, true),
		new THREE.MeshLambertMaterial({vertexColors: THREE.FaceColors, })
	);
	Heart.scale.set(20, 20, 20);  // 拡大
	Heart.rotation.set(0, 15 * (Math.PI / 180), 0); //回転
	scene.add(Heart); //シーンに追加

	// ライティング -------------------------------------------------------------------------------------
	const night = false;
	const dirLight = new THREE.DirectionalLight(night ? 0x9999cc: 0xB4B4B4);
	var d = 50;
	dirLight.position.set(-1, 2.25, 1).normalize();
	dirLight.castShadow = true;
	dirLight.shadow.mapSize.width = 2048;
	dirLight.shadow.mapSize.height = 2048;
	dirLight.shadow.camera.left = -d;
	dirLight.shadow.camera.right = d;
	dirLight.shadow.camera.top = d;
	dirLight.shadow.camera.bottom = -d;
	dirLight.shadow.camera.far = 3500;
	dirLight.shadow.bias = -0.0001;
	scene.add(dirLight);
	var ambientLight = new THREE.AmbientLight(night ? 0x333333: 0x5D5D5D );
	scene.add( ambientLight );
   
	// Run tick -------------------------------------------------------------------------------------
	tick();
   
	function tick() {
		// OrbitControl用。省略可
		requestAnimationFrame(tick);
   
		// Rendering
		renderer.render(scene, camera);
	}
  });
```


#ソースコード
2019-07-28 更新：UVマッピングを追加。テクスチャを使うこともできるようになりました。
長くなったのでこちらからどうぞ。
<p class="codepen" data-height="600" data-theme-id="0" data-default-tab="html,result" data-user="pneuma01" data-slug-hash="WVoVRR" style="height: 600px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="Pixel Art Geometry">
  <span>See the Pen <a href="https://codepen.io/pneuma01/pen/WVoVRR/">
  Pixel Art Geometry</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>

#解説
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102207/1aea7dda-a7ab-9a83-39dc-8b23968c5bf7.png)
Context2DのgetImageDataを使ってピクセルの色情報を拾い、透過度が0でない場合、6個の立方体の頂点座標を作ってverticesへ追加します。
それぞれの面には取得した色をそのまま付けています。
(半透明には対応しきれなかったので、とりあえず透過度を明るさにして色を変換しています)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102207/652cee59-8fe7-457f-85f9-d6c93e2477de.png)チェック無し
このままでも見た目はいい感じなのですが、隣合わせの面の情報が無駄なのと、同じ位置の面同士が表示時にチラつくため、それを回避するように上下左右のピクセルをチェックして、透明の場合だけ側面を追加するようにしています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102207/b79f3e2a-b449-6028-4ff2-d99f36a3d433.png)チェックあり

#苦労した点
###verticesへの頂点座標の追加は反時計回りが鉄則？
頂点座標を追加してポリゴンフレームを作るまでは良かったのですが、面(Face3)を付けた時、面の向きがどうやっても裏側になってしまい、最初はマテリアルに``.side=THREE.DoubleSide``(両面を描画)を指定しないとダメでした。
色々ググった結果、verticesに頂点(Vector3)を反時計回りに追加していないと表向きにならないという謎セオリーがあることが発覚！！

###表面の謎の白っぽさ・・・
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102207/0215c1ea-a5eb-54be-5607-1f2c8099ca95.png)これ全部、赤(255,0,0)なんです
もう一つ、3日ほど悩んだ部分ですが、キャンバスから色を拾った時、やたら白みがかった色しか取れなくなる現象が続いていました。
``ctx.getImageData(0, 0, w, h);``の時点で既に色が変化している！？ということは、Three.jsが原因ではない？
ちなみに普通のテクスチャを張ったキューブだとこの現象がおきず、原因不明。
さらにさらに調べた結果、

```javascript
	const renderer = new THREE.WebGLRenderer({
	  canvas: document.querySelector('#myCanvas'),
	  antialias: false
	});
	renderer.gammaInput = true; //<---------  こ い つ ら !!
	renderer.gammaOutput = true;//<---------
```

この何の変哲もないスニペットをコピペしてきたのが原因でした。
このgammmaInput/Outputは色の取得/設定時にgammaFactorを使うかどうかの設定なのですが、HTML5のcanvasオブジェクトにも影響があるそうです。