Трёхмерный мир на плоском экране: как отобразить банковскую 3D-карту в приложении на Android

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

Привет, меня зовут Дмитрий Гайдук, я Android-разработчик KODE. В 2018 году к нам пришёл новый заказчик — болгарский банк TBI. У нас был опыт разработки банковских приложений, и в TBI был знакомый функционал: заявки на кредит, платежи и переводы. Кроме реализации кода, вёрстки и сетевых запросов, заказчик попросил добавить трёхмерности и покрутить банковскую карту вокруг своей оси. 

Хорошо, сказали сделать — значит, сделаем. Всё же век цифровых технологий, кругом 3D, дополненная реальность и много разных библиотек для реализации. Но не всё было так просто, как я думал. Рассказываю о трудностях, с которыми мы столкнулись, и как в итоге решили такую нестандартную и интересную задачу.

Примечание. TBI Bank — один из лидеров болгарского рынка. С 2018 года компания KODE помогает банку создать digital-инфраструктуру, чтобы перевести процессы выдачи кредита и обслуживания клиентов полностью в онлайн. Подробнее о кейсе читайте здесь.


Часть 1. Задача и поиск решений

Задача: отобразить 3D-объект банковской карты с интерактивными возможностями. Объект представлен одним OBJ и двумя PNG-файлами для каждой стороны карты.

Финальный результат работы
Финальный результат работы

Начинаем искать библиотеку.

Критерии:

  • Минимум усилий на добавление 3D-карты, адекватное время на изучение и реализацию задачи. Так как мы занимаемся разработкой мобильного приложения, а не геймдевом, мы хотели обойтись без погружения в тонкости OpenGL.

  • Хорошо документированное API.

  • Возможность добавить библиотеку в обычную вёрстку вместе с остальным GUI и использовать её как обычный компонент View. Данный компонент нужно было добавить в RecyclerView.

  • Интерактивность. Возможность крутить карту жестами.

Нашли не так много вариантов:

Выбор пал на ARCore. Мы учитывали опыт разработчиков приложения Revolut: в статье Interactive 3D cards for Revolut Android app было описано использование этой библиотеки без дополненной реальности. Код выглядел несложным — кажется, то, что нужно.


Часть 2. Welcome to Android

Читаем статьи, как всё хорошо в Android, затем добавляем банковскую карту в приложение и радуемся. Эх, мечты!

Сначала коротко расскажу о том, как добавить 3D-объект на экран, используя библиотеку ARCore. Более подробную инструкцию можно найти в интернете.

  1. Добавляем файлы 3D-объекта в проект. В нашем случае это OBJ и PNG для каждой стороны карты.

  2. В конфигурации build.gradle добавляем плагин Sceneform

apply plugin: 'com.google.ar.sceneform.plugin'
sceneform.asset('card/card.obj',
        'default',
        'card/card.sfa',
        'src/main/assets/card')

3. Консольная команда для генерации SFB-файлов для ScenefForm:

./gradlew compileSceneformAssets
4. Добавляем отображение 3D-объекта на экране

Немного кода, и трёхмерный мир на экране вашего устройства!


Далее я рассмотрю проблемы, выявленные во время разработки с ARCore без дополненной реальности. Многие решения не идеальны. Нам пришлось прибегнуть к некому workaround: в документации нет информации, как это сделать правильно с помощью API.

Проблема 1. Источник света

Освещение окружающей среды задается по умолчанию, и это нельзя изменить.

Официальный ответ:

Sceneform adds an environment light based on an image (IBL/image-based lighting). This environment light cannot be customized currently, hopefully in a future version.

Перевод
На карте отражаются объекты, которых не должно быть

Нам нужен только источник света без каких-либо объектов из окружающего мира. В классе com.google.ar.sceneform.Scene освещение окружающей среды задаётся таким образом:

LightProbe.builder()
 .setAssetName("custom light probe")
 .setSource(this, R.raw.custom_light_probe)
 .build().thenAccept {
    scene.lightProbe = it
 }

Где custom_light_probe — это файл .sfb. По умолчанию используется R.raw.sceneform_default_light_probe.

В этом же классе можно увидеть код создания объекта Light Probe

Возможно, файл sceneform_default_light_probe.sfb создан на основе small_empty_house_2k.exr.

Изображение внутри EXR-файла, которое видно в отражении карты
Изображение внутри EXR-файла, которое видно в отражении карты

Как создать собственный файл .sfb из .exr — неизвестно. Есть утилита cmgen для 3-движка Filament, которая позволяет получить Image Based Lighting из файла .exr. Но нужно собирать из исходников, и не подходит из-за формата: нам нужен именно .sfb. Команда Gradle — compileSceneformAssets — тоже это делать не умеет.

Решение

Шаг 1. Убираем источник света от LightProbe и используем только источник от Scene.sunlight:

scene.lightProbe.dispose()
scene.sunlight?.isEnabled = true
scene.setUseHdrLightEstimate(true)
Результат

Не совсем то, что нужно: получается темноватое изображение.

Шаг 2. Пробуем добавить ещё один источник света.

В поле Scene.sunlight находится объект класса com.google.ar.sceneform.Sun, который наследуется от Node. Получается, нужно добавить на сцену ещё один Node с определённой конфигурацией. С использованием HDR должно выглядеть круто!

Код
Результат

Картинка стала ярче, но нам нужно более «охватывающее» освещение, без резких тёмных сторон при повороте карты. В процессе проверки разных вариантов освещения выяснилось, что через Node нельзя добавить несколько источников света.

Шаг 3. Сводим к минимуму отражение внешнего мира.

Такое решение было принято, так как мы не можем обойтись без источника света LightProbe и изменить картинку по умолчанию. Чтобы убрать отражение, нужно повернуть «освещение окружающей среды»:

val lightProbe = scene.lightProbe
lightProbe.rotation = Quaternion(Vector3.up(), 78F)
lightProbe.intensity = 2.5f
scene.lightProbe = lightProbe

Проблема 2. Материал объекта

Далее нам нужно настроить материал для сторон карты и сделать отражение немного размытым. Для этого открываем файл .mtl и редактируем нужные параметры. Как устроен этот файл и какие параметры доступны можно посмотреть в Wiki, а всю исчерпывающую информацию ищите здесь.

Для уменьшения «зеркальности» материала нужно изменить значение для Ns — коэффициента зеркального отражения. Подбираем остальные параметры:

Ns 40.000000
Ka 1.000000 1.000000 1.000000
Kd 0.800000 0.800000 0.800000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.450000
d 1.000000
illum 2

Теперь карта выглядит практически так, как нужно.

Результат

Проблема 3. Цвет фона

В тёмной теме мы заметили, что фон области просмотра 3D отличается от остального приложения: у 3D-сцены он немного темнее. Сделать фон прозрачным не получилось. Если выставить цвет color/transparent или null, то фон становится чёрным.

Вёрстка с цветом для тёмной темы, цвет общего фона и фона SceneView одинаковый.

Код
Как выглядит карта на данном этапе

Выяснилось, что это из-за специфической логики работы библиотеки. Конвертирование цвета происходит в классе com.google.ar.sceneform.rendering.Color:

Код

Мы не нашли универсальной логики для вычисления цвета фона. Если делать обратную формулу для логики в классе Color, то получается цвет с RGB-составляющими, значения которых будут выходить за допустимые границы. Следовательно, мы не можем получить нужным нам цвет.

Решение

Вручную подбирать цвет, который не будет отличаться от остального приложения.

Проблема 4. Определение поддержки 3D на устройстве

Библиотека ARCore не работает на Android ниже 7 версии. Но не всё так просто. На некоторых устройствах приложение падает и на Android 7. Нужно индивидуально определять: есть поддержка или нет.

Решение

Создаём объект SceneView:

fun isSceneViewAvailable(context: Context): Boolean {
  return try {
	SceneView(context).destroy()
	true
  } catch (e: Throwable) {
	false
  }
}

Проблема 5. Асинхронная инициализация SceneView

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

Выяснилось, что инициализация объекта Scene происходит асинхронно. То есть после создания Scene поле lightProbe какое-то время имеет значение null. Более того, если попытаться получить значение Scene.lightProbe до того, как произошла инициализация, то получим исключение, и приложение упадёт.

Реализация метода getLightProbe() в библиотеке ARCore:

public LightProbe getLightProbe() {
    if (this.lightProbe == null) {
        throw new IllegalStateException("Scene's lightProbe must not be null.");
    } else {
        return this.lightProbe;
    }
}

Чтобы настроить освещение и добавить объект на экран, нужно дождаться, когда библиотека инициализируется. Но публичных методов для этого нет.

Решение

Создаём цикличную проверку, которая будет регулярно определять, всё ли инициализировалось. И только после этого будет начинаться работа с библиотекой.

Код

Часть 3. Наслаждаемся результатом и подводим итоги

Запускаем приложение — карта крутится. Классно! 

Был получен хороший опыт работы с 3D в Android, потому что нам удалось реализовать поставленную задачу с помощью небольшого объёма кода: чуть более 200 строк на отображение карты и добавление интерактивности.

Пример кода со всеми наработками — на GitHub.

Конечно, хотелось бы использовать более удобный инструмент, чтобы можно было проще импортировать 3D-модель, редактировать основные параметры и сразу видеть результат. Библиотека ARCore всё-таки нацелена на использование для дополненной реальности, поэтому для других целей её использовать проблематично.

Подсмотрел, как это выглядит при разработке iOS-приложений

Возможно, стоит посмотреть в сторону Filament, на которой построена ARCore. Она позволит настраивать построение 3D-сцены более точно. А время на изучение Filament должно компенсировать время, потраченное на подводные камни ARCore.

Если есть другие предложения для решения данной задачи или появились вопросы — ждём в комментариях.

Источник: https://habr.com/ru/post/551238/


Интересные статьи

Интересные статьи

Я не являюсь профессиональным разработчиком с огромным стажем в данной области (и это даже не хобби, а лишь нужда в разработке конкретного приложения), потому данная стат...
Запуск приложения – это первое впечатление наших пользователи после установки приложения. Это то, что происходит каждый раз. Простое и быстрое приносит пользователям гора...
В статье «Делаем современное веб-приложение с нуля» я рассказал в общих чертах, как выглядит архитектура современных высоконагруженных веб-приложений, и собрал для демонстрации простейшую...
До недавнего времени у Dropbox была техническая стратегия использовать общий код C++ для мобильных приложений iOS и Android. Идея понятна: написать код один раз на C++ вместо его дублирования отд...
Гугл любит пасхалки. Любит настолько, что найти их можно практически в каждом продукте компании. Традиция пасхалок в Android тянется с самых первых версий операционной системы (я думаю, все в ку...