p5.jsのbaseMaterialShader()を使ってmirror表現を実装する

# はじめに
　p5.jsの1.11.1を使います。baseMaterialShader()という関数があります。
　[baseMaterialShader()](https://p5js.org/reference/p5/baseMaterialShader/)
　この関数を使うとライティングに使うシェーダーを改変することができます。今回はこれを使ってミラー描画を実行してみたいと思います。これはいわゆる鏡写しですね。
![wdfef3eef.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437349/f2e49b0d-97f3-a315-9e17-9fdc7aa72070.png)
　baseMaterialShaderの使い方がなんとなくわかってきたので、大幅に書き換えを実行しました。

## 方針
　あらかじめcreateGraphics()でキャンバスを作っておきます。
　最初に準備描画をします。その際、描画位置をxy平面で反転させます。法線の向きも逆にします。ライティングの向きも逆にします。
　次にその時点での描画結果をキャンバスに焼き、元のキャンバスは破棄します。
　そのうえでさっき描画した内容を平面に焼きます。
　最後に、反転を考えずにさっきのオブジェクトを描画します。

## コード全文
コード：[projTexture Mirror p5](https://openprocessing.org/sketch/2464906)
```javascript
/*
2024-11-26
一応助けました。形だけ。
*/

let mirrorShader;
let projTexShader;

let mirrorGraphic;

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

  createMirrorShader();
  createProjTexShader();

  camera(100,100,100,0,0,0,0,0,-1);
  perspective(PI/3, 1, 10, 2000);

  noStroke();

  mirrorGraphic = createGraphics(600, 600);
}

function draw() {
  orbitControl();

  // ミラー用の描画を実行する
  clear();
  shader(mirrorShader);
  push();
  setLight(true);
  drawFigures(true);
  pop();

  // キャンバスの内容を保存したうえで一度クリアする
  mirrorGraphic.clear();
  mirrorGraphic.image(this._renderer, 0, 0);
  clear();
  background(255);

  // ミラーの平面を描画する
  shader(projTexShader);
  setLight(false);
  projTexShader.setUniform("uReflectionCoeff", 0.45);
  texture(mirrorGraphic);
  push();
  plane(300);
  pop();

  // 改めて本来の描画
  shader(mirrorShader);
  push();
  drawFigures(false);
  pop();
}

function setLight(mirror){
  directionalLight(128,128,128, -2, -2, (mirror ? 1 : -1));
  ambientLight(128);
  specularMaterial(64);
}

function drawFigures(mirror){
  mirrorShader.setUniform("uMirror", mirror);

  fill("teal");
  for(let k=0; k<6; k++){
    const t = frameCount/60 + TAU*k/6;
    translate(60*cos(t), 60*sin(t), 30);
    rotateX(noise(0, k)*100 + frameCount*TAU/240);
    rotateY(noise(k, 0)*100 + frameCount*TAU/120);
    torus(20, 5, 48);
    resetMatrix();
  }
  fill("orange");
  translate(0, 0, 60 + 20 * cos(frameCount*TAU/180));
  sphere(15, 48);
}

function createMirrorShader(){

  mirrorShader = baseMaterialShader().modify({
    vertexDeclarations:`
      uniform mat4 uModelMatrix;
      uniform bool uMirror;
    `,
    'vec3 getLocalPosition':`(vec3 position){
      position = (uModelMatrix * vec4(position, 1.0)).xyz;
      if(uMirror) position.z = -position.z;
      return (inverse(uModelMatrix) * vec4(position, 1.0)).xyz;
    }`,
    'vec3 getLocalNormal':`(vec3 normal){
      normal = uNormalMatrix * normal;
      if(uMirror) normal.z = -normal.z;
      return inverse(uNormalMatrix) * normal;
    }`
  });
}

function createProjTexShader(){
  
  projTexShader = baseMaterialShader().modify({
    vertexDeclarations:`
      vec4 mvp;
      OUT vec4 vProjTexCoord;
    `,
    fragmentDeclarations:`
      vec4 myTexColor;
      IN vec4 vProjTexCoord;
      uniform float uReflectionCoeff;
    `,
    'vec3 getWorldPosition':`(vec3 position){
      mvp = uProjectionMatrix * vec4(position, 1.0);
      return position;
    }`,
    'void afterVertex':`(){
      mvp.x = 0.5 * mvp.w + 0.5 * mvp.x;
      mvp.y = 0.5 * mvp.w - 0.5 * mvp.y;
      vProjTexCoord = mvp;
    }`,
    'void beforeFragment':`(){
      myTexColor = vec4(vec3(0.1,0.45,0.9), 1.0);
      if (!gl_FrontFacing) {
        vec4 mirrorTexColor = textureProj(uSampler, vProjTexCoord);
        myTexColor = myTexColor * (1.0 - uReflectionCoeff) + mirrorTexColor * uReflectionCoeff;
      }
    }`,
    'Inputs getPixelInputs': `(Inputs inputs) {
      inputs.color = myTexColor;
      // ambientMaterial()を呼び出している場合、ここはinputs.colorの色が反映
      // されないので、同時にこっちもいじらないと不適切な描画結果になる。
      inputs.ambientMaterial.rgb = myTexColor.rgb;
      // まあ、shader内部見ないとわからんわ、これは。
      return inputs;
    }`
  });
}
```

## カメラ設定
　z軸負方向が上になるよう設定します。
```javascript
  camera(100,100,100,0,0,0,0,0,-1);
  perspective(PI/3, 1, 10, 2000);
```

## ミラーシェーダー
　ミラー用のシェーダーを作る部分。
```javascript
  mirrorShader = baseMaterialShader().modify({
    vertexDeclarations:`
      uniform mat4 uModelMatrix;
      uniform bool uMirror;
    `,
    'vec3 getLocalPosition':`(vec3 position){
      position = (uModelMatrix * vec4(position, 1.0)).xyz;
      if(uMirror) position.z = -position.z;
      return (inverse(uModelMatrix) * vec4(position, 1.0)).xyz;
    }`,
    'vec3 getLocalNormal':`(vec3 normal){
      normal = uNormalMatrix * normal;
      if(uMirror) normal.z = -normal.z;
      return inverse(uNormalMatrix) * normal;
    }`
  });
```
　uniform変数や配列を用意するにはvertexDeclarationsを用意します。ここで文字列として用意するとヘッダー扱いになるらしいです。もちろん、バーテックスシェーダーの、です。フラグメントとは役割が違うので。共通のヘッダーなどという状況はあまり考えられません（違和感しかないです）。明示すべきでしょう。
　uModelMatrixは用意されてはいますがシェーダー内には準備されてないので書き換えで追加します。またboolのuMirrorが欲しいので追加します。
　localPositionのところですが、まずモデル行列でグローバル座標にしたあとで z を反転させ、再びアトリビュートに戻す処理を実行しています。法線も同様です。ビューとモデルを分離できないので、こうするしかないです。

## 射影テクスチャシェーダー
　射影テクスチャマッピングで平面を描画するパート。その前に...
```javascript
  // キャンバスの内容を保存したうえで一度クリアする
  mirrorGraphic.clear();
  mirrorGraphic.image(this._renderer, 0, 0);
  clear();
  background(255);
```
mirrorGraphicとしてあらかじめ用意しておいたグラフィックにその時点での描画内容を焼きます。焼いたら現在のキャンバスは破棄して白でクリアします。え？フレームバッファを使えって？
　ごめんなさい。
　（フレームバッファを使わない理由は中と外で同じカメラを使うのが異常なまでに困難を伴うからです。詳しくは[こちらの記事](https://qiita.com/inaba_darkfox/items/0311dbdb9613ff251c98)を参照してください。）
　次に、シェーダーを作るところを説明します。
```javascript
  projTexShader = baseMaterialShader().modify({
    vertexDeclarations:`
      vec4 mvp;
      OUT vec4 vProjTexCoord;
    `,
    fragmentDeclarations:`
      vec4 myTexColor;
      IN vec4 vProjTexCoord;
      uniform float uReflectionCoeff;
    `,
    'vec3 getWorldPosition':`(vec3 position){
      mvp = uProjectionMatrix * vec4(position, 1.0);
      return position;
    }`,
    'void afterVertex':`(){
      mvp.x = 0.5 * mvp.w + 0.5 * mvp.x;
      mvp.y = 0.5 * mvp.w - 0.5 * mvp.y;
      vProjTexCoord = mvp;
    }`,
    'void beforeFragment':`(){
      myTexColor = vec4(vec3(0.1,0.45,0.9), 1.0);
      if (!gl_FrontFacing) {
        vec4 mirrorTexColor = textureProj(uSampler, vProjTexCoord);
        myTexColor = myTexColor * (1.0 - uReflectionCoeff) + mirrorTexColor * uReflectionCoeff;
      }
    }`,
    'Inputs getPixelInputs': `(Inputs inputs) {
      inputs.color = myTexColor;
      // ambientMaterial()を呼び出している場合、ここはinputs.colorの色が反映
      // されないので、同時にこっちもいじらないと不適切な描画結果になる。
      inputs.ambientMaterial.rgb = myTexColor.rgb;
      // まあ、shader内部見ないとわからんわ、これは。
      return inputs;
    }`
  });
```
　vertexDeclarationsを使ってグローバルのmvpとvaryingのuProjTexCoordを用意します。varyingなので、フラグメントシェーダサイドでも同じように用意する必要があります。webgl2なので、OUTとINで対にします。この辺は勉強してくださいとしか言いようがないですね...基本なので。詳しくは：
　[WebGL2 contents](https://wgld.org/d/webgl2/)
を参照のこと。
　射影テクスチャマッピングの詳しい解説はいつもお世話になっているこちらのサイトに譲ります：
　[射影テクスチャマッピング](https://wgld.org/d/webgl/w049.html)
　そういう感じです。FrontFacingはいわゆるカリングの状態によって処理を分けるために用いています。これがtrueになるのはポリゴンが正の向きの場合です。ところでコード内でこれがfalseのときに鏡の描画をするようになっていますね。なぜかというとp5.jsは射影行列に-1を入れて、y軸下向きの座標系を採用しているからです（Processingも同様、あちらは原点もいじっていますね...）。つまり通常（のwebglライブラリ）と向きが逆なので、それを考慮しています。まあp5.jsはwebgl特化じゃ無いので...
　inputsを修正するところでcolorだけいじろうと思ったら変な結果になったのでambientMaterialのrgbもいじったらそれっぽくなりました。この辺はよくわかんないんですが、おそらくambientMaterial()関連の複雑な処理をしているのでしょう。

## ミラーを考慮した描画準備
こんな感じで、ミラーかどうかで処理を分けています。
```javascript
function setLight(mirror){
  directionalLight(128,128,128, -2, -2, (mirror ? 1 : -1));
  ambientLight(128);
  specularMaterial(64);
}

function drawFigures(mirror){
  mirrorShader.setUniform("uMirror", mirror);

  fill("teal");
  for(let k=0; k<6; k++){
    const t = frameCount/60 + TAU*k/6;
    translate(60*cos(t), 60*sin(t), 30);
    rotateX(noise(0, k)*100 + frameCount*TAU/240);
    rotateY(noise(k, 0)*100 + frameCount*TAU/120);
    torus(20, 5, 48);
    resetMatrix();
  }
  fill("orange");
  translate(0, 0, 60 + 20 * cos(frameCount*TAU/180));
  sphere(15, 48);
}
```
uMirrorにtrue/falseを入れれば、あとはシェーダーがよろしくやってくれます。

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