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

# はじめに
　キューブ環境マッピングとは、キューブマップの内容を映り込みのようにテクスチャに簡易的に反映させる手法です。詳しくは、非常に参考になるh_doxasさんの次のサイトを参照してください：
　[キューブ環境マッピング](https://wgld.org/d/webgl/w044.html)
　これをbaseMaterialShaderによるシェーダーの改変で実行します。内容的には、前回のcubemapの回で作ったコードの改変なので、cubemapについての解説はしません：
　[p5.jsのbaseMaterialShaderを使ってcubemapを利用して背景を作る](https://qiita.com/inaba_darkfox/items/cdd506e2fccc55dbca99)
また、内容をちょっとだけ変更して、cubemapを構造体（struct）で扱っています。p5.jsは構造体については普通に使ってるので...まあ、慣れておいた方がいいかと思い、書き換えを実行しました。

## コード全文
　こちらです：[
cube environment mapping P5
](https://openprocessing.org/sketch/2473376)
```js
// cubemapping: https://wgld.org/d/webgl/w044.html
// 素材：https://www.humus.name/index.php?page=Textures&start=80
// の、Bridge2ですね。

// 2024-11-29
// modifyで完結するように追記

// 2024-12-03
// 構造体で整理
// 環境マッピング！参考：https://wgld.org/d/webgl/w044.html

/*
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/
*/

const skyCubeMaps = {};
let myShader;

// --------------------- preload ------------------- //
function preload(){
  skyCubeMaps.xp = loadImage("https://inaridarkfox4231.github.io/assets/cubeMaps/bridgeXP.jpg");
  skyCubeMaps.xn = loadImage("https://inaridarkfox4231.github.io/assets/cubeMaps/bridgeXN.jpg");
  skyCubeMaps.yp = loadImage("https://inaridarkfox4231.github.io/assets/cubeMaps/bridgeYP.jpg");
  skyCubeMaps.yn = loadImage("https://inaridarkfox4231.github.io/assets/cubeMaps/bridgeYN.jpg");
  skyCubeMaps.zp = loadImage("https://inaridarkfox4231.github.io/assets/cubeMaps/bridgeZP.jpg");
  skyCubeMaps.zn = loadImage("https://inaridarkfox4231.github.io/assets/cubeMaps/bridgeZN.jpg");
}

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

function draw() {
  orbitControl();
  background(0);

  shader(myShader);

  noLights();
  noStroke();
  setCubemapTexture()
  myShader.setUniform("uUseEnvMapping", false);
  sphere(2000);

  lights();
  specularMaterial(128);
  setCubemapTexture()
  myShader.setUniform("uUseEnvMapping", true);
  const cam = this._renderer._curCamera;
  myShader.setUniform("uEyePosition", [cam.eyeX, cam.eyeY, cam.eyeZ]);
  rotateY(frameCount*TAU/400);
  push();
  translate(200,0,0);
  rotateX(frameCount*TAU/360);
  rotateY(frameCount*TAU/540);
  torus(100, 30, 48);
  pop();
  translate(-200,0,0);
  setCubemapTexture();
  sphere(100);
}

function setCubemapTexture(){
  // 構造体uniformへのsettingはこうする
  myShader.setUniform("uTexture.xp", skyCubeMaps.xp);
  myShader.setUniform("uTexture.xn", skyCubeMaps.xn);
  myShader.setUniform("uTexture.yp", skyCubeMaps.yp);
  myShader.setUniform("uTexture.yn", skyCubeMaps.yn);
  myShader.setUniform("uTexture.zp", skyCubeMaps.zp);
  myShader.setUniform("uTexture.zn", skyCubeMaps.zn);
}

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

  // これでいいみたいですね。まあ仕方ないな。
  myShader = sh.modify({
    vertexDeclarations: `
      uniform mat4 uModelMatrix;
      OUT vec3 vLocalNormal;
      OUT vec3 vGlobalPosition;
      OUT vec3 vGlobalNormal;
    `,
    'void afterVertex':`(void){
      vec4 p = uModelMatrix * vec4(aPosition, 1.0);
      vGlobalPosition = p.xyz;
      vGlobalNormal = (inverse(transpose(uModelMatrix)) * vec4(aNormal, 0.0)).xyz;
      vLocalNormal = aNormal;
    }`,
    fragmentDeclarations: `
      uniform vec3 uEyePosition;
      uniform bool uUseEnvMapping;
      IN vec3 vLocalNormal;
      IN vec3 vGlobalPosition;
      IN vec3 vGlobalNormal;
      // 構造体を使おう
      struct cubemap {
        sampler2D xp;
        sampler2D xn;
        sampler2D yp;
        sampler2D yn;
        sampler2D zp;
        sampler2D zn;
      };
      // 構造体uniform
      uniform cubemap uTexture;
      vec4 getXP(sampler2D tex, in vec3 n){
        vec2 uv = n.zy/n.xx;
        uv.x = 0.5 - 0.5*uv.x;
        uv.y = 0.5 + 0.5*uv.y;
        return texture(tex, uv);
      }
      vec4 getXN(sampler2D tex, in vec3 n){
        vec2 uv = -n.zy/n.xx;
        uv = 0.5 + 0.5*uv;
        return texture(tex, uv);
      }
      vec4 getYP(sampler2D tex, in vec3 n){
        vec2 uv = -n.xz/n.yy;
        uv = 0.5 + 0.5*uv;
        return texture(tex, uv);
      }
      vec4 getYN(sampler2D tex, in vec3 n){
        vec2 uv = n.xz/n.yy;
        uv.x = 0.5 + 0.5*uv.x;
        uv.y = 0.5 - 0.5*uv.y;
        return texture(tex, uv);
      }
      vec4 getZP(sampler2D tex, in vec3 n){
        vec2 uv = n.xy/n.zz;
        uv = 0.5 + 0.5*uv;
        return texture(tex, uv);
      }
      vec4 getZN(sampler2D tex, in vec3 n){
        vec2 uv = -n.xy/n.zz;
        uv.x = 0.5 - 0.5*uv.x;
        uv.y = 0.5 + 0.5*uv.y;
        return texture(tex, uv);
      }
      vec4 getCubeTexture(cubemap texture, in vec3 normal){
        vec3 n = normalize(normal);
        // これを実行するとこっちと同じ見た目になる：
        // https://www.humus.name/index.php?page=Cubemap&item=Bridge2
        // cubemapは基本的に外から見た場合の割り当てなので
        // 中から見る分には鏡写しになる
        // 文字を置くと分かる
        // n.x=-n.x; // まあ背景に使う分には特に問題ないかと
        if(n.x >= abs(n.y) && n.x >= abs(n.z)){
          return getXP(texture.xp, n);
        }else if(n.x <= -abs(n.y) && n.x <= -abs(n.z)){
          return getXN(texture.xn, n);
        }else if(n.y <= -abs(n.x) && n.y <= -abs(n.z)){
          return getYP(texture.yp, n);
        }else if(n.y >= abs(n.x) && n.y >= abs(n.z)){
          return getYN(texture.yn, n);
        }else if(n.z >= abs(n.x) && n.z >= abs(n.y)){
          return getZP(texture.zp, n);
        }else if(n.z <= -abs(n.x) && n.z <= -abs(n.y)){
          return getZN(texture.zn, n);
        }
        return vec4(0.0);
      }
    `,
  'Inputs getPixelInputs':`(Inputs inputs){
    if(!uUseEnvMapping){
      return inputs;
    }
    vec3 ref = reflect(vGlobalPosition - uEyePosition, normalize(vGlobalNormal));
    vec4 color = getCubeTexture(uTexture, ref);
    inputs.color = color;
    inputs.ambientMaterial.rgb = color.rgb;
    return inputs;
  }`,
  'vec4 getFinalColor':`(vec4 color){
    // なお、ルーチン内部でuniform変数を使うのは基本的にあまりいいマナーでは
    // 無いと私は考えています。汎用性が下がるんで。
    if(!uUseEnvMapping){
      return getCubeTexture(uTexture, vLocalNormal);
    }
    return color;  
  }`
  });
}
```
実行結果：
![wfwf33rfwffw.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437349/9bd9fadd-f2d2-f04f-585d-7e48a9275f6a.png)

## 構造体
　構造体とは要するに複数の型をまとめたものです。C言語には普通に出てくるので...glslがもともとC言語ベースなので普通に持ってるわけですね（多分ね）。vec3でもsampler2Dでも何でもOKです。
```glsl
  // 構造体を使おう
  struct cubemap {
    sampler2D xp;
    sampler2D xn;
    sampler2D yp;
    sampler2D yn;
    sampler2D zp;
    sampler2D zn;
  };
  // 構造体uniform
  uniform cubemap uTexture;
```
cubemapの各面に対応するテクスチャをsampler2Dという構造体の形でまとめました。この方が分かりやすくていいですね！uniformも当然cubemapクラスですが、これsetUniformどうすんのと思うでしょう。簡単です。
```javascript
function setCubemapTexture(){
  // 構造体uniformへのsettingはこうする
  myShader.setUniform("uTexture.xp", skyCubeMaps.xp);
  myShader.setUniform("uTexture.xn", skyCubeMaps.xn);
  myShader.setUniform("uTexture.yp", skyCubeMaps.yp);
  myShader.setUniform("uTexture.yn", skyCubeMaps.yn);
  myShader.setUniform("uTexture.zp", skyCubeMaps.zp);
  myShader.setUniform("uTexture.zn", skyCubeMaps.zn);
}
```
このようにドットでアクセスするだけです。内部でそういうuniform変数として扱われるので、これでセットされます。ただp5は描画のたびにセットされたテクスチャを破棄しているので、これを適用するには描画のたびにこれをセットしなければなりません。そこで関数化して分離しました。
　ルーチンも構造体引数で書かれています：
```glsl
  vec4 getCubeTexture(cubemap texture, in vec3 normal){
    vec3 n = normalize(normal);
    // これを実行するとこっちと同じ見た目になる：
    // https://www.humus.name/index.php?page=Cubemap&item=Bridge2
    // cubemapは基本的に外から見た場合の割り当てなので
    // 中から見る分には鏡写しになる
    // 文字を置くと分かる
    // n.x=-n.x; // まあ背景に使う分には特に問題ないかと
    if(n.x >= abs(n.y) && n.x >= abs(n.z)){
      return getXP(texture.xp, n);
    }else if(n.x <= -abs(n.y) && n.x <= -abs(n.z)){
      return getXN(texture.xn, n);
    }else if(n.y <= -abs(n.x) && n.y <= -abs(n.z)){
      return getYP(texture.yp, n);
    }else if(n.y >= abs(n.x) && n.y >= abs(n.z)){
      return getYN(texture.yn, n);
    }else if(n.z >= abs(n.x) && n.z >= abs(n.y)){
      return getZP(texture.zp, n);
    }else if(n.z <= -abs(n.x) && n.z <= -abs(n.y)){
      return getZN(texture.zn, n);
    }
    return vec4(0.0);
  }
`,
```
## 環境マッピングの実装
　wgldに詳しく書かれているので詳細は省きますが、要はモデル変換を施した後の頂点、法線に対して、目線をそこで反射させ、球は十分大きいとしているので、反射したベクトルが中央から出ていると想定して、該当する方向のキューブマップテクスチャを持ってきているわけです。で、例によってモデル変換後の結果というのは取得できないので（「モデルだけ」をすっとばしてビュー掛けちゃってるので）、uniformでmodelMatrixを用意して計算結果をvaryingで渡しています。なおreflect関数は法線の正規化をしてくれないので正規化を怠ると普通にバグります。注意してください。
```glsl
    if(!uUseEnvMapping){
      return inputs;
    }
    vec3 ref = reflect(vGlobalPosition - uEyePosition, normalize(vGlobalNormal));
    vec4 color = getCubeTexture(uTexture, ref);
    inputs.color = color;
    inputs.ambientMaterial.rgb = color.rgb;
    return inputs;
```
uUseEnvMappingというフラグで、背景描画と分けています。理由はキューブマッピングが共通なので、別のshaderを作りたくなくて横着しているからです。なおライティングをしたかったのでambientMaterialのrgbも変更しています（以前の記事参照）。finalColorは逆にenvMappingの場合にそのまま返しています。
```glsl
    // なお、ルーチン内部でuniform変数を使うのは基本的にあまりいいマナーでは
    // 無いと私は考えています。汎用性が下がるんで。
    if(!uUseEnvMapping){
      return getCubeTexture(uTexture, vLocalNormal);
    }
    return color;  
```
## 描画
```javascript
function draw() {
  orbitControl();
  background(0);

  shader(myShader);

  noLights();
  noStroke();
  setCubemapTexture()
  myShader.setUniform("uUseEnvMapping", false);
  sphere(2000);

  lights();
  specularMaterial(128);
  setCubemapTexture()
  myShader.setUniform("uUseEnvMapping", true);
  const cam = this._renderer._curCamera;
  myShader.setUniform("uEyePosition", [cam.eyeX, cam.eyeY, cam.eyeZ]);
  rotateY(frameCount*TAU/400);
  push();
  translate(200,0,0);
  rotateX(frameCount*TAU/360);
  rotateY(frameCount*TAU/540);
  torus(100, 30, 48);
  pop();
  translate(-200,0,0);
  setCubemapTexture();
  sphere(100);
}
```
例の記事に倣って球とトーラスにしました。setCubemapTextureは描画のたびに呼び出しています。背景の球は以前と同じようにnoLight()で描画しています。カメラのeyePositionが必要なので_curCameraにアクセスしていますが、グローバルカメラを使う場合、その必要はありません。

# おわりに
　ここまでお読みいただいてありがとうございました。キューブマップが実装されれば、もっと楽に書けるようになる...かもしれません。
　
# 参考：#defineを使って処理を分ける
　参考までに、#defineを用いて目的ごとにshaderを生成するコードを記しておきます。この場合、フラグは必要がなくなります。shadertoyなどではこの手の手法がよく用いられています。自分はpavelさんの流体シミュレーションのコードで学びました（使うのは初めてですが...）。いちいちフラグを立てるのは面倒なので、知ってると便利かもしれないです。
<details>
<summary>#defineにより書き換えられたコード</summary>

```js
// cubemapping: https://wgld.org/d/webgl/w044.html
// 素材：https://www.humus.name/index.php?page=Textures&start=80
// の、Bridge2ですね。

// 2024-11-29
// modifyで完結するように追記

// 2024-12-03
// 構造体で整理
// 環境マッピング！参考：https://wgld.org/d/webgl/w044.html

// 気が向いたら#define使って複数のshaderを生成する形で書き直すかも？
// (pavelさんがやってる)
// できたみたい！

/*
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/
*/

const skyCubeMaps = {};

let cubemapBackgroundShader;
let envMappingShader;

// --------------------- preload ------------------- //
function preload(){
  skyCubeMaps.xp = loadImage("https://inaridarkfox4231.github.io/assets/cubeMaps/bridgeXP.jpg");
  skyCubeMaps.xn = loadImage("https://inaridarkfox4231.github.io/assets/cubeMaps/bridgeXN.jpg");
  skyCubeMaps.yp = loadImage("https://inaridarkfox4231.github.io/assets/cubeMaps/bridgeYP.jpg");
  skyCubeMaps.yn = loadImage("https://inaridarkfox4231.github.io/assets/cubeMaps/bridgeYN.jpg");
  skyCubeMaps.zp = loadImage("https://inaridarkfox4231.github.io/assets/cubeMaps/bridgeZP.jpg");
  skyCubeMaps.zn = loadImage("https://inaridarkfox4231.github.io/assets/cubeMaps/bridgeZN.jpg");
}

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

  cubemapBackgroundShader = createCubemapShader(false);
  envMappingShader = createCubemapShader(true);
}

function draw() {
  orbitControl();
  background(0);

  shader(cubemapBackgroundShader);

  noLights();
  noStroke();
  setCubemapTexture(cubemapBackgroundShader)
  sphere(2000);

  shader(envMappingShader);

  lights();
  specularMaterial(128);
  const cam = this._renderer._curCamera;
  envMappingShader.setUniform("uEyePosition", [cam.eyeX, cam.eyeY, cam.eyeZ]);

  rotateY(frameCount*TAU/400);
  push();
  translate(200,0,0);
  rotateX(frameCount*TAU/360);
  rotateY(frameCount*TAU/540);
  setCubemapTexture(envMappingShader);
  torus(100, 30, 48);
  pop();
  translate(-200,0,0);
  setCubemapTexture(envMappingShader);
  sphere(100);
}

function setCubemapTexture(sh){
  // 構造体uniformへのsettingはこうする
  sh.setUniform("uTexture.xp", skyCubeMaps.xp);
  sh.setUniform("uTexture.xn", skyCubeMaps.xn);
  sh.setUniform("uTexture.yp", skyCubeMaps.yp);
  sh.setUniform("uTexture.yn", skyCubeMaps.yn);
  sh.setUniform("uTexture.zp", skyCubeMaps.zp);
  sh.setUniform("uTexture.zn", skyCubeMaps.zn);
}

function createCubemapShader(envmapping = false){
  // envmappingかどうかで処理をフラグで分ける
  // それぞれの場合にshaderを生成する

  return baseMaterialShader().modify({
    vertexDeclarations: `
      uniform mat4 uModelMatrix;
      OUT vec3 vLocalNormal;
      OUT vec3 vGlobalPosition;
      OUT vec3 vGlobalNormal;
    `,
    'void afterVertex':`(void){
      vec4 p = uModelMatrix * vec4(aPosition, 1.0);
      vGlobalPosition = p.xyz;
      vGlobalNormal = (inverse(transpose(uModelMatrix)) * vec4(aNormal, 0.0)).xyz;
      vLocalNormal = aNormal;
    }`,
    fragmentDeclarations: `
      ${(envmapping ? '#define ENV_MAPPING' : '')}
      uniform vec3 uEyePosition;
      IN vec3 vLocalNormal;
      IN vec3 vGlobalPosition;
      IN vec3 vGlobalNormal;
      // 構造体を使おう
      struct cubemap {
        sampler2D xp;
        sampler2D xn;
        sampler2D yp;
        sampler2D yn;
        sampler2D zp;
        sampler2D zn;
      };
      // 構造体uniform
      uniform cubemap uTexture;
      vec4 getXP(sampler2D tex, in vec3 n){
        vec2 uv = n.zy/n.xx;
        uv.x = 0.5 - 0.5*uv.x;
        uv.y = 0.5 + 0.5*uv.y;
        return texture(tex, uv);
      }
      vec4 getXN(sampler2D tex, in vec3 n){
        vec2 uv = -n.zy/n.xx;
        uv = 0.5 + 0.5*uv;
        return texture(tex, uv);
      }
      vec4 getYP(sampler2D tex, in vec3 n){
        vec2 uv = -n.xz/n.yy;
        uv = 0.5 + 0.5*uv;
        return texture(tex, uv);
      }
      vec4 getYN(sampler2D tex, in vec3 n){
        vec2 uv = n.xz/n.yy;
        uv.x = 0.5 + 0.5*uv.x;
        uv.y = 0.5 - 0.5*uv.y;
        return texture(tex, uv);
      }
      vec4 getZP(sampler2D tex, in vec3 n){
        vec2 uv = n.xy/n.zz;
        uv = 0.5 + 0.5*uv;
        return texture(tex, uv);
      }
      vec4 getZN(sampler2D tex, in vec3 n){
        vec2 uv = -n.xy/n.zz;
        uv.x = 0.5 - 0.5*uv.x;
        uv.y = 0.5 + 0.5*uv.y;
        return texture(tex, uv);
      }
      vec4 getCubeTexture(cubemap texture, in vec3 normal){
        vec3 n = normalize(normal);
        // これを実行するとこっちと同じ見た目になる：
        // https://www.humus.name/index.php?page=Cubemap&item=Bridge2
        // cubemapは基本的に外から見た場合の割り当てなので
        // 中から見る分には鏡写しになる
        // 文字を置くと分かる
        // n.x=-n.x; // まあ背景に使う分には特に問題ないかと
        if(n.x >= abs(n.y) && n.x >= abs(n.z)){
          return getXP(texture.xp, n);
        }else if(n.x <= -abs(n.y) && n.x <= -abs(n.z)){
          return getXN(texture.xn, n);
        }else if(n.y <= -abs(n.x) && n.y <= -abs(n.z)){
          return getYP(texture.yp, n);
        }else if(n.y >= abs(n.x) && n.y >= abs(n.z)){
          return getYN(texture.yn, n);
        }else if(n.z >= abs(n.x) && n.z >= abs(n.y)){
          return getZP(texture.zp, n);
        }else if(n.z <= -abs(n.x) && n.z <= -abs(n.y)){
          return getZN(texture.zn, n);
        }
        return vec4(0.0);
      }
    `,
  'Inputs getPixelInputs':`(Inputs inputs){
    #ifdef ENV_MAPPING
      vec3 ref = reflect(vGlobalPosition - uEyePosition, normalize(vGlobalNormal));
      vec4 color = getCubeTexture(uTexture, ref);
      inputs.color = color;
      inputs.ambientMaterial.rgb = color.rgb;
      return inputs;
    #else
      return inputs;
    #endif
  }`,
  'vec4 getFinalColor':`(vec4 color){
    // なお、ルーチン内部でuniform変数を使うのは基本的にあまりいいマナーでは
    // 無いと私は考えています。汎用性が下がるんで。
    #ifdef ENV_MAPPING
      return color;
    #else
      return getCubeTexture(uTexture, vLocalNormal);
    #endif
  }`
  });
}
```
</details>

条件コンパイルについては次の記事が参考になるようです。
　[C言語 ifdef 【プログラムをカットする技術と使い方を紹介】](https://monozukuri-c.com/langc-ifdef/)

ここで紹介した使い方が正しいかどうかは分かんないです。フラグ分岐がちょっと面倒だったので練習のために利用しました。