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

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

Теневые карты (Shadow Maps)

Суть теневых карт заключается в следующем. Сцену рендерят в несколько проходов как и впредыдущем примере. Количество проходов может быть различным. Это зависит от многих факторов, смотря какое качество теней вы желаете получить. Но сейчас это не главное, я постараюсь объяснить сам принцип этого подхода. Первым делом мы рендерим сцену с позиции источника освещения. И делаем это хитрым способом, так как будто это не просто координата источника света, а камера. Иными словами источник света может видеть сцену. То что видет источник можно сохранить в текстуру. Сперва увиденное записывается в цель визуализации или RenderTarget2D, а после из нее в текстуру. Более того, эта текстура должна в себе хранить не просто вид сцены как мы привыкли его видеть обычно, с цветными обьектами, а информацию о глубине сцены. Иначе говоря записываем в текстуру, своего рода, буфер глубины. Делается это таким образом, что бы цвет (значение) пикселя соответствовал расстоянию от источника света до каждого действительного пикселя сцены. Что бы вам легче было представить вышесказанное рисунок ниже показывает пример такой текстуры глубины.


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


На рисунке точкой A показана поверхность видимая источнику света. Точкой B поверхность не видимая. Теперь надеюсь, что вам общая теоретическая часть понятна и для более конкретного изучения можно переходить к практике. Давайте создадим новый проект и назовем его myShadowWorld. Сразу же добавим в него компонент Camera. Он нам уже знаком по предыдущим примерам. Добавим так же и класс Terrain. Но в этот раз это уже будет не компонент с внешним вызовом функций, а обычный класс. Это нужно для того, что бы мы имели возможность вызывать метод отрисовки в любой момент когда нам это будет нужно, ведь сцену придется рендерить дважды.
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;

namespace myShadowWorld
{
public class Terrain
{
private int WIDTH;
private int HEIGHT;
private VertexBuffer vertexBuffer;
private IndexBuffer indexBuffer;
private VertexDeclaration vertexDecalaration;
private float[,] heightData;
Texture2D texture;
Game game;
public Terrain(Game game)
{
this.game = game;
}
public void Initialize()
{
texture = game.Content.Load<Texture2D>("Terrain/grass");
LoadHeightData(game.Content.Load<Texture2D>("Terrain/HeightMap"));
SetUpTerrainVertices(game.GraphicsDevice);
SetUpTerrainIndices(game.GraphicsDevice);
vertexDecalaration = new VertexDeclaration(game.GraphicsDevice, VertexPositionNormalTexture.VertexElements);
}
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);
}
public void Update()
{
}
public void Draw(Effect eff, string tech)
{
eff.Parameters["World"].SetValue(Matrix.CreateTranslation(0, 0, 0));
eff.Parameters["MeshTexture"].SetValue(game.Content.Load<Texture2D>("Terrain/grass"));
eff.CurrentTechnique = eff.Techniques[tech];
eff.Begin();
foreach (EffectPass pass in eff.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();
}
eff.End();
}
}
}
 Так же добавим класс Scene. В нем нет ничего нового для нас. Этот класс нарисует нам несколько моделей но не спомощью BasicEffect а с нашим эффектом.

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;
namespace myShadowWorld
{
  class Scene
  {
    Model tractor;
    Matrix tractorWorld;
    Model uaz;
    Matrix uazWorld;
    Model mi24;
    Matrix mi24World;
    Game game;
    Matrix[] transforms;
   
    public Scene(Game game)
    {
      this.game = game;
    }
    public void Load(Effect sEffect)
    {  
      tractor = game.Content.Load<Model>("Models/tractor");
      transforms = new Matrix[tractor.Bones.Count];
      tractor.CopyAbsoluteBoneTransformsTo(transforms);
      tractorWorld = transforms[0] *
        Matrix.CreateScale(2) *
        Matrix.CreateRotationY(0.5f) *
        Matrix.CreateTranslation(new Vector3(150, 0, 100));
      RemapModel(tractor, sEffect);
      mi24 = game.Content.Load<Model>("Models/mi24");
      transforms = new Matrix[mi24.Bones.Count];
      mi24.CopyAbsoluteBoneTransformsTo(transforms);
      mi24World = transforms[0] *
        Matrix.CreateScale(2) *
        Matrix.CreateRotationY(-0.3f) *
        Matrix.CreateTranslation(new Vector3(120, 0, 140));
      RemapModel(mi24, sEffect);
      uaz = game.Content.Load<Model>("Models/uaz");
      transforms = new Matrix[uaz.Bones.Count];
      uaz.CopyAbsoluteBoneTransformsTo(transforms);
      uazWorld = transforms[0] *
        Matrix.CreateScale(2) *
        Matrix.CreateRotationY(0.3f) *
        Matrix.CreateTranslation(new Vector3(140, 0, 170));
      RemapModel(uaz, sEffect);
    }
    public static void RemapModel(Model model, Effect effect)
    {
      foreach (ModelMesh mesh in model.Meshes)
      {
        foreach (ModelMeshPart part in mesh.MeshParts)
        {
          part.Effect = effect;
        }
      }
    }
    public void Update()
    {

    }
    public void Draw(string tech)
    {
      foreach (ModelMesh mesh in tractor.Meshes)
      {
        foreach (Effect effect in mesh.Effects)
        {
          effect.Parameters["World"].SetValue(tractorWorld);
          effect.Parameters["MeshTexture"].SetValue(game.Content.Load<Texture2D>("Models/traktor_color"));
          effect.CurrentTechnique = effect.Techniques[tech];
        }
        mesh.Draw();
      }
     
      foreach (ModelMesh mesh in mi24.Meshes)
      {
        foreach (Effect effect in mesh.Effects)
        {
          effect.Parameters["World"].SetValue(mi24World);
          effect.Parameters["MeshTexture"].SetValue(game.Content.Load<Texture2D>("Models/mi24_color"));
          effect.CurrentTechnique = effect.Techniques[tech];
        }
        mesh.Draw();
      }

      foreach (ModelMesh mesh in uaz.Meshes)
      {
        foreach (Effect effect in mesh.Effects)
        {
          effect.Parameters["World"].SetValue(uazWorld);
          effect.Parameters["MeshTexture"].SetValue(game.Content.Load<Texture2D>("Models/uazik_color"));
          effect.CurrentTechnique = effect.Techniques[tech];
        }
        mesh.Draw();
      }
   
    }
  }
}

Напомню что функция RemapModel служит для замены эффекта который присвоен модели по умолчанию импортером среды разработки, на наш эффект. Ну и наконец наш главный класс.
    GraphicsDeviceManager graphics;
    SpriteBatch spriteBatch;
    Camera CAM;
    Terrain terrain;
    Scene scene;
    Effect sEffect;
    Vector3 LightPos;

Эти переменные вам уже знакомы.
    RenderTarget2D shadowRT;
    DepthStencilBuffer shadowDB;
Цель визуализации для хранения данных о глубине сцены с позиции источника света. И буфер глубины который мы использовали в предыдущем примере.
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
shadowRT = new RenderTarget2D(GraphicsDevice, 1024 * 4, 1024 * 4, 0, SurfaceFormat.Rgb32);
shadowDB = new DepthStencilBuffer(GraphicsDevice, 1024 * 4, 1024 * 4, DepthFormat.Depth24);
LightPos = new Vector3(128, 100, 0);
sEffect = Content.Load<Effect>("Effects/ShadowMap");
terrain.Initialize();
scene.Load(sEffect);
}
Здесь наиболее важным является создоние shadowRT и shadowDB. Чем большие величины будут установлены, тем точнее будут результаты ваших вычислений. Однако тем сильнее станет нагрузка на видеопроцессор. В зависимости от конфигурации вашего компьютера вам, возможно, придется искать компромис между производительностью и качеством (это нормально).
protected override void Update(GameTime gameTime)
{
if (Keyboard.GetState().IsKeyDown(Keys.Escape))
this.Exit();
sEffect.Parameters["CameraPos"].SetValue(Camera.Position);
sEffect.Parameters["CameraView"].SetValue(Camera.viewMatrix);
sEffect.Parameters["CameraProj"].SetValue(Camera.projectionMatrix);
sEffect.Parameters["LightPos"].SetValue(LightPos);
sEffect.Parameters["LightView"].SetValue((Matrix.CreateLookAt(LightPos, new Vector3(128, 0, 128), Vector3.Up)));
sEffect.Parameters["LightProj"].SetValue(Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(45), 1.45f, 90, 300));

terrain.Update();
scene.Update();
base.Update(gameTime);
}
В методе Update() мы присваеваем нашему эффекту те значения параметров которые будут использоваться и в первом и во втором проходах рендерринга. Далее  метод Draw()
protected override void Draw(GameTime gameTime)
{
//========= первый проход =============
GraphicsDevice.RenderState.DepthBufferFunction = CompareFunction.LessEqual;
// Устанавливаем Render Target для shadow map
GraphicsDevice.SetRenderTarget(0, shadowRT);
// Сохраняем текущий буфер глубины
DepthStencilBuffer old = GraphicsDevice.DepthStencilBuffer;
// Устанавливаем наш самодельный буфер глубины
GraphicsDevice.DepthStencilBuffer = shadowDB;
GraphicsDevice.Clear(Color.Black);
terrain.Draw(sEffect, "ShadowMapRender");
scene.Draw("ShadowMapRender");
GraphicsDevice.SetRenderTarget(0, null);
// Возвращаем предыдущий буфер глубины
GraphicsDevice.DepthStencilBuffer = old;
В первом проходе мы подготавливаем графическое устройство таким образом, что бы рендерринг сцены с позиции источника освещения происходил в shadowRT. если из него взять текстуру и визуализировать ее с помощью SpriteBatch то мы увидим глубину сцены.
spriteBatch.Begin(SpriteBlendMode.None, SpriteSortMode.Immediate, SaveStateMode.SaveState);
spriteBatch.Draw(shadowRT.GetTexture(), new Rectangle(0, 0, 300, 200), Color.White);
spriteBatch.End();



Во втором проходе рисуется сама сцена с позиции камеры
//========== второй проход ============
sEffect.Parameters["ShadowMapTexture"].SetValue(shadowRT.GetTexture());
GraphicsDevice.RenderState.CullMode = CullMode.None;
GraphicsDevice.RenderState.AlphaBlendEnable = false;
GraphicsDevice.Clear(Color.CornflowerBlue);
terrain.Draw(sEffect, "ShadowRender");
scene.Draw("ShadowRender");
При этом в эффект передается текстура глубины. Зачем она нужна я говорил в первой части этой главы. И в результате сравнения необходимых данных получаем такое изображение


Как видите теперь наши тени от моделей отбрасываются не только на землю но и на самих себя, и на другие модели.  Само собой разумеется, что такие результаты можно было получить только с помощью специального эффекта. Мы в начале книги касались темы шейдеров. Тема эта очень не простая и требует глубокого разбирательства. Вторая часть этой книги, как раз будет посвящена высокоуровневому языку шейдеров. В заключении этой ститьи приведу код этого эффекта, попутно его коментируя. Глобальные переменные которые будут использоваться в эффекте это:
float4 MaterialAmbientColor;    
float4 MaterialDiffuseColor;
две переменные хранящие параметры цвета.
float3 LightPos;
float3 LightDir;
float4x4 LightView;
float4x4 LightProj;
переменные хранящие все данные о источнике света, а именно его позиция, направление, и матрицы 4х4 вида и проекции.
texture MeshTexture;      
texture ShadowMapTexture;
sampler MeshTextureSampler = sampler_state
{
    Texture = <MeshTexture>;
    MipFilter = LINEAR;
    MinFilter = LINEAR;
    MagFilter = LINEAR;
};
sampler ShadowMapSampler = sampler_state
{
 Texture = <ShadowMapTexture>;
    MinFilter = POINT;
    MagFilter = POINT;
    MipFilter = POINT;
    AddressU = Clamp;
    AddressV = Clamp;
};
две текстуры , одна для текстурирования моделей, другая для хранения дпнных глубины.
float4x4 World;            
float3 CameraPos;
float4x4 CameraView; 
float4x4 CameraProj;
переменные хранящие данные о камере
float4 GetPositionLight(float4 position)
{
  float4x4 WorldViewProjection = mul(mul(World, LightView), LightProj);
  return mul(position, WorldViewProjection);
}
Функция помогающая получить позицию света.
struct VS_SHADOW_OUTPUT
{
 float4 Position : POSITION;
 float Depth : TEXCOORD0;
};

VS_SHADOW_OUTPUT RenderShadowMapVS(float4 vPos: POSITION)
{
 VS_SHADOW_OUTPUT Out;
 Out.Position = GetPositionLight(vPos);
 // Глубина это Z/W. Это значение будет возвращено пиксельному шейдеру.
 // Вычитание из еденицы даёт нам большую точность в floating-point данных
 Out.Depth.x = 1-(Out.Position.z/Out.Position.w);
 return Out;
}
float4 RenderShadowMapPS( VS_SHADOW_OUTPUT In ) : COLOR
{
 // Глубина это Z деленный на W. Мы возвращаем
 // это значение полностью в 32 битном красном канале
 // используя SurfaceFormat.Single. Это сохраняет данные
 // floating-point с наибольшнй точностью.
    return float4(In.Depth.x, 0, 0, 1);
}
Это код шейдера получающего значение глубины для каждого пикселя.
struct VS_OUTPUT
{
    float4 Position   : POSITION0;
    float2 TextureUV  : TEXCOORD0;
    float3 vNormal   : TEXCOORD1;
    float4 vPos       : TEXCOORD2; 
};

VS_OUTPUT RenderSceneVS( float3 position : POSITION, 
                           float3 normal : NORMAL, 
                           float2 vTexCoord0 : TEXCOORD0 )
{
     VS_OUTPUT Output;
     float4x4 wvp = mul(mul(World, CameraView), CameraProj);    
     Output.Position = mul(float4(position, 1.0), wvp);
     Output.vNormal =  mul(normal, World);    
     Output.vPos = float4(position,1.0);    
  Output.TextureUV = vTexCoord0;
   
     return Output;
}
struct PS_INPUT
{
    float2 TextureUV  : TEXCOORD0; 
    float3 vNormal   : TEXCOORD1;
    float4 vPos       : TEXCOORD2; 
};
struct PS_OUTPUT
{
    float4 RGBColor : COLOR0;
};
PS_OUTPUT RenderScenePS( PS_INPUT In )
{
    PS_OUTPUT Output;   
   
    // Стандартное выравнивание освещения
    float4 vTotalLightDiffuse = float4(1, 1, 1, 1);
    float3 lightDir = normalize(LightPos-In.vPos);  // направление света
    vTotalLightDiffuse += LightDiffuse * max(0,dot(In.vNormal, lightDir));
    vTotalLightDiffuse.a = 1.0f;
 // Сейчас, принимая во внимание ShadowMap смотрим если мы в тени
    float4 lightingPosition = GetPositionLight(In.vPos);// Полючаем нашу позицию на shadow map   
    // Получаем значение глубины ShadowMap для этого пикселя
    float2 ShadowTexC = 0.5 * lightingPosition.xy / lightingPosition.w + float2(0.5, 0.5);
    ShadowTexC.y = 1.0f - ShadowTexC.y;
    float shadowdepth = tex2D(ShadowMapSampler, ShadowTexC).r;       
    // Проверяем наше значение по отношению к значению глубины
    float ourdepth = 0.98 - (lightingPosition.z / lightingPosition.w);   
    // Проверяем shadowdepth по отношению к глубине этого пикселя (фактор неточности добавлен из расчета ошибки плавающей точки)
 if (shadowdepth - 0.03 > ourdepth)
 {    
     // если мы в тени, выключаем свет
  vTotalLightDiffuse = float4(0.3, 0.3, 0.3, 1);
 };
    Output.RGBColor = tex2D(MeshTextureSampler, In.TextureUV) * (vTotalLightDiffuse + LightAmbient);       
    return Output;   
}
Финальный шейдер в котором происходит сравнение данных глубины с данными камеры и на основании результатов сравнения выполняется затенение. Как я уже говорил ранее, для новичков этот код достаточно сложен и именно по этому моя следующая книга будет посвящена языку шейдеров, в которой мы шаг за шагом изучим все особенности и способы работы с HLSL.

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

Трафаретные тени (Stencil Shadows)

Большинство разработчиков компьютерных игр стремятся к фотореалистичности. Ведь чем сильнее сходство игрового изображентя и реальности, тем захватывающе становится пребывание в игровом мире. И как следствие, игра имеет большее количество поклонников. На реалистичность картинки влияет множество факторов. В идеале если разработчик математически опишет все процессы происходящие в окружающем его мире, то для реализации всех этих вычислений потребуется очень большие, по сегодняшним меркам, вычислительные ресурсы. Хотя бы потому, что все они должны происходить в реальном времени. Это потребует использования очень мощных микропроцессоров которых на данный момент не существует. Понятное дело, что наука не стоит на одном месте, специалисты в области нанотехнологий и микроэлектроники ищут все новые и новые пути решения тех или иных задач. Создаются новейшие центральные и видео процессоры, растет количество их ядер, совершенствуется внутренняя архитектура. Вспоминая революционные события прошлых лет, когда только происходил переход от первой шейдерной модели и фиксированного графического конвеера, к современным технологиям, казалось что еще немного, и компьютерное железо выйдет на такой уровень, когда  все вопросы по производительности исчезнут. Действительно многое изменилось в лучшую сторону, но вычислительной мощности как и раньше, попрежнему, не хватает. Одним из необходимых свойств, которыми должен обладать ваш рендер, является умение рисовать тени от света. Без теней о какой бы то нибыло реалистичности не может быть и речи. Вообще существует много различных способов создания тени. В этой главе мы рассмотрим создание и использование теней с помощью так называемого Stencil Buffer (буфер трафаретов). Что же такое буфер трафаретов? Я не непрасно начал разговор о новых технологиях в процессорном мире, и вот почему. Буфер трафаретов - это новшество которым стали обладать современные видеочипы. Он имеется у многих видеокарт и может отличаться друг от друга размерами.
Благодаря ему у нас появилась возможность быстро и качественно нарисовать тени от обьектов. Переходим к практической части. Создадим новый проект под названием myFirstWorld. Это будет не просто очередной пример, а совокупность рассмотренных ранее техник и примеров этой книги. Здесь мы будем использовать уже пройденый нами класс динамического террайна. Подключим класс летающей камеры, что бы полетать над сценой. Ну и конечно же разберемся с созданием и использованием теней. Первое что нам необходимо будет сделать - это проверить нашу видеокарту на наличие буфера трафаретов и выбрать его подходящий режим.
private static DepthFormat SelectStencilMode()
{
GraphicsAdapter adapter = GraphicsAdapter.DefaultAdapter;
SurfaceFormat format = adapter.CurrentDisplayMode.Format;
if (adapter.CheckDepthStencilMatch(DeviceType.Hardware,
                                  format,
                                  format,
                                  DepthFormat.Depth24Stencil8))
return DepthFormat.Depth24Stencil8;
else if (adapter.CheckDepthStencilMatch(DeviceType.Hardware,
                             format,
                             format,
                             DepthFormat.Depth24Stencil8Single))
return DepthFormat.Depth24Stencil8Single;
else if (adapter.CheckDepthStencilMatch(DeviceType.Hardware,
                                format,
                                format,
                                DepthFormat.Depth24Stencil4))
return DepthFormat.Depth24Stencil4;
else if (adapter.CheckDepthStencilMatch(DeviceType.Hardware,
                                format,
                                format,
                                DepthFormat.Depth15Stencil1))
return DepthFormat.Depth15Stencil1;
else
throw new InvalidOperationException("Could Not Find Stencil Buffer for Default Adapter");
}
Далее инициализируем графическое устройство и подключаем два игровых компонента Terrain и Camera.

public Game1()
{
graphics = new GraphicsDeviceManager(this);
graphics.PreferredBackBufferWidth =
GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Width;
graphics.PreferredBackBufferHeight =
GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Height;
Aspect =
GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.AspectRatio;
Content.RootDirectory = "Content";
graphics.PreferredDepthStencilFormat = SelectStencilMode();
Terrain t = new Terrain(this);
Components.Add(t);
Camera c = new Camera(this);
Components.Add(c);
Scene s = new Scene(this);
Components.Add(s);
}
Далее добавляем новый игровой компонент который будет называться Scene. Выглядит он так.
    Model tractor;
    Matrix TractorWorld;
    Model mi24;
    Matrix mi24World;
    Model UAZ;
    Matrix UazWorld;
    Matrix[] transforms;
Переменные которые будут хранить наши модели сцены и их матрицы мира. Массив матриц трансформаций transforms для того чтобы модели правильно расположились в трехмерном пространстве с учетом внутренней иерархии самих моделей.

protected override void LoadContent()
    {
      tractor = Game.Content.Load<Model>("Models/tractor");
      transforms = new Matrix[tractor.Bones.Count];
      tractor.CopyAbsoluteBoneTransformsTo(transforms);
      TractorWorld = transforms[0] *
        Matrix.CreateRotationY(0.5f) *
        Matrix.CreateTranslation(new Vector3(150, 0, 100));
      mi24 = Game.Content.Load<Model>("Models/mi24");
      transforms = new Matrix[mi24.Bones.Count];
      mi24.CopyAbsoluteBoneTransformsTo(transforms);
      mi24World = transforms[0] *
        Matrix.CreateRotationY(-0.3f) *
        Matrix.CreateTranslation(new Vector3(120, 0, 150));
      UAZ = Game.Content.Load<Model>("Models/uaz");
      transforms = new Matrix[UAZ.Bones.Count];
      UAZ.CopyAbsoluteBoneTransformsTo(transforms);
      UazWorld = transforms[0] *
        Matrix.CreateRotationY(0.3f) *
        Matrix.CreateTranslation(new Vector3(140, 0, 150));
     
      base.LoadContent();
    }
В методе Load() мы загружаем модель и создаем позицию моделей с учетом трансформаций. Эти данные нам потребуются как для отрисовки самих моделей, так и их теней.

public override void Draw(GameTime gameTime)
    {
      DrawScene();
      DrawShadow();
      base.Draw(gameTime);
    }
Метод Draw() разбит на два внутренних метода. Один будет только рисовать модели другой рисовать их тени. Всё это можно делать и в одном методе но для понятности я эти действия разделил.
public void DrawScene()
    {    
      Game.GraphicsDevice.RenderState.CullMode = CullMode.None;
      foreach (ModelMesh mesh in tractor.Meshes)
      {
        foreach (BasicEffect effect in mesh.Effects)
        {
          effect.World = TractorWorld;
          effect.View = Camera.viewMatrix;
          effect.Projection = Camera.projectionMatrix;
          effect.EnableDefaultLighting();
        }
        mesh.Draw();
      }
     
      foreach (ModelMesh mesh in mi24.Meshes)
      {
        foreach (BasicEffect effect in mesh.Effects)
        {
          effect.World = mi24World;
          effect.View = Camera.viewMatrix;
          effect.Projection = Camera.projectionMatrix;
          effect.EnableDefaultLighting();
        }
        mesh.Draw();
      }

      foreach (ModelMesh mesh in UAZ.Meshes)
      {
        foreach (BasicEffect effect in mesh.Effects)
        {
          effect.World = UazWorld;
          effect.View = Camera.viewMatrix;
          effect.Projection = Camera.projectionMatrix;
          effect.EnableDefaultLighting();
        }
        mesh.Draw();
      }
    }
Как видите ничего нового тут нет. Стандартный способ отрисовки моделей с помощью BasicEffect. Запускаем приложение и видим нашу сцену пока что без теней.


Сразу бросается в глаза какая-то неестественность пейзажа. Иными словами не очень реалистично. Движемся далее.
void DrawShadow()
{
//Создаем матрицу тени
Matrix shadow = Matrix.CreateShadow(new Vector3(-50, -100, 40),
new Plane(new Vector3(0, 0.01f, 0),
new Vector3(500, 0.01f, 0),
new Vector3(0, 0.01f, 500)));
//Очищаем буфер трафаретов
Game.GraphicsDevice.Clear(ClearOptions.Stencil, Color.Black, 0, 0);
//Включаем буфер трафаретов
Game.GraphicsDevice.RenderState.StencilEnable = true;
// Вывод на экран элемента буфера в том случае, если он установлен в 0
Game.GraphicsDevice.RenderState.ReferenceStencil = 0;
Game.GraphicsDevice.RenderState.StencilFunction = CompareFunction.Equal;
//При выводе применяем операцию увеличения
Game.GraphicsDevice.RenderState.StencilPass = StencilOperation.Increment;
//Включаем альфа-смешивание для того, чтобы сделать тень полупрозрачной
//Выводим тень
foreach (ModelMesh mesh in tractor.Meshes)
{
foreach (BasicEffect effect in mesh.Effects)
{
effect.AmbientLightColor = Vector3.Zero;
effect.Alpha = 0.5f;
effect.DirectionalLight0.Enabled = false;
effect.DirectionalLight1.Enabled = false;
effect.DirectionalLight2.Enabled = false;
effect.View = Camera.viewMatrix;
effect.Projection = Camera.projectionMatrix;
//При выводе тени умножаем мировую матрицу
//на матрицу вывода тени
effect.World = TractorWorld * shadow;
}
mesh.Draw();
}
foreach (ModelMesh mesh in mi24.Meshes)
{
foreach (BasicEffect effect in mesh.Effects)
{
effect.AmbientLightColor = Vector3.Zero;
effect.Alpha = 0.5f;
effect.DirectionalLight0.Enabled = false;
effect.DirectionalLight1.Enabled = false;
effect.DirectionalLight2.Enabled = false;
effect.View = Camera.viewMatrix;
effect.Projection = Camera.projectionMatrix;
//При выводе тени умножаем мировую матрицу
//на матрицу вывода тени
effect.World = mi24World * shadow;
}
mesh.Draw();
}

foreach (ModelMesh mesh in UAZ.Meshes)
{
foreach (BasicEffect effect in mesh.Effects)
{
effect.AmbientLightColor = Vector3.Zero;
effect.Alpha = 0.5f;
effect.DirectionalLight0.Enabled = false;
effect.DirectionalLight1.Enabled = false;
effect.DirectionalLight2.Enabled = false;
effect.View = Camera.viewMatrix;
effect.Projection = Camera.projectionMatrix;
//При выводе тени умножаем мировую матрицу
//на матрицу вывода тени
effect.World = UazWorld * shadow;
}
mesh.Draw();
}
//Отключаем буфер
Game.GraphicsDevice.RenderState.StencilEnable = false;
//Отключаем альфа-смешивание
Game.GraphicsDevice.RenderState.AlphaBlendEnable = false;
}

Вот результат с применением буфера трафаретов. Уже намного лучше. Только сейчас наши тени слишком черные. Если бы действия игры происходили где нибудь на луне, где нет атмосферы - то такие тени нам бы вполне подошли. Однако нам хотелось бы сделать тени полупрозрачными. Для этого воспользуемся настройками альфасмешивания.
Game.GraphicsDevice.RenderState.AlphaBlendEnable = true;
Game.GraphicsDevice.RenderState.SourceBlend = Blend.SourceAlpha;
Game.GraphicsDevice.RenderState.DestinationBlend = Blend.InverseSourceAlpha;
В результате чего мы получили полупрозрачные тени.


Для большей понятности сути происходящего поясню. Stencil buffer это так называемая, проекция сцены на геометрическую фигуру Plane (прямоугольная плоскость не имеющая толщины). Вы заметили, что во время создания матрицы теней мы передаем параметрами позицию тени и создаем Plane на который эта тень будет спроецирована. Из плюсов такого подхода можно выделить простоту использования матрицы теней. Из минусов это то, что наши тени могут быть отброшены только на Plane. То есть тень не может быть отброшена саму модель. Для таких теней нам потребуется техника ShadowMapping. О ней я расскажу в следующих главах.

Практическое применение 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. Теперь, я надеюсь, вам понятны базовые принципы динамического создания ландшафтов.

Практическое применение 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. Думаю вы оцените эту технику и найдете ей применение.      

понедельник, 9 апреля 2012 г.

Lightning Models

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

Lambert Lighthing Model

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

I = max(0, dot(N, L));

где:
I - итоговый коэффициент затенения,
N - нормаль полигона в мировом пространстве,
L - направление света.

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

float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
   const float3 lightDir = float3(1, 0, 1);
   float LdotN = max(0.0, dot(lightDir, input.Normal));
   return float4(0.5, 0.5, 0.5, 1) * LdotN;
}



Исходный код к статье  lambert.zip



Phong Lightning Model

Следующая модель освещения - это бликовая модель освещения Фонга.
В отличии от модели Ламберта, она не только учитывает нормали, но так-же учитывает отражение света по отношению к наблюдателю.
Код пиксельного шейдера приведен ниже:

float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
    const float3 lightDir = float3(1, 0, 1);
    const float3 viewDir = normalize(float3(0, -75, 0));
    float LdotN = dot(lightDir, input.Normal);
    float3 Reflection = normalize(2.0f * input.Normal * LdotN - lightDir);
    float RdotV = max(0.0, dot(Reflection, viewDir));
    float SpecExp = 50;
    float4 SpecColor = float4(1, 1, 1, 1);
    float4 TotalSpecular = SpecColor * pow(RdotV, SpecExp);
    return float4(0.5, 0.5, 0.5, 1) * saturate(max(0.0, LdotN)) + TotalSpecular;
}

Исходный код к статье  PhongModel.zip


Normal + Specular Mapping


 Совмещая бликовую и диффузную модели освещения, можно получать более сложные эффекты. Одним из таких эффектов является Normal Mapping. Мы уже знаем что для расчета диффузного освещения нужно использовать дот продукт векторов нормали и направления света. Нормали обычно хранятся в самой модели. Можно сказать, что для одного полигона
(треугольника) нормаль расчитывается как среднее арифметическое из трех нормалей вершин полигона. Соответственно, детализация объекта ограничивается сложностью модели. Для того что-бы придать модели более детализированный вид, был придуман следующий способ. Художники сохраняли нормали в текстуру, и после текстурирования такой текстурой модели, нужно считать нормаль из нее и использовать для расчета освещения. В результате, можно добиться высокой визуальной сложности моделей, не затрагивая ее сетки.



Реализацию этой техники можно найти здесь SpecularBump.zip

Physically Based Rendering