Трехмерные живые обои и OpenGL ES



Доброго времени суток, Хабр!

Я — участник маленькой компании (из двух человек), которая делает живые обои (live wallpapers) для Android-девайсов. В этой статье будет рассказано о развитии наших приложений, от сравнительно простых до более сложных, примененных технологиях, трюках и решенных проблемах — все на конкретных примерах, в (почти) хронологическом порядке. Все наши обои — полностью трехмерные, написаны с использованием OpenGL ES.

В качестве бонуса — небольшая коллекция шейдеров, пользуйтесь ими как хотите. Предложения об их улучшении или исправлении будут только приветствоваться — мы не претендуем на звание гуру в этой области.

“Часы” и “HTC” — первая радость




Использовался движок JPCT-AE, еще старый, который использует OpenGL ES 1.0. Загрузка моделей происходила из формата 3DS. Никаких шейдеров, сжатых текстур, render-to-texture, и прочих модных штучек здесь не было. Впрочем, здесь можно было обходиться и без всего этого.

“Новогодняя елка” — недостатки OpenGL ES 1.0




Здесь выяснилось 2 вещи: эмулятор отвратительно рисует трехмерную графику и JPCT-AE пожирает много памяти на текстуры.

Тогдашний эмулятор не просто плохо рисовал трехмерную графику OpenGL ES 1.0, а рисовал ее через чур некорректно и медленно. С этих пор разработка пошла только на реальном девайсе.

Используемая тогда версия JPCT-AE выделяла оперативную память на каждую текстуру, и программа могла внезапно закрываться из-за недостатка памяти. Надеемся, этот баг уже исправлен в новых версиях движка. Известно, что у Android-приложений ресурсы OpenGL не идут в расчет выделенной памяти Dalvik, но здесь у движка налицо были проблемы с неосвобождением памяти после загрузки текстуры в видеопамять. Пришлось уменьшать размер текстур не из-за того что видео-карта не справлялась с отрисовкой, а из-за того что все они застревали в памяти и программа падала.

Еще, при использовании тогдашней версии JPCT-AE возникал мерзкий баг при пересоздании контекста OpenGL — текстуры могли потеряться или попутаться. Решения этой проблемы так и не было найдено. Опять же надеемся, что в текущей версии JPCT-AE этот баг исправлен.

Справедливости ради добавим, что в последних версиях JPCT-AE добавлена поддержка OpenGL ES 2.0 и собственных шейдеров, так что начинающие разработчики могут пробовать использовать его.

“Роза” — OpenGL ES 2.0 и alpha testing




Здесь мы перешли на использование чистого OpenGL ES 2.0, без использования каких-либо фреймворков и движков.

Причиной перехода на OpenGL ES 2.0 стало то, что для отображения розы в OpenGL ES 1.0 надо было использовать alpha-blending, с сортировкой полигонов, естественно. Что все равно привело бы к низкой производительности из-за чрезмерной повторной прорисовки (depth-testing ведь при этом отключается).

Естественно, эти проблемы легко решаются применением alpha testing, и OpenGL ES 2.0 позволил нам элементарно его реализовать. Однако, с этим возникли некоторые сложности. Сам по себе alpha testing реализуется элементарно:

vec4 base = texture2D(sTexture, vTextureCoord);
gl_FragColor = base;
if(base.a < 0.5) { discard; }


Однако, эта очевидная реализация имеет некоторые недостатки, связанные с загрузкой текстуры из PNG файла.
Во-первых, края лепестков получают тонкую черную грань, что особо заметно на светлых лепестках:

image


Это происходит если использовать стандартный метод загрузки текстуры, который производит умножение цвета на альфа-канал. Артефакта можно избежать, реализовав собственную загрузку текстуры.

Во-вторых, текстура получается несжатая, занимает много видеопамяти, а следовательно медленней рисуется. Поверьте, сжатые текстуры действительно сразу же дают прирост производительности. Здесь проблема в том, что единственный стандартизированный алгоритм сжатия текстур для OpenGL ES 2.0 — это ETC1. А он не поддерживает альфа-канал. Так что приходится делать две текстуры — для цвета (diffuse), и прозрачности (opacity). Две сжатые текстуры все равно занимают меньше памяти, чем одна несжатая того же размера (и, соответственно, работают быстрее). Также, вы можете решить, что для конкретных случаев текстуру прозрачности можно сделать меньше, чем текстуру цвета, или наоборот.
Но решены все эти проблемы были позже, когда разрабатывались следующие живые обои — Турбины.

“Турбины” — шейдеры, шейдеры, шейдеры...




Здесь мы стали по-настоящему использовать возможности шейдеров OpenGL ES 2.0. Из новых шейдеров здесь — туман (пока по-пиксельный) и шейдер ландшафта, по которому пробегают тени от туч.

Туман выбрали линейный, хотя сразу и попробовали более реалистичный экспоненциальный. Экспоненциальный туман на мобильных девайсах затормозил так, что мы от него сразу отказались.

Шейдер ландшафта использует две текстуры — повторяющуюся текстуру травы размером 512*512 и вторую текстуру, в которой объединены статический lightmap ландшафта и текстура теней от туч (размером всего 64*128). Здесь выполняются различные операции над цветами — lightmap умножается, тени и туман объединяются с помощью mix().
Дальше выяснилось, что эти шейдеры были чертовски неоптимизированными, так как все писалось в коде пиксельного шейдера.

“Тыква” — новые шейдеры, ускорение загрузки




Здесь мы реализовали быструю загрузку моделей в готовом двоичном формате, а потом и VBO, и впервые применили сжатие текстур ETC1 (про сжатые текстуры уже было сказано раньше на примере розы).

Так получилось, что новая загрузка моделей, реализованная в этих живых обоях, принесла больше пользы, когда была применены на обоях розы. Изначально модели загружались из формата OBJ, что было довольно медленно и вызывало нарекания пользователей “Розы”. Ведь модель здесь на самом деле немаленькая, чуть больше 1000 треугольников, и OBJ-файл парсился долго. Была написана утилитка, которая создавала файлы с данными, готовыми для передачи в glDrawArrays(). Прирост производительности трудно описать — раньше роза грузилась 10 и более секунд, теперь же загрузку можно охарактеризовать только как “мгновенно”.

Хочу отметить, что мобильные девайсы сильно удивили нас в своей способности отрисовывать большое количество полигонов — как только что было сказано, 1000 треугольников в розе для любого девайса не оказалось проблемой, а в других наших тестах и обоях мы уже используем куда больше. А вот VBO разочаровал — абсолютно никакого прироста производительности замечено не было.

Шейдер тыквы основан на каком-то примере ткани из RenderMonkey, немного упрощен и доработан.

“Тающая свеча” — анимация, простая сортировка




Для этих обоев мы разработали простой алгоритм вертексной анимации, с простой линейной интерполяцией между кадрами. Анимация программная, на т.е. на CPU. Используется она в двух целях — анимация свечи (несколько кадров анимации) и анимация теней. Да, никаких shadow map здесь нет, тени — это отдельные модели с текстурой, просчитанной в 3ds max. Модельки теней имеют по 2 кадра анимации — обычное состояние и немного растянутое в направлении от источника света. Между этими кадрами и производится плавная анимация, синхронно с масштабированием пламени свечи:



Также, разработка вертексной анимации позволила нам добавить птиц в обои “Турбины”.

Ну и как вы можете представить, здесь мы потренировались с blending’ами и переключением depth-testing. Здесь есть 2 прозрачных объекта которые нужно сортировать — это пламя свечи и перо. Вместо точной сортировки по расстоянию до камеры мы проверяем текущее положение камеры. Камера движется по окружности, на отдельном секторе которой нужно рисовать сперва пламя, потом перо, на остальном — наоборот.

“Праздник фонарей” — вертексные шейдеры, порядок отрисовки




Как вы могли заметить по картинке, здесь мы будем рассказывать про шейдер воды и тумана. Что вы могли не заметить, так это то что про шейдер неба тоже есть что рассказать.

Шейдер воды взят из примера PowerVR, и упрощен (выкинули рефракцию, может где-то вычисления упростили). Отражение рендерится в небольшую текстуру — всего 256*256.

Для ландшафта используется довольно сложный шейдер тумана: плотность тумана зависит от расстояния и высоты, а цвет тумана зависит от положения луны (вокруг луны туман подсвечен). Все это оказалось возможным реализовать с достаточной производительностью только в вертексном шейдере. После этого, мы переписали шейдер тумана в Wind Turbines на вертексный, это дало заметный прирост производитлельности.

Теперь про шейдер неба. Небо рисуется из 3-х слоев: собственно текстура неба с луной, слой звезд (хранится в альфа-канале текстуры цвета) и движущихся туч. Звезды не объединены с текстурой неба потому что им нужна большая яркость. Изначально, шейдер был написан как-то так:

const float CLOUDS_COVERAGE = 12.0;

vec4 base = texture2D(sTexture, vTextureCoord);
vec4 clouds = texture2D(sClouds, vec2(vTextureCoord.x + fOffset, vTextureCoord.y));
float stars = (1.0 - base.a) * starsCoeff;
stars *= pow(1.0 - clouds.r, CLOUDS_COVERAGE);
base = base + stars;
gl_FragColor = base + base * clouds * 5.0;


Казалось бы, кода немного но все равно производительность была низкой, что-то нужно было оптимизировать. Как видно, для затемнения звезд тучами используется довольно жирная функция pow(), а также выполняется дополнительное усиление яркости туч (clouds * 5.0). Затемнение звезд тучами удалось заменить на линейный clamp(), а от усиления (clouds * 5.0) удалось избавиться вообще, сделав текстуру туч поярче в Photoshop’е. Итоговый шейдер стал работать немного быстрее:

vec4 base = texture2D(sTexture, vTextureCoord);
vec4 clouds = texture2D(sClouds, vec2(vTextureCoord.x + fOffset, vTextureCoord.y));
float stars = (1.0 - base.a) * starsCoeff;
stars *= clamp(0.75 - clouds.r, 0.0, 1.0);
base = base + stars;
gl_FragColor = base + base * clouds;


Однако, этой производительности по-прежнему не хватало. Попытки рендерить упрощенное небо в текстуру отражения ничего не давали — текстура всего-то 256*256, даже если в нее не рисовать небо вообще, ничего не изменялось. Проблема оказалась совсем в другом — в том, что небо рисовалось первым, а не последним. Т.е. ресурсы видео-карты расходовались на то чтобы нарисовать целую полусферу неба, а потом все равно затереть ее ландшафтом и другими объектами. Мы внимательно пересмотрели порядок отрисовки в нашей программе, выстроили объекты в оптимальном порядке и этим добились необходимой производительности. Так что даже когда Вам кажется что Вы завершили свое OpenGL приложение и все хорошо работает, пересмотрите напоследок свой порядок отрисовки — переставив пару строк кода местами, Вы можете неплохо улучшить производительность.

“Тюльпан” — сортировка объектов




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

Сортировать нужно, разумеется, прозрачные “расфокусированные” тюльпаны. Каждый из них — это отдельный объект, вычисляем квадрат расстояния от него до камеры и сортируем по этому значению (достаточно и квадрата расстояния, ведь значение нужно исключительно для сравнения).

Производительность этих обоев оказалась не столь большая как у Розы, что было вполне ожидаемо — здесь добавлено много прозрачных полигонов, а они никак не оптимизируются, рисуются все что на экран попали. Чтобы добиться приемлемой производительности, пришлось поподбирать FOV и количество “размытых” цветов на экране — чем меньше, тем лучше.

“Jelly Bean” — блики, blending




Для бликов на стекле и “бобах” мы использовали упрощенные вычисления. Обычно в вычислениях блика участвует вектор направления света, и если принять что он равен какой-нибудь простой константе вроде (0; 0; 1), то вычисления очень упрощаются — выбрасывается операция dot() и т.д. Результат же получается такой, как будто источник света находится там же где и камера, а для такой простой сцены этого более чем достаточно.

Шейдер стекла подбирает цвет и alpha в зависимости от экранной нормали и блика. Работает вместе с правильно выбранным blending-ом, естественно.

“Лотос” — стрекозы и интерполяция




Начнем с шейдеров. Вода здесь та же что и в “фонарях”, только с измененными параметрами. Для смены дня и ночи, ко всем объектам дописано умножение на ambient-цвет:

...

gl_FragColor *= ambientColor;


Анимация крыльев стрекозы реализована необычно — геометрия крыльев не анимируется. Такое махание крыльев невозможно передать анимацией геометрии. Крылья здесь сродни лопастям вертолета — они движутся очень быстро и для невооруженного глаза сливаются в единую медленно переливающуюся, вращающуюся плоскость. Так что модель крыльев мы сделали из нескольких пересекающихся плоскостей, а эффект быстрого махания создается шейдером, выполняя простой сдвиг UV-координат.

Статическая геометрия крыльев:



В этих обоях вы можете наблюдать множество объектов, движущихся по плавным траекториям, — стрекозы, бабочки, рыба под водой и камера. Все они движутся по сплайнам, а для плавности движения реализована bicubic-интерполяция на основе алгоритма Catmull-Rom. Отметим, что интерполяция немного отличается от той, которыми сглаживаются сплайны в 3ds max со Smooth-вершинами. Сто-процентная точность для большинства объектов нам здесь не пригодилась. Также, недостатком примененного алгоритма является то, что для равномерного движения отрезки между вершинами сплайна должны быть одинаковой длинны — иначе движение на коротких отрезках будет замедляться, а на длинных — ускоряться. А для движения камеры это уже важно.

Заметим, что для создания сплайна с равномерными отрезками вы можете применить модификатор “Normalize Spline” в 3ds max.

Но в нашем случае недостаток с длинной отрезков был исключен тем, каким образом мы экспортировали анимацию из 3ds max в текстовый формат (для того чтобы вынуть из него значения в массив). Для экспорта анимации мы воспользовались командой “Export Animation”, которая генерирует XML-файлик (*.xaf) с анимацией. В нем можно найти не только все вершины кривой по которой движется объект, но и его положение для каждой позиции на track bar. Таким образом, вы можете анимировать свой объект как угодно — любыми контроллерами, кейфрейм-анимацией, путями и даже всем этим одновременно, а на выходе получить координаты его положения в пространстве в зависимости от времени.

Для эффекта ореола у светляков был сделан вертексный шейдер, который позиционирует и масштабирует спрайт ореола:



Коллекция шейдеров


Коллекцию шейдеров можно скачать здесь: dl.dropbox.com/u/20585920/shaders.zip

Все шейдеры сделаны в программе RenderMonkey, скачать ее можно с сайта AMD: developer.amd.com/resources/archive/archived-tools/gpu-tools-archive/rendermonkey-toolsuite/#download

Если Вы владелец видеокарты GeForce, то RenderMonkey может не запускаться. Вам понадобится использовать утилитку ‘nvemulate’. Скачать здесь: developer.nvidia.com/nvemulate. Запустите ее и измените настройку ‘GLSL Compiler Device Support’ на ‘NV40’:



UPD: С чего начать самому


Хотите написать свои живие обои с OpenGL? Не знаете что делать с GLWallpaperService и прочими умностями? Все уже давно написано, берите код примеров и меняйте под свои нужды:
пример OpenGL ES 2.0 от Гугла: code.google.com/p/gdc2011-android-opengl
пример рабочего live wallpaper с использованием OpenGL: github.com/markfguerra/GLWallpaperService
Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 29

  • UFO just landed and posted this here
      +3
      Есть давний пример от Гугла, берите и меняйте под себя: code.google.com/p/gdc2011-android-opengl/
      Это Вам тоже пригодится: github.com/markfguerra/GLWallpaperService — здесь есть готовый пример рабочих обоев.
      Это тот случай, когда по туториалу дольше получится, чем использовать готовое решение.
      0
      Отлично! Спасибо! Переходите к мобильной 3D визуализации товаров.
        +1
        Я конечно понимаю, что это делалось под экраны с повышенной плотностью пикселей, но не хотите сделать какой-нибудь антиалиасинг? Например, простой в вычислительном плане FXAA.
          0
          Для девайсов на NVidia Tegra мы используем ихний «Coverage Sampling Antialiasing», который работает без потери производительности (проверено). Для него выбираем конфиг с такими значениями:
              EGL_COVERAGE_BUFFERS_NV, 1,
              EGL_COVERAGE_SAMPLES_NV, 2,
          

          Для других девайсов выбирать обычный АА — опасненько, производительность может упасть, и очень сильно. Даже без всяких АА приходиться биться за каждый FPS :)
            0
            Это он для PC видеокарт простой. А для мобильных чипов — оверкилл.
            На PowerVR MSAA будет быстрее
            +1
            Я так понимаю с такими обоями батарейки хватает на час?
            А вообще круто, хоть и без анти-алиасинга, как заметили выше.
              +2
              Я вижу вас уже минусуют :)
              Это типичное заблуждение, любые живые обои имеют массу комментариев на Гугл Плее про то что они красивые, но садят батарейку. На самом деле, батарейку они садят, да, но только когда ты на них смотришь, т.е. находишься на хоум-скрине. Пользователи очень мало времени проводят просто глядя на хоум-скрин. А как только пользователь ушел с хоум-скрина — процесс обоев прекращает существование.
              На собственных телефонах — никаких изменений в расходе батарейки, у всех знакомых которым давали потестировать — тоже.
                +3
                Если он так мало смотрит на хому скрин, нафига ему там кино?
                  +1
                  Потому, что хочется. Для красоты. Для удовлетворения чувства прекрасного. У вас в квартире, наверняка, красивые обои на стенах поклеены или штукатурка интересная или плитка. А ведь можно было просто белой краской покрасить :)

                  Я, например, рабочий стол своего компа вижу, от силы, минуты три за весь рабочий день, но это не мешает мне периодически вешать на него свежие нескучные обои.
                    +2
                    Обои-то дома я постоянно вижу. И, кстати, в последнее время тенденция к чисто белым, максимум пастельного цвета, стенам. На рабочем компьютере, который работает от сети, да, возможно и есть смысл. А вот когда осталось 1% батареи, а мне смарт из последних сил отрисовывает анимированный рабочий стол, а потом на звонок не хватает… Лучше я выключу такой рабочий стол. Чувство прекрасного не сравнить с чувством раздражения ;(
                    Вот Вам бесплатная фича — сделайте чтоб этот Ваш декстоп проверял заряд аккумулятора и при определенном пороге заменял себя на статику. Люди Вам спасибо скажут.
                      0
                      Вот кстати поддержу, такая опция многим придётся по нраву.
                  +1
                  А точно известно, что процесс с обоями завершается, когда переходишь с хоум-скрина, например, в список приложений телефона? Объясню ситуацию: Android 2.3, «живые обои» с плавающим пузырьком, который лопается (со звуком!), его в него ткнуть. Перехожу с хоум-скрина в общий список приложений, выбираю нужное мне и вдруг слышу опять тот же характерный звук с -вроде-как-отключенных- обоев.
                +1
                А вы использовали какую-то стороннюю либу для того, чтобы подружить GLSurfaceView и WallpaperService? Или сами писали требуемый код? Я так понимаю, что заставить их работать вместе — отдельная задача.
                  0
                  Да, использовали: github.com/markfguerra/GLWallpaperService
                    0
                    Спасибо, как раз к ней присматриваюсь… Я правильно понимаю, что она поддерживает OpenGL 2.0 и шейдеры с ходу, без танцев с бубном?
                      0
                      Это просто обертка для запуска Renderer (любого, хоть 1.0 хоть 2.0 — как проинициализируете). Работа с шейдерами — это дело вашего кода, а не этого.
                        0
                        Ага, понятно! Буду разбираться, спасибо!
                  0
                  Автор, Вам вопрос: неужели живые обои пользуются такой популярностью среди обычных пользователей?
                  Просто вспоминая про серьёзное потребление батареи вопрос напрашивается сам собой.
                    0
                    >серьёзное потребление батареи
                    Да, ладно ка =)
                      0
                      среди кого? обычная пользователь никак не связывает потребление батареи и свистелку на рабочем столе. А свистелки они любят какой-то необъяснимой любовью
                      0
                      Просто не могу не похвалить.
                      Статья супер, молодцы, так держать!!!
                        0
                        Красиво. Lantern Festival понравился, но какие-то глюки на Nexus 10. Такое ощущение что не хватает глубины zbuffer. Треугольники моргают на горах и отражениях светильников.
                          0
                          А почему glDrawArrays? Не пробовали с индексами (glDrawElements)? По идее, должно быть более эффективно.
                            0
                            Используем оба метода — VBO для статических объектов, а glDrawArrays — для анимированных. Разницы в производительности не заметно. См. stackoverflow.com/questions/13060299/opengl-es-2-0-vbo-performances-in-a-shared-memory-architecture
                              0
                              Вы меня неправильно поняли.

                              Суть glDrawElements в том, что используется ещё один буфер. Последний аргумент glDrawElements есть либо указатель на него в «клиентской памяти», либо offset в текущем привязанном GL_ELEMENT_ARRAY_BUFFER (аналогичные вещи происходят в glVertexAttribPointer, только там GL_ARRAY_BUFFER).

                              В этом буфере хранятся индексы.

                              Например, если в массиве атрибутов хранятся числа: 0.0 1.0 2.0 3.0
                              А в element array — 0 1 2 2 3 0
                              То в результате glDrawElements(GL_TRIANGLES, GL_UNSIGNED_SHORT, 6, NULL) на соответствующий вход шейдера придут числа
                              0.0 1.0 2.0 2.0 3.0 0.0.

                              Более близкий пример — тот самый формат .obj. Только там тройные независимые индексы (вершина/нормаль/uv), OpenGL же такого не позволяет.

                              В каком-то смысле это обобщенный triangle strip/triangle fan/что там ещё есть не знаю даже. Вершины нередко дублируются в разных треугольниках, так что память экономится прилично. Да и последние вершины в кеш наверняка попадают, так что и прозводительность должно положительно влиять.
                                0
                                Разницы в производительности не заметно.

                                Так тем более надо использовать VBO. Ничего не теряете, а когда память будет выделенная, станет только лучше.
                              0
                              А финансовые результаты есть?

                              Мы (я программист и моя девушка дизайнер) просто сделали обои под НГ для Android заработали около 600$ за месяц не вкладывая ни копейки в продвижение. Но поняли, что программировать рисовать и продвигать(если серьезно) вдвоем очень сложно. Поэтому решили найти программистов, но что-то на фрилансе очень сложно найти толковых OpenGL спецов, которые учтут все нюансы мобильной разработки. Отсюда у меня к вам предложение, объединить силы, если интересно конечно?
                                0
                                Где то я уже это видел…
                                image

                                А, вспомнил. Сделайте такие анимированные обои, куплю:

                                :D

                                Only users with full accounts can post comments. Log in, please.