p5.jsでbaseMaterialShaderとTEXTURE_3Dを使ってスライドショーを作ってみる

# はじめに
　TEXTURE_3Dを使いたかったので使ってみました。

https://ics.media/web3d-maniacs/webgl2_texture3d/

　TEXTURE_3Dというのは複数枚の同じサイズのテクスチャをまとめて扱うための機構です。2D_ARRAYと似ていますが、少し違います。こちらは3つ目の成分が複数のテクスチャにまたがる場合、roundで特定せず、フィルターによっては補間します。たとえば0番と1番の画像が補間されたりします。これをうまく使うとスライドショーが作れたりします。そこで作ってみました。

　画像は自分のgithubにおいてある3枚の富士山の画像です。今回はただのテストなので、3枚分の縦長のグラフィックに順番で落として使っています。コードの内容は2D_ARRAYで前回作ったものとほぼ全く一緒です。

## コード全文
　[p5 TEXTURE_3D](https://openprocessing.org/sketch/2611361)
```js
/*
  参考：p5でTEXTURE_3Dを使おう！

  ICSの記事：
  https://ics.media/web3d-maniacs/webgl2_texture3d/

  1/6と3/6と5/6でぴったり
  それ以外は補間になるわけだ
  それもそうか
  0って0番と1番の境界だからな
  なるほど...
*/

let fujisan_images = [];

function preload(){
  // 680x510が3枚
  fujisan_images.push(loadImage("https://inaridarkfox4231.github.io/assets/backgrounds/fujisan.jpg"));
  fujisan_images.push(loadImage("https://inaridarkfox4231.github.io/assets/backgrounds/fujisan2.jpg"));
  fujisan_images.push(loadImage("https://inaridarkfox4231.github.io/assets/backgrounds/fujisan3.jpg"));
}

function setup() {
  createCanvas(800, 800, WEBGL);

  // shaderを作る
  const sh = baseMaterialShader().modify({
    'fragmentDeclarations':`
      precision mediump sampler3D;
      uniform sampler3D uFuji;
      uniform float uShift; // 0~1,1~2,2~3,...
    `,
    'vec4 getFinalColor':`(vec4 color){
      color = texture(uFuji, vec3(vTexCoord, 1.0/6.0 + uShift/3.0));
      return color;
    }`
  });

  shader(sh); // これでprogramができる
  const gl = this._renderer.GL;
  const pg = sh._glProgram;
  gl.useProgram(pg); // これでprogramが走る
  const loc = gl.getUniformLocation(pg, "uFuji");
  // 3番に入れる。
  gl.uniform1i(loc, 3);

  const tex = gl.createTexture();
  // 3番に入れる
  gl.activeTexture(gl.TEXTURE3);
  gl.bindTexture(gl.TEXTURE_3D, tex);

  const gr = createGraphics(680, 510*3);
  for(let i=0; i<3; i++){
    gr.image(fujisan_images[i], 0, 510*i, 680, 510, 0, 0, 680, 510);
  }
  const src = gr.elt;
  gl.texImage3D(
    gl.TEXTURE_3D, 0, gl.RGBA, src.width, src.height/3, 3,
    0, gl.RGBA, gl.UNSIGNED_BYTE, src
  );
  gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
  gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
  gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_WRAP_R, gl.REPEAT);

  const loopFunction = () => {
    background(0,0,128);

    shader(sh);
    noStroke();

    const fc = frameCount%360;
    if((fc%120)<60){
      sh.setUniform("uShift", floor(fc/120));
    }else{
      sh.setUniform("uShift", floor(fc/120) + (fc%60)/60);
    }

    plane(680, 510);
  }

  draw = loopFunction;
}
```

## ポイント
　引数のvec3の3番目は奥行方向で0～1により指定します。2D_ARRAYではここは番号でしたが、3Dでは0～1で指定するようになっています。しかも値によっては補間されます。今回フィルタをLINEARにしているのでそうなります。で、たとえば0の場合0番と2番で補間されます。なぜかというと
```js
  gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_WRAP_R, gl.REPEAT);
```
ラッピングの奥行方向をREPEATにしているからです。この場合、1/6, 3/6, 5/6でちょうど0,1,2番の画像になります。なのでスライドショーにする場合、これらの値で固定し、間の値の時補間されるので、そのように整えればいいわけですね。
```js
    const fc = frameCount%360;
    if((fc%120)<60){
      sh.setUniform("uShift", floor(fc/120));
    }else{
      sh.setUniform("uShift", floor(fc/120) + (fc%60)/60);
    }
```
それをやっているのがここで、
```glsl
      color = texture(uFuji, vec3(vTexCoord, 1.0/6.0 + uShift/3.0));
      return color;
```
このように反映されます。それで添付画像のようになるわけですね。

# おわりに
　3Dの使い方というか仕様を確認したかっただけです。ボリュームレンダリングみたいなこともできるらしいですが今は興味無いのでとりあえず使ってみただけですね。
　ここまでお読みいただいてありがとうございました。

## サムネ事件
　このコードでスケッチを作ったらサムネが崩壊しました。

![ergvdgfvrdfgdrg.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437349/d0115505-b146-4678-8b14-09efb4d8c272.png)

これはオフキャンバスでcreateGraphicsを使ってでかいサイズのを作るとたまにおきます。起きないようにするにはcreateGraphicsを使わなければ問題ありません。そのように書き換えたのが次のコードです。
　[p5 TEXTURE_3D_fuji](https://openprocessing.org/sketch/2611903)
```js
/*
  参考：p5でTEXTURE_3Dを使おう！

  ICSの記事：
  https://ics.media/web3d-maniacs/webgl2_texture3d/

  1/6と3/6と5/6でぴったり
  それ以外は補間になるわけだ
  それもそうか
  0って0番と1番の境界だからな
  なるほど...

  サムネイルが死んでるのでcreateGraphicsを使わないでやります
  サムネイル復活しました！！
*/

let fujisan_images = [];

function preload(){
  // 680x510が3枚
  fujisan_images.push(loadImage("https://inaridarkfox4231.github.io/assets/backgrounds/fujisan.jpg"));
  for(let i=2; i<=10; i++){
    fujisan_images.push(loadImage(`https://inaridarkfox4231.github.io/assets/backgrounds/fujisan${i}.jpg`));
  }
  fujisan_images.push(loadImage("https://inaridarkfox4231.github.io/assets/backgrounds/shippei.jpg"));
}

function setup() {
  createCanvas(800, 800, WEBGL);

  // shaderを作る
  const sh = baseMaterialShader().modify({
    'fragmentDeclarations':`
      precision mediump sampler3D;
      uniform sampler3D uFuji;
      uniform float uFrame; // 0,1,2,...
      uniform float uTransition; // 0～1
      uniform float uCount;
    `,
    'vec4 getFinalColor':`(vec4 color){
      float t = uTransition;
      vec2 uv = vTexCoord;
      if(fract(16.0*uv.x) < t){ t = 1.0; }else{ t = 0.0; }
      color = texture(uFuji, vec3(uv, 1.0/(uCount*2.0) + (uFrame + t)/uCount));
      return color;
    }`
  });

  shader(sh); // これでprogramができる
  const gl = this._renderer.GL;
  const pg = sh._glProgram;
  gl.useProgram(pg); // これでprogramが走る
  const loc = gl.getUniformLocation(pg, "uFuji");
  // 3番に入れる。
  gl.uniform1i(loc, 3);

  const tex = gl.createTexture();
  // 3番に入れる
  gl.activeTexture(gl.TEXTURE3);
  gl.bindTexture(gl.TEXTURE_3D, tex);

  const IMG_WIDTH = fujisan_images[0].width;
  const IMG_HEIGHT = fujisan_images[0].height;
  const IMG_COUNT = fujisan_images.length;

  const pixelData = new Uint8Array(IMG_WIDTH*IMG_HEIGHT*4*IMG_COUNT);

  const cvs = document.createElement('canvas');
  cvs.width = IMG_WIDTH;
  cvs.height = IMG_HEIGHT;
  // 複数回getImageDataを実行する場合はこれをtrueにする
  const ctx = cvs.getContext('2d', {willReadFrequently:true});

  for(let i=0; i<IMG_COUNT; i++){
    ctx.clearRect(0, 0, IMG_WIDTH, IMG_HEIGHT);
    ctx.drawImage(
      fujisan_images[i].canvas, 0, 0, IMG_WIDTH, IMG_HEIGHT,
      0, 0, IMG_WIDTH, IMG_HEIGHT
    );
    const imd = ctx.getImageData(0, 0, IMG_WIDTH, IMG_HEIGHT);
    pixelData.set(imd.data, IMG_WIDTH*IMG_HEIGHT*4*i);
  }

  // Uint8Arrayの場合プレ丸が使えないので、一旦切る。
  gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);

  gl.texImage3D(
    gl.TEXTURE_3D, 0, gl.RGBA, IMG_WIDTH, IMG_HEIGHT, IMG_COUNT,
    0, gl.RGBA, gl.UNSIGNED_BYTE, pixelData
  );
  gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
  gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
  gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_WRAP_R, gl.REPEAT);

  // 戻す。今回は不透明なので、特に気にする必要は無い。
  gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);

  const loopFunction = () => {
    background(0,0,128);

    shader(sh);
    noStroke();

    const fc = frameCount%(120*IMG_COUNT);
    sh.setUniform("uFrame", floor(fc/120));
    if((fc%120)<60){
      sh.setUniform("uTransition", 0);
    }else{
      sh.setUniform("uTransition", (fc%60)/60);
    }
    sh.setUniform("uCount", IMG_COUNT);

    plane(IMG_WIDTH, IMG_HEIGHT);
  }

  draw = loopFunction;
}
```
　このコードではICSの記事に倣ってUint8Arrayを使っています、まず同じサイズのキャンバスを直に作り、そこに貼り付けたいイメージを貼って、getImageDataでImageDataオブジェクトを作り、その中のRGBAデータをはめ込む形で作成しています。こうするとp5のキャンバスが出てこないので、サムネが汚染されません。注意点としては、getImageDataの呼び出し回数が多い場合はwillReadFrequentlyをtrueにするのと....
```js
  // 複数回getImageDataを実行する場合はこれをtrueにする
  const ctx = cvs.getContext('2d', {willReadFrequently:true});
```
（参考：[willReadFrequently](https://developer.mozilla.org/ja/docs/Web/API/HTMLCanvasElement/getContext)）
Uint8Arrayの場合はプレ丸が使えないことですね。事前に切ります。
```js
  // Uint8Arrayの場合プレ丸が使えないので、一旦切る。
  gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);

  /* 略 */

  // 戻す。今回は不透明なので、特に気にする必要は無い。
  gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
```
サムネが復活しました。

![wcwcd2f23f3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437349/a6d56374-a145-45d8-8278-540d3479a4a5.png)

まあ、他にも死んでるのがあるんですが...[PLANE BACK FRONT
](https://openprocessing.org/sketch/2569859)

![wdcqwf2ff.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437349/927495da-f1e6-43c9-b37f-b943b39eb669.png)