Через тернии к облакам: создание облачного сервиса для 3D проектирования и дизайна помещений на базе ядра C3D и WebGL

в 4:52, , рубрики: C3DKernel, cad, CAD/CAM, WebGL, Анализ и проектирование систем, облака, Разработка веб-сайтов

Нынче в интернетах только и говорят об облаках, как они бесконечны и прекрасны… о серверах, которые они там видели… А ты? Вот и я решил поделиться с читателями своим опытом разработки онлайн сервиса проектирования помещений и интерьеров в 3D. Здесь я постараюсь рассказать об архитектуре проекта в целом и о деталях реализации.

image

Что такое облачная система 3D проектирования? Поскольку в последнее время термин “облачные вычисления” очень популярен и используется к месту и не к месту, я начну с определения. Облачное 3D проектирование в моем понимании и моей реализации — это такая архитектура программного обеспечения, при которой все данные о 3D модели и действия по её обработке расположены на удаленных серверах (т.е. в облаках), а клиентские устройства запрашивают ту или иную часть данных или результатов расчетов по сети интернет. Другими словами, подобные системы отличаются от классических систем проектирования тем, что производят большинство расчетных операциях на серверах, а не на клиентских устройствах и передают только небольшую часть данных для визуализации модели и ее параметров клиенту. Архитектура подобных систем оказывается разделённой на тесно взаимодействующие, но удаленно расположенные, серверную и клиентскую части, что требует особого подхода для обеспечения их взаимодействия незаметно для пользователя продукта.

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

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

Разумеется, подобный подход имеет и свои недостатки. Из них я выделяю следующие:

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

Архитектура проекта

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

Для загрузки среднестатистической квартиры с мебелью мне потребуется примерно 25 Мб несжатых геометрических данных и дополнительных атрибутов (5 Мб сжатых) + 10Мб текстур. Время генерации данных от 0.2 сек. до 5 сек. (в самых сложных случаях). Я планирую ограничить объем модели на уровне 3-5 млн треугольников.

Во время работы по проектированию плана и расстановке различных изделий пользователем, на одну операцию (вставка и редактирование изделий, регенерация плана) приходится в среднем 100 — 500 Кб исходящего трафика. Время выполнения каждой операции на сервере в среднем составляет 0,1-0.5 сек.
Активность пользователей находится на уровне открытия одной модели в минуту или выполнения 5 — 10 операций редактирования в минуту.

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

В выборе средств разработки я изначально был связан несколькими ограничениями. Во-первых, использование в качестве геометрического ядра C3D и высокие требования по скорости геометрических расчетов при работе с моделью предопределило использование С++ на стороне сервера. Во-вторых, запуск клиентской части на браузере также сузило выбор языков до тех, что поддерживают компиляцию в JavaScript.

В результате на данном этапе проект состоит из четырех независимых частей: два внутренних (backend) сервиса и два внешних Web приложения. Главный внутренний сервис отвечает за геометрическое моделирование и расчеты. Он написан на С++ с использованием библиотек C3D и Qt Core. Вспомогательный сервис отвечает за управлением файлами, каталогами пользователей и обработкой текстур. Он написан на ASP.NET Core. Веб приложения разделены аналогично. Одно отвечает непосредственно за моделирование и написано на TypeScript + WebGL, а второе предоставляет интерфейс пользователя для управлением проектами и каталогами пользователя и написано на связке Angular 2 + TypeScript. Взаимодействие между клиентской и серверной частями идёт с помощью простых HTTP запросов. В части, отвечающей за интерактивное моделирование помещений, используются WebSocket соединения, по которым передаются сжатые бинарные данные. Во избежание дублирования кода между серверными сервисами они также обмениваются необходимой информацией по HTTP протоколу.

Серверная часть

Главная изюминка проекта — это комбинация серверной и клиентской части, отвечающая за геометрическое моделирование, визуализацию и сохранение истории редактирования моделей. К этой части проекта предъявляется высокие требования по производительности, потреблению оперативной памяти, распараллеливанию и масштабируемости, т.к. геометрическое моделирование само по себе достаточно затратная вычислительная задача, а выполнение запросов построения моделей от множества активных пользователей усложняет её еще больше. В качестве “сердца” системы для выполнения задач геометрического моделирования на сервере было выбрано ядро C3D от компании C3D Labs, причины выбора которого описаны в моей предыдущей статье “Ядерные технологии в CAD” [1]. Для реализации функционала по управлению комплексными 3D проектами была разработана собственная система хранения данных 3D модели, в основу которой взята иерархическая ECS (Entity Component System), популяризированная разработчиками игр. Она представляет собой древовидную структуру модели, состоящую из разных элементов(сущностей), где у каждого элемента есть разные наборы данных (компоненты), такие как геометрические параметры, BREP оболочки, треугольные сетки, пользовательские данные и т.п. Для того чтобы система отвечала необходимым требованиям, её реализация имеет целый ряд отличительных особенностей:

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

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

Благодаря предыдущему механизму реализован специальный компонент-транзакция, в котором автоматически сохраняется история изменения структуры модели во время того, как различные команды редактируют её содержимое. Разделение сущности на относительно небольшие компоненты позволило поставить “слушатель” на обращение к каждому компоненту, и отслеживать, тем самым, его изменение автоматически. Это позволяет хранить всю историю изменений модели и возвращаться к любому моменту её создания, даже сделанному много месяцев назад (т.к. вся история хранится также в компонентах, которые без необходимости не загружаются в оперативную память). С точки зрения разработчика это означает наличие у геометрической модели аналога транзакций, аналогичных существующим в СУБД.

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

На практике подобная система работает достаточно быстро, обеспечивая время обработки большинства запросов к модели в пределах 5-50 мс. Однако у системы образовалось узкое место: открытие модели требует передачи всех данных для её визуализации, что в случае массивных моделей требует множества обращений к БД для извлечения данных компонентов, приводя к значительным задержкам, достигающим нескольких секунд на моделях из десятков тысяч элементов. Побороть эту проблему позволило кэширование файлов-патчей в Redis. Поскольку патчи не теряют своей актуальности (патчи с версии 0 до версии сто + патч с версии 100 до версии 200 эквиваленты единственному патчу с версии 0 до версии 200), это легко решает проблему инвалидации кэша: его можно обновлять в фоновом режиме, не заботясь о потери актуальности данных.

Клиентская часть

Проектирование клиентской части началось с выбора движка для визуализации. Были перепробованы почти все популярные WebGL движки, однако остановиться ни на одном из них не удалось по следующим причинам:

  • Слабая поддержка CAD режимов визуализации, таких как удаление невидимых линий и обрисовка силуэтов криволинейных поверхностей.
  • Слабое развитие инструментов для качественного вывода текста небольших размеров в 3D режиме в сочетании с рисованием линий произвольной толщины (эта комбинация нужна для прорисовки планов на 3D модели)
  • Отсутствие эффективной техники batching. Модели зачастую состоят из десятков тысяч небольших элементов с разными материалами, и пользователь может изменить любой объект в любой момент времени, поэтому необходимы эффективные техники по динамическому склеиванию маленьких объектов в большие вершинные буферы в видеопамяти для достижения приемлемого уровня производительности.
  • Необходимость написания специфичных компонентов управления камерой, наложения материалов и анимации, т.к. предлагаемые “коробочные” варианты не подходят под нужды системы проектирования.

В результате анализа пришло понимание того, что написать свой “велосипед” будет и быстрее, и лучше, и значительно легче в поддержке. За за основу была выбрана великолепная библиотека https://twgljs.org

Вот примеры отрисовки плана помещения и 3D вида на WebGL:

image

image

Следующим вопросом был выбор языка программирования и платформы в целом. У меня уже был опыт разработки JavaScript приложения размером порядка 10 тысяч строк. Исходя из этого опыта, идея разработки на JavaScript нечто большего лично мне внушала благоговейный ужас. Очередной релиз TypeScript и тот факт, что за ним стоит Андерс Хейлсберг, предопределило выбор языка. Выбор платформы для Web пал на Angular 2 ( который теперь уже 4): мне и так предстояло собрать проект из немалого количества разношерстных библиотек, а собирать свой комбайн для Web-приложения не было ни малейшего желания. Хотелось иметь именно framework, в котором “все включено”. Развитые возможности отложенной загрузки модулей системы, эффективная кодогенерация (AOT) и возможности интернационализации только укрепили мой выбор. Единственное, что меня все еще смущает на данный момент — это отсутствие локализации сообщений в исходных файлах, но я искренно надеюсь, что уж к четвертой версии они реализуют эту функциональность))

Реализация замыслов

Проект начался с реализации на С++ прототипа структуры будущей модели и экспериментальной визуализации на OpenGL. После нескольких месяцев отладки я занялся переводом приложения на клиент-серверную модель. Первоначально я сделал это следующим образом: написал совмещенный REST+WebSocket сервер на C# и подключил геометрический сервис, как динамическую библиотеку с C-интерфейсом для работы с моделями, которая будет вызываться для геометрических запросов. Крайнее неудобство отладки такого гибридного приложения и ненужные накладки при копировании данных из C++ в С, а затем и в C# вынудили меня искать альтернативные решения. В конце концов я включил WebSocket сервер внутрь С++ части и маршрутизировал все запросы к нему через прокси-сервер. При этом для аутентификации клиентов геометрический сервис делает внутренние запросы к основному REST-сервису.

Следующим этапом стала реализация алгоритма синхронизации модели, изменяемой на сервере, с моделью, отображаемой на клиенте. Первоначальные идеи слежения сервером за состоянием клиента, либо отправки клиентом своего текущего состояния на сервер перед синхронизацией пришлось отмести, как не очень надежные и сложные в реализации. Остановился я на следующей реализации: в каждом компоненте хранится целочисленная версия компонента. Таким образом, версия модели в целом определяется максимальной версией среди всех её компонентов и компонентов дочерних сущностей. При синхронизации клиент отправляет на сервер запрос, содержащий версию его модели, в ответ на который сервер отправляет данные всех компонентов, версия которых старше клиентской версии. Это обеспечило синхронизацию древовидной модели между клиентами и сервером с минимально возможным трафиком (запрос синхронизации — одно число, а в ответе содержатся лишь измененные компоненты).

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

  • Удобство записи и чтения формата как из C++, так и из TypeScript кода
  • Поддержка схемы данных
  • Минимально возможное время упаковки и распаковки данных
  • Передача чисел с плавающей точкой без потери точности
  • Эффективность работы при среднем размере пакета данных в диапазоне 100Кб — 10Мб
  • Версионность формата для удобства развития в будущем и возможности поэтапного обновления разных частей системы

Использованный в экспериментах JSON пришлось отмести сразу, а дальше был выбор между MessagePack, Google Protocol Buffers, Apache Thrift, BSON и аналогичных им библиотек. Мой выбор остановился на Google Protocol Buffers по причине лучшей производительности, хорошего сжатия и удобного кодогенератора. Важным фактором также стала распространенность библиотеки и надежда, что она не будет заброшена в долгосрочной перспективе. В результате я использую родной protobuf на С++, protobufjs — для чтения и записи на клиенте, proto2typescript — для использования единой схемы между C++ и TypeScript. Кроме того, данные дополнительно сжимаются zlib при передачи через WebSockets. Это схема позволила очень комфортно и быстро передавать все необходимые данные по модели.

PS: не так давно наткнулся на библиотеку FlatBuffers от того же разработчика, и у меня появилась мысль, что этот вариант будет еще лучше, однако времени попробовать эту библиотеку совершенно не хватает, кроме того, поддержка TypeScript пока отсутствует в основной ветке.

После устаканивания формата данных и первых экспериментов над каждой частью системы стало приблизительно понятно, как будет функционировать сервис в целом. Помимо этого были оценены узкие места и сделаны наброски вариантов масштабирования в будущем. Затем были созданы первые варианты программы, показывающие работу связки “действие пользователя — запрос к серверу моделирования — визуализация действия пользователя”. На этом этапе наступило первое разочарование: такая схема работы обеспечивает обновление данных в стиле “потянул за маркер, отпустил мышку, объект перестроился”. Это было слишком медленно для интерактивного отображения действий пользователя при перемещении курсора.

Данный результат потребовал переосмыслить границу между серверной и клиентской частью и сделать клиент более обширным, а также продублировать функционал между сервером и клиентом таким образом, чтобы клиент проводил предварительные расчеты для интерактивной полигональной визуализации, а сервер производил финальные действия над BREP моделью и синхронизировал всех клиентов между собой. Это заставило меня глубоко задуматься о проекте WebAssembly, который теоретически позволил бы иметь единую кодовую базу для клиентской и серверной части и оперативно управлять выполнением расчетов между клиентом и сервером, перераспределяя нагрузку по мере необходимости. Но пока это всё мечты…

Следующим этапом стала реализация полноценного WebGL рендера. Пока его возможности достаточно скромные, однако и над ними пришлось изрядно попотеть. Перечислю основные момента реализации:

На текущем этапе для отрисовки использую классический Forward-rendering и несколько проходов. На перспективу планирую реализовать затенение на основе Screen Space Ambient Occlusion или Scalable Ambient Obscurance.

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

Отрисовка в 3D линий толщиной отличной от 1 пикселя крайне в WebGL нетривиальна. Реализовал её через отрисовку двух треугольников, все вершины которых лежат на одной линии, а толщина хранится в атрибутах — тангенсных векторах. Конечные вершины треугольников рассчитываются в вершинном шейдере путем перевода точек в систему координат экрана (для учета соотношения ширины и высоты экрана), прибавки необходимой толщины линий и перевода в нормализованные координаты. Сглаживание линий реализуется через альфа-канал во фрагментном шейдере.
Отрисовка текста сделана через технику SDF, опубликованной компанией Valve. Для подготовки шрифтов использовалась утилита Hiero от libgdx. Полученный мной результат — удовлетворительный: при размере шрифта 14-16 пикселов текст выглядит неплохо, если же размер меньше, и текст расположен под острым углом к плоскости экрана, то он практически нечитаемый. Возможно, я просто не умею готовить SDF, но потеряв много времени, кардинального улучшения результатов получить не удалось. В перспективе планирую попробовать эту технику.

Хочу также отметить, что поддержка WebGL в современных браузерах отличная по сравнению с OpenGL под Windows. Это, видимо, благодаря проекту Angle, эмулирующему вызовы WebGL через DirectX. Код без костылей отлично работает даже под IE 11. С другой стороны, в приложении, оперирующим большими объемами данных со сложными структурами, остро стоит проблема утечек памяти, с которыми бороться очень непросто.

Эпилог

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

Автор: x512

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js