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

Практическое применение 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. О ней я расскажу в следующих главах.

6 комментариев:

  1. Доброго времени суток! Скажите, пожалуйста, у меня C# 2010 EXPRESS GAME STUDIO 4.0, я новичок в программировании, я использую ваш код у себя, но он не работает, выдаёт ошибки на многих строках, как Game.GraphicsDevice.RenderState.StencilEnable = true, подчёркивается RenderState и т.д, как это исправить? может библиотеку какую-то качать надо или ещё что-то?

    ОтветитьУдалить
    Ответы
    1. Это из-за того, что я использовал XNA 3.1, а вы XNA4.0. В четвертом фреймворке были некоторые изменения. Строки с ошибками нужно заменить в соответствии с XNA4.0

      Удалить
  2. Здравствуйте. Первый день сегодня смотрю на XNA. Он у меня тоже 4.0. Никак не могу нагуглить как его правильно применять. Везде пишут что то типа GraphicsDevice dev = new GraphicsDevice(...). Но рефлектором вижу, что GraphicsDevice абстрактный. По логике где то фабрика должна быть, которая родит мне нужного наследника GraphicsDevice. Но где она?

    ОтветитьУдалить
    Ответы
    1. Добрый вечер. Просто создайте темплейт проекта в Visual Studio, и в нем будет сгенерен GraphicsDevice:) Без всяких фабрик, просто через оператор new.

      Удалить
    2. Да не. Не может быть так. Если класс абстрактный, то его объект с помощью new не создать. Еще порылся на просторах интернета. На сайте M$ нашел Microsoft XNA Game Studio 4.0 Refresh, который добавил библиотек XNA, в которых таки обнаружился GraphicsDeviceManager, который как раз является фабрикой по созданию GraphicsDevice`ов. Буду пробовать дальше. И у Вас тут почитаю тоже. Спасибо. =)

      Удалить
    3. Снимаю все свои вопросы. Лажу написал. После установки Microsoft XNA Game Studio 4.0 Refresh GraphicsDevice перестал быть абстрактным. Стыднооо. =)

      Удалить

Physically Based Rendering