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

3D программирование

Так как же получить трехмерные данные, импортированные из файла модели, такого как rocket.x на экране? Экран может отображать только двухмерные изображения; это значит, что вам надо преобразовать трехмерные данные для двухмерного экрана. Процесс называется проекцией, и есть много способов выполнить ее. В ранние дни трехмерных игр использовались такие техники, как приведение лучей (ray-casting), полностью выполняемые центральным процессором. Для каждого столбца пикселов на экране по простым формулам вычислялись верхняя и нижняя границы, и в результате появились первые трехмерные игры, такие как Ultima Underground, Wolfenstein 3D и позже Doom, которые стали очень популярными.

Несколько позже были разработаны более реалистичные игры, такие как Descent, Terminal Velocity и Quake, применявшие более лучшие способы преобразования трехмерных данных в двухмерные изображения. В середине 90-х годов возросла популярность видеокарт с аппаратной поддержкой трехмерной графики и все больше игровых PC внезапно получали возможность весьма эффективно помочь в визуализации полигонов.

К счастью, вам больше вообще не надо беспокоиться о ранних проблемах трехмерных игр. Сегодня трехмерная графика полностью обрабатывается GPU (графическим процессором, расположенным на видеокарте). GPU не только визуализирует полигоны на экране и заполняет их пикселями, но и выполняет все преобразования для проекции трехмерных данных на двухмерную плоскость (рис. 5.7). Вершинные шейдеры используются для преобразования всех точек трехмерного пространства в экранные координаты, а затем пиксельные шейдеры применяются для заполнения всех видимых на экране полигонов пикселами. Пиксельные шейдеры обычно являются более интересной и важной частью шейдерного конвейера, поскольку они больше влияют на вид результата (меняют цвета, смешивают текстуры, учитывают влияние освещения и теней на результат и т.д.). Старое оборудование, которое не поддерживает вершинные и пиксельные шейдеры, не может использоваться с XNA, поэтому вы не должны беспокоиться о возврате к функциям фиксированного конвейера или даже к программной визуализации. Для XNA вам требуется как минимум шейдерная модель версии 1.1, а это значит, что у вас должна быть установлена видеокарта не хуже GeForce 3 или ATI 8000.


Рис. 5.7

Рис. 5.7


Код и изображения, которые я исследую на нескольких последующих страницах, описывают только основы программирования трехмерной графики и работы с матрицами в XNA. Если вы хотите знать больше, я настоятельно рекомендую вам почитать документацию XNA, или, что еще лучше, оригинальную документацию DirectX, где есть очень хорошие разделы об основах программирования трехмерной графики, матрицах, и том, как обрабатывать все это в каркасе приложения (который очень похож на XNA).

У меня нет времени подробно рассказывать обо всех этих вычислениях, и, благодаря множеству вспомогательных классов и методов в XNA Framework, вы можете прожить без обширных знаний о преобразовании матриц. Проекция трехмерных данных обычно состоит из трех этапов:

Обязательно следует упомянуть одно важное изменение в XNA: в DirectX все методы Matrix, уроки и примеры используют левосторонние матрицы, а в XNA матрицы всегда правосторонние; здесь нет медодов для создания каких-либо левосторонних матриц. Если вы создаете новую игру или вообще новичок в программировании трехмерной графики, это, скорее всего, не будет иметь для вас значения, но если у вас есть старый код или старые модели можно столкнуться с проблемами, вызванными способом использования трехмерных данных. Использование левосторонних данных в правостороннем окружении приведет к тому, что все будет выглядеть вывернутым наизнанку, если вы не измените ваши матрицы. Отбрасывание обратных граней также выполняется в XNA по-другому; оно работает против часовой стрелки, что является математически правильным. DirectX использует по умолчанию отбрасывание по часовой стрелке. Если ваши модели вывернуты наизнанку и вы переключаете отбрасывание по часовой стрелке на отбрасывание против часовой стрелки, модели снова будут выглядеть нормально, только будут зеркально отражены (трехмерные данные остаются левосторонними).

Это особенно верно, если вы импортируете файлы .x, которые по умолчанию используют левостороннюю систему (рис. 5.8). Вы можете либо самостоятельно конвертировать все матрицы и точки, либо продолжать жить с тем фактом, что все будет зеркально отражено. В этой книге вы используете много файлов .x, и не имеет значения, левосторонние ли они (подобно модели ракеты из Rocket Commander) или правосторонние (подобно всему новому трехмерному содержимому, создаваемому здесь), поскольку вас не будет волновать зеркальное отражение левосторонних данных. Если вы хотите гарантировать, что все данные в вашей игре будут корректно взаимодействовать, пожалуйста убедитесь, что у вас есть все трехмерные данные в экспортируемом формате, тогда вы сможете позже заново экспортировать любые оказавшиеся неправильными данные, вместо того, чтобы корректировать их в своем коде самостоятельно.


Рис. 5.8

Рис. 5.8


Из школьной программы вы, возможно, помните, что левосторонняя система координат названа так по правилу левой руки, позволяющему определить направление осей X (большой палец), Y (указательный палец) и Z (средний палец). Просто возьмите левую руку, расположите эти три пальца под углами 90 градусов друг к другу и получите левостороннюю систему координат. В школе и на математике и на физике использовалась только правосторонняя система координат и если бы вы писали правой рукой и использовали бы левую для показа направления осей, то получили бы неверный результат, если только не базировали все решение задачи на этом предположении.

В любом случае правосторонняя система координат предпочтительнее, поскольку почти все программы трехмерного моделирования работают с правосторонними данными, и большинство программ, за исключением игр, также работают с правосторонними данными. Правосторонняя система координат может быть показана с использованием правой руки для осей X (большой палец), Y (указательный палец) и Z (средний палец), как показано на рис. 5.9.


Рис. 5.9

Рис. 5.9


Выглядит почти также, но ось Z направлена к вам, что кажется неестественным, если до этого вы работали с левосторонними системами. Другой причиной вращения этой системы может быть попытка добиться того же поведения, что и в прграммах трехмерного моделирования, где ось Z направлена вверх. Я также помещаю оси X и Y в основание, чтобы позиционировать трехмерные данные при построении трехмерных ландшафтов (рис. 5.10).


Рис. 5.10

Рис. 5.10


Достаточно говорить о преобразовании 3D в 2D и координатных системах. Теперь вы должны достаточно знать об основах трехмерных вычислений для визуализации трехмерных моделей. Если вы неуверенны и хотите знать больше о трехмерной математике и программировании, приобретите хорошую книгу по этим темам или почитайте дополнительные материалы в Интернете. Вам доступно множество великолепных сайтов и учебников.

Тестовые модули для моделей

Сейчас обсуждавшийся ранее файл модели Rocket.x должен быть импортирован в ваш проект (используйте каталог содержимого, чтобы разделить код и данные содержимого). Визуализация данных модели не слишком сложна, но в классе Model XNA нет метода Render, а вы хотите иметь такой метод для вашего тестового модуля. Чтобы достичь этого и упростить последующее расширение возможностей визуализации модели, ее оптимизацию и добавление новых крутых возможностей, вы пишете собственный класс Model (подобно тому, как чуть раньше вы написали класс Texture). Внутри он также использует класс модели из XNA, но предоставляет вам метод Render, обрабатывает все вопросы масштабирования и автоматически за вас устанавливает все требуемые матрицы.

Хорошей практикой будет написание новых тестовых модулей для разрабатываемого в текущий момент класса, даже если имеющиеся тестовые модули уже охватывают базовую функциональность. В таких тестовых модулях класса вы можете сделать более специализированные условия и проверить больше деталей. Также полезно проверить корректны ли новые данные, просто заменив загрузку содержимого в тестовом модуле. Вот очень простой, но очень полезный тестовый модуль для класса Model:

public static void TestRenderModel()
{
  Model testModel = null;
  TestGame.Start("TestRenderModel",
    delegate
    {
      testModel = new Model("Rocket");
    },
    delegate
    {
      testModel.Render(Matrix.CreateScale(10));
    });
} // TestRenderModel()

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

Единственный интересный метод в классе Model (рис. 5.11) — это метод Render.


Рис. 5.11

Рис. 5.11


Конструктор просто загружает модель из содержимого и устанавливает внутренние переменные, чтобы обеспечить правильное масштабирование объекта и матрицы, упрощающие помещение в ваш мир:

/// <summary>
/// Создание модели
/// </summary>
/// <param name="setModelName">Имя файла модели</param>
public Model(string setModelName)
{
  name = setModelName;

  xnaModel = BaseGame.Content.Load<XnaModel>(
    @"Content\" + name);

  // Получаем матрицы для каждой части сетки
  transforms = new Matrix[xnaModel.Bones.Count];
  xnaModel.CopyAbsoluteBoneTransformsTo(transforms);

  // Вычисляем масштабирование объекта, используемое для визуализации
  scaling = xnaModel.Meshes[0].BoundingSphere.Radius *
            transforms[0].Right.Length();
  if (scaling == 0)
    scaling = 0.0001f;

  // Применяем масштабирование к objectMatrix
  // для масштабирования объекта к размеру 1.0
  objectMatrix *= Matrix.CreateScale(1.0f / scaling);
} // Model(setModelName)

После загрузки экземпляра xnaModel из каталога содержимого заранее вычисляется преобразование, поскольку данные анимации здесь не поддерживаются (XNA вообще не поддерживает ее). Список матриц преобразования содержит матрицу визуализации для каждой части визуализируемой сетки модели (сетка ракеты состоит из единственной части, использующей только один эффект). Эти преобразования устанавливаются художником в программе трехмерного моделирования и ориентируют вашу модель так, как хотел ее создатель. Затем вы определяете масштаб и убеждаетесь, что он не равен нулю, поскольку в делении на ноль нет ничего забавного. В конце вы вычисляете матрицу объекта, которая будет использоваться совместно с матрицами преобразований и матрицей визуализации при отображении.

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

/// <summary>
/// Визуализация
/// </summary>
/// <param name="renderMatrix">Матрица визуализации</param>
public void Render(Matrix renderMatrix)
{
  // Применяем objectMatrix
  renderMatrix = objectMatrix * renderMatrix;

  // Перебираем все сетки модели
  foreach (ModelMesh mesh in xnaModel.Meshes)
  {
    // Назначаем мировую матрицу каждому используемому эффекту 
     BaseGame.WorldMatrix = transforms[mesh.ParentBone.Index] *
                            renderMatrix;

    // И для каждого эффекта, используемого этой сеткой
    // (обычно только 1, многочисленные материалы хороши в 3ds max,
    // но неэффективны для визуализации)
    foreach (Effect effect in mesh.Effects)
    {
      // Устанавливаем технику
      // (не выполняется автоматически в XNA framework).
      effect.CurrentTechnique = effect.Techniques["Specular20"];

      // Устанавливаем матрицы, в шейдере ParallaxMapping.fx
      // мы используем world, viewProj и viewInverse
      effect.Parameters["world"].SetValue(
                              BaseGame.WorldMatrix);

      // Примечание: для увеличения производительности эти значения
      // должны устанавливаться только один раз в каждом кадре!
      // Кроме того, вы должны обращаться к ним через EffectParameter,
      // а не по имени (что медленнее)! За дополнительной информацией
      // обращайтесь к главам 6 и 7.
      effect.Parameters["viewProj"].SetValue(
                              BaseGame.ViewProjectionMatrix);
      effect.Parameters["viewInverse"].SetValue(
                              BaseGame.InverseViewMatrix);

      // Также задаем направление света
      // (здесь не используется, пригодится позже)
      effect.Parameters["lightDir"].SetValue(
                              BaseGame.LightDirection);
    } // foreach (effect)

    // Визуализируем все с помощью метода Draw класса XNA ModelMesh,
    // который перебирает все части сетки и визуализирует их
    // (с буферами вершин и индексов).
    mesh.Draw();
  } // foreach (mesh)
} // Render(renderMatrix)

Как видите, в коде достаточно много комментариев, и вы столкнетесь с этим во всех написанных мной методах, которые достаточно длинны. Я считаю, что это очень важно для быстрого понимания кода, когда позже вы захотите что-нибудь изменить, выполнить рефакторинг для соответствия новым требованиям или просто оптимизировать ваш код. Для большинства программистов это трудный шаг; мы все ленивы и требуется много времени, чтобы понять как полезны такие комментарии, не только если другие люди будут читать код, но и когда какое-то время спустя приходится возвращаться к частям своего кода. Пожалуйста, пытайтесь писать комментарии обычным языком, чтобы упростить процесс чтения и не используйте в комментариях код.

Матрица визуализации, переданная из тестового модуля (это просто матрица масштабирования с коэффициентом 10), умножается на матрицу объекта, которая снова масштабирует матрицу визуализации с коэффициентом, необходимым для того, чтобы размер исходного объекта стал равен 1, поэтому итоговый размер ракеты в вашем трехмерном мире будет равен 10 единицам. Затем вы перебираете каждую сетку модели (у вашей ракеты всего одна сетка) и вычисляете мировую матрицу для этой сетки, используя список предварительно вычисленных матриц преобразования и матрицу визуализации. Используя эту мировую матрицу в вершинном шейдере вы теперь можете гарантировать, что каждая трехмерная точка вашей ракеты будет преобразована желаемым образом. Очевидно, что перед вызовом этого метода должны быть инициализированы матрицы вида и проекции, поскольку вершинному шейдеру нужны все эти значения, чтобы суметь преобразовать трехмерные данные для пиксельного шейдера, который будет визуализировать их на экране.

Модели XNA хранят все эффекты, используемые для каждой сетки модели, и у вас может быть несколько эффектов для единственной сетки, но у большинства моделей, с которыми вы столкнетесь, будет применяться только один эффект. Позже вы увидите более эффективный способ визуализации набора трехмерных данных, заключающийся в накапливании частей сцены, использующих один и тот же шейдер, и последующем скидывании их на экран за один раз. Это позволяет визуализировать в каждом кадре тысячи объектов, сохраняя высокую частоту кадров. Чтобы больше узнать о шейдерах и эффективной организации работы с ними прочитайте главы 6 и 7.

Но перед тем, как визуализировать вашу сетку, вы должны сперва установить используемую технику, поскольку XNA не делает это автоматически, хотя техника и задается художником, создавшим модель. В главе 7 вы увидите, как обойти эту проблему. Затем вы устанавливаете мировую матрицу, матрицу вида и матрицу проекции, в том виде, в каком шейдер ожидает наличия этих значений. Например, матрица вида никогда не требуется непосредственно, вы используете инвертированную матрицу вида для вычисления местоположения камеры, используемого в расчете отражаемого освещения. Все эти параметры шейдеров устанавливаются через свойство Parameters класса эффекта путем указания имени требуемого параметра. Это не слишком эффективный способ установки параметров эффектов, поскольку использование строк для доступа к данным наносит ощутимый удар по производительности. Позже вы узнаете способы сделать это лучшим образом, но сейчас это работает и вы должны увидеть вашу небольшую ракету из тестового модуля после вызова mesh.Draw (рис. 5.12).


Рис. 5.12

Рис. 5.12


Тестирование других моделей

Если на вашем диске есть другие файлы .x или вы хотите протестировать другие файлы .x из игры Rocket Commander или одной из ее многочисленных модификаций, можно быстро сделать это перетащив файл .x в каталог содержимого и изменив тестовый модуль для загрузки новой модели.

Давайте проделаем это с моделью Apple.x из модификации Fruit Commander:

public static void TestRenderModel()
{
  Model testModel = null;
  TestGame.Start("TestRenderModel",
    delegate
    {
      testModel = new Model("Apple");
    },
    delegate
    {
      testModel.Render(Matrix.CreateScale(10));
    });
} // TestRenderModel()

Результат показан на рис. 5.13. Обратите внимание, что отражаемый свет рассчитывается в шейдере на основе эффекта наложения нормалей, который работает здесь некорректно из-за неправильных данных тангенсов у модели яблока (они даже не импортируются, поскольку модели XNA не поддерживают данные тангенсов, а свой собственный способ вы реализуете в главе 7).


Рис. 5.13

Рис. 5.13



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

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