ゲーム数学 苦手であっても、ゲーム開発に絶対に必要な数学を理解しましょう。 三角関数  三角関数とは、平面三角法における、 角度の大きさ と 線分の長さ の 関係 を記述する関数 の族、およびそれらを拡張して得られる関数の総称である。  簡単にいうと、上記の図で 角度 θ が知っていれば、成分 p x と p y の数値は何? 成分 p x と p y が知っていれば、角度 θ はどうなる? この関係が「三角関数」で求めることができる。 「ラジアン」とは  三角法に由来する三角関数という呼び名のほかに、単位円を用いた定義に由来する円関数という呼び名がある。単位円とは、半径が 1 の円のことである。  円の周囲の長さは 2πr である前提で、今回の半径 r の長さは「1」であるため、単位円の周囲はちょうど 2π であること証明する。また、円は「度」で分割すると、0度から360度まで表すことができる。従って、次の関係を作ることができる: 2π=360度  ここで「ラジアン」という新しい角度の測り方を考えることができる。360度 は  2π ラジアンであるとしたら、「度」から「ラジアン」へ変換にしたいなら: ここで「rad」は「ラジアン」(radians)にし、「deg」は「度」(degrees)である。また、ラジアンから度に変換したいなら:  の逆計算もできる。例えば、90度をラジアンに変換すると:  90度は約1.57079ラジアンになる。具体的に、図でみると  Unityの多くの関数は「度」(degree)を使うが、これから学ぶ(復習?)三角関数は必ず「ラジアン」を使うので、注意してください。また、UnityのAPIの中でラジアンと度を変換するための定数がすでに定義されている。 // 度(degree)からラジアン(radian)へ変換する場合 float radian = degree * Mathf.Deg2Rad; // ラジアン(radian)から度(degree)へ変換する場合 float degree = radian * Mathf.Rad2Deg; 角度(ラジアン単位)と成分 x, y の関係性  では、単位円の上にある点  p=(p x ,p y ) を配置してみましょう: 点 p を自由に選んだため、 p x と p y の長さがわかるはず。ここで角度を求めたいなら、 p までの弧の長さを測れば良いでしょう! つまり、ある点 p の座標 ( p x , p y ) がわかるし、弧の長さ θ もわかるので関係性を作ることができる。その関係性は: サイン(sin)とコサイン(cos)は、ある角度 θ と 成分の長さ px, py の関係を表す関数(三角関数)である 分かりやすい例 解けやすい成分を確認してみましょう: p x = 1, p y = 0 この場合は、角度どうなる?  明らかに角度は 0度 になっていることがわかる。ラジアンもゼロである。つまり、関係性は: p x = 0, p y = 1 この場合は、角度どうなる?  明らかに角度は 90度 になっていることがわかる。ラジアンにすれば π/2である。つまり、関係性は: p x = -1,  p y = 0 では、ここは:  明らかに角度は 180度 になっていることがわかる。ラジアンにすれば πである。つまり、関係性は: Unityの三角関数  Unityでは、以下の関数を準備されている: // theta はラジアン単位である! float px = Mathf.Cos(theta); float py = Mathf.Sin(theta); 逆にできないか? 今まで、角度から成分の変換してきたが、成分から角度の変換もできる。計算がやや難しいので、Unityは単純な関数がある: // 成分から角度(ラジアン単位)へ変換 float theta = Mathf.Atan2(py, px); 単位円ではない場合  今までの関係性はすべて「単位円」である前提で求めてきたが、ゲームを開発中には、半径が必ず「長さ1」であることを保証できない。この場合は、適切に半径の長さを掛ける(または割る)。例えば:半径は「10」であるとしたら: float radius = 10; float px = Mathf.Cos(theta) * radius; // 単位円 * 半径 float py = Mathf.Sin(theta) * radius; // 単位円 * 半径 float theta = Mathf.Atan2(py / radius, px / radius); // 単位円に戻し、計算 練習 Unityで三角関数を試してみましょう。スクリプトを作成し、角度と半径を変えながら、ある物体の位置(x, y)を変えてみてください。 スクリプト:SinCosTest // 三角関数(Sin, Cos)の練習 public class SinCosTest : MonoBehaviour { // 角度(度) [SerializeField] private float angle = 0; // 半径 [SerializeField] private float radius = 1; private void Update() { // 度からラジアン単位へ変換 float radian = angle * Mathf.Deg2Rad; // 角度から成分を求める float px = Mathf.Cos(radian); float py = Mathf.Sin(radian); // px と py は単位円の位置なので、半径を掛ける px *= radius; py *= radius; // 位置を設定 transform.position = new Vector3(px, py, 0); } }   線型代数学 ベクトル  ベクトルとは、一般的には「大きさと向きをもつ量」であり「矢印で表すことのできる量」である。  2次元のベクトルは2つの成分( x , y )があり、3次元の場合は3の成分( x , y , z )がある。Unityでは  Vector2 と Vector3 の構造体があり、それぞれのベクトルを作成ができる。 Vector2 vec2D = new Vector2(vx, vy); // vxとvyはfloatである Vector3 vec3D = new Vector3(vx, vy, vz); // vzもfloatである  ゲームの中で、ベクトルをよく使うものである。例えば、3D空間で、キャラクターの速度が「左向き:秒速2メートル、前方:秒速10メートル」であるとしたら… // 3D空間だと、x:左右、y:上下、z:前後 Vector3 velocity = new Vector3(-2, 0, 10);  また、ゲームの場合は、向きと量はもちろんそうであるが、その他に「位置」もベクトルで表現する。 // プレーヤーはシーンの中心点から前方100メートルにいる Vector3 playerPosition = new Vector3(0, 0, 100); 演算  やはり、ベクトルは重要なので、ベクトルの演算を学ばないといけない。 加算  2つのベクトルを足すことができる。その結果は各成分の足し算である: 2次元の場合 : 3次元の場合 : Unityでは「+」の演算子で2つのベクトルを足すことができる Vector3 v = new Vector3(vx, vy, vz); // vx, vyとvzは何かのfloat型の数値 Vector3 w = new Vector3(wx, wy, wz); // wx, wyとwzは何かのfloat型の数値 Vector3 vw = v + w;  幾何学的に、足し算は2つの矢印を連続し、1つ目のベクトルの始点から2つ目のベクトルの終点までに新しいベクトルを作成するイメージ: 減算  引き算は同じ形で行う: Vector3 v = new Vector3(vx, vy, vz); // vx, vyとvzは何かのfloat型の数値 Vector3 w = new Vector3(wx, wy, wz); // wx, wyとwzは何かのfloat型の数値 Vector3 vw = v - w;  幾何学的には、引くベクトルの向きを反転してから連続に繋がる 練習 加算と減算を使い、2つのオブジェクトを相対的に移動 シーンに2つの3Dゲームオブジェクトを追加し、距離的に放してください。 1つ目の名前は「Control」 2つ目の名前は「Follower」 スクリプト「VectorTest」を作成し… Follower と Control の相対位置を求める(減算) Control をシーン内で動かしたら、Follower が移動を真似する(加算) スクリプト:VectorTest // ベクトル演算の練習」 public class VectorTest : MonoBehaviour { // 移動すべきのオブジェクト [SerializeField] private GameObject control; // controlを追跡するオブジェクト [SerializeField] private GameObject follower; // controlとfollowerの相対位置 private Vector3 relativePosition; private void Start() { // 初期位置を取得 var v = control.transform.position; var w = follower.transform.position; // 相対位置を計算(減算で) relativePosition = w - v; } private void Update() { // followerの位置が、controlの位置 + 相対位置 follower.transform.position = control.transform.position + relativePosition; } } 実数との掛け算、割り算  ベクトルを実数との掛け算ができる。各成分との掛け算になる: Unityでは、普通の掛け算演算子「*」で計算できる: Vector3 v = new Vector3(vx, vy, vz); // vx, vyとvzは何かのfloat型の数値 Vector3 nv = v * n; // nは何かの実数(float)である 練習 先ほど作った「VectorTest」を更新し、相対位置の「倍率(scale)」を Inspector で変えられるようにする。 スクリプト:VectorTest // ベクトル演算の練習」 public class VectorTest : MonoBehaviour { // 移動すべきのオブジェクト [SerializeField] private GameObject control; // controlを追跡するオブジェクト [SerializeField] private GameObject follower; // 相対位置の倍率 [SerializeField] private float scale = 1; // controlとfollowerの相対位置 private Vector3 relativePosition; private void Start() { // 初期位置を取得 var v = control.transform.position; var w = follower.transform.position; // 相対位置を計算(減算で) relativePosition = w - v; } private void Update() { // 倍率を適用する Vector3 scalePos = relativePosition * scale; // followerの位置が、controlの位置 + 相対位置 follower.transform.position = control.transform.position + scalePos; } } ベクトルの長さ 場合により、ベクトルの長さを求めたいときもある。これには「ピタゴラスの定理」(三平方の定理)を利用する。ピタゴラスの定理を思い出しましょう。直角三角形があれば… c 辺の長さは: つまり… これはベクトルの成分とよく似ているので: これができる! ここで || v || はベクトルの「ノルム」といわれ、ベクトルの長さを表している。Unityでは、Vectorの便利な「magnitude」がある: Vector2 p = new Vector2(3, 4); float len = p.magnitude; // len は p の長さ(5)である 単位ベクトル  ベクトルの長さを「1」にすると、そのベクトルは単位円の中に入り、各成分がsinとcosを一致させることができる。その他にも、ベクトルの長さを調整する前に、まず長さを1に戻さないといけないが必要である。その場合は単位ベクトルを求めないといけない。  単位ベクトルを求めるのは、ベクトルをノルムで割るだけ: Unityでは「normalized」というプロパティーで単位ベクトルを求めることができる。 // 任意の長さのベクトル v があるとして: Vector3 v = new Vector3(30, 12, -15); // uはvと同じ向き(方向)であるが、長さは「1」である(単位ベクトル) Vector3 u = v.normalized; ベクトル同士の”掛け算”(内積と外積)  ベクトル同士の掛け算はできないが、2つの特別な関数がある。内積(dot product)と外積(cross product)。それぞれは限られた使い方があるが、必要な時にとても便利である。 内積  内積の定義は以下の通りになる:  ある2つのベクトル「v」と「w」の内積は、それぞれのベクトルのノルム(長さ)とその2つのベクトルの間の角度のコサインの掛け算である。答えは実数である。  これは何に便利かというと、2つのベクトルの間の角度を求めたいなら、内積を使えば良いでしょう。まず、vとwを単位ベクトルにすると、長さは1になるので、式は単純になる: そして、角度を求めるのは: Unityで内積の計算を実装されている: Vector3 v = new Vector3(vx, vy, vz); Vector3 w = new Vector3(wx, wy, wz); float dot = Vector3.Dot(v, w); // 内積 // 角度を求めたいなら float radians = Mathf.Acos(dot); // そして float angle = radians * Mathf.Rad2Deg; 練習 警備員の視野を利用するゲーム 警備員は「視野」があり、前方±角度しか見えない 泥棒が視野の中に入ってれば→発見! 警備員のスクリプト // 警備員の前に泥棒がいるかいないか確認する public class GuardMan : MonoBehaviour { // 泥棒への向きが欲しいので、泥棒が必要 [SerializeField] private GameObject thief; // 視野(度) [SerializeField] private float viewAngle; // 色を変えるため private MeshRenderer rend; private void Start() { rend = GetComponent(); } private void Update() { // 警備員の前向きのベクトル Vector3 v = transform.forward; // 青い矢印のこと // 泥棒への向き Vector3 w = thief.transform.position - transform.position; // 単位ベクトルにする w.Normalize(); // 内積で角度のコサインを求める float dot = Vector3.Dot(v, w); // 内積からラジアン角度を求める float radian = Mathf.Acos(dot); // 度に変換 float angle = radian * Mathf.Rad2Deg; // 警備員と泥棒の角度が視野よりも小さければ、発見! if (angle < viewAngle) rend.material.color = Color.red; // 見えない else rend.material.color = Color.green; } }   外積 2つのベクトル外積の式は以下の通りである: そして、Unityでも計算を実装されている: Vector3 v = new Vector3(vx, vy, vz); Vector3 w = new Vector3(wx, wy, wz); Vector3 cross = Vector3.Cross(v, w); // 外積の結果は新しいベクトルである  式よりも、外積の幾何学的な意味は重要。2つの3Dベクトル「v」と「w」は平面を定義すし、それで「v」と「w」の外積の結果は: 新しい3Dベクトルである このベクトル「v」と「w」で定義された平面を直交する また、長さは 従って、2つのベクトルがあれば、そのベクトルで作られた「面」を直交するベクトルを求めることができる。面の「法線ベクトル」、または3D空間の傾きの方向を求めることができる! 行列  数を縦と横に矩形状に配列したものである。横に並んだ一筋を行(row)、縦に並んだ一筋を列(column)と呼ぶ。例えば、下記のような行列: は2つの行と3つの列によって構成されているため、(2,3)型または2×3型の行列と呼ばれる。 Unity は行列のクラスがないため、自分で作ってみましょう。 練習 「Matrix」クラスを実装して: サイズ(行数、列数)を設定できるようにする 各要素を(行:r、 列 c) float Get(int r, int c) で取得できるようにする。 void Set(int r, int c, float value) で代入できるようにする。 string ToString(); のメソッドを実装し、中身を文字列として返すようにする 最後に、TestMatrix スクリプトを作成し、操作を確認してください。 補足:C#の1次元の配列を2次元の行列へ  行列は「行」と「列」2次元があるが、C#の配列は1次元しかない。2次元の行列を作成するため、以下の変換を行わないといけない: int i = r * cols + c; ここで、 i = 1次元(配列)の指数 r = 行列の「行」 c = 行列の「列」 cols = 行列の列数 例えば、4×3の行列で「 列:3、行:2」の要素を求めたい。ならば…  c = 2  r = 1  cols = 4  i = r * cols + c = 1 * 4 + 2 = 6 Matrix クラス // 行列クラス public class Matrix { // 列の数 private int rows; // 行の数 private int cols; // 行列の数値(配列) private float[] data; // コンストラクタ(インスタンスする時に呼び出す) // rows: 行の数 // cols: 列の数 public Matrix(int rows, int cols) { this.cols = cols; this.rows = rows; data = new float[cols * rows]; } // 数値を読む // r: 行 // c: 列 // 行列の数値を返す public float Get(int r, int c) { // 配列の位置(指数)を計算 int i = r * cols + c;; return data[i]; } // 数値を代入 // r: 行 // c: 列 // value:代入したい数値 public void Set(int r, int c, float value) { // 配列の位置(指数)を計算 int i = r * cols + c; // 代入 data[i] = value; } // 文字列にする public override string ToString() { // 表示用の文字列 string s = ""; // 1行ずつ for (int j = 0; j < rows; j++) { // 1列ずつ for (int i = 0; i < cols; i++) { // 数値を取得 float v = Get(j, i); // 数値を文字列にして、連結する s += $"{v:0.00} "; } // 改行 s += "\n"; } // 返す return s; } } TestMatrix スクリプト // 行列を確認するため public class MatrixTest : MonoBehaviour { private void Start() { PrintMatrix(); } private void PrintMatrix() { Matrix m = new Matrix(2, 3); m.Set(0, 0, 1); m.Set(0, 1, 9); m.Set(0, 2, -13); m.Set(1, 0, 20); m.Set(1, 1, 5); m.Set(1, 2, -6); // ToString() を実装したので、自動的に文字列に変換される Debug.Log(m); } } 足し算(加法)  2つの行列「A」と「B」、「A」と「B」のサイズが「m × n」である場合、A+Bの結果行列「C」は、各要素の足し算である。 もっと簡単にすると、各要素 c ij の値は: ここで「i」は「行」と「j」は「列」とする 練習 「Matrix」クラスを拡張して: Matrix Sum (Matrix b) を追加し、加法を実装する 自分と渡された行列「b」を足し、新しい行列を返す 行数、または列数が合わない場合は Debug.LogError でエラーを表示し、null を返す TestMatrix を更新し、以下の2つの行列を足してください。 Sum メソッド // 行列の加算 public Matrix Sum(Matrix b) { // 同じサイズかを確認する if (cols != b.cols || rows != b.rows) { Debug.LogError("行列のサイズが合わない"); return null; } // 結果の行列 Matrix result = new Matrix(rows, cols); // 1行ずつ for (int r = 0; r < rows; r++) { // 1列ずつ for (int c = 0; c < cols; c++) { float va = Get(r, c); // 自分の数値を求める float vb = b.Get(r, c); // 相手の数値を求める result.Set(r, c, va + vb); // 足して、代入 } } // 返すだけ return result; } TestSum メソッド void TestSum() { // 3行2列の行列を作成 Matrix a = new Matrix(3, 2); // 数値を代入 a.Set(0, 0, 4); a.Set(0, 1, 6); a.Set(1, 0, -1); a.Set(1, 1, 0); a.Set(2, 0, 8); a.Set(2, 1, -3); Debug.Log(a); Matrix b = new Matrix(3, 2); b.Set(0, 0, 5); b.Set(0, 1, 1); b.Set(1, 0, -2); b.Set(1, 1, 2); b.Set(2, 0, 2); b.Set(2, 1, 3); Debug.Log(b); // 行列の加算 Matrix c = a.Sum(b); Debug.Log(c); } 掛け算(乗法)  2つの行列「A」と「B」をかけることができる。ただし、「A」の列数は「B」の行数を等しくする条件を満たさないといけない。つまり、「A」は「l × m」であれば、「B」は「m × n」を確かめないといけない。この場合は、結果行列「C」のサイズは「l × n」になる。 この場合は、各要素 c ij の掛け算の式は以下にある: つまり… 行列「C」の「i, j」要素は、行列「A」の「i行目」× 行列「B」の「j列目」を全て足す結果。例えば:   注意:行列の積は可換でない (行列による変換の話のときに、ここを注意しましょう) 練習 「Matrix」クラスを拡張して: Matrix Multiply (Matrix b) を追加し、乗法を実装する 自分と渡された行列「b」を足し、新しい行列を返す 行数、または列数が合わない場合は Debug.LogError でエラーを表示し、null を返す 上記のAとB行列で確認。結果は: Multiply メソッド // 行列の掛け算 public Matrix Multiply(Matrix b) { // 掛け算ができることを確認 if (cols != b.rows) { Debug.LogError("行と列の数が一致していない"); return null; } // 返す行列(自分の行数 x 相手の列数) Matrix result = new Matrix(rows, b.cols); // 1行ずつ(結果行列) for (int i = 0; i < rows; i++) { // 1列ずつ(結果行列) for (int j = 0; j < b.cols; j++) { // 1数値ずつ float sum = 0; for (int k = 0; k < cols; k++) { sum += Get(i, k) * b.Get(k, j); } result.Set(i, j, sum); } } return result; } TestMultiply メソッド void TestMultiply() { // 2行3列の行列を作成 Matrix a = new Matrix(4, 2); // 数値を代入 a.Set(0, 0, 1); a.Set(0, 1, 0); a.Set(1, 0, 3); a.Set(1, 1, -1); a.Set(2, 0, 2); a.Set(2, 1, 2); a.Set(3, 0, 5); a.Set(3, 1, 1); Debug.Log(a); Matrix b = new Matrix(2, 3); b.Set(0, 0, 4); b.Set(0, 1, 1); b.Set(0, 2, 2); b.Set(1, 0, 5); b.Set(1, 1, 0); b.Set(1, 2, 1); Debug.Log(b); // 行列の掛け算 Matrix c = a.Multiply(b); Debug.Log(c); } ベクトルと行列の掛け算 ベクトルを「m × 1」の行列として扱えば、普段に掛け算ができる: 練習 「Matrix」クラスを拡張し、Unityの Vector3 と Vector4 と掛け算(乗法)ができるようにする。 ベクトルを 3x1 または 4x1 の行列にする 普通の掛け算を計算 出来上がった行列をまたベクトルに変換し、返す 確認用: Multiply メソッド (Vector3) // 行列×ベクトルの掛け算 public Vector3 Multiply(Vector3 vector) { // ベクトルを3x1の行列にする Matrix b = new Matrix(3, 1); b.Set(0, 0, vector.x); b.Set(1, 0, vector.y); b.Set(2, 0, vector.z); // 普通の掛け算 Matrix c = Multiply(b); // cをまたベクトルにする Vector3 result = new Vector3(); result.x = c.Get(0, 0); result.y = c.Get(1, 0); result.z = c.Get(2, 0); return result; } Multiply メソッド (Vector4) // 行列×ベクトルの掛け算 public Vector4 Multiply(Vector4 vector) { // ベクトルを3x1の行列にする Matrix b = new Matrix(4, 1); b.Set(0, 0, vector.x); b.Set(1, 0, vector.y); b.Set(2, 0, vector.z); b.Set(3, 0, vector.w); // 普通の掛け算 Matrix c = this.Multiply(b); // cをまたベクトルにする Vector4 result = new Vector4(); result.x = c.Get(0, 0); result.y = c.Get(1, 0); result.z = c.Get(2, 0); result.w = c.Get(3, 0); return result; } TestMultiplyV3 メソッド private void TestMultiplyV3() { Matrix a = new Matrix(3, 3); a.Set(0, 0, 3); a.Set(0, 1, -5); a.Set(0, 2, 0); a.Set(1, 0, 2); a.Set(1, 1, 8); a.Set(1, 2, -2); a.Set(2, 0, 1); a.Set(2, 1, 3); a.Set(2, 2, 4); Vector3 v = new Vector3(3, -2, 1); Vector3 w = a.Multiply(v); Debug.Log(w); } TestMultiplyV4 メソッド private void TestMultiplyV4() { Matrix a = new Matrix(4, 4); a.Set(0, 0, 3); a.Set(0, 1, 1); a.Set(0, 2, 8); a.Set(0, 3, -2); a.Set(1, 0, 0); a.Set(1, 1, 4); a.Set(1, 2, 3); a.Set(1, 3, 7); a.Set(2, 0, 4); a.Set(2, 1, 11); a.Set(2, 2, -5); a.Set(2, 3, 2); a.Set(3, 0, 1); a.Set(3, 1, 5); a.Set(3, 2, 0); a.Set(3, 3, 9); Vector4 v = new Vector4(4, -3, 1, 1); Vector4 w = a.Multiply(v); Debug.Log(w); } この Matrix クラスを追って、後ほど使うので、大事にしてください~ 3Dモデルの仕組み(その1)  今まで、FBXファイルでモデルをダウンロードし、そのまま使ってきましたが、実際にどう作られているのか調べてみましょう。 頂点(Vertex / バーテックス)  頂点は3D空間の「ある位置」を表している。普通、3次元ベクトルとして表現する。この位置を適切に繋がることにより、3Dの形状を作成できる。例えば、普通の立方体は頂点は8点がある: 頂点の配列   0 (-1, -1, -1) 1 ( 1, -1, -1) 2 ( 1, 1, -1) 3 (-1, 1, -1) 4 (-1, -1, 1) 5 ( 1, -1, 1) 6 ( 1, 1, 1) 7 (-1, 1, 1)    Unityの場合は「Vertex3」型の配列で頂点を作成ができる。それぞれの頂点に「指数」(Index)を割り当てる必要がある。上の図だと、「0〜7」の指数をアサインした。ただし、必要に応じて、同じ頂点を重複する必要な場合もある。このときは、同じ位置に複数の頂点があり、それぞれが別々の指数を持っている。 頂点の配列   0 8 16 (-1, -1, -1) 1 9 17 ( 1, -1, -1) 2 10 18 ( 1, 1, -1) 3 11 19 (-1, 1, -1) 4 12 20 (-1, -1, 1) 5 13 21 ( 1, -1, 1) 6 14 22 ( 1, 1, 1) 7 15 23 (-1, 1, 1)  ここでは、各面は自分の頂点を持ち、隣の面と共有しない。つまり、この場合は6面 × 4点=24点である。 練習 MyCube スクリプトを作成して… Vector3 型の配列「 vertices 」を宣言 CreateVertices() メソッドを作成し、上記の頂点の配列を作成 Awake() メソッドの中に CreateVertices() を呼び出す          ※ Awake は Start よりも一歩早く走る MyCube スクリプト // 立方体 public class MyCube : MonoBehaviour { // 頂点の配列 private Vector3[] vertices; // Awake は Start よりも一歩早く実行する // 内部初期化(他のコンポーネントに依存しない)ときにはおすすめ private void Awake() { CreateVertices(); } // 頂点を作成する private void CreateVertices() { vertices = new Vector3[24]; vertices[0] = new Vector3(-1, -1, -1); vertices[1] = new Vector3(1, -1, -1); vertices[2] = new Vector3(1, 1, -1); vertices[3] = new Vector3(-1, 1, -1); vertices[4] = new Vector3(-1, -1, 1); vertices[5] = new Vector3(1, -1, 1); vertices[6] = new Vector3(1, 1, 1); vertices[7] = new Vector3(-1, 1, 1); vertices[8] = new Vector3(-1, -1, -1); vertices[9] = new Vector3(1, -1, -1); vertices[10] = new Vector3(1, 1, -1); vertices[11] = new Vector3(-1, 1, -1); vertices[12] = new Vector3(-1, -1, 1); vertices[13] = new Vector3(1, -1, 1); vertices[14] = new Vector3(1, 1, 1); vertices[15] = new Vector3(-1, 1, 1); vertices[16] = new Vector3(-1, -1, -1); vertices[17] = new Vector3(1, -1, -1); vertices[18] = new Vector3(1, 1, -1); vertices[19] = new Vector3(-1, 1, -1); vertices[20] = new Vector3(-1, -1, 1); vertices[21] = new Vector3(1, -1, 1); vertices[22] = new Vector3(1, 1, 1); vertices[23] = new Vector3(-1, 1, 1); } } 三角形(Triangle)  頂点3点を繋がると三角形(Triangle)を作ることができ、三角形の群は「メッシュ」(Mesh)という。  三角形を作成するときに、次の点を考慮しないといけない: ① 頂点の順番(三角形は時計回りか?逆時計回りか?)  3つの頂点の繋がり方は2つある:時計回り(Clockwise - CW)と反時計回り(Counter - Clockwise - CCW)。ゲーム開発では、この順番はとても重要である。これは「Winding Order」(巻き順)という。  ゲームエンジンの最適化と高速化のため、無駄な面(三角形)を計算処理から抜いた方が良いでしょう。普通、カメラに映らない面(カメラの後ろにあるもの)を計算から抜く。また、カメラに向いていない面(三角形の裏側)も、計算から抜くことが一般である。  三角形の「表」と「裏」が分かるには、頂点の順番を使う。Unityの場合は「時計回りの三角形は表面である」となっているが、別のエンジンは逆になる可能性があるので、注意してください。 ② 頂点を共有するか?別々にするのか?  2枚の三角形は1つの辺が接着しているときに、2つの選択しがある: 同じ頂点を使いまわし、共有する 頂点を重複し、面を作成するとき、別々の頂点を使う  とくにどれにするかは別にルールがなく、ゲームでやりたいことにより、適切な繋がり方を選ぶべきが、一般的には: なめらかなカーブを表現しようとし、同じ面であるとき→頂点を共有する(連続する面) 例:キャラクター、服装、植物、動物 硬い角を持っているオブジェ、別の面であるとき→別々の頂点にする(連続しない面) 例:壁、さいころ、ブロック、かくかくしかじかの形 ならば、普通の立方体の繋がりは、以下のようになる: 各面が自分の頂点を持っているが、同じ面の三角形は頂点を共有する 練習 MyCube スクリプトを拡張して… triangles の 整数型の配列を宣言 長さは「三角形の数 × 3」= 6 × 2 × 3 = 36 CreateTriangles() のメソッドを作成し、図のように3つの指数ごとに三角形を作成 例: triangles[0] = 0; triangles[1] = 3; triangles[2] = 1; Awake() で CreateTriangles() を呼び出す MyCube スクリプト(編集分) // 三角形の配列 private int[] triangles; // Awake は Start よりも一歩速く実行する // 内部初期化(他のコンポーネントに依存しない)ときにはおすすめ private void Awake() { CreateVertices(); CreateTriangles(); } // 三角形を作成する private void CreateTriangles() { triangles = new int[36]; int i = 0; // 前 triangles[i++] = 0; triangles[i++] = 3; triangles[i++] = 1; triangles[i++] = 1; triangles[i++] = 3; triangles[i++] = 2; // 右 triangles[i++] = 9; triangles[i++] = 10; triangles[i++] = 6; triangles[i++] = 9; triangles[i++] = 6; triangles[i++] = 5; // 下 triangles[i++] = 20; triangles[i++] = 16; triangles[i++] = 17; triangles[i++] = 20; triangles[i++] = 17; triangles[i++] = 21; // 左 triangles[i++] = 7; triangles[i++] = 8; triangles[i++] = 4; triangles[i++] = 7; triangles[i++] = 19; triangles[i++] = 8; // 後ろ triangles[i++] = 14; triangles[i++] = 12; triangles[i++] = 13; triangles[i++] = 14; triangles[i++] = 15; triangles[i++] = 12; // 上 triangles[i++] = 18; triangles[i++] = 11; triangles[i++] = 23; triangles[i++] = 18; triangles[i++] = 23; triangles[i++] = 22; } メッシュ(Mesh)  メッシュは三角形の群である。メッシュはあくまで「形状」を定義し、色や模様は別途で作成しなければならない(シェーダーのときにまた学ぼう!)  Unityでは「Mesh」というクラスがある。この「Mesh」クラスは、頂点と三角形を定義するクラスである。また、光の計算のための「法線ベクトル」や模様を貼り付けるための「UV座標」も持っている(これもシェーダーの時に学ぼう)  メッシュができたら、「MeshFilter」コンポーネントに渡し、「MeshRenderer」で描画する。 練習 「MyCube」クラスを作成し Mesh を作成 vertices と triangles を利用し、立方体メッシュを作成 マテリアルをInspectorで設定 Awake で 実行中に MeshFilter と MeshRenderer を追加し、描画する。 MyCube スクリプト(完成) using UnityEngine; // 立方体のメッシュを作成し、描画する public class MyCube : MonoBehaviour { // 立方体のマテリアル [SerializeField] private Material material; // 頂点の配列 private Vector3[] vertices; // 三角形の配列 private int[] triangles; // 作成したメッシュ private Mesh mesh; // Awake は Start よりも一歩速く実行する // 内部初期化(他のコンポーネントに依存しない)ときにはおすすめ private void Awake() { CreateVertices(); CreateTriangles(); mesh = new Mesh(); mesh.vertices = vertices; mesh.triangles = triangles; mesh.RecalculateNormals(); // 法線ベクトルを自動計算(※) // ゲームオブジェクトにMeshFilterを追加 MeshFilter mf = gameObject.AddComponent(); mf.mesh = mesh; // MeshRendererも追加 MeshRenderer mr = gameObject.AddComponent(); mr.material = material; } // 頂点を作成する private void CreateVertices() { vertices = new Vector3[24]; vertices[0] = new Vector3(-1, -1, -1); vertices[1] = new Vector3(1, -1, -1); vertices[2] = new Vector3(1, 1, -1); vertices[3] = new Vector3(-1, 1, -1); vertices[4] = new Vector3(-1, -1, 1); vertices[5] = new Vector3(1, -1, 1); vertices[6] = new Vector3(1, 1, 1); vertices[7] = new Vector3(-1, 1, 1); vertices[8] = new Vector3(-1, -1, -1); vertices[9] = new Vector3(1, -1, -1); vertices[10] = new Vector3(1, 1, -1); vertices[11] = new Vector3(-1, 1, -1); vertices[12] = new Vector3(-1, -1, 1); vertices[13] = new Vector3(1, -1, 1); vertices[14] = new Vector3(1, 1, 1); vertices[15] = new Vector3(-1, 1, 1); vertices[16] = new Vector3(-1, -1, -1); vertices[17] = new Vector3(1, -1, -1); vertices[18] = new Vector3(1, 1, -1); vertices[19] = new Vector3(-1, 1, -1); vertices[20] = new Vector3(-1, -1, 1); vertices[21] = new Vector3(1, -1, 1); vertices[22] = new Vector3(1, 1, 1); vertices[23] = new Vector3(-1, 1, 1); } // 三角形を作成する private void CreateTriangles() { triangles = new int[36]; int i = 0; // 前 triangles[i++] = 0; triangles[i++] = 3; triangles[i++] = 1; triangles[i++] = 1; triangles[i++] = 3; triangles[i++] = 2; // 右 triangles[i++] = 9; triangles[i++] = 10; triangles[i++] = 6; triangles[i++] = 9; triangles[i++] = 6; triangles[i++] = 5; // 下 triangles[i++] = 20; triangles[i++] = 16; triangles[i++] = 17; triangles[i++] = 20; triangles[i++] = 17; triangles[i++] = 21; // 左 triangles[i++] = 7; triangles[i++] = 8; triangles[i++] = 4; triangles[i++] = 7; triangles[i++] = 19; triangles[i++] = 8; // 後ろ triangles[i++] = 14; triangles[i++] = 12; triangles[i++] = 13; triangles[i++] = 14; triangles[i++] = 15; triangles[i++] = 12; // 上 triangles[i++] = 18; triangles[i++] = 11; triangles[i++] = 23; triangles[i++] = 18; triangles[i++] = 23; triangles[i++] = 22; } } 各種の変換(トランスフォーム)  今まで、物体の移動(位置)、回転と拡大縮小を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(); } 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=(v x , v y ) 」の位置を角度「 θ 」で回転したら、新しい位置「 v’=(v’ x , v’ y ) 」を求めたい。これで難しいので、問題を分解して解決しましょう。 ベクトル v=(v x , v y ) を成分 v x と v y に分解し、別々で回転し、加法でまた合体すれば、回転した 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の行列ならば、各成分の掛け算になる が、以下のように工夫すれば… 4x4の行列との掛け算ができるので、 移動行列 を、以下のように作ることができる: ベクトルと行列の掛け算をすると: 3Dベクトルに「1」の成分を追加されたベクトルは「 同次座標 ベクトル」という。成分を増やしても、次元数は同じである。 3D座標 ⇒ 同次座標の変換 普通の3Dベクトルを同次座標に変換するのは、1つの成分を追加し、「1」にするだけ: 同次座標 ⇒  3D座標の変換 ベクトルの移動、拡大縮小、回転で変換したあとに、最後の成分が必ず「1」であることを保証できない。なので、まずベクトルを最後の成分で割り、外す: 変換を統一する したがって、同次座標を使えば、すべての変換を 4x4 の行列として表現ができる: 移動 拡大縮小 回転(X) 回転(Y) 回転(Z) 練習 Matrix を編集し、Vector4との乗算できるようにする MyTransform を編集し: 頂点の配列を同次座標に変換 移動、拡大縮小、回転を同次座標に対応できるようにする 変換した後に同次座標の頂点をまた3Dベクトルに戻す Matrix の Multiply(Vector4) メソッド // 行列×ベクトルの掛け算 public Vector4 Multiply(Vector4 vector) { // ベクトルを4x1の行列にする Matrix b = new Matrix(4, 1); b.Set(0, 0, vector.x); b.Set(1, 0, vector.y); b.Set(2, 0, vector.z); b.Set(3, 0, vector.w); // 普通の掛け算 Matrix c = Multiply(b); // 結果をまたベクトルにする Vector4 result = new Vector4(); result.x = c.Get(0, 0); result.y = c.Get(1, 0); result.z = c.Get(2, 0); result.w = c.Get(3, 0); return result; }   同次座標を使用する MyTransform // 自分で作った変換(Transform)コンポーネント public class MyTransform : MonoBehaviour { // 位置・移動量 [SerializeField] private Vector3 position; // 拡大縮小 [SerializeField] private Vector3 scale = new Vector3(1, 1, 1); // 回転 [SerializeField] private Vector3 rotation; // 立方体 private MyCube cube; private void Start() { // MyCubeを取得し、変換する前の頂点を保管しておく cube = GetComponent(); } private void Update() { // 頂点を取得 Vector3 [] vertices = cube.GetVertices(); // 同次座標の頂点 Vector4[] v4 = Vector3ToVector4(vertices); // 回転 RotateEuler(v4); // 拡大縮小 Scale(v4); // 移動を適用 Translate(v4); // 同次座標からまた3次元へ vertices = Vector4ToVector3(v4); // 変換した頂点を適用 cube.SetVertices(vertices); } // 3Dベクトルから4D同次座標へ private Vector4[] Vector3ToVector4(Vector3[] vertices) { Vector4[] convert = new Vector4[vertices.Length]; for (int i = 0; i < convert.Length; i++) { Vector3 v3 = vertices[i]; convert[i] = new Vector4(v3.x, v3.y, v3.z, 1); } return convert; } // 4D同次座標から3Dベクトルへ private Vector3[] Vector4ToVector3(Vector4[] vertices) { Vector3[] convert = new Vector3[vertices.Length]; for (int i = 0; i < convert.Length; i++) { Vector4 v4 = vertices[i]; convert[i] = new Vector3(v4.x, v4.y, v4.z) / v4.w; } return convert; } // X軸周りの回転行列を作成 private Matrix MakeRotX() { // 度→ラジアン float radian = Mathf.Deg2Rad * rotation.x; // サインとコサインを計算 float c = Mathf.Cos(radian); float s = Mathf.Sin(radian); // 行列を構築 Matrix m = new Matrix(4, 4); m.Set(0, 0, 1); m.Set(0, 1, 0); m.Set(0, 2, 0); m.Set(0, 3, 0); m.Set(1, 0, 0); m.Set(1, 1, c); m.Set(1, 2, s); m.Set(1, 3, 0); m.Set(2, 0, 0); m.Set(2, 1, -s); m.Set(2, 2, c); m.Set(2, 3, 0); m.Set(3, 0, 0); m.Set(3, 1, 0); m.Set(3, 2, 0); m.Set(3, 3, 1); 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(4, 4); m.Set(0, 0, c); m.Set(0, 1, 0); m.Set(0, 2, -s); m.Set(0, 3, 0); m.Set(1, 0, 0); m.Set(1, 1, 1); m.Set(1, 2, 0); m.Set(1, 3, 0); m.Set(2, 0, s); m.Set(2, 1, 0); m.Set(2, 2, c); m.Set(2, 3, 0); m.Set(3, 0, 0); m.Set(3, 1, 0); m.Set(3, 2, 0); m.Set(3, 3, 1); 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(4, 4); m.Set(0, 0, c); m.Set(0, 1, -s); m.Set(0, 2, 0); m.Set(0, 3, 0); m.Set(1, 0, s); m.Set(1, 1, c); m.Set(1, 2, 0); m.Set(1, 3, 0); m.Set(2, 0, 0); m.Set(2, 1, 0); m.Set(2, 2, 1); m.Set(2, 3, 0); m.Set(3, 0, 0); m.Set(3, 1, 0); m.Set(3, 2, 0); m.Set(3, 3, 1); return m; } private void RotateEuler(Vector4[] vertices) { Matrix rotX = MakeRotX(); Matrix rotY = MakeRotY(); Matrix rotZ = MakeRotZ(); // 1個ずつを処理 for (int i = 0; i < vertices.Length; i++) { // X→Y→Zの順番で回転 Vector4 v = vertices[i]; v = rotX.Multiply(v); v = rotY.Multiply(v); v = rotZ.Multiply(v); vertices[i] = v; } } // 拡大縮小 private void Scale(Vector4[] vertices) { // スケール行列を作成 Matrix s = new Matrix(4, 4); s.Set(0, 0, scale.x); s.Set(1, 1, scale.y); s.Set(2, 2, scale.z); s.Set(3, 3, 1); // 1個ずつを処理 for (int i = 0; i < vertices.Length; i++) { // ベクトルの乗算で拡大縮小を実現! vertices[i] = s.Multiply(vertices[i]); } } // 頂点を移動する private void Translate(Vector4[] vertices) { // 移動行列を作成 Matrix t = new Matrix(4, 4); t.Set(0, 0, 1); t.Set(1, 1, 1); t.Set(2, 2, 1); t.Set(0, 3, position.x); t.Set(1, 3, position.y); t.Set(2, 3, position.z); t.Set(3, 3, 1); // 1個ずつを処理 for (int i = 0; i < vertices.Length; i++) { vertices[i] = t.Multiply(vertices[i]); } } } 変換の連結 ー 変換行列が連続に  移動、スケール、回転それそれは別々に実現できたが、同時に様々な変換できない状態になっている。あくまで1つの変換だけ。  でも、よく考えてみると、スケールと回転の場合は、行列とベクトルの乗算で変換できているので、連続に乗算を続けていけば、変換の連結ができる!つまり:  これは「Transform」コンポーネントをやっている仕事!各種の変換(Position, Scale, Rotate)のすべてを一つの行列にまとめて、最後に元の頂点にかけて、各頂点の新しい位置を求める。 練習 よく見ると、MyTransform では、変換ごとに for 文で頂点変換されている: // 移動、回転、拡大縮小の変換では、以下の処理がある: for (int i = 0; i < vertices.Length; i++) { vertices[i] = (なにかの変換行列).Multiply(vertices[i]); } 同じ処理が何度もかけるのは効率が悪いので MyTransform を編集し: 各変換を Matrix として返す Updateですべての行列を掛け算で連結する 回転X × 回転Y × 回転Z × 拡大縮小 × 移動 出来上がった 変換行列 を使って、1回だけ for文で頂点を更新 変換連結を使用する MyTransform using System; using UnityEngine; using UnityEngine.UIElements; // 自分で作った変換(Transform)コンポーネント public class MyTransform : MonoBehaviour { // 位置・移動量 [SerializeField] private Vector3 position; // 拡大縮小 [SerializeField] private Vector3 scale = new Vector3(1, 1, 1); // 回転 [SerializeField] private Vector3 rotation; // 立方体 private MyCube cube; private void Start() { // MyCubeを取得し、変換する前の頂点を保管しておく cube = GetComponent(); } private void Update() { // 頂点を取得 Vector3 [] vertices = cube.GetVertices(); // 同次座標の頂点 Vector4[] v4 = Vector3ToVector4(vertices); // 回転 Matrix rotX = MakeRotX(); Matrix rotY = MakeRotY(); Matrix rotZ = MakeRotZ(); // 拡大縮小 Matrix scale = MakeScale(); // 移動 Matrix translate = MakeTranslate(); // すべて連結する Matrix final = rotX.Multiply(rotY).Multiply(rotZ).Multiply(scale).Multiply(translate); // 変換 for (int i = 0; i < v4.Length; i++) v4[i] = final.Multiply(v4[i]); // 同次座標からまた3次元へ vertices = Vector4ToVector3(v4); // 変換した頂点を適用 cube.SetVertices(vertices); } // 3Dベクトルから4D同次座標へ private Vector4[] Vector3ToVector4(Vector3[] vertices) { Vector4[] convert = new Vector4[vertices.Length]; for (int i = 0; i < convert.Length; i++) { Vector3 v3 = vertices[i]; convert[i] = new Vector4(v3.x, v3.y, v3.z, 1); } return convert; } // 4D同次座標から3Dベクトルへ private Vector3[] Vector4ToVector3(Vector4[] vertices) { Vector3[] convert = new Vector3[vertices.Length]; for (int i = 0; i < convert.Length; i++) { Vector4 v4 = vertices[i]; convert[i] = new Vector3(v4.x, v4.y, v4.z) / v4.w; } return convert; } // X軸周りの回転行列を作成 private Matrix MakeRotX() { // 度→ラジアン float radian = Mathf.Deg2Rad * rotation.x; // サインとコサインを計算 float c = Mathf.Cos(radian); float s = Mathf.Sin(radian); // 行列を構築 Matrix m = new Matrix(4, 4); m.Set(0, 0, 1); m.Set(0, 1, 0); m.Set(0, 2, 0); m.Set(0, 3, 0); m.Set(1, 0, 0); m.Set(1, 1, c); m.Set(1, 2, s); m.Set(1, 3, 0); m.Set(2, 0, 0); m.Set(2, 1, -s); m.Set(2, 2, c); m.Set(2, 3, 0); m.Set(3, 0, 0); m.Set(3, 1, 0); m.Set(3, 2, 0); m.Set(3, 3, 1); 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(4, 4); m.Set(0, 0, c); m.Set(0, 1, 0); m.Set(0, 2, -s); m.Set(0, 3, 0); m.Set(1, 0, 0); m.Set(1, 1, 1); m.Set(1, 2, 0); m.Set(1, 3, 0); m.Set(2, 0, s); m.Set(2, 1, 0); m.Set(2, 2, c); m.Set(2, 3, 0); m.Set(3, 0, 0); m.Set(3, 1, 0); m.Set(3, 2, 0); m.Set(3, 3, 1); 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(4, 4); m.Set(0, 0, c); m.Set(0, 1, -s); m.Set(0, 2, 0); m.Set(0, 3, 0); m.Set(1, 0, s); m.Set(1, 1, c); m.Set(1, 2, 0); m.Set(1, 3, 0); m.Set(2, 0, 0); m.Set(2, 1, 0); m.Set(2, 2, 1); m.Set(2, 3, 0); m.Set(3, 0, 0); m.Set(3, 1, 0); m.Set(3, 2, 0); m.Set(3, 3, 1); return m; } // 拡大縮小 private Matrix MakeScale() { // スケール行列を作成 Matrix s = new Matrix(4, 4); s.Set(0, 0, scale.x); s.Set(1, 1, scale.y); s.Set(2, 2, scale.z); s.Set(3, 3, 1); return s; } // 移動変換行列を返す private Matrix MakeTranslate() { // 移動行列を作成 Matrix t = new Matrix(4, 4); t.Set(0, 0, 1); t.Set(1, 1, 1); t.Set(2, 2, 1); t.Set(0, 3, position.x); t.Set(1, 3, position.y); t.Set(2, 3, position.z); t.Set(3, 3, 1); return t; } } オイラー(Euler)回転の弱点:ジンバルロック ここまで学んできた回転方法は「オイラー」(Euler)回転といい、x, y, zの各軸ごとに別々に回転することにより、オブジェクトに連続に適用すると、自由に回転できる… 設定しても、回転順番を決めないといけない。X→Y→Z / Y→Z→X / X→Z→Y などの様々な組み合わせがあり、それぞれの最終結果がことなるので、混乱する可能性がある。  また、「ジンバルロック」という問題もある。次の装置を見てください。この装置は「X(赤)→  Y(緑)→  Z(青)」の順番で回転する装置。この状態で、物体が各軸の周りの回転を自由にできる。 X 軸の周り Y 軸の周り Z 軸の周り  ここで、Y軸の周りを90度回転してみると2つの回転軸が重なってしまうこの状態は「ジンバルロック」といい、1つの回転自由度が失ってしまった: 重なったXとZを回転しても、同じ回転になってしまう。 矢印の方向に回転したくても、できない  この場合はうまく回転できなくなるので、何かの解決を実装しないといけない。 一時的な解決:Angle-Axis(度-軸)回転  ジンバルロックになってしまった場合、一時的に新しい回転軸を作成し、その「仮想軸」の周りに回転ができる。この方法を使うと、定番のXYZだけではなく、お好みのベクトルの周りに回転ができる。  式は、今までの回転と同じアイデアである。sinとcosを使うが、回転する面はあるベクトル「 v 」を直交する  この場合は、回転行列は以下のようになる: ここで、 θ は回転角度で、   v は回転軸になる任意の3D単位ベクトルである これは今までの回転行列の一般な形である。例えば、回転軸は Z にすると: とすると、行列を更新すると: でも、 、なので… これで、Z 軸周りの回転行列に戻った! Unityのサポート Unityでは「 Quaternion.AngleAxis 」を使用し、任意の軸周りの回転ができる: // 以下、X 軸周りに45度を回転する相当: Vector3 axis = new Vector3(1, 0, 0); // 軸 float angle = 45; // 角度 transform.localRotation = Quaternion.AngleAxis(angle, axis); // 度・軸の回転 練習 惑星と月を真似しましょう。月が惑星の軸周りを従って回転させましょう: Moonスクリプトを作成し、 回転速度 追跡する惑星 惑星から離れる距離 Inspectorで設定できるようにする Quaternion.AngleAxis を使用し、惑星の「上向きベクトル」( transform.up ) の周りに回転する 実行し、Sceneビューで惑星を傾いて、回転を確認する。 汎用的な解決 Quaternion(クォータニオン - 四元数) 「クォータニオン」は、回転を表す4次元の数値である。一般な形は ここでは、「+」は加算ではなく、次元を表してる。ベクトルの表現にすると:  と等しいのである。4次元の数値であるため演算は多少ややこしいので、ゲームで使う目的だけに注目しましょう。   単位クォータニオン の特徴は: ジンバルロックせず、どの回転でも表現ができる 2つのクォータニオンを乗算は、2つの回転を連続に適用すると同じものである 逆クォータニオン(共役)は、逆回転と同じである クォータニオン⇔回転行列の変換ができる  クォータニオンがゲームの中で回転でよく使われるので、Unityがすでに実装されている。Unityは「 Quaternion 」というクラスがあり、すべての回転は「 Quaternion 」で行う。そもそも「 transform.rotation 」のデータ型は「 Quaternion 」だ! // Unityの中で、すべての回転は「Quaternion型」である Quaternion rot = transform.rotation; オイラー角度と Quaternion の変換 Unityではオイラー角度(XYZ回転)と Quaternion の変換メソッドが実装されている: // 回転を Quaternion として表現されている Quaternion rotA = transform.rotation; // オイラー角度を知りたいなら: Vector3 angles = rot.eulerAngles; // 逆に、オイラー角度から Quaternion にしたいなら: Quaternion rotB = Quaternion.Euler(x, y, z); // x, y, z は各軸の回転である // このまま回転として利用できる transform.rotation = rotB; 掛け算(乗算) クォータニオンが重要なので簡単にクォータニオンの掛け算を理解していきましょう。なお、式を覚えるよりも、掛け算はどのような影響があるのかを覚えてください。 2つのクォータニオン「q」と「p」があり、それぞれは別の回転であるとしたら「p × q」は、連続に2つの回転を適用したことと同じである。 クォータニオン「p」による回転 クォータニオン「p」による回転 クォータニオン「q」による回転 q × p の結果 式は教科書にある(泣きたくなるぐらい大変)が、Unity の Quaternion なら「*」で計算できる: // 「p」と「q」それぞれは「Quaternion」である Quaternion qp = q * p;  注意:クォータニオンの積は可換でない。回転の順番により、最後の結果が変わるので、行列と同様に: q × p p × q 回転行列との関係 任意の単位クォータニオンを今まで勉強してきた回転行列として表現ができるので、他の変換と組み合わせることができる。 「a, b, c, d」はクォータニオンの成分である。これで、 クォータニオンを回転行列に変換でき、他の変換(移動、拡大縮小)との連結できるようになる。 単純な物理 ゲームでよく使われそうな物理を練習しましょう。以下の処理は、Unity の Rigidbody を対応してくれる処理である。 移動、速度、加速度 移動と速度の関係性  次の状況を考えてみてください:あるキャラクターは2D空間の位置「 p = (p x , p y ) 」にいる。このキャラクターは定めた時間「 Δt 」をかけて、ある目的地「 q = (q x , q y ) 」に到着させたい。  この場合は、移動に必要な速度「 v = (v x , v y ) 」を求めることができる: 始点「 p 」から終点「 q 」までの距離(と向き)を到着までの必要な時間で割ると、速度「 v 」を求める。それでは、始点「 p 」と速度「 v 」があれば、定めた時間「 t 」の後に、どこにいるか知りたいなら: Unityでは、フレーム間の時間を「 Time.deltaTime 」で取得できるので、移動を計算したいなら: // 始点(現在の位置) Vector3 p = transform.position; // vは「Vector3」型の速度(量と向き) Vector3 q = p + v * Time.deltaTime; // 位置を更新 transform.position = q; 練習 シーンに何かの3Dオブジェクトを追加。スペースキーを押したら、一定速度で移動する。キーを放したら、速度を0にする。速度を Inspectorで設定できるようにしてください。 TestVelocityのスクリプト // 物理の練習:一定速度で移動 public class TestVelocity : MonoBehaviour { // 速度ベクトル(量+向き) [SerializeField] private Vector3 velocity; void Update() { // まず、速度はゼロにする Vector3 v = Vector3.zero; // 何かのキーを押したら if (Input.GetKey(KeyCode.Space)) v = velocity; // 速度を書き換える // 物理演算を使って、移動する Vector3 p = transform.position; // 移動の式 Vector3 q = p + v * Time.deltaTime; // 位置更新 transform.position = q; } }   加速度  速度は「時間単位当たりの物体の 位置 の変化量」であるとしたら、加速度は「時間単位当たりの物体の 速度 の変化量」である(速度の速度)。つまり: 速度「 v 」は、初期速度「 v 0 」に加速度「 a 」と加速した時間「 Δt 」の乗算を足す。Unityで実装するなら: // 内部の速度ベクトル private Vector3 velocity; private void Update() { // 初期速度 Vector3 v0 = velocity; // 新しい速度を計算 Vector3 v = v0 + a * Time.deltaTime // 次のフレームの初期速度を更新 velocity = v; } 練習 加速度を試せる TestAccel のスクリプトを作成してください。「A」キーを押したら一定加速度で移動する。「S」キーを押すと一定速度で減速する。速度が0以下にならないようにする。なお、加速度を Inspectorで設定できるようにしてください。 TestAccelのスクリプト // 物理の練習:加速度 public class TestAccel : MonoBehaviour { [SerializeField] Vector3 acceleration; // 加速度ベクトル(量+向き) Vector3 velocity; // 速度(内部) void Update() { // まずは、速度を更新 Vector3 a = Vector3.zero; // 加速なし // 「a」を押したら、加速 if (Input.GetKey(KeyCode.A)) a = acceleration; // 「s」を押したら、減速 else if (Input.GetKey(KeyCode.S)) a = -acceleration; // 現在の速度 velocity = velocity + a * Time.deltaTime; // 位置を更新(移動) Vector3 p = transform.position; Vector3 q = p + velocity * Time.deltaTime; transform.position = q; } } 重力 重力は下に向かう加速度ベクトルである。地球の場合は、おおよそ 9.81 m/s 2 である。 // 重力ベクトル Vector3 gravity = new Vector3(0, -9.81f, 0); 練習 ボールを投げるスクリプト「ThrowBall」を作成する。Inspectorでボールに初期速度を設定し、斜め上に投げ、重力で落とす。無限に落ちないように、Y軸の位置は0以下にならないようにする。 v 0 の初期速度にてボールを投げる。 g 重力の加速度であり、時間により落下してくる。 ThrowBall スクリプト // ボールを投げ、重力で落とす public class ThrowBall : MonoBehaviour { // ボールの初期速度 [SerializeField] private Vector3 velocity; private void Update() { // 重力による加速度を使い、速度を更新 Vector3 gravity = new Vector3(0, -9.81f, 0); velocity += gravity * Time.deltaTime; // 位置を更新 var position = transform.position; position += velocity * Time.deltaTime; // 高さが0以下にならないようにする position.y = Mathf.Max(0, position.y); transform.position = position; } } 質量、力と加速度の関係性 すべての物体は「質量」があり、重ければ重いほど動かすのは力が必要。従って、物体の加速度は、物体の重さ(質量)と力に依存する。その関係性以下の通りである: ここで、「 a 」は加速度であり、「 f 」は力、「 m 」は質量である。これは「 Rigidbody.AddForce 」の処理である。 練習 ThrowBall を更新し、ボールに質量を与え、キーを押したら力を与える。力のベクトルと質量をInspectorで変えられるようにする。 m はボールの質量である。 f は投げる力である。 g 重力の加速度であり、時間により落下してくる。 ThrowBall スクリプト // ボールを投げ、重力で落とす public class ThrowBall : MonoBehaviour { // ボールの質量 [SerializeField] private float mass; // 投げる力 [SerializeField] private Vector3 force; // 速度(内部) private Vector3 velocity; private void Start() { // 力と質量から加速度を計算 Vector3 a = force / mass; // 加速度は時間をかけて速度を変える // 投げる時間を 0.5秒にしましょう velocity = a * 0.5f; } private void Update() { // 重力による加速度を使い、速度を更新 Vector3 gravity = new Vector3(0, -9.81f, 0); velocity += gravity * Time.deltaTime; // 位置を更新 var position = transform.position; position += velocity * Time.deltaTime; // 高さが0以下にならないようにする position.y = Mathf.Max(0, position.y); transform.position = position; } } 当たり判定 Unityのコライダーのおかげで、当たり判定は自動的に計算され、「 OnCollisionXXX 」「 OnTriggerXXX 」で衝突があった時にお知らせを受け取る、処理ができるが、実際に裏側でなにを行っているのか理解してみましょう。 球体同士 まず、一番楽なのは、球体同士の当たり判定である。以下の図を確認し、考えてみましょう 円「 C 」(中心点 (c x , c y ) 、半径 c r )と「 D 」(中心点 (d x , d y ) 、半径 d r )があるとして、中心の間の距離は、中心の差のノルムである:    この距離は半径の和よりも小さければ、2つの円が衝突しているといえる。 つまり 練習 Unityで コライダーなし の2つの球体を作成し、MySphereCollider を作成し、半径を設定できるようにしてください(注意:コライダーの半径とメッシュの半径を合わせてください) つぎ、SphereCollisionCheck のスクリプトを作成し すべての MySphereCollider を取得 コライダー同士の距離を計算 半径よりも小さければ、色を変える MySphereCollider スクリプト // 当たり判定:球体のコライダー public class MySphereCollider : MonoBehaviour { // 半径 [SerializeField] private float radius; // 半径を返す public float GetRadius() { return radius; } // 色を変える public void ChangeColor(Color color) { // MeshRendererの色を変える MeshRenderer mr = GetComponent(); mr.material.color = color; } } SphereCollisionCheck スクリプト // コライダー同士の当たり判定を確認 public class SphereCollisionCheck : MonoBehaviour { // すべてのコライダー private MySphereCollider[] colliders; private void Start() { // FindObjectsByType = 「このスクリプトを付いているオブジェクトのすべて下さい」 colliders = FindObjectsByType(FindObjectsSortMode.None); } void Update() { // 初期値はすべて「false」 bool[] isCollision = new bool[colliders.Length]; // 全部 vs 全部を比較 for (int i = 0; i < colliders.Length - 1; i++) { for (int j = i + 1; j < colliders.Length; j++) { MySphereCollider iObje = colliders[i]; MySphereCollider jObje = colliders[j]; // 中心と中心の距離を計算 float distance = (iObje.transform.position - jObje.transform.position).magnitude; // 2つの半径の足し算を計算 float radiusSum = iObje.GetRadius() + jObje.GetRadius(); // 当たり判定 if (distance - radiusSum <= 0) { isCollision[i] = true; isCollision[j] = true; } } } // 衝突しているコライダーだけの色を変える for (int i = 0; i < isCollision.Length; i++) { if (isCollision[i]) colliders[i].ChangeColor(Color.red); else colliders[i].ChangeColor(Color.white); } } } 箱同士(回転なし) BoxCollider の処理を把握しましょう。2つの箱「C」と「D」があり、それぞれが「最小X」「最大X」「最小Y」「最大Y」の数値を持っている。 この場合は: ある箱の「最大X」は他の箱の「最小X」よりも小さければ → 衝突しない ある箱の「最大Y」は他の箱の「最小Y」よりも小さければ → 衝突しない それ以外、衝突する 3次元の場合はZも含める。 練習 Unityでコライダーなしの2つの立方体を作成し、MyBoxCollider を作成し、幅、高さ、深さを設定できるようにしてください(注意:コライダーの半径とメッシュの半径を合わせてください) つぎ、BoxCollisionCheck のスクリプトを作成し すべての MyBoxCollider を取得 各軸の「最大」と「最小」を比較 衝突したら、色を変える MyBoxCollider スクリプト // 箱コライダー public class MyBoxCollider : MonoBehaviour { // サイズ(幅、高さ、深さ) [SerializeField] private Vector3 size; // 各軸の最大位置を返す public Vector3 GetMax() { return transform.position + size / 2; } // 各軸の最小位置を返す public Vector3 GetMin() { return transform.position - size / 2; } // 色を変える public void ChangeColor(Color color) { // MeshRendererの色を変える MeshRenderer mr = GetComponent(); mr.material.color = color; } } BoxCollisionCheck スクリプト // MyBoxCollider同士の当たり判定を確認 public class BoxCollisionCheck : MonoBehaviour { // すべてのコライダー private MyBoxCollider[] colliders; private void Start() { // FindObjectsByType = 「このスクリプトを付いているオブジェクトのすべて下さい」 colliders = FindObjectsByType(FindObjectsSortMode.None); } void Update() { // 初期値はすべて「false」 bool[] isCollision = new bool[colliders.Length]; // 全部 vs 全部を比較 for (int i = 0; i < colliders.Length - 1; i++) { for (int j = i + 1; j < colliders.Length; j++) { // 最大と最小位置を取得 Vector3 iMax = colliders[i].GetMax(); Vector3 iMin = colliders[i].GetMin(); Vector3 jMax = colliders[j].GetMax(); Vector3 jMin = colliders[j].GetMin(); // 軸ずつを確認 if (iMax.x < jMin.x) continue; if (iMax.y < jMin.y) continue; if (iMax.z < jMin.z) continue; // 逆の比較も if (jMax.x < iMin.x) continue; if (jMax.y < iMin.y) continue; if (jMax.z < iMin.z) continue; // 当たった! isCollision[i] = true; isCollision[j] = true; } } // 衝突しているコライダーだけの色を変える for (int i = 0; i < isCollision.Length; i++) { if (isCollision[i]) colliders[i].ChangeColor(Color.red); else colliders[i].ChangeColor(Color.white); } } } 球体と箱 SphereCollider と BoxCollider の当たり判定もあるので、その処理を学ぼう。 ここで、球体「D」の中心点 (d x , d y ) に、箱のある最も近い点「 p=(p x , p y ) 」を求める: そして、球体同士と同じように、球体の中心と点「p」の距離が半径よりも小さければ、衝突になる。 練習 BoxSphereCollisionCheck を作成し、 MyBoxCollider と MySphereCollider のすべてを求める 1個ずつの球体に対して、すべての箱を確認 球体に一番近い点を求めて、半径よりも小さければ、衝突にする BoxSphereCollisionCheck の当たり判定 // 球体と箱の当たり判定 public class BoxSphereCollisionCheck : MonoBehaviour { // すべての箱 private MyBoxCollider[] boxes; // すべての球体 private MySphereCollider[] spheres; void Start() { boxes = FindObjectsByType(FindObjectsSortMode.None); spheres = FindObjectsByType(FindObjectsSortMode.None); } void Update() { // 初期値はすべて「false」 bool[] isBoxCollision = new bool[boxes.Length]; bool[] isSphereCollision = new bool[spheres.Length]; // すべての球体 vs すべての箱 for (int i = 0; i < spheres.Length; i++) { for (int j = 0; j < boxes.Length; j++) { // コライダーを準備 MySphereCollider s = spheres[i]; MyBoxCollider b = boxes[j]; // 球体の中心 Vector3 center = s.transform.position; // 箱の最大と最小位置 Vector3 max = b.GetMax(); Vector3 min = b.GetMin(); // 球体の中心に一番近い点「p」を求める Vector3 p = new Vector3(); p.x = Mathf.Clamp(center.x, min.x, max.x); p.y = Mathf.Clamp(center.y, min.y, max.y); p.z = Mathf.Clamp(center.z, min.z, max.z); // 中心からpまでの距離が小さければ、当たっている float dist = (p - center).magnitude; if (dist < s.GetRadius()) { isSphereCollision[i] = true; isBoxCollision[j] = true; } } } // 衝突しているコライダーだけの色を変える for (int i = 0; i < isBoxCollision.Length; i++) { if (isBoxCollision[i]) boxes[i].ChangeColor(Color.red); else boxes[i].ChangeColor(Color.white); } for (int i = 0; i < isSphereCollision.Length; i++) { if (isSphereCollision[i]) spheres[i].ChangeColor(Color.red); else spheres[i].ChangeColor(Color.white); } } }     3Dモデルの仕組み(その2) 模様を付けましょう:UV座標 今までのモデルはすべて白い色で表示してきたが、実際にゲームで使うものは一定色ではない。前回作成した「MyCube」をベースにし、複雑な模様(テクスチャ)を付けてみましょう。具体的に、以下のようなサイコロを作ってみましょう この効果を再現するため、2次元の画像を3次元の立方体に貼らないといけないが、そのためなんとかして、「ペーパークラフト」のように3Dのオブジェクトを真っ平にし、2Dに展開しなければならない。 3Dモデルの場合、このプロセスは「 UV展開 」という。「UV」は「 正規化した画像座標 」のことである。「正規化」とは画像のサイズに関係なく、 必ず0〜1 の間にある座標系である。 以下、TimmyがUV展開された画像である: 画像のサイズが異なっても、どちらとも同じUV座標系(0〜1)を持っているため、モデル自体はそのまま使える。 テクスチャサイズ:4096 x 4096 テクスチャサイズ:256 x 256 今回のサイコロのテクスチャは以下にある これで、メッシュを作るときに、頂点ごとに「UV座標」を与え、それぞれの三角形は画像のどの部分を使えば良いか指定しなければならない。これおで、テクスチャを貼り付けるときに、正しく模様を表示される。 課題 「MyCube」を編集し、UV座標を追加してください。 CreateUVsのメソッドを追加し、上に表示された画像に基づいて、メッシュにUV座標を追加してください たらしいマテリアルを作成し、Base Map をサイコロのテクスチャにし、結果を確認 MyCube スクリプト(編集分) // UV座標 private Vector2[] uvs; // Awake は Start よりも一歩速く実行する // 内部初期化(他のコンポーネントに依存しない)ときにはおすすめ private void Awake() { CreateVertices(); CreateTriangles(); CreateUVs(); mesh = new Mesh(); mesh.vertices = vertices; mesh.triangles = triangles; mesh.uv = uvs; mesh.RecalculateNormals(); // 法線ベクトルを自動計算(※) // ゲームオブジェクトにMeshFilterを追加 MeshFilter mf = gameObject.AddComponent(); mf.mesh = mesh; // MeshRendererも追加 MeshRenderer mr = gameObject.AddComponent(); mr.material = material; } // UV座標を作成する // 返す:UV座標(2D)の配列 private void CreateUVs() { uvs = new Vector2[24]; // 前 (1) uvs[0] = new Vector2(0.375f, 0.75f); uvs[1] = new Vector2(0.625f, 0.75f); uvs[2] = new Vector2(0.625f, 1.00f); uvs[3] = new Vector2(0.375f, 1.00f); // 上 (3) uvs[11] = new Vector2(0.375f, 0.50f); uvs[18] = new Vector2(0.625f, 0.50f); uvs[22] = new Vector2(0.625f, 0.75f); uvs[23] = new Vector2(0.375f, 0.75f); // 後ろ (6) uvs[12] = new Vector2(0.625f, 0.25f); uvs[13] = new Vector2(0.375f, 0.25f); uvs[14] = new Vector2(0.375f, 0.50f); uvs[15] = new Vector2(0.625f, 0.50f); // 下(4) uvs[16] = new Vector2(0.375f, 0.00f); uvs[17] = new Vector2(0.625f, 0.00f); uvs[21] = new Vector2(0.625f, 0.25f); uvs[20] = new Vector2(0.375f, 0.25f); // 右 (5) uvs[9] = new Vector2(0.625f, 0.50f); uvs[5] = new Vector2(0.875f, 0.50f); uvs[6] = new Vector2(0.875f, 0.75f); uvs[10] = new Vector2(0.625f, 0.75f); // 左 (2) uvs[8] = new Vector2(0.375f, 0.50f); uvs[4] = new Vector2(0.125f, 0.50f); uvs[7] = new Vector2(0.125f, 0.75f); uvs[19] = new Vector2(0.375f, 0.75f); } 光の計算の入門:法線ベクトル 3Dでの光の計算が大変複雑なテーマであるが、最も単純なケースから光の計算、および法線ベクトルについて理解していきましょう。まず、以下のようなシーンについて考えてみましょう。 図:光源、光線と表面の関係。ここで一番単純な光源を使用(すべての光線は並行である) この場合は、光源からいくつかの光線が発射し、表面に当たる。ここで、平面の傾きにより、明るさがどう変わるのか解析してみましょう。表面が光源と真正面になるときに、一番明るく見えるが、表面が表面が光線と並行すると、光が当たらず、真っ黒になってしまう。 従って、表面の明るさを計算するため、その「向き」は必須な乗法である。この「表面の向き」は単位ベクトルとして表現し、「法線ベクトル」と呼ぶ。(英:Normal Vector) なので、光線と法線ベクトルの角度「0度」であるとき、表面の明るさの「倍率」は「1」である。逆に、光線と法線ベクトルの角度が「90度」であると、表面の明るさの「倍率」は「0」になる。 「0度」のときに「1」になり、「90度」の時「0」 になる関数はすでにある!そう、 「コサイン」 だ!また、内積で2つのベクトルの間にある角度のコサインを求めることができるので、光の明るさは内積で計算ができる。 内積の計算を思い出すと… 「n」(表面の法線)と「l」(光線のベクトル)は単位ベクトルだったら、ノルムは「1」となるので、結局: Unityは光の計算をしてくれるが、3Dモデルの法線ベクトルを渡さないといけない。今までは「 Mesh.RecalculateNormals() 」で自動計算してきましたが、手動で調整したいときもある。その時は頂点ごとに表面の向きを設定する。 では、なぜ法線ベクトルを調整したいのか?法線ベクトルの向きによって、光の計算が変わるため、法線ベクトルを変えることにより、ゲームエンジンをダマすことができる。具体的には、真っ平の表面でも丸めたり、凸凹に見えるようにできる。 次の図を見てください:左は普通の表面であるが、法線ベクトルはすべて同じ向きになり、表面の明るさは一定であるが、わざと法線ベクトルを傾くと、場所により明るさが異なり、丸く見えるエフェクトを再現ができる 法線ベクトルを変えることにより、光の計算が変わり 「丸く見える」 という効果を再現できる 練習: 以下の円筒を作成するスクリプト「MyCylinder」を解析してみてください。特に、sinとcosの使い方。次、Unityで実行して、表示結果を見てください。その後、法線ベクトルを直し、丸めてみましょう。 360度の円の「n」面に割って、左の図のように、1面ずつを作っていく。高さと半径を調整できるようにしましょう。 // 円筒を作る public class MyCylinder : MonoBehaviour { // 半径 [SerializeField] float radius; // 高さ [SerializeField] float height; // 面の数 [SerializeField] int numSide; // 立方体のマテリアル [SerializeField] private Material material; // 頂点の配列 private Vector3[] vertices; // 三角形の配列 private int[] triangles; // 作成したメッシュ private Mesh mesh; // Awake は Start よりも一歩速く実行する // 内部初期化(他のコンポーネントに依存しない)ときにはおすすめ private void Awake() { CreateVertices(); CreateTriangles(); mesh = new Mesh(); mesh.vertices = vertices; mesh.triangles = triangles; mesh.RecalculateNormals(); // 法線ベクトルを自動計算 // ゲームオブジェクトにMeshFilterを追加 MeshFilter mf = gameObject.AddComponent(); mf.mesh = mesh; // MeshRendererも追加 MeshRenderer mr = gameObject.AddComponent(); mr.material = material; } // 頂点を作成する private void CreateVertices() { // 必要な頂点の数 int numVertex = 10 * numSide; vertices = new Vector3[numVertex]; // 必要な角度(ラジアン) float angle = (360.0f / numSide) * Mathf.Deg2Rad; // 1面ずつを作っていく int idx = 0; for (int i = 0; i < numSide; i++) { float startAngle = i * angle; float endAngle = startAngle + angle; // 上の面 float y = height / 2; vertices[idx++] = new Vector3(0, y, 0); // 0 float x = Mathf.Cos(startAngle) * radius; float z = Mathf.Sin(startAngle) * radius; vertices[idx++] = new Vector3(x, y, z); // 1 x = Mathf.Cos(endAngle) * radius; z = Mathf.Sin(endAngle) * radius; vertices[idx++] = new Vector3(x, y, z); // 2 // 下の面 y = -height / 2; vertices[idx++] = new Vector3(0, y, 0); // 3 x = Mathf.Cos(startAngle) * radius; z = Mathf.Sin(startAngle) * radius; vertices[idx++] = new Vector3(x, y, z); // 4 x = Mathf.Cos(endAngle) * radius; z = Mathf.Sin(endAngle) * radius; vertices[idx++] = new Vector3(x, y, z); // 5 // サイド(重複) vertices[idx] = vertices[idx - 5]; idx++; // 1の重複 (6) vertices[idx] = vertices[idx - 5]; idx++; // 2の重複 (7) vertices[idx] = vertices[idx - 4]; idx++; // 4の重複 (8) vertices[idx] = vertices[idx - 4]; idx++; // 5の重複 (9) } } // 三角形を作成する private void CreateTriangles() { int triCount = 4 * 3 * numSide; triangles = new int[triCount]; int idx = 0; for (int i = 0; i < numSide; i++) { // 指数の「オフィス」(ずらす分) int offset = i * 10; // 上 triangles[idx++] = 1 + offset; triangles[idx++] = 0 + offset; triangles[idx++] = 2 + offset; // 下 triangles[idx++] = 4 + offset; triangles[idx++] = 5 + offset; triangles[idx++] = 3 + offset; // サイド triangles[idx++] = 8 + offset; triangles[idx++] = 6 + offset; triangles[idx++] = 7 + offset; triangles[idx++] = 8 + offset; triangles[idx++] = 7 + offset; triangles[idx++] = 9 + offset; } } } MyCylinder スクリプト(編集分) // 法線ベクトル(頂点ごと) private Vector3[] normals; // Awake は Start よりも一歩速く実行する // 内部初期化(他のコンポーネントに依存しない)ときにはおすすめ private void Awake() { CreateVertices(); CreateTriangles(); CreateNormals(); mesh = new Mesh(); mesh.vertices = vertices; mesh.normals = normals; mesh.triangles = triangles; //mesh.RecalculateNormals(); // 法線ベクトルを自動計算 // ゲームオブジェクトにMeshFilterを追加 MeshFilter mf = gameObject.AddComponent(); mf.mesh = mesh; // MeshRendererも追加 MeshRenderer mr = gameObject.AddComponent(); mr.material = material; } // 各頂点の法線ベクトルを作成 private void CreateNormals() { // 必要な方法ベクトルの数 int numVertex = 10 * numSide; normals = new Vector3[numVertex]; // 1面ずつを処理していく for (int i = 0; i < numSide; i++) { int offset = i * 10; // 上の面 normals[0 + offset] = Vector3.up; normals[1 + offset] = Vector3.up; normals[2 + offset] = Vector3.up; // 下の面 normals[3 + offset] = Vector3.down; normals[4 + offset] = Vector3.down; normals[5 + offset] = Vector3.down; // サイドの面 // 差分で、法線ベクトルを計算(単位ベクトル) Vector3 n = (vertices[1 + offset] - vertices[0 + offset]).normalized; normals[6 + offset] = n; normals[8 + offset] = n; n = (vertices[2 + offset] - vertices[0 + offset]).normalized; normals[7 + offset] = n; normals[9 + offset] = n; } } 法線ベクトルの細かい調整:法線マップ(Normal Maps) 法線ベクトルを変えることにより、光の計算を変えて、表面の見え方が変わることが分かったが、法線ベクトルは「頂点ごと」しか調整できなく、頂点の間の法線ベクトルが自動的に計算され、変えることができない…  …と思ったら、実は表面の各ピクセルの法線ベクトルを変えることができる。そのため、特別なテクスチャ「法線マップ」が存在する。  「UV展開」を学んだとき、普通のテクスチャは模様として貼り付けることができることが分かったが、法線マップは表面の色ではなく、表面の法線ベクトルを設定するテクスチャである。このテクスチャの各ピクセルは法線ベクトルの向きを表している。  普通の色テクスチャは3の成分で作られている:R(赤)G(緑)とB(青)。それぞれの数値は0〜255の範囲で、各成分の明るさを表している。 法線マップは普通のRGBテクスチャであるが、それぞれの成分の意味が異なる。 RGBの数値は色ではなく、法線ベクトルのXYZの向きを表している (赤:X、緑:Y、青: Z)。そして、0~255の範囲は以下のように変わる: RGBテクスチャ の明度 法線ベクトルの XYZ成分 0~126 -1~0 127 0 128~255 0~1 + + = そして、上の法線マップを真平な平面に適用すると:  これで、頂点の法線ベクトルの他にも、ピクセルごとに細かく法線ベクトルを調整でき、凸凹の表面を再現できる! 練習 サイコロに法線マップを加えて、細かい凸凹の効果を追加してください(Unityのみ、スクリプトなし) シェーダー シェーダーはGPU(グラフィックスカード)で実行する特別なプログラムである。シェーダーを使うことにより、3Dモデルの「見せ方」(描画)などを自由に変えることができる。ただし、GPUで実行するプログラムは普通のCPUのプログラムと異なり、使い方を学ばないといけない。 GPUとCPUの違い CPUは「多目的のプロセッサー」で、タスクを切り替えるのは得意な要素である。例えば、Chromeでネットを調べ、見つけたソースコードをVisual Studioに貼り付けたい。プログラムを切り替えるときに、CPUは一旦Chromeを休ませて、Visual Studioを起こして、作業をし続ける。  一方、GPUは「専門的な部品」である。GPUは同時に1つのタスクしかできず、実行中に切り替えることができない。代わりにそのタスクを大変高速に実行ができる。 CPU GPU 目的 多目的。同時に様々なプログラムを実行できる(マルチタスク) 専門的。同時に1つのタスクしかできない(描画のみ)※1 タスク切り替え 実行中にタスクを切り替えできる タスクが終わってから切り替えることができる 切り替えるコスト 低い 高い 条件分岐(if) 条件により、実行の流れを切り替えることができる。 できない※2 並列化 コアが空いていれば、ある程度並列化できる 非常に強い(千~万単位のコアがある) ※1:計算専用の「Compute Shader」というものが存在する。計算シェーダーは描画のためではなく、並列化し、速くなる処理に使える。 ※2:「if」命令があるが、実行コストが高くて、できるだけ使わないようにしましょう。 GPUとCPUの協力:シェーダーの流れ  OS(Windows)は、ゲームをハードディスクからメモリに転送し、CPUはメモリから1行ずつの命令を読み、実行する。今まで作ってきたスクリプトのすべてはこの流れで実行する。  ただ、ゲームを実行中に「これからGPUのプログラム(シェーダー)が始まる」という特別な命令が現れる可能性がある。この時、CPUは一旦ゲームを休んで、そのプログラムをGPUに転送する。転送し終わったら、ゲームをもう一度起こして、普通の動作に戻る。 この「一旦止まり→転送→続く」の処理効率が悪いため、GPUのタスク(プログラム)を切り替えるコストが高いといわれる。従って、できるだけゲーム開始の時に必要なシェーダーをGPUに転送し、その後にあまり変わらないようにすれば良いでしょう。  Unityはある程度にシェーダーの転送をしてくれるため、実装しなくても大丈夫だが、ゲームエンジンにより、手動でシェーダーの転送をおこなわないといけない。  シェーダー(プログラム)の他に、必要なデータも転送しなければならない。シェーダーの場合は、主なデータは3Dモデルの頂点や三角形、または位置や変換などである。Unityの「標準シェーダー」の場合はすべてのデータを自動的に転送されるが、「カスタムシェーダー」の場合は、一部のデータを手動で送信する必要な時もある。  これからUnityのシェーダーについて学び、そして特殊な効果を再現するため、カスタムシェーダーの作り方を学ぼう。 シェーダーとマテリアルの関係性 Unityでは「シェーダー」と「マテリアル」2つの概念がある。シェーダーはGPUで実行するプログラム。マテリアルは「一部のパラメータを設定したシェーダー」のこと。例えば、シェーダーに「Color」というパラメータがあるとしたら、マテリアルでは「Color=Red」にする。  従って、 1つのシェーダーからたくさんのマテリアルを作成することができる 。例えば、以下の「赤いプラ」「輝く金属」と「草と土」が全く同じシェーダー(Universal Render Pipeline / Lit)を使用しているが、パラメター(色、金属性、滑らか性、など)を変えることによって、様々な表面を作ることが可能。 URP(Universal Rendering Pipeline)のシェーダー UnityのURPモードは、すぐ使えるたくさんのシェーダーがあるので、最も使うシェーダーについてを細かく見てみましょう。 光と陰の計算あり(Universal Render Pipeline / Lit) URP で新しいマテリアルを作成すると、このシェーダーになる。いくつかのパラメータがあるので、よく使うのを確認しましょう。 Workflow Mode 反射の計算方法。2つがある: 「Metallic」は膨大な素材を再現できる。主には金属を再現するのは得意 「Specular」軽くて速い。金属を再現できず、主にプラスチックの雰囲気を再現するモード Surface Type 透明設定。2つがある: 「Opaque」不透明。計算するは速い 「Transparent」半透明。後ろのあるものも計算しないといけないので、比較的に遅い Render Face 見えない三角形(裏側)も描画するかしないか 「Front」表のみ 「Back」裏のみ 「Both」両面 Alpha Clipping 透明度(Alpha)はある数値以下をし下回ると透明になる。この設定は「不透明(Opaque)」でも効く   透明(Alpha)を持っている画像 Alpha Clippingなし Alpha Clippingあり Base Map 模様と色。画像(テクスチャ)を使わないなら、一定の色になるが、テクスチャを使うと模様を付けることができる。 Base Mapなし(色のみ) テクスチャ Base Mapあり なお、テクスチャを正しく貼り付けるため「UV座標」が必要である。「UV座標」については、また後で説明する。 Metallic Map / Specular Map 金属(またはプラスチック)である部分・ではない部分のテクスチャ。全体的に変わりがなければテクスチャは不要であるが、部分的に変えたいなら、このテクスチャを設定: Metallic Map なし テクスチャ Metallic Map あり Smoothness 表面のなめらかさ。数値が高いほど表面が輝くパラメータ Normal Map 法線マップのテクスチャ。光の計算を変えるテクスチャであり、細かい凸凹を再現するための特別な画像。 Normal Map なし 法線マップ Normal Map あり Emission 光る色とテクスチャ(実際に光らないが、光るようにみえる) Emission なし Emmision テクスチャ Emissionあり(色:緑) 光と陰の計算なし(Universal Render Pipeline / Unlit) 陰は計算されない。UIと2Dゲームなどに便利である。 光と陰の計算あり・複雑(Universal Render Pipeline / Complex Lit) 「Lit」(光計算あり)と同じで、さらに「Clear Coat」というパラメータがある。「Clear Coat」は、車のペンキのように、透明な輝くレイヤ(コーティング)を付けることができる。 パーティクル専用(光の計算あり・なし) パーティクルエフェクト専用のシェーダー。軽くて速いので、たくさんのパーティクルを作成してもあまり重たくならない。光を計算するとしないバージョンがある。 シェーダの種類 シェーダはいくつかの種類がある: Fragment Shader(フラグメントシェーダー)三角形のピクセル(フラグメント)ごとに実行するプログラム(シェーダー)である。モデルの陰、色、模様などをこのシェーダで計算する。 Vertex Shader(バーテックス・頂点シェーダー)三角形の頂点ごとに実行するシェーダーである。頂点の回転、移動、スケールなどを調整できるシェーダー Geometry Shader(三角形シェーダー)三角形ごとに実行するシェーダー。LOD(Level Of Detail)やTesselation(三角形再分割)に使ったりする Compute Shader(計算用シェーダー)汎用的な計算をするためのシェーダー。並列化ができる処理があったら、GPUに転送し、速く処理できる。 ゲーム開発での使う頻度 フラグメントシェーダー:よく使う!モデルの変色、模様のアニメーションやエフェクトなどにカスタムシェーダーを作ることが多い 頂点シェーダー:まぁまぁ使う。柔らかいものの変形(海の波、風による草の動き)などの時に便利 三角形シェーダー:ほとんど使わない。ゲームエンジン(Unity)は自動的に操作する 計算用シェーダー:あまり使わないが、何かの特別な計算を並列に実行したいなら使える。 シェーダの開発言語 シェーダーはGPUで実行するプログラムで、GPUを理解できる言語を使わないといけない。主に2つある:GLSL(GL Shader Language)とHLSL(High-Level Shader Language)。GLSLはOpenGL / Vulkan系のライブラリを使うシェーダー言語である。HLSL は Microsoft社が開発した「DirectX」をベースにしたライブラリで使用する言語である。  Unity は HLSLを基づいた「ShaderLab」という言語を使う。HLSLで使える命令はほとんどそのままUnityで使えるようになり、その上に様々な便利な機能がすでに実装されている。例えば、モデルの変換(移動、回転、スケール)や光の計算は実装済みであり、それ以外に機能だけ実装すれば、カスタムシェーダーを作成ができる。  また、よりも楽にシェーダーを作るには、Unityは「Shader Graph」も対応し、ノードをつなげるだけでシェーダーを作成ができる。  GLSL / HLSL / ShaderLabのどれでも、だいたいC言語と似ている。ただし: if文を使わないでほしい(一応できるが、計算のコストが高い) 使える関数ははGPU専用のものを使用(テクスチャ・画像の読み方が異なる) デバッグしにくい:「Debug.Log」がないし、実行中に止めることができない(ブレーキポイントを使えない) すべてのプログラムは「フラグメントごと」または「頂点ごと」に実行することを忘れないでほしい。 Unityでシェーダー実行する方法 シェーダーは「フラグメントごと」、または「頂点ごと」に実行するため、当然、シェーダーとモデルを連携しないといけない。Unityでは、マテリアル(Material)経由でモデルとシェーダーのひも付けが行われる。マテリアルを作成したあと、シェーダーを変えることにより、そのマテリアルを使うモデル(メッシュ)をそのシェーダーで処理する。 ShaderLab / HLSL ShaderLab および HLSL は全く新しい言語であり、学ぶ時間がないので、興味がある学生は個別でお調べください。以下、参考できる URL をご確認ください: UnityのShader(ShaderLab/HLSL)入門その1 : Shaderの書き方 【Unity入門】シェーダーを書いてみよう!ShaderLabの簡単な書き方! 【Unity Shader入門①】ShaderLabとShader Graphの基礎を解説!【初心者向け】 代わりに、もう少し直感的な「Shader Graph」でシェーダーの基礎を学び、フラグメントシェーダーと頂点シェーダを把握できるようになったら、ShaderLab と HLSL 言語についてお調べください。 Shader Graph プロジェクト設定 ShaderLabの自由度が高く、とてもパワフルなシェーダの作り方であるが、やや難しいところもある。複雑で高速なシェーダが必要なら、ShaderLabの方が良いと思われるが、単純なシェーダなら、Shader Graphの方が楽で、作るのも速くなる。  Shader Graphは言語ではなく、「ノード」というブロックを紐でつなげることにより、視覚的にシェーダを作成ができる。  まず、Shader Graphをインストールしましょう。Unityの Package Manger から Shader Graph をインストールしてください。 初めてのシェーダーグラフ まず、空のシェーダを作成しましょう。「Project」の中に、専用の「Shader」フォルダーを作成し、その中に「Lit Shader Graph」を作成してください。このシェーダーは後ほど使うので名前を「Chessboard」にしてください。 新しく作ったシェーダーグラフをダブルクリックすると、シェーダーエディタが開く: 各部品を理解しましょう: 頂点シェーダー(Vertex) 頂点シェーダーは、 メッシュの各頂点 の位置、または法線ベクトルを変えることができるシェーダーである。マテリアルを設定するだけで、3Dモデルの変形ができるシェーダー: フラグメントシェーダー(Fragment) フラグメントシェーダーは、 メッシュの各ピクセル の色、法線ベクトル、金属性などを変えることができるシェーダーである。マテリアルを設定するだけで、3Dモデルの見え方を変えれるシェーダー: ブラックボード(Blackboard) パラメータを作成ができるところ。Unityのインスペクターで変えることができる数値をここに追加する。 グラフ・インスペクター(Graph Inspector) 普通のインスペクターと同様に、現在選択されているものに関する情報や数値を確認し、変えることができるパネル: ノード設定(ノードによって変わる) グラフ設定(全般設定) プレビュー(Main Preview) シェーダの結果のプレビュー 様々なシェーダを作ろう! Shader Graph で使える「ノード」を紹介しながら、いろんな効果を再現し、3Dモデルの見せ方を変えてみましょう。 無限チェスボード 考え方 :「XZ平面の位置により、白いコマ、または黒いコマを表示したい」 ならば、3D平面の各ピクセルは3D空間の中にどこにあるのかを求める必要がある。その場合は、以下のノードを使える: Positionノード Positionノード Vertexと繋ぐと:各頂点の位置 Fragmentと繋ぐと:各ピクセルの位置 入力 なし 設定 Space:どの位置を使う? World:3Dシーンの絶対位置 Object:GameObject内の相対位置 View:カメラから見た位置 出力 Out(3):XYZの位置 Positionノードを Base Color(Fragment)と繋ぐと: ってどういうこと?実は、「位置」を「色」に書き換えた。元々の位置はXYZとして表現していたものを、そのままRGBに書き換えて、表示しているだけ。つまり: X=R→X軸の位置が大きくなると、色が赤くなる Y=G→Y軸の位置が大きくなると、色が緑になる Z=B→Z軸の位置が大きくなると、色が青くなる 確認:平面の位置を変えて、色がどう変わるのか、確認してみてください。 ※ポイント:色はいつも0~1の間で表現するので、位置は「1」を超えると最大値になり、それ以上色が変わらない  Positionノードで位置を取れることが分かったが、なかなかチェスボードにならない。問題を分けて、少しずつ解決してみましょう。まず、X軸だけで白黒の「しましま」のパターンを作成しましょう。YとZは要らないので、新しいノードが必要: Split ノード Splitノード ベクトルを成分に分割する 入力 In(1):2D, 3Dまたは4Dのベクトル 設定 なし 出力 R(1):赤色、またはX軸 G(1):緑色、またはY軸 B(1):青色、またはZ軸 A(1):アルファ値、または4Dの4つ目の成分(W) グラフを直して、Position→Split(R)→Base Colorをつなげてみましょう: X軸の数値だけしか残らないので、Xの位置が大きいほど、白くなる!(RGBはすべてXの数値になる)。ちょっと近づいた気がしますが、しましまのパターンになっていない。何か周期的に繰り返すものがないか? この図は「sin(θ)」のグラフであり、2πごとになみのように繰り返す関数である!x軸はどれだけ大きくなっても、出力は必ず-1~1の間になるので、次のノードを追加しましょう: Sine ノード Sineノード ベクトルを成分に分割する 入力 In(1):角度(ラジアン) 設定 なし 出力 Out(1):サイン関数の結果 Position→Split(R)→Sine→Base Colorにすると: しましまのパターンが現れた!…が、色がなめらかに変わる。確かに、Sinのグラフを見ると、-1〜1の間にゆっくり変わる。「白→黒→白→黒」の2色だけがほしい、以下のように: つまり、「sin」が「0以上」だったら、「1」にし、「0未満」の場合は、「0」とする(青い線)。これは、「Step」ノードで実現できる! Step ノード Stepノード 入力した値を「しきい値」以上だと「1」を返す、以下だと「0」を返す 入力 Edge(1):しきい値 In(1):入力値 設定 なし 出力 Out(1):「0」か「1」 Sineの後にStepノードを挟んで、確認しましょう。Edge は「0」にしてください。 かなりよくなった!ただし、1つの「ストライプ」はUnityのグリッドと比べると、サイズが合わない。なぜなら、Unityのグリッドの間隔は「1単位」なのだが、 sin の周期は π (約3.1416)である。これは簡単に解決できる。 sin の周期は Unity の1単位にすれば良いでしょう。つまり、1つの単位をπ倍数にすれば良いということです。掛け算のノードは: Multiply ノード Multiplyノード 2つの数値の掛け算を返す 入力 A(1): 1つ目の数値、またはベクトル B(1):2 つ目の数値、またはベクトル 設定 なし 出力 Out(1) 数値と数値:A × B  10 x 4 = 40 数値とベクトル:数値と各成分でできる新しいベクトル 3 x [1, 2, 3] = [3, 6, 9] ベクトルの場合は、各成分の掛け算を返す 例: [1, 2, 3] × [4, 5, 6] = [4, 10, 18] Split → Multiply → Sine を繋ぎ、「B」は「3.1416」にしておいてください: Sineの周期をUnityの単位に合わせたので、1つのストライプはグリッドにぴったり! これで、チェスボードの半分ができた!Z方向の処理は同じになるので、ノードを重複してもOK!(Splitノードの(R)の代わりに(B)を使ってください) では、ZとXどう合成すれば良いでしょう?Zは「黒」の場合は、Xのストライプはそのままにして、Zは「白」になったら、Xの色を反転してほしい(白↔黒) 色は「白:1」「黒:0」というのを分かっているので、XとZの差分(X-Z)を計算してみましょう: X方向の値(色) Z方向の値(色) X-Z 期待 1(白) 0(黒) 1(白) 1:✓ 0(黒) 0(黒) 0(黒) 0:✓ 1(白) 1(白) 0(黒) 0:✓ 0(黒) 1(白) -1(?) 1:× 最後だけは「1」ではなく、「-1」になってしまうので、絶対値を取れば直せる。以下の2つのノードで「X」と「Z」を合成しましょう:  Subtract ノード Subtract ノード 2つの数値の引き算を返す 入力 A(1): 1つ目の数値、またはベクトル B(1):2 つ目の数値、またはベクトル 設定 なし 出力 Out(1) 数値と数値:A - B  10 - 4 = 6 数値とベクトル:数値と各成分でできる新しいベクトル 3 - [1, 2, 3] = [2, 1, 0] ベクトルの場合は、各成分の引き算を返す 例: [1, 2, 3] - [4, 5, 6] = [-3, -3, -3] Absolute ノード Absolute ノード 絶対値を返す 入力 In(1): 数値、またはベクトル 設定 なし 出力 Out(1): 数値、また各成分の絶対値 X方向とZ方向の差分+絶対値を計算し、Base Colorと繋ぐと: レインボーキューブ このシェーダは、オブジェクトのUVを変えて、テクスチャをアニメーションするシェーダである。まず、「rainbow」のテクスチャをドラッグドロップでシェーダに追加する。自動的に、Sample Texture 2Dのノードを作成される: Sample Texture ノード Sample Texture ノード テクスチャの色データを読み込む 入力 Texture(T2): テクスチャ画像 UV(2):読み込むべきのUV座標。 初期値としてメッシュのUV座標が使われるが、上書きができる。 Sampler(SS):(高級)色の補完方法 設定 Type:画像の種類 Default:色テクスチャ(模様) Normal:法線マップ Space:(高級)ノーマルマップの空間(TangentでOK) 出力 RGBA(4):画像から取得したRGB色とアルファ値(透明度) R(1):赤の成分のみ G(1):緑の成分のみ B(1):青の成分のみ A(1):アルファ値の成分のみ このままで「RBGA」を「Base Color」と繋いでみましょう: UnityのCubeの各面のUVは(0, 0)~(1, 1)になっているため、各面が同じテクスチャをそのまま貼っている! このままだと、エフェクトを作れないので、MeshのUVを上書きしましょう。Meshデータの代わり、オブジェクトの相対位置を使って、UをXに一致し、VをYに一致しましょう、つまり: 「Position」から位置を取得し「Space」は「Object」(相対位置)にする。それをUVとして使えば テクスチャの縦(V座標)はY座標と一致しているので、上から下の方向でテクスチャを読み込んで貼り付ける。テクスチャ横(U座標)はX軸に従うので、思い通り貼り付けることができた。  ただ、これが動いていないので、時間に従って、縦方向の「V座標」だけ動かしましょう。このため、この3つのノードが必要 Time ノード Time ノード 時間を返す 入力 なし 設定 なし 出力 Time(1):シェーダ開始してからの時間 Sine Time(1):Timeのサイン Cosine Time(1):Timeのコサイン Delta Time(1):前のフレームから過ごした時間 Vector2 ノード Vector2 ノード 2Dベクトルを作成 入力 X(1): X成分 Y(1):Y成分 設定 なし 出力 Out (1): 出力のベクトル Add ノード Add ノード 2つの数値の足し算を返す 入力 A(1): 1つ目の数値、またはベクトル B(1):2 つ目の数値、またはベクトル 設定 なし 出力 Out(1) 数値と数値:A + B  10 + 4 = 14 数値とベクトル:数値と各成分でできる新しいベクトル 3 + [1, 2, 3] = [4, 5, 6] ベクトルの場合は、各成分の足し算を返す 例: [1, 2, 3] + [4, 5, 6] = [5, 7, 9] 完成シェーダグラフ 魔法巻物を燃やす まず、シーンを設定してください(scroll.fbx を追加し、空のシェーダを作成、Materialも準備しましょう)そして、「scroll」のテクスチャをシェーダに追加し、そのまま「Base Color」と繋げてみる: テクスチャがちゃんと貼り付けられたことが分かる。もう一つの「progress」追加し、「Base Color」とつなげてください 「progress」は 色テクスチャではなく、「データテクスチャ」 という。 このテクスチャは 表示するべきではなく、シェーダの計算のために使うテクスチャである。今回は穴が開くようなエフェクトを作るので、「progress」はアニメーションの進歩のために使い、0(黒)から1(白)まで徐々に開けていく。  色テクスチャではないため、Unityにその旨を伝えないといけない。「progress」を選択し、Inspectorで「sRGB (Color Texture)」のチェックを外してください また、今回のエフェクトでは、巻物が消えていくので、シェーダが透明度に対応するようにしなければならない。透明度の処理は主に2つある Alpha Blending(アルファ・ブレンディング):半透明のものに使う(グラス、PETボトルなど)アルファ値は透明度を表し、0の場合は透明になり、1の場合は不透明で、その間に透明度を調整できる。 Alpha Clipping(アルファ・クリッピング):半透明ではなく、「見える・見えないか」のしかない。クリッピングのしきい値があり、アルファ値はそのしきい値を超えるか超えないかにより、透明・不透明が切り替わる 今回のエフェクトでは「Alpha Clipping」は適切であり、シェーダの設定でAlpha Clippingを有効化しよう。「Graph Inspector」で「Alpha Clipping」のチェックを付けてください。これでシェーダが2つの新しい出力ができる:  「Alpha」はアルファ値(透明度)であり、0〜1の間に設定できる。「Alpha Clip Threshold」は「見える・見えない」のしきい値である。これで、「progress」の(R)を「Alpha」と繋ぎ、いくつかのしきい値を試してみましょう: しきい値:0.25 しきい値:0.5 しきい値:0.7 しきい値:0.9 何度もしきい値を変えて確認するのは大変なので、Materialのプロパティとして自由に変えられるようにしましょう。「Blackboard」で新しいパラメータ「Progress」の「Float」を作成し、「Alpha Clip Threshold」とつなげてみましょう: そして、Materialを選択すると、編集できるパラメータが現れる。この数値を変えれば、すぐその効果を確認できる。  ただし、Alpha Clippingは0〜1の間だけしか効かないので、-100や100にしても意味がない。Progressに制限を付けましょう。「Blackboard」でパラメータを選択し、「Node Settings」で範囲を設定しましょう。「Mode」を「Slider」にし、少し余裕があるため -0.2~1.2の間の制限を設定: ➡ をすると、Materialの見え方が変わる:   つぎに、巻物が消える領域の周りに光るエフェクトを実現しましょう。つまり、「しきい値のちょっと手前」の領域を求めないといけない。  一旦、すべてのノードを外して、各領域を可視化しましょう。「progress」テクスチャの(R)をStepノードを通して、「Progress」しきい値で白黒の範囲を「Base Color」で確認しましょう きい値未満の値は「黒」になり、しきい値以上の値は白になる。「少し手前」を求めたいなら、基本的には、「 Progress値 + 少し 」で新しいしきい値を作れば良いでしょう。 そして、この2つの 領域の差分 だけ残せば、 リングを作成する ことができる: ←「Progress」と「Progress+少し」の差分で残った領域 このリングの厚さをパラメータとしてBlackBoardで作ってください。名前は「Edge Size」にしましょう。 これを「光る」輪郭のために使うので、「Emission」出力と繋げば良いでしょう。ただし、色も選択したいので「黒(0):光らない/白(1):光る」光る色とリングを掛け算すれば良いでしょう。 なお、光る強さを調整できるようにするため、EdgeColor のパラメータを「HDR 対応」にしないといけない。EdgeColor を選択し、Node Settings で「Mode:HDR」にしてください。これをすることにより、Inspectorで「Intensity」(強度)を調整ができる。 さて、「Scroll」テクスチャを「Base Color」にし、「Emission」は今まで通りで、「Alpha」と「Clip Threshold」を元に戻せば、以下のように見える: はこうなる ↓ これで燃えるエフェクトが近づいてきたが、円の形が不自然で、中央から広げてもちょっと揺れる境界線を再現したい。これを実現できるため、Shader Graphは「Noise」ノードを提供する: Simple Noise ノード Simple Noise ノード ランダムノイズを生成 入力 UV(2): UV座標。デフォルトとしてモデルのUVを使用 Scale(1):ノイズサイズ(拡大・縮小) 設定 なし 出力 Out(1):2Dのノイズ画像。数値は0~1の間にランダムの数値になる  このノイズを「progress」テクスチャと組み合わせれば良いでしょう。これで進歩テクスチャのタイミングを多少ずらすことができる。例えば、Progressは「0.5」(進歩の半分)であったところがランダムノイズを追加するので「0.6」や「0.8」になる可能性があり、リングのしきい値がランダムに変わる。   ただし、これで試してみると、2つの問題が現れる: ノイズが強すぎ、調整できない。 Progressを1.2にしても、エフェクトが途中で 終わってしまう。 なっぜなら、最大のランダム数値は「1」であり、progressテクスチャの最大数値も「1」であるため、2つの数値を足せば、可能な最大数値は「2」になってしまうので、1.2を超えず、途中で終わってしまう。 このため、ノイズの強さを調整できれば良いでしょう。0〜1ではなく、例えば、-0.1〜0.1の間にすれば、progressの範囲は -0.1〜1.1の間になる。Unityは便利な「Remap」ノードを提供する。 Remap ノード Remap ノード ある範囲を別の範囲へ変換 入力 In(1): 変換すべきの数値 In Min Max (2): 入力数値の範囲 (最小値・最大値) Out Min Max(2): 希望の出力範囲(最小値・最大値) 設定 なし 出力 Out(1):「In」 を変換した後の結果 例 In Min Max(入力範囲)0~1 Out Min Max(出力範囲)-0.1~0.1 In(変換すべき):0.8 Out(変換のあと): 0.06 今回のノイズの強さを調整したいので、Addする前に、Remapで範囲を調整しましょう。 これでシェーダは完成。Simple Noise の Scale や Remap の範囲をパラメータとして調整できるようにすれば、Materialでいつでも簡単に調整ができる。 完成したシェーダーグラフ: ランダム地形を作成 今まで、フラグメントシェーダーで、3Dモデルの各ピクセルの色を調整してきたが、次「頂点シェーダー」を使い、メッシュの頂点をGPUで変えましょう。 まず、シーンに「terrain」メッシュを追加してください。このメッシュが真平に見えるが、実はたくさんの三角形で分割されている: 頂点がたくさんあるので、頂点シェーダーで弄ってみましょう。新しいシェーダ「Terrain」を作成し、頂点の位置を編集するので、 Positionノード を追加してください。なお、それぞれの頂点の高さ(Y軸の位置)を変えるので、「Height A」パラメータを追加しましょう。Positionノードから得た位置を Split ノード で分解し、頂点のY軸を「Height A」で上書きしましょう。 ただし、分解したベクトルをもう一度3Dベクトルに合体しないといけないので、Splitノードの逆「Combine ノード」を使いましょう: Combine ノード Combine ノード 最大に、4つの数値を1つのベクトルにする 入力 R(1):赤色、またはX軸 G(1):緑色、またはY軸 B(1):青色、またはZ軸 A(1):アルファ値、または4Dの4つ目の成分(W) 設定 なし 出力 RGBA(4):4つの成分のベクトル RGB(3):3つの成分のベクトル RG(2):2つの成分のベクトル 以下のようになる: Inspector で Height A の値を変えて、効果を確認してください。たしかに、すべての頂点のXとZが変わらず、Y軸だけ高くなったり、低くなったりするが、すべての頂点が全く同じ高さになってしまう! ランダムに高さを変えるには、またノイズを使いましょう。山の形やそのた自然な効果を再現できる「Voronoi」ノイズを紹介: Voronoi ノード Voronoi ノード 自然な効果を真似するノイズ 入力 UV(2): UV座標。デフォルトとしてモデルのUVを使用 AngleOffset(1):黒い中心点を揃えるランダム性 0:縦横できれいに揃える 大きくなると:だんだん混ざっていく CellDensity(1):黒い中心点の数(ノイズ拡大縮小) 設定 Hash Type:ノイズを生成方法 出力 Out(1):ノイズの値(0~1) Cells(1):(高級)各ピクセルの黒い中心点までの距離 ノイズの出力を Height A との掛け算し、新しい頂点の高さにしましょう。 は以下のようになる: 一応、頂点の高さはランダムに変わり、山っぽいものができたが、 陰がおかしい 。なぜなら、頂点の変更で三角形の向きが変わったが、 方法ベクトルの向きは更新されていない から。 この問題を解決するには、Shader Graph は以下のノードがある: Normal From Height ノード Normal From Height ノード 高さ(Heightmap)から法線マップを作成 入力 In(1):高さ Strength(1):法線ベクトルの強さ(長さ) 設定 Output Space:方法ベクトルの向き Tangent:(標準)各三角形に相対に作る World:3D空間に対して、絶対位置を使い作る 出力 Out(3):各ピクセルの方法ベクトル Voronoi ノードから高さを取れるので、グラフを分岐し、高さを Normal From Height の In(1) と繋ぎ、方法ベクトルをフラグメントシェーダーの「Normal」と接続。 これで陰が直る: Voronoi の「AngleOffset」と「CellDensity」で山の頻度の分配をコントロールできるので、Blackboard で2つのパラメータ(Randomness, Density)を追加し、繋いでみましょう。 これで、マテリアル側で山の頻度とランダム性を調整ができる! これで単純な山を作成できたが、とてもシャープで、きれいすぎるので、2つのノイズをまぜて、もう少しデコボコの現象を再現してみましょう。また「 Simple Noise 」を追加し、すでにある高さとの加算してください。なお、2つののノイズの高さは「Height B」で調整できるようにしてください: 以下のようになる: もう少し改善できたが、ノイズが細かすぎるので、「Scale」パラメータを Blackboard に追加し、お好みに調整してみてください: 次に色を付けましょう。「Ground」テクスチャをシェーダーに追加し「Base Color」と接続すると。ついでに、表面が輝きすぎるので、Smoothness を 0 にしましょう: これで地形っぽくなったが、頂上周りには雪が欲しくて、残りは「Ground」のままにしたいので、テクスチャを混ぜるノードが必要。Shader Graph で2種類があるが、最も単純なのは: Lerp ノード Lerp ノード L inear Int erp olation(線形補間)を計算する 入力 A(1):1つ目の数値、またはベクトル B(1):2つ目の数値、またはベクトル T(1):補間パラメータ(0~1) 設定 なし 出力 Out(1):補間した数値、また各成分。 線形補間は2つの数値の間に線形的に変えてゆく関数である。 T は 0 の場合は、Lerp が A を返す。 T は 1 の場合は、Lerp が B を返す。 その間には、A と B を混ぜて返す。   例えば、 A:20 B:40 T:0.5 Lerp(20, 40, 0.5)⇒ 30(T は 0.5=AとBのちょうど真ん中) 2つの画像を Lerp で補間すると: T = 0 T = 1 T = 0.5 それぞれのピクセルの色を補間し、混ぜる。まず、雪と地面のテクスチャを Lerp で 混ぜましょう: これで: T は 0 の時 ⇒ 地面 T は 1 の時 ⇒ 雪 の切り替えができる!そうすると、「ある高さ(しきい値)以上1,それ以下0」を返す Step ノード をを使い、Lerpノードの T と接続しましょう: これで、高さは「4」以上だったら「雪」にする処理になっている! 雪の高さを Blackboard で調整できるようにすれば完成! 完成したグラフ(クリックで拡大):