02.11.2023 • C3D Web Vision

Технологии создания графических веб-приложений на примере C3D Web Vision

Последние три года я занимаюсь разработкой компонента C3D Web Vision. Это модульное клиент-серверное решение для визуализации 3D в браузере, которое легко интегрируется с любым веб-приложением. Я поделюсь нашим опытом разработки и инструментами, которые мы используем при разработке C3D Web Vision. Предлагаю рассмотреть графический API, используемый в браузере, сборку проекта C++ под веб и работу с микросервисом.

Первые шаги

C3D Labs начала разработку веб-визуализации три год назад. У нас уже был опыт создания десктопных продуктов. Перед стартом разработки мы посмотрели на наше приложение C3D Viewer и подумали, как его перенести в веб-среду. Выделили основные модули — модуль десктопной визуализации C3D Vision, модуль математики C3D Modeler, модуль конвертора C3D Converter, бизнес-логику, написанную на C++, и графический интерфейс на Qt. Компоненты C3D Modeler и C3D Converter перенесли на сторону сервера, модули без проблем работают на backend без наших дополнительных доработок. Business logic решили реализовать на TypeScript, чтобы связать ее с интерфейсом. Для реализации интерфейсной части мы взяли фреймворк Vue. В качестве библиотеки визуализации мы решили оставить собственное решение — C3D Vision, собрать в Web Assembly, это была самая сложная часть разработки.

TypeScript

В браузере выполняется JavaScript, но на самом деле большинство программистов не хочет работать с JavaScript. Причиной этому является то, что это сложный язык, динамический, не типизированный, в нем постоянно можно «наступать на грабли». В основном разработка ведется на TypeScript. Этот язык типизирован, есть возможность реализовывать объекты классов. И мы не стали исключением, вся frontend-часть проекта у нас написана на TypeScript. На рисунке 1 слева вы видите написание класса на TypeScript, а справа это результат сборки TypeScript в JavaScript.

Технологии создания графических веб-приложений на примере C3D Web Vision, фото 1

Согласитесь, TypeScript выглядит приятнее, а на этапе отладки разобраться в JavaScript будет уже тяжеловато.

У TypeScript и JavaScript есть модули. В основном программисты, когда разрабатывают программный продукт, разбивают его на отдельные части, куски — логически выделяют файлы, подпапки и модули. Так же можно сделать и в TypeScript. Единственная проблема в том, что JavaScript работает не только в браузере, но и может функционировать на backend под управлением node. Поэтому разновидностей модулей для JavaScript очень много — commonjs, umd, amd. Пример того, как выглядят эти модули, изображен на рисунке 2.

Технологии создания графических веб-приложений на примере C3D Web Vision, фото 2

Слева изображен пример, написанный на TypeScript, а справа вы видите umd-модуль, с которым работать не очень удобно. Причем к правому модулю нужно подключать зависимую стороннюю библиотеку, чтобы все заработало в браузере.

Modules: commonjs, umd, amd

Разработчики могут разрабатывать собственные модули, как мы и сделали с модулями визуализации, и делиться с другими разработчиками. В состав модуля обычно входит package.json, сам JavaScript и описание TypeScript. С помощью такого инструмента, как npm, можно подключать внешние модули в свой проект и публиковать их для других пользователей. На рисунке 3 можно увидеть примерное подключение npm-модуля.

Технологии создания графических веб-приложений на примере C3D Web Vision, фото 3

Посередине рисунка изображен package.json, внизу располагается devDependencies, где мы подключили наш C3D Vision wasm. Справа показано, как мы с ним работаем внутри самого исходного кода. Мы делаем импорт обычного модуля и начинаем его использовать. На этапе, когда мы уже работаем с внешними модулями, в директории проекта появляется папка node_modules, на этапе инсталляции в нее подкачиваются все зависимые модули.

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

Worker, многопоточность, promise

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

На рисунке 4 изображен Task 1:

Технологии создания графических веб-приложений на примере C3D Web Vision, фото 4

У первой задачи выполняется запрос на загрузку (она что-то загружает, запрашивает какой-то файл с сервера), и после ожидания загрузки запускаются Task 2 и Task 3. Но мы можем с помощью promise решить эту задачу таким образом — запустить загрузку, а в то же время будут выполняться следующие задачи. Тут можно сказать, что в этом случае все работает в два потока, но давайте рассмотрим другой пример.

Если у нас есть долгая по времени задача, я рекомендую ее делить на маленькие таски (рис. 5). Делать в какой-то период времени тайм-ауты, чтобы можно было, например с помощью UI, прервать задачу. Иначе может получиться так, что мы на двадцать секунд заблокировали браузер и он больше не отвечает на наши запросы, а пользователь будет постоянно открывать-закрывать вкладку браузера и не понимать, почему все зависает.

Технологии создания графических веб-приложений на примере C3D Web Vision, фото 5

В браузере можно выполнять код в нескольких потоках. Для этих целей предусмотрены webworker. Webworker — это отдельный поток или процесс (рис. 6) он позволяет перенести долгоиграющие задачи отдельно от основного потока. Существует два вида worker — shared и обычные. Их отличие в том, что shared worker можно использовать между двумя разными вкладками браузера. Например, вы запустили Яндекс.Музыка — для прослушивания в одной вкладке, а потом открываете вторую вкладку и можете управлять плеером там.

Технологии создания графических веб-приложений на примере C3D Web Vision, фото 6

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

Local store, offline mode

Существует еще один вид worker — service worker. Его отличиe от обычного и shared worker состоит в том, что он работает даже тогда, когда браузер выключен. Он необходим для реализации такой задачи, как offline mode — инструмент, который позволяет нам реализовать режим работы с данными без подключения к Интернету. Например, мы подключились к рабочей сети, чтобы открыть модельку, синхронизировали данные с сервером и потом поехали в командировку. В первом случае в service worker мы могли сохранить данные в кэше браузера, а во втором случае сервис выступил бы в виде офлайн-сервера и поднял бы нам все данные из кэша браузера. Для работы с данными существует четыре вида хранилища:

  • IndexDB — хранилище базы данных;
  • LocalStorage — хранилище для ключа значения;
  • SessionStorage — хранилище для ключа значения
  • Storage — хранилище, которое позволяет использовать 50% диска.

API для визуализации в браузере

С выходом HTML5 появился такой тег, как canvas, позволяющий рисовать в браузере 2D и 3D-графику. На сегодняшний день для визуализации 3D доступны WebGl и WebGl2. В 2022 году WebGL2 стал официально поддерживаться на всех устройствах, сегодня всем разработчикам рекомендуют переходить на WebGl2. API WebGL почти полностью отражает возможности на OpenGL/ES, но есть небольшие отличия в некоторых функциях. Например, при работе с памятью: так как у браузера есть своя особенность работы с памятью, такие функции, как glMapBufferRange, недоступны в вебе. Она реализована через функцию glGetBufferSubData, а в OpenGl ES нет этой функции.

Кто работал с OpenGL, знает про такую возможность, как shared context — возможность рисовать в двух окнах одну и ту же модель одновременно. Вроде как в браузере то же есть shared context (в документации), а на самом деле ни один браузер не поддерживает shared context. Выход есть — это offscreen context. Например, мы можем рисовать не в канву (canvas), а сразу рисовать в картинку, а картинку, в свою очередь, выводить на canvas. Если использовать offscreen context совместно в shared web worker, то есть возможность рисовать в двух вкладках одновременно. Это очень удобная возможность, если у вас более одного монитора — интерфейс управления можно вывести в одну вкладку, а визуализацию 3D-модели вытащить на другую вкладку и тем самым разделить управление и визуализацию. (рис. 7).

Технологии создания графических веб-приложений на примере C3D Web Vision, фото 7

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

В третьем квартере 2023 года запланирован выпуск WebGPU — это уже более продвинутая графика на основе Vulcan и DirectX12. Она уже доступна в браузере для разработчиков, но в пользовательских браузерах ее еще нет.

C++ в браузере

Самое интересное то, как мы решили собирать проект C++ под веб. Существует такой компилятор, как Emscripten. Он позволяет собрать проект C++ для выполнения в вебе, по итогу мы получаем два файла — JavaScript и WebAssembly. Wasm — это ассемблерный код для браузера, т. е. браузер воспроизводит его так же, как и JavaScript. На рисунке 8 показано, как выглядит исходный текст на C++, который мы собрали, а справа — инициализация собранного модуля, который выведет в текст output.

Технологии создания графических веб-приложений на примере C3D Web Vision, фото 8

Модуль hello.js подключается к странице в виде глобальной переменной, к которой добавляется функция-обработчик onRuntimeInitialized. Эта функция вызывается, когда wasm скомпилирован браузером и инициализированы все binding, после этого мы можем использовать функции из wasm.

emscripten очень хорошо интегрируется с CMake и conan. Для того чтобы использовать внешние conan-библиотеки для веб-сборки, необходимо в рецепте указать архитектуру wasm. А для сборки зависимостей под веб необходимо в зависимость добавить emsdk_installer. На этапе сборки conan подменит компилятор из системы (например, gcc) на emcc. В составе emsdk есть заголовочные файлы, которые мы можем использовать в проекте. Например, мы можем использовать OpenGL, а на этапе сборки он подменяет функции OpenGL на функции WebGl. Для нас ничего не меняется — у нас как был кроссплатформенный код, так он и остается.

Для того чтобы была возможность вызывать функции C++ из JavaScript, на этапе сборки у нас есть два варианта связки(binding) — Embind (который мы используем) и WebIDl.

На рисунке 9 слева представлен список типов, который можно использовать. У Embind намного больше возможностей:

Технологии создания графических веб-приложений на примере C3D Web Vision, фото 9

Так это выглядит внутри исходных кодов (рис. 10):

Технологии создания графических веб-приложений на примере C3D Web Vision, фото 10

Слева с помощью специальных макросов мы описываем класс, как будет он выглядеть в JavaScript. Справа мы пишем definition, который описывает нам, как он выглядит в TypeScript. Т. к. результатом сборки является JavaScript-файл, а мы работаем с TypeScript, то TypeScript definition нам приходится описывать вручную.

Сборка проекта C++ под веб: проблемы

Результатом сборки проекта С++ под веб является модуль JavaScript, а именно экспортная функция, которая загружает wasm-файл для компиляции и возвращает promise с модулем. Немного нестандартное поведение для модуля, поэтому есть проблемы с описанием его в TypeScript. Для решения проблемы мы реализовали обертку, которая экспортирует объект с функцией инициализации модуля. Еще есть проблемы с зависимостями от wasm-файла: т. к. мы поставляем библиотеку визуализации, то всегда есть условие, что wasm-файл должен быть подключен в проект, и про него не нужно забывать. На помощь приходит webpack, позволяющий собрать проект таким образом, что wasm-файл будет содержаться как ArrayBuffer в проекте.

Есть еще пару нюансов, про которые необходимо знать при сборке C++ под веб. Первое то, что для объектов, созданных в модуле C++, необходимо вызывать деструктор, т. к. ресурсы используют wasm-память (эмуляция кучи). Для JavaScript используется garbage collector, а для объектов С++ garbage collector не работает, потому что вся память для объектов С++ представляется в одном array buffer. С этим можно смириться, главное не забыть вызвать деструктор.

Второе — у нас есть ограничение по памяти в два гигабайта по умолчанию. Его можно расширить до четырех гигабайт с помощью опции компиляции, но можно столкнуться с проблемой OpenGl, потому что в компиляторе есть ошибки, именно в модуле OpenGL. Мы смогли пропатчить компилятор и исправить эти ошибки, а именно добиться правильной работы OpenGL при использовании 4 Гб памяти. Но сами вкладки то же имеют ограничение четыре гигабайта в браузере.

Третье — у нас медленные вызовы функций C++ из JavaScript, это описано в документации. С++ все-таки типизированный язык, а JavaScript все равно, какую переменную использовать, и внутри стоит много проверок — передали нам указатель или нужную переменную и т. д., поэтому вызов C++ из JavaScript выполняется медленно. Предложение — поменьше вызывать функции С++ из JavaScript, если такое возможно. У нас были проблемы с загрузкой данных, например при загрузке большой сборки. Там очень много мелких объектов, и на них мы получали большую потерю производительности. Нам пришлось перенести всю сборку на уровень C++. Всю реализацию мы уже делаем внутри C++, на уровне TypeScript мы выделяем с помощью внешних функций кусок памяти, кладем туда загруженный буфер, а потом сериализуем плюсовые объекты внутри wasm-модуля. Это дало нам существенный прирост в производительности.

Еще существует проблема с инструментами отладки. Так выглядит отладка кода C++ в браузере, рис. 11:

Технологии создания графических веб-приложений на примере C3D Web Vision, фото 11

Посередине кусок кода, мы можем поставить точку на отладку, прийти туда, но никогда не увидим там переменные, что и какая переменная означает. Мы можем это увидеть справа под номерами var 23, var 24. Есть небольшой стек справа, но он не очень информативный.

В данном материале я рассказал про опыт разработки front-части графических веб-приложений на примере C3D Web Vision. Это наш взгляд на разработку веб-версий программных продуктов. Надеюсь, этот опыт будет полезен и поможет компаниям-разработчикам в создании веб-приложений.

Сергей Климкин. Руководитель группы C3D WebVision. C3D Labs
Автор:
Сергей Климкин
Руководитель группы C3D Web Vision
C3D Labs
Поделиться материалом
Вверх