GrabDuck

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

:

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

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

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


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


Здесь выяснилось 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, без использования каких-либо фреймворков и движков.

Причиной перехода на 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 файла.
Во-первых, края лепестков получают тонкую черную грань, что особо заметно на светлых лепестках:

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

Во-вторых, текстура получается несжатая, занимает много видеопамяти, а следовательно медленней рисуется. Поверьте, сжатые текстуры действительно сразу же дают прирост производительности. Здесь проблема в том, что единственный стандартизированный алгоритм сжатия текстур для 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 и количество “размытых” цветов на экране — чем меньше, тем лучше.


Для бликов на стекле и “бобах” мы использовали упрощенные вычисления. Обычно в вычислениях блика участвует вектор направления света, и если принять что он равен какой-нибудь простой константе вроде (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’:


Хотите написать свои живие обои с OpenGL? Не знаете что делать с GLWallpaperService и прочими умностями? Все уже давно написано, берите код примеров и меняйте под свои нужды:
пример OpenGL ES 2.0 от Гугла: code.google.com/p/gdc2011-android-opengl
пример рабочего live wallpaper с использованием OpenGL: github.com/markfguerra/GLWallpaperService