Статья, скорее всего, не расскажет ничего нового тем, кто давно занимается разработкой приложений с фишкой Augmented Reality, но возможно будет полезна тем, кто интересуется этой темой и близок к написанию AR-приложения.
Предыстория
Немного лирического отступления – что такое AR и почему она стала так популярна на мобильных устройствах.
Когда-то давно, наверное ещё в начале 80-х годов прошлого века, в военные истребители начали внедрять проекторы, которые проецировали информацию прямо на стекло лобового обзора самолёта. Пилоты очень обрадовались нововведению, т.к. следить за самыми важными показателями таким образом было намного удобнее, чем отрывать взгляд на аналоговые, да пусть даже и цифровые, датчики.
К теме особо не относится, но нам доподлинно известен факт (отец одного из наших сотрудников участвовал в разработке), что в советских Су-27 уже в 1992 году крутились 3D-модельки самолёта с подсветкой узлов. Графический движок был написан на ассемблере, на процессоре то ли в 1 МГц, то ли в 2 МГц. Позже такие же индикаторы пришли и в гражданскую авиацию, и в 1990 году один из инженеров Boeing ввёл само понятие “Augmented Reality”.
Много позже, когда в смартфоны пришёл акселерометр и гироскоп, светлой голове пришла идея соединить их с камерой и OpenGL ES – так родилось множество игр, навигационных помощников, но больше всего бюджета в этом направлении расходуется на маркетинговые и промоушн-приложения. Например, вырезав из журнала бумажный каркас для часов, одев его на руку и посмотрев через камеру телефона, пользователь может “примерить” любую марку часов из тех, что рекламирует журнал.
Теперь чисто техническая часть, те небольшие проблемы, с которыми программист столкнётся на самых популярных мобильных платформах.
iOS
Версии iOS для iPhone и iPad очень схожи, хотя и различаются по нескольким параметрам. К таким параметрам относится, к сожалению, то, что в iPad OS окно вывода изображения с камеры (UIImagePicker) – обычный UIView, а в iPhone OS – UIViewController. Если в случае с айпадом всё понятно – управляем и ложим его как и любую другую вьюшку, то в айфоне всё немного сложнее – окно ImagePicker-а обязательно должно быть модальным, а добавление вьюшек поверх камеры возможно только используя параметр cameraOverlayView. Т.е. чтобы добавить некое 3D поверх камеры, нужно сделать следующее:
imagePicker .cameraOverlayView = [[ARView new] autorelease];
Скорее всего, это анахронизм, оставшийся от iOS 3 и ниже. К каким неудобствам это приводит? К целому списку:
- UIImagePickerController прячет Status Bar. Возвращать его в переходе из режима AR в, например, игровое меню, нужно будет самим. Иначе у вас, в большинстве случаев, поедут на 20 пикселей все вьюшки;
- Невозможно показать изображение с камеры не на весь экран, т.к. превьюшка была сделана под стандартный UIImagePicker, с контролами снизу. И у нас остается только два варианта: делать снизу свои контроллы, либо применять матрицу для трансформации изображения, главное в ней правильно сохранить пропорции изображения;
- Если необходимо сменить изображение камеры, например, на статический фон по нажатию кнопки, это приведёт к целому ряду действий – нужно оборачивать вьюшку со статическим бэкграундом в свой вью-контроллер, убирать UIImagePickerController, добавлять в свою вьюшку оверлэй, живший раньше в cameraOverlayView, и делать present новый вью-контроллер. Так же необходимо не потерять вьюшку в памяти и сделать вовремя retain/release, либо держать её всегда в retain property. Это всё вместо imagePickerView.hidden = YES; как в айпаде, плюс явно медленнее по производительности;
- Ещё можно столкнуться со слишком умными UIButton. Для реализации переключения AR/non-AR режима, код должен был выполняться в экземпляре ViewController, но у UIButton на это свои планы. Если у UIButton был выставлен обработчик нажатия с target – ViewController, то после того как вьюшку изъяли из ViewController и поместили в imagePicker.cameraOverlayView, этот обработчик перестает работать. Видимо UIButton в курсе что target ViewController больше не видим из-за того, что имейдж пикер поверх всего, и не посылает ему сообщений. Пришлось внутренней вьюшке давать указатель на вью контроллер и добавлять промежуточный код, который будет уже дергать непосредственно вью контроллер.
В общем, куча костылей – всегда лучше, когда все стандартные классы – это вьюшки, а вью контроллерами распоряжается только пользователь.
Android
С Android дела обстоят немного иначе.
Превью камеры, точнее SurfaceView, можно поместить во вьюшку любых размеров, и нет необходимости создавать какую-то модальную активити поверх всего. Но без специфичных телодвижений не обошлось. Оказалось, что мы сами должны найти подходящий размер превью (список размеров иногда большой, и может различаться на девайсах от разных производителей, да что там, даже от одного производителя). В поисках оптимального разрешения и пропорции придется перебирать все размеры из возможных вариантов и сравнивать их с размером и пропорцией вью, куда мы хотим поместить это превью в рантайме. Размер превью не всегда будет соответствовать размеру SurfaceView, так что для соблюдения пропорций картинки и получения подходящего размера превью придется делать свой ViewGroup, размещать там SurfaceView, и делать расчеты, что и как размещать в методе onLayout.
Ещё одна интересная вещь — если в iOS ты хочешь нарисовать 3D модельку поверх превьюшки с камеры, то ты снизу располагаешь превьюшку (UIImagePicker), а дальше сверху рисуем любые вьюшки, в том числе и с 3D моделями. В Android решили сделать как-то по своему — если стандартные UI элементы можно спокойно рисовать поверх превьюшки (SurfaceView), то 3D модельки в GLSurfaceView нужно размещать под(!) превьюшкой. При этом нужно выполнить ряд телодвижений:
- для SurfaceView вызвать метод getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS), хоть в документации и написано для SURFACE_TYPE_PUSH_BUFFERS “This constant is deprecated. this is ignored, this value is set automatically when needed.”, но без этого вызова превью не работает и приложение даже крэшится;
- В GLSurfaceView нужно указать, чтобы она была прозрачной — getHolder().setFormat(PixelFormat.TRANSLUCENT). Без этого вызова на некоторых девайсах может всё работать (работало на HTC Desire S), а может и не работать (не работало на Google GalaxyNexus). Возможно, это из-за различий GPU на разных аппаратах. Так что лучше не пренебрегать данным методом.
Для работы AR этого достаточно, но сразу же возникли проблемы со статическим фоном для non-AR режима. Видите ли, по умолчанию GLSurfaceView не прозрачный, под ним нельзя что-либо отобразить средствами стандартного UI (ни виджет ImageVIew, ни даже background самого GLSurfaceView не работают). Но его можно сделать прозрачным с помощь метода setZOrderOnTop(true) — GLSurfaceView становится прозрачным, но при это начинает отображаться поверх всех элементов в активити.Без разницы — они под, над или вообще в другой вьюшке. Так что выход только один — если что-то нужно рисовать под 3D моделькой, и это не превью с камеры, то нам в помощь OpenGL ES. Для этого нужно загрузить картинку в память как текстуру, предварительно ресайзнув её для получения сторон, кратных степени двойки (на некоторых GPU работает без этого с- или без падения производительности; на некоторых вообще не работает, так что делать нужно обязательно). Выводиться эта текстура будет на плоскости, размеры которой равны размерам вью порта. Нам остается только рассчитать правильную пропорцию текстуры, т.к. есть множество различных размеров экрана с различными пропорциями.
Например
Скриншот в начале статьи взят из приложения Euro Horn, которое посвящено Евро-2012 и доступно для скачивания в AppStore и Google Play (бесплатное с рекламой, хотя реклама почти никогда не показывается в пределах СНГ – так что считайте без рекламы). 3D-модель футбольной дудки выводится поверх фона или, опционально, камеры, и наклоняется вместе в наклоном девайса пользователя, создавая иллюзию настоящей футбольной дудки.
Автор: iago