Когда на меня накатывает хандра, я бросаю всё и пилю свой игровой движок. Это неблагодарное занятие, но меня прёт.
В самом начале у меня были такие планы: вжух-вжух, щас возьму ведро, накидаю туда всяких библиотек для графики, физики и звуков, добавлю сетевую библиотеку по вкусу, перемешаю всё с какой-нибудь системой сообщений, и готово. Приключение на 15 минут.
Ладно, если быть честным, то я почти не уделял времени разработке, потому что постоянно спотыкался на всяких бесящих меня ошибках: то сериализация не работает с наследованием, то потоки не хотят нормально разделять память, то обновление языка ломало совместимость… Я могу, блин, целую Камасутру написать про соитие с игровым движком. Все эти ошибки сильно демотивируют, потому что хочется уже наконец-то заняться делом, а не ковыряться с байтиками.
Это не моё видео, но оно очень точно передаёт, как у меня происходит разработка:
С другой стороны, конечно, когда эти проблемы решаешь, чувствуешь себя богом и королём жизни, и после этого ты вроде как опять хочешь программировать. И даже кажется, что это была последняя трудность. Ха-ха, наивный!.. Но мне это нравится. Типа как альпинисты идут в гору и страдают, когда можно пойти в бар с друзьями и попить пивко. Каждому своё.
Ну и вот про одну такую ошибку я хотел поговорить. Есть такой движок — ODE (Open Dynamics Engine). Он появился где-то в палеолите, динозавры его накодили, от документации остались только царапины на скалах. Но он работает, он простой в использовании, и у него есть сишные заголовки, поэтому я мог просто написать враппер на Nim и использовать его в своём движке. В Nim вообще ни хрена нету, поэтому канонический способ — это взять какую-нибудь библиотеку из Си, научиться её вызывать, а потом говорить всем, что ты написал крутую программу на Nim.
Итак, сначала я просто добавил кубики на сцену и отрисовал их. Физический движок был в полной гармонии с графическим, и когда мне графика отрисовывала, что я приближаюсь к кубу и толкаю его, кубик действительно отлетал и вращался. Короче говоря, всё шло так, как я и планировал.
Разумеется, если вы не планируете делать Minecraft, то вам может понадобиться что-то поинтересней, чем кубик. В ODE есть специальный класс Trimesh
, который как раз позволяет вам сделать сложную геометрию. Фактически, вы можете создать любое тело из набора треугольников. Типа такого:
Машина глупая, поэтому мы не можем ей сказать "нарисуй зайца", мы можем ей сказать "вот такие есть вершины, соедини их вот так-то, и это будет называться зайцем". Я, естественно, не стал рубить с плеча и решил вместо зайца сделать простой треугольник и проверить, что всё корректно с ним работает.
Я создал треугольник, он успешно отрисовался на сцене, я начал ходить по миру… и обнаружил, что треугольник не в том месте, где нарисован, а где-то непонятно где.
Так и появилась эта дурацкая невидимая стена.
Фактически, я мог ходить по сцене, и где-то я упирался в тот самый треугольник, который вообще-то должен был быть там, где нарисован. Я в принципе даже что-то такое и ожидал, потому что, как я уже где-то писал, только три раза в своей жизни я написал код, который заработал с первого раза. Наверняка я где-то перепутал координату — вместо X передал Y, или наоборот, ну что-нибудь такое. Эти программисты, вы знаете!
Отрисовать этот треугольник я не мог, потому что координаты были правильные, графический движок всё отрисовывал правильно, но вот физический движок как-то неправильно интерпретировал мои правильные данные. Поэтому я стал ходить по миру и пытаться определить очертания этой невидимой стены. В конце концов я её нашёл (она была достаточно странной), и я решил немного подвигать треугольник, чтобы посмотреть, как он влияет на эту стену. Казалось бы, если я просто где-то перепутал координаты, то подвинув треугольник, я немножко подвину эту стену. Но хрен мне там! Стена исчезала и появлялась совершенно случайно, прыгала далеко даже от малейшего изменения координат, и я не мог понять, почему.
И тут я вспомнил эту недалёкую женщину из заЩИТников! Если кто не знает, она сделалась невидимой и решила спрятаться в дожде. Отличный план, надёжный, как швейцарские часы:
Я подумал, что это прям мой случай, и решил полить свою стену дождём, чтобы увидеть её. Дождя у меня не было, зато были кубики, поэтому я создал штук 50 и стал кидать их вниз. При касании стены они к ней прилипали, и я мог видеть её очертания. А когда что-то видишь — отлаживать в разы легче!
Что ж… Это была хорошая попытка понять, по какому закону стена появляется в том месте, где она появляется, но это мне ничего не дало. Даже видя эту стену, я не находил никакой закономерности.
Если нужно где-то найти таких же неудачников, как я, то самое лучшее место для этого — интернет. И я нашёл его — единственного человека, который отстрадал своё и рассказал об этом. Представляете, в 2006 году у какого-то чувака из Германии пятая точка горела точно так же, как у меня сейчас! Не знаю, что он выкурил (похоже, что исходники), но, ОКАЗЫВЕТСЯ, физический движок ожидает от вас трёхмерные точки, но передавать их надо как четырёхмерный вектор, просто в четвертой координате надо поставить мусор, типа так: [x1, y1, z1, 0, x2, y2, z2, 0, ...]
. Скажите, как по const dReal* Vertices
я должен понять, что там ждут в гости четырёхмерные вершины?
За что я люблю опенсорс — можно всегда докопаться до истоков всего. Я полез в исходники, и вот что обнаружил.
В 2003 году пришёл Russel Smith и добавил всю эту функциональность с trimesh collisions, в том числе интересующую меня строчку:
typedef dReal dVector3[4];
void dGeomTriMeshDataBuildSimple(
dTriMeshDataID g,
const dVector3* Vertices, // норм
int VertexCount,
const int* Indices,
int IndexCount,
);
Тут всё понятно, потому что в определении чётко говорится, что dVector3
— это четырёхмерный вектор (есть некий шарм в этой логике).
А потом через пару месяцев врывается Erwin Coumans и переписывает так, чтобы тип был непонятен:
void dGeomTriMeshDataBuildSimple(
dTriMeshDataID g,
const dReal* Vertices, // опа-па
int VertexCount,
const int* Indices,
int IndexCount,
);
И только представьте себе, через 20 лет это изменение находит какого-то чувака (меня), который пишет вообще на другом языке программирования, и заставляет этого чувака гореть в тщетных попытках понять, какого хрена не работает.
Я переписываю код с добавлением четвертой координаты, и все начинает работать.
Такие дела.
Вообще этот пост был задуман как развлекательный, типа "смотрите, погромист опять страдает, хахаха". Но мне кажется, что он поднимает достаточно глубокую проблему: как только вы выкладываете код, он начинает свой долгий путь сквозь время. Никто не знает, когда и кто его будет читать — может, вы или ваш коллега через пару месяцев, может, тысячи независимых разработчиков через пару лет, может, какой-то парень с горящим продом.
Получается такой вот эффект бабочки, как с этой невидимой стеной. Поэтому когда вы в следующий раз сядете писать код, представьте, что какой-то разраб через 20 лет будет в нём разбираться, и, пожалуйста, постарайтесь сделать жизнь этого чувака хоть чутоку легче. Ведь однажды этим кем-то можете оказаться вы сами.
Не только лишь все могут смотреть в будущее, но вы сможете, если подпишетесь на мой уютненький канал Блог Погромиста.
А ещё я держу все свои яйца в одной корзине (в смысле, все проекты у одного облачного провайдера) — Timeweb. Поэтому нагло рекламирую то, чем сам пользуюсь — вэлкам:
Комментарии (44)
insighter
17.11.2023 08:27извиняюсь за дилетантский вопрос, но почему не погромировать игры на каком-нибудь UE?
ArchieHabr
17.11.2023 08:27+6Вероятно, автору интереснее работа с трехмерной графикой, чем гейм-дизайн. "Игровой движок" в таких контекстах лично я уже понимаю как движок "интерактивной трехмерной графики"
avartprite
17.11.2023 08:27+23каждый уважающий себя человек должен посадить дерево, построить дом и написать свой движок
codeforcoffe
17.11.2023 08:27+32Потому что ТС не хочет делать игры, он хочет ковыряться в движке :)
Как тот батя с разобранной шахой.
Отличное хобби, так то )Meklon
17.11.2023 08:27+21Как 3D-принтер. Всегда разобран. Если не разобран, то печатает апгрейды для самого себя.
domix32
17.11.2023 08:27+1Там же одно из первых предложений про это - чтобы развлечься написанием собственного движка.
funca
17.11.2023 08:27+15представьте, что какой-то разраб через 20 лет будет в нём разбираться, и, пожалуйста, постарайтесь сделать жизнь этого чувака хоть чутоку легче
Мне кажется ваши статьи и через 20 лет будут читаться так же легко. Вы показываете абсолютно обыденные вещи, с которыми разработчики сталкиваются по сто раз на дню, но всегда удивительно красиво и романтично. Как если бы Бианки или Паустовский писали свои записки о природе IT.
Moog_Prodigy
17.11.2023 08:27+57Николай Дроздов : Мы видим синий экран смерти, это нормальное состояние компьютера для разработчика драйверов. Разработчики драйверов - хитрые и ловкие программисты .Давайте посмотрим, что он предпримет сейчас...
Wolf4D
17.11.2023 08:27+11О, а игровой движок 15-летнего возраста, на котором мы сейчас делаем игру, как раз использует для физики ODE. Привет "заводчикам динозавров" от коллег по увлечению :)
perfect_genius
17.11.2023 08:27+4Программирование графики всегда веселее, чем просто программирование. Т.к. картинка на экране - это уже своего рода дебаг, сразу видно если что-то не то. Если одно видимое, конечно.
MiyuHogosha
17.11.2023 08:27+3Скорее это иллюстрация что происходит с любым специалистом, возвращающимся в область своих знаний 30 лет спустя.
Сейчас 4-х мерный вектор для описания точки в пространстве какой-то связанной системы координат - сейчас это НОРМА. Такая норма, что первокурсник это знает. Это практически аппаратный стандарт на данный момент. Три координаты и 4й компонент - своего рода квалификатор: 0 - это вектор направления (если единичный) или сдвига, 1 - точка в пространстве, != 1 точка в каком-то масштабе или неоднородный масштаб.
Изменение с `Vector3d` на `dReal` скорее всего было связано с обновлениями стандарта и портом движка на что-нибудь... браузерное. Дело в том что с 98 стандарта Си++ и с 99 Си преобразовать массив структур `struct{ dReal[4] v;}` в массив dReal формально нельзя. Си или Си++ могут быть реализованы на виртуальной платформе со сборщиком мусора и тогда меду этими блоками может быть что-то еще... А вот API для 3d графики стало активно использовать именно массивы для передачи их драйверу.
SquareRootOfZero
17.11.2023 08:27+24й компонент - своего рода квалификатор: 0 - это вектор направления (если единичный) или сдвига, 1 - точка в пространстве, != 1 точка в каком-то масштабе или неоднородный масштаб
Это в какой области? Вроде, последние годы писал графику - ни разу не сталкивался, если 4-мерный вектор для описания координат - значит, координаты однородные, и только. Шейдеры и пр., правда, я не писал - определил геометрию и отдал какой-нибудь графической библиотеке, типа, нарисуй мне там как-нибудь - ну, так вроде и автор статьи примерно тем же занимается, и в качестве "квалификаторов" у него там эти четвёртые компоненты не используются.
преобразовать массив структур struct{ dReal[4] v;} в массив dReal формально нельзя
Так там же не структуры были, а от массива массивов перешли на "плоский" массив? Вот такое, что, запрещено стало с C++ 98, или undefined behavior теперь?
#include <iostream> typedef float dReal; typedef dReal dVector3[4]; int main() { dVector3 vectors[] = { {1, 2, 3}, {4, 5, 6} }; dReal *array = (dReal*)vectors; // UB?? for (int i = 0; i < 8; ++i) std::cout << array[i] << '\t'; std::cout << '\n'; return EXIT_SUCCESS; }
DartRaven
17.11.2023 08:27+1ODE вообще довольно заброшенный движок, по ощущениям. Как-то написал с его использованием машинку, которая ездила по ландшафту, потом долго удивлялся мелко дрожащим колёсам (а я их сделал "честными" цилиндрами, а не просто вектором-сенсором) и иногда ловил проблему на резком повороте, когда вся система улетала в небеса (где-то в процессе вычислений вылезал NaN, который потом разваливал всю симуляцию). В общем, физический сыроватый в лучшие годы и околозаброшенный к текущему времени.
kesn Автор
17.11.2023 08:27Вы использовали double precision и фиксированный шаг симуляции? Если да, и при этом всё равно словили глюки, то это печально
DartRaven
17.11.2023 08:27+2Да, по обоим пунктам. Мелкое дрожание ещё худо-бедно полечилось настройкой перехода объекта в неподвижное состояние (~засыпание при отсутствии внешних воздействий, не помню сейчас, как именно это называется в ODE), а вот где вылезал NaN дебажить уже лень было.
StreamThread
17.11.2023 08:27"мелкое дрожание" - сразу вызывает флешбэки по бюджетным играм нулевых, для которых небольшие группы разработчиков собственные движки писали.
ImagineTables
17.11.2023 08:27Кидайте в меня чем-нибудь тяжёлым, но проблемы бы не было, если бы параметр назвали
paQuadVertices
(pointer to array of quad vertices).Конечно, так компилятор ничего не проверит, но любая попытка решить проблему через типы или алиасы в системах с прямым доступом к памяти только сделает хуже (что мы и видели).
slonopotamus
17.11.2023 08:27Написано хорошо. Но вообще вы тут оба молодцы. У одного vector3 имеет 4 координаты, а другой видя
const dReal* Vertices
решил методом тыка угадывать формат, вместо того чтобы посмотреть в документацию/код.kesn Автор
17.11.2023 08:27+1Ну как бы да, мануал наше всё, но я ещё примеры от других людей смотрю, туториалы там всякие почитываю. И везде эта ошибка встречается.
Вот, например, неправильная подсказка, потому что автор забыл про то, что это 4D вектор:
кому интересно формат вот такой
Vertices[count]={x1,y1,z1,x2,y2,z2,x3,y3,z3................................}Или вот чел наступил на те же грабли, что и я:
Ok, so I decided to use the test_trimesh trimesh to make sure everything was working ok.
So everything seems to work fine, but I get an error in my collide callback
I get a fat access violation.
Urrrrrrrrrrrrrgh this triangle collider is really making me mad ><float vertices[vCount * 3] = { 0,0,0, 20*30,0,0, 20*30,0,20*30, 0,0,20*30 };
vladvul
это наверное те самые кватернионы? А кубики почему работали нормально с тремя координатами?
kesn Автор
Разрабы говорят, что четыре координаты нужны для "better alignment", наверно для работы с кватернионами там им удобнее, когда сразу 4x1, чтобы память не перекладывать.
Кубики работали, потому что у них интерфейс без этих выкрутасов с векторами, просто задаются размеры куба, и готово:
proc createBox*(space: dSpaceID; lx: dReal; ly: dReal; lz: dReal): dGeomID
. Ошибиться сложно, даже для меня :)JerryI
Возможно так проще множить на четырехмерные матрицы (чтобы операцию трансляции сделать линейной из аффинной)
kozlov_de
Наверняка
slupoke
Изначально я подумал, что движок ожидает однородные координаты, которые часто используются в компьютерной графике, но фраза:
Меня смутила, так как четвертая компонента имеет важный смысл в однородной системе координат
SquareRootOfZero
Вот тоже подумал, что с "мусором", конечно, странный момент, но совершенно не ожидать однородных координат из четырёх чисел как-то странно - можно быть поверхностно знакомым с трёхмерной графикой и не понимать, нахера они нужны, но если пилить движки на основе чужих движков, то точно с ними столкнёшься, рано или поздно. Возможно, в описанном случае четвёртая координата вообще движком игнорируется (т. е., как бы принимается за 1, несмотря на "мусор" в массиве координат), но, по счастливой случайности, в используемых автором операциях ни на что не влияет?
Сишный код, конечно, оформлен херово - не говорю даже про
dReal* Vertices
, но с какого перепугуdVector3[4]
- при том что, зачастую, в библиотеках используются и трёх-, и четырёхмерные вектора, сколько помню, обычно их отдельно определяют:dVector3
для "простых" координат,dVector4
для однородных.Но, вообще, страдания автора понятны: тоже, бывало, надо перемножить туда-сюда десяток матриц трансформации, в вычислении какого-нибудь одного коэффициента чуть-чуть ошибёшься, а на конечном результате это отражается совсем не "чуть-чуть", трёхмерный объект улетает куда-нибудь в бесконечность, что на экране не найдёшь, и, как говорится, happy debugging!
kesn Автор
Я не поленился:
http://ode.org/wiki/index.php/Manual#Why_is_dVector3_the_same_size_as_dVector4.3F_Why_is_a_dMatrix_not_3x3.3F
JerryI
ну да, теперь укладывается в теорию. SIMD + то же самое при перемножении на 4x4 матрицу аналогично применяется. Красиво закинул сразу вектор в регистр без дополнительных операций
niko_zvt
Как уже было отмечено ранее это связано с понятием "однородных координат". В общем случае модификации (изменение положения и формы геометрических объектов в пространстве) векторов и точек требуется выполнять простейшие операции: сдвиг (трансляция), поворот, симметрия, масштабирование и т.п. Модификаторы поворота, симметрии и масштабирования описываются формулами, имеющими одинаковый вид:
p = q + A * (p0 ‑ q) = A * p0 + t, где
p — результирующая точка,
A — матрица аффинных преобразований,
t = q - A * q — преобразованный вектор сдвига,
q — радиус-вектор точки, определяющей плоскость симметрии или оси-вращения.
Так сдвиг (трансляцию, перемещение) можно описать единичной матрицей A и вектором t.
Но хотелось бы обобщить эту формулу на любые модификации и это можно сделать. Для этого нужно увеличить размерность векторов и матриц на единицу. Вектор теперь будет называться расширенным вектором. И его компоненты теперь будут называться однородными координатами P = {px, py, pz, 1}. Индекс матрицы A_t будет говорит, что она расширенная - та же матрица А, окаймленная снизу нулями, а справа стоящий вектор сдвига t.
Поэтому общая формула примет вид P = A_t * P0.
При этом можно ввести вес w и строить рациональные кривые
R = {R1, R2, R3, R4} = {w * rx, w * ry, w * rz, w * 1}. Вычисления теперь будут проводиться в однородных координатах, что уменьшит их сложность, а декартовы координаты будут определены на последнем этапе как ri = Ri / R4, где i = 1, 2, 3.
Итогом всего будет являться тот факт, что для точек имеющих вес, сдвиг, поворот, симметрия, масштабирование и другие модификаторы опишутся одной единственной общей формулой. Также это можно применить в операциях в 2D пространстве, при этом формула остается прежней. Именно поэтому использование однородных координат оказывается полезным там, где дополнительная компонента точки отсутствует или наоборот требуется.
zzzzzzerg
This problem may be overcome by use of a fourth gimbal, actively driven by a motor so as to maintain a large angle between roll and yaw gimbal axes. Another solution is to rotate one or more of the gimbals to an arbitrary position when gimbal lock is detected and thus reset the device.
Modern practice is to avoid the use of gimbals entirely. In the context of inertial navigation systems, that can be done by mounting the inertial sensors directly to the body of the vehicle (this is called a strapdown system)[3] and integrating sensed rotation and acceleration digitally using quaternion methods to derive vehicle orientation and velocity. Another way to replace gimbals is to use fluid bearings or a flotation chamber.
Gimbal lock - Wikipedia
SadOcean
Много причин, если кратко, очень многие типы операций требуют четырехмерных векторов, потому что матрица 3д вида 4-х мерная.
Ну и да, для кватернионов можно использовать