Когда речь заходит о программировании графики под iPhone, люди сразу представляют UIKit’ы, Core Craphics’ы и прочие красоты Cocoa Touch, отягощенные Objective-C интерфейсом. Все это пестрое разнообразие, слабо известное за пределами Mac OS X, энтузиазма не вселяет. Меж тем все как-то забыли про OpenGL ES, программирование под который почти не отличается от такового под «большой» OpenGL и сильно облегчает жизнь.

 

Необходимый инструментарий

Прежде всего нам придется смириться с тем, что ребята из Apple устроили большую подставу, выпустив iPhone SDK только под Mac OS X. Учитывая то, что в основе этого продукта лежат GCC-кросскомпиляторы для ARM v6 и LLVM, это вдвойне странно. Но делать нечего, и из этого следует, что первое, что нам понадобится – это Mac OS X. Не надо сразу посыпать голову пеплом и бежать за макбуком в ближайший магазин, все не так плохо. К счастью, на свете не первый год существует инициатива OSX86, задачами которой является установка Mac OS X на обычные PC (с разной долей успеха), и превращение их в так называемые «hackintosh’и». Решение это вполне жизнеспособно, я, к примеру, активно использую его на ноутбуке, у которого в процессоре даже нет SSE3, благо ядро XNU/Voodoo позволяет решать даже такие проблемы.

Следующее, что нам понадобится, это xCode – по сути, IDE в виде фронтэнда для GCC. Лучше качать его сразу с интегрированным iPhone SDK, в таком виде его раздают на сайте Apple после бесплатной регистрации (надо принять во внимание, что они потом будут тебя спамить, так что рабочий адрес лучше не указывать). Если кто-то переживает насчет покупки сертификатов разработчика, спешу успокоить – для наших целей вполне хватит симулятора, если нет возможности заливать собранное приложение на настоящую железку, это совсем не страшно.

 

Генерируем шаблон GLES-приложения

Итак, обладатели хакинтошей отплясали с бубнами, xCode запустился, и мы, наконец, приступаем. Воспользуемся тем, что IDE умеет генерить различные шаблоны, в том числе и GLES-приложений, – это избавит нас от некоторого объема рутинной работы. Создаем Project –> New Project –> iPhone OS –> Application –> OpenGL ES Application –> Choose и получаем простенькое рабочее приложение, демонстрирующее нам квадратик с градиентной заливкой.

Здесь наблюдаются даже зачатки архитектуры с выделенным рендерером. В принципе, кроме метода render одного из ESRenderer’ов, на практике нас ничего не интересует, но все же предлагаю быстро просмотреть иерархию созданного проекта. Управление основным приложением располагается в интерфейсе AppDelegate, здесь живут обработчики запуска/приостановки/завершения приложения и им подобные. Тут же в главное окно встраивается GLES’ная вьюшка EAGLView, создающая анимированный OpenGL контекст, а также принимающая в себя тычки мышкой… простите, пальцами. Непосредственно алгоритмы отображения графики вынесены в интерфейс ESRenderer, в котором мы и будем вести разработку. Я, как мог, попытался изобразить описанное на диаграмме.

 

Модификация шаблона

Теперь настала пора причесать проект для удобства дальнейшей разработки. Сначала я удалил абстрактный интерфейс ESRenderer.h, реализацию ES2Renderer.m/h и папку Shaders, ибо нам сейчас все эти навороты без надобности. ES1Renderer я сделал наследником NSObject, так как интерфейса ESRenderer больше не стало. В методе EAGLView::initWithCoder я также оставил только один рендерер безо всяких выкрутасов : renderer = [[ES1Renderer alloc] init].

Мусор мы подчистили, но это не все. Не знаю, кто как, а лично я не в восторге от Objective-C и постоянно испытываю желание написать класс-другой на C++. К счастью, это вполне возможно.

Не знаю, кто как, а лично я не в восторге от Objective-C и постоянно испытываю желание написать класс-другой на C++. К счастью, это вполне возможно

Переименуем всем нашим файлам расширение .m в .mm – это позволит нам микшировать код на Objective-C и C++ без ограничений, чем мы тут же и займемся – добавим в проект файлы Cube.mm и Cube.h и реализуем в них безумно сложный синглтон:

class GLCube {
public :
static GLCube * getInstance();
static void destroyInstance();
void render();
private :
GLCube();
~GLCube();
static GLCube *_internal_instance;
};

Теперь примемся за ES1Renderer::render. Системные тонкости вроде биндинга фреймбуферов там уже сгенерены до нас, и трогать это мы не будем. Уберем исходную инициализацию OpenGL-окружения, которая там прописана, и впишем нормальную, 3D’шную, с Z-бефером и прочими радостями. Я сделал проекцию 60-градусного поля зрения через glFrustumf :

glMatrixMode(GL_PROJECTION);
glEnable(GL_DEPTH_TEST);
glEnable(GL_CULL_FACE);
glFrustumf(...);
glViewport(0, 0, backingWidth, backingHeight);

Теперь уберем алгоритмы рисования разноцветного квадрата и прикрутим наш будущий C++’ный куб:

GLCube::getInstance()->render();

После вышеозначенных действий, если все было проделано правильно, наше хозяйство должно собираться.

 

Кубик

Вот и настала пора писать непосредственно алгоритм отрисовки нашего кубика. Основное отличие от классического подхода состоит в том, что в ES не используются блоки вида glBegin/glEnd, содержащие повертексные вызовы; вместо этого необходимые данные складываются в массивы, которые обрабатываются векторными вызовами, сразу по несколько вертексов за раз. Также в ES не используются примитивы сложнее треугольников во избежание сложностей с неплоскими полигонами.

Итак, создаем в GLCube::render массив из 72 значений и отрисовываем его проходами по 12 (3 координаты на 4 вертекса – одна сторона куба):

static const GLfloat verts[] = {...};
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glLoadIdentity();
glTranslatef(0.0f,0.0f,-4.0f);
glEnableClientState(GL_VERTEX_ARRAY);
for (size_t i=0; i<6; ++i)
{
glVertexPointer(3, GL_FLOAT, 0, verts + i*12);
glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
}

После этого должны появится очертания нашего кубика.

 

Да будет свет

Если честно, полученный в предыдущем шаге результат не особо впечатляет. Но его можно достаточно легко украсить, используя освещение. Делается это так же, как и в «большом» OpenGL. Идем в ES1Renderer::render, и в том месте, где происходит инициализация OpenGL-окружения, прикручиваем источник света. Для начала разрешаем освещение и добавляем одну лампочку:

glEnable(GL_LIGHTING);
glEnable(GL_LIGHT0);

Теперь зададим свойства освещаемого материала со всеми блестючестями и прочими красотами (инициализацию массивов я опустил для упрощения):

glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, matAmbient);
glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, matDiffuse);
glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, matSpecular);
glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, lightShininess);

Разместим и сконфигурируем лампочку:

glLightfv(GL_LIGHT0, GL_AMBIENT, lightAmbient);
glLightfv(GL_LIGHT0, GL_DIFFUSE, lightDiffuse);
glLightfv(GL_LIGHT0, GL_POSITION, lightPosition);

И не забудем, что грани нашего куба должны быть четко видны, а не сглажены:

glShadeModel(GL_FLAT);

Результатом проделанных немудреных действий станет то, что наш кубик приобретет ощутимый объем.

 

Текстуры

Несмотря на приобретенный объем, куб все равно выглядит скучновато. Появляется желание его разрисовать. В OpenGL это принято делать посредством текстур. Текстуры принято хранить в картинках. Картинки будут жить в ресурсах приложения, я загружать их мы будем с помощью класса UIImage из iPhone SDK. Я взял логотип небезызвестного журнала, привел его к размеру 256x256 (размерность в степенях двойки – необходимое условие для OpenGL текстур) и добавил в проект. Наш класс GLCube расширился методом GLCube::loadTexture(const char *tex_name), в котором мы разберем наиболее интересные моменты.

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

NSString* nsTexName = [[NSBundle mainBundle] pathForResource: [NSString stringWithUTF8String:tex_name] ofType:nil];
UIImage* uiImage = [UIImage imageWithContentsOfFile:nsTexName];
CGImageRef spriteImage = uiImage.CGImage;

На данном этапе мы впервые встретились с функциями CG* из семейства CoreGraphics. Поскольку CoreGraphics Framework не включен в наш проект по умолчанию, необходимо сделать это самостоятельно, иначе ничего не соберется. Данный фреймворк поставляется вместе с iPhone SDK, просто нужно добавить его в папочку Frameworks в проекте.

Далее необходимо подготовить буфер, из которого мы потом сформируем текстуру. В моей картинке используется RGBA модель, по байту на компонент, поэтому я умножаю размерность на 4:

int tex_width = CGImageGetWidth(spriteImage);
int tex_height = CGImageGetHeight(spriteImage);
GLubyte *spriteData = (GLubyte *) malloc(tex_width * tex_height * 4);

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

CGContextRef spriteContext = CGBitmapContextCreate(spriteData, tex_width, tex_height, 8, tex_width * 4, CGImageGetColorSpace(spriteImage), kCGImageAlphaPremultipliedLast);
UIGraphicsPushContext(spriteContext);
[uiImage drawInRect:CGRectMake(0, 0, tex_width, tex_height)];
UIGraphicsPopContext();
CGContextRelease(spriteContext);

После этого мы формируем из буфера spriteData OpenGL-текстуру абсолютно стандартным методом и помещаем ее идентификатор в атрибут GLCube::tex_id для дальнейшего использования.

Теперь настало время использовать загруженную текстуру. Для начала идем в ES1Renderer::render и разрешаем текстуры в инициализации OpenGL-окружения:

glEnable(GL_TEXTURE_2D);

Текстурные координаты передаются в конвейер тоже векторными вызовами, мы будем отдавать пачками по 8 (2 координаты на 4 вертекса). Дополним метод GLCube::render:

static const GLfloat texCoords[] = {...};
...
glBindTexture(GL_TEXTURE_2D, tex_id);
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
...
glTexCoordPointer(2, GL_FLOAT, 0, texCoords + i*8);

И получаем в награду за наши труды изумительной красоты текстурированный кубик!

 

Интерактив

Мне кажется, с накручиванием графической составляющей уже пора остановиться, пока мы не переплюнули движок id tech 4. Статичный кубик наводил на меня уныние, и я решил, что раз у нас есть устройство с сенсорным экраном, то должна быть возможность крутить кубик пальцами. И без промедления я начал с добавления в класс GLCube углов поворота ang_x и ang_y, завернутых в акцессор incrementAngles. В том же классе, в методе render, эти углы были использованы:

glRotatef(ang_x, 0.0f, 1.0f, 0.0f);
glRotatef(ang_y, 1.0f, 0.0f, 0.0f);

Теперь осталось только прикрутить отслеживание тычков пальцами. Они приходят к наследнику UIView, в нашем случае это интерфейс EAGLView, так что мы идем в EAGLView.mm и начинаем реализовывать там необходимые методы.

Прикосновение к экрану – запоминаем последний палец из мультитача, как начальную позицию:

- (void)touchesBegan:(NSSet)touches withEvent:(UIEvent)event {
for (UITouch *touch in touches) {
last_touch_x = [touch locationInView:self].x;
last_touch_y = [touch locationInView:self].y;
}
}

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

(void)touchesMoved:(NSSet)touches withEvent:(UIEvent)event {
for (UITouch touch in touches) {
int delta_x = [touch locationInView:self].x - last_touch_x;
int delta_y = [touch locationInView:self].y - last_touch_y;
GLCube::getInstance()->incrementAngles(180.0f
delta_x/320.0f, 180.0f*delta_y/480.0f);
last_touch_x = [touch locationInView:self].x;
last_touch_y = [touch locationInView:self].y;
}
}

Отрыв пальца от экрана и его выход за границы экрана для нашего алгоритма будет являться просто последним сдвигом:

(void)touchesEnded:(NSSet)touches withEvent:(UIEvent)event { [self touchesMoved:touches withEvent:event]; }
(void)touchesCancelled:(NSSet)touches withEvent:(UIEvent)event { [self touchesMoved:touches withEvent:event]; }

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

 

Tips & Tricks

Чтобы наше творение не выглядело, как наколенная поделка, я считаю, ему необходимо придать лоск.

  1. Создаем файл Default.png размером 320x480 и добавляем его в корень проекта. Теперь картинка из этого файла будет служить splash-screen’ом, пока приложение стартует;
  2. Создаем файл Icon.png размером 57x57 и добавляем его в ресурсы проекта. Находим в папке Resources манифест с расширением *.plist, выбираем, в поле «Icon file» вписываем Icon.png. Теперь у нашего приложения есть достойная иконка;
  3. Добавляем в тот же манифест строку с названием «Status bar is initially hidden» и ставим галку напротив. Теперь наше приложение будет полноэкранным.
    Вот и все. Кодинг окончен! Запускай и развлекайся.
 

CD

На диске ты найдешь исходный код описываемого приложения.

 

WWW

 

INFO

Понимание основ в разработке графических приложений – востребованное качество для AppStore-издателя, который будет брать тебя на работу.

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    0 комментариев
    Межтекстовые Отзывы
    Посмотреть все комментарии