Очень долгое время MonoGame был не вполне комфортным для использования. Отсутствие собственного Content Pipeline вынуждало устанавливать XNA и Visual Studio 2010, плясать с бубном, а в итоге единственным надёжно рабочим решением было добавление в проект уже скомпилированных файлов ресурсов *.xnb. Кроме того, многие возможности XNA оставались нереализованными. Но теперь всё изменилось…
Начать проще простого. Идём на официальный сайт и загружаем версию для Visual Studio. Сразу после установки появляются шаблоны проектов MonoGame.
Или не появляются… В этом случае проблема может быть в том, что в настройках Visual Studio был изменён путь к каталогу с шаблонами и придётся скопировать их вручную из каталога где обычно находятся шаблоны проектов C:\Documents and Settings\UserName\Documents\Visual Studio 2013\Templates\ProjectTemplates\Visual C#.
Выбираем проект для той платформы, которая нам нужна и вперёд. Я выбираю Windows Project, т.е. проект для ОС Windows использующий DirectX в качестве графического API. Естественно, вновь созданный пустой проект тут же можно скомпилировать, запустить и узреть пустой экран закрашенный цветом васильков, он же CornflowerBlue.
В последних версиях MonoGame работа с контентом стала наконец такой же удобной как и при использовании XNA. И даже лучше. Больше не нужно никаких Content Project! Прямо в основном проекте теперь есть файл Content.mgcb, при попытке редактирования которого открывается новая утилита MonoGame Pipeline, предназначенная для добавления ресурсов в наше приложение.
Добавим какую-нибудь модель в формате .fbx и текстуру для неё. Выполним билд (F6). Утилита должна рапортовать об успехе. Радует, что запускать её придётся только для добавления новых ресурсов. При редактировании ранее добавленных их перекомпиляция выполняется автоматически при построении проекта в Visual Studio, а в случае неудачи причины сразу видны в окне Error List.
Теперь попробуем отрисовать нашу модель. Для этого нам понадобится объявить несколько переменных для модели, текстуры и эффекта.
1 2 3 |
Model model; Texture texture; Effect modelEffect; |
Далее в метод LoadContent добавляем код для загрузки необходимых ресурсов. Для эффекта указываем в качестве параметров матрицы мира, вида и проекции, а также текстуру.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
model = Content.Load<Model>("Statue"); texture = Content.Load<Texture2D>("Bronze"); modelEffect = Content.Load<Effect>("Model"); modelEffect.Parameters["world"].SetValue(Matrix.CreateFromYawPitchRoll(-1,0,0)); modelEffect.Parameters["projection"].SetValue(Matrix.CreatePerspectiveFieldOfView( MathHelper.ToRadians(90), graphics.GraphicsDevice.Viewport.AspectRatio, 0.01f, 100.0f )); modelEffect.Parameters["view"].SetValue(Matrix.CreateLookAt( new Vector3(15, 20, 15), new Vector3(0, 25, 0), Vector3.Up )); modelEffect.Parameters["colorMap"].SetValue(texture); |
В метод Draw добавим код для отрисовки модели с нашим кастомным эффектом.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.Black); modelEffect.CurrentTechnique = modelEffect.Techniques["Model"]; modelEffect.CurrentTechnique.Passes[0].Apply(); foreach (ModelMesh mesh in model.Meshes) { foreach (ModelMeshPart meshPart in mesh.MeshParts) { graphics.GraphicsDevice.Indices = meshPart.IndexBuffer; graphics.GraphicsDevice.SetVertexBuffer(meshPart.VertexBuffer); graphics.GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, meshPart.NumVertices, meshPart.StartIndex, meshPart.PrimitiveCount); } } base.Draw(gameTime); } |
Далее переходим к коду шейдера на языке HLSL. В нём нам понадобятся переменные для ранее заданных матриц и текстуры.
1 2 3 4 5 6 |
float4x4 world; float4x4 view; float4x4 projection; Texture2D colorMap; SamplerState filter : register(s0); |
Фильтры для текстур установленные непосредственно в коде шейдера не всегда корректно работают, поэтому лучше устанавливать их в коде программы.
1 2 3 4 5 6 7 |
graphics.GraphicsDevice.SamplerStates[0] = new SamplerState() { AddressU = TextureAddressMode.Clamp, AddressV = TextureAddressMode.Clamp, Filter = TextureFilter.Anisotropic, MaxAnisotropy = 8 }; |
Далее структуры для входа и выхода вершинного шейдера.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
struct VSInput { float3 Position : SV_POSITION; float3 Normal : NORMAL0; float2 UV : TEXCOORD0; float4 Color : COLOR0; }; struct VSOutput { float4 Position : SV_POSITION; float4 WorldPosition : COLOR0; float3 Normal : NORMAL0; }; |
Вершинный шейдер лишь трансформирует координаты вершин и нормали в соответствии с указанными матрицами.
1 2 3 4 5 6 7 8 9 10 11 |
VSOutput ModelVS(VSInput input) { VSOutput output; output.WorldPosition = mul(float4(input.Position, 1), world); float4 viewPosition = mul(output.WorldPosition, view); output.Position = mul(viewPosition, projection); output.Normal = mul(float4(input.Normal, 1), world).xyz; return output; } |
Наконец пиксельный шейдер выполняет сэмплинг текстуры, чтобы вывести пиксель нужного цвета. К сожалению, используемая модель содержит информацию только о координатах вершин и нормалях. Поэтому используется GPU реализация симплекс шума для того, чтобы получить нечто похожее на текстурные координаты по мировым координатам точки.
1 2 3 4 5 6 7 8 9 10 |
float4 ModelPS(VSOutput input) : SV_TARGET0 { float2 uv = float2( snoise(input.WorldPosition.xyz*2), snoise(input.WorldPosition.zxy*2) ); float3 color = colorMap.Sample(filter, uv).rgb; return float4(color, 1); } |
В самом конце создаём однопроходную технику использующую вершинный и пиксельный шейдеры.
1 2 3 4 5 6 7 8 |
technique Model { pass Pass1 { VertexShader = compile vs_5_0 ModelVS(); PixelShader = compile ps_5_0 ModelPS(); } } |
Данный шейдер расcчитан на использование DirectX 11. В дополнение можно прикрутить примитивное освещение от одного источника и увидеть результат.
Проект для Visual Studio доступен здесь.
Портирование готового проекта с XNA на MonoGame в текущих реалиях является относительно несложной задачей. Потребности править код скорее всего не будет. Единственное несоответствие API XNA, с которым я столкнулся это отсутствие метода Transform у структуры Matrix. Если сложности и возникнут, то они будут связаны с шейдерами. Самое большое неудобство – крайне агрессивная оптимизация вне зависимости от того выполняется построение DEBUG или RELEASE версии проекта. Как следствие, стоит только в шейдере не воспользоваться текстурой, как переменная для неё исчезает и получаем ошибку исполнения при попытке установить соответствующий параметр шейдера. Раздражает…
Для Windows мы можем выбирать между двумя графическими API: OpenGL и DirectX. Выбирая первое, как и в XNA мы будем ограничены возможностями шейдеров модели 3.0. При этом код на HLSL будет при компиляции транслироваться в GLSL. Выбирая DirectX, мы теоретически можем пользоваться возможностями шейдеров вплоть до модели 5.0, но только теоретически. Пока что MonoGame поддерживает только вершинные и пиксельные шейдеры, а значит самые интересные возможности DirectX 10 и 11 остаются недоступными. Многие относительно простые вещи, которых по понятным причинам не могло быть в XNA, отсутствуют и в MonoGame. Метода для генерации mipmap нет, возможности прочитать hardware-глубину нет и т.д. Разумеется всё решаемо. Исходный код MonoGame доступен и при желании всё необходимое можно доделать. Что-то делается совсем просто, что-то сложнее…
Утилита для компиляции ресурсов также не лишена проблем. Они часто возникают при обработке .fbx моделей. Иногда проблема – лишь следствие устаревшего формата входного файла и решается повторным экспортом из blender в новый формат. Иногда это не помогает. Обработка текстур с неполным набором каналов также может быть неудачной.
Поводя итог… Использование MonoGame никогда не было совсем безболезненным процессом и сейчас таковым не является. Но теперь настал тот момент, когда можно говорить о достаточной зрелости проекта, чтобы те кто до сих пор не отказался от XNA наконец сделали это. Хотя бы по той причине, что MonoGame продолжает развиваться. Пусть и не так быстро как хотелось бы…