o2 dev
108 subscribers
49 photos
4 videos
25 files
54 links
About o2 engine development
加入频道
ориентация тел в пространстве, их вращение осуществляется через линейные движения вершин. По сути они крутятся друг от друга, удерживаются между собой связями.

самый интересный момент - это связи. Как их решать? Попробуем снова пойти от наивного решения проблемы, представим что это просто пружины. Но пружины пружинят, и тело из пружин скорее похоже на желе. А если сделать их очень твердыми пружинами? Тогда в рассчете получаются огромные скорости которые при интегрировании 1/60 секунды превращаются в огромные расстояния, и тела взрываются
Окей, есть еще второй подход, физика мягких тел, если вы еще не забыли. Мне лично он нравится даже больше чем физика твердых тел, потому что он просто прикольнее и в какой-то мере изящнее

где он применяется? Для всяких тканей или деформируемых тел. Например beamng - это физика магких тел. Выглядит круто и сложно, но на самом деле это все ппц просто до усрачки
Немного поподробнее про сами частицы. В версии Томаса используется интеграция Верле. В таком подходе у частиц нет скорости, вместо этого хранится предыдущая позиция на прошлом кадре. Следующая позиция на новом кадре рассчитывается как текущая позиция + дельта от предыдущей. Предыдущая перезаписывается. Таким образом каждая вершина представлена 2мя векторами - текущая и предыдущая позиции.

Здесь солвер обновляет текущие позиции вершин, что обновляет дельту от предыдущей позиции, и фактически влияет на скорость и направление движения. В сумме вся эта система работает так, что тела движутся довольно правдоподобно. Но, честно говоря, это нечестные расчеты, они не соответствуют реальности.

Трение и взаимопроникновения решаются просто сдвигом точки в нужном направлении. К сожалению, это дает порой плохие результаты, при сильном проникновении тела могут вылетать друг из друга с невероятно большой скоростью. В отличие от физики твердного тела это не так плохо выглядит, тк большая скорость "проникшей" точки уравновешивается другими точками, которые находятся снаружи
Что ж, подход удивительно похож на солвер их физики тведых тел - решаем систему связей не один раз, а раз 6-8 за кадр. Однако, тут все гораздо проще. Томас представил систему связей как систему бесконечно упругих пружин. И пришел к выводу, что с такими пружинами, две частицы мгновенно приходят в стабильное состояние и расстояние между ними равно целевому. Повторить 8 раз. Все.

Это безумно простая идея, безумно простой код, дающие огромнй потенциал для реализации всяких штук
здесь чуть сложнее проверка столкновений, тк сложно описать гибкое тело негибкой формой. Для кубов еще можно использовать примитивы, но для тряпок уже не получится. Здесь отлично ложится подход разбиения на сферы. Обычно можно каждую точку описать сферой и проверка столкновений выглядит весьма правдоподобной
Физика твердого тела - наиболее распространенный вид физики в играх. Это всякие ящики, машины, регдоллы и др. Можно делать разрушаемые или гибкие тела с помощью джоинтов. Пример с besiege выше - это физика твердого тела. Просто тел много, они маленькие и соединены множеством джоинтов. Джоинты могут разрушаться при достижении определенного порога импульса
итак, отличие от физики твердых тел в том, что физическое тело не описыается единой матрицей трансформации и оболочкой. Тело описывается набором точек и связей между ними. Например квадрат - это 4 точки и 2 треугольника. Каждая точка ведет себя как отдельное упрощенное твердое тело - у нее есть позиция, скорость, масса. Но нет вращения. Точки соединены связами, или балками (beam -> beam.ng). Цель связей проста - сохранять расстояние между точками. Ну или вести себя как пружина, или как эластичная связь - сокращаться под действием силы

В общем, все тела они похожи на такие балочные конструкции, а-ля металлические мосты из треугольнико или эйфелева башня. Три точки, соединенные связями в треугольник образуют твердое тело - треугольник нельзя сжать или деформировать при абсолютно упругих связях. Из треугольников можно делать жесткие формы, например квадрат - это 2 треугольник. А так же и деформируемые - тряпки, джоинты и тп.

в остальном это очень похоже на физику твердых тел:
- интегрируем положения точек
- ищем столкновения
- решаем ограничения связей
- применяем силы
- повторяем
Вот и разобрали оба подхода в физике. В реальности эти оба подхода комбинируются в одном движке, частицы мягких тел могут влиять на твердые тела посредством импульсов и наоборот. В итоге мы может делать весьма интересные комбинации
Но над всеми этими подходами есть одна существенная проблема - это точность расчетов. Да, методы позволили значительно ускорить и уточнить рассчеты. Однако, для сложной физики этого недостаточно. Сложно сделать маленькие и сложные механизмы. Нельзя, например, сделать полностью физичный автомат со всеми механизмами внутри. Ну, по правде говоря, наверное для одного автомата на сцене можно посчитать, со значительными упрощениями и уменьшением шага времени. По сути шаг времени уточняет физику, однако пропорционально отъедает ресурсы.

тем не менее даже для физики авто в продвинутых моделях используются отдельные системы, которые гораздо точнее в рассчетах. В том же beam.ng при больших скоростях или крупных планах видно что физика дрожит, металл становится более резиновым, а поведение авто на дороге отличается от реального
На мой взгляд здесь может быть следующий виток развития физики в играх. Ведь кроме игровой физики существует и более точная физика, применяемая для рассчета целых зданий или для симуляций поведения материалов и тд. Там используются похожие методы, но там не делаются допущения в угоду производительности, от чего точность выше и ей можно доверять. Однако, это в разы, сотни раз тяжелее в плане производительности

И, как в примере комбинации физики твердых и мягких тел, почему бы не скомбинировать подходы точных и "игровых" расчетов. Мощности стали выше, и даже точные методы можно достаточно быстро посчитать.

Например, взять кузов автомобиля и систему рычагов подвески, и посчитать честным подходом. А весь "навес", типа кузовных панелей сделать по упрощенному подходу, с физикой мягких тел в играх. В итоге мы получим более точную модель поведения на дороге, при этом достаточно правдоподобные поврежения
Ну и уже готовые подходы мало используются. С помощью физики мягких тел можно симулировать мускулатуру, кожу и жир, придавая лицам и телам более правдоподобное поведение. Деревья можно сделать гибкими, разрушения более реалистичными. Не обязательно всю сцену симулировать, а только то что находится в ближайшей видимости, этого хватит чтобы обмануть глаз зрителя и поднять качество картинки на кновый уровень

Моя мечта - попробовать реализовать все эти детали в игре или движке, чтобы сделать убер-песочницу, где все можно ломать-строить и экспериментировать
Теперь немного при физику самолета и авто. Начнем с самолета.

В голову сразу лезут страшные мысли об аэродинамике, потоках воздуха, форме крыла и сложных формулах... Но вот эту демку в видео я сделал в бородатые годы буквально за неделю, и принципы там довольно простые. Однако, она вполне тянет на некую ступень симуляторной, уж явно не аркадной. Самолет ускоряется и замедляется там где должен, при сильном падении скорости падает в штопор, работают все нужные способы управления.

Итак, попробуем подумать как можно реализовать физику самолета. первая мысль - сложная, пытаться эмулировать потоки воздуха. Это в целом возможно, но крайне сложно и затратно по производительности. Нужно построить трехмерный куб вокруг самолета, разбитый на много ячеек, и считать поток воздуха в каждой отдельной ячейке. Если в ячейку попадает какая-то часть самолета, учитывать ее влияние. Далее, потоки воздуха влияют на те же части (ветер дует - давит на эту часть в секторе), силы суммируются, и вауля - самолет летит.

Но это довольно сложно реализовать по всем фронтам - нужно разбираться с физикой воздуха, бить на сектора, понимать какая часть самолета в каком кубе и тд. При этом секторов будет овердофига, тк мы в 3х измерениях. Если разбить на 1000 по высоте/ширине/длине, то получается миллиард ячеек. Поди посчитай их 60 раз в секунду, для одного самолета. А их может быть несколько... В общем это зубодробильный подход и вряд ли он применим в играх (разве что во flight simulator)
Попробуем упростить и декомпозировать задачу. Разобьем самолет на куски:
- винт впереди, он тупо тянет
- фюзеляж
- крылья
- хвост
- всякие элероны, закрылки и тп, в общем всякие управляющие штуки

И попробуем применить вышеописанную симуляцию для каждой отдельной части в предрассчитанном виде. То есть, можно задать определенные графики зависимости скорости воздуха относительно каждой оси объекта (XYZ), к силе, которая оказывает влияние на эту часть. Какие это графики будут?

Возьмем за пример крыло. Попробуем представить какие это графики были бы. Еще раз - по каждой оси строим график силы от скорости воздуха вдоль оси. Ось X располагается вдоль крыла, ось Z перпендикулярно в плоскости крыла "вперед", ось Y перпендикулярна крылу и направлена вверх.
- по оси X: ветер почти не влияет на крыло, сила маленькая
- по оси Y: ветер влияет сильно, чем сильнее ветер, тем сильнее сила. Вероятно график не лениейны
- по оси Z: как и по оси X почти не влияет
Можем дополнительно сделать графики так же для момента силы, то есть закручивания. По идее крыло еще немножко подкручивает при сильном ветре по оси Z.

Таким макаром описываем графики для каждой отдельной части самолета. Вопрос - откуда брать графики? Нууу, я их нарисовал рукой точно так же как сейчас в пеинте, исходя из здравого смысла. Можно их предрассчитать используя физическую симулацию.

Далее применяем эти графики для рассчета сил, действующих на части самолета. Каждый кадр измеряем скорость каждой части самолета (крыло, например), от ее теоретического центра. По графику получаем силу по всем осям. Прикладываем ее к этому же центру.

Отдельно обсчитываем элементы управления. Можно строить графики сил для их полного открытия и умножать на коэффициент от 0 до 1 в зависимости от управления этим элементом. Ну или просто прикладывать какую-то очевидную силу с тем же коэффцициентом.

Винт, или двигатель, работает супер просто - в зависимости от "газа", прикладываем соответствующуюю силу по вектору тяги
В общем-то все, этого было достаточно сделать для более-менее правдоподобной физики самолета. К сожалению, демка пропала, видео осталось короткое, а делать с нуля не интересно уже
Замечательную херь в С++ выявил. Связана она с порядком инклюдов и частичной специализацией шаблонных классов. Ну как херь, это поведение by design, но я считаю лютая херь!

В общем, в чем суть. Я сейчас перевожу движок с сырых указателей на умные, и под это дело пишу свои указатели. Зачем - отдельный вопрос, вкратце ответ как всегда - для бОльшего контроля. По-сути умный указатель - это шаблонный класс: Ref<T>; (да, еще и ссылками обозвал, ну да не суть).

Есть в движке особенность - это ссылки на другие акторы, компоненты и ассеты. Они, по сути, те же смарт поинтеры, но немного сложнее: они умеют сериализоваться, скриптоваться и другие полезные штуки. Меня посетила кристально чистая идея - "а чего бы не специализовать смарт-поинтеры под определенные типы, добавив им нужной функциональности??"

Получается довольно красиво: любая ссылка в движке - это Ref<T>, который ведет себя немного по-разному, в зависимости от того чем Т является. Однако, это факап...

Да да, тупая фича С++, из-за которой он компилится миллоны лет, то что придумали тогда когда я еще не жил на свете - это инклюды. Честно говоря такой херни я не видел ни в одном языке... Однако, это дерьмо мамонтов живет и в наши дни (прям как cmake, хех), несмотря на то что это просто НИКОМУ не нужно.

В чем проблема, брат? В том что твои сорцы могут менять свое поведение в зависимости от того, что попало в инклюд. Например, у вас используется в коде класс Asshole. Но он может быть объявлен в двух разных хидерах. И поведение кода изменится в зависимости от того, какой инклюд попадется. То есть нет какого-то глобального неймспейса, куда попадают все имена классов и неймспейсов. Есть вот такая вот система подключения глобального пространства имен.

Черт, я понимаю почему так получилось очень давно. Но почему это до сих пор существует - не понимаю. Точнее, я знаю, это из-за обратной совместимости. Но, камон, какая версия из стандарта была по-настоящему обратно совместимой? Любой, кто мигрировал проекты на новый стандарт знает, что это банально не правда.

К тому же, для всех очевидно, что объявлять одни и те же имена в разных инклюдах - это идиотизм, кривые руки и убожество. Но нет, мы находимся там где находимся.

Естественно такого я не делаю. Нуу, напрямую - нет. Однако, как и написал в начале, проблема в частичной специализации есть. И вот в чем штука: в хидере в классе может быть включен заголовочник просто смарт-поинтера, а в .срр уже его частичная специализация.

Получается лютая дичь: структура класса генерится с одной специализацией шаблона, а код функций - с другой. Та-ба-думс... Да, и если их размеры разные, то получишь по щщам абсолютно лютым методом - рандомным крешем по порче памяти... где-то там. При включении санитайзера видишь, что ты выходишь за рамки объекта еще в конструкторе, падая вообще в каком-то рандомном месте

А суть в том, что у тебя структура класса с первоначальной специализацией занимает Х байт, с определенным расположением переменных внутри. А в конструктор, скомпиленный с другой специализацией уже занимает не Х и с другим расположением по памяти. Далее, при выполнении, это рандомно падает.

По-моему это дичь. Во-первых, все эти неявные действия - зло. Ибо ведет к путанице. Во-вторых, кому сегодня нужны эти инклюды? Почему не парсить все хидеры сразу и компилить с общим глобальным пространством имен?
Написал статью на хабр: https://habr.com/ru/articles/811369/

Не был уверен, закатит или нет. Получился некий вид профессионального суицида - позариться на святое в сообществе С++. Пожалуй, это самое токсичное сообщество. Любая ошибка или малейшее незнание стандарта немедленно карается. Уровень снобизма просто запределен. И если ты не такой же головастый сноб, то скорее всего тебе напихают членов полную жопу.

Что, собственно и произошло - у статьи 16 минусов, куча гневных комментариев внутри по типу "афтар тупой" и "не разобрался а критикуешь". Да, есть косяки, довольно большие. Конечно же, все обращают внимания на них, молча обходя другие вроде бы правильные доводы. Кстати, это хороший лайвхак если тебе нужно не совсем честным путем кого-то в чем-то переубедить: указывай только на ошибки оппонента, обходи моменты где он прав.

Однако плюсов оказалось больше, что все таки значит что тема уродства С++ больная для сообществе. Забавно что плюсов оказалось больше, чем у других моих статей про физику или систему UI в o2
Закончил рефакторинг, длившийся пол года. Это перевод движка с сырых указателей на умные. Казалось бы это 5 минут туда и обратно, но оказалось что лишь казалось
В целом все довольно скучно и рутинно, но особенность в том что я написал свои собственные умные указатели. В процессе разобрался в особенностях умных указателей, попробовал разные подходы, о чем и хотел бы поведать.

Для начала, почему я стал писать свои собственные? Тут пара причин.

Во-первых я хочу прикрутить отладочный сборщик мусора, который можно будет включить дефайном и проанализировать утечки и расход памяти в игре. То есть это по сути не сборщик мусора, а просто дополнительный инструмент анализа памяти, работающий на принципах сборщика мусора: составляем дерево используемой памяти, какие объекты кем владеют, затем ищем висячие и зацикленные объекты, которые утекли. Таким образом можно делать дамп памяти и изучать на что ушла память и где утекает.

Во-вторых, это отличный способ тонко разобраться во всех аспектах умных указателей, попутно реализовав свои хотелки, которых не хватает в стандартной реализации. Об этом будет далее

Итак, начнем с концепции. Я взял концепцию сильных и слабых ссылок (shared/weak_ptr), с внешним счетчиком, но в одном блоке памяти с объектом. Сильные и слабые ссылки позволяют относительно удобно управлять владением объекта, это довольно понятная и распространенная концепция.

Чтобы ей эффективно пользоваться, достаточно принять за правило, что сильную ссылку на объект держит только его владелец, и в идеале он один. Все остальные держат только слабую ссылку. Например, в древовидной иерархии родитель-дети, владельцами детей является родитель, соответственно он держит сильные ссылки на детей. Дети же, как правило хотят знать о своем родителе, но не могут им владеть, иначе получится зацикленная ссылка. Поэтому они имеют лишь слабую ссылку на родителя. Так же сильную ссылку может держать объект или часть алгоритма, в которой ссылаемый объект должен быть гарантированно живой. Допустим, мы его апдейтим, и нам нужно чтобы во время апдейта он не уничтожился, иначе в самом алгоритме будет обращение к удаленному объекту.

Эту концепцию можно сравнить, например, с концепцией уникальных указателей или чего-то наподобие Rust. Да, они сильнее защищают от утечек, цикличных ссылок и т.п. Однако это сильно усложняет код и для геймдева не очень подходит.

Так же я решил немного изменить нейминг и сделать название короче, назвав сильную ссылку Ref<>, а слабую WeakRef<>. По ссылкам можно найти сорцы

Далее, моя большая архитектурная ошибка. Дело в том, что у меня в движке, наподобие Unity3D, компоненты на сцене могут хранить ссылки на другие компоненты или акторы. Эти ссылки - по сути указатели, но с дополнительным функционалом. Например, у нас есть прототип объекта (или префаб), и в нем есть ссылки внутри, то при копировании прототипа ссылки должны иметь такую же структуру, но уже в новом объекте. А так же нужно уметь записывать эти ссылки в файл и восстанавливать при загрузке.

У меня сходу возникла идея использовать специализацию для ссылок на Actor/Component/Asset, с добавлением нужного функционала. То есть для обычных объектов используется дефолтный шаблонный класс Ref<>, а для вышеперечисленных специальные частичные специализации, то есть другие Ref<>-классы, с особым функционалом. Таким образом в синтаксисе всегда используется единая простая концепция с Ref<>/WeakRef<>.. А под капотом уже разруливается нужное поведение.

Немного о частичной специализации, как ее делать. Допустим, у вас есть класс шаблонный MyClass<T>, и вы хотите чтобы если T - это наследник от MyBase, то MyClass<T> вел себя как-то иначе. Для этого создается дополнительный "сервисный" параметр шаблона, который в оригинальном классе по-умолчанию равен void:
template<typename T, typename E = void> MyClass {};