Game Development / Windows Phone 7 XNA: гнем пиксели или нет шейдерам

в 20:38, , рубрики: Gamedev, Grid, WP7, xna, метки: , , ,

Game Development / Windows Phone 7 XNA: гнем пиксели или нет шейдерам
Привет дорогой друг.
Опять прошло много времени и я не радовал вас интересной информацией по
поводу разработки игр. Сегодня эту статью я хочу посветить разработке игр под WP7, используя
замечательный фреймворк XNA, о котором я писал здесь, здесь и здесь и тут. А конкретно, я вам расскажу, как можно сделать красивые эффекты без шейдеров. В этой статье рассмотрим эффект искажения. Видео, теория, практика под катом.
Теория

Последние две статьи я писал про шейдеры и о том, как можно улучшить визуальное восприятие в вашей игре. Но если посмотреть на сравнение Reach и HiDef профилей XNA, то можно с ужасом
увидеть, что Reach поддерживает Shader Modele 2.0, а WP7 её не поддерживает вообще. И от этого хочется взять и ударить придумать, как это все можно сделать без шейдеров.
Конечно, речь не идет о крутом освещении с normal mapping (хотя, извратиться можно), а просто о том, как можно погнуть пиксели с помощью BasicEffect. Такой метод прост до безумия, но крайне эффективен.
Итак, если вспомнить, кто я такой и о чем я писал, то можно вспомнить алгоритм, который мы
использовали в шейдерах: есть карта искажений и цветная карта. По карте искажений — гнем цветную карту. Просто? Забудьте. Такой метод крайне сложен в реализации под WP7 без вмешательства GPU (доступа к которому у нас, к сожалению, нет).— Как же быть, парень?
Вспомним, по каком принципу у нас рисуется что-либо в 3D, например обычный плоский квадрат? Он рисуется с помощью двух треугольников. В чем это может нам помочь? Все просто, создаем много треугольников, а потом, с помощью координат текстуры — будем двигать «какбэ» сам треугольник, отчего создается эффект искажения.
На деле — есть картинка — 480x800, мы создаем сетку размером 48x80 (поверьте, для красивого эффекта — в самый раз). Сетка — одномерный массив, состоящий из 3840 элементов. Просчитывается это все на WP7 примерно 3-4 ms, при более низком качестве сетки — 1-2 ms. Но если сетка будет слишком маленькая, то при искажении будет заметно, что треугольники все-таки существуют. А вот когда сетка меньше в 10 раз, это мало заметно, для сравнения — шаг на экране в 3 мм = 10 пикселям. Ну да ладно, что-то я разговорился.— Эй, чувак, хватит теории, переходи к практике.
Практика

Чтобы рисовать что-либо на экране из примитивов, нужен BasicEffect. Например, spriteBatch — огромный класс, который скрывает от наших глаз всякие BasicEffect, но в конечном счете — все сводится к рисованию примитивов, накладывание текстур на них. Постараюсь более подробно объяснить об использовании BasicEffect в нашем случае.
Собираемся в путь, ищем материал.
Для начала нам нужна текстура, которую мы будем гнуть, встречайте нашего любимого друга:
И как-то странно, но нам еще нужен пустой проект, создаем его.
Сразу скажу, что одна из особенностей XNA под WP7, что по дефолту там 30 FPS (взамен, родных 60 FPS). Но что-то мне подсказывает, что можно и больше; С другой стороны — кому нужен батарея-киллер, а не таймкиллер? Поэтому, мы будем использовать 30 FPS.
В пустом проекте вы найдете:
// Frame rate is 30 fps by default for Windows Phone.
TargetElapsedTime = TimeSpan.FromTicks(333333);

// Extend battery life under lock.
InactiveSleepTime = TimeSpan.FromSeconds(1);

Строчка, отвечающая за FPS — догадайтесь сами.
Следующий момент, это отсутствие клавиатуры, все действия выполняются с помощью мультитача.
Единственную кнопку, какую можно перехватить, это кнопка Back (назад), по дефолту — она выходит из приложения:
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();

Поэтому, трогать в пустом проекте мы ничего не будем, приступаем к програмированию и собственно, практике. Для начала — наполним Game1 смыслом и любовью.
Создаем переменные:
Texture2D background;
BasicEffect basicEffect;
background — наша текстура, ну или RenderTarget какой-то.BasicEffect — наш герой, нужный для отрисовки примитивов.
Грузим контент:
background = Content.Load("distortion");

Чуть не забыл, выставляем в конструкторе:
graphics.PreferredBackBufferWidth = 480;
graphics.PreferredBackBufferHeight = 800;
graphics.IsFullScreen = true;

Дабы была одна ориентация и не было проблем с позиционированием.
Инициализируем BasicEffect (в Initialize):
basicEffect = new BasicEffect(GraphicsDevice);
basicEffect.TextureEnabled = true; // включаем накладывание текстур на примитивы

basicEffect.Projection = Matrix.CreateOrthographicOffCenter(0, 480, 800, 0, 0f, 10f);
basicEffect.View = Matrix.Identity;
basicEffect.World = Matrix.Identity;
Projection — матрица-проекция трехмерного объекта на двухмерную плоскость (экран).View — матрица вида, камеры, если хотите.World — мировая матрица: вращение, размер, позиция.
Зададим View и World — единичные матрицы.
А Projection зададим ортогональную проекцию, т.е. у нас будет примитив проецироваться на экран полностью. Концы примитива с концами экрана, если объяснить проще.
Так, пока с Game1 все, создадим новый класс GridVertexPositionColorTexture, и вот его полный листинг (прошу прощения за полный, но он с комментариями):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework;

namespace GridDistortion
{
public class GridVertexPositionColorTexture
{
public VertexPositionColorTexture[] Vertices; // Массив из вертексов, которые несут в

себе позицию вертекса, цвет вертекса и UV координаты текстуры

public short[] Indices; // Индексы, о них я расскажу в другой статье, где будет 3D тематика, заодно и расскажу, как строить примитивы

public int Width; // размер сетки по X
public int Height; // размер сетки по Y

public Vector2 CellSize; // шаг между точками в сетке по X и Y соответственно

public void BuildGeometry(int colums, int rows, Vector2 cellSize) // строим геометрию нашего примитива
{
Width = colums;
Height = rows;
CellSize = cellSize;

Vertices = new VertexPositionColorTexture[(Width + 1) * (Height + 1)]; // инициализация массива вертексов
Indices = new short[Width * Height * 6]; // тоже самое и про индексы

/* заполнение массива вертексов */

for (int i = 0; i < Width + 1; i++)
{
for (int j = 0; j < Height + 1; j++)
{
int index = j * (Width + 1) + i;
VertexPositionColorTexture vertex = new VertexPositionColorTexture()
{
Position = new Vector3(new Vector2(i, j) * CellSize, 0f), // позиция вертекса
Color = Color.White,
TextureCoordinate = GetDefaultUV(index) // текстурная координата
};
Vertices[index] = vertex;
}
}

/* заполнение массива индексов */

int indexPos = 0;
for (int i = 0; i < Width; i++)
{
for (int j = 0; j < Height; j++)
{
int v0 = j * (Width + 1) + i;
int v1 = j * (Width + 1) + i + 1;
int v2 = (j + 1) * (Width + 1) + i;
int v3 = (j + 1) * (Width + 1) + i + 1;

Indices[indexPos] = (short)v0;
Indices[indexPos + 1] = (short)v1;
Indices[indexPos + 2] = (short)v2;
Indices[indexPos + 3] = (short)v2;
Indices[indexPos + 4] = (short)v1;
Indices[indexPos + 5] = (short)v3;
indexPos += 6;
}
}
}

public void Draw(GraphicsDevice graphicsDevice) // отрисовка массива из наших VertexPositionColorTexture
{
graphicsDevice.DrawUserIndexedPrimitives(PrimitiveType.TriangleList, Vertices, 0, Vertices.Length, Indices, 0, Indices.Length / 3);
}

public void ResetUVs() // сброс UV сетки
{
for (int i = 0; i < Vertices.Length; i++)
{
VertexPositionColorTexture v = Vertices[i];
v.TextureCoordinate = GetDefaultUV(i);
Vertices[i] = v;
}
}

public Vector2 GetUV0(int index) // получить значение сетки
{
return Vertices[index].TextureCoordinate;
}

public void SetUV0(int index, Vector2 value) // задать значение сетки
{
Vertices[index].TextureCoordinate = value;
}

public Vector2 GetDefaultUV(int index) // получить значение для сетки по дефолту
{
int i = index % (Width + 1);
int j = index / (Width + 1);
return new Vector2((float)i / Width, (float)j / Height);
}
}
}

Все хорошо, класс, отвечающий за прорисовку примитивов и за саму сетку — создан. Теперь нужно придумать контроллер к этой сетке, который будет её гнуть. В этой статье — расскажу вам про два контроллера сетки: SimpleGrid, ElasticGrid.
Первый у нас будет сбрасывать сетку, применять к ней текущие искажения.
Второй превратит нашу сетку в желе, которая будет колебаться, пока не придет к дефолтному состоянию.
Напишем первый контроллер, создадим новый класс SimpleGrid и его листинг:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;

namespace GridDistortion
{
public class SimpleGrid
{
publicGridVertexPositionColorTexture grid; // наша подконтрольная сетка

public SimpleGrid(GridVertexPositionColorTexture grid)
{
this.grid = grid;
}

public void Update()
{
swap();
}

public virutal void swap() // просто сбрасываем сетку на стандартные значения
{
grid.ResetUVs();
}

public void Rebellion(Vector2 pos_rebellion, float radius) // делаем искажение на сетке с позицией и радиусом
{
Vector2 gridSize = new Vector2(grid.Width, grid.Height) * grid.CellSize; // размер нашего примитива

for (int i = 0; i < grid.Vertices.Length; i++)
{
Vector2 pos = grid.GetUV0(i) * gridSize; // получаем реальную позицию вертекса из сетки
Vector2 newPos = pos;

Vector2 center = pos_rebellion; // где произошло искажение

float distance = Distance(pos, center); // получаем дистанцию от центра искажение до текущей точки
if (distance < radius) // если дистанция больше радиуса, то не трогаем пиксель, экономим ресурсы
{
Vector2 dir = pos - center; // получаем вектор направления

float length = dir.Length();
float minDisplacement = -length;

if (dir.Length() != 0)
{
dir.Normalize(); // нормализуем вектор
}

Vector2 displacement = dir * Math.Max(-100f, minDisplacement); // задаем вектор искажения, где -100f — его сила, положительная величина — не увеличит изображение, а сожмет

newPos += displacement * (1f - distance / radius) * 0.25f;
grid.SetUV0(i, newPos / gridSize); // задаем новую позицию
}

}
}

public static float Distance(Vector2 vector1, Vector2 vector2)
{
double value = ((vector2.X - vector1.X) * (vector2.X - vector1.X)) + ((vector2.Y -

vector1.Y) * (vector2.Y - vector1.Y));
return (float)Math.Sqrt(value);
}
}
}

Контроллер написан, теперь вернемся к Game1, две новых переменных:
GridVertexPositionColorTexture grid;
SimpleGrid simpleGrid;

Их инициализация:
grid = new GridVertexPositionColorTexture();
grid.BuildGeometry(48, 80, new Vector2(10, 10));
simpleGrid = new SimpleGrid(grid);

Сам Update:
simpleGrid.Update(); // обновляем контроллер сетки

TouchCollection collection = TouchPanel.GetState(); // получаем все прикосновения
foreach (TouchLocation point in collection)
{
if (point.State == TouchLocationState.Moved)
{
simpleGrid.Rebellion(point.Position, 100f); // создаем искажение в точки point.Position с радиусом 100f
}
}

Ну и наконец прорисовка:
GraphicsDevice.SamplerStates[0] = SamplerState.LinearClamp; // уставливаем Clamp, т.к. с Wrap-ом будут проблемы в Reach профиле, чьи размеры не степень двойки

basicEffect.Texture = background; // задаем текстуру для примитива
basicEffect.CurrentTechnique.Passes[0].Apply(); // применяем basicEffect

grid.Draw(GraphicsDevice); // рисуем примитив

Запускаем, касаемся экрана и видим искажения или эффект линзы.
Но повеселимся еще, сделаем желе из текстуры, класс ElasticGrid, наследуемый от SimpleGrid:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;

namespace GridDistortion
{
public class ElasticGrid : SimpleGrid
{
private Vector2[] Velocity; // скорости вертексов

public ElasticGrid(GridVertexPositionColorTexture grid) : base(grid) { this.Velocity =

new Vector2[(grid.Width + 1) * (grid.Height + 1)]; }

public override void swap()
{
Vector2 gridSize = new Vector2(grid.Width, grid.Height) * grid.CellSize;

for (int i = 0; i < grid.Vertices.Length; i++)
{
//Get the position in pixels
Vector2 pos = grid.GetUV0(i) * gridSize;
Vector2 pos_default = grid.GetDefaultUV(i) * gridSize;

Vector2 dir = (pos_default - pos) / 1.1f; // получаем вектор скорости и делим

его каждый раз на 1.1f,

//Set the new Texture Coordinates
grid.SetUV0(i, (pos + Velocity[i]) / gridSize); // задаем позицию + вектор

скорости

Velocity[i] = dir; // пишем в массив скорость

}
}
}
}

Меняем контроллер сетки в Game1 и любуемся искажениями.
Вот такой простой и интересный подход. Контроллеров может быть бесконечное кол-во, например, реалистичная вода в реалтайме без всяких шейдеров с волнами и шлюхами.
В другой раз я попробую описать другие методы придания вашей игре красоты.
Так же из серии по XNA планируется написание статей на тематику 3D.
Ну и как бонус, следующая статья возможно будет о том, как можно написать игру на WP7, разместить её в маркете бесплатно, без рекламы и сидеть в нищете.
Сейчас для теста делаю один простенький филлер (скрин самый первый) для WP7, однако списать видео из нее Fraps-ом я не могу, т.к. WP7 формат сурфейса: BGR, а Fraps получает в формате RGB.
А вот видео демонстрация ElasticGrid:
Исходники скачать можно тут.
Экспериментируйте, творите; до новых встреч :-)


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js