p5.jsのbaseMaterialShaderを使って大量の正方形を高速描画する

# はじめに
　p5.jsのbaseMaterialShader()を使うとshaderを自由に改変することができます。
　[baseMaterialShader()](https://p5js.org/reference/p5/baseMaterialShader/)
これを使って以前、ノイズをglslに移植しました。
　[p5.jsのbaseMaterialShaderを使ってnoise関数をシェーダー(GLSL)で用いる](https://qiita.com/inaba_darkfox/items/d57ac9948a5fc388d835)
　今回はこれの応用として、簡単な弾幕を作成します。

## コード全文
　[DANMAKU 2](https://openprocessing.org/sketch/2487979)
```js
let sh, geom, randomTex;

function setup() {
  createCanvas(600, 600, WEBGL);
  pixelDensity(1);

  randomTex = createRandomTableArray(64, 64);

  geom = new p5.Geometry();
  for(let i=0;i<4000;i++){
    geom.vertices.push(createVector(i, PI/4, 1/1.4142));
    geom.vertices.push(createVector(i, 3*PI/4, 1/1.4142));
    geom.vertices.push(createVector(i, 5*PI/4, 1/1.4142));
    geom.vertices.push(createVector(i, 7*PI/4, 1/1.4142));
    geom.uvs.push(1,1,0,1,0,0,1,0);
    geom.faces.push([4*i,4*i+1,4*i+2],[4*i,4*i+2,4*i+3]);
  }
  sh = baseMaterialShader().modify({
    vertexDeclarations:`
      uniform float uTime;
      uniform sampler2D uRandom;

      // color -> float
      float convertToFloat(vec4 c){
        uint r = uint(round(c.r*255.0));
        uint g = uint(round(c.g*255.0));
        uint b = uint(round(c.b*255.0));
        uint a = uint(round(c.a*255.0));
        return uintBitsToFloat(r|(g<<8)|(b<<16)|(a<<24));
      }

      // Perlinの取得
      float getPerlin(int n){
       n = n & 4095;
       float i = float(n);
       float x = mod(i, 64.0) + 0.5;
       float y = floor(i / 64.0) + 0.5;
       return convertToFloat(texture(uRandom, vec2(x, y) / 64.0));
      }

      float scaled_cosine(float x){ return 0.5 * (1.0 - cos(x * 3.14159)); }

      float p5noise(float x, float y, float z){
        x = abs(x);
        y = abs(y);
        z = abs(z);
        int xi = int(floor(x));
        int yi = int(floor(y));
        int zi = int(floor(z));
        float xf = x - floor(x);
        float yf = y - floor(y);
        float zf = z - floor(z);
        float rxf;
        float ryf;
        float r = 0.0;
        float ampl = 0.5;
        float n1 = 0.0;
        float n2 = 0.0;
        float n3 = 0.0;
        for(int octave = 0; octave < 4; octave++){
          int of = xi + (yi << 4) + (zi << 8);
          rxf = scaled_cosine(xf);
          ryf = scaled_cosine(yf);
          n1 = getPerlin(of);
          n1 += rxf * (getPerlin(of + 1) - n1);
          n2 = getPerlin(of + 16);
          n2 += rxf * (getPerlin(of + 16 + 1) - n2);
          n1 += ryf * (n2 - n1);
          of += 256;
          n2 = getPerlin(of);
          n2 += rxf * (getPerlin(of + 1) - n2);
          n3 = getPerlin(of + 16);
          n3 += rxf * (getPerlin(of + 16 + 1) - n3);
          n2 += ryf * (n3 - n2);
          n1 += scaled_cosine(zf) * (n2 - n1);
          r += n1 * ampl;
          ampl *= 0.5;
          xi <<= 1;
          xf *= 2.0;
          yi <<= 1;
          yf *= 2.0;
          zi <<= 1;
          zf *= 2.0;
          if (xf >= 1.0) {
            xi++;
            xf--;
          }
          if (yf >= 1.0) {
            yi++;
            yf--;
          }
          if (zf >= 1.0) {
            zi++;
            zf--;
          }
        }
        return r;
      }
    `,
    'vec3 getLocalPosition':`(vec3 p){
      float x = mod(uTime*120.0 + fract(p5noise(p.x, 0.0, 0.0)*2.0)*500.0, 500.0);
      float angle = 6.28318*p.x/4000.0;
      float direction = angle + p.y;
      vec2 c = vec2(x*cos(angle), x*sin(angle));
      vec3 q = vec3(c.x + 2.0*p.z*cos(direction), c.y + 2.0*p.z*sin(direction), 0.0);
      return q;
    }`
  });
}

function draw() {
  shader(sh);

  sh.setUniform("uTime", millis()/1000);
  sh.setUniform("uRandom", randomTex);

  background(0);
  noLights();
  fill(255);
  model(geom);

  if(frameCount%10===0)console.log(frameRate());
}

function createRandomTableArray(w, h){
  const ab = new ArrayBuffer(w*h*4);
  const view = new DataView(ab);
  for(let k=0; k<w*h; k++){
    const r = random();
    view.setFloat32(4*k, r, true); // バイト単位なので注意
  }
  const ca = new Uint8ClampedArray(w*h*4);
  for(let i=0; i<w*h*4; i++){
    ca[i] = view.getUint8(i);
  }
  const imd = new ImageData(ca, w, h);

  // バイト列を無修正で格納
  const gl = this._renderer.GL;
  gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
  const result = new p5.Texture(this._renderer, imd);
  gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);

  return result; // おわりです
}
```
drawCallの最後に10フレームカウントおきでパフォーマンスを出力しています。比較用です。内容的には、画面中央から無数の正方形が外に向かって出力されるありがちなものです。

![erwafwegf4efe4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437349/4a0f62a6-ee67-5923-035a-53a26a139124.png)

## 簡単な説明
　ノイズに関する部分は先ほどのリンク先で説明したので割愛します。ざっくりいうと、p5のnoise関数をglsl内部で使うための関数です。今回の工夫したポイントは、それによって描画するgeometryです。
```js
  geom = new p5.Geometry();
  for(let i=0;i<4000;i++){
    geom.vertices.push(createVector(i, PI/4, 1/1.4142));
    geom.vertices.push(createVector(i, 3*PI/4, 1/1.4142));
    geom.vertices.push(createVector(i, 5*PI/4, 1/1.4142));
    geom.vertices.push(createVector(i, 7*PI/4, 1/1.4142));
    geom.uvs.push(1,1,0,1,0,0,1,0);
    geom.faces.push([4*i,4*i+1,4*i+2],[4*i,4*i+2,4*i+3]);
  }
```
geometryには4つずつ頂点をぶち込んでいます。これらが正方形の4辺をあらわしています。一つ目はidです。二つ目が角度ですね。三つめは縦横の比率です。長方形にするなら別の表現方法もあるのですがこのやり方の方が回転させるのに向いているのでそうしています（別の理由もありますが...）。あとuvsですね。今回は使いませんが...facesは通常のものです。4つずつ正方形を表現しているので、それを作るものです。
　これの位置と回転を決めているのがbaseMaterialShader()のmodify()内部でのgetLocalPosition関数です。もう慣れたでしょうか...
```glsl
  float x = mod(uTime*120.0 + fract(p5noise(p.x, 0.0, 0.0)*2.0)*500.0, 500.0);
  float angle = 6.28318*p.x/4000.0;
  float direction = angle + p.y;
  vec2 c = vec2(x*cos(angle), x*sin(angle));
  vec3 q = vec3(c.x + 2.0*p.z*cos(direction), c.y + 2.0*p.z*sin(direction), 0.0);
  return q;
```
基本的な位置をuTime（秒数）の120倍で決めてノイズでオフセットを表現しています。noiseはランダムでありながら引数に縛られるのでこの手の用途にうってつけです。2倍してfractを取ることでいい感じにばらけるのですが、この辺は
　[rotateとnoiseで簡単な弾幕を作る](https://zenn.dev/kurogitsune/articles/b464a8d73a879b)
が参考になるかも？あとはdirectionをp.yから決めていますね。こんな感じで使えるのです。今回描画する正方形は4000個なので4000個の均等な間隔で角度をずらしていますね。
　動きについてはリンク先で試してみてください（動画を載せると重くなるので...）。

## 2Dの場合
　同じことを2Dでも試してみました。共にpixelDensity(1)で、内容的にも寄せています。
　[DANMAKU 1](https://openprocessing.org/sketch/2487978)

 ![wdcd23d2d2おそい.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437349/1aa1b114-0294-13c6-dd4a-735a076e6b4e.png)

```js
function setup() {
  createCanvas(600, 600);
  pixelDensity(1);
}

function draw() {
  background(0);
  translate(300, 300);
  let r=4000;
  noStroke();
  fill(255);
  const t = millis()/1000;
  while(r--){
    const x = (t*120 + fract(noise(r)*2)*500)%500;
    translate(x,0);
    rect(-1,-1,2,2);
    translate(-x,0);
    rotate(TAU/4000);
  }
  if(frameCount%10===0)console.log(frameRate());
}
```
非常にシンプルで分かりやすいコードです。パフォーマンス的には、自分のパソコンでは大差ありませんでした（ファンの音が若干していましたが...この記事の方はほぼ無音ですね）。ちなみに自前のスマホで試したところ、この記事の方はfps60前後をキープしていたんですが、この2Dの方は...10いくのがやっとですね...まあ、遅いです。

# おわりに
　もちろん全く同じことができるわけではないのであくまで選択肢です。グラフィックの表現には実にいろんなやり方、目的があるので、用途に応じて使い分けるべきでしょう。なお、uvを使えばテクスチャを用いた大量描画も可能なので興味のある人は挑戦してみてください。
　ここまでお読みいただいてありがとうございました。