Реализация для Symbian 9 S60 (Nokia) и UIQ3 (Sony Ericsson) под Open GL EX на С++

Реализация для Symbian 9 S60 (Nokia) и UIQ3 (Sony Ericsson) под Open GL EX на С++

Источник: MGDC
Автор: Ребрин Виталий Владимирович (Vito)

Приветствую Вас читатель. Перед публикацией второго
урока, из цикла посвященного трехмерной графике, я решил закончить
первый урок.
В этой статье будет рассмотрена реализация для Symbian 9 S60 (Nokia) и
UIQ3 (Sony Ericsson) под open GL EX на С++. Также для сравнения
реализация под Windows Mobile 6 под DX на С++ и С#.

Image

Первое с чего необходимо начать, это что необходимо скачать и установить.

Вам понадобиться Nokia S60 SDK 3 rd (скачать с сайта
Nokia), UIO3 SDK (обратите внимание не 3.1). Скачать можно аналогично с
сайта компании.

Второе – необходимо определиться со средой
разработки. Конечно можно писать и в блокноте, но я не сторонник
подобного подхода. Я предпочитаю Visual Studio 2005. Для работы
необходимо скачать и установит плагин Carbide.vs. Так же мне очень
понравилась сама среда Carbide.c++. Поэтому разработку будем вести в
этих двух IDE. Впрочем, читатель может сам выбрать понравившуюся среду.

Часть 2. Немного общих слов.

Начнем мы с Symbian. Целью этих уроков не является
обучение программированию под эту операционную систему. Для этого
готовится специальный цикл статей. Я дам лишь общие необходимые
понятия, и тщательно рассмотрим реализацию 3D.

Несколько общих слов о этой ОС. О ее
предыстории вы можете почитать на множестве сайтов. Поэтому не будем
повторяться. Что представляет из себя эта ОС на сегодняшний день? Это
очень гибкая, стабильная, производительна ОС. Она является основным
конкурентом WM(Windows Mobile). S (Symbian) на данном этапе превосходит
по популярности WM, которая по сути является урезанной версией Windows.
Но.
WM не позволяет запускать «десктопные» приложения. А S уже практически
не в чем не уступает WM и продолжает развиваться, в то время как WM
практически мало изменилась. На данном этапе S номинируется как ОС для
смартфоноф, а WM для КПК. Но стандартов нет. И деление очень условно.

И вот на рынок вышел новый игрок – Apple. Который
предлагает мобильную платформу с полноценной ОС, удобным вводом.
Теоретически ничто не мешает запускать на таком «телефоне» приложения
для больших ПК. Но о iphone в другой статье.

Часть 3. Приступаем к работе.

Пример приложения для S60 вы можете скачать отсюда. Итак вы открыли проект, скомпилировали его, посмотрели результат. Теперь рассмотрим подробнее.

1. Проект делался максимально возможно «просто».
Минимум кода и файлов. Тем не менее этот шаблон мы будем использовать
для нескольких последующих уроков. Потом мы будем использовать
«оболочку» которая значительно облегчит разработку и портирование
приложения.

2. В проекте два основных файла, на которые вам следует обратить внимание.
“Lesson1S60.h” и “Lesson1S60.cpp”. Весь код собран в этих уроках.
Коротко рассмотрим их структуру. Существуют четыре основных класса.

Application (CLessonApplication) – определяет приложение:
Document(CLessonTriDocument) – определяет данные приложения.
View (CViewWindow) – вывод на экран.
Ui (CLessonTriAppUi) – пользовательский интерфейс.

На этом пожалуй и все о структуре приложения. Добавим только, что
это основные классы и они так же присутствуют в реализации S UIQ. А
теперь приступим к рассмотнию OGL (open GL).

OGL это графическая библиотека, а не только 3D. 3D
реализации, это ее часть. Для обеспечения плавности вывода используются
буферы. Передний и задний. В то время как передний выводится на экран,
в заднем буфере происходит создание изображения. Когда изображение
создано, указатель просто переключается( впрочем это традиционный
подход).
Реализация для мобильных платформ является адаптированной версией и во
многом урезанной. Все нацелено на максимальную производительность.

Для работы конвейера OGL необходим цикл. Мы не будем
подробно рассматривать работу конвейера. Потому как я думаю на данном
этапе это и не интересно и существует много источников, где о этом
можно прочитать.
Абстрагируясь, этот цикл можно представить как создание изображения и
вывод его на экран. Цикл этот можно организовать по разному, все
зависит от задачи. Мы рассмотрим два примера – отрисовка в «свободное»
время ( для S60)и отрисовка по таймеру (UIQ).

3. Цикл отрисовки.


void CLessonTriAppUi::Start()

{

iFrame = 0;

CActiveScheduler::Add(this); // these 4 lines of code tell the OS to start executing the appUI's

TRequestStatus * status = &iStatus; // RunL() method when it has the time

User::RequestComplete(status, 0);

SetActive();

CActiveScheduler::Start();

}

Этот код запускает цикл. Вся отрисовка у нас расположена в функции RunL().

4. Инициализация.

Перед тем как использовать OGL, ее необходимо инициализировать.

Код инициализации расположен в функции.


void CLessonTriAppUi::ConstructL()

Рассмотрим его подробно. Добавим только, что инициализация максимально проста.



/*

Step 1 - Get the default display.

EGL uses the concept of a "display" which in most environments

corresponds to a single physical screen. Since we usually want

to draw to the main screen or only have a single screen to begin

with, we let EGL pick the default display.

Querying other displays is platform specific.

*/

eglDisplay = eglGetDisplay(0);

Получаем заданный по умолчанию дисплей (приношу
извинения за комментарии на английском, статья верстается в основном
для западной аудитории).


/*

Step 2 - Initialize EGL.

EGL has to be initialized with the display obtained in the

previous step. We cannot use other EGL functions except

eglGetDisplay and eglGetError before eglInitialize has been

called.

If we're not interested in the EGL version number we can just

pass NULL for the second and third parameters.

*/

EGLint iMajorVersion, iMinorVersion;

if (eglInitialize(eglDisplay, &iMajorVersion, &iMinorVersion)==EGL_FALSE)

{

_LIT(KInitializeFailed, "eglInitialize failed");

User::Panic( KInitializeFailed, 0 );

}

Собственно сама инициализация.



/*

Step 3 - Specify the required configuration attributes.

An EGL "configuration" describes the pixel format and type of

surfaces that can be used for drawing.

For now we just want to use a 16 bit RGB surface that is a

Window surface, i.e. it will be visible on screen. The list

has to contain key/value pairs, terminated with EGL_NONE.

*/

EGLint pi32ConfigAttribs[3];

pi32ConfigAttribs[0] = EGL_SURFACE_TYPE;

pi32ConfigAttribs[1] = EGL_WINDOW_BIT;

pi32ConfigAttribs[2] = EGL_NONE;

Задаем свойства конфигурации.



/*

Step 4 - Find a config that matches all requirements.

eglChooseConfig provides a list of all available configurations

that meet or exceed the requirements given as the second

argument. In most cases we just want the first config that meets

all criteria, so we can limit the number of configs returned to 1.

*/

int iConfigs;

if (eglChooseConfig(eglDisplay, pi32ConfigAttribs, &eglConfig, 1, &iConfigs) == EGL_FALSE)

{

_LIT( KChooseConfigFailed, "eglChooseConfig failed" );

User::Panic( KChooseConfigFailed, 0 );

}

if ( iConfigs == 0 )

{

// No configs found without antialiasing

_LIT( KNoConfig, "Can't find the requested config." );

User::Panic( KNoConfig, 0 );

}

Ищем нужную конфигурацию. Если такой нет, то выходим.



/*

Step 5 - Create a surface to draw to.

Use the config picked in the previous step and the native window

handle when available to create a window surface. A window surface

is one that will be visible on screen inside the native display (or

fullscreen if there is no windowing system).

Pixmaps and pbuffers are surfaces which only exist in off-screen

memory.

*/

eglSurface = eglCreateWindowSurface(eglDisplay, eglConfig, (NativeWindowType)&(iView->iWin), NULL);
if ( eglSurface == NULL )

{

_LIT(KCreateWindowSurfaceFailed, "eglCreateWindowSurface failed");

User::Panic( KCreateWindowSurfaceFailed, 0 );

}

Создаем поверхность для рисования.


/*

Step 6 - Create a context.

EGL has to create a context for OpenGL ES. Our OpenGL ES resources

like textures will only be valid inside this context

(or shared contexts)

*/

eglContext = eglCreateContext(eglDisplay, eglConfig, NULL, NULL);

if ( eglContext == NULL )

{

_LIT(KCreateContextFailed, "eglCreateContext failed");

User::Panic( KCreateContextFailed, 0 );

}


Создаем контекст рисования.


/*

Step 7 - Bind the context to the current thread and use our

window surface for drawing and reading.

Contexts are bound to a thread. This means you don't have to

worry about other threads and processes interfering with your

OpenGL ES application.

We need to specify a surface that will be the target of all

subsequent drawing operations, and one that will be the source

of read operations. They can be the same surface.

*/

if (eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext)== EGL_FALSE)

{

_LIT(KMakeCurrentFailed, "eglMakeCurrent failed");

User::Panic( KMakeCurrentFailed, 0 );

}

Связываем полученный контекст, с поверхностью для рисования.

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

Рутина осталась позади. Теперь займемся самой сценой.



// Reinitialize viewport and projection.

// Устанавливаем область просмотра - весь экран

glViewport( 0, 0, iScreenWidth, iScreenHeight );

// Recalculate the view frustrum

// Устанавливаем область отсечения ( выбираем матрицу проецирования)

glMatrixMode( GL_PROJECTION );

glLoadIdentity();// Сбрасываем текущую матрицу

GLfloat aspectRatio = (GLfloat)(iScreenWidth) / (GLfloat)(iScreenHeight);

glFrustumf( FRUSTUM_LEFT * aspectRatio, FRUSTUM_RIGHT * aspectRatio,

FRUSTUM_BOTTOM, FRUSTUM_TOP,

FRUSTUM_NEAR, FRUSTUM_FAR );// Область отсечения

glMatrixMode( GL_MODELVIEW );// Матрица моделирования

// Set the screen background color.

glClearColor( 0.f, 0.f, 0.5f, 1.f );

glEnable(GL_DEPTH_TEST); // Enables Depth Testing

glDepthFunc(GL_LEQUAL); // The Type Of Depth Testing To Do

// Set the initial shading mode

// Интерполяция цветов

//glShadeModel( GL_FLAT );

glShadeModel( GL_SMOOTH );

// Do not use perspective correction

// С песпктивной коррекцией

glHint( GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST );

// Enable vertex arrays

glEnableClientState(GL_VERTEX_ARRAY);

// Enable color arrays

glEnableClientState( GL_COLOR_ARRAY);

Эта часть кода, достаточно хорошо закомментирована, поэтому думаю пояснений. Теоретические пояснения см. урок 1 для Java.

Отрисовка сцены.


void CLessonTriAppUi::RunL() //periodically executed function which runs the demo for a frame

{

if (!IsActive())

{

++iFrame;

/*

Clears the color buffer.

glClear() can also be used to clear the depth or stencil buffer

(GL_DEPTH_BUFFER_BIT or GL_STENCIL_BUFFER_BIT)

*/

// Очистим

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

// Animate

glLoadIdentity();// сбросим резултаты предыдущих трансформаций

glTranslatef(-1.5f,0.0f,-10.0f);// сдвинем объект влево и назад (x,z)

glRotatef( iFrame ,0, 1 , 0);// повернем объект

/*

Draw a triangle

*/

// Set array pointers.

// Наши точки - треугольник

glVertexPointer( 3, GL_BYTE, 0, vertices );

// Set color pointers.

// Наш массв цыетов

glColorPointer( 4, GL_UNSIGNED_BYTE, 0, colors );

// Рисуем треугольник

glDrawElements( GL_TRIANGLES, 3, GL_UNSIGNED_BYTE, triangles );

///////////////////////////////////////////////////////////////////////////

glLoadIdentity();

glTranslatef(1.5f,0.0f,-10.0f);

glRotatef( iFrame , 1, 0 ,0);

// Set array pointers.

// Собственно наши точки

glVertexPointer( 3, GL_BYTE, 0, vertices1 );

// Set color pointers.

// Наш массв цветов

glColorPointer( 4, GL_UNSIGNED_BYTE, 0, colors1 );

// Рисуем квадрат

glDrawElements( GL_TRIANGLES, 6, GL_UNSIGNED_BYTE, сuard );

/*

Swap Buffers.

Brings to the native display the current render surface.

*/

eglSwapBuffers(eglDisplay, eglSurface);// переключим буферы

Вот и все. В данном уроке мы не использовали нормалей и работали без освещения. Это аспекты рассмотрим в уроке 2.

Реализацию для UIQ можно скачать отсюда.

Разница в вызове функции отрисовки (как впрочем и другие заголовочные файлы, классы).


/*!********************************************************************

@Function ViewActivatedL

@Description

**********************************************************************/

void CAppView::ViewActivatedL(const TVwsViewId &aPrevViewId, TUid /*
aCustomMessageId */, const TDesC8 & /* aCustomMessage */)

{

SetParentView(aPrevViewId);

m_Periodic = CPeriodic::NewL(CActive::EPriorityIdle);

m_Periodic->Start(100,100,TCallBack(CAppView::DrawCallback,this));

}

Здесь мы через определенный интервал времени вызываем функцию DrawCallback.


/*!********************************************************************

@Function DrawCallback

@Description Called every frame - runs shell code, and user demo

code.

**********************************************************************/

TInt CAppView::DrawCallback(TAny* aInstance){

CAppView* instance = (CAppView*) aInstance;

instance->draw();

// To keep the background light on

if ( !(instance->iFrame%100) )

{

User::ResetInactivityTime();

}

/* Suspend the current thread for a short while. Give some time

to other threads and AOs, avoids the ViewSrv error in ARMI and

THUMB release builds. One may try to decrease the callback

function instead of this. */

// Приостановка текущего процесса

if ( !(instance->iFrame%50) )

{

User::After(0);

}

return 0;

}

Которая вызывает нашу функцию draw();

instance->draw();

Остальное идентично.

Любознательный читатель заметит различия и схожести
реализации двух версий Symbian (не стоит забывать, что это разные
версии S, поэтому названия классов могут отличаться. Но впрочем это
отдельная тема).

Часть 4. Windows Mobile.

Для работы вам необходимо скачать (бесплатно) WM6 SDK. И конечно потребуется Visual Studio.
Итак приступим. Исходный код проекта можно скачать отсюда.
Для программистов под Windows здесь не будет ничего нового. И это конечно плюс WM.

D3D часть библиотеки DX которая позволяет получить
прямой доступ к видео и отвечает за трехмерные реализации. В отличии,
от OGL она использует интерфейсы, т.е опирается на С++, тогда как OGL
написана на С. Получается довольно интересная картина. Программируя под
S мы используем С++ и ООП, а вызов функций OGL в стиле С. С WM ситуация
противоположная. Свое стремительное восхождение DX начала c 8 версии.
Причины этого мы рассмотрим ниже когда разберем код.

1. Цикл.



while( msg.message!=WM_QUIT )

{

if( PeekMessage( &msg, NULL, 0U, 0U, PM_REMOVE ) )

{

TranslateMessage( &msg );

DispatchMessage( &msg );

}

else

Render();

}

Функция отрисовки Render(); вызывается в свободное время. Этот цикл
обработки сообщений хорошо знаком всем Windows программистам.

2. Инициализация.


memset( &d3dmpp, 0, sizeof(d3dmpp) );

d3dmpp.Windowed = TRUE;

d3dmpp.SwapEffect = D3DMSWAPEFFECT_DISCARD;

d3dmpp.BackBufferFormat = D3DMFMT_UNKNOWN;

if( FAILED( g_pD3DM->CreateDevice( uAdapter,

D3DMDEVTYPE_DEFAULT,

hWnd, 0,

&d3dmpp, &g_pd3dmDevice ) ) )

{

OutputDebugString(TEXT("Unable to create a D3DM device.\n"));

return E_FAIL;

}

vreturn S_OK;

}

D3D отличается от OGL. Невозможно в рамках одного урока подробно объяснить ньюансы. Поэтому будем кратки.
Структура D3DMPRESENT_PARAMETERS d3dmpp; отвечает за свойства.


d3dmpp.Windowed = TRUE;

d3dmpp.SwapEffect = D3DMSWAPEFFECT_DISCARD;

d3dmpp.BackBufferFormat = D3DMFMT_UNKNOWN;

Инициализация минимальна. Так же как и в OGL можно сказать она происходит по умолчанию.



if( FAILED( g_pD3DM->CreateDevice( uAdapter,

D3DMDEVTYPE_DEFAULT,

hWnd, 0,

&d3dmpp, &g_pd3dmDevice ) ) )

{

OutputDebugString(TEXT("Unable to create a D3DM device.\n"));

return E_FAIL;

}

Создание устройства, с выбранными параметрами. Пожалуй на данном этапе, это два момента, на кторые следует обратить внимание.

А вот этот момент выделим особо. Он будет важен для сравнения библиотек.



// Create the vertex buffer. Here we are allocating enough memory

// (from the default pool) to hold all our 3 custom vertices. We also

// specify the FVF, so the vertex buffer knows what data it contains.

if( FAILED( g_pd3dmDevice->CreateVertexBuffer( 3*sizeof(CUSTOMVERTEX),

0, D3DMFVF_CUSTOMVERTEX,

pool, &g_pVB ) ) )

{

return E_FAIL;

}

/ Now we fill the vertex buffer. To do this, we need to Lock() the VB to

// gain access to the vertices. This mechanism is required becuase vertex

// buffers may be in device memory.

void* pVertices;

vif( FAILED( g_pVB->Lock( 0, sizeof(vertices), &pVertices, 0 ) ) )

return E_FAIL;

memcpy( pVertices, vertices, sizeof(vertices) );

g_pVB->Unlock();

Здесь мы создаем и заполняем буфер вершин.
Аналогичный по функциональности код для OGL.



// Set array pointers.

// Наши точки - треугольник

glVertexPointer( 3, GL_BYTE, 0, vertices );

Да, всего одна строчка кода.

Область просмотра задается в функции.

void ReSizeScene(int width, int height)

Нарисуем нашу сцену.



VOID Render()

{

if( NULL == g_pd3dmDevice )

return;

UINT iTime = GetTickCount() % 1000000;

FLOAT fAngle = iTime * (2.0f * D3DMX_PI) / 6000.0f;

// Clear the backbuffer to a blue color

g_pd3dmDevice->Clear( 0, NULL, D3DMCLEAR_TARGET, D3DMCOLOR_XRGB(0,0,255), 1.0f, 0 );

// Begin the scene

if( SUCCEEDED( g_pd3dmDevice->BeginScene() ) )

{

D3DMXMatrixIdentity( &matWorld ); // сбрасываем предыдущие трансформации

D3DMXMatrixTranslation(&matWorld,-1.5f,0,1);// смещаем объект влево и назад.

D3DMXMatrixRotationY( &matWorld1, fAngle );// трансоформация матрицы (вращение)

D3DMXMatrixMultiply(&temp,&matWorld1,&matWorld); //умножаем две матрицы (смещения и вращения)

g_pd3dmDevice->SetTransform( D3DMTS_WORLD, (D3DMMATRIX*)
&temp,D3DMFMT_D3DMVALUE_FLOAT ); // применям трнсформацию к мировой матрице

// Render the vertex buffer contents

g_pd3dmDevice->SetStreamSource( 0, g_pVB, sizeof(CUSTOMVERTEX) );// указываем на нужный нам массив вершин

g_pd3dmDevice->DrawPrimitive( D3DMPT_TRIANGLELIST, 0, 1 ); // рисуем треугольник

// далее аналогично

D3DMXMatrixIdentity( &matWorld );

D3DMXMatrixTranslation(&matWorld,1.5f,0,1);

D3DMXMatrixRotationX( &matWorld1, fAngle );

D3DMXMatrixMultiply(&temp,&matWorld1,&matWorld);

g_pd3dmDevice->SetTransform( D3DMTS_WORLD, (D3DMMATRIX*)
&temp,D3DMFMT_D3DMVALUE_FLOAT );

// Render the vertex buffer contents

g_pd3dmDevice->SetStreamSource( 0, g_pVB1, sizeof(CUSTOMVERTEX) );

g_pd3dmDevice->DrawPrimitive( D3DMPT_TRIANGLESTRIP , 0, 2 );

// End the scene

g_pd3dmDevice->EndScene();

}

// Present the backbuffer contents to the display

g_pd3dmDevice->Present( NULL, NULL, NULL, NULL );// переключаем буфера

}

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

Часть 5. С#.

Скачать проект можно отсюда.

Изначально я не планировал реализовывать это на С#,
но любопытство взяло верх. Все таки это новый язык и очень серьезный
конкурент Java. В силу этих причин, рассматривать эту реализацию не
буду. Скажу только, что писать на нем очень приятно. Впрочем, если
будет интерес со стороны читателей, эта тема будет освещаться подробно.

Приведу только функцию рисования.


public void Render()

{

if (device == null)

return;

if (pause)

return;

//Clear the backbuffer to a blue color (ARGB = 000000ff)

device.Clear(ClearFlags.Target | ClearFlags.ZBuffer, System.Drawing.Color.Black, 1.0f, 0);

//Begin the scene

device.BeginScene();

int iTime = Environment.TickCount % 1000000;

float fAngle = iTime * (2.0f * (float)Math.PI) / 6000.0f;

Matrix worldMatrix = Matrix.Translation(new Vector3(-1.5f, 0.0f, 1.0f));// смещаем объект влево и назад.

Matrix matRot = Matrix.RotationY(fAngle);// трансоформация матрицы (вращение)

device.Transform.World = Matrix.Multiply(matRot, worldMatrix);
//умножаем две матрицы (смещения и вращения) и применяем к мировой
матрице

device.SetStreamSource(0, vertexBuffer, 0);

device.DrawPrimitives(PrimitiveType.TriangleStrip, 0, 1);

worldMatrix = Matrix.Identity;

worldMatrix = Matrix.Translation(new Vector3(1.5f, 0.0f, 1.0f));

matRot = Matrix.Identity;

matRot = Matrix.RotationX(fAngle);

device.Transform.World = Matrix.Multiply(matRot, worldMatrix);

device.SetStreamSource(0, vertexBuffer1, 0);

device.DrawPrimitives(PrimitiveType.TriangleStrip, 0, 2);

//End the scene

device.EndScene();

device.Present();// переключаем буфера

}

Часть 6. Общие выводы.

Интересно сравнить Java 3D с другими библиотеками.
Но на данном этапе делать это рано. Но несколько слов можно сказать о
D3D и OGL. Как было сказано раньше, D3D стала пользоваться большой
популярностью начиная с 8 версии. Секрет прост – D3D разделяла нагрузку
между центральным и графическим процессором. И выражалось это не только
не вертексных и пиксельных шейдерах. Как мы видели инициализация
массива вершин в D3D очень трудоемка в отличии от OGL. Очевидно это
должно иметь смысл. И смысл есть.

При соответствующей аппаратной поддержке, мы
отправим этот массив уже в видеопамять и останется только отдать
команду на отрисовку. Выигрыш огромный.

В OGL основная нагрузка приходится на центральный
процессор. Но не стоит забывать, что OGL создавалась задолго до
появления технологий разнесения нагрузки и используется она не только
для игр, к тому же подобный подход обеспечивает значительно большую
кроссплатформенность. В OGL есть свои способы оптимизации и при прочих
равных, без аппаратной поддержки (когда D3D работает в режиме эмуляции,
или говоря проще железо не поддерживает технологию и приходится
загружать центральный процессор), она работает гораздо быстрее. Но
впрочем и OGL не стоит на месте.

Итак, первый урок закончен.

Прочитать первый урок сначала можно здесь - "Урок 1. Отображение полигонов и трансформации".