Розробка

Математика в Gamedev по-простому. Вектори і інтеграли

Всім привіт! Сьогодні хотілося б поговорити про математику. Математика дуже цікава наука і вона може дуже стати в нагоді при розробці ігор, так і в цілому при роботі з комп’ютерною графікою. Багато хто (особливо новачки) просто не знають про те, як вона застосовується при розробці. Існує безліч завдань, що не вимагають глибокого розуміння таких понять як: інтеграли, комплексні числа, групи, кільця і ін, але завдяки математиці ви можете вирішувати багато цікаві завдання. У цій статті ми розглянемо вектори та інтеграли. Якщо цікаво, ласкаво просимо під кат. Ілюструє Unity проект, як завжди, додається.

Векторна математика.

Вектори і векторна математика є необхідними інструментами для розробки ігор. Багато операції і дії зав’язані на неї цілком. Забавно, що для реалізації класу, який відображає стрілочку вектора в Unity, вже знадобилося більшість типових операцій. Якщо ви добре розбираєтеся в векторної математики даний блок вам буде нецікавий.

Векторна арифметика і корисні функції

Аналітичні формули та інші деталі легко нагуглити, так що не будемо витрачати на це час. Самі операції будуть проілюстровані гіф-анімаціями нижче.

Важливо розуміти, що будь-яка точка в сутності є вектором з початком в нульовій точці.

Your browser does not support HTML5 video.

Гифки робилися за допомогою Unity, так що потрібно було б реалізовувати клас, який відповідає за малювання стрілок. Стрілка вектора складається з трьох основних компонент – лінії, наконечника і тексту з ім’ям вектора. Для малювання лінії і наконечника я скористався 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).

Далі розберемо дві базові операції:

Your browser does not support HTML5 video.
Знаходження нормалі (перпендикуляра) і середини вектора – це дуже часто зустрічаються завдання при розробці ігор. Розглянемо їх на прикладі розміщення підпису над вектором.

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;
}
}

Your browser does not support HTML5 video.
В даному прикладі контролла нормаль до площини використовується, щоб змістити кінцеву точку траєкторії право, щоб планету не загороджував інтерфейс. Нормаль в 3д графіці – це нормалізоване векторний добуток двох векторів. Що зручно, Юніті є обидві ці операції і ми отримуємо красиву компактну запис:

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;
 }

Завдяки цій площі ми далі вже знаємо, яке відносне відстань. А далі порівнявши два пройдених шляху ми отримуємо коефіцієнт швидкості speedK, який відповідає за те, щоб ми пройшли задану відстань.

Your browser does not support HTML5 video.
Your browser does not support HTML5 video.
Your browser does not support HTML5 video.
Можна помітити, що об’єкти не зовсім збігаються, це пов’язано з помилкою float. В цілому можна перерахувати теж саме в decimal, а потім перегнати в float для більшої точності.

Власне на цьому на сьогодні все. 

Related Articles

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *

Close