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

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

Instancing.


В этой главе, мы с вами рассмотрим технологию отображения объектов имеющую название instancing, что в переводе с английского означает "отдельный". Суть этой техники такова, что за один вызов метода Draw(), мы отрисовуем не один обьект, как например наш куб, а множество таких же кубов или чего либо еще. Вот здесь важно сразу понять разницу и преимущество техники instancing. Pечь идет о том, что бы за единственный вызов метода Draw() получить к примеру 10000 моделей на экране. Как же такое возможно, спросите вы? А возможно такое стало с приходом видеокарт, которые поддерживают третью шейдерную модель, именно в ней за один вызов драйвера. Т.е., нам достаточно один раз вызвать DrawIndexedPrimitives(…) и получить на экране не однин, а несколько mesh’ей, расположенных по разным координатам и повернутых под разными углами.
Многие современные игры с помощью этой техники рисуют такие объекты как трава, астероиды и прочие множественные модели, счет которых может переваливать за тысячи экземпляров, и сами понимаете, если каждый раз вызывать Draw(), то нарисовав одну лишь траву, количество кустов которой например 10000, FPS (количество кадров в секунду) вашей игры может сильно пострадать. Что бы избежать подобных проблем, на помощь придет техника instancing.
 Для более глубокого изучения этой замечательной техники заведем новый проект под названием instancing. В нем мы создадим уже знакомый нам примитив, куб с помощью VertexPositionColored вершин и нарисуем его 10000 раз, и так начнем.
Глобальными переменными у нас будут выступать:


        private VertexDeclaration MyVertexDeclaration;
        private VertexPositionColor[] myVertices;
        private int[] myIndices;
        private VertexBuffer myVertexBuffer;
        private IndexBuffer myIndexBuffer;
        private Effect myEffect;
        private Matrix[] myInstansedWorld;
        private const int sizeofMatrix = sizeof(float) * 16;
        private int instanceDataSize;


Их назначение понятно из названия если вы внимательно ознакомились с предыдущими примерами, тем более что они уже использовались нами ранее. Далее создадим наш куб:


protected override void Initialize()
        {
          myEffect = Content.Load<Effect>("Instancing");
          //Создаем свой VertexDeclaration
          VertexElement[] elements = GetVertexElementVertexPositionColorMatrix();
          MyVertexDeclaration = new VertexDeclaration(graphics.GraphicsDevice, elements);
          // Строим треугольник и создаем вершинный и индексный буфер
          myVertices = new VertexPositionColor[8];
          myIndices = new int[36];

В методе Update() для большей наглядности мы пересчитываем наши матрицы мира так, что бы кубики вращались.

         protected override void Update(GameTime gameTime)
        {
          if(Keyboard.GetState().IsKeyDown(Keys.Escape))
          {
            this.Exit();
          }
          float ang = 0.001f;
          Matrix matrRot = Matrix.CreateRotationX(ang * (float)gameTime.ElapsedGameTime.TotalMilliseconds) *
                           Matrix.CreateRotationY(ang * (float)gameTime.ElapsedGameTime.TotalMilliseconds) *
                           Matrix.CreateRotationZ(ang * (float)gameTime.ElapsedGameTime.TotalMilliseconds);
          for (int i = 0; i < myInstansedWorld.Length; i++)
          {
            myInstansedWorld[i] = matrRot * myInstansedWorld[i];
          }
            base.Update(gameTime);
        }

 Далее следует самый главный метод Draw(). В нем следует особое внимание обратить на две новые сущности

DynamicVertexBuffer instanceDataStream
VertexStreamCollection vertexCollection

Благодаря этим классам и реализуется наша техника.

protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);
            // настримваем вывод
            graphics.GraphicsDevice.VertexDeclaration = MyVertexDeclaration;
            graphics.GraphicsDevice.Indices = myIndexBuffer;
            //graphics.GraphicsDevice.RenderState.DepthBufferEnable = true;
            // инициируем динамический вершинный буфер - поток с данными о матрицах
            DynamicVertexBuffer instanceDataStream = new DynamicVertexBuffer(graphics.GraphicsDevice, instanceDataSize, BufferUsage.WriteOnly);
            instanceDataStream.SetData(myInstansedWorld, 0, myInstansedWorld.Length, SetDataOptions.Discard);
            // назначаем вершинные буферы нашего GraphicsDevice
            VertexStreamCollection vertexCollection = graphics.GraphicsDevice.Vertices;
            vertexCollection[0].SetSource(myVertexBuffer, 0, VertexPositionColor.SizeInBytes);
            vertexCollection[0].SetFrequencyOfIndexData(myInstansedWorld.Length);
            vertexCollection[1].SetSource(instanceDataStream, 0, sizeofMatrix);
            vertexCollection[1].SetFrequencyOfInstanceData(1);
            // настраиваем effect
            myEffect.Parameters["matVP"].SetValue(Matrix.CreateLookAt(new Vector3(-40f, 40f, 40f), new Vector3(0f, 15f, 0f), new Vector3(0f, 1f, 0f)) *
                                                  Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(30), 1.5f, 0.01f, 2000.0f));           
            // стандартный вывод
            myEffect.Begin();
            foreach (EffectPass pass in myEffect.CurrentTechnique.Passes)
            {
              pass.Begin();
              graphics.GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, myVertices.Length, 0, myIndices.Length);
              pass.End();
            }
            myEffect.End();
            // освобождаем поток с данными о матрицах
            instanceDataStream.Dispose();
            // очищаем вершинные буферы нашего GraphicsDevice
            vertexCollection[0].SetSource(null, 0, 0);
            vertexCollection[1].SetSource(null, 0, 0);
            base.Draw(gameTime);
        }
Запускаем программу и видим желаемый результат



Еще нужно упомянуть и о шейдере, с помощью которого мы осуществили вывод изображение
float4x4 matVP  : VIEWPROGECTION;
struct VS_INPUT
{
    float4  Position  : POSITION0;
    float4  Color   : COLOR0;
    float4x4 InstanceWorld : TEXCOORD0;
};
struct VS_OUTPUT
{
    float4  Position  : POSITION0;
    float4  Color   : COLOR0;
};
VS_OUTPUT VS(VS_INPUT input)
{
 VS_OUTPUT output = (VS_OUTPUT)0;
    output.Position = mul(input.Position, mul(transpose(input.InstanceWorld), matVP));
    output.Color = input.Color;
    return output;
}
struct PS_INPUT
{
 float4 Position  : POSITION0;
 float4 Color  : COLOR0;
};
float4 PS(PS_INPUT input) : COLOR0
{
 return input.Color;
}
technique Instancing
{
    pass Pass1
    {
        VertexShader = compile vs_3_0 VS();
        PixelShader  = compile ps_3_0 PS();
    }
}

как видите он достаточно прост. Здесь вам все должно быть знакомо, за исключением одного момента. Обратите внимание на стуруктуру VS_INPUT, а конкретно на поле:

{… float4x4 InstanceWorld:TEXCOORD0;}

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

        int frameRate = 0;
        int frameCounter = 0;
        TimeSpan elapsedTime = TimeSpan.Zero;
        string gameName;
Добавили глобальные переменные с которыми будем проводить операции. Далее в методе Update()
          elapsedTime += gameTime.ElapsedGameTime;
          if (elapsedTime > TimeSpan.FromSeconds(1))
          {
            elapsedTime -= TimeSpan.FromSeconds(1);
            frameRate = frameCounter;
            frameCounter = 0;
          }


щитаем количество кадров в секунду и в методе Draw() выводим это значение в Title нашего окна.
            frameCounter++;
            string fps = frameRate.ToString();
            this.Window.Title = gameName + "   " + fps + "fps";
в результате частота обновления буффера составила около 430 кадров в секунду.


Неплохой результат. Но и это еще не предел для нашего примера. Если вспомнить, что в методе Update() мы производим пересчет всех матриц мира для вращения, то становится понятно что перемножение 10000 матриц не может не украсть драгоценный FPS. Если это убрать, что же теперь получится? К стати показатели из моей статьи могут резко отличаться от ваших. И происходить это будет неизбежно, в зависимости от конфигурации вашего компьютера. Само собой разумеется, чем мощнее видеокарта, тем выше будут ваши значения. Важно знать, что в среде разработки XNA, создатели платформы зафиксиовали FPS по умолчанию равной около 60 кадров в секунду. Этого значения хватает для комфортного созерцания картинки на экране. Переопределенный метод


protected override void Draw(GameTime gameTime)


не вызывается чаще установленного значения, экономя тем самым ресурсы компьютера, ведь они как воздух нужны для обработки игровых данных подобных физике или искусственному интеллекту. К тому же XNA если помните, ориентирована на консольную приставку XBOX 360, а в ней есть ряд обязательных требований по графике. Что бы снять ограничения по FPS и увидеть максимально возможное значение надо написать две строчки в конструкторе класса:


IsFixedTimeStep = false;


говорит о том, что мы хотим фиксированый временной шаг. И


graphics.SynchronizeWithVerticalRetrace = false;


говорит о том, что мы выключили вертикальную синхронизацию.
Всё это необходимо сделать, иначе FPS, будет держаться на отметке 60.
 После всех изменений нашем случае получился прирост FPS около 780 кадров в секунду.
Как видите вообще отлично, на таком количестве обьектов получить такую FPS. И на конец самое интересное. Изменим наш код так, что бы модели рисовались в цикле, так же как в предидущих примерах. Закомментарим содержимое метода Draw() кроме кода отвечающего за вывод значения FPS, и напишем следующее:

GraphicsDevice.Clear(Color.CornflowerBlue);
          GraphicsDevice.VertexDeclaration = MyVertexDeclaration;
          for (int i = 0; i < 10000; i++)
          {
            eff.World = myInstansedWorld[i];
            eff.View = Matrix.CreateLookAt(new Vector3(-40f, 40f, 40f), new Vector3(0f, 15f, 0f), new Vector3(0f, 1f, 0f));
            eff.Projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(30), 1.5f, 0.01f, 2000.0f);
            eff.VertexColorEnabled = true;
            eff.Begin();
            foreach (EffectPass pass in eff.CurrentTechnique.Passes)
            {
              pass.Begin();
              graphics.GraphicsDevice.DrawUserIndexedPrimitives(PrimitiveType.TriangleList, myVertices, 0, 8, myIndices, 0, 12);
              pass.End();
            }
            eff.End();
          }


Стартуем и смотрим на результат = 130;
Комментарии, как говорится, излишни. Где можно использовать instancing ( в нашем случае правильнее будет сказать hardware instancing ), потому что для реализации данной техники использовались исключительно ресурсы видеокарты!

Наверняка, помимо отрисовки примитивов с помощью instancinga, вам понадобится умение рисовать и готовые модели. Об этом вторая половина этой главы. Как всегда создадим новый проект с названием myInstancedModels. Нам понадобятся такие переменные

    public List<InstancedModelPart> ModelParts = new List<InstancedModelPart>();
    private List<Matrix> instanceMatrices;
    private Model myModel;
    private int myModelCount = 10000;
    private Effect instancedEffect;
    private Texture2D myTexture;

Метод Load() выглядит таким образом

    protected override void LoadContent()
    {
      spriteBatch = new SpriteBatch(GraphicsDevice);
      instancedEffect = Content.Load<Effect>("InstancedModel");
      instanceMatrices = new List<Matrix>();
      myTexture = Content.Load<Texture2D>("10");
      myModel = Content.Load<Model>("box");
      foreach (ModelMesh mesh in myModel.Meshes)
      {
        foreach (ModelMeshPart part in mesh.MeshParts)
        {
          ModelParts.Add(new InstancedModelPart(this, part, mesh.VertexBuffer, mesh.IndexBuffer));
        }
      }
      for (int X = 0; X < Math.Sqrt((double)myModelCount); X++)
      {
        for (int Z = 0; Z < Math.Sqrt((double)myModelCount); Z++)
        {
          instanceMatrices.Add(Matrix.CreateScale(0.1f) * Matrix.CreateTranslation(new Vector3(X * 2, 0, Z * 2)));
        }
      }
    }
здесь мы последовательно загрузили наш эффект, с помощью которого непосредственно будет происходить отрисовка моделей. Далее создается список мировых матриц, ктотрый будет хранить данные для каждой из множества моделей. Далее идет загрузка текстуры, которую мы хотим видеть на нашей модели, и сама модель. После этого запускается цикл в котором мы заполняем список ModelParts данными из модели. Как вы уже убедились ранее, каждая модель состоит из сетки, каждая сетка из треугольников (шестиугольные и восьмиугольные сетки мы пока не рассматриваем). Для того, что бы видеокарта которая как мы выяснили кроме точек, линий, и как результат того и другого - треугольников, в принципе рисовать ничего не умеет, смогла нарисовать модель, ей как и вслучае с примитивами необходим тот же вершинный буфер и буфер индексов. Где же их взять для нашей модели, ведь руками написать их задача не для слабонервных,тем более, если модель имеет сложную форму не такую как куб, а например, как тело человека. Ответ заключается в том, что все эти данные хранятся в самой модели, а попадают они туда в процессе создания во всевозможных генераторах геометрии таких как 3DS Max и тд. Наша задача их оттуда получить. Для этого мы создадим отдельный класс InstancedModelPart ,да и так будет нагляднее показан процесс изьятия данных.

    public InstancedModelPart(Game game, ModelMeshPart part, VertexBuffer vertexBuffer, IndexBuffer indexBuffer)
    {
      this.game = game;
      //передаем данные из параметров в переменые
      _primitiveCount = part.PrimitiveCount;
      _vertexCount = part.NumVertices;
      _vertexStride = part.VertexStride;
      _vertexDeclaration = part.VertexDeclaration;
      _vertexBuffer = vertexBuffer;
      _indexBuffer = indexBuffer;
      originalVertexDeclaration = part.VertexDeclaration.GetVertexElements();
      VertexElement[] extraElements = new VertexElement[4];
      short offset = 0;
      byte usageIndex = 1;
      short stream = 1;
      //получаем рвзмер Vector4 в байтах
      short sizeOfVector4 = sizeof(float) * 4;
      for (int i = 0; i < extraElements.Length; i++)
      {
        extraElements[i] = new VertexElement(stream, offset, VertexElementFormat.Vector4, VertexElementMethod.Default, VertexElementUsage.TextureCoordinate, usageIndex);
        offset += sizeOfVector4;
        usageIndex++;
      }
      _vertexDeclaration.Dispose();
      int length = originalVertexDeclaration.Length + extraElements.Length;
      VertexElement[] elements = new VertexElement[length];
      originalVertexDeclaration.CopyTo(elements, 0);
      extraElements.CopyTo(elements, originalVertexDeclaration.Length);
      _vertexDeclaration = new VertexDeclaration(game.GraphicsDevice, elements);
    }
Далее в методе Load() запускается еще один цикл, который заполняет список instanceMatrices матрицами мира так, что бы получился квадрат. Math.Sqrt((double)myModelCount)нужен для того, чтобы указав общее количество моделей, которое хотим увидеть, в данном случае

 private int myModelCount = 10000;

у нас получился квадрат. Далее следует главный метод Draw().

    protected override void Draw(GameTime gameTime)
    {
      GraphicsDevice.Clear(Color.CornflowerBlue);
      instancedEffect.CurrentTechnique = instancedEffect.Techniques["HardwareInstancing"];
      instancedEffect.Parameters["View"].SetValue(Matrix.CreateLookAt(new Vector3(100, 100, -100), new Vector3(100, -50, 200), Vector3.Up));
      instancedEffect.Parameters["Projection"].SetValue(Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(40), 1.6f, 0.1f, 1000f));
      instancedEffect.Parameters["Texture"].SetValue(myTexture);
      foreach (InstancedModelPart part in ModelParts)
      {
        GraphicsDevice.VertexDeclaration = part.VertexDeclaration;
        GraphicsDevice.Vertices[0].SetSource(part.VertexBuffer, 0, part.VertexStride);
        GraphicsDevice.Indices = part.IndexBuffer;
        instancedEffect.Begin();
        foreach (EffectPass pass in instancedEffect.CurrentTechnique.Passes)
        {
          pass.Begin();
          Matrix[] subList = new Matrix[myModelCount];
          instanceMatrices.CopyTo(0, subList, 0, myModelCount);
          const int sizeofMatrix = sizeof(float) * 16;
          int instanceDataSize = sizeofMatrix * subList.Length;
          DynamicVertexBuffer instanceDataStream = new DynamicVertexBuffer(GraphicsDevice, instanceDataSize, BufferUsage.WriteOnly);
          instanceDataStream.SetData(subList, 0, subList.Length, SetDataOptions.Discard);
          VertexStreamCollection vertices = GraphicsDevice.Vertices;
          vertices[0].SetFrequencyOfIndexData(subList.Length);
          vertices[1].SetSource(instanceDataStream, 0, sizeofMatrix);
          vertices[1].SetFrequencyOfInstanceData(1);
          GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, part.VertexCount, 0, part.PrimitiveCount);
          instanceDataStream.Dispose();
          vertices[0].SetSource(null, 0, 0);
          vertices[1].SetSource(null, 0, 0);
          pass.End();
        }
        instancedEffect.End();
      }
      base.Draw(gameTime);
    }
Здесь практически всё вам уже известно. Те же DynamicVertexBuffer и  VertexStreamCollection ну и конечно же
GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, part.VertexCount, 0, part.PrimitiveCount);
В качестве модели которую будем "размножать", был выбран куб




В результате всего вышесказанного получится изображение



состоящее из 10000 обьектов построеных, как и планировалось, квадратом.
А на последок скажу пару слов о шейдере

// Настройки для камеры
float4x4 View;
float4x4 Projection;
// Это приложение использует модель освещения Ламберта
float3 LightDirection ;
float3 DiffuseLight = 1;
float3 AmbientLight = 1;
texture Texture;
sampler Sampler = sampler_state
{
    Texture = (Texture);
   
    MinFilter = Linear;
    MagFilter = Linear;
    MipFilter = Linear;   
    AddressU = Wrap;
    AddressV = Wrap;
};
struct VertexShaderInput
{
    float4 Position : POSITION0;
    float3 Normal : NORMAL0;
    float2 TextureCoordinate : TEXCOORD0;
};
struct VertexShaderOutput
{
    float4 Position : POSITION0;
    float4 Color : COLOR0;
    float2 TextureCoordinate : TEXCOORD0;
};
VertexShaderOutput VertexShaderCommon(VertexShaderInput input, float4x4 instanceTransform)
{
    VertexShaderOutput output;


//Применяя мировуюматрицу и матрицы камеры расчитываем позицию в //пространстве.
    float4 worldPosition = mul(input.Position, instanceTransform);
    float4 viewPosition = mul(worldPosition, View);
    output.Position = mul(viewPosition, Projection);
    // Расчитываем свет
    float3 worldNormal = mul(input.Normal, instanceTransform);   
    float diffuseAmount = max(-dot(worldNormal, LightDirection), 0);   
    float3 lightingResult = saturate(diffuseAmount * DiffuseLight + AmbientLight);   
    output.Color = float4(lightingResult, 1);
    // Копируем текстурные координаты
    output.TextureCoordinate = input.TextureCoordinate;
    return output;
}
VertexShaderOutput HardwareInstancingVertexShader(VertexShaderInput input, float4x4 instanceTransform : TEXCOORD1)
{
    return VertexShaderCommon(input, transpose(instanceTransform));
}
float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
    return tex2D(Sampler, input.TextureCoordinate) * input.Color;
}
technique HardwareInstancing
{
    pass Pass1
    {
        VertexShader = compile vs_3_0  HardwareInstancingVertexShader();
        PixelShader = compile ps_3_0
 PixelShaderFunction();
    }
}
Шейдер довольно простой и предназначен только лишь для отображения модели без каких либо преобразований, использующий модель освещения Ламберта (о ней я ранее уже упоминал). Особого внимания требует лишь float4x4 instanceTransform. Именно сюда приходят данные о мировых матрицах для обработки.
На этом мы заканчиваем знакомство с Hardware Instancing. Думаю вы оцените эту технику и найдете ей применение.      

2 комментария:

Physically Based Rendering