p5.jsのカメラのslerpを利用して回転の補間を実行する

# はじめに
　p5.jsのwebglにおいて物体の回転を定義するのにapplyMatrixを使う方法があります。
　[applyMatrix](https://p5js.org/reference/p5/applyMatrix/)
これは4x4行列で指定する場合、平行移動の成分が無ければ、軸を3本用意してそれに基づいて定義する形になります。とはいえ、任意の回転でいいなら、3本のたがいに直交する単位ベクトルを用意すればいいです。それらの回転の状態を補間できれば、次のようにして、2つの状態の間を滑らかに行き来させることができます。

<iframe width="560" height="315" src="https://www.youtube.com/embed/WfIRQhjRiJc?si=gkVHnFu1jS3XPoAQ" 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>

それをカメラのslerpを応用して実行します。

## コード全文
　teapotのジオメトリーを使います。こちらから拝借しました：[WebGLシェーダーデモ](https://catincat.jp/javascript/shader/index.html)
　[lerp transform](https://openprocessing.org/sketch/2495513)
```js
// quarternion?
// ってなんだっけ
// ああー...
// x,y,z,wとあるとき...直交行列があれで、...
// 逆は？無理。
// 直交行列の補間ならできる。それだけですね、できるのは。...

let cam0, cam1, cam2, defaultCamera;

let teapot, geom;

function preload(){
  teapot = loadJSON("https://inaridarkfox4231.github.io/models/teapot.json");
}

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

  cam0 = createCamera();
  cam1 = createCamera();
  cam2 = createCamera();

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

  geom = new p5.Geometry();
  for(let i=0; i<teapot.v.length; i+=3){
    geom.vertices.push(createVector(teapot.v[i], teapot.v[i+1], teapot.v[i+2]));
    geom.vertexNormals.push(createVector(teapot.n[i], teapot.n[i+1], teapot.n[i+2]));
  }
  for(let k=0; k<teapot.f.length; k+=3){
    geom.faces.push([teapot.f[k], teapot.f[k+1], teapot.f[k+2]]);
  }
  noStroke();
}

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

  lights();
  fill("orange");
  specularMaterial(64);

  plane(400);
  translate(0, 0, 100);
  push();
  const r = 0.5 - 0.5 * cos((frameCount%120)*TAU/120);
  applyTransform(4*r);
  scale(160);
  model(geom);
  pop();
}

function applyTransform(r){
  const nx = createVector(1,0,0);
  const ny = createVector(0,0,1);
  const nz = createVector(0,-1,0);

  const mx = createVector(1,2,1).normalize();
  const my = createVector(-3,1,1).normalize();
  //const mz = mx.cross(my); // 実は不要

  cam0.camera(nx.x, nx.y, nx.z, 0, 0, 0, ny.x, ny.y, ny.z);
  cam1.camera(mx.x, mx.y, mx.z, 0, 0, 0, my.x, my.y, my.z);
  cam2.slerp(cam0, cam1, r);

  const u = createVector(cam2.eyeX, cam2.eyeY, cam2.eyeZ).normalize();
  const v = createVector(cam2.upX, cam2.upY, cam2.upZ).normalize();
  const w = u.cross(v);

  vectorRotate(u,v,w);
}

function vectorRotate(x,y,z){
  applyMatrix(
    x.x, x.y, x.z, 0,
    y.x, y.y, y.z, 0,
    z.x, z.y, z.z, 0,
    0, 0, 0, 1
  );
}
```
実行結果：上記。

## モデルの用意
　teapotのJSONは、v,n,fに数値データが配列の形で順繰りに入ってます。これをp5.Geometryの枠組みに落としています。
```js
  geom = new p5.Geometry();
  for(let i=0; i<teapot.v.length; i+=3){
    geom.vertices.push(createVector(teapot.v[i], teapot.v[i+1], teapot.v[i+2]));
    geom.vertexNormals.push(createVector(teapot.n[i], teapot.n[i+1], teapot.n[i+2]));
  }
  for(let k=0; k<teapot.f.length; k+=3){
    geom.faces.push([teapot.f[k], teapot.f[k+1], teapot.f[k+2]]);
  }
```
JSONの方が扱いやすいのでそうしています。汎用性が高いので...

## カメラの準備
　transformの補間のために、ある状態を表すカメラと、別の状態を表すカメラと、それらの補間をセットするカメラを用意します。それらとは別に、描画用のカメラ（defaultCamera）を用意して、描画にはこれを用います。仕様により最後に生成したカメラが使われてしまうので、これを最後に生成します。
```js
  cam0 = createCamera();
  cam1 = createCamera();
  cam2 = createCamera();

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

## 描画部分
　orbitControl()の対象はdefaultCameraです。常に、です。applyTransformで回転を適用しています。push()とpop()で囲っているので適用されるのはgeomだけです。
```js
function draw() {
  orbitControl();
  background(220);

  lights();
  fill("orange");
  specularMaterial(64);

  plane(400);
  translate(0, 0, 100);
  push();
  const r = 0.5 - 0.5 * cos((frameCount%120)*TAU/120);
  applyTransform(4*r);
  scale(160);
  model(geom);
  pop();
}
```

## 回転の適用
　applyTransform()では次のような処理をしています。
```js
function applyTransform(r){
  const nx = createVector(1,0,0);
  const ny = createVector(0,0,1);
  const nz = createVector(0,-1,0);

  const mx = createVector(1,2,1).normalize();
  const my = createVector(-3,1,1).normalize();
  //const mz = mx.cross(my); // 実は不要

  cam0.camera(nx.x, nx.y, nx.z, 0, 0, 0, ny.x, ny.y, ny.z);
  cam1.camera(mx.x, mx.y, mx.z, 0, 0, 0, my.x, my.y, my.z);
  cam2.slerp(cam0, cam1, r);

  const u = createVector(cam2.eyeX, cam2.eyeY, cam2.eyeZ).normalize();
  const v = createVector(cam2.upX, cam2.upY, cam2.upZ).normalize();
  const w = u.cross(v);

  vectorRotate(u,v,w);
}
```
　nx,ny,nzはteapotの正位置を定義するための軸ベクトルたちです。ジオメトリーがyUpなので、それに合うように調整しています。おそらく鏡写しになっているんですがまああまり気にしないことですね...
　mx,my,mzはもうひとつの回転の状態を定義しています。たがいに直交し、かつすべて単位ベクトルであるように、雑に決めています。
　次に、これらがカメラを定めるようにcam0,cam1をセットします。このとき使うのはcenterからeyeに向かうベクトルとしてのx軸と、上方向を定義するy軸だけです。
　そしてこれらを引数の「割合」でslerp（補間）します。「」でくくったのは実際にははみ出すこともできるからです（回転がオーバーするだけです）。その結果をcam2に格納します。
　最後にcam2のeyeとupのベクトルから補間結果のx軸とy軸を抽出し、それらの外積でz軸を決めます。
　vectorRotateでは軸ベクトルに基づいてapplyMatrix()を適用しています。
```js
function vectorRotate(x,y,z){
  applyMatrix(
    x.x, x.y, x.z, 0,
    y.x, y.y, y.z, 0,
    z.x, z.y, z.z, 0,
    0, 0, 0, 1
  );
}
```

# おわりに
　平行移動も加味すればもうちょっと込み入った補間もできそうですが、めんどうなのでやりたくないですね...
　ここまでお読みいただいてありがとうございました。