canvasで取得した画像のピクセルデータをWebGLで扱ってみた

[クソアプリ Advent Calendar 2021](https://qiita.com/advent-calendar/2021/kuso-app)の**12日目**です。
最近、`three.js`を調べるなかで素敵サイトを沢山見つけ、自分も3Dコンテンツを実装してみたくなったので挑戦してみました。

## 作ったもの
画像のピクセルデータを読み取り、WebGLで描画した3Dオブジェクトに変換するまでを作りました。
![スクリーンショット 2021-12-12 3.42.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1116978/54bdf1d3-3fd9-7d06-78f7-cbdf44923e87.png)

※動画が何故かアップロードできないのでよければこちらで
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">WebGLを利用した画像の3D表現 <a href="https://t.co/NV4JGn7dRu">pic.twitter.com/NV4JGn7dRu</a></p>&mdash; Taro Tenugui (@tenugui_taro) <a href="https://twitter.com/tenugui_taro/status/1469739027125661696?ref_src=twsrc%5Etfw">December 11, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

## 技術的な話し
- ピクセルデータを読み取る

本記事のメインです。
意外と遠回りな実装しか情報がなく、疑似canvas要素に描画してからデータ取り出しと少々手間でした。
（もっと簡潔な処理はないものか？）

参考：[Canvas とピクセル操作](https://developer.mozilla.org/ja/docs/Web/API/Canvas_API/Tutorial/Pixel_manipulation_with_canvas)

```javascript
/** 画像設定 */
const img = new Image();
img.src = 'https://mdn.mozillademos.org/files/5397/rhino.jpg';
img.onload = function() {
  draw(this);
};

/** 画像読み込み時の処理 */
function draw(img) {
  /** canvas要素の作成 */
  const canvas = document.createElementById('canvas');
  const ctx = canvas.getContext('2d');
  ctx.drawImage(img, 0, 0);

  /** ピクセルデータの取り出し */
  const imageData = ctx.getImageData(0,0,canvas.width, canvas.height);
  const data = imageData.data;

  /** 反転処理 */
  const invert = function() {
    for (let i = 0; i < data.length; i += 4) {
      data[i]     = 255 - data[i];     // red
      data[i + 1] = 255 - data[i + 1]; // green
      data[i + 2] = 255 - data[i + 2]; // blue
    }
    ctx.putImageData(imageData, 0, 0);
  };

  /** グレースケール処理 */
  const grayscale = function() {
    for (let i = 0; i < data.length; i += 4) {
      let avg = (data[i] + data[i +1] + data[i +2]) / 3;
      data[i]     = avg; // red
      data[i + 1] = avg; // green
      data[i + 2] = avg; // blue
    }
    ctx.putImageData(imageData, 0, 0);
  };

  const invertbtn = document.getElementById('invertbtn');
  invertbtn.addEventListener('click', invert);
  const grayscalebtn = document.getElementById('grayscalebtn');
  grayscalebtn.addEventListener('click', grayscale);
}
```

下記画像は実際に`ImageDataオブジェクト`で得られたデータ形式です。
`data`プロパティには「縦1280 * 横1280 * 4( RGBA )」とごつい配列が格納されていました。
![スクリーンショット 2021-12-12 1.54.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1116978/d4d41915-ce85-a9ad-b031-a89b38e272ea.png)

<br />


- 大量の球体を描画する

今回は、それぞれの球体のマテリアル（色）を変えるので、`THREE.Point`といった一括描画がやりづらかったです。
ジオメトリーの共通化、BufferGeometryの使用、matrixAutoUpdate設定とあれこれやってみましたが、fpsはかなり低いままで27fpsほどでした。
この辺りのテクニックは「[初めてのWebGl 2](https://www.oreilly.co.jp/books/9784873119373/)」読んで勉強しなきゃ...

また、球体描画時には、明るい色は手前に、暗い色は奥にZ軸を設定することでより立体的に見えるよう工夫しました。

```javascript
// 計算処理を省き簡潔に示しています。

/** 共通するジオメトリーを先に定義 */
const geometry = new THREE.SphereBufferGeometry(2, 2, 2);

for (let gridX = 0; gridX < props.imageData.height); gridX++) {
    for (let gridY = 0; gridY < props.imageData.width; gridY++) {

        /** ピクセルデータの取り出し */
        let r = props.imageData.data[i];     // red
        let g = props.imageData.data[i + 1]; // green
        let b = props.imageData.data[i + 2]; // blue
        
        /** メッシュ生成 */
        const mesh = new THREE.Mesh(geometry, material);
        mesh.updateMatrix();
        mesh.matrixAutoUpdate = false; // アニメーションさせず軽くする
        scene.add(mesh);
    }
}
```
<br />


- 開発環境

本記事の主題とは異なりますが、開発環境は「Vite + Vue + TypeScript」としました。
[Vite](https://vitejs.dev)は早さが売りのフロントエンド開発ツールで、噂に違わぬ早さ！
また、`<script setup lang="ts">`の書き方はこれまでのVueコンポーネント特有の記述をしないで済むため素敵。

```vuejs
// Vite + Vue + TypeScript の例

<script setup lang="ts">
import { ref } from 'vue'

defineProps<{ msg: string }>()

const count = ref(0)
</script>

<template>
  <h1>{{ msg }}</h1>

  <p>
    Recommended IDE setup:
    <a href="https://code.visualstudio.com/" target="_blank">VSCode</a>
    +
    <a href="https://github.com/johnsoncodehk/volar" target="_blank">Volar</a>
  </p>

</template>

<style scoped>
a {
  color: #42b983;
}
</style>

```

## おわりに
実は特定サイズ比の画像しか正しく表示されない致命的なバグがあり、公開までは持って行けませんでした（どこかで計算ミスしている...）

WebGLは3D表現を取り入れたWebサイトやWebXRなどで目にする機会が多いので引き続き取り組んでいきたいです。
それではありがとうございました:whale2: