OpenGL ES 1.1 Windows 8 і Windows Phone 8.1

У далекому 1998 році я намагався зробити свою гру з OpenGL. Розробка насилу дійшла до альфи і була покинута, але що особливо запам'яталося, так це як зручно було робити під GL інтерфейси — ортогональная проекція, пара трансформацій, биндинг декількох вершин з GL_TRIANGLE_STRIP і у нас вже є кнопка. І от, через шістнадцять років і займаючись мобільним игростроем я зіткнувся з таким же підходом в OpenGL ES 1.*, хіба що 2D текстури без обертань можна тепер малювати через glDrawTexfOES.
Я підтримував кілька проектів, зроблених за цим принципом і потроху в голові вишикувався підступний план: зробити крос-платформний 2D гру на мобільних OpenGL ES і на C#, а на десктопах з звичайним OpenGL. Цілі я домігся не з першого разу і було з цим багато проблем, але в результаті черговий проект у мене працює без змін бізнес-логіки на iOS, Android, BlackBerry, Windows XP/7, Mac OS X, Linux, ReactOS, Windows 8, Windows Phone 8.1. Матеріалу набралося на багато статей, але в цей раз я розповім саме про підтримку Windows Runtime.

OpenTK
Можна багато сперечатися на рахунок зручності OpenGL саме для 2D, до хрипоти в горлі переконувати себе, що для повноцінної гри необхідні шейдери і багатопрохідний рендеринг, а заодно і знаходити підтвердження, що застарілий OpenGL ES 1.1 часто реалізований саме на рівні емуляції через шейдери. Це я залишу для Дон Кіхотів і теоретиків. Мене хвилювало, що це найпростіший спосіб написати неодноразово код 2D відтворення і запускати його на різних платформах, причому не використовуючи монструозна Unity, MonoGame та інші движки.
На iOS і Android під Xamarin все пройшло гладко, робота з GL робиться через бібліотеку OpenTK з неймспейсом OpenGL.Graphics.GL11, константи і методи на обох платформах однакові. На десктопах я вирішив використовувати OpenTK.Graphics.OpenGL, тобто звичайний десктопний OpenGL з C# обгорткою. Там в принципі немає glDrawTexfOES, але без проблем можна зробити заміну для нього і малювати два трикутника через GL_TRIANGLE_STIP/GL_TRIANGLES і glDrawElements — в порівнянні з мобільними, продуктивності вистачає з лишком і VBO тут не потрібні.
Приклад враппера з GL_TRIANGLES
private static readonly int[] s_textureCropOesTiv = new int[4];
private static readonly short[] s_indexValues = new short[] { 0, 1, 2, 1, 2, 3 };
private static readonly float[] s_vertexValues = new float[] { -0.5 f, 0.5 f, 0.5 f, 0.5 f, -0.5 f, -0.5 f, 0.5 f, -0.5 f };

public void glDrawTexfOES(float x, float y, float z, float w, float h)
{
glPushMatrix();
glLoadIdentity();

glTranslatef(w / 2.0 f + x, h / 2.0 f + y, 0.0 f);

glScalef(w, h, 1.0 f);

int[] tiv = s_textureCropOesTiv; // NOTE: clip rectangle, should be set call before

int[] texW = new int[1];
glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, texW);
int[] texH = new int[1];
glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, texH);

float[] texCoordValues = new float[8];

float left = 1.0 f - (тiv[0] + tiv[2]) / (float)texW[0];
float bottom = 1.0 f - tiv[1] / (float)texH[0];
float right = 1.0 f - tiv[0] / (float)texW[0];
float top = 1.0 f - (тiv[1] + tiv[3]) / (float)texH[0];

texCoordValues[0] = right;
texCoordValues[2] = left;
texCoordValues[4] = right;
texCoordValues[6] = left;

texCoordValues[1] = bottom;
texCoordValues[3] = bottom;
texCoordValues[5] = top;
texCoordValues[7] = top;

glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
glVertexPointer(2, GL_FLOAT, 0, s_vertexValues);
glTexCoordPointer(2, GL_FLOAT, 0, texCoordValues);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, s_indexValues);

glPopMatrix();
}

Врахуйте, що копіпаст собі цей код не варто — він не буде працювати там, де немає констант GL_TEXTURE_WIDTH/GL_TEXTURE_HEIGHT. Заодно мінлива s_textureCropOesTiv повинна бути заповнена до дзвінка, а сам код не виконує переворот вьюпорта по осі ординат.


XAML
Деяка кількість магії знадобилося, щоб проект запускався на актуальних версій Mono, .Net 2.0-4.5, Wine, а заодно і під ReactOS, але в цілому крім зоопарку з текстурами особливих проблем тут не було. А ось проблеми почалися на Windows 8 і Windows Phone, де OpenGL відсутня в принципі. З початку я пробував вирішити це малою кров'ю, буквально дописавши свою версію glDrawTexfOES, яка б всередині викликала щось специфічне для цих систем. У ході експериментів я використовував XAML елемент Canvas, а в ньому малював Rectangle, у якого в Brush використовувалася потрібна трансформація для відображення лише частини текстури.
Код трансформації в XAML
TransformGroup group = new TransformGroup();

ScaleTransform scale = new ScaleTransform();
scale.ScaleX = (double)texture.PixelWidth / (double)clipRect.Width;
scale.ScaleY = (double)texture.PixelHeight / (double)clipRect.Height;
group.Children.Add(scale);

TranslateTransform translate = new TranslateTransform();
translate.X = -scale.ScaleX * (double)clipRect.X / (double)texture.PixelWidth;
translate.Y = -scale.ScaleY * (double)clipRect.Y / (double)texture.PixelHeight;
group.Children.Add(translate);

imageBrush.RelativeTransform = group;

clipRect — прямокутник з параметрами обрізки, аналог s_textureCropOesTiv з прикладу вище
texture — BitmapSource з самої текстурою

Цей метод здається дивним, але треба пам'ятати, що XAML часто hardware accelerated і досить швидкий. Я портувати з таким подходомнесколько мобільних OpenGL ES ігор на Windows 8 і працюють вони прийнятно, тільки немає можливості змінювати колір текстур, як у GL через glColor. Тобто в принципі в XAML дозволяється змінювати прозорість елемента, але ніяк не можна змінювати його Color Tint. Наприклад, якщо у вас використовуються білі шрифти і потім розфарбовуються в різні кольори, то з цим подоходом вони так і залишаться білими.

В цілому, варіант з XAML досить сумнівний і не зовсім відповідав початковим планом, так і без колірної диференціації штанів модуляції, бо коли гра була на 80% готова і вже працювала на мобільних і стаціонарному .Net/Mono, я почав шукати більш прийнятні варіанти для Windows 8. Багато було чуток і захоплень навколо порту бібліотеки Angle, але на той момент вона була вже дуже сира і без підтримки C#. Безпосередньо з C# працювати з DirectX виявилося теж не можливо, а сама Microsoft пропонує розробнику кілька простих шляхів: переробити весь C# код на C++, використовувати сторонню бібліотеку SharpDX (C# биндинг над DirectX), або перейти на MonoGame. Бібліотека MonoGame це спадкоємець XNA, використовує ту ж SharpDX для виведення графіки на Windows 8, вона досить непогана, але досить специфічна і переходити на неї в моєму проекті було запізно. SharpDX виглядав не менш монструозным, тягне за собою всі існуючі можливості DirectX, хоча і досить близький до того, що мені було треба. Я вже почав проводити з ним серйозні розмови з паяльником і мануалом, коли натрапив на проект gl2dx.

GL2DX
Ця бібліотека була викладена юзером average на CodePlex кілька років тому і більше не оновлювалася. Це С++ бібліотека, яка оголошує такі ж функції, як і в OpenGL, а всередині транслює їх в виклики D3D11. До бібліотеки йшов приклад на C++/CX, який створював XAML сторінку з SwapChainBackgroundPanel і ініціалізувати її через D3D11CreateDevice для роботи з С++ частиною. Проект був би гарний, коли б хоч трохи вийшов із стадії прототипу. Технічно, в ньому працює всього кілька відсотків OpenGL методів, а в інших стоять ассерти. З іншого боку, вона справляється з виведенням 2D текстур, трансформацією і найпростішої геометрією. На цьому етапі я взявся за бібліотеку і довів її до стану продукту, який підключається до C# проекту Visual Studio Extension і дозволяє писати такий код:
Код
GL.Enable(All.ColorMaterial);
GL.Enable(All.Texture2D);
GL.Color4(1.0 f, 1.0 f, 1.0 f, 1.0 f);

GL.TexParameter(All.Texture2D, All.TextureCropRectOes, new int[] { 0, 0, 1024, 1024 });
GL.BindTexture(All.Texture2D, m_textureId1);
GL.DrawTex(0, - (m_width - m_height) / 2, 0, m_width, m_width);

for (int i = 0; i < 10; i++)
{
if (i % 2 == 0)
{
GL.BindTexture(All.Texture2D, m_textureId2);
GL.TexParameter(All.Texture2D, All.TextureCropRectOes, new int[] { 0, 0, 256, 256 });
}
else
{
GL.BindTexture(All.Texture2D, m_textureId2);
GL.TexParameter(All.Texture2D, All.TextureCropRectOes, new int[] { 256, 0, 256, 256 });
}

int aqPadding = 20;
int fishSize = 128;
int aqWidth = (int)m_width - aqPadding * 2 - fishSize;

float x = (Environment.TickCount / (i + 10)) % aqWidth;
float alpha = 1.0 f;
if (x < fishSize)
alpha = x / fishSize;
else
if (x > aqWidth - fishSize)
alpha = (aqWidth - x - fishSize) / 128.0 f;

GL.Color4(1.0 f, 1.0 f, 1.0 f, alpha);
GL.DrawTex(x + aqPadding, m_height / 20 * (i + 5), 0, fishSize, fishSize);
}

P. S. Код у форматі викликів OpenTK, що трохи збиває з пантелику тих, що звик писати glColor4f замість GL.Color4.
Це поділився отримало від мене горду назву MetroGL.

MetroGL
Приклад на C++/CX трансформувався в бібліотеку на цьому ж пташиному сучасною мовою, обріс великою кількістю додаткових функцій, а C++ отримала реалізацію багатьох OpenGL методів, блендинги, оптимізацію внутрішнього VertexBuilder, завантаження довільних зображень і DDS текстур, а головне — точну імітацію glDrawTexfOES, що дає 1в1 таку ж картинку, як на OpenGL ES, а заодно сполучає послідовні операції з однієї текстурою єдиний DrawCall. Дещо довелося доводити напилком, сам код місцями грязноват (як до мене, так і після), а для створення VSIX розширення треба перезбирати проект вручну під кожну архітектуру (x86, x64, ARM) і лише потім билдить VSIX проект. Головне, що якщо у вас є OpenGL ES 1.* код з 2D інтерфейсом або не складним 3D, то з цією бібліотекою його можна використовувати прямо з C#, не думаючи про нутрощах, С++ коді, D3D11 контекстах та інших гидотрадощах. Заодно зроблений приклад з рибками праворуч і кодом під ката. Звичайно, якщо код у вас на OpenGL 2.0+ з шейдерами і экстеншенами, то ні про яке портуванні мови і не буде.
Інший неприємний момент у тому, що у мене немає настрою та бажання доводити бібліотеку до рівня 50-100% сумісності з OpenGL, а значить під ваші конкретні завдання її доведеться заточувати своїми силами. Благо, весь код викладений на github та я поки нікуди не зникаю і буду радий коммитам або взагалі бажаючим взяти на себе цей вантаж. Бібліотека збирається під Windows 8.0 і Windows Phone 8.1, для VSIX може знадобитися не-Express версія Visual Studio.

Епілог
Ну і в кінці-кінців трохи про іграх. Проект свій я на 100% закінчив і саме комбінація C# і OpenGL дала можливість зробити високорівневий код взагалі не змінним — це бібліотека без єдиного дефайна, не використовує будь-яких системних викликів. Потім йде код середнього рівня: малювання через OpenGL в 2D, з обертанням, трансформацією і колірної модуляцією, тут трохи код відрізняється на різних платформах — різні текстури, по-різному зберігаються дані. Низькорівнева частина вже для кожної платформи різна, це створення вікна, ініціалізація контексту, вивід звуку. У будь-якому випадку, дев'ять платформ, перераховані на початку статті, реально працюють, і нехай C# в зв'язці з OpenGL поки не можна використовувати у вебі чи на Firefox OS, але все-одно хіба це не відблиск міжплатформового майбутнього, панове?



Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.