p5.jsのbaseMaterialShader()を使って射影テクスチャマッピングを実装する

# はじめに
　射影テクスチャマッピングとはカメラから見た映像をオブジェクトに貼り付ける処理です。詳しくは：
　[射影テクスチャマッピング](https://wgld.org/d/webgl/w049.html)
p5.jsには最近baseMaterialShader()という、シェーダーを改変するための関数が実装されました。
　[baseMaterialShader()](https://p5js.org/reference/p5/baseMaterialShader/)
これを使って、射影テクスチャマッピングを実装します。
　baseMaterialShader()の使い方が分かってきたので、当初から大幅に更新しています。

## コード全文
コードは割と短いです。
　[projTextureMapping p5](https://openprocessing.org/sketch/2465182)
```javascript
let camProj;

let shippei;

let myShader;

function preload(){
  shippei = loadImage("https://inaridarkfox4231.github.io/assets/texture/Shippei.jpg");
}

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

  camProj = createCamera();
  camProj.camera(600, 0, 0, 0, 0, 0, 0, 1, 0);
  camProj.perspective(PI/7, 1, 20, 2000);

  const cam = createCamera();
  cam.camera(0, 0, 300*sqrt(3),0,0,0,0,1,0);
  cam.perspective(PI/3, width/height, 30*sqrt(3),3000*sqrt(3));

  createProjShader();
}

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

  myShader.setUniform("uCameraMatrix", camProj.cameraMatrix.mat4);
  myShader.setUniform("uProjMatrix", camProj.projMatrix.mat4);
  myShader.setUniform("uTextureProj", shippei);
  myShader.setUniform("uEye", [camProj.eyeX, camProj.eyeY, camProj.eyeZ]);

  noStroke();
  // 面倒なのでライティングはしない。
  noLights();
  sphere(200);

  // helper.
  resetShader();
  stroke(255);
  const v0 = createVector(0, 600*tan(PI/14), 600*tan(PI/14));
  const v1 = createVector(0, v0.y, -v0.z);
  const v2 = createVector(0, -v0.y, v0.z);
  const v3 = createVector(0, -v0.y, -v0.z);
  const vEye = createVector(600, 0, 0);
  line(v0.x, v0.y, v0.z, vEye.x, vEye.y, vEye.z);
  line(v1.x, v1.y, v1.z, vEye.x, vEye.y, vEye.z);
  line(v2.x, v2.y, v2.z, vEye.x, vEye.y, vEye.z);
  line(v3.x, v3.y, v3.z, vEye.x, vEye.y, vEye.z);
}

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

  // vertex shader:
  // varying: vec4/vNDC vec3/globalPosition vec3/globalNormal
  // uniform: mat4/uModelMatrix, mat4/uCameraMatrix, mat4/uProjMatrix

  // fragment shader:
  // varying: vec4/vNDC vec3/globalPosition vec3/globalNormal
  // uniform: vec3/uEye, sampler2D/uTextureForProj

  myShader = sh.modify({
    vertexDeclarations:`
      uniform mat4 uCameraMatrix;
      uniform mat4 uProjMatrix;
      uniform mat4 uModelMatrix;
      OUT vec4 vNDC;
      OUT vec3 vGlobalPosition;
      OUT vec3 vGlobalNormal;
    `,
    fragmentDeclarations:`
      uniform vec3 uEye;
      uniform sampler2D uTextureProj;
      IN vec4 vNDC;
      IN vec3 vGlobalPosition;
      IN vec3 vGlobalNormal;
    `,
    'void afterVertex':`(){
      vec4 p = uModelMatrix * vec4(aPosition, 1.0);
      vGlobalPosition = p.xyz;
      vGlobalNormal = (inverse(transpose(uModelMatrix)) * vec4(aNormal, 0.0)).xyz;
      vec4 ndc = uProjMatrix * (uCameraMatrix * p);
      ndc.x = 0.5*ndc.w + 0.5*ndc.x;
      ndc.y = 0.5*ndc.w - 0.5*ndc.y;
      vNDC = ndc;
    }`,
    'Inputs getPixelInputs': `(Inputs inputs) {
      vec4 textureColor = textureProj(uTextureProj, vNDC);

      // 以降、見栄えをよくするための下処理。
      vec2 uv = vNDC.xy / vNDC.w;
      // 外周が消えるようにちょっとした細工
      // 実際には長方形とは限らないしこれが最善ではないかもね
      // もちろん事前erase＆textureWrapという手段の方が楽ですし
      float resX = smoothstep(0.0, 0.001, uv.x) * smoothstep(1.0, 0.999, uv.x);
      float resY = smoothstep(0.0, 0.001, uv.y) * smoothstep(1.0, 0.999, uv.y);
      textureColor *= resX * resY;

      // 以降、裏側に描画されないための下処理。
      vec3 toCam = normalize(uEye - vGlobalPosition);
      float toEyeFactor = dot(toCam, normalize(vGlobalNormal));
      textureColor *= smoothstep(0.0, 0.1, toEyeFactor);

      // いわゆるnormalMaterial
      vec4 mainColor = vec4(0.5+0.5*normalize(vGlobalNormal), 1.0);
      // 雑にblend. まあ何でもいいんよ。 
      vec3 finalCol = mainColor.rgb * (1.0 - textureColor.a) + textureColor.rgb * textureColor.a;
      inputs.color = vec4(finalCol, 1.0);
      inputs.ambientMaterial = finalCol;
      return inputs;
    }`
  });
}
```
実行結果：
![dff3weeetr434e.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437349/ce493e96-3807-97d4-ae3c-ce5e47456ac5.png)

線はカメラのヘルパーです。このように、用意したカメラに対して、そこから見た時のような画像がそのままマッピングされるわけです。

## カメラの設定
　上の方で用意しているcamProjが射影テクスチャマッピングに使うカメラです。
```javascript
  camProj = createCamera();
  camProj.camera(600, 0, 0, 0, 0, 0, 0, 1, 0);
  camProj.perspective(PI/7, 1, 20, 2000);

  const cam = createCamera();
  cam.camera(0, 0, 300*sqrt(3),0,0,0,0,1,0);
  cam.perspective(PI/3, width/height, 30*sqrt(3),3000*sqrt(3));
```
createCamera()を使うとカレントカメラが勝手にそれになってしまうので、そのあとで描画に使うカメラ（というかorbitControl()で動かすカメラ）を別に用意します。そのあと、シェーダーを用意します。

## シェーダー
　shaderを作るところ。
```javascript
  myShader = baseMaterialShader().modify({
    vertexDeclarations:`
      uniform mat4 uCameraMatrix;
      uniform mat4 uProjMatrix;
      uniform mat4 uModelMatrix;
      OUT vec4 vNDC;
      OUT vec3 vGlobalPosition;
      OUT vec3 vGlobalNormal;
    `,
    fragmentDeclarations:`
      uniform vec3 uEye;
      uniform sampler2D uTextureProj;
      IN vec4 vNDC;
      IN vec3 vGlobalPosition;
      IN vec3 vGlobalNormal;
    `,
    'void afterVertex':`(){
      vec4 p = uModelMatrix * vec4(aPosition, 1.0);
      vGlobalPosition = p.xyz;
      vGlobalNormal = (inverse(transpose(uModelMatrix)) * vec4(aNormal, 0.0)).xyz;
      vec4 ndc = uProjMatrix * (uCameraMatrix * p);
      ndc.x = 0.5*ndc.w + 0.5*ndc.x;
      ndc.y = 0.5*ndc.w - 0.5*ndc.y;
      vNDC = ndc;
    }`,
    'Inputs getPixelInputs': `(Inputs inputs) {
      vec4 textureColor = textureProj(uTextureProj, vNDC);

      // 以降、見栄えをよくするための下処理。
      vec2 uv = vNDC.xy / vNDC.w;
      // 外周が消えるようにちょっとした細工
      // 実際には長方形とは限らないしこれが最善ではないかもね
      // もちろん事前erase＆textureWrapという手段の方が楽ですし
      float resX = smoothstep(0.0, 0.001, uv.x) * smoothstep(1.0, 0.999, uv.x);
      float resY = smoothstep(0.0, 0.001, uv.y) * smoothstep(1.0, 0.999, uv.y);
      textureColor *= resX * resY;

      // 以降、裏側に描画されないための下処理。
      vec3 toCam = normalize(uEye - vGlobalPosition);
      float toEyeFactor = dot(toCam, normalize(vGlobalNormal));
      textureColor *= smoothstep(0.0, 0.1, toEyeFactor);

      // いわゆるnormalMaterial
      vec4 mainColor = vec4(0.5+0.5*normalize(vGlobalNormal), 1.0);
      // 雑にblend. まあ何でもいいんよ。 
      vec3 finalCol = mainColor.rgb * (1.0 - textureColor.a) + textureColor.rgb * textureColor.a;
      inputs.color = vec4(finalCol, 1.0);
      inputs.ambientMaterial = finalCol;
      return inputs;
    }`
  });
```
　まずvertexDeclarationsで射影テクスチャマッピングに使うカメラのビュー行列と射影行列を用意しています。modelMatrixはカメラ関係ないので注意してください。あとでグローバルの（モデル変換後の）頂点と法線を使いたいので、ちょっとvaryingしています。NDCは射影テクスチャ生成用です。fragmetDeclarationsは、カメラの目の位置の情報と射影テクスチャマッピングに使う画像です。画像はこれを使います。
（磐田駅にて撮影。天候：曇り）

![Shippei.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437349/631b2b67-e2fc-58e2-4f1a-66f756645587.jpeg)
[しっぺい](https://shippei.jp/)

しっぺいくんかわいいですね...！大道芸の時ハグしてもらったこと、一生の思い出です。

afterVertexはどうもmain内で最後にやるのではなく別スコープのようです...？多分。なので内部で用意された変数は使えません。最も欲しいのはモデル行列を掛けただけの値なので、そもそも用意されていません。そこで別に計算しています。
　射影テクスチャマッピングの手法にのっとってndcを渡し、テクスチャを取得します。
　しかしこのままだと、カメラの外のエリアがテクスチャの外になってしまい、clampで色が伸びてしまいます。これを防ぐために、カメラの及ぶ範囲の外ではalphaが0となるようにしています（resX * resYを掛けているところ）。次に、裏側にも描画されてしまうのは具合が悪いので、描画位置のグローバルポジションとカメラの目の位置からそこに向かうベクトルを作り、法線と比べて、そっぽを向いてるなら描画しないようにします（smoothstepとかその辺）。最後にベースカラーとしてnormalMaterialを用意して適当にブレンドしています。今回ライティングは使わないので、これで良しとします。

## ヘルパーの描画
　fovとaspectから簡単にベクトルを作り、線を引いています。

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

# 追記
複数のオブジェクトに適用する場合、描画ごとにテクスチャをuniformで送る必要があります。
```javascript
  noStroke();
  // 面倒なのでライティングはしない。
  noLights();
  translate(100,0,0);
  // どうも描画のたびにtextureをuniformでセットしないと
  myShader.setUniform("uTextureProj", shippei);
  sphere(200);
  translate(300,0,0);
  // ダメなようです。共通というわけにはいかないみたい。
  // とはいえテクスチャは基本的に描画のたびにリセットするのがマナーらしいけれど。
  // まとめて適用したい場合は、一度に描画するといいです。
  myShader.setUniform("uTextureProj", shippei);
  sphere(40);
  translate(-400,0,0);
```

![feeeg4t4gg4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437349/ebea4306-e633-4b91-0141-6e6ec2458615.png)