netlib.narod.ru< Назад | Оглавление | Далее >

Класс ShaderEffect

Чтобы визуализировать шейдер вы будете использовать класс ShaderEffect, введенный две главы назад. Все основы были описаны в предыдущей главе, и единственная вещь, которую осталось добавить здесь, — большее количество параметров шейдера, а остальная функциональность остается той же самой (рис. 7.13). Вы также используете метод Render для визуализации трехмерных данных с шейдером, подобно тому, как делали это в классе диспетчера линий из главы 5.


Рис. 7.13

Рис. 7.13


Для визуализации трехмерных данных вам нужна не только трехмерная геометрия или код трехмерной визуализации, но также и параметры материала, включающие значения цвета материала и текстуры, подобно FX Composer. Для управления этими данными вводится новый небольшой вспомогательный класс, который хранит все данные материала в одном месте: Material.cs (рис. 7.14).


Рис. 7.14

Рис. 7.14


Метод SetParameters класса ShaderEffect получает в качестве параметра экземпляр класса Material, который также может передаваться в метод Render. Это упрощает назначение материалов, исключая необходимость устанавливать все параметры эффекта самостоятельно.

С этими новыми классами вы теперь легко можете написать новый тестовый модуль для изменения визуализации модели яблока из предыдущей главы с целью поддержки вашего нового шейдера NormalMapping.fx. Кроме того, вы измените материал на материал астероида, загрузив текстуру рассеивания и карту нормалей астероида с целью проверки нового класса Material.

Формат TangentVertex

Перед тем, как вы сможете написать тестовый модуль для проверки нашего шейдера наложения нормалей, вам необходимо в вашем коде определить такую же структуру, что и VertexInput из файла .fx. В отличие от структуры VertexPositionNormalTexture из главы 6, сейчас у нас нет предопределенной структуры, которая содержала бы данные касательных. Вы должны определить ее самостоятельно. Показанный ниже класс TangentVertex (рис. 7.15) используется для определения структуры VertexInput, применяемой в NormalMapping.fx и всех шейдерах из этой книги, которые вы напишете, начиная с данного момента.


Рис. 7.15

Рис. 7.15


В определении полей структуры нет ничего особенного; вы просто описываете четыре необходимых типа — местоположение, нормаль, касательную и координаты текстуры UV.

/// <summary>
/// Местоположение
/// </summary>
public Vector3 pos;

/// <summary>
/// Координаты текстуры
/// </summary>
public Vector2 uv;

/// <summary>
/// Нормаль
/// </summary>
public Vector3 normal;

/// <summary>
/// Касательная
/// </summary>
public Vector3 tangent;

Некоторые методы вершинного буфера требуют указания размера этой структуры. В MDX вы можете использовать методы из Direct3DX или воспользоваться небезопасным кодом для обращения к методу sizeof. В XNA все не так просто; мы определяем размер самостоятельно:

/// <summary>
/// Размер шага, в XNA вызываем SizeInBytes.
/// </summary>
public static int SizeInBytes
{
  get
  {
    // 4 байта на float:
    // 3 float для pos, 2 float для uv,
    // 3 float для normal и 3 float для tangent.
    return 4 * (3 + 2 + 3 + 3);
  } // get
} // SizeInBytes

Оставшаяся часть структуры весьма прямолинейна; единственное поле, которое вам понадобится извне, — это VertexDeclaration, генерируемое вашим собственным кодом:

#region Генерирование объявления вершины
/// <summary>
/// Элементы вершины для Mesh.Clone
/// </summary>
public static readonly VertexElement[] VertexElements =
                                        GenerateVertexElements();

/// <summary>
/// Объявление вершины для буфера вершин
/// </summary>
public static VertexDeclaration VertexDeclaration =
          new VertexDeclaration(BaseGame.Device, VertexElements);

/// <summary>
/// Генерация объявления вершины
/// </summary>
private static VertexElement[] GenerateVertexElements()
{
  VertexElement[] decl = new VertexElement[]
    {
      // Конструируем новое объявление вершины с данными касательной.
      // Сперва идет обычный хлам (мы уже имели дело с ним)
      new VertexElement(0, 0, VertexElementFormat.Vector3,
                        VertexElementMethod.Default,
                        VertexElementUsage.Position, 0),
      new VertexElement(0, 12, VertexElementFormat.Vector2,
                        VertexElementMethod.Default,
                        VertexElementUsage.TextureCoordinate, 0),
      new VertexElement(0, 20, VertexElementFormat.Vector3,
                        VertexElementMethod.Default,
                        VertexElementUsage.Normal, 0),
      // А теперь касательная
      new VertexElement(0, 32, VertexElementFormat.Vector3,
                        VertexElementMethod.Default,
                        VertexElementUsage.Tangent, 0),
    };
  return decl;
} // GenerateVertexElements()
#endregion

Чтобы определить, в каком порядке объявляются трехмерные данные, VertexElement получает следующие параметры:

Тестовый модуль наложения нормалей

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

Взгляните на тестовый модуль шейдера наложения нормалей:

public static void TestNormalMappingShader()
{
  Model testModel = null;
  Material testMaterial = null;

  TestGame.Start("TestNormalMappingShader",
    delegate
    {
      testModel = new Model("apple");
      testMaterial = new Material(
        Material.DefaultAmbientColor,
        Material.DefaultDiffuseColor,
        Material.DefaultSpecularColor,
        "asteroid4~0", "asteroid4Normal~0", "", "");
    },
    delegate
    {
      // Визуализируем модель
      BaseGame.WorldMatrix = Matrix.CreateScale(0.25f, 0.25f, 0.25f);
      BaseGame.Device.VertexDeclaration =
                         TangentVertex.VertexDeclaration;

      ShaderEffect.normalMapping.Render(testMaterial, "Specular20",
        delegate
        {
          // Визуализируем все сетки
          foreach (ModelMesh mesh in testModel.XnaModel.Meshes)
          {
            // Визуализируем все части сетки
            foreach (ModelMeshPart part in mesh.MeshParts)
            {
              // Визуализируем данные собственным способом
              BaseGame.Device.Vertices[0].SetSource(
                    mesh.VertexBuffer, part.StreamOffset, part.VertexStride);
              BaseGame.Device.Indices = mesh.IndexBuffer;
              
              // Визуализация
              BaseGame.Device.DrawIndexedPrimitives(
                PrimitiveType.TriangleList,
                part.BaseVertex, 0, part.NumVertices,
                part.StartIndex, part.PrimitiveCount);
            } // foreach
          } // foreach
        });
    });
} // TestNormalMappingShader()

Модель загружается так же, как в тестовом модуле из предыдущей главы, но вместо загрузки текстуры вы сейчас загружаете целый материал с несколькими цветовыми значениями, текстурой рассеивания и картой нормалей из вашей модели asteroid4. В цикле визуализации вы, как и прежде, обеспечиваете установку мировой матрицы, а затем устанавливаете вашу новую структуру TangentVertex. Код визуализации тот же самый, что и в методе RenderModel класса SimpleShader, и просто перебирает все сетки и визуализирует все с новым материалом астероида. Результат работы этого тестового модуля показан на рис. 7.16.


Рис. 7.16

Рис. 7.16


Яблоко осталось, но оно очень темное, карта нормалей не выглядит хорошо и уж, конечно, не так, как это смотрелось в FX Composer. Ладно, это снова вопрос касательных. Вы сообщили XNA как использовать входные данные, но данные касательных по-прежнему отсутствуют и должны быть сгенерированы перед тем, как модель будет помещена в ваш конвейер содержимого.

Добавляем данные касательных в собственном обработчике

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

Сначала вам надо создать новый проект DLL; просто добавьте его к существующему решению и выберите тип XNA DLL или C# DLL. Теперь убедитесь, что DLL XNA Framework и конвейера XNA добавлены в раздел ссылок этого нового проекта (рис. 7.17).


Рис. 7.17

Рис. 7.17


Теперь поместите приведенный ниже код в главный класс DLL. Вы выполняете наследование от класса ModelProcessor и расширяете поведение по умолчанию моделей XNA. Код просто вызывает CalculateTangentFrames для каждой сетки в модели. Код в проекте для этой главы более сложен и исправляет некоторые другие вещи, а также подготавливает модель для сложной высокооптимизированной визуализации.

/// <summary>
/// Обработчик моделей XnaGraphicEngine для x-файлов. Загружает модели
/// тем же способом, что и класс ModelProcessor, но генерирует
/// касательные и некоторые дополнительные данные.
/// </summary>
[ContentProcessor(DisplayName = "XnaGraphicEngine Model (Tangent support)")]
public class XnaGraphicEngineModelProcessor : ModelProcessor
{
  #region Process
  /// <summary>
  /// Обработка модели
  /// </summary>
  /// <param name="input">Входные данные</param>
  /// <param name="context">Контекст для журналирования</param>
  /// <returns>Содержимое модели</returns>
  public override ModelContent Process(
    NodeContent input, ContentProcessorContext context)
  {
    // Сперва генерируем данные касательных, поскольку x-файлы не хранят их
    GenerateTangents(input, context);

    // И пусть оставшуюся работу выполнит обработчик моделей по умолчанию
    return base.Process(input, context);
  } // Process
  #endregion

  #region Генерирование касательных
  /// <summary>
  /// Вспомогательный метод генерирования касательных,
  /// в x-файлах нет экспортированных касательных, мы должны
  /// генерировать их самостоятельно
  /// </summary>
  /// <param name="input">Входные данные</param>
  /// <param name="context">Контекст для журналирования</param>
  private void GenerateTangents(
             NodeContent input, ContentProcessorContext context)
  {
    MeshContent mesh = input as MeshContent;
    if (mesh != null)
    {
      // Генерируем касательные для сетки. Нам не нужны бинормали,
      // поэтому в последнем параметре передается null
      MeshHelper.CalculateTangentFrames(mesh,
                 VertexChannelNames.TextureCoordinate(0),
                 VertexChannelNames.Tangent(0), null);
    } // if
    // Перебираем всех потомков
    foreach (NodeContent child in input.Children)
    {
      GenerateTangents(child, context);
    } // foreach
  } // GenerateTangents(input, context)
  #endregion
} // class

Класс MeshHelper доступен здесь в конвейере содержимого; в нем есть несколько методов, которые помогут вам, если возникнет потребность модифицировать исходные данные. Метод GenerateTangents генерирует фреймы касательных из первого набора координат текстуры и игнорирует создание значений бинормалей. Хотя хорошо наконец-то получить сгенерированные касательные, они часто могут выглядеть неправильно, особенно если модель была создана недавно. Новые версии 3D Studio Max (8 и 9) используют для создания карт нормалей так называемый Rocket Mode (названный в честь модели Rocket в Rocket Commander), но более ранние версии и другие утилиты могут генерировать сферические карты нормалей, которые будут испорчены автоматическим созданием касательных в XNA (как, впрочем, и в MDX). В оригинальной игре я реализовал собственный обработчик и генератор касательных, но в XNA это не так просто. У вас нет прямого доступа к данным вершин моделей, но есть некоторые вспомогательные методы. Требуется много работы, чтобы извлечь все данные, а потом собрать их воедино и поместить назад.

Вероятно, было бы проще написать полностью новый импортер содержимого, чем пачкаться с существующим форматом, если для данных ваших трехмерных моделей действительно нужно много изменений и дополнительные возможности. Например, многие трехмерные модели существуют в многих различных форматах; md3 или md5 исключительно популярны в мире Quake/Doom и люди часто пишут собственные импортеры только для того, чтобы получить возможность отображать некоторые крутые модели из других игр в своем графическом движке. За информацией о том, как написать собственный импортер содержимого обратитесь к документации XNA. Кроме того, надеюсь, в будущем в Сети появится много написанных пользователями нестандартных импортеров содержимого. По моему мнению, если вы не хотите возиться с файлами .x или .fbx, простейший формат для импорта — Collada. Он требует некоторого объема индивидуального кода, но когда вы все сделаете, будет очень легко расширять ваш импортер, добавлять новые возможности и полностью менять поведение в соответствии с вашими пожеланиями.

Сейчас вам для счастья достаточно собственного обработчика моделей. Он генерирует данные касательных, а это все, что нужно для вашего шейдера наложения нормалей. Чтобы ваш обработчик работал в вашем проекте XNA необходимо самостоятельно выбрать его, открыв свойства каждого файла содержимого. Перед тем, как вы будете выбирать свой собственный обработчик моделей, необходимо убедиться, что конвейер содержимого знает о его существовании; пока вы только создали файл DLL где-то на диске. Чтобы включить ваш обработчик откройте свойства проекта (щелкните в Solution Explorer правой кнопкой мыши по файлу XnaGraphicsEngine.csproj, находящемуся вверху под решением XnaGraphicsEngine, и в предложенном меню выберите Properties). Последняя страница параметров в свойствах проекта называется Content Pipeline и именно здесь вы можете выбрать дополнительные импортеры содержимого и обработчики (рис. 7.18). Если вы щелкнете Add, то сможете выбрать файл XnaGraphicsEngineContentProcessor.dll, перейдя на два уровня вверх и выбрав его в каталоге bin\Debug. Для окончательного варианта вы, возможно, решите выбрать bin\Release.


Рис. 7.18

Рис. 7.18


После того, как новая сборка конвейера содержимого загружена, вы сможете выбрать XnaGraphicsEngine Model (Tangent support) для каждого файла .x модели в вашем проекте. Вы можете выбрать несколько файлов .x в Solution Explorer и изменить обработчик содержимого моделей для всех них (рис. 7.19).


Рис. 7.19

Рис. 7.19


Когда все это будет вздыматься за вами, можно снова запустить ваш последний тестовый модуль и посмотреть, что изменилось. Обратите внимание, что для поддержки нового обработчика содержимого вам не пришлось менять ни одной строки кода в вашем проекте; все делается в DLL обработчика. Если вы допустили какую-нибудь ошибку в обработчике содержимого или один из файлов содержимого содержит недопустимые данные, вы получите предупреждение или сообщение об ошибке прежде чем проект будет запущен, поскольку конвейер содержимого выполняет построение всего содержимого перед запуском проекта. Как только содержимое построено с помощью вашего нового обработчика, оно больше не меняется и его не надо заново строить. XNA автоматически пропускает этот этап.

Как видно на рис. 7.20, ваше яблоко с материалом астероида выглядит гораздо лучше, и вы можете даже назначить ему другие материалы (просто для удовольствия). Заметьте, что внутренняя структура модели яблока отличается от вашего собственного формата TangentVertex. Если вы устанавливаете материал яблока непосредственно, убедитесь, что используется правильное объявление вершин из сетки, а не ваше собственное, иначе координаты текстур не будут соответствовать (но это не имеет значения для вашего тестирования; в нем и так ни одна из текстур не соответствует).


Рис. 7.20

Рис. 7.20


Обратите внимание, что тестовые текстуры ракеты и мрамора выглядят зеркальными. Это нормально, поскольку модель яблока создавалась для DirectX и использует левосторонние матрицы. Вы можете заново экспортировать все модели в правостороннем режиме (который поддерживает Panda DirectX Exporter в 3D Studio Max) или просто игнорировать эту проблему, поскольку она не так важна в Rocket Commander. Если в вашей игре есть текстуры с текстом или вы хотите, чтобы текстуры точно соответствовали и не были зеркальными, с самого начала убедитесь, что все правильно выравнивается. Всегда важно использовать один и тот же формат в программе моделирования, которую использует ваш художник, и в вашем графическом движке. Если они не совпадают, вам надо будет либо подстраивать ваш движок, либо преобразовать содержимое необходимым образом.

Итоговый тестовый модуль астероида

Вам не нужны все эти знания, чтобы только отобразить простую модель астероида, поскольку, как показывает следующий тестовый модуль, можно просто загрузить модель и автоматически будет использован шейдер, который был выбран в 3D Studio. Проблема с этим подходом возникнет, когда вы попытаетесь визуализировать в одном кадре 1000 астероидов. Очень медленно и непрактично запускать новый шейдер для каждого отдельного астероида, снова и снова устанавливать все параметры и затем использовать неоптимизированный метод рисования сетки.

Вместо этого в дальнейшем вы будете создавать ваш собственный класс шейдера, только один раз инициализировать материалы и параметры шейдера и максимально быстро визуализировать сразу много моделей. Этот способ позволит вам достичь великолепной производительности в игре Rocket Commander.

public static void TestAsteroidModel()
{
  Model testModel = null;
  
  TestGame.Start("TestNormalMappingShader",
    delegate
    {
      testModel = new Model("asteroid4");
    },
    delegate
    {
      // Визуализация модели
      testModel.Render(Matrix.CreateScale(10.25f, 10.25f, 10.25f));
    });
} // TestAsteroidModel()

Если модель asteroid4 (кстати, здесь для тестирования вы можете также использовать asteroid4Low) использует правильный обработчик моделей с поддержкой касательных, вы должны увидеть правильный результат в 3D (рис. 7.21) с эффектом наложения параллакса (это то же, что и наложение нормалей, но здесь используется дополнительная карта высот для перемещения выходной текстуры с целью имитации эффекта смещения). Проблема с моделью астероида — ясно видимые необработанные грани на ней. Может показаться, что это из-за неточного соответствия текстуры, но если вы отключите эффект наложения нормалей, все выглядит хорошо. Проблема возникает из-за данных касательных. Карта нормалей верна, что вы можете увидеть в версии Rocket Commander для DirectX (все точно соответствует, благодаря собственному способу регенерации данных касательных, что, к сожалению, невозможно в XNA для файлов .x). Другие модели, такие как ракета и будущие модели, создаваемые для XNA, выглядят замечательно, но все равно остается неисправимая проблема со старыми моделями и ограничениями XNA. Вам придется жить с этим.


Рис. 7.21

Рис. 7.21


Наверное, было бы можно как-нибудь исправить все астероиды, повторно экспортировав их, убедившись, что все данные правильны, и даже, возможно, заново сгенерировав карту нормалей для точного соответствия. Но это потребует много работы, а астероиды уже выглядят достаточно хорошо. Оригинальная игра Rocket Commander по-прежнему существует и показывает, как все должно быть сделано; в версии XNA вы можете жить с некоторыми незначительными проблемами.

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


netlib.narod.ru< Назад | Оглавление | Далее >

Сайт управляется системой uCoz