今回はUnityでスクリプトからメッシュを作成し、剣の軌跡を表示してみたいと思います。
TrailRendererを使えば簡単に剣の軌跡を表示出来そうなんですが、わたくしの場合うまく出来なかった為、スクリプトからメッシュを生成する事にしました。<`ヘ´>
剣の軌跡はあらかじめ3DCG等で軌跡のメッシュを手動で作成したりする事でも作成出来るみたいですが、わたくしはスクリプト向きっぽいですね(謎)
スクリプトからメッシュを生成するようにすれば、剣を振るアニメーションがどんなものであれ、剣の根元と剣先を設定すれば勝手に軌跡を作成してくれるので、個別のアニメーションに対応して軌跡を作成する必要はありません。
勝手に軌跡を作成してくれると簡単に言ってますが、機能を作るのは結構大変です・・・・(^_^;)
剣の軌跡は↓のような感じになりました。
なかなか綺麗ですね!
今回の機能を作成する前にスクリプトからメッシュの生成をする方法について知っておく必要があるので、
を参照してください。
それでは剣の軌跡を表示する方法を考えていきましょう。
剣の軌跡を表示する方法
剣の軌跡を表示する為にはメッシュを構成する頂点、UV、三角形を作る方法を考えなくてはいけません。
剣の過去と現在の位置情報を頂点にする
剣の軌跡を表示する為には剣の根元と剣の先の位置がいくつかわかればその点を繋いでメッシュの頂点情報に使えそうです。
剣元と剣先は剣の子要素に空のゲームオブジェクトで作成しておきデータを保存すれば良さそうです。
その為には1フレーム毎(UpdateやLateUpdateでデータを取得)に剣元と剣先の位置をデータとして残しておきます。
過去の位置データと現在の位置データを頂点にすればそこからUV、三角形を設定出来るのでメッシュの生成が出来ますね。
↑のように1フレーム前の過去の剣元と剣先の位置と、現在の剣元と剣先の位置の4点がわかれば三角形2つを作って四角形の面が出来、剣の動いた間に軌跡を作る事が出来ますね。
UV座標の設定
剣の軌跡用のテクスチャを、作成したメッシュに割り当てる為にはテクスチャの横方向を軌跡のメッシュの数に応じて値を変更しなければいけません。
例えば↓のようなテクスチャの場合は
過去の1フレームの剣の位置と現在のフレームの剣の位置から三角形を2つ作成するので、縦のVの値は0か1の値で、横方向も0か1の値です。
ですが、剣の軌跡を前のデータを元にさらに作成した場合は縦のVの値は0か1で固定ですが、横のUの値は四角形の数によって変わってきます。
↑のように過去2フレーム前から現フレームまでのデータを使ってメッシュを作成した場合、中間の1フレーム前のUV座標は剣元が(0.5, 0)、剣先が(0.5, 1)にしなければいけません。
頭が混乱しますね・・・\(-o-)/
三角形の作成
三角形の作成は剣元と剣先の過去フレームのデータから作成する事が出来るので、後は配列に設定する順番だけですね。
剣の軌跡は手前側を表で統一したい為、過去フレームから時計回りに頂点を指定し三角形のデータを作成していきます。
↑の例でいくと、
剣元の2フレーム前→剣先の2フレーム前→剣元の1フレーム前(ここで三角形1つが出来る)→剣元の1フレーム前→剣先の2フレーム前→剣先の1フレーム前(ここで三角形2つ目)→剣元の1フレーム前→剣先の1フレーム前→・・・・・
と続けていきます。
こうする事で同じ側にメッシュの表面が作成される事になります。
過去フレームデータからメッシュを生成してみる
剣の軌跡を表示する方法がわかったので、作成してみましょう。
キャラクターには武器を持たせ、攻撃ボタンが押されたら攻撃アニメーションが再生されているとします。
剣元と剣先のオブジェクトの作成
剣の子要素に空のゲームオブジェクトで剣元(StartPosition)と剣先(EndPosition)を作成し設定します。
↑のように剣の子要素に剣元と剣先を作成します(名前はわかりやすく変えた方がいいですね・・・・)。
剣元の位置を調整し、
↑のように刃の根元に移動させます。
剣先も同じように移動させ、
↑のように剣の先にします。
メッシュ生成スクリプト
StartPositionとEndPositionが設定出来たので、これらの位置をスクリプトから取得し、メッシュを生成してみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 | using UnityEngine; using System.Collections; using System.Collections.Generic; [RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))] public class CreateSwordTrail : MonoBehaviour { // 剣元 [SerializeField] private Transform startPosition; // 剣先 [SerializeField] private Transform endPosition; // メッシュ private Mesh mesh; // 軌跡用の四角形の表示個数 [SerializeField] private int saveMeshNum = 10; // 頂点リスト [SerializeField] private List<Vector3> verticesLists = new List<Vector3>(); // UVリスト [SerializeField] private List<Vector2> uvsLists = new List<Vector2> (); // 剣元の位置リスト [SerializeField] private List<Vector3> startPoints = new List<Vector3> (); // 剣先の位置リスト [SerializeField] private List<Vector3> endPoints = new List<Vector3> (); // 三角形のリスト [SerializeField] private List<int> tempTriangles = new List<int>(); // Use this for initialization void Start () { mesh = GetComponent <MeshFilter> ().mesh; } void LateUpdate () { // 必要頂点数を超えたら削除 if (startPoints.Count >= saveMeshNum + 1) { startPoints.RemoveAt (0); endPoints.RemoveAt (0); } // 現在の剣元、剣先を登録 startPoints.Add (startPosition.position); endPoints.Add (endPosition.position); // 頂点がメッシュ数+1以上になったら剣の軌跡メッシュを作成 if (startPoints.Count >= saveMeshNum + 1) { CreateMesh (); } } // 剣の軌跡作成メソッド void CreateMesh() { // メッシュのクリア mesh.Clear (); // リストのクリア verticesLists.Clear (); uvsLists.Clear (); tempTriangles.Clear (); for (int i = 0; i < saveMeshNum; i++) { verticesLists.AddRange (new Vector3[] { startPoints[i], endPoints[i], startPoints[i + 1], startPoints[i + 1], endPoints[i], endPoints[i + 1] }); Debug.DrawLine (startPoints [i], endPoints [i], Color.red); Debug.DrawLine (startPoints[i + 1], endPoints[i + 1], Color.yellow); } // UVMapのパラメータ設定 float addParam = 0f; for(int i = 0; i < saveMeshNum; i++) { // 四角形のテクスチャの割り当てを設定 uvsLists.AddRange (new Vector2[]{ new Vector2(addParam, 0f), new Vector2(addParam, 1f), new Vector2(addParam + 1f / saveMeshNum, 0f), new Vector2(addParam + 1f / saveMeshNum, 0f), new Vector2(addParam, 1f), new Vector2(addParam + 1f / saveMeshNum, 1f) }); // 表示する四角形数で1を割って割合を計算 addParam += 1f / saveMeshNum; Debug.Log (addParam); } // メッシュ用の三角形を登録した頂点で設定 for (int i = 0; i < verticesLists.Count; i++) { tempTriangles.Add (i); } mesh.vertices = verticesLists.ToArray (); mesh.uv = uvsLists.ToArray (); mesh.triangles = tempTriangles.ToArray (); mesh.RecalculateBounds (); mesh.RecalculateNormals (); } } |
いきなり難しいですね・・・(‘_’)
RequireComponentアトリビュートを使って、このスクリプトをゲームオブジェクトに取り付けた際にMeshFilterとMeshRendererコンポーネントを自動で取り付けます。
先ほど作成した剣元と剣先をインスペクタで設定出来るようにしています。
頂点、UV、三角形はリストを使って保存出来るようにしています。
過去フレームの剣元、剣先の位置を保存する時はStartPointsリストとEndPointsリストを使います。
これらのフィールドはインスペクタで確認出来るようにSerializeFieldアトリビュートを取り付けています。
LateUpdateメソッドで剣元と剣先の位置の保存や削除、メッシュの生成を行っています。
1つの四角形を作るには最低でもStartPointsに2点、EndPointsに2点が保存されている必要がある為、条件を加えています。
また、StartPointsとEndPointsにどんどんデータを保存してもメッシュの数に応じて必要な数は決まって来るので、それ以上になったらデータを削除しています。
剣元と剣先でメッシュの数+1のデータが保存されていればメッシュを生成出来ます。
メッシュ数を3にすればStartPointsとEndPointsはそれぞれ4点づつが必要になりますね。
↑はメッシュが3つなので剣元と剣先に4つの頂点が必要です。
CreateMeshメソッドの解説
CreateMeshメソッドがわかり辛いので少しづつ解説します。
リストのクリア処理
まずは最初のリストのクリア処理です。
1 2 3 4 5 6 7 8 9 | // メッシュのクリア mesh.Clear (); // リストのクリア verticesLists.Clear (); uvsLists.Clear (); tempTriangles.Clear (); |
mesh.ClearメソッドでMeshクラスを一旦クリアにしています。
その他の頂点リスト、UVリスト、三角形リストをクリアにし、CreateMeshメソッドが呼ばれる度にデータを消して再度データを作成していきます。
頂点計算処理
次は頂点計算処理を見ていきます。
1 2 3 4 5 6 7 8 9 10 | for (int i = 0; i < saveMeshNum; i++) { verticesLists.AddRange (new Vector3[] { startPoints[i], endPoints[i], startPoints[i + 1], startPoints[i + 1], endPoints[i], endPoints[i + 1] }); Debug.DrawLine (startPoints [i], endPoints [i], Color.red); Debug.DrawLine (startPoints[i + 1], endPoints[i + 1], Color.yellow); } |
頂点の登録はメッシュ毎に登録していきます。
その為、saveMeshNumより下の間、for分で繰り返します。
1回の処理で四角形1つ分の頂点を作成していくので、頂点を6つ登録します。
メッシュの表面を手前側にする為に順番を考え、
前の剣元の位置→前の剣先の位置→次の剣元の位置→次の剣元の位置→前の剣先の位置→次の剣先の位置
という順番にします。
startPoints(剣元)とendPoints(剣先)は一番古いデータが0番目なので、一番古いデータから新しいデータへと頂点を登録していきます。
Debug.DrawLineを使って剣元と剣先に線を繋げわかりやすくしています。
この線を引いてシーンビューでどこのデータなのか表示し確認する事は非常に重要です!
UV座標の登録
次はUV座標の登録処理です。
1 2 3 4 5 6 7 8 9 10 11 12 13 | // UVMapのパラメータ設定 float addParam = 0f; for(int i = 0; i < saveMeshNum; i++) { // 四角形のテクスチャの割り当てを設定 uvsLists.AddRange (new Vector2[]{ new Vector2(addParam, 0f), new Vector2(addParam, 1f), new Vector2(addParam + 1f / saveMeshNum, 0f), new Vector2(addParam + 1f / saveMeshNum, 0f), new Vector2(addParam, 1f), new Vector2(addParam + 1f / saveMeshNum, 1f) }); // 表示する四角形数で1を割って割合を計算 addParam += 1f / saveMeshNum; } |
UV座標は横向きのUの値をメッシュの数で分割して計算しなければいけません。
そこで分割値をaddParamとして計算し、その値を使用しています。
UV座標も四角形1つづつで設定しています。
三角形の登録
三角形の登録処理を見ていきます。
1 2 3 4 5 6 | // メッシュ用の三角形を登録した頂点で設定 for (int i = 0; i < verticesLists.Count; i++) { tempTriangles.Add (i); } |
今回は頂点を共有しない形で作っているので、三角形はその頂点を順番に繋いでいくだけです。
剣の軌跡の場合は頂点を共有した方が光の当たり具合がいいような気がするので、後でそちらのスクリプトに切り替えますが、シェーダ―を切り替えるだけでそれほど問題がないような気もします。
わたくしの場合、最終的に頂点を共有するバージョンに変えてしまいましたが・・・・。
メッシュの登録
作成した頂点、UV、三角形のリストをメッシュクラスに入れる処理を見ていきます。
1 2 3 4 5 6 7 8 | mesh.vertices = verticesLists.ToArray (); mesh.uv = uvsLists.ToArray (); mesh.triangles = tempTriangles.ToArray (); mesh.RecalculateBounds (); mesh.RecalculateNormals (); |
リストを配列化した後にMeshクラスのそれぞれの値に入れています。
RecalculateBoundsはメッシュを覆うBoundsの再計算、RecalculateNormalsはメッシュの法線方向の再計算です。
頂点、UV、三角形はそれぞれSetVertices、SetUVs、SetTrianglesを使うとリストを配列に変換しなくても引数に設定すればメッシュを生成出来ますが、
SetUVsとSetTrianglesは別の引数も指定する必要がある為、配列化してデータを入れるようにしています。
剣の軌跡専用のゲームオブジェクトを作成
スクリプトが出来たので、そのスクリプトを取り付ける空のゲームオブジェクトを作成します。
空のゲームオブジェクトを作成したら名前をSwordTrailとし、Transformの歯車からResetを選び位置をデフォルト値にします。
剣の軌跡用テクスチャを作成
剣の軌跡用マテリアルに設定するテクスチャを作成します。
お絵かきソフト等で256×256ピクセルか512×512ピクセルサイズの画像を作成し、灰色から白色のグラデーションの画像を作成します。
↑のように右側が剣の色に近い色、左側が色落ちした白色にします(剣の色によって色を変えてください)。
↑の剣の軌跡用テクスチャは右クリックからダウンロードし使用してもかまいません。
画像を作成したらUnityのAssetsフォルダで右クリック→Import New Assetを選択し、画像を取り込みます。
取り込んだ画像を選択し、インスペクタでWrap ModeをClampにして、画像が繰り返されないようにします。
剣の軌跡用マテリアルの作成
次は剣の軌跡用のマテリアルを作成します。
Assetsフォルダで右クリック→Create→Materialを選択し、名前をCreateSwordTrailにします。
インスペクタでAlbedoに先ほど取り込んだ剣の軌跡用テクスチャを設定します。
これでマテリアルの作成が終わりました。
先ほど作成したSwordTrailゲームオブジェクトのマテリアルにCreateSwordTrailを設定します。
↑の赤い四角の部分で設定等を行ってください。
SwordTrailはEthan(キャラクター)の子要素ではなく同じレベルの階層に位置しています。
剣を振って軌跡を確認してみる
それでは機能が出来たので確認してみましょう。
↑のように剣の軌跡のメッシュが作成されました。
しかし軌跡が透明ではないですね。
さらにメッシュの表面をカメラの方向にしている為、カメラ側から見ると軌跡が表示されていますが、カメラの反対側から見ると、
↑のようにメッシュが見えません。
頂点を反時計回りに同じように登録し、三角形を反対側にも作れば裏側からも見えますが面倒くさいので、軌跡の透明化と裏側から見えるようにする処理をシェーダ―を変える事で実現しましょう。
CreateSwordTrailマテリアルを選択しインスペクタを開きます。
↑のようにシェーダ―をParticle/Multiplyにします。
シェーダ―を変更すると
↑のように剣の軌跡が透明になり、裏側から見てもメッシュが見えるようになりました。
剣の軌跡を滑らかにする
剣の軌跡を表示する事は出来ましたが、過去フレームの剣元と剣先の位置をそのまま繋いでいる為、軌跡がカクカクしています。
攻撃のアニメーションが速ければ速いほどカクつくことになりますね。
現在の状態でも十分だとは思うんですが、曲線を作る方法を調べてしまった為、ここからが大変でした・・・(/_;)しくしく
曲線を作るには?
ある点Aからある点Bの間の曲線を作るには、A点での速度、B点での速度が必要だったり、A点の前の点、B点の次の点が必要だったりします。
今回はCatmull-Romスプライン曲線というA点の前の点、A点、B点、B点の先の点を使ってA点B点間の曲線を求めていきます。
曲線の求め方の式は記事の最後の参考サイトを参照してください。
わたくしには説明できません。(^_^;)
Catmull-Romスプライン曲線
Catmull-Romスプライン曲線を使うとp1とp2間の曲線をp1、p2、p1の前の点p0、p2の次の点p3の4点を使って曲線を作る事が出来ます。
ただこの場合、p1の前の点p0とp2の次の点p3が得られない時は曲線を求める事が出来ません。
わたくしの場合はp2の先のp3が得られない場合はp0、p2を足して2で割って計算する事にしました。
実際に曲線を計算するメソッドは
1 2 3 4 5 6 7 8 9 | // 曲線を作る為の頂点の計算メソッド Vector3 Catmull_Rom(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t) { return 0.5f * ((2 * p1) + (-p0 + p2) * t + (2 * p0 - 5 * p1 + 4 * p2 - p3) * t * t + (-p0 + 3 * p1 - 3 * p2 + p3) * t * t * t); } |
↑のように定義しました。
最後のtは0~1の間の値を渡します。
その割合が0の時はp1の点、1の時はp2の点が得られます。
ここまでの情報を画像にすると↑のようになります。
Catmull-Romスプライン曲線を使ったサンプル
2019/04/27に新たに追加した項目です。
p0、p1、p2、p3の点を使ってCutmull-Romスプライン曲線を使ってp1とp2間の曲線が作られる様子を確認出来るサンプルを作成します。
シーンビュー上でそれぞれの点を動かして出来る曲線を確認したり、p0やp3に点がない場合の曲線の作られ方を確認出来ますが、個人的に確認するように作ったサンプルなのであえて作らなくてもいいです。(^_^;)
自分でも点を動かして作られる曲線を確認してみたい方だけ作成してみてください。
まずはシーンビュー上で右クリック→Create→Sphereを選択し名前をposition0とし、TransformのScaleを0.3にします。
position0をCtrl+Dキーでコピーし名前をposition1~position3とします。
この4つのpositionがCatmull-Romスプライン曲線の計算に使う4点になります。
次に右クリック→Create→Emptyを選択し、名前をCatmullSplineTestとします。
CatmullSplineTestには新しくCatmullSplineTestスクリプトを作成し取り付けます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | using UnityEngine; using System.Collections; using System.Collections.Generic; using UnityEngine.UI; public class CatmullSplineTest : MonoBehaviour { // Cutmullスプライン曲線で使う4点 [SerializeField] private Transform position0; [SerializeField] private Transform position1; [SerializeField] private Transform position2; [SerializeField] private Transform position3; // 軌跡用の四角形の表示個数 [SerializeField] private int splitNum = 20; // スプライン曲線の計算結果で得られた位置を保持するリスト private List<Vector3> cutmullPoints = new List<Vector3>(); private void LateUpdate() { // リストの初期化 cutmullPoints.Clear(); // デバッグ用の線の開始点を初期化 var pos = position1.position; // スプライン曲線の点を計算する for (int i = 0; i <= splitNum; i++) { // 最初の点p0がない場合の計算 if (position0 == null) { cutmullPoints.Add(Catmull_Rom((position1.position + position3.position) / 2f, position1.position, position2.position, position3.position, (float)i / splitNum)); // 最後の点p3がない場合の計算 } else if (position3 == null) { cutmullPoints.Add(Catmull_Rom(position0.position, position1.position, position2.position, (position0.position + position2.position) / 2f, (float)i / splitNum)); // p0~p4の点全てが揃っている場合の計算 } else { cutmullPoints.Add(Catmull_Rom(position0.position, position1.position, position2.position, position3.position, (float)i / splitNum)); } } // p1から計算したそれぞれの頂点、p1からp2に線を引いて曲線を確かめる foreach (var item in cutmullPoints) { Debug.DrawLine(pos, item, Color.red); } } // 曲線を作る為の頂点の計算メソッド private Vector3 Catmull_Rom(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t) { return 0.5f * ((2 * p1) + (-p0 + p2) * t + (2 * p0 - 5 * p1 + 4 * p2 - p3) * t * t + (-p0 + 3 * p1 - 3 * p2 + p3) * t * t * t); } } |
インスペクタのposition0からposition3には先ほど作成したゲームオブジェクトをそれぞれ設定します。
Catmull_Romメソッドは4点からtに受け取った0~1の割合でp1からp2の間の点を返します。
ただp0、p3の点がインスペクタで設定されていない時は得られる点から計算します。
例えばp0の点がない場合はp1とp3の点を足して2で割ったベクトルをp0の引数に渡します。
2で割らずに0.5を掛けた方が計算処理が早くなるかもしれませんが、見た時にわかりやすくしておきました。
これでサンプルが完成しました。
Unityを実行してシーンビュー上でposition0からposition3の点をそれぞれ動かすとposition1とposition2の間で作られる曲線が変わるのを確認出来ます。
CutmullSplinTestスクリプトのインスペクタでposition0かposition3の設定をnullにすると曲線の作られ方が少し変わります。
実際にシーンビュー上でposition0を動かすと、
上のようにposition1からposition2の曲線の作られ方が変わります。
曲線を2パターンで作成
曲線を作るにあたって、今回は2つのやり方を作成しました。
一つは一度作った頂点情報はそのままにして、現在の剣の位置と前のフレームの位置で曲線を作り頂点リストに追加していくパターン。
こちらはCreateMeshメソッドでは現在の剣の位置と前のフレームの位置だけで計算する為(要はp3の点が求められない)、軌跡の形は変わりませんが曲線があまり綺麗にはなりません。
もう一つはCreateMeshメソッドで毎回曲線を作り直すパターン。
こちらは現在の剣の位置と前のフレームの剣の位置から作る曲線部分は先ほどのパターンと同じですが、その前のフレームの曲線は4点が求まるので綺麗な曲線になります。
CreateMeshメソッドで毎回曲線を作り直すので、前回作成した剣の位置と前のフレームの位置の曲線を再計算し、綺麗な曲線になります。
こちらの方が綺麗な線を描けますが、現在の剣の位置付近の軌跡の形が変わります。
文章で見ても解り辛いので実行した後に結果の違いを見る事にします。
現フレームと前フレームで曲線を作るパターン
それでは現フレームの剣の位置と前フレームの剣の位置で曲線を作っていくスクリプトを作成します。
先ほどのスクリプトでは頂点を共有しない形で作成していましたが、ここからは共有する形でスクリプトを組んでいきます。
スクリプトが長いので一部フィールドとStartメソッドは省いています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 | using UnityEngine; using System.Collections; using System.Collections.Generic; using UnityEngine.UI; [RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))] public class SwordTrail : MonoBehaviour { // 軌跡用の四角形の表示個数 [SerializeField] private int saveMeshNum = 10; // 面の分割数 [SerializeField] private int faceDivisionNum = 3; // 軌跡の表示のオン・オフフラグ private bool isSwordTrail = false; void LateUpdate () { // 必要頂点数を超えたら削除 if (startPoints.Count >= 3 + saveMeshNum) { startPoints.RemoveAt (0); endPoints.RemoveAt (0); } // 現在の剣元、剣先を登録 startPoints.Add (startPosition.position); endPoints.Add (endPosition.position); // 頂点が3+saveMeshNum以上になったら剣の軌跡メッシュを作成 if (startPoints.Count >= 3 + saveMeshNum) { CreateMesh (); } // メッシュの保存数を超えたら前のメッシュを削除 if (verticesLists.Count >= saveMeshNum * (faceDivisionNum * 2) + 2) { DeleteMesh (); } } // 剣の軌跡作成メソッド void CreateMesh() { mesh.Clear (); // 頂点以外のリストのクリア // verticesLists.Clear (); uvsLists.Clear (); tempTriangles.Clear (); // ポイントの間の点の保存変数 Vector3[] startHalf = new Vector3[faceDivisionNum]; Vector3[] endHalf = new Vector3[faceDivisionNum]; // ポイント間の位置割合 float addFloatParam = 1f / faceDivisionNum; // 登録した剣元、剣先の間の位置を取得 for (int i = 0; i < faceDivisionNum - 1; i++) { startHalf [i] = Catmull_Rom (startPoints [startPoints.Count - 3], startPoints [startPoints.Count - 2], startPoints [startPoints.Count - 1], (startPoints [startPoints.Count - 3] + startPoints[startPoints.Count - 1]) / 2f, addFloatParam); endHalf [i] = Catmull_Rom (endPoints [endPoints.Count - 3], endPoints [endPoints.Count - 2], endPoints [endPoints.Count - 1], (endPoints [endPoints.Count - 3] + endPoints[endPoints.Count - 1]) / 2f, addFloatParam); // 分割の割合を計算 addFloatParam += 1f / faceDivisionNum; } // 頂点が登録されていなければ登録した最後のひとつ前の位置を登録 if (verticesLists.Count == 0) { verticesLists.AddRange (new Vector3[] { startPoints [startPoints.Count - 2], endPoints [endPoints.Count - 2] }); } Debug.DrawLine (startPoints [startPoints.Count - 2], endPoints [endPoints.Count - 2], Color.red); // ポイントと間の点から三角形を作成 for (int i = 0; i < faceDivisionNum - 1; i++) { verticesLists.AddRange (new Vector3[] { startHalf [i], endHalf [i] }); } // 最後の剣元、剣先の位置を登録 verticesLists.AddRange (new Vector3[] { startPoints [startPoints.Count - 1], endPoints [endPoints.Count - 1] }); Debug.DrawLine (startPoints [startPoints.Count - 1], endPoints [endPoints.Count - 1], Color.green); addFloatParam = 1f / faceDivisionNum; // UVMapのパラメータ設定 float addParam = 0f; for(int i = 0; i < verticesLists.Count; i++) { if (i % 2 == 0) { uvsLists.Add (new Vector2 (addParam / (saveMeshNum * faceDivisionNum), 0f)); } else { uvsLists.Add (new Vector2 (addParam / (saveMeshNum * faceDivisionNum), 1f)); addParam++; } } // メッシュ用の三角形を登録した頂点で設定 for(int i = 0, j = 0; j < (verticesLists.Count - 2) / 2; i += 2, j++) { tempTriangles.AddRange (new int[] { i, i + 1, i + 2, i + 2, i + 1, i + 3 }); } if (isSwordTrail) { mesh.vertices = verticesLists.ToArray (); mesh.uv = uvsLists.ToArray (); mesh.triangles = tempTriangles.ToArray (); mesh.RecalculateBounds (); mesh.RecalculateNormals (); } } // 古い剣の軌跡メッシュの削除 void DeleteMesh() { verticesLists.RemoveRange (0, faceDivisionNum * 2); uvsLists.RemoveRange (0, faceDivisionNum * 2); } // 曲線を作る為の頂点の計算メソッド Vector3 Catmull_Rom(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t) { return 0.5f * ((2 * p1) + (-p0 + p2) * t + (2 * p0 - 5 * p1 + 4 * p2 - p3) * t * t + (-p0 + 3 * p1 - 3 * p2 + p3) * t * t * t); } public void SetSwordTrail(bool swordTrailFlag) { isSwordTrail = swordTrailFlag; } } |
スクリプトが長いので少しづつ解説していきます。
宣言部
追加した宣言を見ていきます。
1 2 3 4 5 6 7 8 9 10 | // 軌跡用の四角形の表示個数 [SerializeField] private int saveMeshNum = 10; // 面の分割数 [SerializeField] private int faceDivisionNum = 3; // 軌跡の表示のオン・オフフラグ private bool isSwordTrail = false; |
faceDivisionNumは曲線を作る時の分割する数で、isSwordTrailは剣の軌跡を表示するかどうかのフラグです。
LateUpdateメソッド
LateUpdateメソッドにはメッシュを削除する処理を追加しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | void LateUpdate () { // 必要頂点数を超えたら削除 if (startPoints.Count >= 3 + saveMeshNum) { startPoints.RemoveAt (0); endPoints.RemoveAt (0); } // 現在の剣元、剣先を登録 startPoints.Add (startPosition.position); endPoints.Add (endPosition.position); // 頂点が3+saveMeshNum以上になったら剣の軌跡メッシュを作成 if (startPoints.Count >= 3 + saveMeshNum) { CreateMesh (); } // メッシュの保存数を超えたら前のメッシュを削除 if (verticesLists.Count >= saveMeshNum * (faceDivisionNum * 2) + 2) { DeleteMesh (); } } |
作成したメッシュを再計算しない為にCreateMeshメソッドではverticesListsのクリアを行いません。
その為、表示するメッシュに不用になった頂点をverticesListsから削除します。
メッシュの数 × (面の分割数 × 2) + 2
で頂点の数が計算出来ますのでそれ以上になった時にDeleteMeshメソッドを呼び出し頂点を削除します。
CreateMeshメソッド
CreateMeshメソッドは長いのでさらに分割して説明していきます。
CreateMeshの初期処理
CreateMeshメソッドの最初の処理を見ていきます。
1 2 3 4 5 6 7 8 9 10 11 12 | mesh.Clear (); // 頂点以外のリストのクリア // verticesLists.Clear (); uvsLists.Clear (); tempTriangles.Clear (); // ポイントの間の点の保存変数 Vector3[] startHalf = new Vector3[faceDivisionNum]; Vector3[] endHalf = new Vector3[faceDivisionNum]; |
startHalfは剣元の間の分割点、endHalfは剣先の間の分割点を入れる配列です。
頂点の登録処理
次に頂点の登録処理を見ていきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | // ポイント間の位置割合 float addFloatParam = 1f / faceDivisionNum; // 登録した剣元、剣先の間の位置を取得 for (int i = 0; i < faceDivisionNum - 1; i++) { startHalf [i] = Catmull_Rom (startPoints [startPoints.Count - 3], startPoints [startPoints.Count - 2], startPoints [startPoints.Count - 1], (startPoints [startPoints.Count - 3] + startPoints[startPoints.Count - 1]) / 2f, addFloatParam); endHalf [i] = Catmull_Rom (endPoints [endPoints.Count - 3], endPoints [endPoints.Count - 2], endPoints [endPoints.Count - 1], (endPoints [endPoints.Count - 3] + endPoints[endPoints.Count - 1]) / 2f, addFloatParam); // 分割の割合を計算 addFloatParam += 1f / faceDivisionNum; } // 頂点が登録されていなければ登録した最後のひとつ前の位置を登録 if (verticesLists.Count == 0) { verticesLists.AddRange (new Vector3[] { startPoints [startPoints.Count - 2], endPoints [endPoints.Count - 2] }); } Debug.DrawLine (startPoints [startPoints.Count - 2], endPoints [endPoints.Count - 2], Color.red); for (int i = 0; i < faceDivisionNum - 1; i++) { verticesLists.AddRange (new Vector3[] { startHalf [i], endHalf [i] }); } // 最後の剣元、剣先の位置を登録 verticesLists.AddRange (new Vector3[] { startPoints [startPoints.Count - 1], endPoints [endPoints.Count - 1] }); Debug.DrawLine (startPoints [startPoints.Count - 1], endPoints [endPoints.Count - 1], Color.green); addFloatParam = 1f / faceDivisionNum; |
現在の剣の位置と前のフレームの剣の位置から曲線を計算をします。
addFloatParamが分割値になり、これをCatmull_Romのtの部分に渡します。
今回は初期値として分割値を与えてstartHalfとendHalfには分割した点のみ入るようにしています。
現在の位置の先の点は取得出来ないので、前の前の点と現在の点を足して2で割った値をp3の値として渡します。
頂点が一つも登録されていない場合は前のフレームの位置を頂点に追加します。
その後、分割点を頂点リストに追加しています。
今回は頂点を共有する形でメッシュを作成するので、分割点を追加するだけです。
最後に剣元と剣先の頂点を追加します。
ここら辺の処理はわかりやすいスクリプトを組めませんでした。
UV座標の登録
UV座標の登録部分を見ていきます。
1 2 3 4 5 6 7 8 9 10 11 12 | // UVMapのパラメータ設定 float addParam = 0f; for(int i = 0; i < verticesLists.Count; i++) { if (i % 2 == 0) { uvsLists.Add (new Vector2 (addParam / (saveMeshNum * faceDivisionNum), 0f)); } else { uvsLists.Add (new Vector2 (addParam / (saveMeshNum * faceDivisionNum), 1f)); addParam++; } } |
UV座標は頂点を共有する形にしたので、頂点番号が偶数かどうかでUV値を変えています。
偶数の時は剣元の座標を計算するのでVの値は0、奇数の時は剣先の座標を計算するのでVの値は1になります。
また剣先の値を設定したら次は剣元の値なのでaddParamの値を増やして分割値を増やします。
ここら辺はノート等に手書きで図を書いてやってみるとわかりやすいかもしれません。
分割値は
分割位置 × (メッシュの数 × 分割数)
で計算しています。
三角形の計算
次に三角形の計算を見ていきます。
1 2 3 4 5 6 7 8 9 | // メッシュ用の三角形を登録した頂点で設定 for(int i = 0, j = 0; j < (verticesLists.Count - 2) / 2; i += 2, j++) { tempTriangles.AddRange (new int[] { i, i + 1, i + 2, i + 2, i + 1, i + 3 }); } |
これまた条件が複雑ですね・・・・(^_^;)
頂点を共有しているので、iが0だったら
0, 1, 2,
2, 1, 3
という頂点を繋いで三角形を作っています。
繰り返し条件は頂点を共有している為、一つの四角形では共有している2点を引いて、さらに一回の繰り返しで三角形を2つ作っている為、2で割っています。
自分でもよくわかりませんなぁ・・・・(‘_’)
メッシュに登録
次は作成した頂点、UV、三角形の情報を登録する処理です。
1 2 3 4 5 6 7 8 9 10 | if (isSwordTrail) { mesh.vertices = verticesLists.ToArray (); mesh.uv = uvsLists.ToArray (); mesh.triangles = tempTriangles.ToArray (); mesh.RecalculateBounds (); mesh.RecalculateNormals (); } |
isSwordTrailは剣の軌跡を表示するかどうかのフラグなので、表示する場合だけデータを書き換えています。
メッシュの削除等
その他の処理について見ていきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | // 古い剣の軌跡メッシュの削除 void DeleteMesh() { verticesLists.RemoveRange (0, faceDivisionNum * 2); uvsLists.RemoveRange (0, faceDivisionNum * 2); } // 曲線を作る為の頂点の計算メソッド Vector3 Catmull_Rom(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t) { return 0.5f * ((2 * p1) + (-p0 + p2) * t + (2 * p0 - 5 * p1 + 4 * p2 - p3) * t * t + (-p0 + 3 * p1 - 3 * p2 + p3) * t * t * t); } public void SetSwordTrail(bool swordTrailFlag) { isSwordTrail = swordTrailFlag; } |
DeleteMeshメソッドは分割数 × 2分の頂点とUVを削除します(UVはいらないかも?)。
これで一つ分の四角形を形成する頂点数分削除出来ます。
Catmull_Romは曲線を計算する式ですね。
SetSwordTrailメソッドはアニメーションイベントを別のスクリプトで受け取り、そこから実行するメソッドで、剣の軌跡を表示するかどうかを切り替えます。
アニメーションイベントについては
を参照して頂き、剣の振り始めと振り終わりのイベントを作ってください。
またキャラクターにReceiveActionEventスクリプトを作り取り付けます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | using UnityEngine; using System.Collections; public class ReceiveActionEvent : MonoBehaviour { [SerializeField] private SwordTrailOther swordTrail; public void StartSwordTrail() { swordTrail.SetSwordTrail (true); } public void EndSwordTrail() { swordTrail.SetSwordTrail (false); } } |
インスペクタでSwordTrailゲームオブジェクトをドラッグ&ドロップしてください。
軌跡を確認する
それでは出来た機能を確認してみましょう。
SaveMeshNumを5、FaceDivisionNumを10にして確認します。
↑のようになりました。
モーションが速くてわかり辛いですが、現在の位置と前のフレームの位置を繋いだ線が曲線にはなっていますが、多少歪んでいますね。
途中コマ送りしてます。
CreateMeshで曲線を再計算するパターン
ここまで来るともうお腹いっぱいですね・・・・( ・_;)( ;_;)( ;_;)
わたくしも解説がしんどいです・・・・(((((((((((((ーー;) さささっ・・・
というわけで、違う部分だけを載せ解説はなしということで・・・・。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 | void LateUpdate () { // 必要頂点数を超えたら削除 if (startPoints.Count >= 3 + saveMeshNum) { startPoints.RemoveAt (0); endPoints.RemoveAt (0); } // 根元と剣先の位置を保存 startPoints.Add (startPosition.position); endPoints.Add (endPosition.position); // 頂点が4以上になったら剣の軌跡メッシュを作成 if (startPoints.Count >= 3 + saveMeshNum) { CreateMesh (); } } // 剣の軌跡作成メソッド void CreateMesh() { // メッシュ、頂点、UVのクリア mesh.Clear (); verticesLists.Clear (); uvsLists.Clear (); // ポイントの間の点の保存変数 Vector3[] startHalf = new Vector3[faceDivisionNum]; Vector3[] endHalf = new Vector3[faceDivisionNum]; // ポイント間の位置割合 float addFloatParam = 1f / faceDivisionNum; // 面番号 int i = 0; // 分割番号 int j = 0; // UVMap分割番号 int k = 0; for (i = 0; i < saveMeshNum; i++) { for (j = 0; j < faceDivisionNum - 1; j++) { // Debug.Log ("i: " + i + "j: " + j); // ポイントの最後 if (i == saveMeshNum - 1) { startHalf [j] = Catmull_Rom (startPoints [startPoints.Count - 3], startPoints [startPoints.Count - 2], startPoints [startPoints.Count - 1], (startPoints[startPoints.Count - 3] + startPoints[startPoints.Count - 1]) / 2f, addFloatParam); endHalf [j] = Catmull_Rom (endPoints [endPoints.Count - 3], endPoints [endPoints.Count - 2], endPoints [endPoints.Count - 1], (endPoints[endPoints.Count - 3] + endPoints[endPoints.Count - 1]) / 2f, addFloatParam); // ポイントとポイントの間 } else { startHalf [j] = Catmull_Rom (startPoints [startPoints.Count - (saveMeshNum - i) - 2], startPoints [startPoints.Count - (saveMeshNum - i) - 1], startPoints [startPoints.Count - (saveMeshNum - i)], startPoints [startPoints.Count - (saveMeshNum - i) + 1], addFloatParam); endHalf [j] = Catmull_Rom (endPoints [endPoints.Count - (saveMeshNum - i) - 2], endPoints [endPoints.Count - (saveMeshNum - i) - 1], endPoints [endPoints.Count - (saveMeshNum - i)], endPoints [endPoints.Count - (saveMeshNum - i) + 1], addFloatParam); } // 分割の割合を計算 addFloatParam += 1f / faceDivisionNum; } // 最初の面の時は最初の2点を追加 if (i == 0) { verticesLists.AddRange (new Vector3[] { startPoints [startPoints.Count - (saveMeshNum - i) - 1], endPoints [endPoints.Count - (saveMeshNum - i) - 1] }); Debug.DrawLine (startPoints [startPoints.Count - (saveMeshNum - i) - 1], endPoints [endPoints.Count - (saveMeshNum - i) - 1], Color.red); } // ポイントと間の点から三角形を作成 for (k = 0; k < faceDivisionNum - 1; k++) { verticesLists.AddRange (new Vector3[] { startHalf [k], endHalf [k] }); // 分割線確認 if (i == 0) { Debug.DrawLine (startHalf [k], endHalf [k], Color.yellow); } else if (i == 1) { Debug.DrawLine (startHalf [k], endHalf [k], Color.white); } else if (i == 2) { Debug.DrawLine (startHalf [k], endHalf [k], Color.blue); } } // 最後の2点を追加 verticesLists.AddRange (new Vector3[] { startPoints [startPoints.Count - (saveMeshNum - i)], endPoints [endPoints.Count - (saveMeshNum - i)] }); Debug.DrawLine (startPoints [startPoints.Count - (saveMeshNum - i)], endPoints [endPoints.Count - (saveMeshNum - i)], Color.green); addFloatParam = 1f / faceDivisionNum; } addFloatParam = 0f; // UVMAP用の頂点の作成 float addParam = 0f; for(i = 0; i < verticesLists.Count; i++) { if (i % 2 == 0) { uvsLists.Add (new Vector2 (addParam / (saveMeshNum * faceDivisionNum), 0f)); } else { uvsLists.Add (new Vector2 (addParam / (saveMeshNum * faceDivisionNum), 1f)); addParam++; } } // 頂点からメッシュの三角形用の頂点番号指定 List<int> tempTriangles = new List<int>(); for(i = 0, j = 0; j < saveMeshNum * faceDivisionNum; i += 2, j++) { tempTriangles.AddRange (new int[] { i, i + 1, i + 2, i + 2, i + 1, i + 3 }); } if (isSwordTrail) { mesh.vertices = verticesLists.ToArray (); mesh.uv = uvsLists.ToArray (); mesh.triangles = tempTriangles.ToArray (); mesh.RecalculateBounds (); mesh.RecalculateNormals (); } } // 古い剣の軌跡メッシュの削除 void DeleteMesh() { verticesLists.RemoveRange (0, faceDivisionNum * 1); uvsLists.RemoveRange (0, faceDivisionNum * 1); } |
非常に複雑すぎて、自分でも時間をかけないと読み解けませんわ・・・・(‘_’)
緩やかな曲線を確認する
それでは最後の確認をしてみましょう。
SaveMeshNumを5、FaceDivisionNumを10にして確認します。
↑のように非常に滑らかな曲線が出来ました!
途中コマ送りして確認している部分でわかりますが、一度描いた軌跡を再計算する為、次のフレームになると元の形を変えています。
ですが、攻撃のモーションが相当遅くなければ気にならないですし、軌跡の曲線が綺麗なのを考慮するとこちらのパターンの方がいいかもしれませんね。
記事の最初に示したサンプルもこちらのパターンで作成したサンプルです。
終わりに
この剣の軌跡をスクリプトから作成する処理は正直しんどかったです・・・。
頭で考えるだけじゃわからないので、紙に絵を書いて頂点の数や、UVの座標、三角形の作成方法を考えました。
絵に描いてもわからず1カ月ぐらい作成に時間がかかったような気がします・・・・。
それでも完全に出来た!という感じではなく、スクリプトも解り辛いままになっています。
処理速度を考慮するともう少し最適化しないといけないかなぁとは思いますが、さすがにもうこの機能はやりたくありません。(^_^;)
いずれまたやる気が起きた時にでも改良したいですが、その時にまた一からスクリプトを読み解いていかないといけないと思うと恐怖ですね・・・(T_T)/~~~
参考サイト
今回の機能を作成するにあたり、非常に参考にさせて頂いたサイトです。