mcfunctionでオイラー角回転を数式をあんまり使わずに実装する

## はじめに
ここ数ヶ月はmcfunctionを全く触っておらず、mcfunctionについて全て忘れてしまったため、1年前くらいに思いついていたことを書きます。
この記事はオイラー角とexecute、data、scoreboardコマンドとMATLABの知識を持つことを前提としています。

## 実装
先に実装、euler_rotation.mcfunctionを示しておきます。動作確認をしたバージョンは1.19.4と1.21.3です。
euler_rotation.mcfunctionはマクロが来る前に実装した記憶があるので今ならマクロを使ってスコアボードを一切使わずに実装できそうです。

````euler_rotation.mcfunction
## calc: Rotate に情報を入れてこのファンクションを実行する
## 入力されたベクトルを固定軸で回転させる
## 実行後に0-0-0-0-1の座標が回転後の単位ベクトルとして出力される
#> calc: Rotate
#{
#  Angle[X,Y,Z] : 回転角度を固定角で指定
#  Vector[A,B] : 回転するベクトルをRotationで記述
#}

## 0-0-0-0-1の場所の初期化
execute as 0-0-0-0-1 in minecraft:overworld positioned 0.0 0.0 0.0 rotated 0.0 0.0 run teleport @s ~ ~ ~ ~ ~

## 1回目の回転
execute store result score _ Calc run data get storage calc: Rotate.Vector[0] 1000000
execute store result score # Calc run data get storage calc: Rotate.Angle[0] 1000000
execute store result entity 0-0-0-0-1 Rotation[0] float 0.000001 run scoreboard players operation _ Calc += # Calc
execute as 0-0-0-0-1 in minecraft:overworld positioned 0.0 0.0 0.0 rotated as @s positioned ^ ^ ^1.0 run teleport @s ~ ~ ~ ~ ~

## 0-0-0-0-1の座標取得
data modify storage calc: Rotate.Pos set from entity 0-0-0-0-1 Pos

## 座標軸 巡回
data modify storage calc: Rotate.Pos append from storage calc: Rotate.Pos[0]
data remove storage calc: Rotate.Pos[0]

## Rotationの取り直し
data modify entity 0-0-0-0-1 Pos set from storage calc: Rotate.Pos
execute as 0-0-0-0-1 positioned 0.0 0.0 0.0 facing entity @s feet positioned as @s run teleport @s ~ ~ ~ ~ ~


## 2回目の回転
execute store result score _ Calc run data get entity 0-0-0-0-1 Rotation[0] 1000000
execute store result score # Calc run data get storage calc: Rotate.Angle[1] 1000000
execute store result entity 0-0-0-0-1 Rotation[0] float 0.000001 run scoreboard players operation _ Calc += # Calc
execute as 0-0-0-0-1 in minecraft:overworld positioned 0.0 0.0 0.0 rotated as @s positioned ^ ^ ^1.0 run teleport @s ~ ~ ~ ~ ~

## 0-0-0-0-1の座標取得
data modify storage calc: Rotate.Pos set from entity 0-0-0-0-1 Pos

## 座標軸 巡回
data modify storage calc: Rotate.Pos append from storage calc: Rotate.Pos[0]
data remove storage calc: Rotate.Pos[0]

## Rotationの取り直し
data modify entity 0-0-0-0-1 Pos set from storage calc: Rotate.Pos
execute as 0-0-0-0-1 positioned 0.0 0.0 0.0 facing entity @s feet positioned as @s run teleport @s ~ ~ ~ ~ ~

## 3回目の回転を行う
execute store result score _ Calc run data get entity 0-0-0-0-1 Rotation[0] 1000000
execute store result score # Calc run data get storage calc: Rotate.Angle[2] 1000000
execute store result entity 0-0-0-0-1 Rotation[0] float 0.000001 run scoreboard players operation _ Calc += # Calc
execute as 0-0-0-0-1 in minecraft:overworld positioned 0.0 0.0 0.0 rotated as @s positioned ^ ^ ^1.0 run teleport @s ~ ~ ~ ~ ~
data modify storage calc: Rotate.Pos set from entity 0-0-0-0-1 Pos

## 座標軸を戻す
data modify storage calc: Rotate.Pos append from storage calc: Rotate.Pos[0]
data remove storage calc: Rotate.Pos[0]


## 0-0-0-0-1の座標に代入
data modify entity 0-0-0-0-1 Pos set from storage calc: Rotate.Pos
````

## 処理の流れ
 まず、euler_rotationはストレージ"calc: Rotate"が入力、0-0-0-0-1というEntityの位置が出力になっています。
 1回の回転の処理はかなり単純で0-0-0-0-1をy軸を軸にして回転させているだけです。
 2、3回目の回転を行う前に座標を巡回させます。これによって3回の回転を全てy軸を回転軸とした回転へと落とし込むことができます。
 最後に適切に巡回置換を行なって元の座標系に戻しています。
 


## 検証
ここまでオイラー角の実装の仕組みについて解説しましたが、「これで本当に動くのか？」という疑問がまだ残ります。
また、オイラー角回転は1種類の回転を指さないため、「どのオイラー角回転と一致しているか？」という疑問も出てきます。
これら2つの疑問をMATLABで軽く確認をすることで解消します。
test.mlxはeuler_rotation.mcfunctionで実装した処理とYZXオイラー角回転を比較するためのスクリプトです。

```test.mlx
r = -10:9;
% 回転角度の指定
Angle = [45,30,60];

% 回転前のベクトルを用意
ang1 = 45;
ang2 = 45;

%極座標から直交座標を定義(yが上になるように定義)
x = r*sind(ang1)*cosd(ang2);
z = r*sind(ang1)*sind(ang2);
y = r*sind(ang1);

% 回転1回目
[theta,rho,zz] = cart2pol(x,z,y);
theta = theta + deg2rad(Angle(1));

% 円柱座標から戻すついでに巡回
[x1,z1,y1] = pol2cart(theta,rho,zz);

% 回転2回目
[theta,rho,zz] = cart2pol(y1,x1,z1);
theta = theta + deg2rad(Angle(2));

% 円柱座標から戻すついでに巡回
[y2,x2,z2] = pol2cart(theta,rho,zz);

% 回転3回目
[theta,rho,zz] = cart2pol(z2,y2,x2);
theta = theta + deg2rad(Angle(3));

% 円柱座標から戻すついでに巡回
[z3,y3,x3] = pol2cart(theta,rho,zz);


% 比較用にオイラー角回転(Y-Z-X)の計算
% 回転行列生成
RotateMatrix = eul2rotm([deg2rad(Angle(1)),deg2rad(Angle(2)),deg2rad(Angle(3))],'YZX');
Pos(1:20,1) = x;
Pos(1:20,2) = y;
Pos(1:20,3) = z;
for l = 1:20
    Pos(l,1:3) = Pos(l,1:3)*RotateMatrix;
end

% 出力
p = plot3(x3,y3,z3,Pos(1:20,1),Pos(1:20,2),Pos(1:20,3));
axis equal;
grid on;
xlabel('X');
ylabel('Y');
zlabel('Z');
```
### 実行結果
test.mlxを実行するとこのようなグラフが生成されます。
比較用にオイラー角で回転させた直線もプロットしているので2本の直線が見えることが期待されます。
直線が1本しか見えていないのは2つの直線が一致しているからと考えられます。実際、スクリプトの比較用のオイラー角回転の回転順を変えて、実行すると2本の直線を見ることができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3591772/0665658f-0212-7e2a-db7d-20d5423616d3.png)


## 課題
euler_rotation.mcfunctionで実装されたオイラー角回転には1つ問題点があります。それは回転順がY軸→Z軸→X軸であることです。
理由はアーマースタンドのNBT、Poseにあります。
Poseは3つの角度を指定し、その角度をもとにオイラー角回転を行います。
その回転順はZ軸→Y軸→X軸の順であり、実装したオイラー角の回転順と一致しません。もちろん、回転順が違えば、一般に回転結果も変わってしまいます。
そのため、2つの回転順が一致しないことは回転結果も一致しないことになります。
この回転結果の不一致は今回実装したオイラー角回転の実用性を著しく下げています。

## おわりに
思いついたときはとても良いアイディアだと思っていたばっかりに実用性を確保できなかったところが悔しいですね。