понедельник, 12 марта 2012 г.

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

Генерация примитивов (Primitives)

Сегодня я хочу рассказать об альтернативном способе "рисования" на платформе XNA. До этого, что бы отобразить объект на экране, мы использовали модель. Это в большинстве случаев, оправдано если объект имеет достаточно сложную геометрическую форму. Однако для рисования простой геометрии, в XNA есть возможность создания простых примитивов.
В этой главе мы с вами рассмотрим принципы создания простых обьектов из самых простых объектов, таких как точки и линии. Вообще любые примитивы "ручного" изготовления в XNA отрисовуются с помощью метода.
DrawUserPrimitives<>()
Начнем наше изучение с самого простейшего объекта - точки. Для того, что бы нарисовать точки нам надо создать список 3D вершин, которые определяют рисуемые точки. Для этого создадим отдельный метод с названием.

InitializePointList()

Результат так же визуально не отличается от предыдущего примера.
Так схематично можно представить индексацию или топологию примитивов.

В XNA есть еще один менее распространенный способ отрисовки треугольников. Это TriangleFan. Справедливости ради надо упомянуть и о нем. В переводе с английского слово Fun означает - веер. Уже из названия многим станет понятно, что и треугольники должны распологаться в виде веера.


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

В этой главе, мы рассмотрели и вершины, и полигоны. Теперь пришло время на основании этих знаний создать более сложный примитив, например куб.
Для этого создадим новый проект под названием myCube и создадим в нем уже известные и необходимые нам вершинный и индексный буферы.
private void InitializeVertices()
{
  vertexDeclaration = new VertexDeclaration(GraphicsDevice,  VertexPositionColor.VertexElements);
 vertices = new VertexPositionColor[8];
 vertices[0] = new VertexPositionColor(new Vector3(0, 300, 300), Color.White);
 vertices[1] = new VertexPositionColor(new Vector3(300, 300, 300), Color.White);
 vertices[2] = new VertexPositionColor(new Vector3(300, 0, 300), Color.White);
 vertices[3] = new VertexPositionColor(new Vector3(0, 0, 300), Color.White);
 vertices[4] = new VertexPositionColor(new Vector3(0, 300, 0), Color.White);
 vertices[5] = new VertexPositionColor(new Vector3(300, 300, 0), Color.White);
 vertices[6] = new VertexPositionColor(new Vector3(300, 0, 0), Color.White);
 vertices[7] = new VertexPositionColor(new Vector3(0, 0, 0), Color.White);
 // Вершинный буфер для точек
 vertexBuffer = new VertexBuffer(GraphicsDevice,  VertexPositionColor.SizeInBytes * (vertices.Length), BufferUsage.None);
 // передаем массив точек в вершинный буфер
vertexBuffer.SetData<VertexPositionColor>(vertices);
}
private void InitializeTriangleCube()
{
  // массив индексов для треугольников
  triangleStripIndices = new short[36] { 0, 1, 2,
                                         2, 3, 0,
                                         5, 4, 7,
                                         7, 6, 5,
                                         4, 5, 1,
                                         1, 0, 4,
                                         3, 2, 6,
                                         6, 7, 3,
                                         1, 5, 6,
                                         6, 2, 1,
                                         4, 0, 3,
                                         3, 7, 4};
    }
Количество индексов взято 36 потому что у куба шесть сторон, на каждой из них лежит по два треугольника, каждый из которых имеет три вершины. А значит получается 6 * 2 * 3 = 36. В данном случае самое важное не запутаться с расстановкой индексов. И после создания вершин и индексов отправляем данные в метод Draw
    protected override void Draw(GameTime gameTime)
    {
      GraphicsDevice.Clear(Color.CornflowerBlue);
      GraphicsDevice.RenderState.FillMode = FillMode.WireFrame;
      GraphicsDevice.RenderState.CullMode = CullMode.None;
      GraphicsDevice.VertexDeclaration = vertexDeclaration;
      effect.World = Matrix.CreateRotationX(0.01f) * Matrix.CreateRotationY(0.01f) * Matrix.CreateRotationZ(0.01f) * effect.World;
      effect.Begin();
      foreach (EffectPass pass in effect.CurrentTechnique.Passes)
      {
        pass.Begin();
        graphics.GraphicsDevice.DrawUserIndexedPrimitives(PrimitiveType.TriangleList, vertices, 0, 8, triangleStripIndices, 0, 12);
        pass.End();
      }
      effect.End();
      base.Draw(gameTime);
    }
используя PrimitiveType.TriangleList для рисования полигонов. В результате у нас получился самый настоящий куб.

На данный момент у нас получился куб состоящий из сетки, как и все трехмерные модели. Однако в компьютерных играх, модели кажутся нам объемными, стены гладкие, земля хоть и не гладкая, но и не сетчатая. Как же нам превратить наш куб в подобие вышесказанного. Если мы укажем рендеру режим заливки
GraphicsDevice.RenderState.FillMode = FillMode.Solid;
то получим белый куб, такой как на рисунке

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

effect.VertexColorEnabled = true;
И в момент создания вершин мы каждой из них присвоим различные цвета
      vertices[0] = new VertexPositionColor(new Vector3(0, 300, 300), Color.White);
      vertices[1] = new VertexPositionColor(new Vector3(300, 300, 300), Color.Green);
      vertices[2] = new VertexPositionColor(new Vector3(300, 0, 300), Color.Blue);
      vertices[3] = new VertexPositionColor(new Vector3(0, 0, 300), Color.Red);
      vertices[4] = new VertexPositionColor(new Vector3(0, 300, 0), Color.Yellow);
      vertices[5] = new VertexPositionColor(new Vector3(300, 300, 0), Color.Violet);
      vertices[6] = new VertexPositionColor(new Vector3(300, 0, 0), Color.Pink);
      vertices[7] = new VertexPositionColor(new Vector3(0, 0, 0), Color.Black);
то получим раскрашенный куб с разноцветными сторонами

Завершающим этапом знакомства с генерированием примитивов можно назвать текстурирование. Создадим новый проект с названием myCubeTextured. Для корректного отображения нам понадобятся вершинный и индексный буферы.
VertexBuffer vertexBuffer;
IndexBuffer indexBuffer;
И естественно структура для хранения информации о вершинах. В отличии от предыдущего примера мы теперь будем использовать
VertexPositionTexture[] cubeVertices;
а не
VertexPositionColored[] cubeVertices;
Потому, что теперь вершина должна хранить не цвет, а координату текстуры и, само собой разумеется, позицию в трехмерном пространстве. Еще нам потребуется массив индексов, так же как и в предыдущем примере.
short[] cubeIndices;
Его величина по прежнему остаётся равной 36, по той же причине, ведь мы собираемся рисовать куб с помощью TriangleList, а не TriangleStrip. Далее следуют два метода которые заполнят данными оба буфера.
    private void InitializeVertices()
    {
      vertexDeclaration = new VertexDeclaration(GraphicsDevice, VertexPositionTexture.VertexElements);
      cubeVertices = new VertexPositionTexture[36];

      //Установка параметров точек, которые будут использованы для рисования фигуры
      Vector3 topLeftFront = new Vector3(0, 300.0f, 300.0f);
      Vector3 bottomLeftFront = new Vector3(0, 0, 300.0f);
      Vector3 topRightFront = new Vector3(300.0f, 300.0f, 300.0f);
      Vector3 bottomRightFront = new Vector3(300.0f, 0, 300.0f);
      Vector3 topLeftBack = new Vector3(0, 300.0f, 0);
      Vector3 topRightBack = new Vector3(300.0f, 300.0f, 0);
      Vector3 bottomLeftBack = new Vector3(0, 0, 0);
      Vector3 bottomRightBack = new Vector3(300.0f, 0, 0);
      // Координаты текстуры
      Vector2 textureTopLeft = new Vector2(0.0f, 0.0f);
      Vector2 textureTopRight = new Vector2(1.0f, 0.0f);
      Vector2 textureBottomLeft = new Vector2(0.0f, 1.0f);
      Vector2 textureBottomRight = new Vector2(1.0f, 1.0f);
      // Массив для хранения списка вершин
      // Он используется для передачи данных в вершинный буфер
      cubeVertices = new VertexPositionTexture[36];
      // Передняя часть фигуры
      cubeVertices[0] =
          new VertexPositionTexture(
          topLeftFront, textureTopLeft); // 0
      cubeVertices[1] =
          new VertexPositionTexture(
          bottomLeftFront, textureBottomLeft); // 1
      cubeVertices[2] =
          new VertexPositionTexture(
          topRightFront, textureTopRight); // 2
      cubeVertices[3] =
          new VertexPositionTexture(
          bottomRightFront, textureBottomRight); // 3
      // Задняя часть фигуры
      cubeVertices[4] =
          new VertexPositionTexture(
          topLeftBack, textureTopRight); // 4
      cubeVertices[5] =
          new VertexPositionTexture(
          topRightBack, textureTopLeft); // 5
      cubeVertices[6] =
          new VertexPositionTexture(
          bottomLeftBack, textureBottomRight); //6
      cubeVertices[7] =
          new VertexPositionTexture(
          bottomRightBack, textureBottomLeft); // 7
      // Верхняя часть фигуры
      cubeVertices[8] =
          new VertexPositionTexture(
          topLeftFront, textureBottomLeft); // 8
      cubeVertices[9] =
          new VertexPositionTexture(
          topRightBack, textureTopRight); // 9
      cubeVertices[10] =
          new VertexPositionTexture(
          topLeftBack, textureTopLeft); // 10
      cubeVertices[11] =
          new VertexPositionTexture(
          topRightFront, textureBottomRight); // 11
      // Нижняя часть фигуры
      cubeVertices[12] =
          new VertexPositionTexture(
          bottomLeftFront, textureTopLeft); // 12
      cubeVertices[13] =
          new VertexPositionTexture(
          bottomLeftBack, textureBottomLeft); // 13
      cubeVertices[14] =
          new VertexPositionTexture(
          bottomRightBack, textureBottomRight); // 14
      cubeVertices[15] =
          new VertexPositionTexture(
          bottomRightFront, textureTopRight); // 15
      // Левая часть фигуры
      cubeVertices[16] =
          new VertexPositionTexture(
          topLeftFront, textureTopRight); // 16
      cubeVertices[17] =
          new VertexPositionTexture(
          bottomLeftFront, textureBottomRight); // 17
      cubeVertices[18] =
          new VertexPositionTexture(
          topRightFront, textureTopLeft); // 18
      cubeVertices[19] =
          new VertexPositionTexture(
          bottomRightFront, textureBottomLeft); // 19

      // Вершинный буфер для точек
      vertexBuffer = new VertexBuffer(GraphicsDevice, VertexPositionTexture.SizeInBytes * (cubeVertices.Length), BufferUsage.None);
      // передаем массив точек в вершинный буфер
      vertexBuffer.SetData<VertexPositionTexture>(cubeVertices);
    }
Подготовили вершинный буфер
    private void InitializeIndices()
    {
      // С помощью этого массива определяем, к каким частям фигуры
      //Относятся те или иные компоненты массива cubeVertices
      cubeIndices = new short[] {
                                     0,  1,  2,  // Передняя //плоскость
                                     1,  3,  2,
                                     4,  5,  6,  // Задняя //плоскость
                                     6,  5,  7,
                                     8,  9, 10,  // Верхняя //плоскость
                                     8, 11,  9,
                                    12, 13, 14,  // Нижняя //плоскость
                                    12, 14, 15,
                                    16, 13, 17,  // Левая //плоскость
                                    10, 13, 16,
                                    18, 19, 14,  // Правая //плоскость
                                     9, 18, 14 };
      //Индексный буфер
      indexBuffer = new IndexBuffer(graphics.GraphicsDevice,
          sizeof(short) * cubeIndices.Length,
          BufferUsage.None,
          IndexElementSize.SixteenBits);
      //Добавляем данные в индексный буфер
      indexBuffer.SetData<short>(cubeIndices);
    }
Подготовили индексный буфер. В качестве эфекта для отображения используем старый добрый BasicEffect. Осталось только передать данные в метод Draw для рисования, и не забыть передать шейдеру саму текстуру, которую мы хотим видеть на сторонах нашего куба. Как все это сделать, показано ниже.
    protected override void Draw(GameTime gameTime)
    {
      GraphicsDevice.Clear(Color.CornflowerBlue);
      GraphicsDevice.RenderState.FillMode = FillMode.Solid;
      GraphicsDevice.RenderState.CullMode = CullMode.None;
      GraphicsDevice.VertexDeclaration = vertexDeclaration;     
      graphics.GraphicsDevice.Indices = indexBuffer;     
      graphics.GraphicsDevice.Vertices[0].SetSource(vertexBuffer, 0, VertexPositionTexture.SizeInBytes);
      effect.World = Matrix.CreateRotationX(0.01f) * Matrix.CreateRotationY(0.01f) * Matrix.CreateRotationZ(0.01f) * effect.World;
      effect.TextureEnabled = true;
      effect.Texture = Content.Load<Texture2D>("picture");
      effect.Begin();
      foreach (EffectPass pass in effect.CurrentTechnique.Passes)
      {
        pass.Begin();
        graphics.GraphicsDevice.DrawUserIndexedPrimitives(PrimitiveType.TriangleList, cubeVertices, 0, cubeVertices.Length, cubeIndices, 0, 12);
        pass.End();
      }
      effect.End();
      base.Draw(gameTime);
    }

И если вы всё сделали верно, то результатом станет следующее изображение.

На этом мы заканчиваем знакомство с примитивами. В качестве упражнения советую вам создать несколько собственных геометрических фигур на досуге. Принципы создания одинаковы как для кубов, так и для всех остальных объектов.







 в нем создадим два экземпляра классов
VertexDeclaration vertexDeclaration;
VertexPositionColor[] pointList;
vertexDeclaration = new VertexDeclaration(GraphicsDevice, VertexPositionColor.VertexElements);
pointList = new VertexPositionColor[8];
и заполним  pointList в цикле
for (int x = 0; x < points / 2; x++)
{
  for (int y = 0; y < 2; y++)
  {
  pointList[(x * 2) + y] = new VertexPositionColor(new Vector3(x  * 100, y * 100, 0), Color.White);
  }
}
указав каждой вершине ее позицию в пространстве и  ее цвет. После этого нам в обязательном порядке нужно  создать вершинный буфер для точек (вершин)
vertexBuffer = new VertexBuffer(GraphicsDevice, VertexPositionColor.SizeInBytes * (pointList.Length), BufferUsage.None);
и передать массив точек в вершинный буфер
vertexBuffer.SetData<VertexPositionColor>(pointList);
Не лишним будет подумать и об шейдере с помощью которого мы будем выводить на экран наши точки. Воспользуемся старым добрым Basic Effect, но его инициализацию осуществим в методе
protected override void Initialize()
а не в методе отрисовки точек. Выглядит это вот так:
  protected override void Initialize()
  {
      effect = new BasicEffect(graphics.GraphicsDevice, null);
      effect.World = Matrix.CreateTranslation
      (GraphicsDevice.Viewport.Width / 2f - 150,
       GraphicsDevice.Viewport.Height / 2f - 50, 0);
      effect.View = Matrix.CreateLookAt
      (new Vector3(0.0f, 0.0f, 1.0f),
       Vector3.Zero, Vector3.Up);
      effect.Projection = Matrix.CreateOrthographicOffCenter
      (0, (float)GraphicsDevice.Viewport.Width,
      (float)GraphicsDevice.Viewport.Height, 0, 1.0f, 1000.0f);
     
      InitializePointList();
      base.Initialize();
  }
как видите все параметры касающиеся матриц создаются и присваиваются здесь. После подготовки нашего эффекта нам остаётся лишь передать наш PointList в метод Draw для отрисовки.
protected override void Draw(GameTime gameTime)
{
  GraphicsDevice.Clear(Color.CornflowerBlue);
  GraphicsDevice.VertexDeclaration = vertexDeclaration;
  GraphicsDevice.RenderState.PointSize = 10;
   effect.Begin();
   foreach (EffectPass pass in effect.CurrentTechnique.Passes)
   {
     pass.Begin();
         graphics.GraphicsDevice.DrawUserPrimitives(PrimitiveType.PointLi  st, pointList, 0, 8);
       pass.End();
    }
      effect.End();
  base.Draw(gameTime);
}
как видите здесь мы установили размер точки равной 10 , что бы лучше было видно, и методом DrawUserPrimitives() нарисовали наши вершины.
при этом передавая первым параметром тип примитива, в нашем случае это точки (тип примитива определяет как данные в переданном массиве вершин будут интерпретированы и показаны), вторым параметром данные о точках (позиция и цвет) хранящиеся в массиве pointList, третьим параметром номер примитива с которого начинать рисовать (у нас это может быть значение от 0 до 8), и накнец, четвертым параметром количество примитивов для отрисовки начиная с указанного. Исходный код примера находится на диске в папке Samples / myPoints.
Аналогично точкам можно рисовать линии. Рисуются они от точки до точки по кратчайшему расстоянию. Для наглядной реализации мы добавим к нашему примеру метод создания индексов для линий.
    private void InitializeLineList()
    {
      // массив индексов для линий
      lineListIndices = new short[(8 * 2) - 2];
      // заполняем индексы
      for (int i = 0; i < 8 - 1; i++)
      {
        lineListIndices[i * 2] = (short)(i);
        lineListIndices[(i * 2) + 1] = (short)(i + 1);
      }
    }
Другими словами, данный код равнозначен следующему и определяет линии соеденяющие наши точки - pointList[0] и pointList[1], pointList[1] и pointList[2], и так далее 7 линий между 8-ми точек.
lineListIndices = new short[14]{ 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7 };
а в методе Draw вместо функции
DrawUserPrimitives()
применим функцию
DrawUserIndexedPrimitives()
и наш метод для рисования линий станет таким
protected override void Draw(GameTime gameTime)
{
  GraphicsDevice.Clear(Color.CornflowerBlue);
  GraphicsDevice.VertexDeclaration = vertexDeclaration;
  effect.Begin();
  foreach (EffectPass pass in  effect.CurrentTechnique.Passes)
  {
     pass.Begin();
     graphics.GraphicsDevice.DrawUserIndexedPrimitives
     (PrimitiveType.LineList,
      pointList,
      0,
      8,
      lineListIndices,
      0,
      7);
      pass.End();
   }
   effect.End();
  base.Draw(gameTime);
}
где первым параметром передается тип примитива LineList, далее все так же как и с точками до пятого параметра,а им передаётся LineListIndices массив индексов, шестым и седьмым параметрами номер и количество линий. В качестве шейдера по прежнему используется BasicEffect. На рисунке ниже изображен результат этих изменений.
 Следующий пример показывает как вместо линий рисовать, так называемую, ленту линий или LineStrip.
Вся разница между этим и предыдущим примером заключается в том, что для ленты нам понадобится в двое меньше индексов так как лента подразумевает последовательно соединенные примитивы (точки). Метод для создания индексов теперь будет выглядеть так.
    private void InitializeLineStrip()
    {
      // массив индексов для линий
      lineStripIndices = new short[8];
      // заполняем индексы
      for (int i = 0; i < 8; i++)
      {
        lineStripIndices[i] = (short)(i);
      }
    }
или
lineStripIndices = new short[8]{ 0, 1, 2, 3, 4, 5, 6, 7 };
Важно сказать, что рисование лент с точки зрения производительности более выгодное, так как индексы выводимых вершин содержат меньшее количество повторений. Результат изображен на рисунке ниже и внешне ничем не отличается от предыдущего примера.
Следующим этапом будет создание треугольника или полигона. Как уже упоминалось ранее из полигонов состоят практически все трехмерные модели. Сразу скажу, что треугольники, подобно линиям, тоже можно рисовать как из списка так и лентой. Следующий пример показывает способ рисования из списка или TriangleList. Создаем новый массив индексов, в котором будет пронумерованный список треугольников, по 3 вершины на один треугольник. Массив вершин не меняем. Метод для этого выглядит так.
 private void InitializeTriangleList()
{
   int width = 4;
   int height = 2;
      // массив индексов для треугольников
      triangleListIndices = new short[(width - 1) * (height - 1)  * 6];
  for (int x = 0; x < width - 1; x++)
  {
    for (int y = 0; y < height - 1; y++)
    {
          triangleListIndices[(x + y * (width - 1)) * 6] = (short)(2 * x);
          triangleListIndices[(x + y * (width - 1)) * 6 + 1] = (short)(2 * x + 1);
          triangleListIndices[(x + y * (width - 1)) * 6 + 2] = (short)(2 * x + 2);
          triangleListIndices[(x + y * (width - 1)) * 6 + 3] = (short)(2 * x + 2);
          triangleListIndices[(x + y * (width - 1)) * 6 + 4] = (short)(2 * x + 1);
          triangleListIndices[(x + y * (width - 1)) * 6 + 5] = (short)(2 * x + 3);
        }
      }
    }
Этот код индексирует вершины так что каждые 3 точки у нас образуют один треугольник, причем в каждом треугольнике есть общие вершины с соседним. Ниже аналог этого кода
triangleListIndices = new short[18]{ 0, 1, 2, 2, 1, 3, 2, 3, 4, 4, 3, 5, 4, 5, 6, 6, 5, 7 };
Выводим список треугольников на экран используя функцию DrawUserIndexedPrimitives() которая в качестве первого аргумента принимает тип выводимого примитива - PrimitiveType.TriangleList. И еще, перед отрисовкой важно указать видеокарте режим рендеринга.
GraphicsDevice.RenderState.FillMode = FillMode.WireFrame;
GraphicsDevice.RenderState.CullMode = CullMode.None;
Здесь мы назначили режим отображения обьектов в виде сетки.
В результате  метод Draw будет выглядеть так:
    protected override void Draw(GameTime gameTime)
    {
      GraphicsDevice.Clear(Color.CornflowerBlue);
      GraphicsDevice.RenderState.FillMode = FillMode.WireFrame;
      GraphicsDevice.RenderState.CullMode = CullMode.None;
      GraphicsDevice.VertexDeclaration = vertexDeclaration;
      effect.Begin();
      foreach (EffectPass pass in effect.CurrentTechnique.Passes)
      {
        pass.Begin();
        graphics.GraphicsDevice.DrawUserIndexedPrimitives(PrimitiveType.TriangleList, pointList, 0, 8, triangleListIndices, 0, 6);
        pass.End();
      }
      effect.End();
      base.Draw(gameTime);
    }
а итоговое изображение так:
Следующий пример демонстрирует рисование тех же треугольников, но с помощью ленты, а не списка. Как вы уже наверное догадались, изменений потребует лишь метод индексирующий вершины. Этот метод будет выглядеть так
    private void InitializeTriangleStrip()
    {
      // массив индексов для треугольников
      triangleStripIndices = new short[8];
      // заполняем индексы
      for (int i = 0; i < 8; i++)
      {
        triangleStripIndices[i] = (short)i;
      }
    }
он практически идентичен методу InitializeLineStrip()из примера myLineStrip. И результат его деятельности так же можно представить в виде
triangleStripIndices = new short[8]{ 0, 1, 2, 3, 4, 5, 6, 7 };
В лентах, треугольники последовательно соеденены между собой, так что индексов нам нужно намного меньше – каждый последующий треугольник использует две вершины предыдущего треугольника. Это наиболее эффективный способ вывода треугольников.

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

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

Physically Based Rendering