各種の変換(トランスフォーム)
今まで、物体の移動(位置)、回転と拡大縮小をUnityの「transform」コンポーネントを使ってやってきました。

実は裏側でなにが行われているのか、学んでいきましょう。
移動(位置)
一番簡単なのは、「移動」(位置)である。基本的に、メッシュの各頂点を移動させたい量を足すだけで実現できる。学んできた「ベクトルの足し算」を使えば、すぐ実装できる:
|
|
ここで「v'」は移動した頂点で、「v」は元の頂点で、「t」は移動ベクトルである。
練習
MyTransform のスクリプトを作成し、MyCube で作った立方体を移動する。
- 頂点の位置を変えないとけいないので、MyCube の頂点を読み込む(
GetVertices)、書き込むメソッド(SetVertices)を作成してください。GetVerticesは頂点配列のコピーを返してください :return (Vector3[])vertices.Clone();- そうしないと、元の頂点の位置が失ってしまいます~
- MyTransform で MyCubeの参照を
GetComponentで取得し、頂点を変換する。- 移動量(位置)を Inspector で変えられるようにしてください。
Translate(Vector3[] vertices)で移動を実装Updateで- 頂点を取得
Translateで移動- 変換した頂点を設定
MyCube の GetVertices と SetVertices
// メッシュの頂点を返す
public Vector3[] GetVertices()
{
// 元々の頂点データを守りたいので、コピーを返す
return (Vector3[])vertices.Clone();
}
// メッシュの頂点を設定
public void SetVertices(Vector3[] vertices)
{
mesh.vertices = vertices;
mesh.RecalculateNormals(); // 方法ベクトル再計算
}
MyTransform スクリプト
// 自分で作った変換(Transform)コンポーネント
public class MyTransform : MonoBehaviour
{
// 位置・移動量
[SerializeField]
private Vector3 position;
// 立方体
private MyCube cube;
private void Start()
{
// MyCubeを取得し、変換する前の頂点を保管しておく
cube = GetComponent<MyCube>();
}
private void Update()
{
// 頂点を取得
Vector3 [] vertices = cube.GetVertices();
// 移動を適用
Translate(vertices);
// 変換した頂点を適用
cube.SetVertices(vertices);
}
// 頂点を移動する
private void Translate(Vector3[] vertices)
{
// 1個ずつを処理
for (int i = 0; i < vertices.Length; i++)
{
// ベクトルの加算で移動を実現!
vertices[i] = vertices[i] + position;
}
}
}
拡大縮小(スケール)
拡大縮小は「すべての頂点を大きくする」または「小さくする」ということで、それぞれの頂点の「倍率」の値をかければ良いでしょう(ベクトルと実数の掛け算)
|
|
練習
MyTransform を編集して…
- Inspectorで倍率の係数を変えれるようにする(デフォルト値:1)
Updateで、Translateの前にScaleを適用
MyTransform スクリプト(編集分)
// 拡大縮小
[SerializeField]
private float scale = 1;
private void Update()
{
// 頂点を取得
Vector3 [] vertices = cube.GetVertices();
// 拡大縮小
Scale(vertices);
// 移動を適用
Translate(vertices);
// 変換した頂点を適用
cube.SetVertices(vertices);
}
private void Scale(Vector3[] vertices)
{
// 1個ずつを処理
for (int i = 0; i < vertices.Length; i++)
{
// ベクトルの乗算で拡大縮小を実現!
vertices[i] = vertices[i] * scale;
}
}
正しいスケール
よく見れば、Unityでは軸は別々でスケールできるよね。作ってきた「Scale」は3つの軸を同時にスケールしてしまう。ならば、少し式を変えないといけないが、ベクトル同士の掛け算ができないはず…
|
|
ベクトル「v」と「s」の各要素の乗算したいけど、ベクトル同士の乗算がない(外積と内積どっちも使えない!)
ちょっと待って…「各要素の乗算」って行列でできるのでは?たしかに、これができる!
|
|
ここで「S」のことは「スケール行列」といい、各頂点にかければ、正しく拡大縮小を実装できる。
練習
MyTransform を編集して…
- 倍率の処理を削除
- Inspectorでx, y, zの各軸の拡大縮小をを変えれるようにする(デフォルト値:1, 1, 1)
Scaleを更新し、スケール行列を作成し、変換してください。
MyTransform スクリプト(編集分)
// 拡大縮小
[SerializeField]
private Vector3 scale = new Vector3(1, 1, 1);
private void Scale(Vector3[] vertices)
{
// スケール行列を作成
Matrix s = new Matrix(3, 3);
s.Set(0, 0, scale.x);
s.Set(1, 1, scale.y);
s.Set(2, 2, scale.z);
// 1個ずつを処理
for (int i = 0; i < vertices.Length; i++)
{
// 行列の乗算で拡大縮小を実現!
vertices[i] = s.Multiply(vertices[i]);
}
}
回転
これはややこしい…
2次元の回転
まず、2Dの回転を考えてみましょう。三角関数を使って回転できるはずなので…

ある頂点「v=(vx, vy)」の位置を角度「θ」で回転したら、新しい位置「v’=(v’x, v’y)」を求めたい。これで難しいので、問題を分解して解決しましょう。
ベクトル v=(vx, vy) を成分 vx と vy に分解し、別々で回転し、加法でまた合体すれば、回転した v' を求めることができる。名前をわかりやすくするため各成分のベクトルを「a」と「b」にしましょう。
「a」の回転
「a」の回転には、単位円、および三角関数で学んだ「サイン」と「コサイン」で回転できることがわかる。確か:

「a」を「θ」度で回転すれば、a' になる。ここで:
|
|
|
ただし、「a」は「v」の「x」成分なので、以下のは事実である:
ので、ベクトル「a」のノルムは:
|
|
つまり
|
|
|
「b」の回転
「b」の回転は同じ考え方で行うのですが、幾何学的にはわかりにくいので、xy 座標系を倒してみましょう:

ここでまたサインとコサインを使えるが、x と y 軸が倒したので、以下のようになる:
|
|
|
そして、「b」は「v」の「y」成分なので、以下のは事実である:
|
|
ので、ベクトル「b」のノルムは:
|
|
したがって、
|
|
|
「a'+b'」の合体
分解したベクトル「v」の回転「v'」、回転した成分「a'」と「b'」の加法で復元できる。つまり:
|
|
ベクトルの加法は各成分の足し算なので:
|
|
上記で求めた式を書き換えるとこうなる:
|
|
|
これは回転ベクトル「v'」になる。ただし、多少読みにくいので、ベクトル×行列として表現ができる:
|
|
ここで「R(θ)」は「回転行列」という。ある2次元の位置(頂点)「v」を角度「θ」を回転したいなら、この行列の掛け算で回転した後に位置を求めることができる。
3次元の回転
2次元の場合は問題ないが、3Dの場合はどう変わる?まず、Z軸周りの回転を見てみましょう。実は、この回転は2Dの回転と全く同じであり、Zの値はずっとゼロになるだけ:

つまり:
|
|
これは「Z軸周りの回転行列」である。上記と同じ考えを持ち、X軸とY軸周りの回転行列を求めることができる
|
|
|
|
練習
MyTransform を編集して…
- Inspectorでx, y, zの各軸の回転角度を設定できるようにする。
- 各軸を回転行列を作成するメソッドを作成してください
Matrix MakeRotX();Matrix MakeRotY();Matrix MakeRotY();
void RotateEuler(Vector3[] vertices)(オイラー角度の回転)メソッドを実装- 3つ回転行列を作成し、X→Y→Zの順番で処理し、回転する
MyTransform スクリプト(編集分)
// X軸周りの回転行列を作成
private Matrix MakeRotX()
{
// 度→ラジアン
float radian = Mathf.Deg2Rad * rotation.x;
// サインとコサインを計算
float c = Mathf.Cos(radian);
float s = Mathf.Sin(radian);
// 行列を構築
Matrix m = new Matrix(3, 3);
m.Set(0, 0, 1);
m.Set(0, 1, 0);
m.Set(0, 2, 0);
m.Set(1, 0, 0);
m.Set(1, 1, c);
m.Set(1, 2, s);
m.Set(2, 0, 0);
m.Set(2, 1, -s);
m.Set(2, 2, c);
return m;
}
// Y軸周りの回転行列を作成
private Matrix MakeRotY()
{
// 度→ラジアン
float radian = Mathf.Deg2Rad * rotation.y;
// サインとコサインを計算
float c = Mathf.Cos(radian);
float s = Mathf.Sin(radian);
// 行列を構築
Matrix m = new Matrix(3, 3);
m.Set(0, 0, c);
m.Set(0, 1, 0);
m.Set(0, 2, -s);
m.Set(1, 0, 0);
m.Set(1, 1, 1);
m.Set(1, 2, 0);
m.Set(2, 0, s);
m.Set(2, 1, 0);
m.Set(2, 2, c);
return m;
}
// Z軸周りの回転行列を作成
private Matrix MakeRotZ()
{
// 度→ラジアン
float radian = Mathf.Deg2Rad * rotation.z;
// サインとコサインを計算
float c = Mathf.Cos(radian);
float s = Mathf.Sin(radian);
// 行列を構築
Matrix m = new Matrix(3, 3);
m.Set(0, 0, c);
m.Set(0, 1, -s);
m.Set(0, 2, 0);
m.Set(1, 0, s);
m.Set(1, 1, c);
m.Set(1, 2, 0);
m.Set(2, 0, 0);
m.Set(2, 1, 0);
m.Set(2, 2, 1);
return m;
}
// オイラー角度の回転
private void RotateEuler(Vector3[] vertices)
{
Matrix rotX = MakeRotX();
Matrix rotY = MakeRotY();
Matrix rotZ = MakeRotZ();
// 1個ずつを処理
for (int i = 0; i < vertices.Length; i++)
{
// X→Y→Zの順番で回転
Vector3 v = vertices[i];
v = rotX.Multiply(v);
v = rotY.Multiply(v);
v = rotZ.Multiply(v);
vertices[i] = v;
}
}
すべての変換を統一する:同次座標
スケールと回転とも3x3の行列で、「行列×ベクトル」のパターンを使うことができるが、位置(移動)は、ベクトル同士の足し算なので、処理の一体化できない。すべての変換を統一できる行列にできれば、処理も統一できるでので、新しい概念を学びましょう。
ちょっと見てみましょう:各成分(x, y, z)を移動量(tx, ty, tz)を足さないといけないので、行列にするとしたら、こういう作戦がある。3x3の行列ならば、各成分の掛け算になるが、:

























