3次元物体の回転~javascript~

## 前回の2次元図形回転の復習
2次元図形回転は[こちら](https://qiita.com/takatakao397/items/67960568d465504b13b9)で実装しています。なかなか好評？いいね2つとストック3つ頂いたので、調子乗って3次元物体の回転も解説したいと思います。
復習がてら2次元の場合の回転行列は下になります。
```math
R(θ) =
\begin{pmatrix}
 \cosθ & -\sinθ \\
 \sinθ & \cosθ 
\end{pmatrix}
```
## 軸を中心に回転させる
2次元図形の回転はxy平面上の動きですが、3次元的に物体を回転させるには下のような動きをします。

![Screenshot 2023-08-29 at 21.52.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2153036/8a0ca3d7-b7d5-5045-816b-f25987fc768f.png)


この例ではy軸を中心に回転させていますが、x軸z軸を中心に回転させることもできます。
```math
\begin{pmatrix}
 x' \\
 y' \\
 z'
\end{pmatrix}
 = 
\begin{pmatrix}
 \cosθ && 0 && \sinθ \\
 0 && 1 && 0 \\
 -\sinθ && 0 && \cosθ
\end{pmatrix}
\begin{pmatrix}
 x \\
 y \\
 z
\end{pmatrix}
```
```math
x' = \cosθx + \sinθz
```
```math
y' = y
```
```math
z' = -\sinθx + \cosθz
```
以上の関係より回転後の座標y'はcosθ sinθに影響されず一定なので、y軸を中心に回転する。
## ピンホールカメラ (透視投影)
上の3次元回転行列で実現できそうであるが、遠近感が得られず奥行きが感じられない。
![回転1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2153036/75b9ab84-b825-da6d-5357-6c11c11fa8a6.gif)
透視投影では、遠くのもの(ここでは奥行き方向のz値が大きいもの)が近くのもの(z値が小さいもの)より小さく描くようにし遠近感を得る。
```math
x' = \frac{x}{z}
```
```math
y' = \frac{y}{z}
```
上の式をコードに組みこみ回転させてみた結果を見てみましょう！
## 実際のプログラム
以下のコードで実装できます！
```javascript:3次元の回転
const canvas = document.getElementById("target");
const context = canvas.getContext("2d"); //2次元描画
let degree = 0;

function triangle(fig,x1,y1,x2,y2,x3,y3,s){
  if (s==true){
    fig.beginPath(); //パスの作成
    fig.moveTo(x1,y1);
    fig.lineTo(x2,y2);
    fig.lineTo(x3,y3);
    fig.closePath();
    fig.fillStyle = "blue";
    fig.fill();
  }else{
    fig.beginPath(); 
    fig.moveTo(x1,y1);
    fig.lineTo(x2,y2);
    fig.lineTo(x3,y3);
    fig.closePath();
    fig.fillStyle = "black";
    fig.fill();
  }
}
// 三角形の重心 (3次元)
let center = (A) => [
    (A[0][0] + A[1][0] + A[2][0]) / 3,
    (A[0][1] + A[1][1] + A[2][1]) / 3,
    (A[0][2] + A[1][2] + A[2][2]) / 3
];
  
function draw(){
    context.clearRect(0,0,canvas.width,canvas.height);
    let V = [
      [0, 0, 0],
      [1, 0, 0],
      [0, 0, 1],
      [0, -1, 0]
    ]
    let F = [
      [0, 1, 2],
      [0, 1, 3],
      [1, 2, 3],
      [0, 2, 3]
    ]
    let width = canvas.width;
    let height = canvas.height;

    let S = V;
    let c = center(S);
    degree+=1
    r=(degree*Math.PI)/180
    let vx = new Array(4);
    let vy = new Array(4);
    let vz = new Array(4);

    for(let i=0; i<4; i++){
      vx[i] = S[i][0] - c[0];
      vy[i] = S[i][1] - c[1];
      vz[i] = S[i][2] - c[2];
  
      // ベクトルを回転
      S[i][0] = vx[i]*Math.cos(r) + vz[i]*Math.sin(r);
      S[i][1] = vy[i];
      S[i][2] = -vx[i]*Math.sin(r) + vz[i]*Math.cos(r);

      for(let j=0; j<3; j++){
          S[i][j]+=c[j];
      }
    }

    // 並行移動
    for(let i=0; i<4; i++){
      S[i] = [S[i][0],S[i][1]+0.5,S[i][2]+2];
    }
    let T = S;

    // ピンホールカメラ (透視投影)
    for(let i=0; i<4; i++){
      S[i] = [S[i][0]/S[i][2], S[i][1]/S[i][2]];
    }

    // 拡大 (画面の大きさに合わせて)
    for(let i=0; i<4; i++){
      S[i] = [S[i][0]/2*width+width/2, S[i][1]/2*height+height/2];
    }

    for(let i=0; i<F.length; i++){
      triangle(context,S[F[i][0]][0],S[F[i][0]][1],S[F[i][1]][0],S[F[i][1]][1],S[F[i][2]][0],S[F[i][2]][1],true); 
    }
    triangle(context,T[0][0],T[0][1]+150,T[1][0],T[1][1]+150,T[2][0],T[2][1]+150,false); 

}

setInterval("draw()",20);
```
![回転2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2153036/c9d2d50b-be4b-1006-c4cb-c140e533d5b1.gif)