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

Математика в 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
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; — эта операция отвечает за то, чтобы текст всегда показывался над вектором. normal = normal.y > 0?

Дальше остаётся поместить его в середину вектора и поднять по нормали на расстояние, которое будет смотреться красиво.

_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 для большей точности.

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

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


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

Разработка buck-преобразователя на STM32F334: принцип работы, расчеты, макетирование

В двух своих последних статьях я рассказал о силовом модуле и плате управления на базе микроконтроллера STM32F334R8T6, которые созданы специально для реализации систем управления силовыми преобразователями и электроприводом. Так же был рассмотрен пример DC/AC преобразователя, который являлся демонстрацией, а не ...

Simulation theory: взаимосвязь квантово-химических расчётов и Реальности

Введение О чём этот текст Если человек услышит о «симуляции реальности», то в наиболее вероятно ему в голову придут или разные научно-фантастические произведения (типа Матрицы, Темного города, или Теоремы Зеро), или компьютерные игры. В случае людей, чьи головы засорены инженерным ...