Использование Unity3D в нативном iOS/Android приложении для моделирования освещения открытых пространств
Unity3D известнейшая платформа для разработки 3D и 2D игр, завоевавшая популярность во всем мире. В то же время ее возможности не ограничены разработкой только игровых приложений, а подходят для применения в любых других областях, требующих создания кроссплатформенных приложений для работы с графикой. В этой статье мы расскажем об опыте использования Unity3D для разработки системы расчета освещения открытых пространств.
Заказчик нашего проекта — производитель осветительного оборудования международная корпорация БЛ ГРУПП. В целях расширения привлекательности своей продукции и упрощения взаимодействия с клиентами ему потребовалось разработать приложение, позволяющее визуально смоделировать расположение осветительных приборов, а также провести расчет освещенности поверхности и вывести необходимую техническую информацию в отчете. Предполагалось, что приложение запускается на iPad или Android планшете потенциальным клиентом или торговым представителем и позволяет клиенту сразу получить представление о возможности осветительных установок.
В общем виде, приложение представляет собой редактор, позволяющий добавлять и редактировать элементы освещения, дорог, декоративных элементов, произвести светотехнический расчет сцены, вывести отчет в pdf. Каждый элемент имеет свой набор параметров для редактирования и подтипы, влияющие на его отображение и расчет.
- Имеются несколько видов мачт освещения, с различными типам крепления светильников, углами наклона светильников и длиной выноса. Для определенных типов светильников возможна индивидуальная настройка с указанием направления освещения.
- Дороги могут быть линейными участками, элементами дуги, площадью, кольцом. Для каждого элемента может быть настроены размеры, положение, тип разметки, слой.
- Декоративные элементы — машины, деревья, кустарники, дорожные знаки
Все элементы сцены можно вращать и перемещать. Также поддерживаются стандартные действия вернуть назад или повторить операцию. Общие настройки проекта позволяют задать текстуру дорг, поверхности земли, отображение дополнительных параметров. Сцена отображается в 2D/3D режимах. А при расчете освещения на поверхности отображается карта освещенности поверхности в фиктивных цветах.
Весь UI по возможности требовалось делать нативными средствами iOS/Android.
Основное техническое требование к приложению — это уметь рассчитать освещение сцены согласно технической спецификации светильников. Также требовалась возможность для каждого светильника отображать и просматривать его диаграмму направленности (кривые сил света) в 3D/2D режимах.
Выбор платформы
Для реализации проекта нами был выбран Unity как более удобный нам для реализации требуемого функционала. Вообще в нашей компании имелся опыт работы и с другими 3D движками и платформами (OpenSceneGraph, Ogre3D, LibGdx) и технически они все могут справится с требуемой задачей, но в этот раз выбор пал на Unity, который позволяет удобнее управлять сценой в процессе разработки и отлаживать в процессе работы.
Основные сложности
Мы не будем вдаваться в тонкости разработки всего приложения, поскольку технически функционал по отображению и редактированию сцены вполне стандартный. Естественно имелись сложности с механизмами специфического редактирования объектов, добавления и их удаления, а также сохранения цепочки команд для возможности повторения и отмены действий.
Нам бы хотелось остановится только на особенностях системы связанных с работой с нативным UI, генерацией pdf отчетов и работой с фотометрией и расчетом освещения.
Работа с нативным UI
В большинстве случаях взаимодействие Unity c нативными функциями системы происходит с использованием системы плагинов, что позволяет встроит нужный функционал в приложение. Однако, в нашем случае ситуация несколько обратная. Нам требовалось иметь полноценное UI, отображаемое поверх окна Unity.
К счастью, Unity умеет экспортировать проект, который можно использовать как основу для нативного приложения. Основная сложность в данном случае заключается в том как интегрировать в полученный проект дополнительный UI. Также, не менее важно то, что при сборке проекта Unity его формат и расположение файлов формируются Unity и частично перезаписывается, что ограничивает возможность модификации проекта.
При разработке iOS приложения мы воспользовались механизмом предложенным в статье. Во время разработки использовалась Unity 5.5 и на данный момент указанное в ней может потерять актуальность. При сборке андроид проекта дополнительных проблем не возникало за исключение того, что Unity каждый раз перезаписывает манифест файл и файлы ресурсов.
Дополнительная проблема состоит в том, что Unity может работать только в одном окне. В тоже время, нам требовалось обеспечить работу Unity для отображения всей сцены, а также при открытии окна настроек должна была отображаться 3D модель фотометрического тела светильника. Для осуществления этого пришлось воспользоваться «хаком» и использовать один и тот же объект UIView в различных окнах.
Для передачи сообщений мы воспользовались стандартным функционалом предлагаемым Unity. То есть, все сообщения были в формате json и передавались простыми строками. На производительность это не накладывало никаких ограничений поскольку размер сообщений максимум доходил до 100 символов, а их частота определялась скоростью работы с программой. В тоже время, в более требовательных приложениях имеет смысл сделать собственный обработчик сообщений как представлено на хабре здесь и здесь.
Расчет освещения
Все источники освещения, используемые в приложении поставляются в стандартном IES формате, который описывает распределение света в различном направлениию (спецификация). Этот формат широко используется в профессиональных CAD системах и 3D редакторах. Он представляет собой текстовой файл с указанием интенсивности света в различных направлениях и дополнительную метаинформацию с указанием типа, общей интенсивности источника, осей и плоскостей симметрии. Учитывая симметрию светильников, ies файл может быть очень маленьким. Например, в случае осевой симметрии, достаточно указать раследеление света только в одной плоскости.
IESNA91[TEST]
Simple demo intensity distribution [MANUFAC]
Lightscape Technologies, Inc.
TILT=NONE
1
-1
1
8
1
1
2
0.0 0.0 0.0
1.0 1.0 0.0
0.0 5.0 10.0 20.0 30.0 45.0 65.0 90.0
0.0
1000.0 1100.0 1300.0 1150.0 930.0 650.0 350.0 0.0
Для отображения диаграммы направленности света использовались два вида отображения:
- Кривые сил света (КСС) — двумерный график на котором представлена интенсивность света в одной из главных плоскостей в зависимости от направления. Этот график для удобства может быть представлен как в полярной так и декартовой системе координат
- Фотометрическое тело — трехмерное изображение интенсивности света в разном направлении
Расчетный модуль
Для расчета освещения у заказчика имелся собственный C++ модуль, используемый в других продуктах компании, а поэтому требовалось интегрировать его в Unity проект. Порядок подключения модуля отличался от используемой платформы.
- На iOS платформе Unitу умеет непосредственно вызывать С функции, поэтому достаточно скопировать исходники модуля непосредственно в проект и добавить классы для его взаимодействия с Unity. Классы возможно хранить как непосредственно в проекте iOS, так и в папке плагинов, которые автоматически копируются при экспорта проекта в XCode. Пример вызова C++ функций следующий:
[DllImport("__Internal")] public static extern void calculateLight([MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] Light[] lights, int size, ref CalculationResult result);
- На Android платформе С++ модуль должен быть предварительно скомпилирован в отдельную библиотеку. Это можно сделать непосредственно добавлением исходников С++ в проект и настройкой gradle для их сборки в so библиотеки.
- Также, для отладки и тестирования Unity части разработка велась на windows машине, поэтому потребовалось подключить исходники модуля и в Windows. Это делается аналогично андроид проекту, только в этом случае файлы скачала собираются в dll библиотеку и подключались к проекту.
Отображение карты освещенности
По требованию заказчика результаты расчета освещенности должны быть отображены на поверхности сцены. На поверхности дорог необходимо использовать фиктивные цвета с отображением шкалы соответствия цвета и интенсивности освещения, а на остальной поверхности достаточно просто отображение ее яркости.
Как ранее упоминалось весь расчет производился подключаемым модулем C++ в который передавались данные об источниках цвета. Результатом расчета был двумерный массив интенсивности света по всей поверхности сцены с заданной детализацией.
Полученная карта освещенности анализировалась на минимальное, максимальное значение, по которым строилась одномерная текстура градиента (GradientRamp). Используя эту текстуру интенсивность освещения преобразовывалась в фиктивные цвета непосредственно во фрагментном шейдере. При этом для различных поверхностей дорог и поверхности земли использовался один и тот же шейдер, а переключение режима освещения обеспечивалось с использованием «multi-compile» шейдера.
Генерация Pdf файла
В соответствии с техническими требованиями для пользователя должен был генерироваться отчет, содержащий информацию об общей сцене (размеры, ее изображения, параметры освещения) и информацию о каждом использованном типе светильников с указанием их положения, направления характеристик, а также диаграммами КСС и отображением фотометрического тела.
Поскольку отчет должен был отображаться как на iOS так и Android, то его генерацию необходимо было производить непосредственно в Unity модуле, а потом уже отображать стандартными нативными средствами.
Для построения pdf была выбрана библиотека iTextSharp, отвечающая нашим требованиям. Создание отчета в ней не представляет особой сложности и заключается в создание блоков текста, таблиц, изображений непосредственно из кода. Однако, в ходе разработки мы столкнулись с множеством нюансов, решение которых иногда требовало значительных усилий. Основной проблемой из которых был запуск генерации отчета в фоновом потоке.
Если при тестировании на десктопной машине генерация pdf составляла порядка нескольких секунд, то при тестировании на iPad mini 3 это время легко достигло 1–3 минут. Естественно, создание отчета потребовалось перенести в отдельный поток, для избегания проблем с подвисанием интерфейса. В общем случае это не является проблемой, но это не так при использовании Unity, в котором явно запрещено использовать Unity API не из главного потока. В тоже время, для отчета нам требовалось, как минимум, рендерить КСС и изображение сцены, что необходимо делать только из главного потока.
Таким образом, для построения отчета нам необходимо запускать задачи в определенной последовательности и при этом часть из них может работать в фоновом потоке, а часть обязательно должна быть запущена в главном.
На первый взгляд для решения этой проблемы можно попробовать использовать стандартный механизм и запускать каждую операцию в отдельной корутине. Однако, это не избавляет нас от проблемы с торможением интерфейса. Как известно, корутины работают в основном потоке и не подходят для медленных операций. В тоже время, при генерации отчета многие операции требуют существенного времени, а поэтому корутины не могут помочь в решении нашей проблемы.
UniRx
Другое решение это разделить код на часть, которой необходимо работать в основном потоке и часть, которая может быть запущена в отдельном потоке. В таком случае, например, изображения можно построить с использованием механизма корутин, а далее встроить их в отчет уже в отдельном потоке. Однако, в этом случае потребуется где-то сохранять промежуточные результаты, что накладывает дополнительные ограничения на объем используемой памяти или свободного места на устройстве.
В нашем приложении мы предпочли пойти прямым путем и запускать задачи последовательно то в главном, то в фоновом потоках. Проблема была только в том, как именно организовать такой запуск задач, чтобы не погрязнуть в этой мешанине и корректно синхронизировать операции.
Существенную помощь в решении этой проблемы принесло использование Rx его воплощение в виде бесплатного ассета UniRx, о котором на хабре уже подробно рассказывалось здесь и здесь.
Его использование позволило значительно упростить взаимодействие между потоками и в примере ниже показано можно запустить несколько методов в строгой последовательности, но в разных потоках
var initializer = Observable.FromCoroutine(initMethod);
var heavyMethod1 = Observable.Start(() => doHardWork());
var mainThread1 = Observable.FromCoroutine(renderImage);
var heavyMethod2 = Observable.Start(() => doHardWork2());
initializer.SelectMany(heavyMethod1)
.SelectMany(mainThread1)
.SelectMany(heavyMethod2)
.ObserveOnMainThread()
.Subscribe((x) => done())
.AddTo(this);
В этом примере последовательно будет запущен метод doHardWork () в фоновом потоке. После его завершения запустится renderImage () в основном потоке, а после выполнится doHardWork2() опять в фоновом потоке.
Также стоит отметить, что в ходе анализа генерации отчета на быстродействие было обнаружено, что наиболее медленная часть это внедрение изображений в отчет. Поиск в интернете показал, что мы не единственные кто сталкиваются с этой проблемой, но подходящего нам решения не нашлось. Нам пришлось несколько снизить качество изображений до приемлемого уровня, что дало прирост в скорости на 20–40%.
Таким образом, в созданном нами приложении получилось удачно внедрить графический движок Unity в нативное приложение iOS/Android. Это отличается от традиционного подхода когда Unity является главной частью приложения и обращается к специфичным свойствам системы через систему плагинов. В тоже время наш подход может быть полезен при необходимости разработки сложного нативного интерфейса, в который требуется встроить нетривиальную 3D графику.