- PVSM.RU - https://www.pvsm.ru -
Вот, как он выглядит:

Майк Вазовски!
Я ненавижу тормознутость компилятора Typescript (поверьте, это относится к теме статьи). Джем показался мне подходящей возможностью реализовать более быстрое подмножество Typescript, обгоняющее по скорости tsc. Мне показалось, что проект можно реализовать, если начать с парсера Typescript esbuild [4] or Bun [5]. Но потом ко мне пришло понимание, что успешный результат будет выглядеть как команда терминала, выполняющая работу быстрее другой. Не особо впечатляюще в качестве демо. Мне хотелось создать крутое демо, поэтому я выбрал 3D.
Единственная причина того, что реализация 3D-проекта с нуля за неделю показалась мне осуществимой, заключается в технике под названием ray marched signed distance fields [6] (SDF). Сцену ray marched SDF с цветами, плавными тенями и рассеянным затенением (ambient occlusion) можно реализовать гораздо быстрее, чем эквивалентный рендерер на основе треугольников. Удивительный Иниго Килез [7] использует SDF для быстрого создания персонажей в стиле Pixar [8]. Раньше я писал SDF-шейдеры, но они были рудиментарными. Моделирование при помощи редактирования кода казалось мне неестественным, хотелось редактировать модели мышью. Я посчитал, что джем — подходящая возможность для превращения этой задумки в реальность.

Визуализированные знаковые поля расстояний (Signed Distance Field) редактора ShapeUp
Я написал ShapeUp на C и воспользовался raylib [9] для создания окна OpenGL. Raylib оказалась одной из тех библиотек, которые позволяют быстро начать, но в длительной перспективе замедляют работу. Подробнее об этом ниже.
Некоторые считают C крайне простым и сырым языком, всё время разработки на котором приходится тратить на борьбу с отсутствием в нём встроенных структур данных и на устранение багов указателей. Истина в том, что в простоте C заключается его сила. Он быстро компилируются. Его синтаксис не скрывает сложные операции. Он достаточно прост, чтобы не приходилось искать по нему информацию. Его можно легко компилировать и нативно, и при помощи web assembly. Хотя C имеет свои особенности, за 22 года работы с ним я научился их избегать.
Мой «повседневный» проект представляет собой 177 тысяч строк на C и Objective-C. По сравнению с ним, ShapeUp — это очень простой в работе маленький единый файл C. Но даже несмотря на это, мне кажется, что стоит рассказать о том, как он использует данные. Модели состоят из фигур (Shape):
typedef struct {
Vector3 pos;
Vector3 size;
Vector3 angle;
float corner_radius;
float blob_amount;
struct {
uint8_t r,g,b;
} color;
struct {
bool x,y,z;
} mirror;
bool subtract;
} Shape;
Shape хранятся в статически выделенном массиве:
#define MAX_SHAPE_COUNT 100
Shape shapes[MAX_SHAPE_COUNT];
int shape_count;
int selected_shape = -1;
Благодаря этому отсутствует возможность ошибок распределения и утечек памяти, нет ничего лишнего. Прелестно. Ограничение в 100 фигур на практике никак нас не ограничивает. У меня было мало времени на оптимизацию рендерера, поэтому частота кадров упадёт ещё до того, как вы доберётесь до 100 фигур. Если бы у меня было время, я бы разбил модель на маленькие кирпичики [10], а затем выполнял бы raymarching в пределах каждого кирпича.
Что касается динамической памяти, то ShapeUp вызывает malloc только в трёх местах:
Во всех трёх случаях в конце функции есть единственная простая free. Повторюсь, всё это тривиально, я упоминаю это только как доказательство того, что работа с памятью в C может быть тривиальной. Разумеется, можно усложнить себе жизнь, выполняя malloc каждого Shape по отдельности и храня указатели в динамическом массиве — работа с языками наподобие Java, Javascript и Python вынуждает использовать такую структуру распределения. Я ценю то, что C обеспечивает мне контроль за структурой памяти.
UI реализован как immediate mode user interface [11] (IMGUI). Мне нравится такой подход к UI. Его очень легко отлаживать, и можно использовать для позиционирования элементов настоящий язык программирования (в отличие от CSS, constraints или SwiftUI). Как и в большинстве IMGUI, я использовал enum для отслеживания того, какой элемент имеет фокус, или того, какое действие выполняет мышь:
typedef enum {
CONTROL_NONE,
CONTROL_POS_X,
CONTROL_POS_Y,
CONTROL_POS_Z,
CONTROL_SCALE_X,
CONTROL_SCALE_Y,
CONTROL_SCALE_Z,
CONTROL_ANGLE_X,
CONTROL_ANGLE_Y,
CONTROL_ANGLE_Z,
CONTROL_COLOR_R,
CONTROL_COLOR_G,
CONTROL_COLOR_B,
CONTROL_TRANSLATE,
CONTROL_ROTATE,
CONTROL_SCALE,
CONTROL_CORNER_RADIUS,
CONTROL_ROTATE_CAMERA,
CONTROL_BLOB_AMOUNT,
} Control;
Control focused_control;
Control mouse_action;
Этому проекту не нужны динамические массивы или hashmap, но если бы были нужны, я бы использовал что-то типа stb_ds.h [12].
Я не пожалел о выборе C, но проблемой оказалась raylib. Во-первых, в ней приняты странные архитектурные решения, вредящие удобству работы:
int везде, где следовало бы использовать тип enum. Это не позволяет компилятору выполнять проверку типов и мешает самодокументированию функций. Возьмём для примера эту строку в заголовке raylib:
// Проверяем, обнаружен ли жест
RLAPI bool IsGestureDetected(unsigned int gesture);
Похоже, что gesture может быть ID жеста. Однако изучив исходники raylib, можно понять, что параметр gesture на самом деле является enum Gesture! И такое встречается повсеместно. Единственная документация raylib — это файл заголовка, так что приходится заходить в реализацию, чтобы проверить, является ли параметр int типом enum, и если да, то каким именно enum.
unsigned char *LoadFileData(const char *fileName, int *dataSize);
Заголовок raylib не даёт понять, что dataSize — это выходной параметр или что он не должен быть равен null. Это решение об отсутствии валидации влияет на множество функций и усложняет выявление тривиальных проблем. Если повезёт, это приводит к segfault где-то в полезном месте (но не выводит ошибку в лог). Если не повезёт, то библиотека просто незаметно делает что-нибудь странное.
UI-библиотека raygui очень несовершенна:
Есть и просто баги:
DrawCircle(...) не используют общие вершины у треугольников. Из-за этого в связи с погрешностями округления чисел с плавающей запятой возникают пиксельные дыры, когда текущая матрица имеет масштабирование или поворот.Какое-то время я сообщал о найденных проблемах, но почти все они были закрыты с формулировкой «исправляться не будет». Это раздражало и демотивировало, а на написание баг-репортов тратилось время, так что я просто перестал.
Итак, raylib создала мне окно OpenGL, но я дорого заплатил за это удобство. К счастью, я обычно всегда находил обходной путь: или использовал непосредственно функции OpenGL, или реализовывал фичу с нуля. В будущем я попробую работать с sokol [14].
Если рассматривать ShapeUp на высоком уровне, он сводится к четырём основным частям, которые мне нужно было написать за шесть дней:
Каждый пункт по отдельности реализовать не так трудно. Трудно было правильно расставить приоритеты и не отклониться в сторону. В решении сложных или времязатратных задач мне помогли обходные способы или использование тупого решения, работавшего в 90% случаев. Иногда помогало оставить фичу на день, чтобы подсознание подсказало мне решение.
Я старался работать так, чтобы у меня всегда был работающий 3D-редактор, и постепенно совершенствовал его настолько, насколько позволяло время. Я подходил к этому как к строительству пирамиды. Если строить её слой за слоем, то к самому концу у тебя не будет пирамиды. Или же можно строить её так, чтобы на каждом из этапов у тебя была готовая пирамида.

К концу недели у меня была 3D-программа, способная создавать осмысленные 3D-модели [18] и экспортировать их в файл .obj. Кроме того, она работает на множестве платформ, и в ней можно открывать/сохранять файлы.

Гаечный ключ, смоделированный в ShapeUp
Проект состоит из 2024 строк C и 250 строк GLSL. Удивительно, что достаточно функциональный 3D-редактор можно написать в примерно 2300 строках.
Других участников джема впечатлил ShapeUp, но я не ощущаю, что достиг чего-то серьёзного, это относительно простой проект. Если уж в нём и есть что-то особенное, так это мой выбор того, что нужно сделать, мои знания, необходимые для этого, и дисциплинированность, чтобы уложиться в неделю.
Можете запустить ShapeUp в браузере [3], только помните, что это было сделано всего за неделю.
Исходный код выложен на Github [19].
Автор: ru_vds
Источник [20]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/3d-modelirovanie/392993
Ссылки в тексте:
[1] Wheel Reinvention Jam: https://handmade.network/jam/2023
[2] видео-демо ShapeUp: https://youtu.be/-Xb3Kk3HhIw
[3] ShapeUp в браузере: https://danielchasehooper.com/projects/shapeup
[4] esbuild: https://esbuild.github.io
[5] Bun: https://bun.sh
[6] ray marched signed distance fields: https://youtu.be/-Xb3Kk3HhIw?si=g95rRsHRZlJYznN8&t=58
[7] Иниго Килез: https://twitter.com/iquilezles
[8] персонажей в стиле Pixar: https://www.youtube.com/watch?v=8--5LwHRhjk
[9] raylib: https://www.raylib.com
[10] разбил модель на маленькие кирпичики: https://youtu.be/u9KNtnCZDMI?si=8QTGMnqVV8TnEPf9&t=937
[11] immediate mode user interface: https://www.youtube.com/watch?v=Z1qyvQsjK5Y
[12] stb_ds.h: https://github.com/nothings/stb/blob/master/stb_ds.h
[13] сделано преднамеренно: https://github.com/raysan5/raylib/issues/3365#issuecomment-1743827668
[14] sokol: https://github.com/floooh/sokol
[15] объяснено в видео: https://youtu.be/-Xb3Kk3HhIw?si=GvNTl31sHP0L2yey&t=58
[16] объяснено в видео: https://youtu.be/-Xb3Kk3HhIw?si=epq9opaS74rgcJSL&t=149
[17] объяснено в видео: https://youtu.be/-Xb3Kk3HhIw?si=UWTZE9hZKTW7yRbY&t=172
[18] осмысленные 3D-модели: https://twitter.com/DanielcHooper/status/1708637378733133841
[19] Github: https://github.com/danielchasehooper/ShapeUp-public
[20] Источник: https://habr.com/ru/companies/ruvds/articles/832168/?utm_source=habrahabr&utm_medium=rss&utm_campaign=832168
Нажмите здесь для печати.