Gamedevの数学は簡単です。 ベクトルと積分

みなさんこんにちは! 今日は数学についてお話したいと思います。 数学は非常に興味深い科学であり、ゲームを開発するとき、そして実際にコンピューターグラフィックスを扱うときに非常に役立ちます。 多くの人(特に初心者)は、それが開発でどのように使用されているかを知らないだけです。 積分、複素数、グループ、リングなどの概念を深く理解する必要のない多くのタスクがありますが、数学のおかげで多くの興味深い問題を解決できます。 この記事では、ベクトルと積分を検討します。 興味があれば、猫へようこそ。 いつものように、Unityプロジェクトの説明が添付されています。







ベクトル数学。



ベクトルとベクトル数学は、ゲーム開発に不可欠なツールです。 多くの操作とアクションは完全に関連しています。 Unityでベクトルの矢印を表示するクラスを実装するために、ほとんどの典型的な操作がすでに行われているのは面白いです。 ベクトル計算に精通している場合、このブロックはおもしろくありません。



ベクトル演算と便利な機能



分析式やその他の詳細は簡単にグーグルで検索できるため、これに時間を浪費しません。 操作自体は、以下のGIFアニメーションで説明されます。



本質的にポイントはゼロポイントで始まるベクトルであることを理解することが重要です。







GifはUnityを使用して作成されたため、矢印のレンダリングを担当するクラスを実装する必要があります。 ベクター矢印は、3つの主要なコンポーネントで構成されます-ライン、ヒント、ベクターの名前のテキスト。 線と先端を描くために、LineRendererを使用しました。 ベクトル自体のクラスを見てみましょう。



矢印クラス
using System.Collections; using System.Collections.Generic; using TMPro; using UnityEngine; public class VectorArrow : MonoBehaviour { [SerializeField] private Vector3 _VectorStart; [SerializeField] private Vector3 _VectorEnd; [SerializeField] private float TextOffsetY; [SerializeField] private TMP_Text _Label; [SerializeField] private Color _Color; [SerializeField] private LineRenderer _Line; [SerializeField] private float _CupLength; [SerializeField] private LineRenderer _Cup; private void OnValidate() { UpdateVector(); } private void UpdateVector() { if(_Line == null || _Cup == null) return; SetColor(_Color); _Line.positionCount = _Cup.positionCount = 2; _Line.SetPosition(0, _VectorStart); _Line.SetPosition(1, _VectorEnd - (_VectorEnd - _VectorStart).normalized * _CupLength); _Cup.SetPosition(0, _VectorEnd - (_VectorEnd - _VectorStart).normalized * _CupLength); _Cup.SetPosition(1, _VectorEnd ); if (_Label != null) { var dv = _VectorEnd - _VectorStart; var normal = new Vector3(-dv.y, dv.x).normalized; normal = normal.y > 0 ? normal : -normal; _Label.transform.localPosition = (_VectorEnd + _VectorStart) / 2 + normal * TextOffsetY; _Label.transform.up = normal; } } public void SetPositions(Vector3 start, Vector3 end) { _VectorStart = start; _VectorEnd = end; UpdateVector(); } public void SetLabel(string label) { _Label.text = label; } public void SetColor(Color color) { _Color = color; _Line.startColor = _Line.endColor = _Cup.startColor = _Cup.endColor = _Color; } }
      
      







ベクトルは特定の長さで、指定したポイントに正確に対応する必要があるため、線の長さは次の式で計算されます。



 _VectorEnd - (_VectorEnd - _VectorStart).normalized * _CupLength
      
      





この式(_VectorEnd-_VectorStart)では.normalizedはベクトルの方向です。 これは、 _VectorEnd_VectorStartが(0,0,0)で始まるベクトルであると仮定して、ベクトルの違いによるアニメーションから理解できます。



次に、残りの2つの基本操作を分析します。





通常の(垂直の)ベクトルの中央を見つけることは、ゲーム開発で非常に一般的なタスクです。 ベクトルに署名を配置する例で分析します。



 var dv = _VectorEnd - _VectorStart; var normal = new Vector3(-dv.y, dv.x).normalized; normal = normal.y > 0 ? normal : -normal; _Label.transform.localPosition = (_VectorEnd + _VectorStart) / 2 + normal * TextOffsetY; _Label.transform.up = normal;
      
      





テキストをベクトルに垂直に配置するには、法線が必要です。 2Dグラフィックスでは、法線は非常に単純です。



 var dv = _VectorEnd - _VectorStart; var normal = new Vector3(-dv.y, dv.x).normalized;
      
      





セグメントの法線を取得しました。



normal = normal.y> 0? normal:-normal; -この操作は、テキストが常にベクターの上に表示されるようにします。



次に、それをベクトルの中央に配置し、美しく見える距離まで垂直に上げます。



 _Label.transform.localPosition = (_VectorEnd + _VectorStart) / 2 + normal * TextOffsetY;
      
      





コードはローカル位置を使用するため、結果の矢印を移動できます。



しかし、それは2Dについてでしたが、3Dについてはどうですか?



3Dでは、プラスまたはマイナスは同じです。 法線はセグメントではなく平面にすでに取り込まれているため、法線式のみが異なります。



カメラ用スクリプト
 using System; using System.Collections; using System.Collections.Generic; using UnityEngine; public class SphereCameraController : MonoBehaviour { [SerializeField] private Camera _Camera; [SerializeField] private float _DistanceFromPlanet = 10; [SerializeField] private float _Offset = 5; private bool _IsMoving; public event Action<Vector3, Vector3, Vector3, float, float> OnMove; private void Update() { if (Input.GetMouseButtonDown(0) && !_IsMoving) { RaycastHit hit; Debug.Log("Click"); var ray = _Camera.ScreenPointToRay(Input.mousePosition); if(Physics.Raycast(ray, out hit)) { Debug.Log("hit"); var startPosition = _Camera.transform.position; var right = Vector3.Cross(hit.normal, Vector3.up).normalized; var endPosition = hit.point + hit.normal * _DistanceFromPlanet + right * _Offset; StartCoroutine(MoveCoroutine(startPosition, endPosition, hit.point + right * _Offset)); OnMove?.Invoke(startPosition, hit.point, hit.normal, _DistanceFromPlanet, _Offset); } } } private IEnumerator MoveCoroutine(Vector3 start, Vector3 end, Vector3 lookAt) { _IsMoving = true; var startForward = transform.forward; float timer = 0; while (timer < Scenario.AnimTime) { transform.position = Vector3.Slerp(start, end, timer / Scenario.AnimTime); transform.forward = Vector3.Slerp(startForward, (lookAt - transform.position).normalized, timer / Scenario.AnimTime); yield return null; timer += Time.deltaTime; } transform.position = end; transform.forward = (lookAt - transform.position).normalized; _IsMoving = false; } }
      
      









この制御例では、平面の法線を使用して軌道の終点を右に移動し、界面が惑星をブロックしないようにします。 3Dグラフィックスの法線は、2つのベクトルの正規化されたベクトル積です。 便利な点は、Unityには次の両方の操作があり、美しいコンパクトな記録が得られることです。



 var right = Vector3.Cross(hit.normal, Vector3.up).normalized;
      
      





数学は必要ないと思っている多くの人にとって、なぜ数学を知る必要があるのか​​について、数学で簡単かつエレガントに解決できる問題が少し明確になったと思います。 しかし、それはすべてのゲーム開発者がインターンとして知ってはならない単純なオプションでした。 バーを上げる-積分について話します。



積分



一般に、積分には、物理​​シミュレーション、VFX、分析など、多くのアプリケーションがあります。 すべてを詳細に説明する準備ができていません。 シンプルで視覚的に理解できるものを説明したいと思います。 物理学について話しましょう。



特定のポイントにオブジェクトを移動するというタスクがあるとします。 たとえば、特定のトリガーを入力すると、棚から本が飛び出します。 物理的にせずに均一に移動したい場合、タスクは簡単で積分を必要としませんが、幽霊が本を棚から押すと、そのような速度の分布は完全に異なって見えます。



積分とは何ですか?



実際、これは曲線の下の領域です。 しかし、これは物理学の文脈で何を意味するのでしょうか? 時間の経過に伴う速度分布があるとします。 この場合、曲線の下の領域はオブジェクトが通過するパスであり、これがまさに必要なものです。







理論から実践へと移行するUnityには、AnimationCurveという素晴らしいツールがあります。 これを使用して、時間の経過に伴う速度の分布を指定できます。 そのようなクラスを作成しましょう。



クラスMoveObj
 using System.Collections; using UnityEngine; [RequireComponent(typeof(Rigidbody))] public class MoveObject : MonoBehaviour { [SerializeField] private Transform _Target; [SerializeField] private GraphData _Data; private Rigidbody _Rigidbody; private void Start() { _Rigidbody = GetComponent<Rigidbody>(); Move(2f, _Data.AnimationCurve); } public void Move(float time, AnimationCurve speedLaw) { StartCoroutine(MovingCoroutine(time, speedLaw)); } private IEnumerator MovingCoroutine(float time, AnimationCurve speedLaw) { float timer = 0; var dv = (_Target.position - transform.position); var distance = dv.magnitude; var direction = dv.normalized; var speedK = distance / (Utils.GetApproxSquareAnimCurve(speedLaw) * time); while (timer < time) { _Rigidbody.velocity = speedLaw.Evaluate(timer / time) * direction * speedK; yield return new WaitForFixedUpdate(); timer += Time.fixedDeltaTime; } _Rigidbody.isKinematic = true; } }
      
      







GetApproxSquareAnimCurveメソッドは統合です。 関数の値を調べて、それらを特定の回数だけ合計するだけで、最も単純な数値手法にします。 忠実度を1000に設定しますが、一般的には最適なものを選択できます。



  private const int Iterations = 1000; public static float GetApproxSquareAnimCurve(AnimationCurve curve) { float square = 0; for (int i = 0; i <= Iterations; i++) { square += curve.Evaluate((float) i / Iterations); } return square / Iterations; }
      
      





このエリアのおかげで、私たちはすでに相対距離が何であるかを知っています。 そして、移動した2つのパスを比較すると、速度係数speedKが得られます。これは、指定された距離を歩くことを保証します。









オブジェクトが完全に一致しないことに気付くかもしれません。これはフロートエラーが原因です。 一般に、同じものを10進数で再計算し、フロートで追い越して精度を高めることができます。



実際、今日は以上です。 いつものように、GitHubプロジェクトへリンクの最後に、この記事のすべてのソースがあります。 そして、あなたは彼らと遊ぶことができます。



記事が掲載されたら、続編を行います。この続編では、複素数、フィールド、グループなど、やや複雑な概念の使用について説明します。



All Articles