вторник, 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.

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

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

Physically Based Rendering