p5.jsでフレームバッファを扱う際のカメラに関する注意

# はじめに
　p5.jsにフレームバッファが実装されました。誰も使っていません。それはいいんですが、カメラに関して注意すべきポイントがあるので触れておきます。AdventCalenderでもフレームバッファの記事が出たようですが、この辺に関して全く言及していなかったので、補足の意味も込められています。
　ざっくりいうと、カメラの向きが逆になっています。というか、射影行列の例の-1がフレームバッファの場合除去されているようです。なんだってぇ！？
## フレームバッファのカメラ
　[p5.Framebuffer](https://p5js.org/reference/p5/p5.Framebuffer/)
　フレームバッファにはカメラを作る関数が付随しています。
　[createCamera()](https://p5js.org/reference/p5.Framebuffer/createCamera/)
これを使ってカメラを作ってフレームバッファ内で使うことを考えます。
```js
let fbo;
let cam;

function setup() {
  createCanvas(400, 400, WEBGL);
  fbo = createFramebuffer();
  cam = fbo.createCamera();
  //cam = createCamera();
  noStroke();

  const gl = this._renderer.GL;
  //gl.enable(gl.CULL_FACE);
  //gl.cullFace(gl.BACK);
}

function draw() {
  fbo.begin();
  setCamera(cam);
  background(128);
  lights();
  fill(255);
  translate(0,80,0);
  rotateX(PI/3);
  torus(80,40,24);
  fbo.end();
  clear();
  image(fbo.color,-width/2,-height/2);
}
```

![fbocamera0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437349/e8c2f39e-68f7-3df1-f792-32865bfcc5fc.png)

　このように下方向にトーラスが描画されます。なお、fboの外ではデフォルトのカメラが使われるので、内部でのトランスフォームはリセットされ、普通にimage関数を用いることができます。
　次に、このfboのカメラを使って通常描画をします。

```js
let fbo;
let cam;

function setup() {
  createCanvas(400, 400, WEBGL);
  fbo = createFramebuffer();
  cam = fbo.createCamera();
  //cam = createCamera();
  noStroke();

  const gl = this._renderer.GL;
  //gl.enable(gl.CULL_FACE);
  //gl.cullFace(gl.BACK);
}

function draw() {
  //fbo.begin();
  setCamera(cam);
  background(128);
  lights();
  fill(255);
  translate(0,80,0);
  rotateX(PI/3);
  torus(80,40,24);
  //fbo.end();
  //clear();
  //image(fbo.color,-width/2,-height/2);
}
```

![fbocamera1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437349/b3098b31-0d1c-2648-16da-53559442eb3f.png)

逆方向に描画されてしまいました。fbo用のカメラを使ったせいで罰が当たったのでしょうか。それはいいとして、普通のカメラではないことが分かりました。
　ちなみに、カリングの向きも逆になっています。p5は射影行列をいじることでy軸を逆向きにしているのですが、それがどうやら修正されているようです。ゆえに、通常はフロントカリングで消えない仕組みになっているのですが、この状態では、フロントカリングをすると消えてしまいます：
```js
let fbo;
let cam;

function setup() {
  createCanvas(400, 400, WEBGL);
  fbo = createFramebuffer();
  cam = fbo.createCamera();
  //cam = createCamera();
  noStroke();

  const gl = this._renderer.GL;
  gl.enable(gl.CULL_FACE);
  gl.cullFace(gl.FRONT);
}

function draw() {
  //fbo.begin();
  setCamera(cam);
  background(128);
  lights();
  fill(255);
  translate(0,80,0);
  rotateX(PI/3);
  torus(80,40,24);
  //fbo.end();
  //clear();
  //image(fbo.color,-width/2,-height/2);
}
```

![fbocamera2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437349/a49fd2f7-e0b1-db4e-afd5-8865237ca20e.png)

fboのカメラではなく通常のカメラを用いれば、消えませんが、やはりy軸は下向きになります。
```js
let fbo;
let cam;

function setup() {
  createCanvas(400, 400, WEBGL);
  fbo = createFramebuffer();
  //cam = fbo.createCamera();
  cam = createCamera();
  noStroke();

  const gl = this._renderer.GL;
  gl.enable(gl.CULL_FACE);
  gl.cullFace(gl.FRONT);
}

function draw() {
  //fbo.begin();
  setCamera(cam);
  background(128);
  lights();
  fill(255);
  translate(0,80,0);
  rotateX(PI/3);
  torus(80,40,24);
  //fbo.end();
  //clear();
  //image(fbo.color,-width/2,-height/2);
}
```

![fbocamera3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437349/9b3b929b-12cd-5b5f-f2b8-fb2f782dcf2c.png)

つまりfbo用のカメラはカリングが逆向きになっているようです。そして外で使うとy軸が逆向きになります。ところで、内部で使ってもカリングは逆向きになっているのか？それを確かめるには、最初のようにimageでfboの結果を焼くことにしたうえで、フロントカリングをしてみればいいです。

```js
let fbo;
let cam;

function setup() {
  createCanvas(400, 400, WEBGL);
  fbo = createFramebuffer();
  cam = fbo.createCamera();
  //cam = createCamera();
  noStroke();

  const gl = this._renderer.GL;
  gl.enable(gl.CULL_FACE);
  gl.cullFace(gl.FRONT);
}

function draw() {
  fbo.begin();
  setCamera(cam);
  background(128);
  lights();
  fill(255);
  translate(0,80,0);
  rotateX(PI/3);
  torus(80,40,24);
  fbo.end();
  clear();
  image(fbo.color,-width/2,-height/2);
}
```

![fbocamera4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437349/774e2cb4-92dd-523b-e692-78e2ea3d17f0.png)

消えました！どうやらカリングが逆になっているようです。じゃあバックカリングなら消えないのか？ええ。しかし画像は消えてしまいます：
```js
let fbo;
let cam;

function setup() {
  createCanvas(400, 400, WEBGL);
  fbo = createFramebuffer();
  cam = fbo.createCamera();
  //cam = createCamera();
  noStroke();

  const gl = this._renderer.GL;
  gl.enable(gl.CULL_FACE);
  gl.cullFace(gl.BACK);
}

function draw() {
  fbo.begin();
  setCamera(cam);
  background(128);
  lights();
  fill(255);
  translate(0,80,0);
  rotateX(PI/3);
  torus(80,40,24);
  fbo.end();
  clear();
  image(fbo.color,-width/2,-height/2);
}
```

![fbocamera5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437349/eed4f89f-da8b-ceee-2af9-39e20c4aaa2a.png)

なぜか？先ほども述べた通り、外ではデフォルトカメラが機能します。デフォルトカメラはバック描画なので、バックカリングをすると消えてしまうわけです。fbo上にはちゃんとトーラスが焼かれています。それを確かめるにはy軸に関して180°回してみればわかります。
```js
let fbo;
let cam;

function setup() {
  createCanvas(400, 400, WEBGL);
  fbo = createFramebuffer();
  cam = fbo.createCamera();
  //cam = createCamera();
  noStroke();

  const gl = this._renderer.GL;
  gl.enable(gl.CULL_FACE);
  gl.cullFace(gl.BACK);
}

function draw() {
  fbo.begin();
  setCamera(cam);
  background(128);
  lights();
  fill(255);
  translate(0,80,0);
  rotateX(PI/3);
  torus(80,40,24);
  fbo.end();
  clear();

  rotateY(PI);
  image(fbo.color,-width/2,-height/2);
}
```

![fbocamera6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437349/d277be47-6b6e-8ead-517b-516eb1048a5d.png)

　ほらね。

　そういうわけで、中と外で異なるカメラを用いているがために、このような事態が発生します。なので、カリングを利用する際には、いちいちカリングを切り替える必要があります。
　orbitControl()の挙動についても触れておきます。次のようなコードを書くと、カメラと一緒に画像が動いてしまいます。
```js
let fbo;
let cam;

function setup() {
  createCanvas(400, 400, WEBGL);
  fbo = createFramebuffer();
  cam = fbo.createCamera();
  //cam = createCamera();
  noStroke();

  const gl = this._renderer.GL;
  //gl.enable(gl.CULL_FACE);
  //gl.cullFace(gl.FRONT);
  //const defaultCam = createCamera();
}

function draw() {
  fbo.begin();
  setCamera(cam);
  orbitControl();
  background(128);
  lights();
  fill(255);
  translate(0,80,0);
  rotateX(PI/3);
  torus(80,40,24);
  fbo.end();
  clear();

  image(fbo.color,-width/2,-height/2);
}
```

![fbocamera7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437349/4ea879cd-776a-87dc-e38d-8de4afac6507.png)

どうやらfboのカメラを生成するタイミングで、描画に使うカメラも同じカメラになってしまうらしい...です？そこら辺の仕様は詳しくないので分かりませんが、内部でカメラをいじると外側にも影響が出てしまうあたり、どうやらそのようです。カリングの方向は逆なのにorbitControl()でどっちも動いてしまうのは、おそらく射影行列の方はいじられていないというのが理由のようですが、詳しくはハッキングしてみないと何とも言えないです（やりませんが）。これを回避するにはいろんな方法がありますが、手っ取り早いのはデフォルトのカメラをダミーで生成することです。これで外ではデフォルトのカメラが使われるようになるようです。
```js
let fbo;
let cam;

function setup() {
  createCanvas(400, 400, WEBGL);
  fbo = createFramebuffer();
  cam = fbo.createCamera();
  //cam = createCamera();
  noStroke();

  const gl = this._renderer.GL;
  //gl.enable(gl.CULL_FACE);
  //gl.cullFace(gl.FRONT);
  const defaultCam = createCamera();
}

function draw() {
  fbo.begin();
  setCamera(cam);
  orbitControl();
  background(128);
  lights();
  fill(255);
  translate(0,80,0);
  rotateX(PI/3);
  torus(80,40,24);
  fbo.end();
  clear();

  image(fbo.color,-width/2,-height/2);
}
```

![fbocamera8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437349/1db8303d-65b5-0e9c-b688-3500d226c898.png)

setupの最後の行でカメラを生成しています。これでimageの際に適用されるカメラが動かないようにできるようです。setupで生成したカメラがそのまま外での描画用として紐付けられ、fboのカメラと別扱いになるようです。
　この状態でフロントカリングするとどうなるのか？もちろんですが、トーラスは消えます（裏面は描画されるので動かせば現れます）。先ほども述べた通り、image()はバック描画なので消えませんが、トーラスはフロント描画だからです。
```js
let fbo;
let cam;

function setup() {
  createCanvas(400, 400, WEBGL);
  fbo = createFramebuffer();
  cam = fbo.createCamera();
  //cam = createCamera();
  noStroke();

  const gl = this._renderer.GL;
  gl.enable(gl.CULL_FACE);
  gl.cullFace(gl.FRONT);
  const defaultCam = createCamera();
}

function draw() {
  fbo.begin();
  setCamera(cam);
  orbitControl();
  background(128);
  lights();
  fill(255);
  translate(0,80,0);
  rotateX(PI/3);
  torus(80,40,24);
  fbo.end();
  clear();

  image(fbo.color,-width/2,-height/2);
}
```

![fbocamera9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437349/1917ebf4-019d-b354-abd1-42511836e1cf.png)

# おわりに
　なぜ中と外で違うカメラになっているかというと、テクスチャが絡む特殊事情によるものらしくて、正直よくわかんないです。実装した人が、その方が便利だろうと踏んだので、そういう仕様になったのでしょう。フレームバッファのサンプルを見ると分かりますが、ほぼすべてフレームバッファ内で描画した結果を外に出す形となっています。そういう使い方しか想定されていないわけです。カリングはどうでもいいということですね。

　余談ですが、自分の使ってる自作ライブラリではこんなめんどうなことはしていなくて、普通に外と中で共用しています。描画時にフェッチの際に$y$成分の上下を逆にすればいいだけの話ですから。

　以上です。

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