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

Практическое применение 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);
    }
  }
}
А результат выглядит вот так:
Для начала не плохо, как мне кажется.


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

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

Physically Based Rendering