OpenGL ES 1.1 Windows 8 і Windows Phone 8.1

Я підтримував кілька проектів, зроблених за цим принципом і потроху в голові вишикувався підступний план: зробити крос-платформний 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

На 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 досить сумнівний і не зовсім відповідав початковим планом, так і без колірної
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

Інший неприємний момент у тому, що у мене немає настрою та бажання доводити бібліотеку до рівня 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 коментарів