вторник, 6 марта 2012 г.

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

Камера  (XNA Camera)


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

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

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 myCamera
{
  public class Game1 : Microsoft.Xna.Framework.Game
  {
    GraphicsDeviceManager graphics;
    SpriteBatch spriteBatch;
    Model _model;
    Camera _camera;
    public Game1()
    {
      graphics = new GraphicsDeviceManager(this);
      Content.RootDirectory = "Content";
    }
    protected override void Initialize()
    {
      _camera = new Camera(this);
      Components.Add(_camera);
      base.Initialize();
    }
    protected override void LoadContent()
    {    
      spriteBatch = new SpriteBatch(GraphicsDevice);
      _model = Content.Load<Model>("torknot");
    }
    protected override void UnloadContent()
    {
     
    }
    protected override void Update(GameTime gameTime)
    {
      if (Keyboard.GetState().IsKeyDown(Keys.Escape))
      {
        this.Exit();
      }
      base.Update(gameTime);
    }
    protected override void Draw(GameTime gameTime)
    {
      GraphicsDevice.Clear(Color.CornflowerBlue);
      foreach (ModelMesh mesh in _model.Meshes)
      {
        foreach (BasicEffect effect in mesh.Effects)
        {
          effect.World = Matrix.CreateTranslation(new Vector3(0, 0, 0));
          effect.View = _camera.viewMatrix;
          effect.Projection = _camera.projectionMatrix;
          effect.EnableDefaultLighting();
        }
        mesh.Draw();
      }
      base.Draw(gameTime);
    }
  }
}


Этот код вам должен быть понятен. Здесь мы лишь вывели модель на экран, но в оличии от предыдущих примеров матрицы вида и проекции взяты из класса камеры. Вот о нем дальше реч и пойдет. Для начала мы обьявили переменные обеих матриц.

public Matrix viewMatrix;
public Matrix projectionMatrix;

Они, как вы могли догадаться, будут хранить вид и проекцию.

float _Pitch;
float _Yaw;

Будут хранить угол поворота камеры вверх вниз и влево вправо

 float distancePerFrame = 10;

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

Vector2 CenterScreen;

Хранит позицию середины экрана

private void CenteredCursor()
{
  Mouse.SetPosition((int)CenterScreen.X, (int)CenterScreen.Y);
}

Кроме предусмотренных игровым компонентом методов мы создадим еще один вспомогательный. Он будет возвращать позицию мыши на середину экрана

public Camera(Game game) : base(game)
{
 CenterScreen =
    new Vector2(game.GraphicsDevice.Viewport.Width / 2,
                game.GraphicsDevice.Viewport.Height / 2);
 viewMatrix = Matrix.CreateLookAt(new Vector3(5, 5, 500),
                                       new Vector3(0, 0, 0),
                                       new Vector3(0, 1, 0));
 projectionMatrix =         Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(45),
                       game.GraphicsDevice.Viewport.Width /
                       game.GraphicsDevice.Viewport.Height,
                                            0.1f, 1000.0f);
 CenteredCursor();
}


В конструкторе класса мы определии центр игрового экрана, создали матрицы вида и проекции и поставили указатель мыши на середину экрана. Далее в методе Update()

Vector2 cursorPosition = new Vector2(Mouse.GetState().X, Mouse.GetState().Y);
Vector2 deltaDampened = (cursorPosition - CenterScreen) * 0.0015f;

мы находим текущее положение указателя мыши, оно могло измениться с момента определения. Вычитаем это значение из значения центра экрана и умножаем результат на 0.0015 для того что бы подобрать подходящую нам скорость поворотов камеры. Движение мышью в этом примере будет управлять вращением камеры соответственно. То есть если перемещать мышь вправо, то камера будет поворачиваться тоже вправо. Аналогично влево, вверх, вниз, по диагоналям да и вообще в любом направлении.

_Yaw -= deltaDampened.X;
_Pitch -= deltaDampened.Y;

переменные _Yaw и _Pitch получают значения в зависимости от манипуляций мышью.

if (_Pitch < -1.55f)
{
  _Pitch = -1.55f;
}
if (_Pitch > 1.55f)
{
  _Pitch = 1.55f;
}

Здесь мы установили пределы поворотов камеры по вертикали, чтобы она поднималась и опускалась подобно человеческой голове.

 Matrix cameraRotation = Matrix.CreateFromYawPitchRoll(_Yaw,_Pitch,0.0f);
Vector3 translateDirection = Vector3.Zero;

Здесь мы создали матрицу вращения и вектор перемещения камеры, заметим что при создании cameraRotation мы воспользовались _Yaw и _Pitch переменными.

KeyboardState states = Keyboard.GetState();
if (states.IsKeyDown(Keys.W))
    translateDirection +=
    Vector3.TransformNormal(Vector3.Forward, cameraRotation);
if (states.IsKeyDown(Keys.S))
    translateDirection +=
    Vector3.TransformNormal(Vector3.Backward, cameraRotation);
if (states.IsKeyDown(Keys.A))
    translateDirection +=
    Vector3.TransformNormal(Vector3.Left, cameraRotation);
if (states.IsKeyDown(Keys.D))
    translateDirection +=
    Vector3.TransformNormal(Vector3.Right, cameraRotation);
if (states.IsKeyDown(Keys.Q))
    translateDirection +=
    Vector3.TransformNormal(Vector3.Up, cameraRotation);
if (states.IsKeyDown(Keys.E))
    translateDirection +=
    Vector3.TransformNormal(Vector3.Down, cameraRotation);

Здесь мы определили клавиши на клавиатуре которые будут отвечать за перемещение камеры: W - движение вперед, S - движение назад, A - движение влево, D - движение вправо, Q - движение вверх, E - движение вниз. Повороты сюда не входят, за них отвечает позиция мыши.

Vector3 newPosition = Matrix.Invert(viewMatrix).Translation;
if (translateDirection != Vector3.Zero)
{
  newPosition += Vector3.Normalize(translateDirection) *     distancePerFrame;
}
Vector3 newForward = Vector3.TransformNormal(Vector3.Forward, cameraRotation);
viewMatrix = Matrix.CreateLookAt(newPosition, newPosition + newForward, Vector3.Up);

Здесь мы инвертируем текущую позицию камеры,  проверяем изменилось ли направление движения камеры, вычисляем новое направление "взгляда" камеры и на основании этих данных, создаётся новая матрица вида.

CenteredCursor();

И напоследок вызываем метод возврата курсора на середину экрана. Этим завершается метод Update(). Если теперь запустить приложение, то вы увидите на экране модель и сможете "полетать" вокруг нее с помощью нашей камеры.

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

Весь код класса камера:

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 myCamera
{
  public class Camera : Microsoft.Xna.Framework.GameComponent
  {
    public Matrix viewMatrix;
    public Matrix projectionMatrix;
    Vector2 CenterScreen;
    float _Pitch;
    float _Yaw;
    float distancePerFrame = 10;
    public Camera(Game game) : base(game)
    {
      CenterScreen = new Vector2(game.GraphicsDevice.Viewport.Width / 2,
                                 game.GraphicsDevice.Viewport.Height / 2);
      viewMatrix = Matrix.CreateLookAt(new Vector3(5, 5, 500),
                                       new Vector3(0, 0, 0),
                                       new Vector3(0, 1, 0));
      projectionMatrix =
      Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(45),
                                          game.GraphicsDevice.Viewport.Width /
                                          game.GraphicsDevice.Viewport.Height,
                                          0.1f, 1000.0f);
      CenteredCursor();
    }
    public override void Update(GameTime gameTime)
    {
        Vector2 cursorPosition = new Vector2(Mouse.GetState().X, Mouse.GetState().Y);
        Vector2 deltaDampened = (cursorPosition - CenterScreen) * 0.0015f;
        _Yaw -= deltaDampened.X;
        _Pitch -= deltaDampened.Y;
        if (_Pitch < -1.55f)
        {
          _Pitch = -1.55f;
        }
        if (_Pitch > 1.55f)
        {
          _Pitch = 1.55f;
        }

        Matrix cameraRotation = Matrix.CreateFromYawPitchRoll(_Yaw, _Pitch, 0.0f);
        Vector3 translateDirection = Vector3.Zero;
        KeyboardState states = Keyboard.GetState();
       
        if (states.IsKeyDown(Keys.W))
          translateDirection +=
            Vector3.TransformNormal(Vector3.Forward, cameraRotation);
       
        if (states.IsKeyDown(Keys.S))
          translateDirection +=
            Vector3.TransformNormal(Vector3.Backward, cameraRotation);
       
        if (states.IsKeyDown(Keys.A))
          translateDirection +=
            Vector3.TransformNormal(Vector3.Left, cameraRotation);
       
        if (states.IsKeyDown(Keys.D))
          translateDirection +=
            Vector3.TransformNormal(Vector3.Right, cameraRotation);
      
        if (states.IsKeyDown(Keys.Q))
          translateDirection +=
            Vector3.TransformNormal(Vector3.Up, cameraRotation);
      
        if (states.IsKeyDown(Keys.E))
          translateDirection +=
            Vector3.TransformNormal(Vector3.Down, cameraRotation);
      
        Vector3 newPosition = Matrix.Invert(viewMatrix).Translation;
        if (translateDirection != Vector3.Zero)
        {
          newPosition += Vector3.Normalize(translateDirection) * distancePerFrame;
        }
        Vector3 newForward = Vector3.TransformNormal(Vector3.Forward, cameraRotation);
        viewMatrix = Matrix.CreateLookAt(newPosition, newPosition + newForward, Vector3.Up);
        CenteredCursor();
      base.Update(gameTime);
    }

    private void CenteredCursor()
    {
      Mouse.SetPosition((int)CenterScreen.X, (int)CenterScreen.Y);
    }
  }
}

Результат полета камеры.


Удачи!





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

4.Введение в HLSL

Вот мы и разобрались с выводом моделей на экран. И теперь пришло время поговорить о шейдерах. Вы не могли не заметить, что наша первая модель появилась на экране уже раскрашенная в различные цвета, хотя мы не одной строчки для этого не написали. Кто же за нас поработал? Ответ заключается в следующем. При добавлении модели в проект, ей автоматически присваевается процессор контента. Если в обозревателе решений один раз кликнуть мышью на каком-либо файле, будь то текстура, модель, файл звука или файл эффекта, то в окне свойств появится процессор контента. Именно он подготовит данные из файла, в формат понятный GameStudio. В нашем случае мы видим, что по умолчанию стоит процессор контента – Model XNA framework. Все аспекты его деятельности мы сейчас рассматривать не станем, но заметим одно – он установил эффектом для нашей модели BasicEffect, а текстурой – текстуру, которая присвоена модели ранее, в той программе, в которой она была создана. Если вы внимательно читали предыдущий код, то должны были заметить вот это:
foreach (ModelMesh mesh in myModel.Meshes)
{
   foreach (BasicEffect effect in mesh.Effects)
   {
      effect.World = world;
      effect.View = view;
      effect.Projection = proj;
   }
   mesh.Draw();
}

В первом цикле foreach мы проходим по всем сеткам модели, а во втором цикле по всем эффектам для каждой сетки. Причем ищем BasicEffect, который нам установил процессор контента. Вот с него мы и начнем знакомство с шейдерами или иначе сказать эффектами.
Шейдеры – это всего лишь программа, состоящая из набора ключевых слов, которая позволяет программисту работать напрямую с вершинами и пикселями. Как и в любом другом языке программирования, освоение шейдеров начинается с понимания общего принципа работы шейдеров и изучению  языка программирования, на котором пишутся шейдеры (HLSL). Особенность шейдеров заключается в том, что весь код, который в нем находится, исполняется графическим процессором видеокарты. Сделано это для того, чтобы  снизить нагрузку центрального процессора и обращаться к процессору видеокарты напрямую. Это увеличивает скорость и качество создаваемых эффектов. Из-за того, что графический процессор работает не так как центральный процессор, для управления им требуется свой набор команд или свой язык программирования. Раньше управление графическим процессором осуществлялось с помощью языка сходного с ассемблером, он содержал небольшое количество инструкций. Но с течением времени шейдеры становились все больше и тяжелее, вот и появилась необходимость в высокоуровневом языке, которым и является HLSL. Вообще шейдер хранится в файле с расширением .fx, и имеет определенную структуру, его можно разделить на четыре основные части.  Первая часть - это набор переменных, значения которых могут быть назначены заранее или передаваться им из кода как параметры. Ими могут быть матрицы различных размерностей, переменные  и много чего ещё. Например, так мы передали шейдеру наши матрицы.

effect.World = world;
effect.View = view;
effect.Projection = proj;
 

Вторая часть - это функция, в которой производятся все преобразования с вершинами называемый вершинный шейдер. Он, как правило, вызывается первым. Прсле окончания вычислений в вершинном шейдере, данные передаются пиксельный шейдер. Это третья часть - функция, в которой производятся все преобразования с пикселями. Присваивание им необходимого цвета. И четвертая часть – это техника, из которой происходит вызов вершинного и пиксельного шейдеров. Вообще тема шейдеров настолько обширна, что для нее надо писать целую книгу, которых уже и так немало написано. Но мы пока ограничимся изучением BasicEffect, и узнаем на что способен наш, пока что единственный шейдер. Помимо матриц, BasicEffect принимает еще информацию о направленных источниках освещения, настройки для тумана, может установить новую текстуру и т.д. Если эти параметры не предать, то BasicEffect будет использовать данные, которые ему присвоены по умолчанию. На рисунке ниже показана реализация тумана на основе BasicEffect. Добиться этого удалось добавлением в наш, уже существующий код, всего лишь нескольких строк в методе Draw.
protected override void Draw(GameTime gameTime)
{
  GraphicsDevice.Clear(Color.CornflowerBlue);
  world = Matrix.CreateRotationY(rotation) * Matrix.CreateTranslation(0, 0, 5);
  view =  Matrix.CreateLookAt(new Vector3(0, 0, -1f), Vector3.Zero, Vector3.Up);
  proj = Matrix.CreateOrthographic(5, 5, 1, 1000);
  foreach (ModelMesh mesh in myModel.Meshes)
  {
    foreach (BasicEffect effect in mesh.Effects)
    {
      effect.World = world;
      effect.View = view;
      effect.Projection = proj;
      effect.FogEnabled = true;
      effect.FogColor = Color.CornflowerBlue.ToVector3();
      effect.FogStart = 0;
      effect.FogEnd = 6;
    }
      mesh.Draw();
   }
    base.Draw(gameTime);
}

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

Такой еффект достигается с помощью строчки
effect.DiffuseColor = new Vector3(0, 0, 0);
Передавая в шейдер какой-либо цвет, он автоматически применяется к модели. Следующий пример показывает, как можно простым способом, добавить освещение к вашей модели или всей сцене. Для этого вам необходимо просто написать следующую строку в методе Draw.
effect.EnableDefaultLighting();
Добавляя тем самым источник света определенный шейдером по умолчанию. Результат изображен на рисунке.

Следующий пример показывает, как можно заменить текстуру, которой покрыта наша модель, либо вообще ее убрать. Строка:
effect.TextureEnabled = false;
отключает текстурирование, и как результат, мы получим следующее изображение:
Как видите, текстура не рисуется, остается лиш силует нашей модели, залитый белым цветом. Для того, чтобы присвоить модели новую текстуру, нам для начала, надо ее загрузить в проект. Делатся это старым проверенным способом. И после этого текстура передается в шейдер, заменяя при этом предыдущую.
effect.Texture = stone;
В результате получилось такое изображение
Само собой розумеется, что все эти приемы можно использоавть как по отдельности, так и все вместе, во всевозможных комбинациях, с различными параметрами. Сам по себе BasicEffect служит своеобразной универсальной заглушкой, для того, что бы начинающие программисты, не знакомые с языками программиования шейдеров, имели возможность отобразить модель на экране. По функциональности он не несет в себе ничего сверхестественного, но с базовыми задачами справляется прекрасно. Его главная заслуга в том, что он уже написан, оптимизирован и удобен в эксплуатации. К тому же, если учесть, что XNA может использоваться не только для написания игр, а, например для каких нибудь 3D презентаций, то большей функциональности чем дает BasicEffect, и не требуется. Но если говорить о серьезных игровых проектах, то в них используются десятки, а то и сотни шейдеров, которые выполняют различные задачи, это и работа с тенями, отражениями, преломлениями света и многое многое другое. Файл еффекта, так же как и стартовый проект GameStudio можно сгенерировать. Для этого, на папке контент кликаем правой кнопкой мыши, и выбираем мункт меню Добавить/Создать Елемент/EffectFile. После этого в проект будет добавлен шейдер, название которому можете присвоить сами, при создании. Этот еффект в отличии от BasicEffect будет пустым, и сможет только лишь отображать залитую красным цветом модель. Без освещения, тумана, текстуры. Всё это и многое другое вам предстоит делать самому. Но об этом потом, а сейчас давайте посмотрим, что же находится внутри нового шейдера.
float4x4 World;
float4x4 View;
float4x4 Projection;
// TODO: add effect parameters here.
struct VertexShaderInput
{
    float4 Position : POSITION0;
    // TODO: add input channels such as texture
    // coordinates and vertex colors here.
};
struct VertexShaderOutput
{
    float4 Position : POSITION0;
    // TODO: add vertex shader outputs such as colors and texture
    // coordinates here. These values will automatically be interpolated
    // over the triangle, and provided as input to your pixel shader.
};
VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
    VertexShaderOutput output;
    float4 worldPosition = mul(input.Position, World);
    float4 viewPosition = mul(worldPosition, View);
    output.Position = mul(viewPosition, Projection);
    // TODO: add your vertex shader code here.
    return output;
}
float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
    // TODO: add your pixel shader code here.
    return float4(1, 0, 0, 1);
}
technique Technique1
{
    pass Pass1
    {
        // TODO: set renderstates here.
        VertexShader = compile vs_1_1 VertexShaderFunction();
        PixelShader = compile ps_1_1 PixelShaderFunction();
    }
}
Теперь давайте подробнее разберемся в этом коде. Как видите сходство синтаксиса HLSL и Си прослеживается сразу же. Как уже было сказано ранее, мы видим четыре основные части.
1. Переменные хранящие матрицы и две структуры хранящие входящие и исходящие данные о вершинах. В данном случае это только позиция.
float4x4 World;
float4x4 View;
float4x4 Projection;
struct VertexShaderInput
{
    float4 Position : POSITION0;
};
struct VertexShaderOutput
{
    float4 Position : POSITION0;
};
2. Вершинный шейдер
VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
    VertexShaderOutput output;
    float4 worldPosition = mul(input.Position, World);
    float4 viewPosition = mul(worldPosition, View);
    output.Position = mul(viewPosition, Projection);
    return output;
}
В нем произведены вычисления позиции каждой вершины, базируясь на трех матрицах, а после результат сохраняется в екземпляре структуры output.
3. Пиксельный шейдер
float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
    return float4(1, 0, 0, 1);
} 
Здесь вообще всё просто, не производится ни каких вычислний, просто возвращается, всегда красный, цвет пикселя.
4. Техника
technique Technique1
{
    pass Pass1
    {
        VertexShader = compile vs_1_1 VertexShaderFunction();
        PixelShader = compile ps_1_1 PixelShaderFunction();
    }
}
Дословно здесь сказано, что перед вызовом вершинного, а затем пиксельного шейдера, сперва определяется шейдерная модель (vs_1_1 и ps_1_1). В момент появления языка HLSL была предложена так называемая Shaders Models 1.0. Это своего рода версия описания шейдерной модели для использования шейдерных программ в программировании графики. Шейдерная модель предъявляет набор требований для графического процессора видеокарты, которые он обязан иметь или исполнять. На сегодняшний день имеются уже три версии Shaders Models. Каждая последующая шейдерная модель лишь усовершенствует свою предшественницу. То есть имеется набор определенных инструкций, которые может выполнить видеоадаптер с поддержкой Shaders Models 1.0, и если вы попробуете на этом видеоадаптере использовать шейдерную модель третьей версии, то произойдет ошибка в работе программы. Фактически шейдерная модель описывает тот набор операций, который графический процессор видеокарты может или даже способен выполнить. Например, в Shaders Models 1.0 нет возможности использовать циклы, а во второй версии такая возможность (для видеоадаптеров) была добавлена. Технология не стоит на месте и развивается, мощности и возможности графических процессоров растут, поэтому и Shaders Models постепенно усовершенствуется. Первая версия Shaders Models со временем отошла и уже практически не используется в программировании шейдеров, но поддерживается всеми видео картами без исключения, если, конечно, производитель умышленно не исключил такой возможности. Сейчас отправной точкой в версии модели шейдеров считается версия ShadersModels 2.0, которая также используется в консольной приставке Xbox 360 (приставка не может использовать Shaders Models 1.0). Более дорогие компьютерные видеоадаптеры могут работать с Shaders Models 2.0, Shaders Models 3.0 и с Shaders Models 4.0. Но это мы немного отвлеклись. Теперь нам надо изменить свой проект так, что бы мы могли отобразить модель с помощью нашего шейдера. Чтобы это сделать, нужно, во первых, создать новый еффект следующими строками,

Effect myEffect;
myEffect = Content.Load<Effect>("Shaders/Effect1");
а потом надо заменить уже готовый BasicEffect установленый в модель, на новый. Сделаем это с помощью функции
public void ChangeEffectUsedByModel(Model model, Effect replacementEffect)
{
      Dictionary<Effect, Effect> effectMapping = new Dictionary<Effect, Effect>();
      foreach (ModelMesh mesh in model.Meshes)
      {
        foreach (BasicEffect oldEffect in mesh.Effects)
        {
          if (!effectMapping.ContainsKey(oldEffect))
          {
            Effect newEffect = replacementEffect.Clone(replacementEffect.GraphicsDevice);
            effectMapping.Add(oldEffect, newEffect);
          }
        }
        foreach (ModelMeshPart meshPart in mesh.MeshParts)
        {
          meshPart.Effect = effectMapping[meshPart.Effect];
        }
      }
 }
И поменять код в методе Draw, потому что теперь нам надо искать не BasicEffect, а наш Effect. Делается это следующим образом, с учетом всей специфики изменений.
foreach (ModelMesh mesh in myModel.Meshes)
{
  foreach (Effect effect in mesh.Effects)
  {
     effect.Parameters["World"].SetValue(world);
     effect.Parameters["View"].SetValue(view);
     effect.Parameters["Projection"].SetValue(proj);
  }
   mesh.Draw();
}
Общий вид нашего кода теперь стал выглядеть так:
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 SimplyModel
{
  public class Game1 : Microsoft.Xna.Framework.Game
  {
    GraphicsDeviceManager graphics;
    SpriteBatch spriteBatch;
    Model myModel;
    Matrix world;
    Matrix view;
    Matrix proj;
    Effect myEffect;
    Texture2D stone;
    float rotation;
    public Game1()
    {
      graphics = new GraphicsDeviceManager(this);
      Content.RootDirectory = "Content";
    }
    protected override void Initialize()
    {
      base.Initialize();
    }
    protected override void LoadContent()
    {
      spriteBatch = new SpriteBatch(GraphicsDevice);
      myModel = Content.Load<Model>("models/BTR");
      stone = Content.Load<Texture2D>("textures/stone");
      myEffect = Content.Load<Effect>("Shaders/Effect1");
      ChangeEffectUsedByModel(myModel, myEffect);
    }
    public void ChangeEffectUsedByModel(Model model, Effect replacementEffect)
    {
      Dictionary<Effect, Effect> effectMapping = new Dictionary<Effect, Effect>();
      foreach (ModelMesh mesh in model.Meshes)
      {
        foreach (BasicEffect oldEffect in mesh.Effects)
        {
          if (!effectMapping.ContainsKey(oldEffect))
          {
            Effect newEffect = replacementEffect.Clone(replacementEffect.GraphicsDevice);
            effectMapping.Add(oldEffect, newEffect);
          }
        }
        foreach (ModelMeshPart meshPart in mesh.MeshParts)
        {
          meshPart.Effect = effectMapping[meshPart.Effect];
        }
      }
    }
    protected override void UnloadContent()
    {
    }
    protected override void Update(GameTime gameTime)
    {
      rotation += MathHelper.ToRadians(0.5f);
      base.Update(gameTime);
    }
    protected override void Draw(GameTime gameTime)
    {
      GraphicsDevice.Clear(Color.CornflowerBlue);
      world = Matrix.CreateRotationY(rotation) * Matrix.CreateTranslation(0, 0, 5);
      view =  Matrix.CreateLookAt(new Vector3(0, 0, -1f), Vector3.Zero, Vector3.Up);
      proj = Matrix.CreateOrthographic(7, 7, 1, 1000);
      foreach (ModelMesh mesh in myModel.Meshes)
      {
        foreach (Effect effect in mesh.Effects)
        {
          effect.Parameters["World"].SetValue(world);
          effect.Parameters["View"].SetValue(view);
          effect.Parameters["Projection"].SetValue(proj);
         }
        mesh.Draw();
      }
      base.Draw(gameTime);
    }
  }
}
А результат выглядит вот так:
Для начала не плохо, как мне кажется.


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

Рисуем первую модель.

Перед тем, как мы нарисуем нашу первую модель, давайте разберемся, что же такое трёхмерное пространство, и каким образом организовано отображение моделей в нем. Первое с чего мы начнем, это система трехмерных координат. Важно отметить, что в XNA используется правосторонняя система координат, в DirectX левосторонняя, и это следует помнить. Вся разница между ними в том, что если ось X (ширина, начинается в нижнем левом углу экрана монитора и направлена вправо) и ось Y (высота, начинается в нижнем левом углу экрана монитора и направлена вверх), и в левосторонней и правосторонней системах одинаковы, то с осью Z дела обстоят диаметрально противоположно. В правосторонней системе ось Z своим положительным лучом направлена к вам , как бы выходя из экрана монитора.
В левосторонней системе ось Z положительным лучом наоборот уходит вдаль от вас вглубь монитора. Если представить, что камера находится в начале системы координат, то - есть X=0, Y=0, Z=0, а модель которую предстоит отобразить в координатах X=0, Y=0, Z=5 , то в правосторонней системе координат мы ее не увидим, так как фактически она должна будет находиться в воздухе между монитором и вами. Если же координаты модели будут X=0, Y=0, Z=-5, то ее местоположение переместится вглубь монитора, и наша видеокарта сможет ее отобразить. Однако важно понимать, что если камеру переместить в отрицательную часть системы координат по оси Z, и развернуть на 180 градусов, например для того чтобы посмотреть на объект с обратной стороны, то теперь чтобы отдалить объект нам надо его двигать в положительном направлении по оси Z.
С пространством, в котором нам предстоит отображать нашу игру, вроде бы всё понятно. Теперь движемся дальше. Как уже было сказано, игра и ее игровой мир состоит из примитивов и моделей. Давайте рассмотрим примитив как простейшую форму модели. и пойдем мы от сложного к простому. Любой примитив состоит из полигонов, а любой полигон из вершин и линий соединяющих эти вершины. Итак, у нас появились новые понятия, суть которых такова:
 Вершина – это точка в трехмерном пространстве имеющая координаты X,Y,Z.
Полигон – это совокупность трех вершин соединенных между собой линиями и образующих треугольник, вершины которого теперь имеют не только координаты, но еще и индекс, так называемый порядковый номер вершины.
Все модели и примитивы состоят из полигонов (треугольников). Модели сложной формы могут включать в себя сотни тысяч полигонов и называются они, на языке создателей игр, высокополигональными моделями (highpoly) , и наоборот, модели, состоящие из нескольких десятков полигонов, которые называются низкополигональными (lowpoly). Чем больше полигонов в модели, тем правильней и точней будет ее форма, округлые части станут менее угловатые и более округлые. Однако высокополигональная модель требует больше компьютерных ресурсов для обработки, поэтому здесь также необходим компромисс между качеством и производительностью.
Для того чтобы вывести на экран нашу модель, создадим новый проект SimplyModel. В папку Content мы добавляем папку models, а в неё модель под названием BTR.x. Следующий код показывает реализацию.

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 SimplyModel
{
  public class Game1 : Microsoft.Xna.Framework.Game
  {
    GraphicsDeviceManager graphics;
    SpriteBatch spriteBatch;
    Model myModel;
    Matrix world;
    Matrix view;
    Matrix proj;
    float rotation;
    public Game1()
    {
      graphics = new GraphicsDeviceManager(this);
      Content.RootDirectory = "Content";
    }
    protected override void Initialize()
    {
      base.Initialize();
    }
    protected override void LoadContent()
    {
      spriteBatch = new SpriteBatch(GraphicsDevice);
      myModel = Content.Load<Model>("models/BTR");
    }
    protected override void UnloadContent()
    {
    }
    protected override void Update(GameTime gameTime)
    {
      rotation += MathHelper.ToRadians(1);
      base.Update(gameTime);
    }
    protected override void Draw(GameTime gameTime)
    {
      GraphicsDevice.Clear(Color.CornflowerBlue);
      world = Matrix.CreateRotationY(rotation) * Matrix.CreateTranslation(0, 0, 5);
      view = Matrix.CreateLookAt(new Vector3(0, 0, -1f), Vector3.Zero, Vector3.Up);
      proj = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(90),
                                                (float)graphics.GraphicsDevice.Viewport.Width /
                                                (float)graphics.GraphicsDevice.Viewport.Height, 1, 1000);
      foreach (ModelMesh mesh in myModel.Meshes)
      {
        foreach (BasicEffect effect in mesh.Effects)
        {
          effect.World = world;
          effect.View = view;
          effect.Projection = proj;
        }
        mesh.Draw();
      }
      base.Draw(gameTime);
    }
  }
}
В этом примере, тоже все достаточно просто. Аналогично предыдущим урокам, мы загружаем
модель из предварительно созданного файла. Далее проходим по всем сеткам модели, их может быть несколько, и вызываем у каждой из них метод Draw(), который и изобразит ее на экране.
Для рисования модели нам так-же потребуются три матрицы.
  Мировая матрица – позволяет производить такие преобразования объекта как масштабирование, перемещение, вращение, различные трансформации сетки на вершинном уровне. Этой матрицей наделяется каждый объект, это локальная система координат каждой модели. Новая локальная система координат значительно упрощает аффинные преобразования объекта в пространстве. Например, чтобы перенести объект с левого верхнего угла дисплея в нижний правый угол дисплея, необходимо просто перенести его локальную точку отсчета, системы координат на новое место. А представьте, если бы не было мировой матрицы и этот объект пришлось переносить по одной вершине из угла в угол монитора... Поэтому любой объект, а точнее все вершины этого объекта проходят через мировую матрицу преобразования.
Матрица вида – определяет местоположение камеры в игровом пространстве. Это вторая по счету матрица, на которую умножаются все вершины моделей. С ее помощью задается направление просмотра 3D объекта или сцены.
Матрица проекции – это наиболее сложная из трех матриц, которая создает проекцию нашего трехмерного игрового мира, на плоскую двухмерную поверхность нашего монитора. С помощью этой матрицы задаются передняя и задняя плоскости отсечения пространства, чтобы снизить нагрузку на видеопроцессор. Чтобы лучше понять выше сказанное, представьте себе, будто вы стоите перед аквариумом прямоугольной формы и смотрите на плавающих внутри него рыб. Стеклянная стенка аквариума, которая ближе к вам – это передняя плоскость отсечения, стенка, которая дальше от вас – это дальняя плоскость. Все что находится к нам ближе, чем ближняя плоскость нас не интересует, равно как и все, что находится за дальней плоскостью отсечения. Наше внимание, как и внимание видеопроцессора, привлекает только содержимое аквариума, а именно все что находится между ближней и дальней плоскостями отсечения. Соответственно увеличивая или уменьшая этот диапазон, мы регулируем нагрузку видеокарты.
А вот и наша моделька.
Как вы снова могли заметить, вся основная работа выполнена внутри классов XNA, мы лишь указали какую модель загрузить, и как ее нарисовать.

Physically Based Rendering