Когда на меня накатывает хандра, я бросаю всё и пилю свой игровой движок. Это неблагодарное занятие, но меня прёт.


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


И вот я тут спустя 5 лет.


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


Это не моё видео, но оно очень точно передаёт, как у меня происходит разработка:



С другой стороны, конечно, когда эти проблемы решаешь, чувствуешь себя богом и королём жизни, и после этого ты вроде как опять хочешь программировать. И даже кажется, что это была последняя трудность. Ха-ха, наивный!.. Но мне это нравится. Типа как альпинисты идут в гору и страдают, когда можно пойти в бар с друзьями и попить пивко. Каждому своё.


Ну и вот про одну такую ошибку я хотел поговорить. Есть такой движок — 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)


  1. vladvul
    17.11.2023 08:27
    +4

    это наверное те самые кватернионы? А кубики почему работали нормально с тремя координатами?


    1. kesn Автор
      17.11.2023 08:27
      +3

      Разрабы говорят, что четыре координаты нужны для "better alignment", наверно для работы с кватернионами там им удобнее, когда сразу 4x1, чтобы память не перекладывать.

      Кубики работали, потому что у них интерфейс без этих выкрутасов с векторами, просто задаются размеры куба, и готово: proc createBox*(space: dSpaceID; lx: dReal; ly: dReal; lz: dReal): dGeomID. Ошибиться сложно, даже для меня :)


      1. JerryI
        17.11.2023 08:27
        +3

        Возможно так проще множить на четырехмерные матрицы (чтобы операцию трансляции сделать линейной из аффинной)


        1. kozlov_de
          17.11.2023 08:27

          Наверняка


      1. slupoke
        17.11.2023 08:27
        +5

        Изначально я подумал, что движок ожидает однородные координаты, которые часто используются в компьютерной графике, но фраза:

        просто в четвертой координате надо поставить мусор

        Меня смутила, так как четвертая компонента имеет важный смысл в однородной системе координат


        1. SquareRootOfZero
          17.11.2023 08:27
          +2

          Вот тоже подумал, что с "мусором", конечно, странный момент, но совершенно не ожидать однородных координат из четырёх чисел как-то странно - можно быть поверхностно знакомым с трёхмерной графикой и не понимать, нахера они нужны, но если пилить движки на основе чужих движков, то точно с ними столкнёшься, рано или поздно. Возможно, в описанном случае четвёртая координата вообще движком игнорируется (т. е., как бы принимается за 1, несмотря на "мусор" в массиве координат), но, по счастливой случайности, в используемых автором операциях ни на что не влияет?

          Сишный код, конечно, оформлен херово - не говорю даже про dReal* Vertices, но с какого перепугу dVector3[4] - при том что, зачастую, в библиотеках используются и трёх-, и четырёхмерные вектора, сколько помню, обычно их отдельно определяют: dVector3 для "простых" координат, dVector4 для однородных.

          Но, вообще, страдания автора понятны: тоже, бывало, надо перемножить туда-сюда десяток матриц трансформации, в вычислении какого-нибудь одного коэффициента чуть-чуть ошибёшься, а на конечном результате это отражается совсем не "чуть-чуть", трёхмерный объект улетает куда-нибудь в бесконечность, что на экране не найдёшь, и, как говорится, happy debugging!


          1. kesn Автор
            17.11.2023 08:27
            +8

            Я не поленился:

            Why is dVector3 the same size as dVector4? Why is a dMatrix not 3x3?
            For making it SIMD friendly. i.e 16 byte aligned (there is no SIMD code in there right now, but there may be in the future).

            http://ode.org/wiki/index.php/Manual#Why_is_dVector3_the_same_size_as_dVector4.3F_Why_is_a_dMatrix_not_3x3.3F


            1. JerryI
              17.11.2023 08:27

              ну да, теперь укладывается в теорию. SIMD + то же самое при перемножении на 4x4 матрицу аналогично применяется. Красиво закинул сразу вектор в регистр без дополнительных операций


            1. niko_zvt
              17.11.2023 08:27

              Как уже было отмечено ранее это связано с понятием "однородных координат". В общем случае модификации (изменение положения и формы геометрических объектов в пространстве) векторов и точек требуется выполнять простейшие операции: сдвиг (трансляция), поворот, симметрия, масштабирование и т.п. Модификаторы поворота, симметрии и масштабирования описываются формулами, имеющими одинаковый вид:
              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 пространстве, при этом формула остается прежней. Именно поэтому использование однородных координат оказывается полезным там, где дополнительная компонента точки отсутствует или наоборот требуется.


      1. zzzzzzerg
        17.11.2023 08:27

        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


    1. SadOcean
      17.11.2023 08:27

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


  1. insighter
    17.11.2023 08:27

    извиняюсь за дилетантский вопрос, но почему не погромировать игры на каком-нибудь UE?


    1. ArchieHabr
      17.11.2023 08:27
      +6

      Вероятно, автору интереснее работа с трехмерной графикой, чем гейм-дизайн. "Игровой движок" в таких контекстах лично я уже понимаю как движок "интерактивной трехмерной графики"


      1. Ratenti
        17.11.2023 08:27

        Что значит интерактивная 3д?


        1. DWM
          17.11.2023 08:27
          +1

          Грубо говоря, реагирующая на действия пользователя(например, нажатия клавиш = модель двигается влево/вправо).


    1. avartprite
      17.11.2023 08:27
      +23

      каждый уважающий себя человек должен посадить дерево, построить дом и написать свой движок


    1. codeforcoffe
      17.11.2023 08:27
      +32

      Потому что ТС не хочет делать игры, он хочет ковыряться в движке :)

      Как тот батя с разобранной шахой.
      Отличное хобби, так то )


      1. yarkov
        17.11.2023 08:27
        +13

        В многих случаях батя в гараж к шохе уходит чтобы просто из дома свалить ))


        1. MixaSg
          17.11.2023 08:27
          +1

          Это теперь называется «родительское собрание» :-( у нас, таких крутых-молодых в 200х выросли собственные дети и, зачастую, коренным образом сменились хобби


      1. Ratenti
        17.11.2023 08:27
        +2

        Он ещё пишет на ноунейм языке - NIM


        1. kesn Автор
          17.11.2023 08:27
          +1

          Ну прям совсем ноунейм не добавили бы в репозитории убунты и, в особенности, дебиана :) Но да, там маленькое коммьюнити. Питон, знаете, тоже когда-то был ноунейм)



        1. inv2004
          17.11.2023 08:27
          +1

          Как это ноунейм - все знают

          Даже вот:

          https://github.com/jinyus/related_post_gen/


          1. Ratenti
            17.11.2023 08:27

            ноунейм репозиторий


      1. Meklon
        17.11.2023 08:27
        +21

        Как 3D-принтер. Всегда разобран. Если не разобран, то печатает апгрейды для самого себя.


    1. domix32
      17.11.2023 08:27
      +1

      Там же одно из первых предложений про это - чтобы развлечься написанием собственного движка.


    1. orefkov
      17.11.2023 08:27
      +8

      Я конечно дилетант в скалолазании, но почему бы не подняться на гору на вертолете?


      1. catana
        17.11.2023 08:27

        Пока вы заведете вертолет, четкий скалолаз успеет пожарить шашлыки на вершине =)


  1. funca
    17.11.2023 08:27
    +15

    представьте, что какой-то разраб через 20 лет будет в нём разбираться, и, пожалуйста, постарайтесь сделать жизнь этого чувака хоть чутоку легче

    Мне кажется ваши статьи и через 20 лет будут читаться так же легко. Вы показываете абсолютно обыденные вещи, с которыми разработчики сталкиваются по сто раз на дню, но всегда удивительно красиво и романтично. Как если бы Бианки или Паустовский писали свои записки о природе IT.


    1. Moog_Prodigy
      17.11.2023 08:27
      +57

      Николай Дроздов : Мы видим синий экран смерти, это нормальное состояние компьютера для разработчика драйверов. Разработчики драйверов - хитрые и ловкие программисты .Давайте посмотрим, что он предпримет сейчас...


  1. Wolf4D
    17.11.2023 08:27
    +11

    О, а игровой движок 15-летнего возраста, на котором мы сейчас делаем игру, как раз использует для физики ODE. Привет "заводчикам динозавров" от коллег по увлечению :)


  1. Ratenti
    17.11.2023 08:27
    -10

    Почему unreal engine 5 не возьмёшь просто?


  1. perfect_genius
    17.11.2023 08:27
    +4

    Программирование графики всегда веселее, чем просто программирование. Т.к. картинка на экране - это уже своего рода дебаг, сразу видно если что-то не то. Если одно видимое, конечно.


  1. NDA15102019
    17.11.2023 08:27

    Нормальные сроки ожидания для стены!


  1. MiyuHogosha
    17.11.2023 08:27
    +3

    Скорее это иллюстрация что происходит с любым специалистом, возвращающимся в область своих знаний 30 лет спустя.

    Сейчас 4-х мерный вектор для описания точки в пространстве какой-то связанной системы координат - сейчас это НОРМА. Такая норма, что первокурсник это знает. Это практически аппаратный стандарт на данный момент. Три координаты и 4й компонент - своего рода квалификатор: 0 - это вектор направления (если единичный) или сдвига, 1 - точка в пространстве, != 1 точка в каком-то масштабе или неоднородный масштаб.

    Изменение с `Vector3d` на `dReal` скорее всего было связано с обновлениями стандарта и портом движка на что-нибудь... браузерное. Дело в том что с 98 стандарта Си++ и с 99 Си преобразовать массив структур `struct{ dReal[4] v;}` в массив dReal формально нельзя. Си или Си++ могут быть реализованы на виртуальной платформе со сборщиком мусора и тогда меду этими блоками может быть что-то еще... А вот API для 3d графики стало активно использовать именно массивы для передачи их драйверу.


    1. SquareRootOfZero
      17.11.2023 08:27
      +2

      4й компонент - своего рода квалификатор: 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;
      }


  1. DartRaven
    17.11.2023 08:27
    +1

    ODE вообще довольно заброшенный движок, по ощущениям. Как-то написал с его использованием машинку, которая ездила по ландшафту, потом долго удивлялся мелко дрожащим колёсам (а я их сделал "честными" цилиндрами, а не просто вектором-сенсором) и иногда ловил проблему на резком повороте, когда вся система улетала в небеса (где-то в процессе вычислений вылезал NaN, который потом разваливал всю симуляцию). В общем, физический сыроватый в лучшие годы и околозаброшенный к текущему времени.


    1. kesn Автор
      17.11.2023 08:27

      Вы использовали double precision и фиксированный шаг симуляции? Если да, и при этом всё равно словили глюки, то это печально


      1. DartRaven
        17.11.2023 08:27
        +2

        Да, по обоим пунктам. Мелкое дрожание ещё худо-бедно полечилось настройкой перехода объекта в неподвижное состояние (~засыпание при отсутствии внешних воздействий, не помню сейчас, как именно это называется в ODE), а вот где вылезал NaN дебажить уже лень было.


    1. StreamThread
      17.11.2023 08:27

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


  1. BobArctor
    17.11.2023 08:27

    dVector3 считали от ноля.


  1. ImagineTables
    17.11.2023 08:27

    Кидайте в меня чем-нибудь тяжёлым, но проблемы бы не было, если бы параметр назвали paQuadVertices (pointer to array of quad vertices).

    Конечно, так компилятор ничего не проверит, но любая попытка решить проблему через типы или алиасы в системах с прямым доступом к памяти только сделает хуже (что мы и видели).


  1. slonopotamus
    17.11.2023 08:27

    Написано хорошо. Но вообще вы тут оба молодцы. У одного vector3 имеет 4 координаты, а другой видя const dReal* Vertices решил методом тыка угадывать формат, вместо того чтобы посмотреть в документацию/код.


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