p5.jsのフレームバッファを使って簡単なピッキングを実装する

# はじめに
　ピッキングとは、マウス位置にあるオブジェクトを検出する技術です。クリックした位置にオブジェクトがあるかどうかを調べます。いろんなやり方がありますが、ここではwgldさんのサイトでも紹介されている：
　[描画結果から色を取得する readPixels](https://wgld.org/d/webgl/w086.html)
色取得による方法でやります。レイキャストは汎用性が高いですが、計算が煩雑になるのとカメラへの対応がめんどくさいので採用しません。目的は、球と立方体を描画して、クリックしたときそこにそれがあるかどうかをconsoleで表示させることです。それだけだと分かりにくいので、色を変えるようにしました。

<iframe width="560" height="315" src="https://www.youtube.com/embed/LIXjxR_ZTfs?si=YYDcixcFroA53tm_" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

こんな感じですね。ちゃんとconsoleにも表示されていますね。
　なお、使うのはフレームバッファです。readPixelsに相当するget()という便利な関数が実装されているので、これを使います。
　[p5.Framebuffer/get](https://p5js.org/reference/p5.Framebuffer/get/)

## コード全文
　短いです。
　[p5 fbo picking](https://openprocessing.org/sketch/2495460)
```js
/*
  こういうことがしたいらしい
  https://x.com/joki_hirooka/status/1790127815880298944
  https://editor.p5js.org/kagikko/sketches/WVSXB86SA
  やりましょう

  boxとsphereにクリックでアクセスして以下略

  ポイントまとめ
  両者で描画の仕方を合わせる。
  描画は共にフレームバッファに対して実行する。
  ピッキング用の場合ライティングは殺して単色で落とす。線も描かない。
  カメラの更新は片方でのみ実行する。
  ピッキング用の色は255で0～255の値を割ったものをそのまま使えばOK
  最終的に描画結果を落とすためにデフォルトのカメラを用意しておく
*/

let fbo;
let pickFBO;
let defaultCamera;
let fboCamera;

let sphereIsActive = false;
let boxIsActive = false;

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

  fbo = createFramebuffer();
  pickFBO = createFramebuffer();

  defaultCamera = createCamera();

  fboCamera = fbo.createCamera();
  fboCamera.camera(300, 300, 300, 0, 0, 0, 0, 0, -1);
  fboCamera.perspective(PI/3, width/height, 30*1.732, 3000*1.732);
}

function draw() {

  pickFBO.begin();
  clear();
  setCamera(fboCamera);
  orbitControl();

  noLights();
  noStroke();
  translate(80, 0, 120);
  fill(64, 96, 32);
  sphere(80);
  translate(-80, 0, -120);
  translate(-80, 0, 120);
  fill(32, 64, 96);
  box(80);
  translate(80, 0, -120);

  pickFBO.end();

  fbo.begin();
  clear();
  setCamera(fboCamera);
  //orbitControl();

  lights();
  specularMaterial(128);
  stroke(0);
  fill(255);
  translate(80, 0, 120);
  if(sphereIsActive){fill("skyblue");}else{fill("gray");}
  sphere(80);
  translate(-80, 0, -120);
  translate(-80, 0, 120);
  if(boxIsActive){fill("orange");}else{fill("gray");}
  box(80);
  translate(80, 0, -120);

  fbo.end();

  clear();
  setCamera(defaultCamera);
  image(fbo.color, -width/2, -height/2);
}

function mousePressed(){
  const mx = constrain(mouseX, 0, width-1);
  const my = constrain(mouseY, 0, height-1);
  const c = pickFBO.get(mx, my);
  if(c[0] === 0 && c[1] === 0 && c[2] === 0) return;
  if(c[0] === 64 && c[1] === 96 && c[2] === 32){
    console.log("Sphere hit!");
    sphereIsActive = !sphereIsActive;
  }
  if(c[0] === 32 && c[1] === 64 && c[2] === 96){
    console.log("Box hit!");
    boxIsActive = !boxIsActive;
  }
}
```
　実行結果は上記のyoutubeを見ての通りです。スマホの方で確認したところ、pixelDensity(1);を実行しない場合フレームバッファが大きくなってしまうので思わしい結果にならないようです。とりあえず必須としておきます。

## fboのカメラ
　p5のフレームバッファは独自のカメラを持っています。ぶっちゃけそのせいで使いづらく、特に利点も無いのですが、フレームバッファが実装されてるだけありがたいので、合わせるしかないです。カメラを作るメソッドがあるので、1つだけ作って使いまわします。フレームバッファは2枚用意します。1枚はピッキングのために単色かつライティング無しで描画します。線も引きません。もう1枚は本描画用です。
```js
  fbo = createFramebuffer();
  pickFBO = createFramebuffer();

  defaultCamera = createCamera();

  fboCamera = fbo.createCamera();
  fboCamera.camera(300, 300, 300, 0, 0, 0, 0, 0, -1);
  fboCamera.perspective(PI/3, width/height, 30*1.732, 3000*1.732);
```
　デフォルトカメラは最終的にimage関数でキャンバスへ落とすのに使います。ほんとは本描画を直にやれればいいんですが、fboが独自のカメラを持ってるせいで難しいので、仕方なくimageで落としています。

## ピッキング用の描画
　まず、ピッキング用のフレームバッファに落とします。
```js
  pickFBO.begin();
  clear();
  setCamera(fboCamera);
  orbitControl();

  noLights();
  noStroke();
  translate(80, 0, 120);
  fill(64, 96, 32);
  sphere(80);
  translate(-80, 0, -120);
  translate(-80, 0, 120);
  fill(32, 64, 96);
  box(80);
  translate(80, 0, -120);

  pickFBO.end();
```
　カメラのセットはbeginしてから実行します。orbitControl()はこっちで実行し、本描画では実行しません。noLights()とnoStroke()を実行します。ピックに使う色は今回、sphereで[64,96,32]とし、boxで[32,64,96]としますがまあなんでもいいです。

## 本描画
　本描画もそれ専用のフレームバッファに対して実行します。
```js
  fbo.begin();
  clear();
  setCamera(fboCamera);
  //orbitControl();

  lights();
  specularMaterial(128);
  stroke(0);
  translate(80, 0, 120);
  if(sphereIsActive){fill("skyblue");}else{fill("gray");}
  sphere(80);
  translate(-80, 0, -120);
  translate(-80, 0, 120);
  if(boxIsActive){fill("orange");}else{fill("gray");}
  box(80);
  translate(80, 0, -120);

  fbo.end();
```
　こっちでは堂々とライティングします。線も描画します。色については後述します。sphereIsActiveとboxIsActiveはそれぞれsphereやboxがクリックでヒットするたびにtrue/falseが入れ替わります。

## ピッキング処理
　mousePressedを使います。
```js
function mousePressed(){
  const mx = constrain(mouseX, 0, width-1);
  const my = constrain(mouseY, 0, height-1);
  const c = pickFBO.get(mx, my);
  if(c[0] === 0 && c[1] === 0 && c[2] === 0) return;
  if(c[0] === 64 && c[1] === 96 && c[2] === 32){
    console.log("Sphere hit!");
    sphereIsActive = !sphereIsActive;
  }
  if(c[0] === 32 && c[1] === 64 && c[2] === 96){
    console.log("Box hit!");
    boxIsActive = !boxIsActive;
  }
}
```
　マウス値を使ってget関数で該当する点の色を調べます。この際にconstrainしないとエラーを出されるので注意が必要です。sphereとboxに先ほど割り当てた色をそのまま判定に使うことができます。ヒットしたらconsole出力し、またアクティブフラグを切り替えます。これで色がflip-flopするわけですね。
　説明は以上です。

　最終的な描画の解説...image関数で落とすだけなので、特に説明は不要ですね...（フレームバッファの利点が死んでますが仕方ないです）。

# おわりに
　getは内部でreadPixelsを使っています。wgldの記事にもありますが、この処理はなかなかに重いので、リアルタイムで実行すると負荷が凄いです。レイキャストに劣るのはそこです。最もレイキャストも高速でやるには工夫がめんどくさいうえ、やり方がそもそも違うので単純な比較はできないです。
　ここまでお読みいただいてありがとうございました。
　（追記：レイキャストは正直全く詳しくないです。パフォーマンスに優れた何らかの方法があるのかもしれませんが調べてないので分かりません。気になる人は調べてみてください。ここで紹介されている方法は基本的にはオライリーのwebgl2の教本のやり方に則ったものです。）