В новой статье, предлагаемой сегодня вашему вниманию, Роман представляет проект, цель которого достаточно ясна из названия статьи. При этом, как и в публикации 2015 года, автор демонстрирует весьма ценную склонность и способность к обоснованию своих решений, что придаёт его статьям дополнительно полезный обзорно-справочный характер.
Что такое облачная система 3D проектирования? Поскольку в последнее время термин «облачные вычисления» очень популярен и используется к месту и не к месту, я начну с определения. Облачное 3D проектирование в моем понимании и моей реализации – это такая архитектура программного обеспечения, при которой все данные о 3D модели и действия по её обработке расположены на удаленных серверах (т.е. в облаках), а клиентские устройства запрашивают ту или иную часть данных или результатов расчетов по сети интернет. Другими словами, подобные системы отличаются от классических систем проектирования тем, что производят большинство расчетных операциях на серверах, а не на клиентских устройствах и передают только небольшую часть данных для визуализации модели и ее параметров клиенту. Архитектура подобных систем оказывается разделённой на тесно взаимодействующие, но удаленно расположенные, серверную и клиентскую части, что требует особого подхода для обеспечения их взаимодействия незаметно для пользователя продукта.
Следующий вопрос: какие преимущества имеет подобная архитектура? Несомненно, облачная архитектура сложнее классической, в которой пользователь, его данные и их обработка находятся и производятся в одном месте. Тем не менее, для моего проекта облачная архитектура имеет ряд неоспоримых преимуществ как с точки зрения разработки, так и с точки зрения использования, делая подобное усложнение архитектуры приложения целесообразным. Попробую их сформулировать:
- Клиентским приложением является веб-браузер. В наше время это означает кроссплатформенность приложения и возможность пользоваться сервисом с любого устройства.
- Быстрый старт приложения без установки снижает порог входа для будущих пользователей.
- Нет необходимости сохранять документы и перемещать их между устройствами, поскольку все данные одновременно доступны со всех устройств.
- Возможность одновременного редактирования или просмотра отдельных документов и целых проектов несколькими пользователями и удобная коммуникация создают единое рабочее пространство между удаленно расположенными клиентами.
- Организация непрерывного процесса разработки и мгновенной доставки обновлений, что позволяет клиентам использовать последнюю версию ПО и способствует активному использованию техник экстремального программирования при разработке.
- Необходимость иметь хороший интернет-канал для комфортной работы с приложением.
- Усложнение программного обеспечения и, как следствие, увеличение времени и стоимости разработки.
- Необходимость развертывания и последующей поддержки сетевой инфраструктуры, необходимой для работы ПО.
Архитектура проекта
При выборе архитектуры я старался учесть возможность масштабирования в будущем и разделил проект на части таким образом, чтобы было легко распараллелить самые нагруженные части. При расчете я использовал следующие предположения, основанные на имеющемся опыте по разработке CAD и уточненные при создании прототипа:- Для загрузки среднестатистической квартиры с мебелью мне потребуется примерно 25 Мб несжатых геометрических данных и дополнительных атрибутов (5 Мб сжатых) + 10Мб текстур. Время генерации данных от 0.2 сек. до 5 сек. (в самых сложных случаях). Я планирую ограничить объем модели на уровне 3-5 млн треугольников.
- Во время работы по проектированию плана и расстановке различных изделий пользователем, на одну операцию (вставка и редактирование изделий, регенерация плана) приходится в среднем 100 - 500 Кб исходящего трафика. Время выполнения каждой операции на сервере в среднем составляет 0,1-0.5 сек.
- Активность пользователей находится на уровне открытия одной модели в минуту или выполнения 5 - 10 операций редактирования в минуту.
В выборе средств разработки я изначально был связан несколькими ограничениями. Во-первых, использование в качестве геометрического ядра C3D и высокие требования по скорости геометрических расчетов при работе с моделью предопределило использование С++ на стороне сервера. Во-вторых, запуск клиентской части на браузере также сузило выбор языков до тех, что поддерживают компиляцию в JavaScript.
В результате на данном этапе проект состоит из четырех независимых частей: два внутренних (backend) сервиса и два внешних Web приложения. Главный внутренний сервис отвечает за геометрическое моделирование и расчеты. Он написан на С++ с использованием библиотек C3D и Qt Core. Вспомогательный сервис отвечает за управлением файлами, каталогами пользователей и обработкой текстур. Он написан на ASP.NET Core. Веб приложения разделены аналогично. Одно отвечает непосредственно за моделирование и написано на TypeScript + WebGL, а второе предоставляет интерфейс пользователя для управлением проектами и каталогами пользователя и написано на связке Angular 2 + TypeScript. Взаимодействие между клиентской и серверной частями идёт с помощью простых HTTP запросов. В части, отвечающей за интерактивное моделирование помещений, используются WebSocket соединения, по которым передаются сжатые бинарные данные. Во избежание дублирования кода между серверными сервисами они также обмениваются необходимой информацией по HTTP протоколу.
Серверная часть
«Семь часов утра – разгон облаков, установление хорошей погоды...» («Тот самый Мюнхгаузен»).
Также, как и Мюнхгаузену, для отзывчивой работы облачного сервиса необходимо разогнать облака по максимуму! Поэтому главная изюминка проекта – это комбинация серверной и клиентской части, отвечающая за геометрическое моделирование, визуализацию и сохранение истории редактирования моделей. К этой части проекта предъявляется высокие требования по производительности, потреблению оперативной памяти, распараллеливанию и масштабируемости, т.к. геометрическое моделирование само по себе достаточно затратная вычислительная задача, а выполнение запросов построения моделей от множества активных пользователей усложняет её еще больше. В качестве «сердца» системы для выполнения задач геометрического моделирования на сервере было выбрано ядро C3D от компании C3D Labs, причины выбора которого описаны в моей предыдущей статье «Ядерные технологии в CAD». Для реализации функционала по управлению комплексными 3D проектами была разработана собственная система хранения данных 3D модели, в основу которой взята иерархическая ECS (Entity Component System), популяризированная разработчиками игр. Она представляет собой древовидную структуру модели, состоящую из разных элементов(сущностей), где у каждого элемента есть разные наборы данных (компоненты), такие как геометрические параметры, BREP оболочки, треугольные сетки, пользовательские данные и т.п.
Для того чтобы система отвечала необходимым требованиям, её реализация имеет целый ряд отличительных особенностей:
- При загрузке модели загружается лишь её структура, а все её данные (компоненты) хранятся в NoSQL базе данных и автоматически подгружаются в оперативную память при обращениям к компонентам, а также автоматически выгружаются из памяти по мере необходимости. Это позволяет работать на сервере с тысячами одновременно открытых моделей при небольших затратах оперативной памяти.
- Внутри компонентов хранятся связи на компоненты в других сущностях в виде пары «ID сущности - ТИП компонента». При операциях копирования элементов модели все элементы получают новые ID из старого ID и случайного кода операции с помощью симметричной хеш-функции, поэтому в сущностях вместо подмены всех измененных ID внутри компонентов запоминается код преобразования старых ID в новые. Это позволяет копировать данные компонентов простым и быстрым побайтным копированием, не теряя ссылочной целостности в структуре модели. В результате можно копировать огромные модели без чтения структуры их компонентов, что, в свою, очередь обеспечивает мгновенное копирование больших сборок внутри проектируемого помещения.
- Благодаря предыдущему механизму реализован специальный компонент-транзакция, в котором автоматически сохраняется история изменения структуры модели во время того, как различные команды редактируют её содержимое. Разделение сущности на относительно небольшие компоненты позволило поставить «слушатель» на обращение к каждому компоненту, и отслеживать, тем самым, его изменение автоматически. Это позволяет хранить всю историю изменений модели и возвращаться к любому моменту её создания, даже сделанному много месяцев назад (т.к. вся история хранится также в компонентах, которые без необходимости не загружаются в оперативную память). С точки зрения разработчика это означает наличие у геометрической модели аналога транзакций, аналогичных существующим в СУБД.
- Версионность каждого элемента и компонента модели обеспечивает быстрое формирование специальных файлов-патчей, в которых содержится информация о том, какие сущности и компоненты нужно скорректировать на клиентской модели, чтобы синхронизировать её с версией на сервере. Вкупе с использованием бинарной версии протокола WebSocket это обеспечивает эффективную синхронизацию данных модели на всех подключенных клиентах в реальном времени.
Клиентская часть
Проектирование клиентской части началось с выбора движка для визуализации. Были перепробованы почти все популярные WebGL движки, однако остановиться ни на одном из них не удалось по следующим причинам:- 1. Слабая поддержка CAD режимов визуализации, таких как удаление невидимых линий и обрисовка силуэтов криволинейных поверхностей.
- Слабое развитие инструментов для качественного вывода текста небольших размеров в 3D режиме в сочетании с рисованием линий произвольной толщины (эта комбинация нужна для прорисовки планов на 3D модели)
- Отсутствие эффективной техники batching. Модели зачастую состоят из десятков тысяч небольших элементов с разными материалами, и пользователь может изменить любой объект в любой момент времени, поэтому необходимы эффективные техники по динамическому склеиванию маленьких объектов в большие вершинные буферы в видеопамяти для достижения приемлемого уровня производительности.
- Необходимость написания специфичных компонентов управления камерой, наложения материалов и анимации, т.к. предлагаемые «коробочные» варианты не подходят под нужды системы проектирования.
Следующим вопросом был выбор языка программирования и платформы в целом. У меня уже был опыт разработки JavaScript приложения размером порядка 10 тысяч строк. Исходя из этого опыта, идея разработки на JavaScript нечто большего лично мне внушала благоговейный ужас. Очередной релиз TypeScript и тот факт, что за ним стоит Андерс Хейлсберг, предопределило выбор языка. Выбор платформы для Web пал на Angular 2 ( который теперь уже 4): мне и так предстояло собрать проект из немалого количества разношерстных библиотек, а собирать свой комбайн для Web-приложения не было ни малейшего желания. Хотелось иметь именно framework, в котором «все включено». Развитые возможности отложенной загрузки модулей системы, эффективная кодогенерация (AOT) и возможности интернационализации только укрепили мой выбор. Единственное, что меня все еще смущает на данный момент — это отсутствие локализации сообщений в исходных файлах, но я искренно надеюсь, что уж к четвертой версии они реализуют эту функциональность .
Реализация замыслов
Проект начался с реализации на С++ прототипа структуры будущей модели и экспериментальной визуализации на OpenGL. После нескольких месяцев отладки я занялся переводом приложения на клиент-серверную модель. Первоначально я сделал это следующим образом: написал совмещенный REST+WebSocket сервер на C# и подключил геометрический сервис, как динамическую библиотеку с C-интерфейсом для работы с моделями, которая будет вызываться для геометрических запросов. Крайнее неудобство отладки такого гибридного приложения и ненужные накладки при копировании данных из C++ в С, а затем и в C# вынудили меня искать альтернативные решения. В конце концов я включил WebSocket сервер внутрь С++ части и маршрутизировал все запросы к нему через прокси-сервер. При этом для аутентификации клиентов геометрический сервис делает внутренние запросы к основному REST-сервису.Следующим этапом стала реализация алгоритма синхронизации модели, изменяемой на сервере, с моделью, отображаемой на клиенте. Первоначальные идеи слежения сервером за состоянием клиента, либо отправки клиентом своего текущего состояния на сервер перед синхронизацией пришлось отмести, как не очень надежные и сложные в реализации. Остановился я на следующей реализации: в каждом компоненте хранится целочисленная версия компонента. Таким образом, версия модели в целом определяется максимальной версией среди всех её компонентов и компонентов дочерних сущностей. При синхронизации клиент отправляет на сервер запрос, содержащий версию его модели, в ответ на который сервер отправляет данные всех компонентов, версия которых старше клиентской версии. Это обеспечило синхронизацию древовидной модели между клиентами и сервером с минимально возможным трафиком (запрос синхронизации - одно число, а в ответе содержатся лишь измененные компоненты).
После написания прототипов клиентской и серверной частей я занялся поиском оптимального формата данных для передачи геометрической модели между клиентом и сервером. В этом формате я хотел иметь следующие возможности:
- Удобство записи и чтения формата как из C++, так и из TypeScript кода
- Поддержка схемы данных
- Минимально возможное время упаковки и распаковки данных
- Передача чисел с плавающей точкой без потери точности
- Эффективность работы при среднем размере пакета данных в диапазоне 100Кб - 10Мб
- Версионность формата для возможности поэтапного обновления разных частей системы
После устаканивания формата данных и первых экспериментов над каждой частью системы стало приблизительно понятно, как будет функционировать сервис в целом. Помимо этого были оценены узкие места и сделаны наброски вариантов масштабирования в будущем. Затем были созданы первые варианты программы, показывающие работу связки «действие пользователя — запрос к серверу моделирования - визуализация действия пользователя». На этом этапе наступило первое разочарование: такая схема работы обеспечивает обновление данных в стиле «потянул за маркер, отпустил мышку, объект перестроился». Это было слишком медленно для интерактивного отображения действий пользователя при перемещении курсора.
Данный результат потребовал переосмыслить границу между серверной и клиентской частью и сделать клиент более обширным, а также продублировать функционал между сервером и клиентом таким образом, чтобы клиент проводил предварительные расчеты для интерактивной полигональной визуализации, а сервер производил финальные действия над BREP моделью и синхронизировал всех клиентов между собой. Это заставило меня глубоко задуматься о проекте WebAssembly, который теоретически позволил бы иметь единую кодовую базу для клиентской и серверной части и оперативно управлять выполнением расчетов между клиентом и сервером, перераспределяя нагрузку по мере необходимости. Но пока это всё мечты...
Следующим этапом стала реализация полноценного WebGL рендера. Пока его возможности достаточно скромные, однако и над ними пришлось изрядно попотеть.
Перечислю основные момента реализации:
- На текущем этапе для отрисовки использую классический Forward-rendering и несколько проходов. На перспективу планирую реализовать затенение на основе Screen Space Ambient Occlusion или Scalable Ambient Obscurance.
- Для достижения приемлемой производительности склеиваю мелкие объекты в большие буферы вершин в глобальной системе координат и отправляю в видеокарту. При изменении объектов все необходимые буферы опять пересчитываются на процессоре. Это может выглядеть диковато, но отправлять матрицу объекта в дополнительных атрибутах еще накладнее.
- Отрисовка в 3D линий толщиной отличной от 1 пикселя крайне в WebGL нетривиальна. Реализовал её через отрисовку двух треугольников, все вершины которых лежат на одной линии, а толщина хранится в атрибутах - тангенсных векторах. Конечные вершины треугольников рассчитываются в вершинном шейдере путем перевода точек в систему координат экрана (для учета соотношения ширины и высоты экрана), прибавки необходимой толщины линий и перевода в нормализованные координаты. Сглаживание линий реализуется через альфа-канал во фрагментном шейдере.
- Отрисовка текста сделана через технику SDF, опубликованной компанией Valve. Для подготовки шрифтов использовалась утилита Hiero от libgdx. Полученный мной результат - удовлетворительный: при размере шрифта 14-16 пикселов текст выглядит неплохо, если же размер меньше, и текст расположен под острым углом к плоскости экрана, то он практически нечитаемый. Возможно, я просто не умею готовить SDF, но потеряв много времени, кардинального улучшения результатов получить не удалось. В перспективе планирую попробовать эту технику: http://wdobbie.com/post/gpu-text-rendering-with-vector-textures/.