
Недавно я столкнулся с необходимостью портировать под iPad одну из своих программ использующую Ogre3d. Немного разобравшись в теме, хочу поделится полученным опытом.
Мы с Вами попробуем написать приложение отображающее простую 3d сцену, а камерой на сцене будем управлять возя пальцами по экрану iOS устройства.
Кроме желания что нибудь сделать нам понадобиться компьютер с установленной MacOS X и iOS SDK. Так же понадобится Ogre SDK для iOS, получить его можно двумя способами: взять готовое или собрать самостоятельно из исходников.
Способ первый: готовое SDK
Скачиваем OGRE 1.7.3 SDK for iPhone с официального сайта Ogre.
Оттуда же скачиваем iPhone Precompiled Dependencies. На всякий случай вот ссылка на sourceforge, качать Ogre iOS Dependencies Source.dmg. Там же есть
Ogre_Xcode4_Templates_20110616.pkg.zip, это шаблон для XCode позволяющий создавать каркас Ogre приложения без лишних телодвижений.
Теперь у нас есть все необходимое, дважды щелкаем по OgreSDK_iOS_v1-7-3.dmg и из появившегося окошка с изображением айфона перетаскиваем папочку OgreSDK в свою рабочую директорию, у меня это /Users/max/Desktop/Projects/ogre. Теперь открываем Ogre iOS Dependencies Source.dmg и копируем папку iPhoneDependencies в OgreSDK. Все готово! В FInder дважды щелкаем по OGRE.xcodeproj, в открывшемся окне XCode выбираем в качестве активной схемы SampleBrowser | iPhone 4.3 Simulator.

Давим на кнопку Run, и через некоторое время видим запущенный в эмуляторе SampleBrowser.

Если все получилось, нетерпеливые могут установить шаблон из Ogre_Xcode4_Templates_20110616.pkg.zip и посмотреть на вполне работоспособный каркас приложения который создает этот шаблон.
SampleBrowser не адаптирован под управление пальцами, поэтому чтобы посмотреть примеры придется немножко попотеть. Библиотеки зависимостей и самого огра собраны в универсальные библиотеки под i386 и arm, поэтому имея подписку разработчика чтобы запустить примеры на реальном устройстве достаточно сменить активную схему на подключенное к компьютеру устройство и настроить в проекте CodeSigning.
Способ второй: сборка из исходников
Собрать огр из исходников не на много сложнее.
Понадобится OGRE 1.7.3 Source for Linux / OSX с офф. сайта, все тот же Ogre iOS Dependencies Source и CMake. Здесь я не буду подробно останавливаться на том как использовать CMake, а буду считать, что те кто любит собирать все самостоятельно это уже умеют или легко научатся.
Распаковываем исходники, внутрь копируем iPhoneDependencies, запускаем cmake-gui и натравливаем его на них. Указываем директорию назначения, жмем Configure и говорим CMake что хотим получить проект под XCode. Остается только поставить галочку напротив OGRE_BUILD_PLATFORM_IPHONE и сгенерировать проект.

Все дальнейшие действия аналогичны первому способу. Открываем OGRE.xcodeproj в XCode, выбираем схему и жмем Run. Спустя довольно продолжительное время (на моем компьютере около часа) мы должны увидеть запущенный SampleBrowser.
Здесь я хочу обратить внимание на то, что собранные библиотеки не будут универсальными (мультиплатформенными). При желании можно собрать либы под эмулятор, скопировать их куда нибудь, а затем собрать под устройство и объединить их в универсальные с помощью утилиты lipo. Готовых скрипов я для этого не нашел (плохо смотрел?), но возможно кто нибудь знает как легко и просто собрать такие либы и поделится с нами этим знанием.
Минимальное приложение
Итак, SDK у нас есть, примеры мы посмотрели и теперь готовы написать что нибудь самостоятельно. Чтобы лучше понять как это все работает не будем использовать шаблон огр приложения, а напишем все сами, своими ручками, а в каркас генерируемый шаблоном будем только подглядывать.
Библиотека Object Oriented Input System (OIS) в огре хоть и имеет в своем составе класс с названием MultiTouch, но на момент написания этой статьи поддерживает только одно касание, поэтому события мультитач мы будем получать самостоятельно.
Запускаем XCode, создаем новый проект на основе шаблона View-based Application для iOS и назовем его OgreMinimalApp. В нашем приложении мы будем использовать умопомрачительную связку из C++ и Objective-C поэтому сразу переименуем все файлы с расширением .m в .mm, иначе компилятор нас не поймет.
Теперь создадим в нашем проекте два файла MyOgreApp.h и MyOgreApp.mm и наполним их следующим содержанием:
MyOgreApp.h
- #ifndef _MY_OGRE_APP_
- #define _MY_OGRE_APP_
-
- #include "Ogre.h"
-
- #define OGRE_STATIC_GLES
- #include "OgreStaticPluginLoader.h"
-
- #include <OISMultiTouch.h>
- #include <SdkTrays.h>
-
- class MyOgreApp : public Ogre::FrameListener
- {
- public:
- MyOgreApp();
- ~MyOgreApp();
-
- void start();
- void renderOneFrame();
-
- void touchesMoved( int count, float x1, float y1, float x2, float y2 );
- void touchesBegan( int count, float x1, float y1, float x2, float y2 );
- void touchesEnded( int count );
-
- bool frameRenderingQueued( const Ogre::FrameEvent &evt );
-
- private:
- void initOgre();
- float calcDistance( float x1, float y1, float x2, float y2 );
- void updateCamera();
-
- private:
- std::string m_resourcePath;
-
- Ogre::Root *m_pRoot;
- Ogre::StaticPluginLoader m_StaticPluginLoader;
- Ogre::RenderWindow *m_pRenderWindow;
- Ogre::SceneManager *m_pSceneManager;
- Ogre::Camera *m_pCamera;
- Ogre::Viewport *m_pViewport;
-
- OgreBites::SdkTrayManager *m_pTrayManager;
-
- Ogre::SceneNode *m_pHeadNode;
- Ogre::Entity *m_pHeadEntity;
-
- float m_prevX;
- float m_prevY;
- float m_prevDistance;
- float m_currentTouchesCount;
-
- float m_camPosAlphaAngle;
- float m_camPosFiAngle;
- float m_camTargetRadius;
- };
-
- #endif // _MY_OGRE_APP_
* This source code was highlighted with Source Code Highlighter.
MyOgreApp.mm
- #include "MyOgreApp.h"
-
- using namespace Ogre;
-
- MyOgreApp::MyOgreApp()
- {
- m_resourcePath = [[[NSBundle mainBundle] resourcePath] cStringUsingEncoding:NSASCIIStringEncoding] + std::string( "/" );
- m_currentTouchesCount = 0;
- m_pTrayManager = 0;
- m_camPosAlphaAngle = 0.0f;
- m_camPosFiAngle = 45.0f;
- m_camTargetRadius = 500.0f;
- }
-
- MyOgreApp::~MyOgreApp()
- {
- if( m_pTrayManager )
- delete m_pTrayManager;
-
- m_StaticPluginLoader.unload();
-
- if( m_pRoot )
- delete m_pRoot;
- }
-
- void MyOgreApp::start()
- {
- initOgre();
-
- m_pSceneManager->setSkyBox( true, "Examples/SpaceSkyBox" );
- m_pSceneManager->createLight( "Light" )->setPosition( 75, 75, 75 );
- m_pHeadEntity = m_pSceneManager->createEntity( "OgreHead", "ogrehead.mesh" );
- m_pHeadNode = m_pSceneManager->getRootSceneNode()->createChildSceneNode( "HeadNode" );
- m_pHeadNode->attachObject( m_pHeadEntity );
- }
-
- void MyOgreApp::initOgre()
- {
- m_pRoot = new Root( "", m_resourcePath + "ogre.cfg" );
- m_StaticPluginLoader.load();
-
- m_pRenderWindow = m_pRoot->initialise( true, "MyOgreApp" );
-
- m_pSceneManager = m_pRoot->createSceneManager( ST_GENERIC, "SceneManager" );
- m_pSceneManager->setAmbientLight( ColourValue( 0.7f, 0.7f, 0.7f ) );
-
- m_pCamera = m_pSceneManager->createCamera( "Camera" );
- m_pCamera->setNearClipDistance( 1.0 );
-
- m_pViewport = m_pRenderWindow->addViewport( m_pCamera );
- m_pViewport->setBackgroundColour( ColourValue( 0.58f, 0.65f, 0.76f, 1.0f ) );
-
- m_pCamera->setAspectRatio( Real( m_pViewport->getActualWidth() ) / Real( m_pViewport->getActualHeight() ) );
- m_pViewport->setCamera( m_pCamera );
-
- m_pRoot->addFrameListener( this );
-
- Ogre::String secName, typeName, archName;
- Ogre::ConfigFile cf;
- cf.load( m_resourcePath + "resources.cfg" );
-
- Ogre::ConfigFile::SectionIterator seci = cf.getSectionIterator();
- while( seci.hasMoreElements() )
- {
- secName = seci.peekNextKey();
- Ogre::ConfigFile::SettingsMultiMap *settings = seci.getNext();
- Ogre::ConfigFile::SettingsMultiMap::iterator i;
- for( i = settings->begin(); i != settings->end(); ++i )
- {
- typeName = i->first;
- archName = i->second;
- if( !Ogre::StringUtil::startsWith( archName, "/", false) )
- archName = Ogre::String( m_resourcePath + archName );
-
- Ogre::ResourceGroupManager::getSingleton().addResourceLocation( archName, typeName, secName );
- }
- }
-
- Ogre::TextureManager::getSingleton().setDefaultNumMipmaps( 5 );
- Ogre::ResourceGroupManager::getSingleton().initialiseAllResourceGroups();
-
- m_pTrayManager = new OgreBites::SdkTrayManager( "TrayManager", m_pRenderWindow, 0 );
- m_pTrayManager->showFrameStats( OgreBites::TL_BOTTOMLEFT );
- m_pTrayManager->showLogo( OgreBites::TL_BOTTOMRIGHT );
- m_pTrayManager->hideCursor();
-
- m_pRenderWindow->setActive( true );
-
- updateCamera();
- }
-
- void MyOgreApp::renderOneFrame()
- {
- m_pRoot->renderOneFrame();
- }
-
- bool MyOgreApp::frameRenderingQueued( const FrameEvent &evt )
- {
- m_pTrayManager->frameRenderingQueued( evt );
- return true;
- }
-
- void MyOgreApp::touchesMoved( int count, float x1, float y1, float x2, float y2 )
- {
- float relX = x1 - m_prevX;
- float relY = y1 - m_prevY;
-
- m_prevX = x1;
- m_prevY = y1;
-
- if ( m_currentTouchesCount == 1 )
- {
- m_camPosAlphaAngle += relY * 0.45f;
- m_camPosFiAngle += relX * 0.45f;
- }
- else if( m_currentTouchesCount == 2 )
- {
- float currDist = calcDistance( x1, y1, x2, y2 );
- if( abs( m_prevDistance - currDist ) > 5.0 )
- {
- if( m_prevDistance > currDist )
- m_camTargetRadius += 10.0;
- else
- m_camTargetRadius -= 10.0;
- }
-
- m_prevDistance = currDist;
- }
-
- updateCamera();
- }
-
- void MyOgreApp::touchesBegan( int count, float x1, float y1, float x2, float y2 )
- {
- m_prevX = x1;
- m_prevY = y1;
-
- m_currentTouchesCount += count;
-
- if( m_currentTouchesCount == 2 )
- m_prevDistance = calcDistance( x1, y1, x2, y2 );
- }
-
- void MyOgreApp::touchesEnded( int count )
- {
- m_currentTouchesCount -= count;
- if( m_currentTouchesCount < 0)
- m_currentTouchesCount = 0;
- }
-
- float MyOgreApp::calcDistance( float x1, float y1, float x2, float y2 )
- {
- return sqrt( (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1) );
- }
-
- void MyOgreApp::updateCamera()
- {
- if( m_camPosFiAngle > 179.0f )
- m_camPosFiAngle = 179.0f;
- else if( m_camPosFiAngle < -179.0f )
- m_camPosFiAngle = -179.0f;
- if( m_camTargetRadius < 1.0f )
- m_camTargetRadius = 1.0f;
-
-
- Ogre::Vector3 camPos;
- camPos.x = m_camTargetRadius * sin( m_camPosAlphaAngle * Math::PI / 360.0f ) * cos( m_camPosFiAngle * Math::PI / 360.0f );
- camPos.y = m_camTargetRadius * sin( m_camPosFiAngle * Math::PI / 360.0f );
- camPos.z = m_camTargetRadius * cos( m_camPosAlphaAngle * Math::PI / 360.0f ) * cos( m_camPosFiAngle * Math::PI / 360.0f );
-
- m_pCamera->setPosition( camPos );
- m_pCamera->lookAt( Vector3::ZERO );
- }
* This source code was highlighted with Source Code Highlighter.
Мы создали класс наследник Ogre::FrameListener в котором будем производить все жизненно важные для нашего приложения действия: инициализацию огра, создание сцены и управление камерой.
Код очень простой и все должно быть понятно, я только кратко пробегусь по основным моментам.
Для того чтобы огр смог загрузить файлы ресурсов мы должны узнать где они лежат, сделаем это в конструкторе.
Вся инициализация у нас будет в методе initOgre(): сначала создадим объект Ogre::Root, первый параметр оставим пустой строкой так как плагины у нас статические, теперь можно «загрузить» сами плагины, об этом мы попросим Ogre::StaticPluginLoader, в MyOgreApp.h у нас определен макрос OGRE_STATIC_GLES благодаря которому StaticPluginLoader знает что нам нужен только плагин рендэра GL ES. Если понадобятся другие плагины, то нужно будет определить соответствующие им макросы. Все дальнейшие действия стандартны.
Камеру будем двигать по поверхности воображаемого шара, центр которого находится в точке с координатами (0,0,0) на сцене. Перемещать и ориентировать камеру будем в методе updateCamera(), в зависимости от углов альфа и фи (углы между вертикальной / горизонтальной плоскостью и точкой задающей положение камеры в пространстве) и радиуса шара. Mетод touchesMoved получает экранные координаты в которых произошло касание и меняет альфу и фи если касание одно, радиус если касаний два. touchesBegan и touchesEnded подсчитывает количество касаний. Можно немного доработать и при трех касаниях перемещать центр шара.
Теперь в OgreMinimalAppViewController создадим экземпляр нашего класса, будем передавать ему события тач, а по таймеру вызывать метод renderOneFrame.
OgreMinimalAppViewController.h
- #ifndef _OGRE_MINIMAL_APP_VIEW_CONTROLLER_
- #define _OGRE_MINIMAL_APP_VIEW_CONTROLLER_
-
- #import <UIKit/UIKit.h>
-
- #include "MyOgreApp.h"
-
-
- @interface OgreMinimalAppViewController : UIViewController
- {
- MyOgreApp myApp;
- NSTimer *timer;
- }
-
- - (void)go;
- - (void)renderOneFrame:(id)sender;
-
- @property (retain) NSTimer *timer;
-
- @end
-
- #endif // MINIMAL_APP_VIEW_CONTROLLER_
* This source code was highlighted with Source Code Highlighter.
OgreMinimalAppViewController.mm
- #include "OgreMinimalAppViewController.h"
-
- @implementation OgreMinimalAppViewController
-
- @synthesize timer;
-
- - (void) go
- {
- try
- {
- myApp.start();
- timer = [NSTimer scheduledTimerWithTimeInterval:(NSTimeInterval)(1.0f / 60.0f) target:self selector:@selector(renderOneFrame:) userInfo:nil repeats:YES];
- }
- catch( Ogre::Exception& e )
- {
- std::cerr << "An exception has occurred: " << e.getFullDescription().c_str() << std::endl;
- }
- }
-
- - (void)renderOneFrame:(id)sender
- {
- myApp.renderOneFrame();
- }
-
- - (void) touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
- {
- NSUInteger touchCount = [touches count];
- UITouch *touch = [[touches allObjects] objectAtIndex:0];
- CGPoint point1 = [touch locationInView:[self view]];
-
- if( touchCount == 2 )
- {
- UITouch *touch2= [[touches allObjects] objectAtIndex:1];
- CGPoint point2 = [touch2 locationInView:[self view]];
- myApp.touchesMoved( touchCount, point1.x, point1.y, point2.x, point2.y );
- }
- else
- myApp.touchesMoved( touchCount, point1.x, point1.y, 0.0f, 0.0f );
- }
-
- - (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
- {
- NSUInteger touchCount = [touches count];
- UITouch *touch1 = [[touches allObjects] objectAtIndex:0];
- CGPoint point1 = [touch1 locationInView:[self view]];
-
- if( touchCount == 2 )
- {
- UITouch *touch2= [[touches allObjects] objectAtIndex:1];
- CGPoint point2 = [touch2 locationInView:[self view]];
- myApp.touchesBegan( touchCount, point1.x, point1.y, point2.x, point2.y );
- }
- else
- myApp.touchesBegan( touchCount, point1.x, point1.y, 0.0f, 0.0f );
- }
-
- - (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
- {
- NSUInteger touchCount = [touches count];
- myApp.touchesEnded( touchCount );
- }
-
- - (void)dealloc
- {
- [super dealloc];
- }
-
- - (void)didReceiveMemoryWarning
- {
- // Releases the view if it doesn't have a superview.
- [super didReceiveMemoryWarning];
-
- // Release any cached data, images, etc that aren't in use.
- }
-
- #pragma mark - View lifecycle
-
-
- // Implement viewDidLoad to do additional setup after loading the view, typically from a nib.
- - (void)viewDidLoad
- {
- [super viewDidLoad];
-
- [[UIApplication sharedApplication] setStatusBarHidden:YES];
-
- timer = nil;
-
- [self go];
- }
-
-
- - (void)viewDidUnload
- {
- Ogre::Root::getSingleton().queueEndRendering();
-
- [timer invalidate];
- timer = nil;
-
- [super viewDidUnload];
- }
-
- - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
- {
- return NO;
- }
-
- @end
* This source code was highlighted with Source Code Highlighter.
Заглянем в MainWindow.xib и установим объекту Window свойство Alpha в 0,02 чтобы сквозь него было видно что там рисует огр (если поставить меньше то перестают приходить события тач, кто знает почему подскажите), и за одно поставим галочку напротив Multiple Touch, нам нужно больше одного касания. На этом наше приложение закончено осталось только добавить файлы ресурсов и настроить проект так чтобы компилятор знал где лежат заголовочные файлы и библиотеки необходимые при компиляции.
Чтобы не собирать все необходимые ресурсы вручную можно создать проект из Ogre_Xcode4_Templates, в нем будет папка media со всем необходимым для нас, ее мы и добавим в наш проект, а так же файлы ogre.cfg и resources.cfg.
Во фрэймворки нужно добавить OpenGLES.framework и QuartzCore.framework.
Теперь идем в TARGETS->Build Settings, в секции Linking/Other Linker Flags пишем:
-lOIS -lOgreMainStatic -lRenderSystem_GLESStatic -lboost_thread -lFreeType -lFreeImage -lzzip -lz
В секции Search Paths/Header Search Paths:
"/Users/max/Desktop/Projects/ogre/OgreSDK//include/OGRE" "/Users/max/Desktop/Projects/ogre/OgreSDK//include/OGRE/RenderSystems/GLES" "/Users/max/Desktop/Projects/ogre/OgreSDK//include/OGRE/iPhone" "/Users/max/Desktop/Projects/ogre/OgreSDK//include" "/Users/max/Desktop/Projects/ogre/OgreSDK//include/OIS" "/Users/max/Desktop/Projects/ogre/OgreSDK//iPhoneDependencies/include"
ВАЖНО: это пути к заголовочным файлам на моем компьютере, Вы должны исправить их на свои. Правильнее будет завести переменную окружения и вписать относительные пути.
То же самое делаем для Search Paths/Library Search Paths:
"/Users/max/Desktop/Projects/ogre/OgreSDK//lib/Release" "/Users/max/Desktop/Projects/ogre/OgreSDK//iPhoneDependencies/lib/release" "/Users/max/Desktop/Projects/ogre/OgreSDK//iPhoneDependencies/lib"
Ну и последнее CodeGeneration / Compile for Thumb ставим в No.
Жмем заветную кнопку Run, наше приложение должно успешно скомпилироваться и запустится.
Надеюсь у Вас все получилось, скриншот того, что получилось у меня в начале статьи.