Создание циферблатов для Android Wear на OpenGL ES 2.0

Как только Google анонсировал возможность создания кастомных циферблатов в новом Android 5.0 для Android Wear, мы заказали в ближайшем интернет-магазине новый ASUS ZenWatch для того, чтобы испытать эту новейшую фичу. Было решено не портировать одни из существующих трехмерных живых обоев, а создать новую сцену именно для часов. В результате был придуман концепт и создано приложение с набором из пяти цифровых циферблатов, реализованное в 3D с помощью OpengGL ES 2.0.



Первоначальный концепт


Идея циферблата возникла под впечатлением от этой работы (автор beeple):



Видео выглядит классно и не кажется сложным на первый взгляд. Однако мы обнаружили некоторые технические ограничения, которые препятствовали создать такую же сцену, как и в видео. Не так-то просто сделать цифры из линий (wireframe), которые бы корректно перекрывали друг друга. Вкратце, это бы потребовало большого количества команд отрисовки (draw calls) и весьма ограниченное железо часов могло бы не справиться с таким количеством draw call-ов. Так что мы перешли к другой идее — добавить глубины к цифрам из пикслельных шрифтов со старых компьютеров.

Конфигурация OpenGL ES без компоненты глубины


Класс Gles2WatchFaceService из Android Wear API не предоставляет доступ ко всем фичам, необходимым для создания полноценной трехмерной сцены циферблата. Основная проблема, с которой мы сразу же столкнулись, это невозможность выбора необходимой конфигурации OpenGL ES. По сути, данный класс вообще не предоставляет доступа к EGL. Его достаточно для запуска циферблата ‘Tilt’ из примера API, но он не имеет никакой гибкости для настройки EGL. Этот пример не требует наличия буфера глубины, так как не содержит никакой пересекающейся геометрии. В Gles2WatchFaceService попросту выбирается конфигурация без EGL_DEPTH_SIZE и API не дает возможности ее изменить.

Ввиду обнаружения столь существенного ограничения было принято решение декомпилировать исходный код и создать свою реализацию Gles2WatchFaceService с блекджеком и правильной конфигурацией EGL.

Также весьма известный разработчик живых обоев и циферблатов Kittehface Software сообщил в своем блоге о том, что Moto360 не работает с вполне корректной 16-битной конфигурацией EGL. Поэтому на всех устройствах наше приложение всегда использует 32-битный цвет. Огромное спасибо Kittehface Software за то, что предупредили нас и других разработчиков об этом — поиск причины падения на Moto360 мог бы занять дни, да и самих часов в распоряжении на тот момент у нас не было.

Мы не будем приводить полный код декомпилированного класса Gles2WatchFaceService потому что он все равно будет меняться с выходом нового API. Вот изменения которые мы сделали в декомпилированном Gles2WatchFaceService:

1. Обновляем поиск подходящего конфига с компонентой глубины:

private static final int[] EGL_CONFIG_ATTRIB_LIST = new int[]{
    EGL14.EGL_RENDERABLE_TYPE, 4,
    EGL14.EGL_RED_SIZE, 8,
    EGL14.EGL_GREEN_SIZE, 8,
    EGL14.EGL_BLUE_SIZE, 8,
    EGL14.EGL_ALPHA_SIZE, 8,
    EGL14.EGL_DEPTH_SIZE, 16, // этого не хватало
    EGL14.EGL_NONE};


2. Делаем переменные mInsetBottom и mInsetLeft доступными из дочерних классов. Они будут использоваться для того чтобы корректно обновлять вьюпорт. К примеру, делаем их protected:

protected int mInsetBottom = 0;
protected int mInsetLeft = 0;


glViewport()


Официальная докуметация имеет очень скудные и расплывчатые упоминания о том, как работать с устройствами с разными экранами, используя метод onApplyWindowInsets(). Там сказано, что этот метод нужно использовать для того, чтобы адаптироваться к экранам с обрезанным нижним краем. Это необходимо для того, чтобы скорректировать вид на таких устройствах, как Moto 360.

Не имея на руках Moto360, было непонятно, как именно использовать этот метод так, чтобы наши первые попытки запустить приложение на этих часах привели к тому, что вид был смещен — сдвинут вверх на размер обрезанной части экрана. Довольно странным было то, что этой проблемы не было в примере циферблата Tilt — он был корректно отцентирирован. Чтобы разобраться в этом, пришлось снова заглянуть в декомпилированный код сервиса Gles2WatchFaceService. Причина была в применении glViewport(). Для реализации эффекта свечения (bloom) мы использовали рендер в текстуру и вызывали glViewport() для переключения рендеринга в текстуру или на экран. Для того, чтобы избежать смещения картинки, нужно принимать во внимание значение нижнего отступа для обрезанных экранов, когда задаете glViewport().

Прозрачность в обычном и ambient режиме


По неизвестным причинам, невозможно рисовать непрозрачные карточки оповещений (peek cards) в режиме экономии экрана (ambient mode). В обычном режиме вы можете задавать вид карточек, но в режиме экономии они всегда полностью прозрачные. Вам прийдется самому рисовать черный прямоугольник в том месте, где над ним будет выводиться карточка, используя метод getPeekCardPosition(). В нашем случае было достаточно использовать карточки малого размера и они не перекрывались с цифрами, но в общем желательно уменьшать размер области отрисовки для того согласно размеров карточки.

Отправка сообщений с настройками для обновления циферблата на часах


В нашей реализации приложения настройки циферблатов мы используем HoloColorPicker для выбора цветов цифр и фона. Изменение цвета на часах происходит в реальном времени, как только пользователь изменяет цвет (в событии ColorPicker.OnColorChangedListener контрола выбора цвета). Однако это вызывает некоторые проблемы. Пользователь может изменять цвет очень быстро (событие возникает при движении пальцем по «кольцу» выбора цвета), что приводит к переполнению очереди сообщений об изменении цвета. Wearable Data Layer API не предназначен для столь интенсивной отправки сообщений. Ничего не падает, но некоторая часть последних сообщений может попросту не дойти до часов. Для избежания этого мы ограничиваем отправку обновлений цвета интервалом 500 мс. Такая несущественная задержка не вызывает никаких неудобств и позволяет не перегружать возможности Wearable Data Layer API:

Handler handlerUpdateColorBackground = new Handler();

Runnable runnableUpdateColorBackground = new Runnable() {
    @Override
    public void run() {
        // update only if color was changed
        if (pickedColorBackground != lastSentColorBackground) {
            sendConfigUpdateMessage(KEY_BACKGROUND_COLOR, pickedColorBackground);
        }
 
        handlerUpdateColorBackground.postDelayed(this, 500);

        // update last color sent to watch
        lastSentColorBackground = pickedColorBackground;
    }
};  


Общие впечатления о железе Android Wear


Большинство часов для Android Wear построены на основе чипа Snapdragon 400 — ASUS, Sony, Samsung и LG используют именно его во всех своих часах. Этот чип имеет впечатляющий четырехъядерный процессор с частотой до 1,2Гц, чего более чем достаточно для часов. Его видеокарта Adreno 305 может показаться немного устаревшей на первый взгляд. Но часы имеют довольно маленькое разрешение 320х320 пикселей, так что ее мощности достаточно для отрисовки достаточно сложных 3D-сцен при 60 кадрах в секунду. Даже Moto 360 с его устаревшим чипом OMAP3630 имеет достаточно производительную видеокарту PowerVR SGX530, которая выдает достаточную производительность при имеющемся разрешении экрана.

Для примера, дополнительный эффект блума в разрешении 128х128 с четырехпроходным размытем не вызвал никакого падения производительности — Adreno 305 запросто справился с этой дополнительной задачей. Однако для часов оказалось достаточным использовать и более низкое разрешение блума 64х64 с двумя циклами размытия.

Есть множество видео, где люди показывают такие игры, как Temple Run 2 или GTA, запущенные на Android-часах, которые работают без лага — это в очередной раз демонстрирует производительность видеокарт в этих устройствах.

Шейдеры, использованные в приложении


Чтобы уменьшить количество команд отрисовки и работы проделываемой CPU, анимация цифр производится в вертексном шейдере. Чтобы проиллюстрировать это, вот пример модели перехода между цифрами «5» и «0»:

Как видно, вертексы модели разделены на 3 группы:
  • желтые части которые не анимируются — они представляют части, общие между цифрами «5» и «0»;
  • темно-серые части будут опускаться во время анимации — они не используются в цифре «0»;
  • светло-серые части будут подыматься при анимации они используются в цифре «0» но не в «5».

Эта информация занесена в текстурные координаты — координата V установлена в 0 для неподвижных частей, 1 для тех что подымаются вверх, и -1 — для тех что опускаются вниз. В дополнение к этому, U-координата задает фазу анимации, так что цилинры не двигаются все вместе, а с небольшим отставанием друг от друга:

Фрагментный шейдер очень простой — чем меньше Z-координата вершины, тем более черным рисуется конечный пиксель. Вот как выглядит переход между цифрами «3» и «4» (модели отзеркалены из-за RenderMonkey) — видно как части от «4» сдвинуты вниз, а части «3» подняты вверх:

Вот финальный результат когда низ модели сливается с черным фоном:


Код вертексного шейдера
uniform mat4 view_proj_matrix;
uniform float uAnim;
uniform float uHeight;
uniform float uHeightColor;
uniform float uHeightOffset;

varying vec2 Texcoord;
varying float vColor;

void main( void )
{
     vec2 uv = gl_MultiTexCoord0.xy;
     vec4 pos = rm_Vertex;
     pos.z += uHeight * uv.y * clamp(uAnim + uv.x, 0.0, 1.0);

     gl_Position = view_proj_matrix * pos;
     Texcoord = uv;

     vColor = (uHeightColor + pos.z + uHeightOffset) / uHeightColor;
}

uAnim управляет анимацией, задается в диапазоне [-1..1]
uHeight задает высоту анимации — значение зависит от «шрифта» и задает высоту отдельного столбика модели — равно 40 юнитам для шрифта показаного на картинках.
uHeightColor и uHeightOffset позволяют варьировать переход цвета в черный. Значения uHeightColor = 40 и uHeightOffset = 0 использовались для примеров на картинках но они разные для разных шрифтов.

Код фрагментного шейдера
uniform vec4 uColor;
uniform vec4 uColorBottom;
varying float vColor;

void main( void )
{
     gl_FragColor = mix(uColorBottom, uColor, clamp(vColor, 0.0, 1.0));
}

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

Ссылка на архив с шейдерами для программы RenderMonkey: dl.dropboxusercontent.com/u/20585920/shaders_watchface.rar

Публикация


При публикации Android Wear приложения на Google Play необходимо отметить галочку «Distribute your app on Android Wear». Это инициирует процесс модерации приложения на соответствие требованиям Wear App Quality. В нашем случае этот процесс занял всего несколько часов.
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 6

    +1
    А я хочу взглянуть на пятый циферблат!
      +1
      Ссылка в плей стор то где?
        0
        Честно говоря, ожидал увидеть хоть одну строчку кода. А тут статья в стиле «Смотрите как классно мы сделали!».
          0
          Согласен! Похоже на самопиар.
            0
            Немного не так.
            «Смотрите как классно мы сделали! Но где скачать не скажем!»
              0
              Ваши пожелания учтены, добавлены примеры кода и шейдеров.

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

            Самое читаемое