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

Всем привет! Сегодня хотелось бы поговорить о математике. Математика очень интересная наука и она может сильно пригодиться при разработке игр, да и в целом при работе с компьютерной графикой. Многие (особенно новички) просто не знают о том, как она применяется при разработке. Существует множество задач, не требующих глубокого понимания таких понятий как: интегралы, комплексные числа, группы, кольца и др, но благодаря математике вы можете решать многие интересные задачи. В этой статье мы рассмотрим векторы и интегралы. Если интересно, добро пожаловать под кат. Иллюстрирующий Unity проект, как всегда, прилагается.

si-v9fgzx1j01amuhr9eueauiie.jpeg
Векторная математика.

Векторы и векторная математика являются необходимыми инструментами для разработки игр. Многие операции и действия завязаны на ней целиком. Забавно, что для реализации класса, который отображает стрелочку вектора в 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 плюс-минус всё тоже самое. Отличается только формула нормали, так как нормаль уже берётся не к отрезку, а к плоскости.

Your browser does not support HTML5 video.


В данном примере контролла нормаль к плоскости используется, чтобы сместить конечную точку траектории право, чтобы планету не загораживал интерфейс. Нормаль в 3д графике — это нормализованное векторное произведение двух векторов. Что удобно, в Юнити есть обе эти операции и мы получаем красивую компактную запись:

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


Думаю, многим, кто думает, что математика не нужна и зачем вообще это знать, стало чуть понятнее какие задачи с помощью неё можно решать просто и элегантно. Но это был простой вариант, который должен знать каждый разработчик игр не стажёр. Поднимем планку — поговорим об интегралах.

Интегралы

Вообще у интегралов очень много применений, таких как: физические симуляции, VFX, аналитика и многое другое. Я не готов сейчас детально описывать все. Хочется описать простой и визуально понятный. Поговорим про физику.

Допустим есть задача — двигать объект в определённую точку. К примеру, чтобы при вхождении в определённый триггер, должны вылетать книги с полок. Если вы хотите двигать равномерно и без физики, то задача тривиальна и не требует интегралов, но когда книги выталкивает с полки призрак, такое распределение скорости будет смотреться совсем не так.

Что такое интеграл?

По сути это площадь под кривой. Но что это означает в контексте физики? Допустим у вас есть распределение скорости по времени. В данном случае площадь под кривой — это путь который пройдёт объект, а это как раз то, что нам и нужно.

v2j1firdsma31y_gey3e6kmquok.png

Если перейти от теории к практике, то в 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();
                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 для большей точности.

Собственно на этом на сегодня всё. Как всегда в конце ссылка на GitHub проект, в котором все исходники по данной статье. И с ними можно поиграться.

Если статья зайдёт — сделаю продолжение в котором расскажу уже про применение чуть более сложных понятий, таких как комплексные числа, поля, группы и другое.

© Habrahabr.ru