Pull to refresh

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

Game developmentDevelopment for Windows PhoneC#
Tutorial
Привет дорогой друг.

Опять прошло много времени и я не радовал вас интересной информацией по
поводу разработки игр. Сегодня эту статью я хочу посветить разработке игр под 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<Texture2D>("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<VertexPositionColorTexture>(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, разместить её в маркете бесплатно, без рекламы и сидеть в нищете.

Исходники скачать можно тут.

Экспериментируйте, творите; до новых встреч :-)
Tags:WP7DistortionGridgamedevbasiceffectxnaискажение
Hubs: Game development Development for Windows Phone C#
Rating +17
Views 4.1k Add to bookmarks 112
Comments
Comments 2

Popular right now

Backend Development Engineer
from 200,000 ₽Pixel Networks LtdRemote job
Devops (Windows)
from 150,000 ₽deeplayНовосибирскRemote job
C# developer (long-term, remote)
from 100,000 to 200,000 ₽Bergmann InfotechRemote job
Business Development Representative
from 1,000 $MixRankRemote job
Senior .Net Engineer (C#)
to 230,000 ₽ItivitiСанкт-Петербург