Рекламодатель:
ООО «Нанософт разработка»

ИНН 7751031421 ОГРН 5167746333838

Рекламодатель: ЗАО «Топ Системы»

ИНН 7726601967 ОГРН 1087746953557

Рекламодатель:
ООО «С3Д Лабс»

ИНН 7715938849 ОГРН 1127747049209

5 - 2024

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

Сергей Климкин, 
руководитель группы C3D WebVision, C3D Labs
Сергей Климкин,
руководитель группы C3D WebVision, C3D Labs

Последние три года автор занимается разработкой компонента 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

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

Рис. 1

Рис. 1

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

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

Рис. 2

Рис. 2

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

Moдули commonjs, umd, amd

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

Рис. 3

Рис. 3

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

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

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

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

На рис. 4 изображен Task 1.

Рис. 4

Рис. 4

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

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

Рис. 5

Рис. 5

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

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

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 ее нет.

Рис. 6

Рис. 6

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

Рис. 7

Рис. 7

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

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

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

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

Рис. 8

Рис. 8

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

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

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

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

Рис. 9

Рис. 9

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

Рис. 10

Рис. 10

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

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

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

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

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

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

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

Рис. 11

Рис. 11

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

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

Регистрация | Войти

Мы в телеграм:

Рекламодатель:
ООО «Нанософт разработка»

ИНН 7751031421 ОГРН 5167746333838

Рекламодатель: ЗАО «Топ Системы»

ИНН 7726601967 ОГРН 1087746953557