7 ноября 2011 в 16:33

OpenGL iBooks-like анимация перелистывания из песочницы

Помню, когда я в первый раз взял в руки iPad, более всего меня поразило приложение iBooks c его натуралистичным перелистыванием страниц. Все остальные «фишки» как-то не особо впечатляли, но это… это показалось воплощением полной интерактивности. По сравнению с обычными графическими интерфейсами, использующими стандартные элементы GUI, интерфейсы с использованием анимации OpenGL это новый шаг в развитии пользовательских интерфейсов.

Историческое отступление: первыми стали использовать элементы анимации в GUI рабочие станции Indy компании Silicon Graphics, которая и является основоположником языка OpenGL. На десктопах этих компьютеров были трехмерные кнопки меню, которые при нажатии переворачивались в 3D, открывая списки подменю из таких же кнопок. Это было здорово, и это было действительно красиво. Жаль, что Silicon Graphics ушла в небытие из-за неправильного менеджмента, эта компания была действительно первой во многом (OpenGL, STL, журналируемые файловые системы).
Поэтому не буду петь дифирамбы Apple, они лишь внедрили хорошо забытое старое. Но справедливости ради стоит отметить, что и это было прорывом, ведь они задали тенденцию и унылые статические интерфейсы, надеюсь, скоро останутся в прошлом.

Итак, OpenGL анимация и GUI.
По роду деятельности мне уже приходилось разрабатывать проекты с использованием OpenGL, поэтому когда я столкнулся с необходимостью создать анимацию, эмулирующую перелистывание бумажных страниц, передо мной встали две основные задачи:

• алгоритм деформации плоскости листа
• интеграция OpenGL и пользовательского интерфейса

проект разрабатывался для операционной системы MacOS X, используя основные фреймворки и язык Objective-C.

Алгоритм деформации бумажного листа
Идея взята отсюда. В качестве алгоритма деформации используется уравнение конуса. Этот алгоритм был разработан в исследовательских лабораториях Xerox PARC еще в 2004 году.
Изгиб бумажного листа определяется поверхностью некоего конуса, изменяя параметры которого можно добиться требуемого эффекта анимации.

image

Я не буду вдаваться в подробности описания самого алгоритма и тригонометрии. Скажу лишь что он пересчитывает любую 2D точку P(x,y) плоскости листа в 3D точку T(x,y,z), используя изменяемые параметры А и θ. Для тех, кто желает подробностей, есть исходники тестового проекта для iOS и OpenGL-ES.

Интеграция OpenGL и пользовательского интерфейса
Методы анимации, используемые в пользовательских интерфейсах, обычно выполнены по следующей схеме:

первоначальное состояние — фреймы анимации — конечное состояние

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

В применении к перелистываемым страницам первоначальное и конечное состояния соответствуют двум последовательным разворотам страниц книги.
То есть алгоритм анимации должен быть следующим:

— взять текстуры текущего разворота страниц
— выполнить прокрутку страниц offscreen
— взять текстуры следующего разворота страниц из offscreen buffer
— перекрыть разворот страниц OpenGL рендерером
— запустить анимацию
— по завершении спрятать OpenGL рендерер и показать следующий разворот, который находится в offscreen.

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

В зависимости от направления перелистывания, рендерер инициализируется четырьмя текстурами страниц, — две статические текстуры и две динамические, деформируемые с помощью вышеприведенного алгоритма.
Рассмотрим перелистывание страницы справа-налево:

Текущий разворот: левая страница – статическая, правая страница – динамическая.
Следующий разворот: левая страница – динамическая, правая – статическая.

Ниже приведен код контроллера, управляющего анимацией перелистывания. Код упрощен и управляет переворотом страницы только в одну сторону.

@implementation PageAnimationController
 
#pragma mark -
#pragma mark *** Animation ***
 
// поворот страницы
- (void)flipPage
{
    // инициализируем рендерер текстурами страниц
    [self prepareRenderer];
 
    // создаём таймер анимации (об этом объекте чуть ниже)
    // передаём ему указатель на наш рендерер
    FlipAnimation *animation = [[FlipAnimation alloc] initWithDuration:1.0f
                                                        animationCurve:NSAnimationEaseInOut
                                                                  view:rendererView];
    // это значит, что startAnimation не выходит, пока анимация
    // не закончена
    [animation setAnimationBlockingMode:NSAnimationBlocking];
    [animation startAnimation];
    [animation release];
 
    // анимация закончена, прячем рендерер
    [self endAnimation];
}
 
- (void) prepareRenderer
{
    // удостоверимся что выбран нужный текущий контекст OpenGL
    [[rendererView openGLContext] makeCurrentContext];
 
    // первый разворот страниц
    [rendererView setAnimatedFront:[self getPageTexture:PAGE_RIGHT]];
    [rendererView setStaticLeft:[self getPageTexture:PAGE_LEFT]];
 
    // переворачиваем страницу за кулисами
    [bookView flipPageOffScreen];
 
    // инициализируем текстуры следующего разворота страниц
    [rendererView setAnimatedBack:[self getPageTexture:PAGE_LEFT]];
    [rendererView setStaticRight:[self getPageTexture:PAGE_RIGHT]];
 
    // наконец-то показываем наш рендерер, перекрывая текущий разворот
    // книги такой же картинкой, только отрисованной в OpenGL
    [rendererView setHidden:NO];
}
 
- (void)endAnimation
{
    // прячем OpenGL рендерер
    [rendererView setHidden:YES];
}
 
@end


Стандартный таймер анимации. Вызывает updateTime: со значением относительного времени анимации в промежутке 0..1

@interface FlipAnimation : NSAnimation
{
    OpenGLPageCurlView * rendererView;
}
@end
 
 
@implementation FlipAnimation
 
- (id)initWithDuration:(NSTimeInterval)duration 
        animationCurve:(NSAnimationCurve)animationCurve
                  view:(OpenGLPageCurlView *)view 
{
    self = [super initWithDuration:duration animationCurve:animationCurve];
    if (self != nil)
    {
        rendererView = view;
    }
    return self;
}
 
- (void)setCurrentProgress:(NSAnimationProgress)progress
{
    // рендеринг анимации, значение progress меняется от 0 до 1
    [rendererView updateTime: progress];
}
 
@end


Анимацией легко управлять и без таймера, например используя события мыши или тачпада. Так достигается эффект «перетаскивания» страницы, достаточно посылать рендереру сообщения updateTime: из обработчика событий.

Код рендерера OpenGL я приводить не буду, так как он банален. Маппинг текстур выполнен страйпами треугольников, добавлена подсветка сцены анимации.
Ну и в заключение вот как это выглядит в работе (извиняюсь за качество):

+41
4334
130
lymes –2,5

комментарии (24)

0
shimapa23, #
Статья хорошоя, спасибо. Уверен, что пригодится

Только вот на видео страница как-то странно сквозь красную закладку «проходит»…
0
lymes, #
Да, закладка там просто нарисована. Убрать надо, действительно мешает.
+4
kibermaks, #
Совсем недавно Mutado выложили альфу замечательного контрола для iOS
Mutado.PaperStack (по ссылке можно увидеть видео в действии и ссылку на GitHub)
Правда малость сыроват пока, но при желании допилить можно(и нужно).
Главное что безопасен для релиза в AppStore, начиная от iOS 3.2.

Спасибо большое разработчикам!
0
Agent_Smith, #
О, уже выпустили, давно лежит их сайт в закладках.
+2
RetroGuy, #
Такое ощущение что у вас страница при повороте справа на лево искривляется в другую сторону. Не знаю, может это так и задумано — но выглядит не очень.

Да, кстати, в iBooks очень важную роль играет тень — она создает впечатление расстояния угла страницы от ее первоначальной плоскости.
+1
crash, #
Еще в ios5 добавили UIPageViewController который «листает»
0
lymes, #
Все верно. Жаль только что в SDK для MacOS X этого нет, стандартный фильтр CIPageCurlTransition — ужасная поделка.
Поэтому приходится делать руками.
НЛО прилетело и опубликовало эту надпись здесь
0
freeznah, #
но зачем? только ради «как в .....»?

0
lymes, #
потому что:

1. красиво
2. надо же как-то использовать GPU
3. книга кажется настоящей
4. еще разок: красиво ;)
0
freeznah, #
ИМХО, именно из-за таких «мотивов» крайние версии софта при запуске отжирают больше памяти, чем было установлено на компах, где использовались их предки (причем без большой разницы в функционале)
0
lymes, #
Вы наверное совсем поверхностно представляете себе работу OpenGL? При чем здесь расход памяти?
0
freeznah, #
это было глобальный комментарий, не применительно к технологии, пример работы с которой вы показали, а в целом ко всей индустрии «сделать штоб красиво»
+1
lymes, #
Тогда я тоже глобально выскажусь.

Никогда не понимал людей, которые «копят» ресурсы. ИМХО, есть в этом что-то от Шейлока или Гарпагона: навесить на систему стопиццот гигабайт оперативной памяти и ревнительно сидеть над открытым Activity Monitor-ом, — не дай бог какой-нить процесс откушает пару лишних мегабайт…

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

Вспоминать седое прошлое, когда на 386SX было 4 мегабайта и «все отлично работало», — глупо. Было так, а теперь иначе.
0
vics001, #
Соглашусь полностью с этим замечаниям, эх если бы были еще стандарты сколько у тебя должно быть памяти и т.п. Приложения же не пишут, как игры, сколько им надо оперативной памяти, процессора, состояние системы (критично для Windows).

Хорошо когда есть еще целевое устройство например Ipad 2, а если запустят на Ipad1, а там по-другому, что делать?

Я вот все жду-жду время когда же наконец всем приложениям будет хватать памяти, процессора, но все время появляется самая крутая ОС, крутой редактор, красивые приложения, так что в конце становится тормознутая ОС, падающий редактор и некрасивые приложения :(
ЗЫ: это я на маркетинг жалуюсь, надо же новые устройства продавать ;)
0
katleta, #
Спасибо за статью, много работал в области разработки приложений для электронных журналов, поэтому было очень интересно.
Реализация через openGl достаточно оригинальная, но все равно есть хороший, стабильный движок на CG.
Пока лучше его сложно было найти с реализаций изгиба, переворота, теней:
Leaves project
Пример скриншота работы есть в статье: http://habrahabr.ru/blogs/macosxdev/131855/
+1
lymes, #
Это не айс. Эту работу должен делать OpenGL, CPU не должен быть нагружен графикой, когда в системе есть мощный GPU, который практически бездействует. Этим должен заниматься исключительно GPU, иначе будет как писал freeznah чуть выше.

0
katleta, #
очень извиняюсь, что промахнулся на «ответить» Вам. Мой комментарий оказался в ветке ниже.
0
katleta, #
я не спорю! я лишь подчеркнул, что реализация именно правдоподобности изгиба, переворота страницы и теней лучше в движке, который указал по ссылке.
Решение намного более выгодное
0
lymes, #
ммм… насчет теней согласен, насчет правдоподобности — нет. бумажный лист не гнётся параллельно плоскости книги, а в Leaves это сделано именно так.
0
katleta, #
раньше еще находил некий движок, работающий даже на iphone 3.2, тоже через CG, где лист мог гнуться как угодно, от той области, за которую ты начинал тянуть. Но лагов там было куча + ужасная пикселизация.
в openGl c aliasing будет полегче.
0
lymes, #
уупс, я тоже промахнулся )))
0
lymes, #
Вот PaperStack на OpenGL-ES, тоже на основе вышеописанного алгоритма, изменяя A и θ конуса можно добиться какого угодно изгиба.
Единственно, там через попу сделаны тени. Я конечно понимаю, в OpenGL работа с тенями довольно трудное дело, но зачем же текстурами-то тени городить? Оно конечно работает и красиво, но… не изящно что ли.
0
Agent_Smith, #
Так альфа же.

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