В далеком 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 тут не нужны.
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.5f, 0.5f, 0.5f, 0.5f, -0.5f, -0.5f, 0.5f, -0.5f };
public void glDrawTexfOES(float x, float y, float z, float w, float h)
{
glPushMatrix();
glLoadIdentity();
glTranslatef(w / 2.0f + x, h / 2.0f + y, 0.0f);
glScalef(w, -h, 1.0f);
int[] tiv = s_textureCropOesTiv; // NOTE: clip rectangle, should be set before call
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.0f - (tiv[0] + tiv[2]) / (float)texW[0];
float bottom = 1.0f - tiv[1] / (float)texH[0];
float right = 1.0f - tiv[0] / (float)texW[0];
float top = 1.0f - (tiv[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 использовалась нужная трансформация для отображения только части текстуры.
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.0f, 1.0f, 1.0f, 1.0f);
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.0f;
if (x < fishSize)
alpha = x / fishSize;
else
if (x > aqWidth - fishSize)
alpha = (aqWidth - x - fishSize) / 128.0f;
GL.Color4(1.0f, 1.0f, 1.0f, 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, но все-равно разве это не отблеск кроссплатформенного будущего, господа?
Автор: Nomad1