p5.jsのbaseMaterialShaderを使ってキューブマッピング

# はじめに
　p5.jsでキューブマップを使う方法。使うのはbaseMaterialShader. 今回ライティングはしない。

https://p5js.org/reference/p5/baseMaterialShader/

cubemapの設定方法はここが参考になる。

https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/texImage2D

　以前、力業でむりやりキューブマッピングをしたが、ぶっちゃけ直接キューブマップの本来の流儀でやる方が圧倒的に楽だったりする。それでもこれがうまくいくのは間違いなくbaseMaterialShaderの枠組みのおかげ。いい時代になったね。

## コード全文
　[p5 cubemap 2](https://openprocessing.org/sketch/2564682)
```js
// cubemapping: https://wgld.org/d/webgl/w044.html
// 素材：https://www.humus.name/index.php?page=Textures&start=80
// の、Bridge2ですね。

/*
Author
======

This is the work of Emil Persson, aka Humus.
http://www.humus.name

License
=======

This work is licensed under a Creative Commons Attribution 3.0 Unported License.
http://creativecommons.org/licenses/by/3.0/
*/

let myShader;

// --------------------- preload ------------------- //
const skyCubemaps = {};
// cubemapping: https://wgld.org/d/webgl/w044.html
// 素材：https://www.humus.name/index.php?page=Textures&start=80
// の、Bridge2ですね。
function preload(){
  skyCubemaps.px = loadImage("https://inaridarkfox4231.github.io/assets/cubeMaps/bridgeXP.jpg");
  skyCubemaps.nx = loadImage("https://inaridarkfox4231.github.io/assets/cubeMaps/bridgeXN.jpg");
  skyCubemaps.py = loadImage("https://inaridarkfox4231.github.io/assets/cubeMaps/bridgeYP.jpg");
  skyCubemaps.ny = loadImage("https://inaridarkfox4231.github.io/assets/cubeMaps/bridgeYN.jpg");
  skyCubemaps.pz = loadImage("https://inaridarkfox4231.github.io/assets/cubeMaps/bridgeZP.jpg");
  skyCubemaps.nz = loadImage("https://inaridarkfox4231.github.io/assets/cubeMaps/bridgeZN.jpg");
}

function setup() {
  createCanvas(window.innerWidth, window.innerHeight, WEBGL);
  createCubemapShader();

  const gl = this._renderer.GL;

  registCubemap(gl, 3, [
    skyCubemaps.px.canvas, skyCubemaps.nx.canvas,
    skyCubemaps.py.canvas, skyCubemaps.ny.canvas,
    skyCubemaps.pz.canvas, skyCubemaps.nz.canvas
  ]);
  registCubemap(gl, 4, getCubemapTextures());

  // 一応0に戻しておく
  gl.activeTexture(gl.TEXTURE0);
}

function registCubemap(gl, index, srcArray){
  const tex = gl.createTexture();
  // 3番に入れる
  gl.activeTexture(gl.TEXTURE0 + index);
  gl.bindTexture(gl.TEXTURE_CUBE_MAP, tex);

  const cubeTargets = [
    gl.TEXTURE_CUBE_MAP_POSITIVE_X, gl.TEXTURE_CUBE_MAP_NEGATIVE_X,
    gl.TEXTURE_CUBE_MAP_POSITIVE_Y, gl.TEXTURE_CUBE_MAP_NEGATIVE_Y,
    gl.TEXTURE_CUBE_MAP_POSITIVE_Z, gl.TEXTURE_CUBE_MAP_NEGATIVE_Z
  ];

  for(let i=0; i<6; i++){
    const src = srcArray[i];
    gl.texImage2D(
      cubeTargets[i], 0, gl.RGBA, src.width, src.height, 0,
      gl.RGBA, gl.UNSIGNED_BYTE, src
    );
  }
  gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
  gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
}

function draw() {
  orbitControl();
  background(0);
  noStroke(); // これが無いとstrokeのshaderで上書きされてしまう

  shader(myShader);
  const gl = this._renderer.GL;
  const pg = myShader._glProgram;
  gl.useProgram(pg);
  const loc = gl.getUniformLocation(pg, "uCubemap");

  // 3番に入れる。
  gl.uniform1i(loc, 3);
  sphere(2000);

  gl.uniform1i(loc, 4);
  box(400);
}

function createCubemapShader(){
  const sh = baseMaterialShader();

  // これでいいみたいですね。まあ仕方ないな。
  myShader = sh.modify({
    vertexDeclarations: `OUT vec3 vLocalPosition;`,
    fragmentDeclarations: `
      IN vec3 vLocalPosition;
      uniform samplerCube uCubemap;
    `,
  'void afterVertex':`(){vLocalPosition = aPosition;}`,
  'vec4 getFinalColor':`(vec4 color){
      return texture(uCubemap, -vLocalPosition);
    }`
  });
}

function getCubemapTextures(){
  const grs = [];
  const texts = ["1","6","2","5","3","4"];
  for(let i=0; i<6; i++){
    const gr = createGraphics(400, 400);
    gr.textAlign(CENTER,CENTER).textStyle(ITALIC).textSize(340);
    gr.background(255);
    gr.fill(0);gr.noStroke();
    gr.text(texts[i],200,200);
    grs.push(gr.elt);
  }
  return grs;
}
```
使うのはこちらのリソースと

https://www.humus.name/index.php?page=Textures&start=80

自前で作った2Dcanvasの画像群である。

実行結果：
![wdrwrcf3frf34f4g.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437349/f6fc509f-a4a8-4519-bfac-ddffe202e47c.png)

## キューブマップの仕組み
　6枚の画像を次のようにして貼り合わせている。仕組みとしてはp5と座標軸の向きが逆になっている（$y$が上、$x$が右、$z$が手前）。それは取得時に法線の向きにマイナスを付けて応急処置で乗り切っている。カメラをいじってもいいが、面倒。デフォルトではちゃんと$z$軸が手前になっているので、法線を逆向きにした方が手っ取り早い。

![cubemap.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437349/e0be27a8-537c-4689-b3ef-b854f59bc2e5.png)

## 登録場所
　テクスチャの仕組みに関する知識は割愛する。

https://qiita.com/inaba_darkfox/items/d64102c28653d3ff3ee0

スロットは3番と4番を使う。フィルシェーダーは0,1,2が予約済みであり、常に何かしら埋まっていて、同じスロットからは1種類しか取得できないので、3と4を使うしかない。そこで3と4のCUBE_MAP枠に使いたいテクスチャを登録することにする。

## 登録方法
```js
function registCubemap(gl, index, srcArray){
  const tex = gl.createTexture();
  // 3番に入れる
  gl.activeTexture(gl.TEXTURE0 + index);
  gl.bindTexture(gl.TEXTURE_CUBE_MAP, tex);

  const cubeTargets = [
    gl.TEXTURE_CUBE_MAP_POSITIVE_X, gl.TEXTURE_CUBE_MAP_NEGATIVE_X,
    gl.TEXTURE_CUBE_MAP_POSITIVE_Y, gl.TEXTURE_CUBE_MAP_NEGATIVE_Y,
    gl.TEXTURE_CUBE_MAP_POSITIVE_Z, gl.TEXTURE_CUBE_MAP_NEGATIVE_Z
  ];

  for(let i=0; i<6; i++){
    const src = srcArray[i];
    gl.texImage2D(
      cubeTargets[i], 0, gl.RGBA, src.width, src.height, 0,
      gl.RGBA, gl.UNSIGNED_BYTE, src
    );
  }
  gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
  gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
}
```
　ソースはあらかじめテクスチャに登録できるようにキャンバス要素の配列としておき、px,nx,py,ny,pz,nzの順番で並べておく。キューブマップを登録するときのポイントは、2Dとは違って、texImage2Dの第一引数をそこに置きたい画像の向きにするところ。上の図にあるような感じで対応を作る。ソースのwidthとheightを使うのがポイント。なお、フィルターは個別に設定することはできず、全部まとめての指定となる。フィルター設定しないと画像が適用されないのもいつも通り。

　ロード画像の場合、canvasでアクセスすると要素になる。
```js
  registCubemap(gl, 3, [
    skyCubemaps.px.canvas, skyCubemaps.nx.canvas,
    skyCubemaps.py.canvas, skyCubemaps.ny.canvas,
    skyCubemaps.pz.canvas, skyCubemaps.nz.canvas
  ]);
```
createGraphicsで作る場合は、eltでアクセスできる。こっちはサイコロ画像とする。
```js
function getCubemapTextures(){
  const grs = [];
  const texts = ["1","6","2","5","3","4"];
  for(let i=0; i<6; i++){
    const gr = createGraphics(400, 400);
    gr.textAlign(CENTER,CENTER).textStyle(ITALIC).textSize(340);
    gr.background(255);
    gr.fill(0);gr.noStroke();
    gr.text(texts[i],200,200);
    grs.push(gr.elt);
  }
  return grs;
}
```
配列として出力する際にもうeltで要素にしてしまうと手っ取り早い。

## キューブマップシェーダー
　シェーダーはbaseMaterialShaderで作る。
```js
function createCubemapShader(){
  const sh = baseMaterialShader();

  // これでいいみたいですね。まあ仕方ないな。
  myShader = sh.modify({
    vertexDeclarations: `OUT vec3 vLocalPosition;`,
    fragmentDeclarations: `
      IN vec3 vLocalPosition;
      uniform samplerCube uCubemap;
    `,
  'void afterVertex':`(){vLocalPosition = aPosition;}`,
  'vec4 getFinalColor':`(vec4 color){
      return texture(uCubemap, -vLocalPosition);
    }`
  });
}
```
　p5のcubeは法線が面の向きになっておりそのままではキューブマップを適用できない。今回適用するのはキューブと球だけなので、もうローカルポジションで採取してしまおう。トーラスとかだと法線じゃないとまずいが、今回は使わないので。
　マイナスを付けて逆転させる。なお今回カリングも考慮しない。

## ロケーションと登録処理
　前にも言ったがshader関数はプログラムを走らせない。なので先に走らせてしまわないとユニフォームを登録できない。そこでそのようにする。
　またコメントにも書いたがnoStroke()を実行しないと余計なシェーダーが走ってしまう。なので切ってある。
```js
function draw() {
  orbitControl();
  background(0);
  noStroke(); // これが無いとstrokeのshaderで上書きされてしまう

  shader(myShader);
  const gl = this._renderer.GL;
  const pg = myShader._glProgram;
  gl.useProgram(pg);
  const loc = gl.getUniformLocation(pg, "uCubemap");

  // 3番に入れる。
  gl.uniform1i(loc, 3);
  sphere(2000);

  gl.uniform1i(loc, 4);
  box(400);
}
```
　球は3番で、立方体は4番で。もう3と4に登録済みなので、あとは3と4をきちんとロケーションに置けば、samplerCubeタイプであることからシェーダーがよろしくやってくれる。お疲れ様でした。

## おわりに
　キューブマップの仕組みはp5の逆座標系と相性が悪い。どうやって実装するつもりなんだろう。まあでも何らかの方法でよろしくやるつもりなんでしょう。

　ここまでお読みいただいてありがとうございました。

## 補足：mipmap
　キューブマップのように3Dでテクスチャを扱う場合、見え方によってはギザギザが目立つ場合がある。この記事の場合もそう。角度を急にするとそれっぽくなる。

![wf3r33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437349/25602dc8-cd7d-4b45-988d-42070892030b.png)

　上面の「２」がギザギザになっている。これを解消するにはmipmapを使ってフィルタリングの精度を高めるといいらしい。
　[mipmap compare](https://openprocessing.org/sketch/2564964)
```js
// MIPMAPってこんな違うの？？

function setup() {
  createCanvas(window.innerWidth, window.innerHeight, WEBGL);
  createCubemapShader();

  const gl = this._renderer.GL;
  const cubemapSources = getCubemapSources();

  registCubemap(gl, 3, cubemapSources);
  registCubemap(gl, 4, cubemapSources, true);

  // 一応0に戻しておく
  gl.activeTexture(gl.TEXTURE0);
}

function registCubemap(gl, index, srcArray, mipmap = false){
  const tex = gl.createTexture();
  // 3番に入れる
  gl.activeTexture(gl.TEXTURE0 + index);
  gl.bindTexture(gl.TEXTURE_CUBE_MAP, tex);

  const cubeTargets = [
    gl.TEXTURE_CUBE_MAP_POSITIVE_X, gl.TEXTURE_CUBE_MAP_NEGATIVE_X,
    gl.TEXTURE_CUBE_MAP_POSITIVE_Y, gl.TEXTURE_CUBE_MAP_NEGATIVE_Y,
    gl.TEXTURE_CUBE_MAP_POSITIVE_Z, gl.TEXTURE_CUBE_MAP_NEGATIVE_Z
  ];

  for(let i=0; i<6; i++){
    const src = srcArray[i];
    gl.texImage2D(
      cubeTargets[i], 0, gl.RGBA, src.width, src.height, 0,
      gl.RGBA, gl.UNSIGNED_BYTE, src
    );
  }

  if (mipmap) {
    gl.generateMipmap(gl.TEXTURE_CUBE_MAP);
    gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
    gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
  }else{
    gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
    gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
  }
}

function draw() {
  orbitControl();
  background(0);
  noStroke(); // これが無いとstrokeのshaderで上書きされてしまう

  shader(myShader);
  const gl = this._renderer.GL;
  const pg = myShader._glProgram;
  gl.useProgram(pg);
  const loc = gl.getUniformLocation(pg, "uCubemap");

  gl.uniform1i(loc, 3);
  myShader.setUniform("uTintColor", [0.1, 0.3, 0.8]); // mipmap無し：ブルー
  translate(200, 0, 0);
  box(200);

  gl.uniform1i(loc, 4);
  myShader.setUniform("uTintColor", [0.8, 0.3, 0.1]); // mipmapあり：オレンジ
  translate(-400, 0, 0);
  box(200);
}

function createCubemapShader(){
  const sh = baseMaterialShader();

  myShader = sh.modify({
    vertexDeclarations: `OUT vec3 vLocalPosition;`,
    fragmentDeclarations: `
      IN vec3 vLocalPosition;
      uniform samplerCube uCubemap; 
      uniform vec3 uTintColor;
    `,
  'void afterVertex':`(){vLocalPosition = aPosition;}`,
  'vec4 getFinalColor':`(vec4 color){
      vec4 col = texture(uCubemap, -vLocalPosition);
      col.rgb *= uTintColor;
      return col;
    }`
  });
}

function getCubemapSources(){
  const grs = [];
  const texts = ["鯱","鮫","鮪","鰐","鱸","鯖"];
  for(let i=0; i<6; i++){
    const gr = createGraphics(512, 512);
    gr.textAlign(CENTER,CENTER).textStyle(ITALIC).textSize(480);
    gr.background(255);
    gr.fill(0);gr.noStroke();
    gr.text(texts[i],256,256);
    grs.push(gr.elt);
  }
  return grs;
}
```
実行結果：

![rget4t44g.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437349/d72ca613-0d05-41d4-9f19-71212ae71a73.png)

　青い方がmipmap無しで、オレンジの方がmipmapあり。使うには、バインドしたターゲットに対して（これもすべての面が一律に対象）、generateMipmapを実行する。これは元の画像より小さくした画像を別に用意することで、精度を高める方法である（詳しくは知らないけど）。フィルターはLINEARではなくLINEAR_MIPMAP_LINEARを使う。ただしMINの方だけ。小さくなる場合の処理なのでMAGは関係ない。
```js
  if (mipmap) {
    gl.generateMipmap(gl.TEXTURE_CUBE_MAP);
    gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
    gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
  }else{
    gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
    gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
  }
```
　3番と4番にそれぞれmipmap無し、mipmapありを登録してそれぞれ描画している。分かりやすさのために色を付けた。見ると分かるが急な角度では青い方はギザギザが目立っているが、オレンジの方は境界がボケて滑らかになっている。そういうわけで、気になる場合はmipmapを使いましょう。