вторник, 10 апреля 2012 г.

Практическое применение XNA. Часть 12.

Создание Terrain.


Terrain в переводе с английского означает - местность, территория, ландшафт. В контексте программирования игр этот термин применяется по отношению к земле, но естесственно имеется ввиду не планета, а тот грунт по которому мы ходим ногами. В этой главе мы узнаем, как самим создавать terrain. Вообще ландшафт это неотьемлемая часть множества игр, и понять принципы его создани всем будет полезно.


 


 Для начала немного теории. Многим из начинающих программистов игр первым делом приходит на ум использовать в качестве ландшафта готовую модель. Не скрою, сам таким был. Вообще создание и использование terraina задача не тривиальная и однозначно сказать какой способ наилучший нельзя. Всё зависит от задач которые ставятся перед разработчиком, и от свойств котрыми должен обладать ландшафт. Ну к примеру если вы будете созерцать землю из окна высокой башни, то действительно обойтись моделью да еще с низким содержанием полигонов, было бы разумно. Но если вы решили создать игру от первого лица, в которой вам предстоит перемещаться во всех направлениях по ландшафту, видеть его во всей своей красе (высокая детализация) да еще в придачу площадь этого самого ландшафта 100 квадратных километров, и переодически требуется получить высоту (координату) любой точки на поверхности земли хотя бы для того, что бы что то на землю поставить, тогда без динамического создания земли нам не обойтись. Вот об этом дальше речь и пойдет. Что вообще собой представляет terrain? Я бы его охарактеризовал так - это геометрическая фигура Plane (плоский квадрат или прямоугольник), имеющий сетчатую структуру с размером n х n,



с различными высотами вершин. Это как раз и имитирует неровности грунта.



Как создать такую сетку, теоретически, вам уже должно быть понятно. Так же как и любой примитив. К примеру вы решили создать terrain с размерами 100 х 100 вершин. Заметьте, я указал размер именно в вершинах, а не метрах или километрах, потому что генерировать я буду вершины, а так сказать условные километры будут зависеть от расстояния между вершинами. Для этого нам понадобится обычный цикл который выглядит примерно так
for(int x = 0; x < 100;  x ++)
{
   for(int z = 0; z < 100; z ++)
   {
      ...new Vector3(x, ..., z);
   }
}
где переменные x и z соответствуют координатам по оси X и Z. Пока, что это очень общий принцип создания сетки, но он необходим для понимания азов. Теперь если ширину и глубину мы получили, то как нам быть с самой интересной величиной - высотой. Для реализации этой задачи существует множество различных способов. Можно например, написать генератор случайных велечин в неком диапазоне высот, и поставить каждой вершине случайную высоту, но предсказать итоговую геометрию будет не просто. Вероятнее всего это будет некий ёжик, которого после десятка интерполяций удастся сгладить, но управлять формой ландшафта будет не так то просто. Я предлагаю пойти иным путем. Наверное многим бы пришелся по душе способ, в котором бы вы просто нарисовали на экране какого нибудь графического редактора желаемую местность, а именно вид сверху, а в игре получили задуменный рельеф. Что захотели то и получили. Или еще лучше, взяли фотографию с самлёта или спутника какой то понравившейся части земли, и довольно достоверно ее повторили у себя в игре. Принцип такого волшебного способа весьма прост. Это использование так называемой карты высот, ее роль играет обыкновенный графический файл в .bmp формате.



 Суть этого принципа заключается в том, чтобы использовать в качестве значения высоты цветовое значение точки изображения. Как вы наверняка уже знаете, цвет в компьютере представлен структурой состоящей из четырех переменных.
Color = {R, G, B, A};
R - хранит информацию о красной составляющей цвета, или правильней будет сказать это красный канал.
G - канал зеленой составляющей.
B - канал синей составляющей.
A - это канал прозрачности или альфа канал, о котором мы упоминали в предыдущих главах. Любой из этих каналов хранит значение в диапазоне от 0 до 255. Для сравнения истинно черный и истинно белый цвета выглядят так:
Color.Black = {0, 0, 0, 0};
Color.White = {255, 255, 255, 0};
В нашем подходе мы можем использовать любой из цветовых каналов при условии что карта высот чернобелая.
Как видите все эти цвета ничто иное как оттенки серого, а значит что любая цветовая точка или пиксел взятая из чернобелого изображения всегда определяется выражением R=G=B, а это и означает то, что изображение должно быть черно-белое. Движемся далее. Для большей наглядности принято считать темные участки карты высот впадинами и низинами ассоциирующиеся с плохой освещенностью, а возвышенности закрашиваются светлыми тонами и ассоциируются с хорошим освещением или заснеженными горными вершинами. Так и для восприятия понятней, да и значение точки чем светлее тем выше, то есть в нашем случае данные именно в том формате в каком нам и требуется. Теперь нам необходим механизм, который считает данные с нашего битмапа и заполнит ими значения в массиве позиций вершин будущего terraina. Плавно переходим от теории к практике. Создадим новый проект под названием myTerrain. Но в отличии от всех предыдущих примеров, для создания ландшафта мы используем оттдельный класс, а точнее  Microsoft.Xna.Framework.DrawableGameComponent
Теперь подробнее что это такое и зачем нам все это нужно. Во первых GameComponent и DrawableGameComponent это два игровых класса (компонента) которые имеют такие же как и главный класс методы, а именно Load(), Update() но метод Draw() имеет только DrawableGameComponent. Вызов этих методов происходит из екстернального кода, можно сказать автоматически без вашего участия, вам лишь остаётся в этих методах написать свой код. Для того чтобы создать новый игровой компонент вам надо в вновь созданом проекте, правой кнопкой мышки сделать следующие манипуляции
myTerrain > Добавить > Компонент. Далее в появившемся окне выбрать категорию XNA GameStudio 3.1 в левой части формы, а в правой части выбрать шаблон Game Component. Указать название игровому компоненту и нажать кнопку [Добавить]. После чего в ваш проект будет добавлен класс игрового компонента. Важно сказать, что игровой компонент это самостоятельный модуль, который вы сможете потом использовать в любых других проектах XNA Game Studio, если добавите его в проект.

После проделаных операций в вашем проекте должен появиться класс Terrain. Он уже будет иметь набор необходимых методов, но эти методы пока еще ни откуда не вызываются. Для того что бы это исправить, мы должны вновь созданный компонент добавить в коллекцию компонентов нашей игры, после чего они непременно начнут вызываться. Делается это так. В методе Initialize() мы создадим экземпляр игрового компонента, а затем добавим в коллекцию компонентов.
    protected override void Initialize()
    {
      Terrain terrain = new Terrain(this);
      Components.Add(terrain);
      base.Initialize();
    }
Вот после этого все начнет работать как надо. Теперь переходим собственно к самому изготовления terraina. Для начала явно добавляем в проект файл HeightMap.bmp по традиции в папку Content. Это наша карта высот. Еще из ресурсов нам понадобится текстура травы которой мы раскрасим нашу землю. Для этого добавим в папку Content файл grass.dds. Далее код который будет находться именно в компоненте Terrain.
    private int WIDTH;
    private int HEIGHT;
    private VertexBuffer vertexBuffer;
    private IndexBuffer indexBuffer;
    private VertexDeclaration vertexDecalaration;
    private float[,] heightData;
    private BasicEffect effect;
    Texture2D texture;
Здесь вы видите уже знакомые вам классы по урокам создания примитивов. Имеются также переменные которые будут хранить ширину и высоту карты высот, и двухмерный массив heightData[,] в котором как видно из названия будут храниться все высоты нашего ландшафта. Создаваться terrain будет в три этапа. Первым делом мы считаем данные из карты высот, и запишем их в массив heightData[,]. Вторым этапом будет создание вершин на основе данных из heightData[,],и записывания их в вершинный буфер.  Третьим по счету, будет создание всех индексов и записываниие их в буфер индексов. Для этого мы используем три отдельных метода, и вызовим их в той последовательности, что оговорили.
    public override void Initialize()
    {
      effect = new BasicEffect(Game.GraphicsDevice, null);
      texture = Game.Content.Load<Texture2D>("grass");
      LoadHeightData(Game.Content.Load<Texture2D>("HeightMap"));
      SetUpTerrainVertices(Game.GraphicsDevice);
      SetUpTerrainIndices(Game.GraphicsDevice);
      vertexDecalaration = new VertexDeclaration(Game.GraphicsDevice, VertexPositionNormalTexture.VertexElements);
      base.Initialize();
    }

Теперь рассмотрим по отдельности каждый метод. Начнем с загрузки данных о высотах.
    private void LoadHeightData(Texture2D heightMap)
    {
      // установили высоту и
      // ширину карты высот в
      // глобальные переменные
      WIDTH = heightMap.Width;
      HEIGHT = heightMap.Height;
      // создали массив цветов для хранения
      // данных карты высот с размерностью
      // высоты умноженной на ширину
      Color[] heightMapColors = new Color[WIDTH * HEIGHT];
      // загружаем данные из карты высот
      // в массив цветов
      heightMap.GetData(heightMapColors);
      // создали массив значений высот
      heightData = new float[WIDTH, HEIGHT];
      // в цикле
      for (int x = 0; x < WIDTH; x++)
      {
        for (int y = 0; y < HEIGHT; y++)
        {
          // если карта высот ч/б то не важно из какого канала
          // брать значение. Мы возьмем из красного
          heightData[x, y] = heightMapColors[x + y * WIDTH].R;
        }
      }
    }
Метод создания вершинного буффера.
    public void SetUpTerrainVertices(GraphicsDevice device)
    {
      // создаем массив вершин с размерностью
      // количества всех точек изображения
      VertexPositionNormalTexture[] vertices =
        new VertexPositionNormalTexture[WIDTH * HEIGHT];
      for (int x = 0; x < WIDTH; x++)
        for (int y = 0; y < HEIGHT; y++)
        {
          // устанавливаем значение высоты в Y
          // каждого вектора позиции вершины
          // делением на 5 регулируем амплитуду
          // высоты
          vertices[x + y * WIDTH].Position = new Vector3(x, -heightData[x, y] / 5, y);
          vertices[x + y * WIDTH].Normal = new Vector3(0, 0, 1);
          // устанавливаем координаты текстуры делением
          // на 50 определяем мозаичность (tiling) текстуры
          vertices[x + y * WIDTH].TextureCoordinate.X = (float)x / 50.0f;
          vertices[x + y * WIDTH].TextureCoordinate.Y = (float)y / 50.0f;
        }
      // создаем вершинный буффер
      // на основе полученного массива
      vertexBuffer =
          new VertexBuffer(device, typeof(VertexPositionNormalTexture),
                           VertexPositionNormalTexture.SizeInBytes * WIDTH * HEIGHT, BufferUsage.Points);
      // записываем в него данные
      vertexBuffer.SetData(vertices);
    }

Метод создания индексного буффера.
    private void SetUpTerrainIndices(GraphicsDevice device)
    {
      short[] indices = new short[(WIDTH - 1) * (HEIGHT - 1) * 6];
      for (int x = 0; x < WIDTH - 1; x++)
      {
        for (int y = 0; y < HEIGHT - 1; y++)
        {
          indices[(x + y * (WIDTH - 1)) * 6] = (short)((x + 1) + (y + 1) * WIDTH);
          indices[(x + y * (WIDTH - 1)) * 6 + 1] = (short)((x + 1) + y * WIDTH);
          indices[(x + y * (WIDTH - 1)) * 6 + 2] = (short)(x + y * WIDTH);
          indices[(x + y * (WIDTH - 1)) * 6 + 3] = (short)((x + 1) + (y + 1) * WIDTH);
          indices[(x + y * (WIDTH - 1)) * 6 + 4] = (short)(x + y * WIDTH);
          indices[(x + y * (WIDTH - 1)) * 6 + 5] = (short)(x + (y + 1) * WIDTH);
        }
      }
      indexBuffer = new IndexBuffer(device, sizeof(short) * indices.Length, BufferUsage.WriteOnly, IndexElementSize.SixteenBits);
      indexBuffer.SetData(indices);
    }
Ну и наконец метод отрисовки всего этого с использованием BasicEffect.
    public override void Draw(GameTime gameTime)
    {
      effect.World = Matrix.CreateTranslation(0, 0, 0);
      effect.View = Matrix.CreateLookAt(new Vector3(100, 400, -250), new Vector3(100, 0, 100), Vector3.Up);
      effect.Projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(18), 1.3f, 0.1f, 1000);
      effect.TextureEnabled = true;
      effect.Texture = texture;
      effect.Begin();
      foreach (EffectPass pass in effect.CurrentTechnique.Passes)
      {
        pass.Begin();
        Game.GraphicsDevice.Vertices[0].SetSource(vertexBuffer, 0, VertexPositionNormalTexture.SizeInBytes);
        Game.GraphicsDevice.Indices = indexBuffer;
        Game.GraphicsDevice.VertexDeclaration = new VertexDeclaration(Game.GraphicsDevice, VertexPositionNormalTexture.VertexElements);
        Game.GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, WIDTH * HEIGHT, 0, (WIDTH - 1) * (HEIGHT - 1) * 2);
        pass.End();
      }
      effect.End();
      base.Draw(gameTime);
    }
И в главном классе в методе Draw() добавим следующие строки

    protected override void Draw(GameTime gameTime)
    {
      GraphicsDevice.Clear(Color.CornflowerBlue);
      GraphicsDevice.RenderState.CullMode = CullMode.None;
      base.Draw(gameTime);
    }

Здесь вам должно быть всё знакомо по нашим предидущим примерам. В результате всей проделанной работы мы теперь можем посмотреть на на обьемную модель ландшафта созданную на основе плоской карты высот. Ниже для сравнения приводятся две иллюстрации.





и напоследок установим нашу камеру так, что бы она находилась немного выше уровня земли, добавим уже известный вам эффект тумана и наложим текстуру травы.


  
 Вот таким симпатичным снимком мы и заканчиваем знакомство с понятием terraina. Теперь, я надеюсь, вам понятны базовые принципы динамического создания ландшафтов.

Комментариев нет:

Отправить комментарий

Physically Based Rendering